1use std::io;
4use std::path::Path;
5use std::process::Command;
6
7const REPO_REDIRECTORS: &[&str] = &[
12 "GIT_DIR",
13 "GIT_WORK_TREE",
14 "GIT_INDEX_FILE",
15 "GIT_COMMON_DIR",
16 "GIT_OBJECT_DIRECTORY",
17 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
18 "GIT_NAMESPACE",
19];
20
21fn scrub_repo_redirectors(command: &mut Command) {
22 for name in REPO_REDIRECTORS {
23 command.env_remove(name);
24 }
25}
26
27pub fn worktree_remove(dir: &Path, spec: super::WorktreeRemove) -> std::io::Result<()> {
30 super::reject_flag_like_path("worktree path", &spec.path)
36 .map_err(|err| io::Error::other(err.to_string()))?;
37 let mut cmd = Command::new(super::BINARY);
38 cmd.current_dir(dir).args(["worktree", "remove"]);
39 scrub_repo_redirectors(&mut cmd);
40 if spec.force {
41 cmd.arg("--force");
42 }
43 cmd.arg(&spec.path);
44 let output = cmd.output()?;
45 if output.status.success() {
46 Ok(())
47 } else {
48 let stderr = String::from_utf8_lossy(&output.stderr);
49 Err(std::io::Error::other(format!(
50 "`git worktree remove` exited with {}: {}",
51 output.status,
52 stderr.trim(),
53 )))
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn blocking_cleanup_scrubs_every_repo_redirector() {
63 let mut command = Command::new("git");
64 scrub_repo_redirectors(&mut command);
65 let removed: std::collections::BTreeSet<_> = command
66 .get_envs()
67 .filter_map(|(name, value)| value.is_none().then_some(name.to_str().unwrap()))
68 .collect();
69 assert_eq!(
70 removed,
71 REPO_REDIRECTORS.iter().copied().collect(),
72 "every redirector must be removed regardless of Command's ordering"
73 );
74 }
75
76 #[test]
80 fn worktree_remove_rejects_flag_like_path_before_spawn() {
81 let err = worktree_remove(
82 Path::new("/repo"),
83 super::super::WorktreeRemove::new("--force"),
84 )
85 .expect_err("a flag-like worktree path must be refused");
86 let message = err.to_string();
87 assert!(
88 message.contains("would be parsed as a flag"),
89 "expected the guard's message, got: {message}"
90 );
91 }
92
93 #[test]
94 fn worktree_remove_rejects_empty_path_before_spawn() {
95 let err = worktree_remove(Path::new("/repo"), super::super::WorktreeRemove::new(" "))
96 .expect_err("an empty worktree path must be refused");
97 let message = err.to_string();
98 assert!(
99 message.contains("would be parsed as a flag"),
100 "expected the guard's message, got: {message}"
101 );
102 }
103
104 #[cfg(unix)]
108 #[test]
109 fn worktree_remove_does_not_panic_on_non_utf8_path() {
110 use std::ffi::OsString;
111 use std::os::unix::ffi::OsStringExt;
112
113 let bytes = vec![0xFFu8, 0xFEu8, b'x'];
114 let path = std::path::PathBuf::from(OsString::from_vec(bytes));
115 let _ = worktree_remove(Path::new("/repo"), super::super::WorktreeRemove::new(path));
118 }
119
120 #[test]
124 #[ignore = "requires the git binary"]
125 fn worktree_remove_failure_includes_captured_stderr() {
126 let temp = vcs_testkit::TempDir::new("blocking-worktree-remove-failure");
127 let dir = temp.path();
128 let path = "missing-worktree";
129 let mut command = Command::new(super::super::BINARY);
130 command.current_dir(dir).args(["worktree", "remove", path]);
131 scrub_repo_redirectors(&mut command);
132 let expected = command.output().expect("run git directly");
133 assert!(
134 !expected.status.success(),
135 "the direct git command must fail for this diagnostic test"
136 );
137 let expected_stderr = String::from_utf8_lossy(&expected.stderr).trim().to_owned();
138 assert!(
139 !expected_stderr.is_empty(),
140 "the failing git command must emit stderr"
141 );
142
143 let err = worktree_remove(dir, super::super::WorktreeRemove::new(path))
144 .expect_err("remove must fail");
145 assert!(
146 err.to_string().contains(&expected_stderr),
147 "error must retain git stderr; expected {expected_stderr:?}, got: {err}"
148 );
149 }
150}