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(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}