Skip to content

Commit 6644cb6

Browse files
feat(worktree): run user-configured post-create command
Adds a Settings field for a shell command that runs after worktree creation with $WORKTREE_DIRECTORY set to the new path. Useful for e.g. `mise trust $WORKTREE_DIRECTORY`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f117437 commit 6644cb6

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

Sources/NeetlyApp/GitWorktree.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ class GitWorktree {
198198
// If worktree already exists, just use it
199199
if FileManager.default.fileExists(atPath: worktreePath) {
200200
NSLog("GitWorktree: worktree already exists at \(worktreePath)")
201+
runPostCreateCommand(at: worktreePath)
201202
return .success(path: worktreePath)
202203
}
203204

@@ -242,6 +243,7 @@ class GitWorktree {
242243
NSLog("GitWorktree: \(cmd1)\(result1)")
243244

244245
if result1.success {
246+
runPostCreateCommand(at: worktreePath)
245247
return .success(path: worktreePath)
246248
}
247249

@@ -251,12 +253,23 @@ class GitWorktree {
251253
NSLog("GitWorktree: \(cmd2)\(result2)")
252254

253255
if result2.success {
256+
runPostCreateCommand(at: worktreePath)
254257
return .success(path: worktreePath)
255258
}
256259

257260
return .failure(message: "Failed: \(result1.output) / \(result2.output)")
258261
}
259262

263+
/// Run the user-configured post-create command (Settings → Post-Create
264+
/// Command) with `$WORKTREE_DIRECTORY` set to the new worktree's path.
265+
/// Best-effort: failures log but don't block. No-op when unconfigured.
266+
private func runPostCreateCommand(at path: String) {
267+
let cmd = NeetlySettings.shared.postWorktreeCreateCommand.trimmingCharacters(in: .whitespacesAndNewlines)
268+
guard !cmd.isEmpty else { return }
269+
let result = shell(cmd, in: repoPath, extraEnv: ["WORKTREE_DIRECTORY": path])
270+
NSLog("GitWorktree: post-create command → \(result)")
271+
}
272+
260273
private func detectDefaultBranch() -> String {
261274
let result = shell("git symbolic-ref refs/remotes/origin/HEAD", in: repoPath)
262275
if result.success {
@@ -279,11 +292,15 @@ class GitWorktree {
279292
}
280293

281294
/// Run a shell command via /bin/zsh -c to get proper PATH and env.
282-
private func shell(_ command: String, in directory: String) -> ShellResult {
295+
/// Pass `extraEnv` to add or override environment variables for the child.
296+
private func shell(_ command: String, in directory: String, extraEnv: [String: String]? = nil) -> ShellResult {
283297
let process = Process()
284298
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
285299
process.arguments = ["-l", "-c", command]
286300
process.currentDirectoryURL = URL(fileURLWithPath: directory)
301+
if let extraEnv {
302+
process.environment = ProcessInfo.processInfo.environment.merging(extraEnv) { _, new in new }
303+
}
287304

288305
let outPipe = Pipe()
289306
let errPipe = Pipe()

Sources/NeetlyApp/NeetlySettings.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class NeetlySettings {
1515
private struct Settings: Codable {
1616
var worktreeBaseDir: String
1717
var diffCommand: String?
18+
/// Shell command to run after a worktree is created. The variable
19+
/// `$WORKTREE_DIRECTORY` is set to the new worktree's absolute path.
20+
var postWorktreeCreateCommand: String?
1821
}
1922

2023
static var defaultWorktreeBaseDir: String {
@@ -43,6 +46,16 @@ class NeetlySettings {
4346
save(s)
4447
}
4548

49+
var postWorktreeCreateCommand: String {
50+
load().postWorktreeCreateCommand ?? ""
51+
}
52+
53+
func setPostWorktreeCreateCommand(_ command: String) {
54+
var s = load()
55+
s.postWorktreeCreateCommand = command
56+
save(s)
57+
}
58+
4659
private func load() -> Settings {
4760
guard let data = try? Data(contentsOf: settingsFile),
4861
let settings = try? JSONDecoder().decode(Settings.self, from: data) else {

Sources/NeetlyApp/SetupView.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ struct DeleteWorktreeSheet: View {
768768
struct SettingsScreen: View {
769769
@State private var worktreeDir: String = NeetlySettings.shared.worktreeBaseDir
770770
@State private var diffCommand: String = NeetlySettings.shared.diffCommand
771+
@State private var postCreateCommand: String = NeetlySettings.shared.postWorktreeCreateCommand
771772
@State private var message: String?
772773
@State private var messageIsError: Bool = false
773774
var onBack: () -> Void
@@ -811,6 +812,18 @@ struct SettingsScreen: View {
811812
}
812813
}
813814

815+
// Post-create command
816+
VStack(alignment: .leading, spacing: 8) {
817+
Text("Post-Create Command")
818+
.font(.system(size: 16, weight: .medium))
819+
Text("Runs after a new worktree is created. Use $WORKTREE_DIRECTORY for the worktree's absolute path. Leave blank to skip. A common use case is running mise trust $WORKTREE_DIRECTORY for the folks who use mise to manage their Ruby versions.")
820+
.font(.system(size: 13))
821+
.foregroundColor(.secondary)
822+
TextField("e.g. mise trust $WORKTREE_DIRECTORY", text: $postCreateCommand)
823+
.textFieldStyle(.roundedBorder)
824+
.font(.system(size: 15, design: .monospaced))
825+
}
826+
814827
Divider()
815828

816829
// Cmd+D: Open Diff
@@ -901,6 +914,10 @@ struct SettingsScreen: View {
901914
let cmd = diffCommand.trimmingCharacters(in: .whitespaces)
902915
NeetlySettings.shared.setDiffCommand(cmd.isEmpty ? NeetlySettings.defaultDiffCommand : cmd)
903916

917+
NeetlySettings.shared.setPostWorktreeCreateCommand(
918+
postCreateCommand.trimmingCharacters(in: .whitespacesAndNewlines)
919+
)
920+
904921
message = "Settings saved."
905922
messageIsError = false
906923
} else {

0 commit comments

Comments
 (0)