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" } } /// 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? 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] enum CodingKeys: String, CodingKey { case number, title, state, author, source, body, milestone, checks, commits, comments, reviews, draft, stacked 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 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) 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) ?? [] } var isOpen: Bool { state == "open" } 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 let createdAt: Date? enum CodingKeys: String, CodingKey { case reviewer, verdict, stale case createdAt = "created_at" } 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" } } }