diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index fdacf7b..ce3525a 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 = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5; + MARKETING_VERSION = 2.6; 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 = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5; + MARKETING_VERSION = 2.6; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Detail/PRDetailViewModel.swift b/GiteaPR/Detail/PRDetailViewModel.swift index 8fb605e..186293b 100644 --- a/GiteaPR/Detail/PRDetailViewModel.swift +++ b/GiteaPR/Detail/PRDetailViewModel.swift @@ -144,6 +144,23 @@ final class PRDetailViewModel: ObservableObject { pr = try? await client.pullRequest(owner: owner, repo: repo.name, number: prNumber) return true + } catch APIError.mergeConflict { + // Branch is behind main (or conflicts). Update it and retry once. + busyMessage = "Updating branch…" + do { + try await client.updateBranch(owner: owner, repo: repo.name, + number: prNumber) + busyMessage = "Merging…" + try await client.merge(owner: owner, repo: repo.name, + number: prNumber, method: method, + deleteBranchAfterMerge: SettingsStore.shared.deleteBranchAfterMerge) + pr = try? await client.pullRequest(owner: owner, repo: repo.name, + number: prNumber) + return true + } catch { + errorMessage = error.localizedDescription + return false + } } catch { errorMessage = error.localizedDescription return false diff --git a/GiteaPR/Networking/APIError.swift b/GiteaPR/Networking/APIError.swift index 873ba53..56b3b0c 100644 --- a/GiteaPR/Networking/APIError.swift +++ b/GiteaPR/Networking/APIError.swift @@ -7,6 +7,7 @@ enum APIError: LocalizedError, Equatable { case decoding(String) case unauthorized case notFound + case mergeConflict var errorDescription: String? { switch self { @@ -18,6 +19,9 @@ enum APIError: LocalizedError, Equatable { if code == 401 || code == 403 { return "Authentication failed. Check your token." } + if code == 409 { + return "This branch is out of date or conflicts with the base branch. Update the branch and try again." + } return message?.isEmpty == false ? "Server error (\(code)): \(message!)" : "Server error (\(code))." @@ -27,6 +31,8 @@ enum APIError: LocalizedError, Equatable { return "Authentication failed. Check your token." case .notFound: return "Not found." + case .mergeConflict: + return "This branch is out of date or conflicts with the base branch. Update the branch and try again." } } } diff --git a/GiteaPR/Networking/GiteaClient.swift b/GiteaPR/Networking/GiteaClient.swift index 715fe19..0eb5199 100644 --- a/GiteaPR/Networking/GiteaClient.swift +++ b/GiteaPR/Networking/GiteaClient.swift @@ -93,6 +93,9 @@ struct GiteaClient { case 404: await LogStore.shared.log(.error, "\(method) \(path) → 404 not found") throw APIError.notFound + case 409: + await LogStore.shared.log(.warn, "\(method) \(path) → 409 conflict") + throw APIError.mergeConflict default: let message = (try? Self.decoder.decode([String: String].self, from: data))?["message"] @@ -230,6 +233,14 @@ struct GiteaClient { method: "DELETE") } + /// Merge the base branch into the head branch (`POST /pulls/{index}/update`). + /// When a PR is behind `main` (e.g. conflicts), the merge returns 409; this + /// rebases/syncs the head onto the base so it can then be merged. + func updateBranch(owner: String, repo: String, number: Int) async throws { + _ = try await request("repos/\(owner)/\(repo)/pulls/\(number)/update", + method: "POST") + } + func setState(owner: String, repo: String, number: Int, state: String) async throws { struct Body: Encodable { let state: String }