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().git.command.clone();
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 git_config = xvc_root.config().git.clone();
175 let use_git = git_config.use_git;
176 let auto_commit = git_config.auto_commit;
177 let auto_stage = git_config.auto_stage;
178 let git_command_str = git_config.command.clone();
179 let git_command = get_absolute_git_command(&git_command_str)?;
180 let xvc_dir = xvc_root.xvc_dir().clone();
181 let xvc_dir_str = xvc_dir.to_str().unwrap();
182
183 if use_git {
184 if auto_commit {
185 git_auto_commit(
186 output_snd,
187 &git_command,
188 xvc_root_str,
189 xvc_dir_str,
190 xvc_cmd,
191 to_branch,
192 )?;
193 } else if auto_stage {
194 git_auto_stage(output_snd, &git_command, xvc_root_str, xvc_dir_str)?;
195 }
196 }
197
198 Ok(())
199}
200
201pub fn git_auto_commit(
203 output_snd: &XvcOutputSender,
204 git_command: &str,
205 xvc_root_str: &str,
206 xvc_dir_str: &str,
207 xvc_cmd: &str,
208 to_branch: Option<&str>,
209) -> Result<()> {
210 debug!(output_snd, "Using Git: {git_command}");
211
212 let git_diff_staged_out = stash_user_staged_files(output_snd, git_command, xvc_root_str)?;
213
214 if let Some(branch) = to_branch {
215 debug!(output_snd, "Checking out branch {branch}");
216 exec_git(git_command, xvc_root_str, &["checkout", "-b", branch])?;
217 }
218
219 match exec_git(
221 git_command,
222 xvc_root_str,
223 &[
226 "add",
227 "--verbose",
228 xvc_dir_str,
229 "*.gitignore",
230 "*.xvcignore",
231 ],
232 ) {
233 Ok(git_add_output) => {
234 if git_add_output.trim().is_empty() {
235 debug!(output_snd, "No files to commit");
236 return Ok(());
237 } else {
238 match exec_git(
239 git_command,
240 xvc_root_str,
241 &[
242 "commit",
243 "-m",
244 &format!("Xvc auto-commit after '{xvc_cmd}'"),
245 ],
246 ) {
247 Ok(res_git_commit) => {
248 debug!(output_snd, "Committing .xvc/ to git: {res_git_commit}");
249 }
250 Err(e) => {
251 debug!(output_snd, "Error committing .xvc/ to git: {e}");
252 return Err(e);
253 }
254 }
255 }
256 }
257 Err(e) => {
258 debug!(output_snd, "Error adding .xvc/ to git: {e}");
259 return Err(e);
260 }
261 }
262
263 if !git_diff_staged_out.trim().is_empty() {
266 debug!(
267 output_snd,
268 "Unstashing user staged files: {git_diff_staged_out}"
269 );
270 unstash_user_staged_files(output_snd, git_command, xvc_root_str)?;
271 }
272 Ok(())
273}
274
275pub fn git_auto_stage(
277 output_snd: &XvcOutputSender,
278 git_command: &str,
279 xvc_root_str: &str,
280 xvc_dir_str: &str,
281) -> Result<()> {
282 let res_git_add = exec_git(
283 git_command,
284 xvc_root_str,
285 &["add", xvc_dir_str, "*.gitignore", "*.xvcignore"],
286 )?;
287 debug!(output_snd, "Staging .xvc/ to git: {res_git_add}");
288 Ok(())
289}
290
291pub fn git_ignored(git_command: &str, xvc_root_str: &str, path: &str) -> Result<bool> {
293 let command_res = exec_git(git_command, xvc_root_str, &["check-ignore", path])?;
294
295 if command_res.trim().is_empty() {
296 Ok(false)
297 } else {
298 Ok(true)
299 }
300}
301
302pub fn gix_list_references(repo_path: &Path) -> Result<Vec<String>> {
306 let repo = gix::discover(repo_path).map_err(|e| Error::GixError {
308 cause: e.to_string(),
309 })?;
310 let mut refs = Vec::new();
311
312 let ref_platform = repo.references()?;
313 ref_platform.all().map(|all| {
314 all.for_each(|reference| {
315 if let Ok(reference) = reference {
316 if let Some((_, name)) = reference.name().category_and_short_name() {
317 refs.push(name.to_string());
318 }
319 }
320 });
321 Ok(refs)
322 })?
323}
324
325pub fn gix_list_branches(repo_path: &Path) -> Result<Vec<String>> {
327 let repo = gix::discover(repo_path).map_err(|e| Error::GixError {
329 cause: e.to_string(),
330 })?;
331 let mut refs = Vec::new();
332
333 let ref_platform = repo.references()?;
334 ref_platform.local_branches().map(|all| {
335 all.for_each(|reference| {
336 if let Ok(reference) = reference {
337 if let Some((_, name)) = reference.name().category_and_short_name() {
338 refs.push(name.to_string());
339 }
340 }
341 });
342 Ok(refs)
343 })?
344}
345
346#[cfg(test)]
347mod test {
348 use super::*;
349 use std::fs;
350 use test_case::test_case;
351 use xvc_test_helper::*;
352 use xvc_walker::MatchResult as M;
353
354 #[test_case("myfile.txt" , ".gitignore", "/myfile.txt" => matches M::Ignore ; "myfile.txt")]
355 #[test_case("mydir/myfile.txt" , "mydir/.gitignore", "myfile.txt" => matches M::Ignore ; "mydir/myfile.txt")]
356 #[test_case("mydir/myfile.txt" , ".gitignore", "/mydir/myfile.txt" => matches M::Ignore ; "from root dir")]
357 #[test_case("mydir/myfile.txt" , ".gitignore", "" => matches M::NoMatch ; "non ignore")]
358 #[test_case("mydir/myfile.txt" , ".gitignore", "mydir/**" => matches M::Ignore ; "ignore dir star 2")]
359 #[test_case("mydir/myfile.txt" , ".gitignore", "mydir/*" => matches M::Ignore ; "ignore dir star")]
360 #[test_case("mydir/yourdir/myfile.txt" , "mydir/.gitignore", "yourdir/*" => matches M::Ignore ; "ignore deep dir star")]
361 #[test_case("mydir/yourdir/myfile.txt" , "mydir/.gitignore", "yourdir/**" => matches M::Ignore ; "ignore deep dir star 2")]
362 #[test_case("mydir/myfile.txt" , "another-dir/.gitignore", "another-dir/myfile.txt" => matches M::NoMatch ; "non ignore from dir")]
363 fn test_gitignore(path: &str, gitignore_path: &str, ignore_line: &str) -> M {
364 test_logging(log::LevelFilter::Trace);
365 let git_root = temp_git_dir();
366 let path = git_root.join(PathBuf::from(path));
367 let gitignore_path = git_root.join(PathBuf::from(gitignore_path));
368 if let Some(ignore_dir) = gitignore_path.parent() {
369 fs::create_dir_all(ignore_dir).unwrap();
370 }
371 fs::write(&gitignore_path, format!("{}\n", ignore_line)).unwrap();
372
373 let gitignore = build_ignore_patterns("", &git_root, ".gitignore").unwrap();
374
375 gitignore.check(&path)
376 }
377}