gitoxide_core/repository/
diff.rs

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