gitbay/Repos/RepoModels.swift

v1.12.0
gitbay-ios/gitbay/Repos/RepoModels.swift history · blame · raw

212 lines · 7232 bytes

  1import Foundation
  2
  3/// One row of `repo list`.
  4nonisolated struct RepoSummary: Decodable, Sendable, Hashable, Identifiable {
  5    let path: String
  6    let visibility: String
  7    let description: String?
  8    let archived: Bool?
  9    /// Present on `repo search` rows, absent on `repo list`.
 10    let topics: [String]?
 11
 12    var id: String { path }
 13    var isArchived: Bool { archived ?? false }
 14    var isPrivate: Bool { visibility == "private" }
 15
 16    /// "krz/gitbay" → ("krz", "gitbay")
 17    var owner: String { String(path.split(separator: "/").first ?? "") }
 18    var name: String { String(path.split(separator: "/").last ?? "") }
 19}
 20
 21/// `repo show <owner/name>`.
 22nonisolated struct RepoDetail: Decodable, Sendable, Hashable {
 23    let path: String
 24    let description: String?
 25    let website: String?
 26    let visibility: String
 27    let defaultBranch: String
 28    let protectedBranches: [String]?
 29    let archived: Bool?
 30    let topics: [String]?
 31    /// The caller's own watch state: `"watching"`, `"muted"`, or absent
 32    /// for the default (neither).
 33    let watch: String?
 34    /// `omitempty` on a Bool, so absent means false, not unknown.
 35    let bookmarked: Bool
 36    /// Absent means either not a fork, or a fork whose parent the caller
 37    /// cannot read — the server withholds it rather than confirm a
 38    /// private parent exists. Nothing further may be inferred from nil.
 39    let forkOf: String?
 40
 41    enum CodingKeys: String, CodingKey {
 42        case path, description, website, visibility, archived, topics, watch, bookmarked
 43        case defaultBranch = "default_branch"
 44        case protectedBranches = "protected_branches"
 45        case forkOf = "fork_of"
 46    }
 47
 48    init(from decoder: any Decoder) throws {
 49        let container = try decoder.container(keyedBy: CodingKeys.self)
 50        path = try container.decode(String.self, forKey: .path)
 51        description = try container.decodeIfPresent(String.self, forKey: .description)
 52        website = try container.decodeIfPresent(String.self, forKey: .website)
 53        visibility = try container.decode(String.self, forKey: .visibility)
 54        defaultBranch = try container.decode(String.self, forKey: .defaultBranch)
 55        protectedBranches = try container.decodeIfPresent([String].self, forKey: .protectedBranches)
 56        archived = try container.decodeIfPresent(Bool.self, forKey: .archived)
 57        topics = try container.decodeIfPresent([String].self, forKey: .topics)
 58        watch = try container.decodeIfPresent(String.self, forKey: .watch)
 59        bookmarked = try container.decodeIfPresent(Bool.self, forKey: .bookmarked) ?? false
 60        forkOf = try container.decodeIfPresent(String.self, forKey: .forkOf)
 61    }
 62
 63    var isArchived: Bool { archived ?? false }
 64}
 65
 66/// The caller's watch/mute state on a repository, from `RepoDetail.watch`.
 67/// `repo watch` sets `.watching`, `repo mute` sets `.muted`, and
 68/// `repo unwatch` clears either back to `.default`, so `available` offers
 69/// unwatch from both explicit states and never from the default.
 70nonisolated enum WatchState: Sendable, Hashable {
 71    case watching
 72    case muted
 73    case `default`
 74
 75    /// Unrecognized raw values (a future server addition) fall back to
 76    /// `.default` rather than crashing or masquerading as watching.
 77    init(_ raw: String?) {
 78        switch raw {
 79        case "watching": self = .watching
 80        case "muted": self = .muted
 81        default: self = .default
 82        }
 83    }
 84
 85    var available: [WatchAction] {
 86        switch self {
 87        case .watching: [.unwatch, .mute]
 88        case .muted: [.watch, .unwatch]
 89        case .default: [.watch, .mute]
 90        }
 91    }
 92}
 93
 94/// A transition `WatchState.available` may offer.
 95nonisolated enum WatchAction: Sendable, Hashable {
 96    case watch
 97    case unwatch
 98    case mute
 99}
