Skip to main content

gitoxide_core/repository/
diff.rs

1use anyhow::Context;
2use gix::{
3    bstr::{BString, ByteSlice},
4    objs::tree::EntryMode,
5    odb::store::RefreshMode,
6    prelude::ObjectIdExt,
7    ObjectId,
8};
9
10pub fn tree(
11    mut repo: gix::Repository,
12    out: &mut dyn std::io::Write,
13    old_treeish: BString,
14    new_treeish: BString,
15) -> anyhow::Result<()> {
16    repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
17    repo.objects.refresh = RefreshMode::Never;
18
19    let old_tree_id = repo.rev_parse_single(old_treeish.as_bstr())?;
20    let new_tree_id = repo.rev_parse_single(new_treeish.as_bstr())?;
21
22    let old_tree = old_tree_id.object()?.peel_to_tree()?;
23    let new_tree = new_tree_id.object()?.peel_to_tree()?;
24
25    let changes = repo.diff_tree_to_tree(&old_tree, &new_tree, None)?;
26
27    writeln!(
28        out,
29        "Diffing trees `{old_treeish}` ({old_tree_id}) -> `{new_treeish}` ({new_tree_id})\n"
30    )?;
31    write_changes(&repo, out, changes)?;
32
33    Ok(())
34}
35
36fn write_changes(
37    repo: &gix::Repository,
38    mut out: impl std::io::Write,
39    changes: Vec<gix::diff::tree_with_rewrites::Change>,
40) -> Result<(), std::io::Error> {
41    for change in changes {
42        match change {
43            gix::diff::tree_with_rewrites::Change::Addition {
44                location,
45                id,
46                entry_mode,
47                ..
48            } => {
49                writeln!(out, "A: {}", typed_location(location, entry_mode))?;
50                writeln!(out, "  {}", id.attach(repo).shorten_or_id())?;
51                writeln!(out, "  -> {entry_mode:o}")?;
52            }
53            gix::diff::tree_with_rewrites::Change::Deletion {
54                location,
55                id,
56                entry_mode,
57                ..
58            } => {
59                writeln!(out, "D: {}", typed_location(location, entry_mode))?;
60                writeln!(out, "  {}", id.attach(repo).shorten_or_id())?;
61                writeln!(out, "  {entry_mode:o} ->")?;
62            }
63            gix::diff::tree_with_rewrites::Change::Modification {
64                location,
65                previous_id,
66                id,
67                previous_entry_mode,
68                entry_mode,
69            } => {
70                writeln!(out, "M: {}", typed_location(location, entry_mode))?;
71                writeln!(
72                    out,
73                    "  {previous_id} -> {id}",
74                    previous_id = previous_id.attach(repo).shorten_or_id(),
75                    id = id.attach(repo).shorten_or_id()
76                )?;
77                if previous_entry_mode != entry_mode {
78                    writeln!(out, "  {previous_entry_mode:o} -> {entry_mode:o}")?;
79                }
80            }
81            gix::diff::tree_with_rewrites::Change::Rewrite {
82                source_location,
83                source_id,
84                id,
85                location,
86                source_entry_mode,
87                entry_mode,
88                ..
89            } => {
90                writeln!(
91                    out,
92                    "R: {source} -> {dest}",
93                    source = typed_location(source_location, source_entry_mode),
94                    dest = typed_location(location, entry_mode)
95                )?;
96                writeln!(
97                    out,
98                    "  {source_id} -> {id}",
99                    source_id = source_id.attach(repo).shorten_or_id(),
100                    id = id.attach(repo).shorten_or_id()
101                )?;
102                if source_entry_mode != entry_mode {
103                    writeln!(out, "  {source_entry_mode:o} -> {entry_mode:o}")?;
104                }
105            }
106        }
107    }
108
109    Ok(())
110}
111
112fn typed_location(mut location: BString, mode: EntryMode) -> BString {
113    if mode.is_tree() {
114        location.push(b'/');
115    }
116    location
117}
118
119fn resolve_revspec(
120    repo: &gix::Repository,
121    revspec: BString,
122) -> Result<(ObjectId, Option<std::path::PathBuf>, BString), anyhow::Error> {
123    let result = repo.rev_parse(revspec.as_bstr());
124
125    match result {
126        Err(err) => {
127            // When the revspec is just a name, the delegate tries to resolve a reference which fails.
128            // We extract the error from the tree to learn the name, and treat it as file.
129            let not_found = err
130                .sources()
131                .find_map(|err| err.downcast_ref::<gix::refs::file::find::existing::Error>());
132            if let Some(gix::refs::file::find::existing::Error::NotFound { name }) = not_found {
133                let root = repo.workdir().map(ToOwned::to_owned);
134                let name = gix::path::os_string_into_bstring(name.into())?;
135
136                Ok((ObjectId::null(gix::hash::Kind::Sha1), root, name))
137            } else {
138                Err(err.into())
139            }
140        }
141        Ok(resolved_revspec) => {
142            let blob_id = resolved_revspec
143                .single()
144                .context(format!("rev-spec '{revspec}' must resolve to a single object"))?;
145
146            let (path, _) = resolved_revspec
147                .path_and_mode()
148                .context(format!("rev-spec '{revspec}' must contain a path"))?;
149
150            Ok((blob_id.into(), None, path.into()))
151        }
152    }
153}
154
155pub fn file(
156    mut repo: gix::Repository,
157    out: &mut dyn std::io::Write,
158    old_revspec: BString,
159    new_revspec: BString,
160) -> Result<(), anyhow::Error> {
161    repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
162    repo.objects.refresh = RefreshMode::Never;
163
164    let (old_blob_id, old_root, old_path) = resolve_revspec(&repo, old_revspec)?;
165    let (new_blob_id, new_root, new_path) = resolve_revspec(&repo, new_revspec)?;
166
167    let worktree_roots = gix::diff::blob::pipeline::WorktreeRoots { old_root, new_root };
168
169    let mut resource_cache = repo.diff_resource_cache(
170        gix::diff::blob::pipeline::Mode::ToGitUnlessBinaryToTextIsPresent,
171        worktree_roots,
172    )?;
173
174    resource_cache.set_resource(
175        old_blob_id,
176        gix::object::tree::EntryKind::Blob,
177        old_path.as_ref(),
178        gix::diff::blob::ResourceKind::OldOrSource,
179        &repo.objects,
180    )?;
181    resource_cache.set_resource(
182        new_blob_id,
183        gix::object::tree::EntryKind::Blob,
184        new_path.as_ref(),
185        gix::diff::blob::ResourceKind::NewOrDestination,
186        &repo.objects,
187    )?;
188
189    let outcome = resource_cache.prepare_diff()?;
190
191    use gix::diff::blob::platform::prepare_diff::Operation;
192
193    let algorithm = match outcome.operation {
194        Operation::InternalDiff { algorithm } => algorithm,
195        Operation::ExternalCommand { .. } => {
196            unreachable!("We disabled that")
197        }
198        Operation::SourceOrDestinationIsBinary => {
199            anyhow::bail!("Source or destination is binary and we can't diff that")
200        }
201    };
202
203    let interner = gix::diff::blob::InternedInput::new(
204        tokens_for_diffing(outcome.old.data.as_slice().unwrap_or_default()),
205        tokens_for_diffing(outcome.new.data.as_slice().unwrap_or_default()),
206    );
207
208    let diff = gix::diff::blob::diff_with_slider_heuristics(algorithm, &interner);
209    let rendered = gix::diff::blob::UnifiedDiff::new(
210        &diff,
211        &interner,
212        gix::diff::blob::unified_diff::ConsumeBinaryHunk::new(BString::default(), "\n"),
213        gix::diff::blob::unified_diff::ContextSize::symmetrical(3),
214    )
215    .consume()?;
216    write!(out, "{rendered}")?;
217
218    Ok(())
219}
220
221pub(crate) fn tokens_for_diffing(data: &[u8]) -> gix::diff::blob::platform::resource::ByteLinesWithoutTerminator<'_> {
222    gix::diff::blob::platform::resource::ByteLinesWithoutTerminator::new(data)
223}