Skip to main content

rtb_vcs/git/
diff.rs

1//! `Repo::diff` — structured tree-to-tree diff.
2//!
3//! v0.5 commit 2 of 7. Surfaces additions, deletions, modifications,
4//! and renames at file granularity. Hunk-level diffing is deferred
5//! to v0.5.x when a concrete consumer asks for it — the [`Diff`]
6//! value type is `#[non_exhaustive]` so the addition is non-breaking.
7
8use std::path::PathBuf;
9
10use super::{Repo, RepoError};
11
12/// What happened to a file between two tree-ish references.
13#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum ChangeKind {
16    /// File appeared in the destination tree but not the source.
17    Added,
18    /// File present on both sides with different content / mode.
19    Modified,
20    /// File present in the source but absent from the destination.
21    Deleted,
22    /// gix's rewrite tracker matched a deletion + addition pair. The
23    /// path here is the destination; `from` is the source.
24    Renamed {
25        /// Source-side path.
26        from: PathBuf,
27    },
28}
29
30/// A single file-level change in a [`Diff`].
31#[derive(Debug, Clone)]
32pub struct FileChange {
33    /// Repository-relative path. For renames, this is the destination
34    /// path (the source is on [`ChangeKind::Renamed::from`]).
35    pub path: PathBuf,
36    /// What kind of change occurred.
37    pub kind: ChangeKind,
38}
39
40/// Structured diff between two tree-ish references.
41///
42/// Returned by [`Repo::diff`]. The `changes` Vec is ordered as gix
43/// emits them — not guaranteed alphabetical; callers needing
44/// deterministic ordering sort on `path`.
45#[derive(Debug, Clone, Default)]
46#[non_exhaustive]
47pub struct Diff {
48    /// One entry per file-level change.
49    pub changes: Vec<FileChange>,
50}
51
52impl Repo {
53    /// Diff two tree-ish references (e.g. `HEAD~1` vs `HEAD`).
54    ///
55    /// # Errors
56    ///
57    /// - [`RepoError::RevspecNotFound`] — either `a` or `b` could
58    ///   not be resolved.
59    /// - [`RepoError::DiffFailed`] — gix could not read the trees
60    ///   or compute the diff.
61    pub async fn diff(&self, a: &str, b: &str) -> Result<Diff, RepoError> {
62        let inner = self.thread_safe();
63        let a = a.to_string();
64        let b = b.to_string();
65        tokio::task::spawn_blocking(move || compute_diff(&inner, &a, &b)).await.map_err(|join| {
66            RepoError::DiffFailed { cause: format!("spawn_blocking join: {join}") }
67        })?
68    }
69}
70
71fn compute_diff(inner: &gix::ThreadSafeRepository, a: &str, b: &str) -> Result<Diff, RepoError> {
72    let repo = inner.to_thread_local();
73
74    let a_id = parse_single(&repo, a)?;
75    let b_id = parse_single(&repo, b)?;
76
77    let a_commit = repo
78        .find_commit(a_id)
79        .map_err(|_| RepoError::RevspecNotFound { revspec: a.to_string() })?;
80    let b_commit = repo
81        .find_commit(b_id)
82        .map_err(|_| RepoError::RevspecNotFound { revspec: b.to_string() })?;
83
84    let a_tree = a_commit
85        .tree()
86        .map_err(|e| RepoError::DiffFailed { cause: format!("source tree: {e}") })?;
87    let b_tree = b_commit
88        .tree()
89        .map_err(|e| RepoError::DiffFailed { cause: format!("destination tree: {e}") })?;
90
91    let changes = repo
92        .diff_tree_to_tree(Some(&a_tree), Some(&b_tree), None)
93        .map_err(|e| RepoError::DiffFailed { cause: format!("tree diff: {e}") })?;
94
95    let mut diff = Diff::default();
96    for change in changes {
97        use gix::diff::tree_with_rewrites::Change as C;
98        let file = match change {
99            C::Addition { location, .. } => {
100                FileChange { path: gix::path::from_bstring(location), kind: ChangeKind::Added }
101            }
102            C::Deletion { location, .. } => {
103                FileChange { path: gix::path::from_bstring(location), kind: ChangeKind::Deleted }
104            }
105            C::Modification { location, .. } => {
106                FileChange { path: gix::path::from_bstring(location), kind: ChangeKind::Modified }
107            }
108            C::Rewrite { source_location, location, .. } => FileChange {
109                path: gix::path::from_bstring(location),
110                kind: ChangeKind::Renamed { from: gix::path::from_bstring(source_location) },
111            },
112        };
113        diff.changes.push(file);
114    }
115    Ok(diff)
116}
117
118fn parse_single(repo: &gix::Repository, revspec: &str) -> Result<gix::ObjectId, RepoError> {
119    let id = repo
120        .rev_parse_single(revspec)
121        .map_err(|_| RepoError::RevspecNotFound { revspec: revspec.to_string() })?;
122    Ok(id.detach())
123}