1use std::{ffi::OsString, path::PathBuf, str::FromStr};
3
4use crate::XvcRoot;
5use subprocess::Exec;
6use xvc_logging::{debug, XvcOutputSender};
7
8use crate::{Error, Result};
9use std::path::Path;
10
11use xvc_walker::{build_ignore_patterns, AbsolutePath, IgnoreRules};
12
13use crate::GIT_DIR;
14
15use super::xvcignore::COMMON_IGNORE_PATTERNS;
16pub fn inside_git(path: &Path) -> Option<PathBuf> {
20 let mut pb = PathBuf::from(path)
21 .canonicalize()
22 .expect("Cannot canonicalize the path. Possible symlink loop.");
23 loop {
24 if pb.join(GIT_DIR).is_dir() {
25 return Some(pb);
26 } else if pb.parent().is_none() {
27 return None;
28 } else {
29 pb.pop();
30 }
31 }
32}
33
34pub fn build_gitignore(git_root: &AbsolutePath) -> Result<IgnoreRules> {
37 let rules = build_ignore_patterns(
38 COMMON_IGNORE_PATTERNS,
39 git_root,
40 ".gitignore".to_owned().as_ref(),
41 )?;
42
43 Ok(rules)
44}
45
46pub fn get_absolute_git_command(git_command: &str) -> Result<String> {
49 let git_cmd_path = PathBuf::from(git_command);
50 let git_cmd = if git_cmd_path.is_absolute() {
51 git_command.to_string()
52 } else {
53 let cmd_path = which::which(git_command)?;
54 cmd_path.to_string_lossy().to_string()
55 };
56 Ok(git_cmd)
57}
58
59pub fn exec_git(git_command: &str, xvc_directory: &str, args_str_vec: &[&str]) -> Result<String> {
61 let mut args = vec!["-C", xvc_directory];
62 args.extend(args_str_vec);
63 let args: Vec<OsString> = args
64 .iter()
65 .map(|s| OsString::from_str(s).unwrap())
66 .collect();
67 let proc_res = Exec::cmd(git_command).args(&args).capture()?;
68
69 match proc_res.exit_status {
70 subprocess::ExitStatus::Exited(0) => Ok(proc_res.stdout_str()),
71 subprocess::ExitStatus::Exited(_) => Err(Error::GitProcessError {
72 stdout: proc_res.stdout_str(),
73 stderr: proc_res.stderr_str(),
74 }),
75 subprocess::ExitStatus::Signaled(_)
76 | subprocess::ExitStatus::Other(_)
77 | subprocess::ExitStatus::Undetermined => Err(Error::GitProcessError {
78 stdout: proc_res.stdout_str(),
79 stderr: proc_res.stderr_str(),
80 }),
81 }
82}
83
84pub fn get_git_tracked_files(git_command: &str, xvc_directory: &str) -> Result<Vec<String>> {
89 let git_ls_files_out = exec_git(
90 git_command,
91 xvc_directory,
92 &["-c", "core.quotepath=off", "ls-files", "--full-name"],
96 )?;
97 let git_ls_files_out = git_ls_files_out
98 .lines()
99 .map(|s| s.to_string())
100 .collect::<Vec<String>>();
101 Ok(git_ls_files_out)
102}
103
104pub fn stash_user_staged_files(
106 output_snd: &XvcOutputSender,
107 git_command: &str,
108 xvc_directory: &str,
109) -> Result<String> {
110 let git_diff_staged_out = exec_git(
112 git_command,
113 xvc_directory,
114 &["diff", "--name-only", "--cached"],
115 )?;
116
117 if !git_diff_staged_out.trim().is_empty() {
119 debug!(
120 output_snd,
121 "Stashing user staged files: {git_diff_staged_out}"
122 );
123 let stash_out = exec_git(git_command, xvc_directory, &["stash", "push", "--staged"])?;
124 debug!(output_snd, "Stashed user staged files: {stash_out}");
125 }
126
127 Ok(git_diff_staged_out)
128}
129
130pub fn unstash_user_staged_files(
132 output_snd: &XvcOutputSender,
133 git_command: &str,
134 xvc_directory: &str,
135) -> Result<()> {
136 let res_git_stash_pop = exec_git(git_command, xvc_directory, &["stash", "pop", "--index"])?;
137 debug!(
138 output_snd,
139 "Unstashed user staged files: {res_git_stash_pop}"
140 );
141 Ok(())
142}
143
144pub fn git_checkout_ref(
146 output_snd: &XvcOutputSender,
147 xvc_root: &XvcRoot,
148 from_ref: &str,
149) -> Result<()> {
150 let xvc_directory = xvc_root.as_path().to_str().unwrap();
151 let git_command_option = xvc_root.config().get_str("git.command")?.option;
152 let git_command = get_absolute_git_command(&git_command_option)?;
153
154 let git_diff_staged_out = stash_user_staged_files(output_snd, &git_command, xvc_directory)?;
155 exec_git(&git_command, xvc_directory, &["checkout", from_ref])?;
156
157 if !git_diff_staged_out.trim().is_empty() {
158 debug!("Unstashing user staged files: {git_diff_staged_out}");
159 unstash_user_staged_files(output_snd, &git_command, xvc_directory)?;
160 }
161 Ok(())
162}
163
164pub fn handle_git_automation(
167 output_snd: &XvcOutputSender,
168 xvc_root: &XvcRoot,
169 to_branch: Option<&str>,
170 xvc_cmd: &str,
171) -> Result<()> {
172 let xvc_root_dir = xvc_root.as_path().to_path_buf();
173 let xvc_root_str = xvc_root_dir.to_str().unwrap();
174 let use_git = xvc_root.config().get_bool("git.use_git")?.option;
175 let auto_commit = xvc_root.config().get_bool("git.auto_commit")?.option;
176 let auto_stage = xvc_root.config().get_bool("git.auto_stage")?.option;
177 let git_command_str = xvc_root.config().get_str("git.command")?.option;
178 let git_command = get_absolute_git_command(&git_command_str)?;
179 let xvc_dir = xvc_root.xvc_dir().clone();
180 let xvc_dir_str = xvc_dir.to_str().unwrap();
181
182 if use_git {
183 if auto_commit {
184 git_auto_commit(
185 output_snd,
186 &git_command,
187 xvc_root_str,
188 xvc_dir_str,
189 xvc_cmd,
190 to_branch,
191 )?;
192 } else if auto_stage {
193 git_auto_stage(output_snd, &git_command, xvc_root_str, xvc_dir_str)?;
194 }
195 }
196
197 Ok(())
198}
199
200pub fn git_auto_commit(
202 output_snd: &XvcOutputSender,
203 git_command: &str,
204 xvc_root_str: &str,
205 xvc_dir_str: &str,
206 xvc_cmd: &str,
207 to_branch: Option<&str>,
208) -> Result<()> {
209 debug!(output_snd, "Using Git: {git_command}");
210
211 let git_diff_staged_out = stash_user_staged_files(output_snd, git_command, xvc_root_str)?;
212
213 if let Some(branch) = to_branch {
214 debug!(output_snd, "Checking out branch {branch}");
215 exec_git(git_command, xvc_root_str, &["checkout", "-b", branch])?;
216 }
217
218 match exec_git(
220 git_command,
221 xvc_root_str,
222 &[
225 "add",
226 "--verbose",
227 xvc_dir_str,
228 "*.gitignore",
229 "*.xvcignore",
230 ],
231 ) {
232 Ok(git_add_output) => {
233 if git_add_output.trim().is_empty() {
234 debug!(output_snd, "No files to commit");
235 return Ok(());
236 } else {
237 match exec_git(
238 git_command,
239 xvc_root_str,
240 &[
241 "commit",
242 "-m",
243 &format!("Xvc auto-commit after '{xvc_cmd}'"),
244 ],
245 ) {
246 Ok(res_git_commit) => {
247 debug!(output_snd, "Committing .xvc/ to git: {res_git_commit}");
248 }
249 Err(e) => {
250 debug!(output_snd, "Error committing .xvc/ to git: {e}");
251 return Err(e);
252 }
253 }
254 }
255 }
256 Err(e) => {
257 debug!(output_snd, "Error adding .xvc/ to git: {e}");
258 return Err(e);
259 }
260 }
261
262 if !git_diff_staged_out.trim().is_empty() {
265 debug!(
266 output_snd,
267 "Unstashing user staged files: {git_diff_staged_out}"
268 );
269 unstash_user_staged_files(output_snd, git_command, xvc_root_str)?;
270 }
271 Ok(())
272}
273
274pub fn git_auto_stage(
276 output_snd: &XvcOutputSender,
277 git_command: &str,
278 xvc_root_str: &str,
279 xvc_dir_str: &str,
280) -> Result<()> {
281 let res_git_add = exec_git(
282 git_command,
283 xvc_root_str,
284 &["add", xvc_dir_str, "*.gitignore", "*.xvcignore"],
285 )?;
286 debug!(output_snd, "Staging .xvc/ to git: {res_git_add}");
287 Ok(())
288}
289
290pub fn git_ignored(git_command: &str, xvc_root_str: &str, path: &str) -> Result<bool> {
292 let command_res = exec_git(git_command, xvc_root_str, &["check-ignore", path])?;
293
294 if command_res.trim().is_empty() {
295 Ok(false)
296 } else {
297 Ok(true)
298 }
299}
300
301pub fn gix_list_references(repo_path: &Path) -> Result<Vec<String>> {
305 let repo = gix::discover(repo_path).map_err(|e| Error::GixError {
307 cause: e.to_string(),
308 })?;
309 let mut refs = Vec::new();
310
311 let ref_platform = repo.references()?;
312 ref_platform.all().map(|all| {
313 all.for_each(|reference| {
314 if let Ok(reference) = reference {
315 if let Some((_, name)) = reference.name().category_and_short_name() {
316 refs.push(name.to_string());
317 }
318 }
319 });
320 Ok(refs)
321 })?
322}
323
324pub fn gix_list_branches(repo_path: &Path) -> Result<Vec<String>> {
326 let repo = gix::discover(repo_path).map_err(|e| Error::GixError {
328 cause: e.to_string(),
329 })?;
330 let mut refs = Vec::new();
331
332 let ref_platform = repo.references()?;
333 ref_platform.local_branches().map(|all| {
334 all.for_each(|reference| {
335 if let Ok(reference) = reference {
336 if let Some((_, name)) = reference.name().category_and_short_name() {
337 refs.push(name.to_string());
338 }
339 }
340 });
341 Ok(refs)
342 })?
343}
344
345#[cfg(test)]
346mod test {
347 use super::*;
348 use std::fs;
349 use test_case::test_case;
350 use xvc_test_helper::*;
351 use xvc_walker::MatchResult as M;
352
353 #[test_case("myfile.txt" , ".gitignore", "/myfile.txt" => matches M::Ignore ; "myfile.txt")]
354 #[test_case("mydir/myfile.txt" , "mydir/.gitignore", "myfile.txt" => matches M::Ignore ; "mydir/myfile.txt")]
355 #[test_case("mydir/myfile.txt" , ".gitignore", "/mydir/myfile.txt" => matches M::Ignore ; "from root dir")]
356 #[test_case("mydir/myfile.txt" , ".gitignore", "" => matches M::NoMatch ; "non ignore")]
357 #[test_case("mydir/myfile.txt" , ".gitignore", "mydir/**" => matches M::Ignore ; "ignore dir star 2")]
358 #[test_case("mydir/myfile.txt" , ".gitignore", "mydir/*" => matches M::Ignore ; "ignore dir star")]
359 #[test_case("mydir/yourdir/myfile.txt" , "mydir/.gitignore", "yourdir/*" => matches M::Ignore ; "ignore deep dir star")]
360 #[test_case("mydir/yourdir/myfile.txt" , "mydir/.gitignore", "yourdir/**" => matches M::Ignore ; "ignore deep dir star 2")]
361 #[test_case("mydir/myfile.txt" , "another-dir/.gitignore", "another-dir/myfile.txt" => matches M::NoMatch ; "non ignore from dir")]
362 fn test_gitignore(path: &str, gitignore_path: &str, ignore_line: &str) -> M {
363 test_logging(log::LevelFilter::Trace);
364 let git_root = temp_git_dir();
365 let path = git_root.join(PathBuf::from(path));
366 let gitignore_path = git_root.join(PathBuf::from(gitignore_path));
367 if let Some(ignore_dir) = gitignore_path.parent() {
368 fs::create_dir_all(ignore_dir).unwrap();
369 }
370 fs::write(&gitignore_path, format!("{}\n", ignore_line)).unwrap();
371
372 let gitignore = build_ignore_patterns("", &git_root, ".gitignore").unwrap();
373
374 gitignore.check(&path)
375 }
376}