Merge icon, delete-branch-after-merge setting, floating tab bar; bump to 2.0
Build iOS App / Build .ipa and publish source (push) Successful in 22s

- Merge button: use the proper SF Symbol 'arrow.triangle.merge' instead of
  the download icon 'arrow.down.circle'.
- Add a Settings toggle 'Delete branch after merge'. When on, the merge call
  passes delete_branch_after_merge=true and the head branch is deleted after
  a successful merge (best-effort).
- PR detail bottom tab bar: floating capsule style (regularMaterial + shadow)
  like the latest iOS tab bars, instead of a full-width bar.
- Bump to 2.0.
This commit is contained in:
Hermes committed 2026-08-12 22:37:42 +00:00
1 parent fce0385708
commit 53b3f69d0f
6 files changed
+49 -12

No files matched your search

+4 -4
View File
@@ -187,7 +187,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10; CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
@@ -197,7 +197,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.9; MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -211,7 +211,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10; CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
@@ -221,7 +221,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.9; MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
+13 -4
View File
@@ -131,7 +131,9 @@ struct PRDetailView: View {
.background(Color(.secondarySystemBackground)) .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 { private var bottomTabBar: some View {
HStack { HStack {
ForEach(DetailTab.allCases) { tab in ForEach(DetailTab.allCases) { tab in
@@ -149,8 +151,15 @@ struct PRDetailView: View {
} }
} }
} }
.padding(.vertical, 6) .padding(.vertical, 8)
.background(.bar) .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 { private var reviewMenu: some View {
@@ -173,7 +182,7 @@ struct PRDetailView: View {
Button { Button {
showMergeSheet = true showMergeSheet = true
} label: { } label: {
Image(systemName: "arrow.down.circle") Image(systemName: "arrow.triangle.merge")
} }
.disabled(vm.ciStatus == .failure && .disabled(vm.ciStatus == .failure &&
SettingsStore.shared.mergeGateMode == .block) SettingsStore.shared.mergeGateMode == .block)
+9 -1
View File
@@ -130,8 +130,16 @@ final class PRDetailViewModel: ObservableObject {
busyMessage = "Merging…" busyMessage = "Merging…"
defer { busyMessage = nil } defer { busyMessage = nil }
do { do {
let deleteBranch = SettingsStore.shared.deleteBranchAfterMerge
try await client.merge(owner: owner, repo: repo.name, 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. // Refresh the PR state to reflect the merge.
pr = try? await client.pullRequest(owner: owner, repo: repo.name, pr = try? await client.pullRequest(owner: owner, repo: repo.name,
number: prNumber) number: prNumber)
+14 -3
View File
@@ -211,12 +211,23 @@ struct GiteaClient {
} }
func merge(owner: String, repo: String, number: Int, func merge(owner: String, repo: String, number: Int,
method: String) async throws { method: String, deleteBranchAfterMerge: Bool) async throws {
struct Body: Encodable { let Do: String } // 'Do' = merge method: merge|rebase|squash struct Body: Encodable {
let Do: String // merge|rebase|squash
let delete_branch_after_merge: Bool
}
_ = try await request( _ = try await request(
"repos/\(owner)/\(repo)/pulls/\(number)/merge", "repos/\(owner)/\(repo)/pulls/\(number)/merge",
method: "POST", 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, func setState(owner: String, repo: String, number: Int,
+5
View File
@@ -23,12 +23,17 @@ final class SettingsStore: ObservableObject {
didSet { UserDefaults.standard.set(mergeGateMode.rawValue, didSet { UserDefaults.standard.set(mergeGateMode.rawValue,
forKey: "mergeGateMode") } forKey: "mergeGateMode") }
} }
@Published var deleteBranchAfterMerge: Bool {
didSet { UserDefaults.standard.set(deleteBranchAfterMerge,
forKey: "deleteBranchAfterMerge") }
}
private init() { private init() {
instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu" instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu"
token = KeychainHelper.get("token") ?? "" token = KeychainHelper.get("token") ?? ""
let raw = UserDefaults.standard.string(forKey: "mergeGateMode") let raw = UserDefaults.standard.string(forKey: "mergeGateMode")
mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn
deleteBranchAfterMerge = UserDefaults.standard.bool(forKey: "deleteBranchAfterMerge")
} }
var isConfigured: Bool { var isConfigured: Bool {
+4
View File
@@ -5,6 +5,7 @@ struct SettingsView: View {
@State private var instanceURL: String @State private var instanceURL: String
@State private var token: String @State private var token: String
@State private var mergeGateMode: MergeGateMode @State private var mergeGateMode: MergeGateMode
@State private var deleteBranchAfterMerge: Bool
@State private var testResult: TestResult? @State private var testResult: TestResult?
@State private var testing = false @State private var testing = false
@State private var showLogoutConfirm = false @State private var showLogoutConfirm = false
@@ -18,6 +19,7 @@ struct SettingsView: View {
_instanceURL = State(initialValue: SettingsStore.shared.instanceURL) _instanceURL = State(initialValue: SettingsStore.shared.instanceURL)
_token = State(initialValue: SettingsStore.shared.token) _token = State(initialValue: SettingsStore.shared.token)
_mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode) _mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode)
_deleteBranchAfterMerge = State(initialValue: SettingsStore.shared.deleteBranchAfterMerge)
} }
var body: some View { 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.") 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) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Toggle("Delete branch after merge", isOn: $deleteBranchAfterMerge)
} }
Section("Diagnostics") { Section("Diagnostics") {
@@ -130,6 +133,7 @@ struct SettingsView: View {
instanceURL.trimmingCharacters(in: .whitespacesAndNewlines) instanceURL.trimmingCharacters(in: .whitespacesAndNewlines)
SettingsStore.shared.token = token SettingsStore.shared.token = token
SettingsStore.shared.mergeGateMode = mergeGateMode SettingsStore.shared.mergeGateMode = mergeGateMode
SettingsStore.shared.deleteBranchAfterMerge = deleteBranchAfterMerge
dismiss() dismiss()
} }
} }