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