gitbay/Builds/BuildDetailViewModel.swift
72 lines · 2298 bytes
1import Foundation
2import Observation
3
4nonisolated struct BuildDetail: Sendable {
5 let build: Build
6 let log: String
7}
8
9/// One build: `build show` for what it was, `build log` for what it did.
10/// The web's build page shows both; the log alone leaves the screen
11/// unable to say whether the build passed.
12@Observable
13@MainActor
14final class BuildDetailViewModel {
15
16 private(set) var state: LoadState<BuildDetail> = .loading
17 private(set) var actionError: String?
18 private(set) var working = false
19
20 private let client: GitbayClient
21 let repoPath: String
22 let number: Int64
23
24 init(client: GitbayClient, repoPath: String, number: Int64) {
25 self.client = client
26 self.repoPath = repoPath
27 self.number = number
28 }
29
30 func load() async {
31 let ref = [repoPath, String(number)]
32 do {
33 async let build = client.read(["build", "show"] + ref, as: Build.self)
34 // A queued build has no log yet; that is not a failure.
35 async let log = client.readText(["build", "log"] + ref)
36 state = .loaded(BuildDetail(build: try await build, log: (try? await log) ?? ""))
37 } catch {
38 state = .from(error)
39 }
40 }
41
42 /// Only `pending` or `running` builds can be cancelled server-side
43 /// (`internal/control/build.go:750`). The refusal message there says
44 /// "queued", but the wire status is always "pending" — matching on
45 /// "queued" would hide this action forever with no error.
46 static func isCancellable(status: String) -> Bool {
47 status == "pending" || status == "running"
48 }
49
50 var isCancellable: Bool {
51 guard let build = state.value?.build else { return false }
52 return Self.isCancellable(status: build.status)
53 }
54
55 func cancel() async {
56 await perform(["build", "cancel", repoPath, String(number)])
57 }
58
59 private func perform(_ argv: [String]) async {
60 working = true
61 actionError = nil
62 defer { working = false }
63 do {
64 try await client.run(argv)
65 await load()
66 } catch let error as GitbayError {
67 actionError = error.userFacingMessage
68 } catch {
69 actionError = GitbayError.transport(error).userFacingMessage
70 }
71 }
72}