Skip to content

Commit 4a258fb

Browse files
author
ScriptedAlchemy
committed
fix(hooks): route linked worktree cwd
1 parent 5de58ec commit 4a258fb

1 file changed

Lines changed: 148 additions & 5 deletions

File tree

src/mcp/hook_events.rs

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,21 @@ fn plan_shell_hook_event(
149149
return HookEventPlan::Noop;
150150
};
151151
let cwd = event.cwd.as_deref().unwrap_or(project_root);
152-
if !crate::hooks::cursor_shell_command_targets_project(command, cwd, project_root) {
152+
let hook_project_root = hook_project_root(cwd, project_root);
153+
if !crate::hooks::cursor_shell_command_targets_project(command, cwd, &hook_project_root) {
153154
return HookEventPlan::Noop;
154155
}
156+
let hook_current_branch;
157+
let current_branch = if paths_same(&hook_project_root, project_root) {
158+
current_branch
159+
} else {
160+
hook_current_branch = crate::branch::current_branch(&hook_project_root);
161+
hook_current_branch.as_deref()
162+
};
155163
match crate::hooks::cursor_shell_sync_plan_with_current_branch(command, current_branch) {
156-
crate::hooks::CursorShellSyncPlan::BranchAdd(branch) => HookEventPlan::AddBranch(branch),
164+
crate::hooks::CursorShellSyncPlan::BranchAdd(branch) => {
165+
branch_plan_for_root(project_root, hook_project_root, branch)
166+
}
157167
crate::hooks::CursorShellSyncPlan::WorktreeBranchAdd {
158168
branch,
159169
worktree_path,
@@ -165,15 +175,59 @@ fn plan_shell_hook_event(
165175
HookEventPlan::DebouncedIncrementalSync(event.agent)
166176
}
167177
crate::hooks::CursorShellSyncPlan::CurrentBranchSync(branch) => {
168-
HookEventPlan::SyncCurrentBranch {
169-
branch,
170-
agent: event.agent,
178+
if paths_same(&hook_project_root, project_root) {
179+
HookEventPlan::SyncCurrentBranch {
180+
branch,
181+
agent: event.agent,
182+
}
183+
} else {
184+
HookEventPlan::AddBranchAt {
185+
root: hook_project_root,
186+
branch,
187+
}
171188
}
172189
}
173190
crate::hooks::CursorShellSyncPlan::Noop => HookEventPlan::Noop,
174191
}
175192
}
176193

194+
fn hook_project_root(cwd: &Path, project_root: &Path) -> PathBuf {
195+
if let Some(root) = crate::config::discover_project_root(cwd) {
196+
return root;
197+
}
198+
let Some(worktree_root) = crate::worktree::git_worktree_root(cwd) else {
199+
return project_root.to_path_buf();
200+
};
201+
let cwd_common = crate::worktree::git_common_dir(&worktree_root);
202+
let project_common = crate::worktree::git_common_dir(project_root);
203+
if cwd_common.is_some() && cwd_common == project_common {
204+
worktree_root
205+
} else {
206+
project_root.to_path_buf()
207+
}
208+
}
209+
210+
fn branch_plan_for_root(
211+
project_root: &Path,
212+
hook_project_root: PathBuf,
213+
branch: String,
214+
) -> HookEventPlan {
215+
if paths_same(&hook_project_root, project_root) {
216+
HookEventPlan::AddBranch(branch)
217+
} else {
218+
HookEventPlan::AddBranchAt {
219+
root: hook_project_root,
220+
branch,
221+
}
222+
}
223+
}
224+
225+
fn paths_same(a: &Path, b: &Path) -> bool {
226+
let a = a.canonicalize().unwrap_or_else(|_| a.to_path_buf());
227+
let b = b.canonicalize().unwrap_or_else(|_| b.to_path_buf());
228+
a == b
229+
}
230+
177231
fn read_marker_secs(path: &Path) -> Option<i64> {
178232
std::fs::read_to_string(path)
179233
.ok()?
@@ -185,6 +239,7 @@ fn read_marker_secs(path: &Path) -> Option<i64> {
185239
#[cfg(test)]
186240
mod tests {
187241
use std::path::Path;
242+
use std::process::Command;
188243

189244
use serde_json::json;
190245

@@ -199,6 +254,52 @@ mod tests {
199254
}
200255
}
201256

257+
fn run_git(cwd: &Path, args: &[&str]) {
258+
let output = Command::new("git")
259+
.args(args)
260+
.current_dir(cwd)
261+
.output()
262+
.unwrap_or_else(|e| panic!("git {args:?} should run: {e}"));
263+
assert!(
264+
output.status.success(),
265+
"git {:?} failed\nstdout:\n{}\nstderr:\n{}",
266+
args,
267+
String::from_utf8_lossy(&output.stdout),
268+
String::from_utf8_lossy(&output.stderr)
269+
);
270+
}
271+
272+
fn setup_linked_session_worktree() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf)
273+
{
274+
let base = tempfile::tempdir().unwrap_or_else(|e| panic!("tempdir should create: {e}"));
275+
let base_root = base
276+
.path()
277+
.canonicalize()
278+
.unwrap_or_else(|e| panic!("tempdir should canonicalize: {e}"));
279+
let project_root = base_root.join("project");
280+
let worktree_root = base_root.join("session-worktree");
281+
std::fs::create_dir_all(project_root.join("src"))
282+
.unwrap_or_else(|e| panic!("project dirs should create: {e}"));
283+
std::fs::write(project_root.join("src/lib.rs"), "pub fn marker() {}\n")
284+
.unwrap_or_else(|e| panic!("source should write: {e}"));
285+
run_git(&project_root, &["init", "-b", "main"]);
286+
run_git(&project_root, &["config", "user.email", "test@test.com"]);
287+
run_git(&project_root, &["config", "user.name", "Test"]);
288+
run_git(&project_root, &["add", "."]);
289+
run_git(&project_root, &["commit", "-m", "initial"]);
290+
run_git(
291+
&project_root,
292+
&[
293+
"worktree",
294+
"add",
295+
worktree_root.to_str().unwrap(),
296+
"-b",
297+
"feature/session",
298+
],
299+
);
300+
(base, project_root, worktree_root)
301+
}
302+
202303
#[test]
203304
fn parses_agent_and_event_kind_from_hook_notification() {
204305
let params = json!({
@@ -359,6 +460,48 @@ mod tests {
359460
);
360461
}
361462

463+
#[test]
464+
fn plans_branch_switch_from_session_worktree_against_worktree_root() {
465+
let (_base, project_root, worktree_root) = setup_linked_session_worktree();
466+
467+
let params = json!({
468+
"agent": "codex",
469+
"event": "postToolUseShell",
470+
"command": "git switch feature/session",
471+
"cwd": worktree_root
472+
});
473+
let event = parse_or_panic(&params);
474+
475+
assert_eq!(
476+
plan_hook_event(&event, &project_root, Some("main")),
477+
HookEventPlan::AddBranchAt {
478+
root: worktree_root,
479+
branch: "feature/session".to_string(),
480+
}
481+
);
482+
}
483+
484+
#[test]
485+
fn plans_ambiguous_git_change_from_session_worktree_with_worktree_branch() {
486+
let (_base, project_root, worktree_root) = setup_linked_session_worktree();
487+
488+
let params = json!({
489+
"agent": "codex",
490+
"event": "postToolUseShell",
491+
"command": "git pull --rebase",
492+
"cwd": worktree_root
493+
});
494+
let event = parse_or_panic(&params);
495+
496+
assert_eq!(
497+
plan_hook_event(&event, &project_root, Some("main")),
498+
HookEventPlan::AddBranchAt {
499+
root: worktree_root,
500+
branch: "feature/session".to_string(),
501+
}
502+
);
503+
}
504+
362505
#[test]
363506
fn plans_workspace_open_as_current_branch_sync() {
364507
let params = json!({

0 commit comments

Comments
 (0)