gitbay/MRs/MRModels.swift

v1.2.0
gitbay-ios/gitbay/MRs/MRModels.swift history · blame · raw

312 lines · 12877 bytes

  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    let createdAt: Date
 87    /// Set once the merge request is resolved. The actor is absent when
 88    /// the account is gone, or the row was imported.
 89    let mergedAt: Date?
 90    let mergedBy: String?
 91    let closedAt: Date?
 92    let closedBy: String?
 93    /// The merge request this one was closed in favour of. Omitted by
 94    /// the server when there is none.
 95    let supersededBy: Int64?
 96    let checks: [Check]?
 97    let checksCombined: String?
 98    let unresolvedThreads: Int?
 99    let commits: [MRCommit]?
100    let comments: [MRComment]?
101    let reviews: [Review]?
102    /// Open but not asking: does not merge, does not appear in a review queue.
103    let draft: Bool
104    let reviewRequests: [String]
105    let stackedOn: StackRef?
106    let stacked: [StackRef]
107    /// The merge gates, computed by the server the way `mr merge` will
108    /// judge them. Absent from a server older than v1.16.0.
109    let gates: Gates?
110
111    enum CodingKeys: String, CodingKey {
112        case number, title, state, author, source, body, milestone, checks, commits, comments, reviews, draft, stacked, gates
113        case bodyFormat = "body_format"
114        case targetRef = "target_ref"
115        case headSHA = "head_sha"
116        case createdAt = "created_at"
117        case mergedAt = "merged_at"
118        case mergedBy = "merged_by"
119        case closedAt = "closed_at"
120        case closedBy = "closed_by"
121        case supersededBy = "superseded_by"
122        case checksCombined = "checks_combined"
123        case unresolvedThreads = "unresolved_threads"
124        case reviewRequests = "review_requests"
125        case stackedOn = "stacked_on"
126    }
127
128    init(from decoder: Decoder) throws {
129        let container = try decoder.container(keyedBy: CodingKeys.self)
130        number = try container.decode(Int64.self, forKey: .number)
131        title = try container.decode(String.self, forKey: .title)
132        state = try container.decode(String.self, forKey: .state)
133        author = try container.decode(String.self, forKey: .author)
134        source = try container.decode(String.self, forKey: .source)
135        targetRef = try container.decode(String.self, forKey: .targetRef)
136        headSHA = try container.decode(String.self, forKey: .headSHA)
137        body = try container.decodeIfPresent(String.self, forKey: .body)
138        bodyFormat = try container.decodeIfPresent(String.self, forKey: .bodyFormat)
139        milestone = try container.decodeIfPresent(String.self, forKey: .milestone)
140        createdAt = try container.decode(Date.self, forKey: .createdAt)
141        mergedAt = try container.decodeIfPresent(Date.self, forKey: .mergedAt)
142        mergedBy = try container.decodeIfPresent(String.self, forKey: .mergedBy)
143        closedAt = try container.decodeIfPresent(Date.self, forKey: .closedAt)
144        closedBy = try container.decodeIfPresent(String.self, forKey: .closedBy)
145        supersededBy = try container.decodeIfPresent(Int64.self, forKey: .supersededBy)
146        checks = try container.decodeIfPresent([Check].self, forKey: .checks)
147        checksCombined = try container.decodeIfPresent(String.self, forKey: .checksCombined)
148        unresolvedThreads = try container.decodeIfPresent(Int.self, forKey: .unresolvedThreads)
149        commits = try container.decodeIfPresent([MRCommit].self, forKey: .commits)
150        comments = try container.decodeIfPresent([MRComment].self, forKey: .comments)
151        reviews = try container.decodeIfPresent([Review].self, forKey: .reviews)
152        draft = try container.decodeIfPresent(Bool.self, forKey: .draft) ?? false
153        reviewRequests = try container.decodeIfPresent([String].self, forKey: .reviewRequests) ?? []
154        stackedOn = try container.decodeIfPresent(StackRef.self, forKey: .stackedOn)
155        stacked = try container.decodeIfPresent([StackRef].self, forKey: .stacked) ?? []
156        gates = try container.decodeIfPresent(Gates.self, forKey: .gates)
157    }
158
159    var isOpen: Bool { state == "open" }
160
161    /// What stands between the merge request and a merge, all at once.
162    nonisolated struct Gates: Decodable, Sendable, Hashable {
163        let draft: Bool
164        let approvalsRequired: Int
165        /// Fresh approvals from reviewers who count.
166        let approvals: [String]
167        let changesRequested: [String]
168        let codeownersRequired: Bool
169        let ownersOutstanding: [Owners]
170        let resolvedRequired: Bool
171        let openThreads: Int
172        let checksRequired: Bool
173        /// Combined check status; nil when none reported.
174        let checks: String?
175        let fastForward: Bool
176        let unmet: [String]
177
178        var isMet: Bool { unmet.isEmpty }
179
180        enum CodingKeys: String, CodingKey {
181            case draft, approvals, checks, unmet
182            case approvalsRequired = "approvals_required"
183            case changesRequested = "changes_requested"
184            case codeownersRequired = "codeowners_required"
185            case ownersOutstanding = "owners_outstanding"
186            case resolvedRequired = "resolved_required"
187            case openThreads = "open_threads"
188            case checksRequired = "checks_required"
189            case fastForward = "fast_forward"
190        }
191
192        init(from decoder: Decoder) throws {
193            let c = try decoder.container(keyedBy: CodingKeys.self)
194            draft = try c.decodeIfPresent(Bool.self, forKey: .draft) ?? false
195            approvalsRequired = try c.decodeIfPresent(Int.self, forKey: .approvalsRequired) ?? 0
196            approvals = try c.decodeIfPresent([String].self, forKey: .approvals) ?? []
197            changesRequested = try c.decodeIfPresent([String].self, forKey: .changesRequested) ?? []
198            codeownersRequired = try c.decodeIfPresent(Bool.self, forKey: .codeownersRequired) ?? false
199            ownersOutstanding = try c.decodeIfPresent([Owners].self, forKey: .ownersOutstanding) ?? []
200            resolvedRequired = try c.decodeIfPresent(Bool.self, forKey: .resolvedRequired) ?? false
201            openThreads = try c.decodeIfPresent(Int.self, forKey: .openThreads) ?? 0
202            checksRequired = try c.decodeIfPresent(Bool.self, forKey: .checksRequired) ?? false
203            checks = try c.decodeIfPresent(String.self, forKey: .checks)
204            fastForward = try c.decodeIfPresent(Bool.self, forKey: .fastForward) ?? false
205            unmet = try c.decodeIfPresent([String].self, forKey: .unmet) ?? []
206        }
207
208        /// Files whose owners have not approved, and who those owners are.
209        nonisolated struct Owners: Decodable, Sendable, Hashable, Identifiable {
210            let files: [String]
211            let owners: [String]
212            var id: String { files.joined(separator: "\u{1f}") }
213        }
214    }
215
216    nonisolated struct Check: Decodable, Sendable, Hashable {
217        let context: String
218        let state: String
219        let url: String?
220        /// When the check last changed state.
221        let updatedAt: Date?
222        /// How long the build behind the check ran, as the server
223        /// formats it ("1m12s"). Only CI checks have a build to time.
224        let duration: String?
225
226        enum CodingKeys: String, CodingKey {
227            case context, state, url, duration
228            case updatedAt = "updated_at"
229        }
230    }
231
232    nonisolated struct MRCommit: Decodable, Sendable, Hashable, Identifiable {
233        let sha: String
234        let subject: String
235        var id: String { sha }
236        var shortSHA: String { String(sha.prefix(10)) }
237    }
238
239    nonisolated struct MRComment: Decodable, Sendable, Hashable, Identifiable {
240        let author: String
241        let body: String
242        let bodyFormat: String?
243        let createdAt: Date
244
245        enum CodingKeys: String, CodingKey {
246            case author, body
247            case bodyFormat = "body_format"
248            case createdAt = "created_at"
249        }
250
251        var id: String { author + createdAt.timeIntervalSince1970.description + body }
252    }
253
254    nonisolated struct Review: Decodable, Sendable, Hashable, Identifiable {
255        let reviewer: String
256        let verdict: String
257        /// The review predates the current head — it approved older code.
258        let stale: Bool
259        /// Whether the merge gates count this verdict: a reader may review
260        /// a public merge request, but only a writer decides it. Absent
261        /// before v1.16.0, when only writers could review at all.
262        let counts: Bool
263        let createdAt: Date?
264
265        enum CodingKeys: String, CodingKey {
266            case reviewer, verdict, stale, counts
267            case createdAt = "created_at"
268        }
269
270        init(from decoder: Decoder) throws {
271            let c = try decoder.container(keyedBy: CodingKeys.self)
272            reviewer = try c.decode(String.self, forKey: .reviewer)
273            verdict = try c.decode(String.self, forKey: .verdict)
274            stale = try c.decodeIfPresent(Bool.self, forKey: .stale) ?? false
275            counts = try c.decodeIfPresent(Bool.self, forKey: .counts) ?? true
276            createdAt = try c.decodeIfPresent(Date.self, forKey: .createdAt)
277        }
278
279        var id: String { reviewer }
280    }
281}
282
283/// One review thread of `mr threads`.
284nonisolated struct ReviewThread: Decodable, Sendable, Hashable, Identifiable {
285    let id: Int64
286    let path: String
287    let side: String // new | old
288    let line: Int64
289    /// Anchored to an older head; the diff has moved under it.
290    let stale: Bool
291    let resolvedBy: String?
292    let comments: [Comment]
293
294    enum CodingKeys: String, CodingKey {
295        case id, path, side, line, stale, comments
296        case resolvedBy = "resolved_by"
297    }
298
299    var isResolved: Bool { resolvedBy != nil }
300
301    nonisolated struct Comment: Decodable, Sendable, Hashable, Identifiable {
302        let id: Int64
303        let author: String
304        let body: String
305        let createdAt: Date
306
307        enum CodingKeys: String, CodingKey {
308            case id, author, body
309            case createdAt = "created_at"
310        }
311    }
312}