Commit c88e4da270
Verified · cmc
Layout: unified · split
gitbay/Releases/ReleaseViewModels.swift +14 −8
| @@ -43,9 +43,10 @@ final class ReleaseListViewModel { | ||
| 43 | 43 | } |
| 44 | 44 | |
| 45 | 45 | /// `release notes <owner/name> <tag>`: one line per merge request |
| 46 | /// landed since the previous release, appended to `notes` the way the | |
| 47 | /// web's Summarize changes appends them. The lines are the CLI's. | |
| 48 | func summarize(tag: String, into notes: String) async -> String { | |
| 46 | /// landed since the previous release, in the CLI's form. nil when | |
| 47 | /// there are none or the server refused, with `summaryNote` saying | |
| 48 | /// which. | |
| 49 | func summary(tag: String) async -> String? { | |
| 49 | 50 | summaryNote = nil |
| 50 | 51 | working = true |
| 51 | 52 | defer { working = false } |
| @@ -55,17 +56,22 @@ final class ReleaseListViewModel { | ||
| 55 | 56 | summaryNote = summary.since.isEmpty |
| 56 | 57 | ? "No merge requests landed on \(tag)." |
| 57 | 58 | : "No merge requests landed since \(summary.since)." |
| 58 | return notes | |
| 59 | return nil | |
| 59 | 60 | } |
| 60 | let lines = summary.items.map { "- \($0.title) (!\($0.number)) by @\($0.author)\n" }.joined() | |
| 61 | let typed = notes.trimmingCharacters(in: .whitespacesAndNewlines) | |
| 62 | return typed.isEmpty ? lines : typed + "\n\n" + lines | |
| 61 | return summary.items.map { "- \($0.title) (!\($0.number)) by @\($0.author)\n" }.joined() | |
| 63 | 62 | } catch let error as GitbayError { |
| 64 | 63 | summaryNote = error.userFacingMessage |
| 65 | 64 | } catch { |
| 66 | 65 | summaryNote = GitbayError.transport(error).userFacingMessage |
| 67 | 66 | } |
| 68 | return notes | |
| 67 | return nil | |
| 68 | } | |
| 69 | ||
| 70 | /// The notes with `lines` after them, as the web's Summarize changes | |
| 71 | /// appends them. | |
| 72 | nonisolated static func appending(_ lines: String, to notes: String) -> String { | |
| 73 | let typed = notes.trimmingCharacters(in: .whitespacesAndNewlines) | |
| 74 | return typed.isEmpty ? lines : typed + "\n\n" + lines | |
| 69 | 75 | } |
| 70 | 76 | |
| 71 | 77 | /// `release create <owner/name> <tag> [--title <t>] [--file -]` — |
gitbay/Views/Releases/ReleaseListView.swift +5 −2
| @@ -111,8 +111,11 @@ private struct ReleaseCreateSheet: View { | ||
| 111 | 111 | .autocorrectionDisabled() |
| 112 | 112 | Button("Summarize changes") { |
| 113 | 113 | Task { |
| 114 | notes = await model.summarize( | |
| 115 | tag: tag.trimmingCharacters(in: .whitespaces), into: notes) | |
| 114 | // Appended to the notes as they are when the lines | |
| 115 | // arrive, so typing meanwhile is kept. | |
| 116 | if let lines = await model.summary(tag: tag.trimmingCharacters(in: .whitespaces)) { | |
| 117 | notes = ReleaseListViewModel.appending(lines, to: notes) | |
| 118 | } | |
| 116 | 119 | } |
| 117 | 120 | } |
| 118 | 121 | .disabled(tag.trimmingCharacters(in: .whitespaces).isEmpty || model.working) |
gitbayTests/ReleaseTests.swift +9 −11
| @@ -173,7 +173,7 @@ struct FeedReleaseDestinationTests { | ||
| 173 | 173 | @MainActor |
| 174 | 174 | struct ReleaseSummaryTests { |
| 175 | 175 | |
| 176 | @Test func summarizingAppendsOneLinePerMergeRequestAfterWhatWasTyped() async throws { | |
| 176 | @Test func theSummaryIsOneLinePerMergeRequestAppendedAfterWhatWasTyped() async throws { | |
| 177 | 177 | let (client, stub) = try makeClient() |
| 178 | 178 | stub.enqueue(.init(status: 200, json: """ |
| 179 | 179 | {"protocol_version":1,"data":{"since":"v1.0.0","items":[\ |
| @@ -182,32 +182,30 @@ struct ReleaseSummaryTests { | ||
| 182 | 182 | """)) |
| 183 | 183 | let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay") |
| 184 | 184 | |
| 185 | let notes = await model.summarize(tag: "v1.1.0", into: "Second cut.") | |
| 185 | let lines = try #require(await model.summary(tag: "v1.1.0")) | |
| 186 | 186 | |
| 187 | #expect(notes == "Second cut.\n\n- Add a thing (!12) by @cmc\n- Fix it (!14) by @dee\n") | |
| 187 | #expect(ReleaseListViewModel.appending(lines, to: "Second cut.\n") | |
| 188 | == "Second cut.\n\n- Add a thing (!12) by @cmc\n- Fix it (!14) by @dee\n") | |
| 189 | #expect(ReleaseListViewModel.appending(lines, to: " ") == lines) | |
| 188 | 190 | #expect(stub.seen.first?.url.query() == "argv=release&argv=notes&argv=krz/gitbay&argv=v1.1.0") |
| 189 | 191 | #expect(model.summaryNote == nil) |
| 190 | 192 | } |
| 191 | 193 | |
| 192 | @Test func anEmptyRangeLeavesTheNotesAndSaysWhy() async throws { | |
| 194 | @Test func anEmptyRangeGivesNoLinesAndSaysWhy() async throws { | |
| 193 | 195 | let (client, stub) = try makeClient() |
| 194 | 196 | stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"since":"v1.0.0","items":[]},"exit_code":0}"#)) |
| 195 | 197 | let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay") |
| 196 | 198 | |
| 197 | let notes = await model.summarize(tag: "v1.1.0", into: "typed") | |
| 198 | ||
| 199 | #expect(notes == "typed") | |
| 199 | #expect(await model.summary(tag: "v1.1.0") == nil) | |
| 200 | 200 | #expect(model.summaryNote == "No merge requests landed since v1.0.0.") |
| 201 | 201 | } |
| 202 | 202 | |
| 203 | @Test func aRefusalLeavesTheNotesAndShowsIt() async throws { | |
| 203 | @Test func aRefusalGivesNoLinesAndShowsIt() async throws { | |
| 204 | 204 | let (client, stub) = try makeClient() |
| 205 | 205 | stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no ref \"v9\" in krz/gitbay","exit_code":3}"#)) |
| 206 | 206 | let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay") |
| 207 | 207 | |
| 208 | let notes = await model.summarize(tag: "v9", into: "") | |
| 209 | ||
| 210 | #expect(notes == "") | |
| 208 | #expect(await model.summary(tag: "v9") == nil) | |
| 211 | 209 | #expect(model.summaryNote == #"no ref "v9" in krz/gitbay"#) |
| 212 | 210 | } |
| 213 | 211 | } |