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 {
100100 }
101101 }
102102
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
103118 /// `release delete <owner/name> <tag> --yes` — `--yes` is required by
104119 /// the command, not optional; omitting it is a usage error.
105120 func delete() async -> Bool {
gitbay/Views/Releases/ReleaseView.swift +23
@@ -7,6 +7,7 @@ struct ReleaseView: View {
77 @State private var draftTitle = ""
88 @State private var draftNotes = ""
99 @State private var confirmingDelete = false
10 @State private var removingAsset: Release.Asset?
1011 @Environment(\.dismiss) private var dismiss
1112
1213 init(client: GitbayClient, repo: String, tag: String) {
@@ -71,6 +72,12 @@ struct ReleaseView: View {
7172 .foregroundStyle(.secondary)
7273 }
7374 }
75 .swipeActions {
76 Button("Remove", role: .destructive) {
77 removingAsset = asset
78 }
79 .disabled(model.working)
80 }
7481 }
7582 }
7683 }
@@ -117,6 +124,22 @@ struct ReleaseView: View {
117124 } message: {
118125 Text("This deletes the release's assets along with it.")
119126 }
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 }
120143 .sheet(isPresented: $editing) {
121144 ComposeSheet(
122145 heading: "Edit \(model.tag)",
gitbayTests/ReleaseTests.swift +29
@@ -117,6 +117,35 @@ struct ReleaseDetailViewModelTests {
117117 #expect(stdin == "new notes")
118118 }
119119
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
120149 @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws {
121150 let (client, _) = try makeClient()
122151 let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")