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