100
101/// `repo refs <owner/name>` — the branches and tags the server knows.
102nonisolated struct RepoRefs: Decodable, Sendable, Hashable {
103    let branches: [RepoRef]
104    let tags: [RepoRef]
105}
106
107nonisolated struct RepoRef: Decodable, Sendable, Hashable, Identifiable {
108    let name: String
109    let sha: String
110    var id: String { name }
111}
112
113/// `repo tree` — one directory listing.
114nonisolated struct TreeListing: Decodable, Sendable, Hashable {
115    let path: String
116    let ref: String
117    let dir: String
118    let entries: [TreeEntry]
119}
120
121/// One entry of a tree. The sha is the git object id — content at a sha
122/// never changes, which is what makes caching on it sound.
123nonisolated struct TreeEntry: Decodable, Sendable, Hashable, Identifiable {
124    let name: String
125    let type: String // blob | tree
126    let mode: String
127    let sha: String
128    let size: Int64?
129
130    var id: String { sha + name }
131    var isDirectory: Bool { type == "tree" }
132    var isSymlink: Bool { mode == "120000" }
133}
134
135/// `repo cat` — one file. Exactly one of `content`/`base64` is set.
136nonisolated struct FileContent: Decodable, Sendable, Hashable {
137    let path: String
138    let ref: String
139    let file: String
140    let size: Int
141    let binary: Bool
142    let truncated: Bool?
143    let content: String?
144    let base64: String?
145
146    var isTruncated: Bool { truncated ?? false }
147
148    var data: Data? {
149        if let content { return Data(content.utf8) }
150        if let base64 { return Data(base64Encoded: base64) }
151        return nil
152    }
153}
154
155/// `repo log` — one commit, with its signature verdict.
156nonisolated struct Commit: Decodable, Sendable, Hashable, Identifiable {
157    let sha: String
158    let subject: String
159    let authorName: String
160    let authorEmail: String
161    let committerEmail: String?
162    let date: Date
163    let signature: Signature
164
165    enum CodingKeys: String, CodingKey {
166        case sha, subject, date, signature
167        case authorName = "author_name"
168        case authorEmail = "author_email"
169        case committerEmail = "committer_email"
170    }
171
172    var id: String { sha }
173    var shortSHA: String { String(sha.prefix(10)) }
174
175    nonisolated struct Signature: Decodable, Sendable, Hashable {
176        let state: SignatureState
177        let signer: String?
178        let keyFingerprint: String?
179
180        enum CodingKeys: String, CodingKey {
181            case state, signer
182            case keyFingerprint = "key_fingerprint"
183        }
184    }
185}
186
187/// The server's verdict on a commit signature; `internal/sig/verify.go`.
188/// Unknown future states decode as `.unrecognized` rather than failing the
189/// whole log.
190nonisolated enum SignatureState: Sendable, Hashable, Decodable {
191    case verified
192    case signedUnknownKey
193    case signedEmailMismatch
194    case signedKeyExpired
195    case signedKeyRevoked
196    case badSignature
197    case unsigned
198    case unrecognized(String)
199
200    init(from decoder: any Decoder) throws {
201        switch try decoder.singleValueContainer().decode(String.self) {
202        case "verified": self = .verified
203        case "signed_unknown_key": self = .signedUnknownKey
204        case "signed_email_mismatch": self = .signedEmailMismatch
205        case "signed_key_expired": self = .signedKeyExpired
206        case "signed_key_revoked": self = .signedKeyRevoked
207        case "bad_signature": self = .badSignature
208        case "unsigned": self = .unsigned
209        case let other: self = .unrecognized(other)
210        }
211    }
212}