import Foundation /// One row of `repo list`. nonisolated struct RepoSummary: Decodable, Sendable, Hashable, Identifiable { let path: String let visibility: String let description: String? let archived: Bool? /// Present on `repo search` rows, absent on `repo list`. let topics: [String]? var id: String { path } var isArchived: Bool { archived ?? false } var isPrivate: Bool { visibility == "private" } /// "krz/gitbay" → ("krz", "gitbay") var owner: String { String(path.split(separator: "/").first ?? "") } var name: String { String(path.split(separator: "/").last ?? "") } } /// `repo show `. nonisolated struct RepoDetail: Decodable, Sendable, Hashable { let path: String let description: String? let website: String? let visibility: String let defaultBranch: String let protectedBranches: [String]? let archived: Bool? let topics: [String]? /// The caller's own watch state: `"watching"`, `"muted"`, or absent /// for the default (neither). let watch: String? /// `omitempty` on a Bool, so absent means false, not unknown. let bookmarked: Bool /// Absent means either not a fork, or a fork whose parent the caller /// cannot read — the server withholds it rather than confirm a /// private parent exists. Nothing further may be inferred from nil. let forkOf: String? enum CodingKeys: String, CodingKey { case path, description, website, visibility, archived, topics, watch, bookmarked case defaultBranch = "default_branch" case protectedBranches = "protected_branches" case forkOf = "fork_of" } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) path = try container.decode(String.self, forKey: .path) description = try container.decodeIfPresent(String.self, forKey: .description) website = try container.decodeIfPresent(String.self, forKey: .website) visibility = try container.decode(String.self, forKey: .visibility) defaultBranch = try container.decode(String.self, forKey: .defaultBranch) protectedBranches = try container.decodeIfPresent([String].self, forKey: .protectedBranches) archived = try container.decodeIfPresent(Bool.self, forKey: .archived) topics = try container.decodeIfPresent([String].self, forKey: .topics) watch = try container.decodeIfPresent(String.self, forKey: .watch) bookmarked = try container.decodeIfPresent(Bool.self, forKey: .bookmarked) ?? false forkOf = try container.decodeIfPresent(String.self, forKey: .forkOf) } var isArchived: Bool { archived ?? false } } /// The caller's watch/mute state on a repository, from `RepoDetail.watch`. /// `repo watch` sets `.watching`, `repo mute` sets `.muted`, and /// `repo unwatch` clears either back to `.default`, so `available` offers /// unwatch from both explicit states and never from the default. nonisolated enum WatchState: Sendable, Hashable { case watching case muted case `default` /// Unrecognized raw values (a future server addition) fall back to /// `.default` rather than crashing or masquerading as watching. init(_ raw: String?) { switch raw { case "watching": self = .watching case "muted": self = .muted default: self = .default } } var available: [WatchAction] { switch self { case .watching: [.unwatch, .mute] case .muted: [.watch, .unwatch] case .default: [.watch, .mute] } } } /// A transition `WatchState.available` may offer. nonisolated enum WatchAction: Sendable, Hashable { case watch case unwatch case mute } /// `repo refs ` — the branches and tags the server knows. nonisolated struct RepoRefs: Decodable, Sendable, Hashable { let branches: [RepoRef] let tags: [RepoRef] } nonisolated struct RepoRef: Decodable, Sendable, Hashable, Identifiable { let name: String let sha: String var id: String { name } } /// `repo tree` — one directory listing. nonisolated struct TreeListing: Decodable, Sendable, Hashable { let path: String let ref: String let dir: String let entries: [TreeEntry] } /// One entry of a tree. The sha is the git object id — content at a sha /// never changes, which is what makes caching on it sound. nonisolated struct TreeEntry: Decodable, Sendable, Hashable, Identifiable { let name: String let type: String // blob | tree let mode: String let sha: String let size: Int64? var id: String { sha + name } var isDirectory: Bool { type == "tree" } var isSymlink: Bool { mode == "120000" } } /// `repo cat` — one file. Exactly one of `content`/`base64` is set. nonisolated struct FileContent: Decodable, Sendable, Hashable { let path: String let ref: String let file: String let size: Int let binary: Bool let truncated: Bool? let content: String? let base64: String? var isTruncated: Bool { truncated ?? false } var data: Data? { if let content { return Data(content.utf8) } if let base64 { return Data(base64Encoded: base64) } return nil } } /// `repo log` — one commit, with its signature verdict. nonisolated struct Commit: Decodable, Sendable, Hashable, Identifiable { let sha: String let subject: String let authorName: String let authorEmail: String let committerEmail: String? let date: Date let signature: Signature enum CodingKeys: String, CodingKey { case sha, subject, date, signature case authorName = "author_name" case authorEmail = "author_email" case committerEmail = "committer_email" } var id: String { sha } var shortSHA: String { String(sha.prefix(10)) } nonisolated struct Signature: Decodable, Sendable, Hashable { let state: SignatureState let signer: String? let keyFingerprint: String? enum CodingKeys: String, CodingKey { case state, signer case keyFingerprint = "key_fingerprint" } } } /// The server's verdict on a commit signature; `internal/sig/verify.go`. /// Unknown future states decode as `.unrecognized` rather than failing the /// whole log. nonisolated enum SignatureState: Sendable, Hashable, Decodable { case verified case signedUnknownKey case signedEmailMismatch case signedKeyExpired case signedKeyRevoked case badSignature case unsigned case unrecognized(String) init(from decoder: any Decoder) throws { switch try decoder.singleValueContainer().decode(String.self) { case "verified": self = .verified case "signed_unknown_key": self = .signedUnknownKey case "signed_email_mismatch": self = .signedEmailMismatch case "signed_key_expired": self = .signedKeyExpired case "signed_key_revoked": self = .signedKeyRevoked case "bad_signature": self = .badSignature case "unsigned": self = .unsigned case let other: self = .unrecognized(other) } } }