Skip to content

Commit 6dadad9

Browse files
committed
feat: configurable merge command with squash + delete-branch default
Merge button now runs a configurable command template from settings (default: `gh pr merge ${number} --squash --delete-branch`). Adds PR Merge section to General settings with a text field for the command, supporting ${number} substitution. Includes reset to default.
1 parent 5587a35 commit 6dadad9

File tree

4 files changed

+58
-6
lines changed

4 files changed

+58
-6
lines changed

Sources/KanbanCode/CardDetailView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,8 @@ struct CardDetailView: View {
11271127
mergeError = nil
11281128
Task {
11291129
let gh = GhCliAdapter()
1130-
let result = try await gh.mergePR(repoRoot: repoRoot, prNumber: pr.number)
1130+
let settings = try await SettingsStore().read()
1131+
let result = try await gh.mergePR(repoRoot: repoRoot, prNumber: pr.number, commandTemplate: settings.github.mergeCommand)
11311132
isMerging = false
11321133
switch result {
11331134
case .success:

Sources/KanbanCode/SettingsView.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ struct GeneralSettingsView: View {
129129
@AppStorage("preferredEditorBundleId") private var editorBundleId: String = "dev.zed.Zed"
130130
@State private var installedEditors: [EditorDiscovery.Editor] = []
131131
@State private var showOnboarding = false
132+
@State private var mergeCommand: String = GitHubSettings.defaultMergeCommand
133+
@State private var mergeSaveTask: Task<Void, Never>?
132134

133135
private let settingsStore = SettingsStore()
134136

@@ -175,6 +177,24 @@ struct GeneralSettingsView: View {
175177
statusRow("Mutagen", available: mutagenAvailable)
176178
}
177179

180+
Section("PR Merge") {
181+
TextField("Merge command", text: $mergeCommand)
182+
.textFieldStyle(.roundedBorder)
183+
.font(.system(.caption, design: .monospaced))
184+
.onChange(of: mergeCommand) { scheduleMergeSave() }
185+
Text("Use ${number} for the PR number. Default: \(GitHubSettings.defaultMergeCommand)")
186+
.font(.caption)
187+
.foregroundStyle(.tertiary)
188+
HStack {
189+
Spacer()
190+
Button("Reset to Default") {
191+
mergeCommand = GitHubSettings.defaultMergeCommand
192+
scheduleMergeSave()
193+
}
194+
.controlSize(.small)
195+
}
196+
}
197+
178198
Section("Settings File") {
179199
HStack {
180200
Text("~/.kanban-code/settings.json")
@@ -201,6 +221,11 @@ struct GeneralSettingsView: View {
201221
.onAppear {
202222
installedEditors = EditorDiscovery.installedEditors()
203223
}
224+
.task {
225+
if let settings = try? await settingsStore.read() {
226+
mergeCommand = settings.github.mergeCommand
227+
}
228+
}
204229
.sheet(isPresented: $showOnboarding) {
205230
OnboardingWizard(
206231
settingsStore: settingsStore,
@@ -212,6 +237,19 @@ struct GeneralSettingsView: View {
212237
}
213238
}
214239

240+
private func scheduleMergeSave() {
241+
mergeSaveTask?.cancel()
242+
mergeSaveTask = Task {
243+
try? await Task.sleep(for: .milliseconds(500))
244+
guard !Task.isCancelled else { return }
245+
do {
246+
var settings = try await settingsStore.read()
247+
settings.github.mergeCommand = mergeCommand.isEmpty ? GitHubSettings.defaultMergeCommand : mergeCommand
248+
try await settingsStore.write(settings)
249+
} catch {}
250+
}
251+
}
252+
215253
private func statusRow(_ name: String, available: Bool) -> some View {
216254
HStack {
217255
Label(name, systemImage: available ? "checkmark.circle.fill" : "minus.circle")

Sources/KanbanCodeCore/Adapters/Git/GhCliAdapter.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,20 @@ public final class GhCliAdapter: PRTrackerPort, @unchecked Sendable {
388388
}
389389
}
390390

391-
/// Merge a PR (respects repo's default merge strategy).
392-
public func mergePR(repoRoot: String, prNumber: Int) async throws -> MergeResult {
391+
/// Merge a PR using the configured merge command template.
392+
/// The template can contain `${number}` which gets replaced with the PR number.
393+
public func mergePR(repoRoot: String, prNumber: Int, commandTemplate: String) async throws -> MergeResult {
394+
let expanded = commandTemplate.replacingOccurrences(of: "${number}", with: "\(prNumber)")
395+
let parts = expanded.components(separatedBy: " ").filter { !$0.isEmpty }
396+
guard parts.count >= 2 else { return .failure("Invalid merge command") }
397+
398+
// Resolve the executable (first part, e.g. "gh")
399+
let executable = ShellCommand.findExecutable(parts[0]) ?? parts[0]
400+
let arguments = Array(parts.dropFirst())
401+
393402
let result = try await ShellCommand.run(
394-
ghPath,
395-
arguments: ["pr", "merge", "\(prNumber)"],
403+
executable,
404+
arguments: arguments,
396405
currentDirectory: repoRoot
397406
)
398407
if result.succeeded {

Sources/KanbanCodeCore/Infrastructure/SettingsStore.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,14 @@ public struct GlobalViewSettings: Codable, Sendable {
8888
public struct GitHubSettings: Codable, Sendable {
8989
public var defaultFilter: String
9090
public var pollIntervalSeconds: Int
91+
public var mergeCommand: String
9192

92-
public init(defaultFilter: String = "assignee:@me is:open", pollIntervalSeconds: Int = 60) {
93+
public static let defaultMergeCommand = "gh pr merge ${number} --squash --delete-branch"
94+
95+
public init(defaultFilter: String = "assignee:@me is:open", pollIntervalSeconds: Int = 60, mergeCommand: String? = nil) {
9396
self.defaultFilter = defaultFilter
9497
self.pollIntervalSeconds = pollIntervalSeconds
98+
self.mergeCommand = mergeCommand ?? Self.defaultMergeCommand
9599
}
96100
}
97101

0 commit comments

Comments
 (0)