krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.8.1: Hutch/Extensions/SRHTWebURL.swift · raw

  1import Foundation
  2
  3enum SRHTWebURL {
  4    static let chat = URL(string: "https://chat.sr.ht")!
  5    static let status = URL(string: "https://status.sr.ht")!
  6    /// RSS/Atom feed for status incidents (HTML snapshot uses `status`).
  7    static let statusIncidentFeed = URL(string: "https://status.sr.ht/index.xml")!
  8
  9    static func repository(_ repository: RepositorySummary) -> URL? {
 10        userScopedURL(
 11            host: "\(repository.service.rawValue).sr.ht",
 12            ownerCanonicalName: repository.owner.canonicalName,
 13            pathComponents: [repository.name]
 14        )
 15    }
 16
 17    static func httpsCloneURL(_ repositorySummary: RepositorySummary) -> String? {
 18        repository(repositorySummary)?.absoluteString
 19    }
 20
 21    static func sshCloneURL(_ repositorySummary: RepositorySummary) -> String {
 22        let host = "\(repositorySummary.service.rawValue).sr.ht"
 23        let user = repositorySummary.service == .hg ? "hg" : "git"
 24        return "\(user)@\(host):\(repositorySummary.owner.canonicalName)/\(repositorySummary.name)"
 25    }
 26
 27    static func commit(repository: RepositorySummary, commitId: String) -> URL? {
 28        userScopedURL(
 29            host: "\(repository.service.rawValue).sr.ht",
 30            ownerCanonicalName: repository.owner.canonicalName,
 31            pathComponents: [repository.name, commitPathComponent(for: repository.service), commitId]
 32        )
 33    }
 34
 35    static func file(repository: RepositorySummary, revspec: String, path: String) -> URL? {
 36        switch repository.service {
 37        case .git:
 38            return userScopedURL(
 39                host: "git.sr.ht",
 40                ownerCanonicalName: repository.owner.canonicalName,
 41                pathComponents: [repository.name, "tree", revspec, "item"] + pathComponents(from: path)
 42            )
 43        case .hg:
 44            var components = URLComponents()
 45            components.scheme = "https"
 46            components.host = "hg.sr.ht"
 47
 48            let ownerUsername = username(from: repository.owner.canonicalName)
 49            let encodedRepository = repository.name.addingPercentEncoding(withAllowedCharacters: pathComponentCharacterSet) ?? repository.name
 50            let encodedPath = path.split(separator: "/").map {
 51                String($0).addingPercentEncoding(withAllowedCharacters: pathComponentCharacterSet) ?? String($0)
 52            }.joined(separator: "/")
 53
 54            components.percentEncodedPath = "/~\(ownerUsername)/\(encodedRepository)/browse/\(encodedPath)"
 55            components.queryItems = [URLQueryItem(name: "rev", value: revspec)]
 56            return components.url
 57        default:
 58            return nil
 59        }
 60    }
 61
 62    static func build(jobId: Int, ownerCanonicalName: String) -> URL? {
 63        userScopedURL(
 64            host: "builds.sr.ht",
 65            ownerCanonicalName: ownerCanonicalName,
 66            pathComponents: ["job", String(jobId)]
 67        )
 68    }
 69
 70    static func tracker(ownerUsername: String, trackerName: String) -> URL? {
 71        userScopedURL(
 72            host: "todo.sr.ht",
 73            ownerUsername: ownerUsername,
 74            pathComponents: [trackerName]
 75        )
 76    }
 77
 78    static func tracker(_ trackerSummary: TrackerSummary) -> URL? {
 79        tracker(ownerUsername: trackerSummary.owner.canonicalName.srhtUsername, trackerName: trackerSummary.name)
 80    }
 81
 82    static func projectSource(_ source: Project.SourceRepo) -> URL? {
 83        userScopedURL(
 84            host: "\(source.repoType.service.rawValue).sr.ht",
 85            ownerUsername: source.ownerUsername,
 86            pathComponents: [source.name]
 87        )
 88    }
 89
 90    static func mailingList(ownerUsername: String, listName: String) -> URL? {
 91        userScopedURL(
 92            host: "lists.sr.ht",
 93            ownerUsername: ownerUsername,
 94            pathComponents: [listName]
 95        )
 96    }
 97
 98    static func ticket(ownerUsername: String, trackerName: String, ticketId: Int) -> URL? {
 99        userScopedURL(
100            host: "todo.sr.ht",
101            ownerUsername: ownerUsername,
102            pathComponents: [trackerName, String(ticketId)]
103        )
104    }
105
106    static func profile(canonicalName: String) -> URL? {
107        userScopedURL(
108            host: "sr.ht",
109            ownerCanonicalName: canonicalName,
110            pathComponents: []
111        )
112    }
113
114    static func paste(ownerCanonicalName: String, pasteId: String) -> URL? {
115        userScopedURL(
116            host: "paste.sr.ht",
117            ownerCanonicalName: ownerCanonicalName,
118            pathComponents: [pasteId]
119        )
120    }
121
122    private static func userScopedURL(
123        host: String,
124        ownerCanonicalName: String,
125        pathComponents: [String]
126    ) -> URL? {
127        userScopedURL(
128            host: host,
129            ownerUsername: username(from: ownerCanonicalName),
130            pathComponents: pathComponents
131        )
132    }
133
134    private static func userScopedURL(
135        host: String,
136        ownerUsername: String,
137        pathComponents: [String]
138    ) -> URL? {
139        var components = URLComponents()
140        components.scheme = "https"
141        components.host = host
142
143        let encodedComponents = (["~\(ownerUsername)"] + pathComponents).map { pathComponent in
144            pathComponent.addingPercentEncoding(withAllowedCharacters: pathComponentCharacterSet) ?? pathComponent
145        }
146        components.percentEncodedPath = "/" + encodedComponents.joined(separator: "/")
147        return components.url
148    }
149
150    private static func username(from canonicalName: String) -> String {
151        if canonicalName.hasPrefix("~") {
152            return String(canonicalName.dropFirst())
153        }
154        return canonicalName
155    }
156
157    private static func commitPathComponent(for service: SRHTService) -> String {
158        switch service {
159        case .hg:
160            return "rev"
161        default:
162            return "commit"
163        }
164    }
165
166    private static let pathComponentCharacterSet: CharacterSet = {
167        var characterSet = CharacterSet.urlPathAllowed
168        characterSet.remove(charactersIn: "/")
169        return characterSet
170    }()
171
172    private static func pathComponents(from path: String) -> [String] {
173        path.split(separator: "/").map(String.init)
174    }
175}