1use crate::error::{GuardError, Result};
4use crate::{parse_filename, MigrationFile};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8pub struct GitCmd {
10 pub program: String,
11 pub repo: PathBuf,
12}
13
14impl GitCmd {
15 pub fn new(repo: impl Into<PathBuf>) -> Self {
16 Self {
17 program: "git".to_string(),
18 repo: repo.into(),
19 }
20 }
21
22 fn run(&self, args: &[&str]) -> Result<String> {
23 let out = Command::new(&self.program)
24 .args(args)
25 .current_dir(&self.repo)
26 .output()?;
27 if !out.status.success() {
28 return Err(GuardError::Git {
29 cmd: format!("{} {}", self.program, args.join(" ")),
30 status: out.status.code(),
31 stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
32 });
33 }
34 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
35 }
36}
37
38pub fn local_branches(git: &GitCmd) -> Result<Vec<String>> {
40 let out = git.run(&["for-each-ref", "--format=%(refname:short)", "refs/heads/"])?;
41 Ok(out
42 .lines()
43 .map(|l| l.trim().to_string())
44 .filter(|l| !l.is_empty())
45 .collect())
46}
47
48pub fn branch_migration_files(
50 git: &GitCmd,
51 branch: &str,
52 dir: &Path,
53) -> Result<Vec<MigrationFile>> {
54 if git
59 .run(&[
60 "rev-parse",
61 "--verify",
62 "--quiet",
63 &format!("{branch}^{{commit}}"),
64 ])
65 .is_err()
66 {
67 return Err(GuardError::Git {
68 cmd: format!("git rev-parse --verify {branch}"),
69 status: None,
70 stderr: format!("ref '{branch}' does not resolve to a commit"),
71 });
72 }
73 let dir_str = dir.to_string_lossy();
74 let out = git.run(&["ls-tree", "-r", "--name-only", branch, "--", &dir_str])?;
75 Ok(files_from_paths(&out, dir))
76}
77
78pub fn topic_new_files(git: &GitCmd, base_ref: &str, dir: &Path) -> Result<Vec<MigrationFile>> {
80 let base = branch_migration_files(git, base_ref, dir)?;
81 let base_names: std::collections::HashSet<_> =
82 base.iter().map(|f| f.filename.clone()).collect();
83 let working = crate::scan_dir("", &git.repo.join(dir))?; Ok(working
85 .files
86 .into_iter()
87 .filter(|f| !base_names.contains(&f.filename))
88 .collect())
89}
90
91fn files_from_paths(ls_tree_out: &str, dir: &Path) -> Vec<MigrationFile> {
92 let mut files = Vec::new();
93 for line in ls_tree_out.lines() {
94 let p = Path::new(line.trim());
95 let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
96 continue;
97 };
98 if let Some((version, width, desc)) = parse_filename(name) {
99 files.push(MigrationFile {
100 version,
101 width,
102 description: desc.to_string(),
103 filename: name.to_string(),
104 path: dir.join(name),
105 });
106 }
107 }
108 files.sort_by_key(|f| f.version);
109 files
110}