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 | /// `release delete <owner/name> <tag> --yes` — `--yes` is required by |
118 | /// `release delete <owner/name> <tag> --yes` — `--yes` is required by |
| 104 | /// the command, not optional; omitting it is a usage error. |
119 | /// the command, not optional; omitting it is a usage error. |
| 105 | func delete() async -> Bool { |
120 | func delete() async -> Bool { |
gitbay/Views/Releases/ReleaseView.swift
+23
| @@ -7,6 +7,7 @@ struct ReleaseView: View { |
| 7 | @State private var draftTitle = "" |
7 | @State private var draftTitle = "" |
| 8 | @State private var draftNotes = "" |
8 | @State private var draftNotes = "" |
| 9 | @State private var confirmingDelete = false |
9 | @State private var confirmingDelete = false |
| |
10 | @State private var removingAsset: Release.Asset? |
| 10 | @Environment(\.dismiss) private var dismiss |
11 | @Environment(\.dismiss) private var dismiss |
| 11 | |
12 | |
| 12 | init(client: GitbayClient, repo: String, tag: String) { |
13 | init(client: GitbayClient, repo: String, tag: String) { |
| @@ -71,6 +72,12 @@ struct ReleaseView: View { |
| 71 | .foregroundStyle(.secondary) |
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 | } message: { |
124 | } message: { |
| 118 | Text("This deletes the release's assets along with it.") |
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 | .sheet(isPresented: $editing) { |
143 | .sheet(isPresented: $editing) { |
| 121 | ComposeSheet( |
144 | ComposeSheet( |
| 122 | heading: "Edit \(model.tag)", |
145 | heading: "Edit \(model.tag)", |
gitbayTests/ReleaseTests.swift
+29
| @@ -117,6 +117,35 @@ struct ReleaseDetailViewModelTests { |
| 117 | #expect(stdin == "new notes") |
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 | @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws { |
149 | @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws { |
| 121 | let (client, _) = try makeClient() |
150 | let (client, _) = try makeClient() |
| 122 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |
151 | let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0") |