gitoxide_core/repository/
diff.rs

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