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(gix::revision::spec::parse::Error::FindReference(gix::refs::file::find::existing::Error::NotFound {
129            name,
130        })) => {
131            let root = repo.workdir().map(ToOwned::to_owned);
132            let name = gix::path::os_string_into_bstring(name.into())?;
133
134            Ok((ObjectId::null(gix::hash::Kind::Sha1), root, name))
135        }
136        Err(err) => Err(err.into()),
137        Ok(resolved_revspec) => {
138            let blob_id = resolved_revspec
139                .single()
140                .context(format!("rev-spec '{revspec}' must resolve to a single object"))?;
141
142            let (path, _) = resolved_revspec
143                .path_and_mode()
144                .context(format!("rev-spec '{revspec}' must contain a path"))?;
145
146            Ok((blob_id.into(), None, path.into()))
147        }
148    }
149}
150
151pub fn file(
152    mut repo: gix::Repository,
153    out: &mut dyn std::io::Write,
154    old_revspec: BString,
155    new_revspec: BString,
156) -> Result<(), anyhow::Error> {
157    repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
158    repo.objects.refresh = RefreshMode::Never;
159
160    let (old_blob_id, old_root, old_path) = resolve_revspec(&repo, old_revspec)?;
161    let (new_blob_id, new_root, new_path) = resolve_revspec(&repo, new_revspec)?;
162
163    let worktree_roots = gix::diff::blob::pipeline::WorktreeRoots { old_root, new_root };
164
165    let mut resource_cache = repo.diff_resource_cache(
166        gix::diff::blob::pipeline::Mode::ToGitUnlessBinaryToTextIsPresent,
167        worktree_roots,
168    )?;
169
170    resource_cache.set_resource(
171        old_blob_id,
172        gix::object::tree::EntryKind::Blob,
173        old_path.as_ref(),
174        gix::diff::blob::ResourceKind::OldOrSource,
175        &repo.objects,
176    )?;
177    resource_cache.set_resource(
178        new_blob_id,
179        gix::object::tree::EntryKind::Blob,
180        new_path.as_ref(),
181        gix::diff::blob::ResourceKind::NewOrDestination,
182        &repo.objects,
183    )?;
184
185    let outcome = resource_cache.prepare_diff()?;
186
187    use gix::diff::blob::platform::prepare_diff::Operation;
188
189    let algorithm = match outcome.operation {
190        Operation::InternalDiff { algorithm } => algorithm,
191        Operation::ExternalCommand { .. } => {
192            unreachable!("We disabled that")
193        }
194        Operation::SourceOrDestinationIsBinary => {
195            anyhow::bail!("Source or destination is binary and we can't diff that")
196        }
197    };
198
199    let interner = gix::diff::blob::intern::InternedInput::new(
200        tokens_for_diffing(outcome.old.data.as_slice().unwrap_or_default()),
201        tokens_for_diffing(outcome.new.data.as_slice().unwrap_or_default()),
202    );
203
204    let unified_diff = UnifiedDiff::new(
205        &interner,
206        ConsumeBinaryHunk::new(BString::default(), "\n"),
207        ContextSize::symmetrical(3),
208    );
209
210    let unified_diff = gix::diff::blob::diff(algorithm, &interner, unified_diff)?;
211
212    out.write_all(unified_diff.as_bytes())?;
213
214    Ok(())
215}
216
217pub(crate) fn tokens_for_diffing(data: &[u8]) -> impl TokenSource<Token = &[u8]> {
218    gix::diff::blob::sources::byte_lines(data)
219}