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