gitoxide_core/repository/
diff.rs1use 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 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}