Skip to main content

git_atomic/git/
diff.rs

1use crate::core::GitError;
2use gix::ObjectId;
3use std::path::{Path, PathBuf};
4
5/// Open a git repository at or above `path`.
6pub fn open_repo(path: &Path) -> Result<gix::Repository, GitError> {
7    gix::open(path).map_err(|e| match e {
8        gix::open::Error::NotARepository { path, .. } => GitError::NotARepo { path },
9        other => GitError::Gix(Box::new(other)),
10    })
11}
12
13/// Resolve a revision string (e.g. "HEAD", "abc1234") to an `ObjectId`.
14pub fn resolve_commit(repo: &gix::Repository, reference: &str) -> Result<ObjectId, GitError> {
15    let obj = repo
16        .rev_parse_single(reference.as_bytes())
17        .map_err(|e| GitError::ResolveRef {
18            reference: reference.to_string(),
19            reason: e.to_string(),
20        })?;
21    Ok(obj.detach())
22}
23
24/// Return the list of changed file paths between a commit and its parent.
25/// For initial commits (no parent), diffs against the empty tree.
26pub fn changed_files(
27    repo: &gix::Repository,
28    commit_id: ObjectId,
29) -> Result<Vec<PathBuf>, GitError> {
30    let commit = repo
31        .find_commit(commit_id)
32        .map_err(|e| GitError::Operation(format!("find commit: {e}")))?;
33    let tree = commit
34        .tree()
35        .map_err(|e| GitError::Operation(format!("get tree: {e}")))?;
36
37    let parent_tree = match commit.parent_ids().next() {
38        Some(parent_id) => {
39            let parent = repo
40                .find_commit(parent_id.detach())
41                .map_err(|e| GitError::Operation(format!("find parent: {e}")))?;
42            Some(
43                parent
44                    .tree()
45                    .map_err(|e| GitError::Operation(format!("parent tree: {e}")))?,
46            )
47        }
48        None => None,
49    };
50
51    let changes = repo
52        .diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None)
53        .map_err(|e| GitError::Operation(format!("diff: {e}")))?;
54
55    let paths: Vec<PathBuf> = changes
56        .iter()
57        .map(|change| PathBuf::from(change.location().to_string()))
58        .collect();
59
60    Ok(paths)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use std::process::Command;
67
68    fn init_repo_with_commit(dir: &Path) -> ObjectId {
69        Command::new("git")
70            .args(["init", "-b", "main"])
71            .current_dir(dir)
72            .output()
73            .unwrap();
74        Command::new("git")
75            .args(["config", "user.email", "test@test.com"])
76            .current_dir(dir)
77            .output()
78            .unwrap();
79        Command::new("git")
80            .args(["config", "user.name", "Test"])
81            .current_dir(dir)
82            .output()
83            .unwrap();
84
85        std::fs::write(dir.join("hello.txt"), "hello").unwrap();
86        Command::new("git")
87            .args(["add", "."])
88            .current_dir(dir)
89            .output()
90            .unwrap();
91        Command::new("git")
92            .args(["commit", "-m", "initial"])
93            .current_dir(dir)
94            .output()
95            .unwrap();
96
97        let repo = open_repo(dir).unwrap();
98        resolve_commit(&repo, "HEAD").unwrap()
99    }
100
101    #[test]
102    fn open_valid_repo() {
103        let dir = tempfile::tempdir().unwrap();
104        Command::new("git")
105            .args(["init", "-b", "main"])
106            .current_dir(dir.path())
107            .output()
108            .unwrap();
109        assert!(open_repo(dir.path()).is_ok());
110    }
111
112    #[test]
113    fn open_not_a_repo() {
114        let dir = tempfile::tempdir().unwrap();
115        assert!(open_repo(dir.path()).is_err());
116    }
117
118    #[test]
119    fn initial_commit_changed_files() {
120        let dir = tempfile::tempdir().unwrap();
121        let id = init_repo_with_commit(dir.path());
122        let repo = open_repo(dir.path()).unwrap();
123        let files = changed_files(&repo, id).unwrap();
124        assert_eq!(files, vec![PathBuf::from("hello.txt")]);
125    }
126}