Commit 250c56ddb3

250c56ddb3c79770a96c3ea23dfa9679807ce98a

parent: bebda41def

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-16 03:33 UTC

Org delete and prune: a refusal in an alert, the profile follows, a pruned head is not offered again

A refused organization delete is shown as an alert where the button is, as repository delete does; the org's profile reloads after a delete and a deleted org's screen does not reload. After a prune the menu no longer offers it.

Ref #47
Ref #48

Layout: unified · split

gitbay/MRs/MRDetailViewModel.swift +2
@@ -151,6 +151,8 @@ final class MRDetailViewModel {
151151 /// kept. The diff is gone afterwards; the thread records who pruned it.
152152 func pruneHead() async {
153153 await perform(["admin", "mr", "prune", repoPath, String(number), "--yes"])
154 // Done: another prune would only add another system comment.
155 if actionError == nil { canPruneHead = false }
154156 }
155157
156158 func cancelQueuedMerge() async {
gitbay/Views/Discovery/ProfileView.swift +4
@@ -80,6 +80,10 @@ struct ProfileView: View {
8080 }
8181 .task { await model.load() }
8282 .refreshable { await model.load() }
83 // The organization screen, pushed from an org's profile, may delete it.
84 .onReceive(NotificationCenter.default.publisher(for: .gitbayOrganizationsChanged)) { note in
85 if note.object as? String == model.name { Task { await model.load() } }
86 }
8387 .sheet(isPresented: $editingProfile) {
8488 if let profile = model.state.value {
8589 ProfileEditSheet(model: model, profile: profile)
gitbay/Views/Orgs/OrgView.swift +17 −4
@@ -16,6 +16,9 @@ struct OrgView: View {
1616 @State private var deleteConfirm = ""
1717 /// Set once the organization is deleted: nothing on this screen applies.
1818 @State private var deleted = false
19 /// A delete refused, shown where the button is rather than in the
20 /// banner at the top of a list scrolled past it.
21 @State private var refusal: String?
1922
2023 init(client: GitbayClient, org: String) {
2124 _model = State(initialValue: OrgViewModel(client: client, orgName: org))
@@ -60,13 +63,23 @@ struct OrgView: View {
6063 }
6164 }
6265 .task { await model.load() }
63 .refreshable { await model.load() }
66 .refreshable {
67 guard !deleted else { return }
68 await model.load()
69 }
70 .alert("Not done", isPresented: Binding(get: { refusal != nil }, set: { if !$0 { refusal = nil } })) {
71 Button("OK", role: .cancel) {}
72 } message: {
73 Text(refusal ?? "")
74 }
6475 .alert("Delete \(model.orgName)", isPresented: $deletingOrg) {
6576 TextField("type \(model.orgName) to confirm", text: $deleteConfirm)
6677 .autocorrectionDisabled()
6778 .textInputAutocapitalization(.never)
6879 Button("Delete", role: .destructive) {
69 Task { deleted = await model.delete() }
80 Task {
81 if await model.delete() { deleted = true } else { refusal = model.actionError }
82 }
7083 }
7184 .disabled(deleteConfirm != model.orgName)
7285 Button("Cancel", role: .cancel) {}
@@ -164,8 +177,6 @@ struct OrgView: View {
164177 }
165178 }
166179
167 /// Labels and milestones every repository under the org sees
168 /// (v1.19.0), managed here with `org label` and `org milestone`.
169180 private var deleteSection: some View {
170181 Section {
171182 Button(role: .destructive) {
@@ -182,6 +193,8 @@ struct OrgView: View {
182193 }
183194 }
184195
196 /// Labels and milestones every repository under the org sees
197 /// (v1.19.0), managed here with `org label` and `org milestone`.
185198 private var issuesSection: some View {
186199 Section {
187200 NavigationLink(value: OrgRoute.labels(model.orgName)) {
gitbayTests/OrgDeleteMRPruneTests.swift +6
@@ -71,14 +71,20 @@ struct MRPruneTests {
7171
7272 @Test func pruneNamesTheMergeRequestAndConfirms() async throws {
7373 let (client, stub) = try makeClient()
74 stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show"))
75 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"user":"cmc","admin":true},"exit_code":0}"#, match: "argv=whoami"))
7476 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[{"number":7,"head":"abc123"}],"exit_code":0}"#, match: "cmd"))
7577 stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show"))
7678 let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
79 await model.load()
80 await model.checkCanPruneHead()
81 #expect(model.canPruneHead, "an admin on a merged merge request is offered the prune")
7782
7883 await model.pruneHead()
7984
8085 #expect(try writes(stub) == [["admin", "mr", "prune", "krz/gitbay", "7", "--yes"]])
8186 #expect(model.actionError == nil)
87 #expect(model.canPruneHead == false, "a pruned head is not offered again")
8288 }
8389
8490 @Test func aStillMergeableHeadIsRefusedWithTheReason() async throws {