import Foundation /// The MR below or above a stack member: `mr show` sends `{number, title}`. nonisolated struct StackRef: Decodable, Sendable, Hashable, Identifiable { let number: Int64 let title: String var id: Int64 { number } } /// One row of `mr list`, and the header half of `mr show`. nonisolated struct MergeRequest: Decodable, Sendable, Hashable, Identifiable { let number: Int64 let title: String let state: String let author: String /// "branch" for a same-repo MR, "owner/name:branch" cross-repo, /// "" when the source is gone. let source: String let targetRef: String let headSHA: String let body: String? let createdAt: Date /// Open but not asking: does not merge, does not appear in a review queue. let draft: Bool let stackedOn: StackRef? enum CodingKeys: String, CodingKey { case number, title, state, author, source, body, draft case targetRef = "target_ref" case headSHA = "head_sha" case createdAt = "created_at" case stackedOn = "stacked_on" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) number = try container.decode(Int64.self, forKey: .number) title = try container.decode(String.self, forKey: .title) state = try container.decode(String.self, forKey: .state) author = try container.decode(String.self, forKey: .author) source = try container.decode(String.self, forKey: .source) targetRef = try container.decode(String.self, forKey: .targetRef) headSHA = try container.decode(String.self, forKey: .headSHA) body = try container.decodeIfPresent(String.self, forKey: .body) createdAt = try container.decode(Date.self, forKey: .createdAt) draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn) } var id: Int64 { number } var isOpen: Bool { state == "open" } } /// One row of `mr revisions`: a head the merge request has had. `n` /// counts from the first push; `current` marks the head it has now. nonisolated struct MRRevision: Decodable, Sendable, Hashable, Identifiable { let n: Int let sha: String let baseSHA: String? let createdAt: String let current: Bool? enum CodingKeys: String, CodingKey { case n, sha, current case baseSHA = "base_sha" case createdAt = "created_at" } var id: Int { n } var isCurrent: Bool { current ?? false } } /// The whole of `mr show`. nonisolated struct MRDetail: Decodable, Sendable, Hashable { let number: Int64 let title: String let state: String let author: String let source: String let targetRef: String let headSHA: String let body: String? let bodyFormat: String? let milestone: String? let createdAt: Date /// Set once the merge request is resolved. The actor is absent when /// the account is gone, or the row was imported. let mergedAt: Date? let mergedBy: String? let closedAt: Date? let closedBy: String? /// The merge request this one was closed in favour of. Omitted by /// the server when there is none. let supersededBy: Int64? let checks: [Check]? let checksCombined: String? let unresolvedThreads: Int? let commits: [MRCommit]? let comments: [MRComment]? let reviews: [Review]? /// Open but not asking: does not merge, does not appear in a review queue. let draft: Bool let reviewRequests: [String] let stackedOn: StackRef? let stacked: [StackRef] /// The merge gates, computed by the server the way `mr merge` will /// judge them. Absent from a server older than v1.16.0. let gates: Gates? enum CodingKeys: String, CodingKey { case number, title, state, author, source, body, milestone, checks, commits, comments, reviews, draft, stacked, gates case bodyFormat = "body_format" case targetRef = "target_ref" case headSHA = "head_sha" case createdAt = "created_at" case mergedAt = "merged_at" case mergedBy = "merged_by" case closedAt = "closed_at" case closedBy = "closed_by" case supersededBy = "superseded_by" case checksCombined = "checks_combined" case unresolvedThreads = "unresolved_threads" case reviewRequests = "review_requests" case stackedOn = "stacked_on" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) number = try container.decode(Int64.self, forKey: .number) title = try container.decode(String.self, forKey: .title) state = try container.decode(String.self, forKey: .state) author = try container.decode(String.self, forKey: .author) source = try container.decode(String.self, forKey: .source) targetRef = try container.decode(String.self, forKey: .targetRef) headSHA = try container.decode(String.self, forKey: .headSHA) body = try container.decodeIfPresent(String.self, forKey: .body) bodyFormat = try container.decodeIfPresent(String.self, forKey: .bodyFormat) milestone = try container.decodeIfPresent(String.self, forKey: .milestone) createdAt = try container.decode(Date.self, forKey: .createdAt) mergedAt = try container.decodeIfPresent(Date.self, forKey: .mergedAt) mergedBy = try container.decodeIfPresent(String.self, forKey: .mergedBy) closedAt = try container.decodeIfPresent(Date.self, forKey: .closedAt) closedBy = try container.decodeIfPresent(String.self, forKey: .closedBy) supersededBy = try container.decodeIfPresent(Int64.self, forKey: .supersededBy) checks = try container.decodeIfPresent([Check].self, forKey: .checks) checksCombined = try container.decodeIfPresent(String.self, forKey: .checksCombined) unresolvedThreads = try container.decodeIfPresent(Int.self, forKey: .unresolvedThreads) commits = try container.decodeIfPresent([MRCommit].self, forKey: .commits) comments = try container.decodeIfPresent([MRComment].self, forKey: .comments) reviews = try container.decodeIfPresent([Review].self, forKey: .reviews) draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false reviewRequests = try container.decodeIfPresent([String].self, forKey: .reviewRequests) ?? [] stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn) stacked = try container.decodeIfPresent([StackRef].self, forKey: .stacked) ?? [] gates = try container.decodeIfPresent(Gates.self, forKey: .gates) } var isOpen: Bool { state == "open" } /// What stands between the merge request and a merge, all at once. nonisolated struct Gates: Decodable, Sendable, Hashable { let draft: Bool let approvalsRequired: Int /// Fresh approvals from reviewers who count. let approvals: [String] let changesRequested: [String] let codeownersRequired: Bool let ownersOutstanding: [Owners] let resolvedRequired: Bool let openThreads: Int let checksRequired: Bool /// Combined check status; nil when none reported. let checks: String? let fastForward: Bool let unmet: [String] var isMet: Bool { unmet.isEmpty } enum CodingKeys: String, CodingKey { case draft, approvals, checks, unmet case approvalsRequired = "approvals_required" case changesRequested = "changes_requested" case codeownersRequired = "codeowners_required" case ownersOutstanding = "owners_outstanding" case resolvedRequired = "resolved_required" case openThreads = "open_threads" case checksRequired = "checks_required" case fastForward = "fast_forward" } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) draft = try c.decodeIfPresent(Bool.self, forKey: .draft) ?? false approvalsRequired = try c.decodeIfPresent(Int.self, forKey: .approvalsRequired) ?? 0 approvals = try c.decodeIfPresent([String].self, forKey: .approvals) ?? [] changesRequested = try c.decodeIfPresent([String].self, forKey: .changesRequested) ?? [] codeownersRequired = try c.decodeIfPresent(Bool.self, forKey: .codeownersRequired) ?? false ownersOutstanding = try c.decodeIfPresent([Owners].self, forKey: .ownersOutstanding) ?? [] resolvedRequired = try c.decodeIfPresent(Bool.self, forKey: .resolvedRequired) ?? false openThreads = try c.decodeIfPresent(Int.self, forKey: .openThreads) ?? 0 checksRequired = try c.decodeIfPresent(Bool.self, forKey: .checksRequired) ?? false checks = try c.decodeIfPresent(String.self, forKey: .checks) fastForward = try c.decodeIfPresent(Bool.self, forKey: .fastForward) ?? false unmet = try c.decodeIfPresent([String].self, forKey: .unmet) ?? [] } /// Files whose owners have not approved, and who those owners are. nonisolated struct Owners: Decodable, Sendable, Hashable, Identifiable { let files: [String] let owners: [String] var id: String { files.joined(separator: "\u{1f}") } } } nonisolated struct Check: Decodable, Sendable, Hashable { let context: String let state: String let url: String? /// When the check last changed state. let updatedAt: Date? /// How long the build behind the check ran, as the server /// formats it ("1m12s"). Only CI checks have a build to time. let duration: String? enum CodingKeys: String, CodingKey { case context, state, url, duration case updatedAt = "updated_at" } } nonisolated struct MRCommit: Decodable, Sendable, Hashable, Identifiable { let sha: String let subject: String var id: String { sha } var shortSHA: String { String(sha.prefix(10)) } } nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable { let author: String let body: String let bodyFormat: String? let createdAt: Date enum CodingKeys: String, CodingKey { case author, body case bodyFormat = "body_format" case createdAt = "created_at" } var id: String { author + createdAt.timeIntervalSince1970.description + body } } nonisolated struct Review: Decodable, Sendable, Hashable, Identifiable { let reviewer: String let verdict: String /// The review predates the current head — it approved older code. let stale: Bool /// Whether the merge gates count this verdict: a reader may review /// a public merge request, but only a writer decides it. Absent /// before v1.16.0, when only writers could review at all. let counts: Bool let createdAt: Date? enum CodingKeys: String, CodingKey { case reviewer, verdict, stale, counts case createdAt = "created_at" } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) reviewer = try c.decode(String.self, forKey: .reviewer) verdict = try c.decode(String.self, forKey: .verdict) stale = try c.decodeIfPresent(Bool.self, forKey: .stale) ?? false counts = try c.decodeIfPresent(Bool.self, forKey: .counts) ?? true createdAt = try c.decodeIfPresent(Date.self, forKey: .createdAt) } var id: String { reviewer } } } /// One review thread of `mr threads`. nonisolated struct ReviewThread: Decodable, Sendable, Hashable, Identifiable { let id: Int64 let path: String let side: String // new | old let line: Int64 /// Anchored to an older head; the diff has moved under it. let stale: Bool let resolvedBy: String? let comments: [Comment] enum CodingKeys: String, CodingKey { case id, path, side, line, stale, comments case resolvedBy = "resolved_by" } var isResolved: Bool { resolvedBy != nil } nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable { let id: Int64 let author: String let body: String let createdAt: Date enum CodingKeys: String, CodingKey { case id, author, body case createdAt = "created_at" } } }