gitoxide_core/repository/
diff.rs

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