diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 0b28ea6..df10e9b 100644 --- a/GiteaPR.xcodeproj/project.pbxproj +++ b/GiteaPR.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -211,7 +211,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Detail/PRDetailView.swift b/GiteaPR/Detail/PRDetailView.swift index 6c519b4..22426a7 100644 --- a/GiteaPR/Detail/PRDetailView.swift +++ b/GiteaPR/Detail/PRDetailView.swift @@ -131,7 +131,9 @@ struct PRDetailView: View { .background(Color(.secondarySystemBackground)) } - /// Bottom tab bar: icon-only navigation between the PR's sections. + /// Bottom tab bar: icon-only navigation between the PR's sections, in a + /// floating capsule style (like the latest iOS tab bars) rather than a + /// full-width bar. private var bottomTabBar: some View { HStack { ForEach(DetailTab.allCases) { tab in @@ -149,8 +151,15 @@ struct PRDetailView: View { } } } - .padding(.vertical, 6) - .background(.bar) + .padding(.vertical, 8) + .padding(.horizontal, 6) + .background( + Capsule() + .fill(.regularMaterial) + .shadow(color: .black.opacity(0.15), radius: 8, y: 2) + ) + .padding(.horizontal, 16) + .padding(.bottom, 8) } private var reviewMenu: some View { @@ -173,7 +182,7 @@ struct PRDetailView: View { Button { showMergeSheet = true } label: { - Image(systemName: "arrow.down.circle") + Image(systemName: "arrow.triangle.merge") } .disabled(vm.ciStatus == .failure && SettingsStore.shared.mergeGateMode == .block) diff --git a/GiteaPR/Detail/PRDetailViewModel.swift b/GiteaPR/Detail/PRDetailViewModel.swift index 1c69a55..8fb605e 100644 --- a/GiteaPR/Detail/PRDetailViewModel.swift +++ b/GiteaPR/Detail/PRDetailViewModel.swift @@ -130,8 +130,16 @@ final class PRDetailViewModel: ObservableObject { busyMessage = "Merging…" defer { busyMessage = nil } do { + let deleteBranch = SettingsStore.shared.deleteBranchAfterMerge try await client.merge(owner: owner, repo: repo.name, - number: prNumber, method: method) + number: prNumber, method: method, + deleteBranchAfterMerge: deleteBranch) + // If the setting is on, delete the head branch after a successful + // merge (best-effort; the branch may already be gone or protected). + if deleteBranch, let head = pr?.head?.ref { + try? await client.deleteBranch(owner: owner, repo: repo.name, + branch: head) + } // Refresh the PR state to reflect the merge. pr = try? await client.pullRequest(owner: owner, repo: repo.name, number: prNumber) diff --git a/GiteaPR/Networking/GiteaClient.swift b/GiteaPR/Networking/GiteaClient.swift index ce71044..715fe19 100644 --- a/GiteaPR/Networking/GiteaClient.swift +++ b/GiteaPR/Networking/GiteaClient.swift @@ -211,12 +211,23 @@ struct GiteaClient { } func merge(owner: String, repo: String, number: Int, - method: String) async throws { - struct Body: Encodable { let Do: String } // 'Do' = merge method: merge|rebase|squash + method: String, deleteBranchAfterMerge: Bool) async throws { + struct Body: Encodable { + let Do: String // merge|rebase|squash + let delete_branch_after_merge: Bool + } _ = try await request( "repos/\(owner)/\(repo)/pulls/\(number)/merge", method: "POST", - body: try JSONEncoder().encode(Body(Do: method))) + body: try JSONEncoder().encode(Body(Do: method, + delete_branch_after_merge: deleteBranchAfterMerge))) + } + + /// Delete a branch (used after merging when the setting is enabled). + func deleteBranch(owner: String, repo: String, branch: String) async throws { + let encoded = branch.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? branch + _ = try await request("repos/\(owner)/\(repo)/branches/\(encoded)", + method: "DELETE") } func setState(owner: String, repo: String, number: Int, diff --git a/GiteaPR/Settings/SettingsStore.swift b/GiteaPR/Settings/SettingsStore.swift index e794679..2d20746 100644 --- a/GiteaPR/Settings/SettingsStore.swift +++ b/GiteaPR/Settings/SettingsStore.swift @@ -23,12 +23,17 @@ final class SettingsStore: ObservableObject { didSet { UserDefaults.standard.set(mergeGateMode.rawValue, forKey: "mergeGateMode") } } + @Published var deleteBranchAfterMerge: Bool { + didSet { UserDefaults.standard.set(deleteBranchAfterMerge, + forKey: "deleteBranchAfterMerge") } + } private init() { instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu" token = KeychainHelper.get("token") ?? "" let raw = UserDefaults.standard.string(forKey: "mergeGateMode") mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn + deleteBranchAfterMerge = UserDefaults.standard.bool(forKey: "deleteBranchAfterMerge") } var isConfigured: Bool { diff --git a/GiteaPR/Settings/SettingsView.swift b/GiteaPR/Settings/SettingsView.swift index ae385c2..396b479 100644 --- a/GiteaPR/Settings/SettingsView.swift +++ b/GiteaPR/Settings/SettingsView.swift @@ -5,6 +5,7 @@ struct SettingsView: View { @State private var instanceURL: String @State private var token: String @State private var mergeGateMode: MergeGateMode + @State private var deleteBranchAfterMerge: Bool @State private var testResult: TestResult? @State private var testing = false @State private var showLogoutConfirm = false @@ -18,6 +19,7 @@ struct SettingsView: View { _instanceURL = State(initialValue: SettingsStore.shared.instanceURL) _token = State(initialValue: SettingsStore.shared.token) _mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode) + _deleteBranchAfterMerge = State(initialValue: SettingsStore.shared.deleteBranchAfterMerge) } var body: some View { @@ -49,6 +51,7 @@ struct SettingsView: View { Text("Warn mode shows CI status and allows merging a red PR after confirmation. Block mode disables merge while any check is failing.") .font(.caption) .foregroundColor(.secondary) + Toggle("Delete branch after merge", isOn: $deleteBranchAfterMerge) } Section("Diagnostics") { @@ -130,6 +133,7 @@ struct SettingsView: View { instanceURL.trimmingCharacters(in: .whitespacesAndNewlines) SettingsStore.shared.token = token SettingsStore.shared.mergeGateMode = mergeGateMode + SettingsStore.shared.deleteBranchAfterMerge = deleteBranchAfterMerge dismiss() } }