diff options
Diffstat (limited to 'src/worktree.rs')
| -rw-r--r-- | src/worktree.rs | 175 |
1 files changed, 108 insertions, 67 deletions
diff --git a/src/worktree.rs b/src/worktree.rs index 0bb091d7..523bc502 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -151,6 +151,23 @@ pub(crate) fn canonical_or_original(path: &Path) -> PathBuf { std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } +fn repository_git_command(repo_root: &Path, trust_repository: bool) -> std::process::Command { + let mut command = crate::noninteractive_process::command("git"); + command.args(repository_git_args(repo_root, trust_repository)); + command +} + +fn repository_git_args(repo_root: &Path, trust_repository: bool) -> Vec<String> { + let mut args = Vec::new(); + if trust_repository { + args.push("-c".to_string()); + args.push(format!("safe.directory={}", repo_root.display())); + } + args.push("-C".to_string()); + args.push(repo_root.display().to_string()); + args +} + pub(crate) fn default_checkout_path(root: &Path, repo_name: &str, branch: &str) -> PathBuf { root.join(repo_name).join(branch_to_path_slug(branch)) } @@ -159,13 +176,10 @@ pub(crate) fn build_worktree_remove_command( repo_root: &Path, path: &Path, force: bool, + trust_repository: bool, ) -> WorktreeCommand { - let mut args = vec![ - "-C".to_string(), - repo_root.display().to_string(), - "worktree".to_string(), - "remove".to_string(), - ]; + let mut args = repository_git_args(repo_root, trust_repository); + args.extend(["worktree".to_string(), "remove".to_string()]); if force { args.push("--force".to_string()); } @@ -197,16 +211,12 @@ pub(crate) fn worktree_dirty_remove_message(path: &Path) -> String { } #[cfg(any(windows, test))] -pub(crate) fn checkout_has_dirty_files(path: &Path) -> Result<bool, String> { - let path_arg = path.display().to_string(); - let output = crate::noninteractive_process::command("git") - .args([ - "-C", - &path_arg, - "status", - "--porcelain", - "--untracked-files=all", - ]) +pub(crate) fn checkout_has_dirty_files( + path: &Path, + trust_repository: bool, +) -> Result<bool, String> { + let output = repository_git_command(path, trust_repository) + .args(["status", "--porcelain", "--untracked-files=all"]) .output() .map_err(|err| err.to_string())?; @@ -230,19 +240,20 @@ pub(crate) fn build_worktree_add_new_branch_command( path: &Path, branch: &str, base: &str, + trust_repository: bool, ) -> WorktreeCommand { + let mut args = repository_git_args(repo_root, trust_repository); + args.extend([ + "worktree".to_string(), + "add".to_string(), + "-b".to_string(), + branch.to_string(), + path.display().to_string(), + base.to_string(), + ]); WorktreeCommand { program: "git".to_string(), - args: vec![ - "-C".to_string(), - repo_root.display().to_string(), - "worktree".to_string(), - "add".to_string(), - "-b".to_string(), - branch.to_string(), - path.display().to_string(), - base.to_string(), - ], + args, } } @@ -250,24 +261,27 @@ pub(crate) fn build_worktree_add_existing_branch_command( repo_root: &Path, path: &Path, branch: &str, + trust_repository: bool, ) -> WorktreeCommand { + let mut args = repository_git_args(repo_root, trust_repository); + args.extend([ + "worktree".to_string(), + "add".to_string(), + path.display().to_string(), + branch.to_string(), + ]); WorktreeCommand { program: "git".to_string(), - args: vec![ - "-C".to_string(), - repo_root.display().to_string(), - "worktree".to_string(), - "add".to_string(), - path.display().to_string(), - branch.to_string(), - ], + args, } } -pub(crate) fn local_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, String> { - let output = crate::noninteractive_process::command("git") - .arg("-C") - .arg(repo_root) +fn local_branch_exists( + repo_root: &Path, + branch: &str, + trust_repository: bool, +) -> Result<bool, String> { + let output = repository_git_command(repo_root, trust_repository) .args(["show-ref", "--verify", "--quiet"]) .arg(format!("refs/heads/{branch}")) .output() @@ -296,11 +310,12 @@ pub(crate) fn run_worktree_add_command( path: &Path, branch: &str, base: &str, + trust_repository: bool, ) -> Result<(), String> { - let command = if local_branch_exists(repo_root, branch)? { - build_worktree_add_existing_branch_command(repo_root, path, branch) + let command = if local_branch_exists(repo_root, branch, trust_repository)? { + build_worktree_add_existing_branch_command(repo_root, path, branch, trust_repository) } else { - build_worktree_add_new_branch_command(repo_root, path, branch, base) + build_worktree_add_new_branch_command(repo_root, path, branch, base, trust_repository) }; run_worktree_command(&command) } @@ -330,15 +345,16 @@ pub(crate) fn run_worktree_remove_command_with_recovery( repo_root: &Path, path: &Path, force: bool, + trust_repository: bool, ) -> Result<(), String> { match run_worktree_command(command) { Ok(()) => Ok(()), Err(err) if force && is_not_working_tree_remove_error(&err) => { - if worktree_list_contains_path(repo_root, path)? { + if worktree_list_contains_path(repo_root, path, trust_repository)? { return Err(err); } if path.exists() { - if !leftover_worktree_checkout_matches_repo(repo_root, path) { + if !leftover_worktree_checkout_matches_repo(repo_root, path, trust_repository) { return Err(err); } std::fs::remove_dir_all(path).map_err(|remove_err| { @@ -354,7 +370,11 @@ pub(crate) fn run_worktree_remove_command_with_recovery( } } -fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> bool { +fn leftover_worktree_checkout_matches_repo( + repo_root: &Path, + path: &Path, + trust_repository: bool, +) -> bool { let git_file = path.join(".git"); let Ok(content) = std::fs::read_to_string(&git_file) else { return false; @@ -368,16 +388,14 @@ fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> boo } else { path.join(gitdir) }; - let Some(worktrees_dir) = git_common_worktrees_dir(repo_root) else { + let Some(worktrees_dir) = git_common_worktrees_dir(repo_root, trust_repository) else { return false; }; canonical_or_original(&gitdir).starts_with(canonical_or_original(&worktrees_dir)) } -fn git_common_worktrees_dir(repo_root: &Path) -> Option<PathBuf> { - let output = crate::noninteractive_process::command("git") - .arg("-C") - .arg(repo_root) +fn git_common_worktrees_dir(repo_root: &Path, trust_repository: bool) -> Option<PathBuf> { + let output = repository_git_command(repo_root, trust_repository) .args(["rev-parse", "--git-common-dir"]) .output() .ok()?; @@ -470,10 +488,11 @@ pub(crate) fn parse_worktree_list_porcelain(output: &str) -> Vec<ExistingWorktre entries } -pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWorktree>, String> { - let output = crate::noninteractive_process::command("git") - .arg("-C") - .arg(repo_root) +pub(crate) fn list_existing_worktrees( + repo_root: &Path, + trust_repository: bool, +) -> Result<Vec<ExistingWorktree>, String> { + let output = repository_git_command(repo_root, trust_repository) .args(["worktree", "list", "--porcelain"]) .output() .map_err(|err| err.to_string())?; @@ -491,9 +510,13 @@ pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWo }) } -pub(crate) fn worktree_list_contains_path(repo_root: &Path, path: &Path) -> Result<bool, String> { +fn worktree_list_contains_path( + repo_root: &Path, + path: &Path, + trust_repository: bool, +) -> Result<bool, String> { let expected = canonical_or_original(path); - Ok(list_existing_worktrees(repo_root)? + Ok(list_existing_worktrees(repo_root, trust_repository)? .into_iter() .any(|entry| canonical_or_original(&entry.path) == expected)) } @@ -538,6 +561,18 @@ mod tests { } #[test] + fn trusted_repository_git_args_are_request_scoped() { + assert_eq!( + repository_git_args(Path::new("/repo/herdr"), false), + ["-C", "/repo/herdr"] + ); + assert_eq!( + repository_git_args(Path::new("/repo/herdr"), true), + ["-c", "safe.directory=/repo/herdr", "-C", "/repo/herdr",] + ); + } + + #[test] fn generated_branch_slug_is_worktree_namespaced_and_stable() { assert_eq!(generated_branch_slug(0), "worktree/brave-river-0000"); assert_eq!(generated_branch_slug(9), "worktree/calm-cloud-0009"); @@ -728,11 +763,11 @@ prunable stale ], ); - assert_eq!(checkout_has_dirty_files(&checkout), Ok(false)); + assert_eq!(checkout_has_dirty_files(&checkout, false), Ok(false)); std::fs::write(checkout.join("README.md"), "dirty\n").unwrap(); - assert_eq!(checkout_has_dirty_files(&checkout), Ok(true)); + assert_eq!(checkout_has_dirty_files(&checkout, false), Ok(true)); - let remove = build_worktree_remove_command(&repo, &checkout, true); + let remove = build_worktree_remove_command(&repo, &checkout, true, false); run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(repo); } @@ -743,6 +778,7 @@ prunable stale Path::new("/repo/herdr"), Path::new("/w/herdr/issue-137"), false, + false, ); assert_eq!(command.program, "git"); assert_eq!( @@ -763,6 +799,7 @@ prunable stale Path::new("/repo/herdr"), Path::new("/w/herdr/issue-137"), true, + false, ); assert_eq!( command.args, @@ -797,6 +834,7 @@ prunable stale Path::new("/w/herdr/worktree-brave-river"), "worktree/brave-river", "HEAD", + false, ); assert_eq!(command.program, "git"); assert_eq!( @@ -820,6 +858,7 @@ prunable stale Path::new("/repo/herdr"), Path::new("/w/herdr/worktree-brave-river"), "worktree/brave-river", + false, ); assert_eq!(command.program, "git"); assert_eq!( @@ -841,7 +880,7 @@ prunable stale let checkout = unique_temp_path("worktree-run-checkout"); let branch = "worktree/test-create-remove"; - let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD"); + let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false); run_worktree_command(&add).unwrap(); assert!(checkout.join("README.md").exists()); @@ -857,7 +896,7 @@ prunable stale branch ); - let remove = build_worktree_remove_command(&repo, &checkout, false); + let remove = build_worktree_remove_command(&repo, &checkout, false, false); run_worktree_command(&remove).unwrap(); assert!(!checkout.exists()); @@ -870,12 +909,14 @@ prunable stale let checkout = unique_temp_path("worktree-recovery-checkout"); let branch = "worktree/recovery"; - let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD"); + let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false); run_worktree_command(&add).unwrap(); - let remove = build_worktree_remove_command(&repo, &checkout, true); + let remove = build_worktree_remove_command(&repo, &checkout, true, false); run_worktree_command(&remove).unwrap(); std::fs::create_dir_all(&checkout).unwrap(); - let stale_admin_dir = git_common_worktrees_dir(&repo).unwrap().join("stale"); + let stale_admin_dir = git_common_worktrees_dir(&repo, false) + .unwrap() + .join("stale"); std::fs::write( checkout.join(".git"), format!("gitdir: {}\n", stale_admin_dir.display()), @@ -883,7 +924,7 @@ prunable stale .unwrap(); std::fs::write(checkout.join("leftover"), "leftover\n").unwrap(); - run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true).unwrap(); + run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false).unwrap(); assert!(!checkout.exists()); let _ = std::fs::remove_dir_all(repo); @@ -895,14 +936,14 @@ prunable stale let checkout = unique_temp_path("worktree-recovery-unrelated-checkout"); let branch = "worktree/recovery-unrelated"; - let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD"); + let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false); run_worktree_command(&add).unwrap(); - let remove = build_worktree_remove_command(&repo, &checkout, true); + let remove = build_worktree_remove_command(&repo, &checkout, true, false); run_worktree_command(&remove).unwrap(); std::fs::create_dir_all(&checkout).unwrap(); std::fs::write(checkout.join("unrelated"), "do not delete\n").unwrap(); - let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true) + let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false) .expect_err("unrelated replacement directory should not be removed"); assert!(is_not_working_tree_remove_error(&err)); |
