Commit 96bfba0e47
96bfba0e47ae0242f87f2e839589ef7303d2230e
parent: 63a1edda14
Unsigned
cmc <hello@cleberg.net> · 2026-09-07 17:12 UTC
Remove a release asset (!67)
Swipe on an asset row, confirm, and the app dispatches release asset remove and reloads the release. No live-suite step: the app cannot add an asset, so there is no fixture to remove.
Layout: unified · split
gitbay/Releases/ReleaseViewModels.swift
+15
| @@ -100,6 +100,21 @@ final class ReleaseDetailViewModel { |
| 100 | 100 | } |
| 101 | 101 | } |
| 102 | 102 | |
| 103 | /// `release asset remove <owner/name> <tag> <filename>`. |
| 104 | func removeAsset(named name: String) async { |
| 105 | working = true |
| 106 | actionError = nil |
| 107 | defer { working = false } |
| 108 | do { |
| 109 | try await client.run(["release", "asset", "remove", repoPath, tag, name]) |
| 110 | await load() |
| 111 | } catch let error as GitbayError { |
| 112 | actionError = error.userFacingMessage |
| 113 | } catch { |
| 114 | actionError = GitbayError.transport(error).userFacingMessage |
| 115 | } |
| 116 | } |
| 117 | |
| 103 | 118 | /// `release delete <owner/name> <tag> --yes` — `--yes` is required by |
| 104 | 119 | /// the command, not optional; omitting it is a usage error. |
| 105 | 120 | func delete() async -> Bool { |
gitbay/Views/Releases/ReleaseView.swift
+23
| @@ -7,6 +7,7 @@ struct ReleaseView: View { |
| 7 | 7 | @State private var draftTitle = "" |
| 8 | 8 | @State private var draftNotes = "" |
| 9 | 9 | @State private var confirmingDelete = false |
| 10 | @State private var removingAsset: Release.Asset? |
| 10 | 11 | @Environment(\.dismiss) private var dismiss |
| 11 | 12 | |
| 12 | 13 | init(client: GitbayClient, repo: String, tag: String) { |
| @@ -71,6 +72,12 @@ struct ReleaseView: View { |
| 71 | 72 | .foregroundStyle(.secondary) |
| 72 | 73 | } |
| 73 | 74 | } |
| 75 | .swipeActions { |
| 76 | Button("Remove", role: .destructive) { |
| 77 | removingAsset = asset |
| 78 | } |
| 79 | .disabled(model.working) |
| 80 | } |
| 74 | 81 | } |
| 75 | 82 | } |
| 76 | 83 | } |
| @@ -117,6 +124,22 @@ struct ReleaseView: View { |
| 117 | 124 | } message: { |
| 118 | 125 | Text("This deletes the release's assets along with it.") |
| 119 | 126 | } |
| 127 | .confirmationDialog( |
| 128 | "Remove \(removingAsset?.name ?? "") from \(model.tag)?", |
| 129 | isPresented: Binding( |
| 130 | get: { removingAsset != nil }, |
| 131 | set: { if !$0 { removingAsset = nil } } |
| 132 | ), |
| 133 | titleVisibility: .visible |
| 134 | ) { |
| 135 | Button("Remove", role: .destructive) { |
| 136 | if let asset = removingAsset { |
| 137 | Task { await model.removeAsset(named: asset.name) } |
| 138 | } |
| 139 | removingAsset = nil |
| 140 | } |
| 141 | Button("Cancel", role: .cancel) {} |
| 142 | } |
| 120 | 143 | .sheet(isPresented: $editing) { |
| 121 | 144 | ComposeSheet( |
| 122 | 145 | heading: "Edit \(model.tag)", |
gitbayTests/ReleaseTests.swift
+29
| @@ -117,6 +117,35 @@ struct ReleaseDetailViewModelTests { |
| 117 | 117 | #expect(stdin == "new notes") |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | @Test func removeAssetSendsTheCommandAndReloads() async throws { |
| 121 | let (client, stub) = try makeClient() |
| 122 | stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show")) |
| 123 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |
| 124 | await model.load() |
| 125 | stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd")) |
| 126 | stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show")) |
| 127 | |
| 128 | await model.removeAsset(named: "SHA256SUMS") |
| 129 | |
| 130 | let (argv, _) = try argvOf(try #require(stub.seen.first { $0.method == "POST" })) |
| 131 | #expect(argv == ["release", "asset", "remove", "krz/gitbay", "v1.0.0", "SHA256SUMS"]) |
| 132 | #expect(stub.seen.filter { $0.method == "GET" }.count == 2) |
| 133 | #expect(model.actionError == nil) |
| 134 | } |
| 135 | |
| 136 | @Test func aRefusedAssetRemovalSurfaces() async throws { |
| 137 | let (client, stub) = try makeClient() |
| 138 | stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show")) |
| 139 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |
| 140 | await model.load() |
| 141 | stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"write access required","exit_code":4}"#, match: "cmd")) |
| 142 | |
| 143 | await model.removeAsset(named: "SHA256SUMS") |
| 144 | |
| 145 | #expect(model.actionError == "write access required") |
| 146 | #expect(model.state.value?.assets?.count == 1) |
| 147 | } |
| 148 | |
| 120 | 149 | @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws { |
| 121 | 150 | let (client, _) = try makeClient() |
| 122 | 151 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |