gitbay/Views/WebURLRoute.swift
107 lines · 4831 bytes
1import Foundation
2
3/// The screen a web page's URL opens, for links to the instance that
4/// arrive as universal links. The shapes follow the forge's route table;
5/// a page the app has no screen for is nil, and goes to Safari.
6nonisolated enum WebURLRoute: Hashable, Sendable {
7 case repo(RepoRoute)
8 case issue(IssueRoute)
9 case mr(MRRoute)
10 case build(BuildRoute)
11 case release(ReleaseRoute)
12 case org(OrgRoute)
13 case snippet(SnippetRoute)
14
15 init?(url: URL) {
16 guard let scheme = url.scheme?.lowercased(), scheme == "https" || scheme == "http",
17 let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil }
18 // Split before decoding, so an escaped slash stays in its segment.
19 let s = components.percentEncodedPath.split(separator: "/").compactMap {
20 String($0).removingPercentEncoding
21 }
22 guard let owner = s.first else { return nil }
23 if s.count == 1 {
24 switch owner {
25 case "bookmarks": self = .repo(.bookmarks)
26 case "notifications": self = .repo(.notifications)
27 default: self = .repo(.profile(owner))
28 }
29 return
30 }
31 if s[1] == "-" {
32 guard let route = Self.ownerPage(owner, Array(s.dropFirst(2))) else { return nil }
33 self = route
34 return
35 }
36 let repo = "\(owner)/\(s[1])"
37 guard s.count > 2 else {
38 self = .repo(.repo(repo))
39 return
40 }
41 guard let route = Self.repoPage(repo, s[2], Array(s.dropFirst(3))) else { return nil }
42 self = route
43 }
44
45 /// `/{owner}/-/...`
46 private static func ownerPage(_ owner: String, _ rest: [String]) -> WebURLRoute? {
47 switch (rest.first, rest.count) {
48 case ("repositories", 1), ("bookmarks", 1), ("people", 1): .repo(.profile(owner))
49 case ("labels", 1): .org(.labels(owner))
50 case ("milestones", 1): .org(.milestones(owner))
51 case ("snippets", 1): .snippet(.list(owner: owner))
52 case ("snippets", 2) where rest[1] != "new": .snippet(.snippet(id: rest[1]))
53 default: nil
54 }
55 }
56
57 /// `/{owner}/{repo}/{section}/...`
58 private static func repoPage(_ repo: String, _ section: String, _ rest: [String]) -> WebURLRoute? {
59 let number = rest.count == 1 ? Int64(rest[0]).flatMap { $0 > 0 ? $0 : nil } : nil
60 let ref = rest.first
61 let path = rest.dropFirst().joined(separator: "/")
62 switch section {
63 case "issues":
64 if rest.isEmpty { return .issue(.list(repo: repo)) }
65 return number.map { .issue(.issue(repo: repo, number: $0)) }
66 case "mrs":
67 if rest.isEmpty { return .mr(.list(repo: repo)) }
68 return number.map { .mr(.mr(repo: repo, number: $0)) }
69 case "builds":
70 if rest.isEmpty { return .build(.list(repo: repo)) }
71 return number.map { .build(.detail(repo: repo, number: $0)) }
72 case "tree":
73 return ref.map { .repo(.tree(repo: repo, directory: path, ref: $0)) }
74 case "blob" where !path.isEmpty:
75 return .repo(.file(repo: repo, path: path, ref: ref))
76 case "blame" where !path.isEmpty:
77 return .repo(.blame(repo: repo, path: path, ref: ref))
78 case "commit" where rest.count == 1:
79 return .repo(.commit(repo: repo, sha: rest[0]))
80 case "log" where rest.count <= 1:
81 return .repo(.log(repo: repo, ref: ref, path: nil))
82 case "wiki" where ref != "_raw":
83 if rest.isEmpty { return .repo(.wiki(repo: repo)) }
84 return .repo(.wikiPage(repo: repo, page: rest.joined(separator: "/")))
85 case _ where !rest.isEmpty:
86 return nil
87 case "refs": return .repo(.refs(repo: repo))
88 case "compare": return .repo(.compare(repo: repo, defaultBranch: nil))
89 case "releases": return .release(.list(repo: repo))
90 case "milestones": return .repo(.milestones(repo: repo))
91 case "labels": return .repo(.labels(repo: repo))
92 default: return nil
93 }
94 }
95
96 /// The signed-in account a link opens under: the current one when it
97 /// is on the link's instance, otherwise the first that is. Both sides
98 /// go through `GitbayInstance`'s normalisation, as `PushTarget` does.
99 static func account(for url: URL, current: Account?, accounts: [Account]) -> Account? {
100 guard let instance = try? GitbayInstance(url: url.absoluteString) else { return nil }
101 let matches = { (account: Account) in
102 (try? GitbayInstance(url: account.instance.baseURL.absoluteString))?.baseURL == instance.baseURL
103 }
104 if let current, matches(current) { return current }
105 return accounts.first(where: matches)
106 }
107}