Commit 174b677412

174b6774123c61271bd6d73415da98622f901d8d

parent: e27fa2c83a

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-14 06:04 UTC

Build log tail: a fetch from the other mode is not shown

A poll that started before the mode changed would land after it and draw
the whole log under the tail's note.

Ref #27

Layout: unified · split

gitbay/Builds/BuildDetailViewModel.swift +8 −2
@@ -5,12 +5,15 @@ nonisolated struct BuildDetail: Sendable {
5 let build: Build 5 let build: Build
6 let log: String 6 let log: String
7 let lines: BuildLog 7 let lines: BuildLog
8 /// Fetched with `--tail`.
9 let tail: Bool
8 10
9 /// `tail` is a log's last lines only, which may start inside a step 11 /// `tail` is a log's last lines only, which may start inside a step
10 /// and cut its marker off, so it is drawn as one block. 12 /// and cut its marker off, so it is drawn as one block.
11 init(build: Build, log: String, tail: Bool = false) { 13 init(build: Build, log: String, tail: Bool = false) {
12 self.build = build 14 self.build = build
13 self.log = log 15 self.log = log
16 self.tail = tail
14 var split = build 17 var split = build
15 if tail { 18 if tail {
16 split.steps = nil 19 split.steps = nil
@@ -49,7 +52,10 @@ final class BuildDetailViewModel {
49 52
50 func load() async { 53 func load() async {
51 do { 54 do {
52 state = .loaded(try await fetch()) 55 let detail = try await fetch()
56 // A fetch from before the mode changed is not this screen's.
57 guard detail.tail == tail else { return }
58 state = .loaded(detail)
53 } catch { 59 } catch {
54 state = .from(error) 60 state = .from(error)
55 } 61 }
@@ -83,7 +89,7 @@ final class BuildDetailViewModel {
83 func poll() async { 89 func poll() async {
84 while let build = state.value?.build, Self.isLive(status: build.status) { 90 while let build = state.value?.build, Self.isLive(status: build.status) {
85 guard (try? await Task.sleep(for: pollInterval)) != nil else { return } 91 guard (try? await Task.sleep(for: pollInterval)) != nil else { return }
86 if let detail = try? await fetch() { 92 if let detail = try? await fetch(), detail.tail == tail {
87 state = .loaded(detail) 93 state = .loaded(detail)
88 } 94 }
89 } 95 }
gitbayTests/BuildLogTests.swift +11
@@ -248,4 +248,15 @@ struct BuildLogTailTests {
248 #expect(stub.seen.last { $0.url.absoluteString.contains("argv=log") }?.url.query()?.contains("tail") == false) 248 #expect(stub.seen.last { $0.url.absoluteString.contains("argv=log") }?.url.query()?.contains("tail") == false)
249 #expect(model.state.value?.lines.stepped == true) 249 #expect(model.state.value?.lines.stepped == true)
250 } 250 }
251
252 @Test func aFetchFromTheOtherModeIsNotShown() async throws {
253 let (client, stub) = try makeClient()
254 stub.enqueue(.init(status: 200, json: buildShowJSON, match: "argv=show"))
255 stub.enqueue(.init(status: 200, json: logEnvelope(failedLog), match: "argv=log"))
256 let model = BuildDetailViewModel(client: client, repoPath: "krz/gitbay", number: 1)
257 await model.load()
258 // The detail records the mode it was fetched in.
259 #expect(model.state.value?.tail == false)
260 #expect(BuildDetail(build: try #require(model.state.value).build, log: "x\n", tail: true).tail)
261 }
251} 262}