gitbay/MRs/MRModels.swift

36835f99e6d463f3e18c5c3fc3562bb92513f52b
gitbay-ios/gitbay/MRs/MRModels.swift history · blame · raw

356 lines · 14817 bytes

10 symbols in this file
  1import Foundation
  2
  3/// The MR below or above a stack member: `mr show` sends `{number, title}`.
  4nonisolated struct StackRef: Decodable, Sendable, Hashable, Identifiable {
  5    let number: Int64
  6    let title: String
  7
  8    var id: Int64 { number }
  9}
 10
 11/// One row of `mr list`, and the header half of `mr show`.
 12nonisolated struct MergeRequest: Decodable, Sendable, Hashable, Identifiable {
 13    let number: Int64
 14    let title: String
 15    let state: String
 16    let author: String
 17    /// "branch" for a same-repo MR, "owner/name:branch" cross-repo,
 18    /// "" when the source is gone.
 19    let source: String
 20    let targetRef: String
 21    let headSHA: String
 22    let body: String?
 23    let createdAt: Date
 24    /// Open but not asking: does not merge, does not appear in a review queue.
 25    let draft: Bool
 26    let stackedOn: StackRef?
 27
 28    enum CodingKeys: String, CodingKey {
 29        case number, title, state, author, source, body, draft
 30        case targetRef = "target_ref"
 31        case headSHA = "head_sha"
 32        case createdAt = "created_at"
 33        case stackedOn = "stacked_on"
 34    }
 35
 36    init(from decoder: Decoder) throws {
 37        let container = try decoder.container(keyedBy: CodingKeys.self)
 38        number = try container.decode(Int64.self, forKey: .number)
 39        title = try container.decode(String.self, forKey: .title)
 40        state = try container.decode(String.self, forKey: .state)
 41        author = try container.decode(String.self, forKey: .author)
 42        source = try container.decode(String.self, forKey: .source)
 43        targetRef = try container.decode(String.self, forKey: .targetRef)
 44        headSHA = try container.decode(String.self, forKey: .headSHA)
 45        body = try container.decodeIfPresent(String.self, forKey: .body)
 46        createdAt = try container.decode(Date.self, forKey: .createdAt)
 47        draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
 48        stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
 49    }
 50
 51    var id: Int64 { number }
 52    var isOpen: Bool { state == "open" }
 53}
 54
 55/// One row of `mr revisions`: a head the merge request has had. `n`
 56/// counts from the first push; `current` marks the head it has now.
 57nonisolated struct MRRevision: Decodable, Sendable, Hashable, Identifiable {
 58    let n: Int
 59    let sha: String
 60    let baseSHA: String?
 61    let createdAt: String
 62    let current: Bool?
 63
 64    enum CodingKeys: String, CodingKey {
 65        case n, sha, current
 66        case baseSHA = "base_sha"
 67        case createdAt = "created_at"
 68    }
 69
 70    var id: Int { n }
 71    var isCurrent: Bool { current ?? false }
 72}
 73
 74/// The whole of `mr show`.
 75nonisolated struct MRDetail: Decodable, Sendable, Hashable {
 76    let number: Int64
 77    let title: String
 78    let state: String
 79    let author: String
 80    let source: String
 81    let targetRef: String
 82    let headSHA: String
 83    let body: String?
 84    let bodyFormat: String?
 85    let milestone: String?
 86    /// Omitted by the server when there are none.
 87    let labels: [String]
 88    let createdAt: Date
 89    /// Set once the merge request is resolved. The actor is absent when
 90    /// the account is gone, or the row was imported.
 91    let mergedAt: Date?
 92    let mergedBy: String?
 93    let closedAt: Date?
 94    let closedBy: String?
 95    /// The merge request this one was closed in favour of. Omitted by
 96    /// the server when there is none.
 97    let supersededBy: Int64?
 98    let checks: [Check]?
 99    let checksCombined: String?
100    let unresolvedThreads: Int?
101    let commits: [MRCommit]?
102    let reactions: [ReactionCount]?
103    let comments: [MRComment]?
104    let reviews: [Review]?
105    /// Open but not asking: does not merge, does not appear in a review queue.
106    let draft: Bool
107    let reviewRequests: [String]
108    let stackedOn: StackRef?
109    let stacked: [StackRef]
110    /// The merge gates, computed by the server the way `mr merge` will
111    /// judge them. Absent from a server older than v1.16.0.
112    let gates: Gates?
113    /// A merge queued with `mr merge --when-ready`, waiting on the gates.
114    let queued: Queued?
115
116    enum CodingKeys: String, CodingKey {
117        case number, title, state, author, source, body, milestone, labels, checks, commits, reactions, comments, reviews, draft, stacked, gates, queued
118        case bodyFormat = "body_format"
119        case targetRef = "target_ref"
120        case headSHA = "head_sha"
121        case createdAt = "created_at"
122        case mergedAt = "merged_at"
123        case mergedBy = "merged_by"
124        case closedAt = "closed_at"
125        case closedBy = "closed_by"
126        case supersededBy = "superseded_by"
127        case checksCombined = "checks_combined"
128        case unresolvedThreads = "unresolved_threads"
129        case reviewRequests = "review_requests"
130        case stackedOn = "stacked_on"
131    }
132
133    init(from decoder: Decoder) throws {
134        let container = try decoder.container(keyedBy: CodingKeys.self)
135        number = try container.decode(Int64.self, forKey: .number)
136        title = try container.decode(String.self, forKey: .title)
137        state = try container.decode(String.self, forKey: .state)
138        author = try container.decode(String.self, forKey: .author)
139        source = try container.decode(String.self, forKey: .source)
140        targetRef = try container.decode(String.self, forKey: .targetRef)
141        headSHA = try container.decode(String.self, forKey: .headSHA)
142        body = try container.decodeIfPresent(String.self, forKey: .body)
143        bodyFormat = try container.decodeIfPresent(String.self, forKey: .bodyFormat)
144        milestone = try container.decodeIfPresent(String.self, forKey: .milestone)
145        labels = try container.decodeIfPresent([String].self, forKey: .labels) ?? []
146        createdAt = try container.decode(Date.self, forKey: .createdAt)
147        mergedAt = try container.decodeIfPresent(Date.self, forKey: .mergedAt)
148        mergedBy = try container.decodeIfPresent(String.self, forKey: .mergedBy)
149        closedAt = try container.decodeIfPresent(Date.self, forKey: .closedAt)
150        closedBy = try container.decodeIfPresent(String.self, forKey: .closedBy)
151        supersededBy = try container.decodeIfPresent(Int64.self, forKey: .supersededBy)
152        checks = try container.decodeIfPresent([Check].self, forKey: .checks)
153        checksCombined = try container.decodeIfPresent(String.self, forKey: .checksCombined)
154        unresolvedThreads = try container.decodeIfPresent(Int.self, forKey: .unresolvedThreads)
155        commits = try container.decodeIfPresent([MRCommit].self, forKey: .commits)
156        reactions = try container.decodeIfPresent([ReactionCount].self, forKey: .reactions)
157        comments = try container.decodeIfPresent([MRComment].self, forKey: .comments)
158        reviews = try container.decodeIfPresent([Review].self, forKey: .reviews)
159        draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
160        reviewRequests = try container.decodeIfPresent([String].self, forKey: .reviewRequests) ?? []
161        stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
162        stacked = try container.decodeIfPresent([StackRef].self, forKey: .stacked) ?? []
163        gates = try container.decodeIfPresent(Gates.self, forKey: .gates)
164        queued = try container.decodeIfPresent(Queued.self, forKey: .queued)
165    }
166
167    /// Who queued the merge, with which strategy, and why it has not
168    /// merged yet.
169    nonisolated struct Queued: Decodable, Sendable, Hashable {
170        let by: String
171        let strategy: String?
172        let reason: String?
173        let queuedAt: Date
174
175        enum CodingKeys: String, CodingKey {
176            case by, strategy, reason
177            case queuedAt = "queued_at"
178        }
179    }
180
181    var isOpen: Bool { state == "open" }
182
183    /// What stands between the merge request and a merge, all at once.
184    nonisolated struct Gates: Decodable, Sendable, Hashable {
185        let draft: Bool
186        let approvalsRequired: Int
187        /// Fresh approvals from reviewers who count.
188        let approvals: [String]
189        let changesRequested: [String]
190        let codeownersRequired: Bool
191        let ownersOutstanding: [Owners]
192        let resolvedRequired: Bool
193        let openThreads: Int
194        let checksRequired: Bool
195        /// Combined check status; nil when none reported.
196        let checks: String?
197        let fastForward: Bool
198        let unmet: [String]
199
200        var isMet: Bool { unmet.isEmpty }
201
202        enum CodingKeys: String, CodingKey {
203            case draft, approvals, checks, unmet
204            case approvalsRequired = "approvals_required"
205            case changesRequested = "changes_requested"
206            case codeownersRequired = "codeowners_required"
207            case ownersOutstanding = "owners_outstanding"
208            case resolvedRequired = "resolved_required"
209            case openThreads = "open_threads"
210            case checksRequired = "checks_required"
211            case fastForward = "fast_forward"
212        }
213
214        init(from decoder: Decoder) throws {
215            let c = try decoder.container(keyedBy: CodingKeys.self)
216            draft = try c.decodeIfPresent(Bool.self, forKey: .draft) ?? false
217            approvalsRequired = try c.decodeIfPresent(Int.self, forKey: .approvalsRequired) ?? 0
218            approvals = try c.decodeIfPresent([String].self, forKey: .approvals) ?? []
219            changesRequested = try c.decodeIfPresent([String].self, forKey: .changesRequested) ?? []
220            codeownersRequired = try c.decodeIfPresent(Bool.self, forKey: .codeownersRequired) ?? false
221            ownersOutstanding = try c.decodeIfPresent([Owners].self, forKey: .ownersOutstanding) ?? []
222            resolvedRequired = try c.decodeIfPresent(Bool.self, forKey: .resolvedRequired) ?? false
223            openThreads = try c.decodeIfPresent(Int.self, forKey: .openThreads) ?? 0
224            checksRequired = try c.decodeIfPresent(Bool.self, forKey: .checksRequired) ?? false
225            checks = try c.decodeIfPresent(String.self, forKey: .checks)
226            fastForward = try c.decodeIfPresent(Bool.self, forKey: .fastForward) ?? false
227            unmet = try c.decodeIfPresent([String].self, forKey: .unmet) ?? []
228        }
229
230        /// Files whose owners have not approved, and who those owners are.
231        nonisolated struct Owners: Decodable, Sendable, Hashable, Identifiable {
232            let files: [String]
233            let owners: [String]
234            var id: String { files.joined(separator: "\u{1f}") }
235        }
236    }
237
238    nonisolated struct Check: Decodable, Sendable, Hashable {
239        let context: String
240        let state: String
241        let url: String?
242        /// When the check last changed state.
243        let updatedAt: Date?
244        /// How long the build behind the check ran, as the server
245        /// formats it ("1m12s"). Only CI checks have a build to time.
246        let duration: String?
247
248        enum CodingKeys: String, CodingKey {
249            case context, state, url, duration
250            case updatedAt = "updated_at"
251        }
252    }
253
254    nonisolated struct MRCommit: Decodable, Sendable, Hashable, Identifiable {
255        let sha: String
256        let subject: String
257        var id: String { sha }
258        var shortSHA: String { String(sha.prefix(10)) }
259    }
260
261    nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable {
262        /// What `--comment` names. Absent from a server older than v1.39.0.
263        let commentID: Int64?
264        let author: String
265        let body: String
266        let bodyFormat: String?
267        let createdAt: Date
268        let reactions: [ReactionCount]?
269        /// "system" for a line the server posted (a retarget, a queued
270        /// merge), which takes no reactions (krz/gitbay#332).
271        let kind: String?
272
273        enum CodingKeys: String, CodingKey {
274            case author, body, reactions, kind
275            case commentID = "id"
276            case bodyFormat = "body_format"
277            case createdAt = "created_at"
278        }
279
280        /// A server without `kind` names the author "system" on those lines;
281        /// an account can carry that name too, so `kind` wins when present.
282        var isSystem: Bool { kind.map { $0 == "system" } ?? (author == "system") }
283
284        var id: String { author + createdAt.timeIntervalSince1970.description + body }
285    }
286
287    nonisolated struct Review: Decodable, Sendable, Hashable, Identifiable {
288        let reviewer: String
289        let verdict: String
290        /// The review predates the current head — it approved older code.
291        let stale: Bool
292        /// Whether the merge gates count this verdict: a reader may review
293        /// a public merge request, but only a writer decides it. Absent
294        /// before v1.16.0, when only writers could review at all.
295        let counts: Bool
296        let createdAt: Date?
297
298        enum CodingKeys: String, CodingKey {
299            case reviewer, verdict, stale, counts
300            case createdAt = "created_at"
301        }
302
303        init(from decoder: Decoder) throws {
304            let c = try decoder.container(keyedBy: CodingKeys.self)
305            reviewer = try c.decode(String.self, forKey: .reviewer)
306            verdict = try c.decode(String.self, forKey: .verdict)
307            stale = try c.decodeIfPresent(Bool.self, forKey: .stale) ?? false
308            counts = try c.decodeIfPresent(Bool.self, forKey: .counts) ?? true
309            createdAt = try c.decodeIfPresent(Date.self, forKey: .createdAt)
310        }
311
312        var id: String { reviewer }
313    }
314}
315
316/// One review thread of `mr threads`.
317nonisolated struct ReviewThread: Decodable, Sendable, Hashable, Identifiable {
318    let id: Int64
319    let path: String
320    let side: String // new | old
321    let line: Int64
322    /// The first line of a range; absent for a one-line thread.
323    let startLine: Int64?
324    /// Anchored to an older head; the diff has moved under it.
325    let stale: Bool
326    let resolvedBy: String?
327    /// What the thread's ```suggestion block proposes (v1.39.0).
328    let suggestion: Suggestion?
329    let comments: [Comment]
330
331    enum CodingKeys: String, CodingKey {
332        case id, path, side, line, stale, suggestion, comments
333        case startLine = "start_line"
334        case resolvedBy = "resolved_by"
335    }
336
337    /// `path:line`, or `path:start–line` for a range.
338    var anchor: String {
339        if let startLine, startLine < line { return "\(path):\(startLine)–\(line)" }
340        return "\(path):\(line)"
341    }
342
343    var isResolved: Bool { resolvedBy != nil }
344
345    nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable {
346        let id: Int64
347        let author: String
348        let body: String
349        let createdAt: Date
350
351        enum CodingKeys: String, CodingKey {
352            case id, author, body
353            case createdAt = "created_at"
354        }
355    }
356}