1use std::path::PathBuf;
9
10use super::{Repo, RepoError};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum ChangeKind {
16 Added,
18 Modified,
20 Deleted,
22 Renamed {
25 from: PathBuf,
27 },
28}
29
30#[derive(Debug, Clone)]
32pub struct FileChange {
33 pub path: PathBuf,
36 pub kind: ChangeKind,
38}
39
40#[derive(Debug, Clone, Default)]
46#[non_exhaustive]
47pub struct Diff {
48 pub changes: Vec<FileChange>,
50}
51
52impl Repo {
53 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}