1use anyhow::{Context, Result};
2use git2::Repository;
3use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5use tracing::debug;
6
7pub struct GitDiff {
8 pub changed_files: Vec<PathBuf>,
9 pub repo_root: PathBuf,
10}
11
12pub fn changed_files(repo_path: &Path, base_ref: &str) -> Result<GitDiff> {
14 let repo = Repository::discover(repo_path).context("Not a git repository")?;
15
16 let repo_root = repo
17 .workdir()
18 .context("Bare repositories are not supported")?
19 .to_path_buf();
20
21 debug!("Found git repository at {}", repo_root.display());
22
23 let base_obj = repo
24 .revparse_single(base_ref)
25 .with_context(|| format!("Could not resolve base ref '{base_ref}'"))?;
26
27 debug!("Resolved base ref '{}' to {}", base_ref, base_obj.id());
28
29 let base_tree = base_obj
30 .peel_to_tree()
31 .with_context(|| format!("Could not peel '{base_ref}' to a tree"))?;
32
33 let head_ref = repo.head().context("Could not get HEAD")?;
34 let head_tree = head_ref
35 .peel_to_tree()
36 .context("Could not peel HEAD to a tree")?;
37
38 let mut files = HashSet::new();
39
40 let diff_committed = repo
42 .diff_tree_to_tree(Some(&base_tree), Some(&head_tree), None)
43 .context("Failed to diff base..HEAD")?;
44
45 for delta in diff_committed.deltas() {
46 if let Some(p) = delta.new_file().path() {
47 files.insert(p.to_path_buf());
48 }
49 if let Some(p) = delta.old_file().path() {
50 files.insert(p.to_path_buf());
51 }
52 }
53
54 let diff_uncommitted = repo
56 .diff_tree_to_workdir_with_index(Some(&head_tree), None)
57 .context("Failed to diff HEAD vs working tree")?;
58
59 for delta in diff_uncommitted.deltas() {
60 if let Some(p) = delta.new_file().path() {
61 files.insert(p.to_path_buf());
62 }
63 if let Some(p) = delta.old_file().path() {
64 files.insert(p.to_path_buf());
65 }
66 }
67
68 let mut changed_files: Vec<PathBuf> = files.into_iter().collect();
69 changed_files.sort();
70
71 debug!("Detected {} changed files", changed_files.len());
72
73 Ok(GitDiff {
74 changed_files,
75 repo_root,
76 })
77}
78
79pub fn merge_base(repo_path: &Path, branch: &str) -> Result<String> {
82 let repo = Repository::discover(repo_path).context("Not a git repository")?;
83
84 debug!("Computing merge-base between HEAD and '{}'", branch);
85
86 let head_oid = repo
87 .head()
88 .context("Could not get HEAD")?
89 .target()
90 .context("HEAD is not a direct reference")?;
91
92 let branch_obj = repo
93 .revparse_single(branch)
94 .with_context(|| format!("Could not resolve branch '{branch}'"))?;
95 let branch_oid = branch_obj.id();
96
97 let merge_base_oid = repo
98 .merge_base(head_oid, branch_oid)
99 .with_context(|| format!("Could not find merge-base between HEAD and '{branch}'"))?;
100
101 let sha = merge_base_oid.to_string();
102 debug!("Merge-base resolved to {}", sha);
103
104 Ok(sha)
105}