1use std::path::Path;
4use std::process::Command;
5
6use crate::{Error, require};
7
8pub fn cmd(dir: &Path, args: &[&str]) -> Result<String, Error> {
10 cmd_with_env(dir, args, &[])
11}
12
13pub fn cmd_with_env(dir: &Path, args: &[&str], env: &[(&str, &str)]) -> Result<String, Error> {
18 require("git")?;
19
20 let has_askpass = env.iter().any(|(k, _)| *k == "GIT_ASKPASS");
21
22 let mut command = Command::new("git");
23 if has_askpass {
24 command.args(["-c", "credential.helper="]);
25 }
26 command.args(args).current_dir(dir);
27 for (k, v) in env {
28 command.env(k, v);
29 }
30 let output = command.output()?;
31
32 if !output.status.success() {
33 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
34 return Err(Error::CommandFailed {
35 cmd: format!("git {}", args.first().unwrap_or(&"")),
36 detail: stderr,
37 });
38 }
39
40 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
41}
42
43pub fn clone_shallow(url: &str, dest: &Path, branch: &str) -> Result<(), Error> {
45 clone_shallow_with_env(url, dest, branch, &[])
46}
47
48pub fn clone_shallow_with_env(
50 url: &str,
51 dest: &Path,
52 branch: &str,
53 env: &[(&str, &str)],
54) -> Result<(), Error> {
55 require("git")?;
56
57 let has_askpass = env.iter().any(|(k, _)| *k == "GIT_ASKPASS");
58
59 let mut command = Command::new("git");
60 if has_askpass {
61 command.args(["-c", "credential.helper="]);
62 }
63 command.args([
64 "clone",
65 "--depth",
66 "1",
67 "--branch",
68 branch,
69 url,
70 &dest.to_string_lossy(),
71 ]);
72 for (k, v) in env {
73 command.env(k, v);
74 }
75 let output = command.output()?;
76
77 if !output.status.success() {
78 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
79 return Err(Error::CommandFailed {
80 cmd: "git clone".to_string(),
81 detail: stderr,
82 });
83 }
84
85 Ok(())
86}
87
88pub fn clone_local(source: &Path, dest: &Path, branch: &str) -> Result<(), Error> {
90 require("git")?;
91
92 let output = Command::new("git")
93 .args([
94 "clone",
95 "--branch",
96 branch,
97 "--single-branch",
98 &source.to_string_lossy(),
99 &dest.to_string_lossy(),
100 ])
101 .output()?;
102
103 if !output.status.success() {
104 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
105 return Err(Error::CommandFailed {
106 cmd: "git clone (local)".to_string(),
107 detail: stderr,
108 });
109 }
110
111 Ok(())
112}
113
114pub fn checkout_new_branch(dir: &Path, branch: &str) -> Result<(), Error> {
116 cmd(dir, &["checkout", "-b", branch])?;
117 Ok(())
118}
119
120pub fn config_set(dir: &Path, key: &str, value: &str) -> Result<(), Error> {
122 cmd(dir, &["config", key, value])?;
123 Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::fs;
130
131 fn temp_repo() -> tempfile::TempDir {
133 let dir = tempfile::tempdir().unwrap();
134 Command::new("git")
135 .args(["init"])
136 .current_dir(dir.path())
137 .output()
138 .unwrap();
139 Command::new("git")
140 .args(["config", "user.email", "test@test.com"])
141 .current_dir(dir.path())
142 .output()
143 .unwrap();
144 Command::new("git")
145 .args(["config", "user.name", "Test"])
146 .current_dir(dir.path())
147 .output()
148 .unwrap();
149 fs::write(dir.path().join("README.md"), "# test").unwrap();
151 Command::new("git")
152 .args(["add", "."])
153 .current_dir(dir.path())
154 .output()
155 .unwrap();
156 Command::new("git")
157 .args(["commit", "-m", "init"])
158 .current_dir(dir.path())
159 .output()
160 .unwrap();
161 dir
162 }
163
164 #[test]
165 fn test_cmd_status() {
166 let repo = temp_repo();
167 let output = cmd(repo.path(), &["status", "--short"]).unwrap();
168 assert!(output.is_empty()); }
170
171 #[test]
172 fn test_cmd_invalid_dir() {
173 let result = cmd(Path::new("/nonexistent_dir_12345"), &["status"]);
174 assert!(result.is_err());
175 }
176
177 #[test]
178 fn test_cmd_invalid_subcommand() {
179 let repo = temp_repo();
180 let result = cmd(repo.path(), &["not-a-real-subcommand"]);
181 assert!(result.is_err());
182 }
183
184 #[test]
185 fn test_config_set_and_read() {
186 let repo = temp_repo();
187 config_set(repo.path(), "user.name", "TestUser").unwrap();
188 let output = cmd(repo.path(), &["config", "user.name"]).unwrap();
189 assert_eq!(output.trim(), "TestUser");
190 }
191
192 #[test]
193 fn test_checkout_new_branch() {
194 let repo = temp_repo();
195 checkout_new_branch(repo.path(), "feature-test").unwrap();
196 let output = cmd(repo.path(), &["branch", "--show-current"]).unwrap();
197 assert_eq!(output.trim(), "feature-test");
198 }
199
200 #[test]
201 fn test_clone_local() {
202 let repo = temp_repo();
203 let dest = tempfile::tempdir().unwrap();
204 let dest_path = dest.path().join("cloned");
205 let branch = cmd(repo.path(), &["branch", "--show-current"])
207 .unwrap()
208 .trim()
209 .to_string();
210 clone_local(repo.path(), &dest_path, &branch).unwrap();
211 assert!(dest_path.join(".git").exists());
212 }
213
214 #[test]
215 fn test_cmd_with_env() {
216 let repo = temp_repo();
217 let output = cmd_with_env(
219 repo.path(),
220 &["status", "--short"],
221 &[("GIT_AUTHOR_NAME", "X")],
222 )
223 .unwrap();
224 assert!(output.is_empty());
225 }
226
227 #[test]
228 fn test_cmd_with_env_askpass_disables_credential_helper() {
229 let repo = temp_repo();
230 let output = cmd_with_env(
236 repo.path(),
237 &["config", "--get-all", "credential.helper"],
238 &[("GIT_ASKPASS", "/bin/echo")],
239 )
240 .unwrap();
241 let lines: Vec<&str> = output.lines().collect();
245 assert!(
246 !lines.is_empty(),
247 "credential.helper should have at least one entry"
248 );
249 assert_eq!(
250 lines.last().unwrap(),
251 &"",
252 "last credential.helper entry should be empty (disabling helpers), got: {lines:?}"
253 );
254 }
255
256 #[test]
257 fn test_cmd_with_env_no_askpass_preserves_credential_helper() {
258 let repo = temp_repo();
259 config_set(repo.path(), "credential.helper", "store").unwrap();
261 let output = cmd_with_env(
263 repo.path(),
264 &["config", "credential.helper"],
265 &[("GIT_AUTHOR_NAME", "X")],
266 )
267 .unwrap();
268 assert_eq!(output.trim(), "store");
269 }
270
271 #[test]
272 fn test_cmd_with_env_askpass_env_is_passed_through() {
273 let repo = temp_repo();
274 let askpass_script = "/usr/bin/true";
277 let output = cmd_with_env(repo.path(), &["status"], &[("GIT_ASKPASS", askpass_script)]);
278 assert!(output.is_ok());
279 }
280}