1use gix_error::{Error, Result};
2use std::{borrow::Cow, cell::RefCell, cmp::Ordering};
3
4use bstr::BStr;
5use gix_error::{ErrorExt, bail, message, validation};
6use gix_filter::attributes::glob::pattern::Case;
7
8use super::{Action, ChangeRef, RewriteOptions};
9use crate::rewrites;
10
11pub fn diff<'rhs, 'lhs: 'rhs, Find>(
32 lhs: &'lhs gix_index::State,
33 rhs: &'rhs gix_index::State,
34 mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action>,
35 rewrite_options: Option<RewriteOptions<'_, Find>>,
36 pathspec: &mut gix_pathspec::Search,
37 pathspec_attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool,
38) -> Result<Option<rewrites::Outcome>>
39where
40 Find: gix_object::FindObjectOrHeader,
41{
42 if lhs.is_sparse() || rhs.is_sparse() {
43 bail!(validation("Cannot diff indices that contain sparse entries"));
44 }
45 if lhs
46 .entries()
47 .iter()
48 .any(|e| e.stage() != gix_index::entry::Stage::Unconflicted)
49 {
50 bail!(validation(
51 "Unmerged entries aren't allowed in the left-hand index, only in the right-hand index"
52 ));
53 }
54
55 let lhs_range = lhs
56 .prefixed_entries_range(pathspec.common_prefix())
57 .unwrap_or_else(|| 0..lhs.entries().len());
58 let rhs_range = rhs
59 .prefixed_entries_range(pathspec.common_prefix())
60 .unwrap_or_else(|| 0..rhs.entries().len());
61
62 let pattern_matches = RefCell::new(|relative_path, entry: &gix_index::Entry| {
63 pathspec
64 .pattern_matching_relative_path(relative_path, Some(entry.mode.is_submodule()), pathspec_attributes)
65 .is_some_and(|m| !m.is_excluded())
66 });
67
68 let (mut lhs_iter, mut rhs_iter) = (
69 lhs.entries()[lhs_range.clone()]
70 .iter()
71 .enumerate()
72 .map(|(idx, e)| (idx + lhs_range.start, e.path(lhs), e))
73 .filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)),
74 rhs.entries()[rhs_range.clone()]
75 .iter()
76 .enumerate()
77 .map(|(idx, e)| (idx + rhs_range.start, e.path(rhs), e))
78 .filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)),
79 );
80
81 let mut resource_cache_storage = None;
82 let mut tracker = rewrite_options.map(
83 |RewriteOptions {
84 resource_cache,
85 rewrites,
86 find,
87 }| {
88 resource_cache_storage = Some((resource_cache, find));
89 rewrites::Tracker::<ChangeRef<'lhs, 'rhs>>::new(rewrites)
90 },
91 );
92
93 let (mut lhs_storage, mut rhs_storage) = (lhs_iter.next(), rhs_iter.next());
94 loop {
95 match (lhs_storage, rhs_storage) {
96 (Some(lhs), Some(rhs)) => {
97 let (lhs_idx, lhs_path, lhs_entry) = lhs;
98 let (rhs_idx, rhs_path, rhs_entry) = rhs;
99 match lhs_path.cmp(rhs_path) {
100 Ordering::Less => match emit_deletion(lhs, &mut cb, tracker.as_mut())? {
101 std::ops::ControlFlow::Continue(()) => {
102 lhs_storage = lhs_iter.next();
103 }
104 std::ops::ControlFlow::Break(()) => return Ok(None),
105 },
106 Ordering::Equal => {
107 if ignore_unmerged_and_intent_to_add(rhs) {
108 rhs_storage = rhs_iter.next();
109 lhs_storage = lhs_iter.next();
110 continue;
111 }
112 if lhs_entry.id != rhs_entry.id || lhs_entry.mode != rhs_entry.mode {
113 let change = ChangeRef::Modification {
114 location: Cow::Borrowed(rhs_path),
115 previous_index: lhs_idx,
116 previous_entry_mode: lhs_entry.mode,
117 previous_id: Cow::Borrowed(lhs_entry.id.as_ref()),
118 index: rhs_idx,
119 entry_mode: rhs_entry.mode,
120 id: Cow::Borrowed(rhs_entry.id.as_ref()),
121 };
122
123 let change = match tracker.as_mut() {
124 None => Some(change),
125 Some(tracker) => tracker.try_push_change(change, rhs_path),
126 };
127 if let Some(change) = change {
128 match cb(change).map_err(callback_error)? {
129 std::ops::ControlFlow::Continue(()) => {}
130 std::ops::ControlFlow::Break(()) => return Ok(None),
131 }
132 }
133 }
134 lhs_storage = lhs_iter.next();
135 rhs_storage = rhs_iter.next();
136 }
137 Ordering::Greater => match emit_addition(rhs, &mut cb, tracker.as_mut())? {
138 std::ops::ControlFlow::Continue(()) => {
139 rhs_storage = rhs_iter.next();
140 }
141 std::ops::ControlFlow::Break(()) => return Ok(None),
142 },
143 }
144 }
145 (Some(lhs), None) => match emit_deletion(lhs, &mut cb, tracker.as_mut())? {
146 std::ops::ControlFlow::Break(()) => return Ok(None),
147 std::ops::ControlFlow::Continue(()) => {
148 lhs_storage = lhs_iter.next();
149 }
150 },
151 (None, Some(rhs)) => match emit_addition(rhs, &mut cb, tracker.as_mut())? {
152 std::ops::ControlFlow::Break(()) => return Ok(None),
153 std::ops::ControlFlow::Continue(()) => {
154 rhs_storage = rhs_iter.next();
155 }
156 },
157 (None, None) => break,
158 }
159 }
160
161 if let Some((mut tracker, (resource_cache, find))) = tracker.zip(resource_cache_storage) {
162 let mut cb_err = None;
163 let out = tracker.emit(
164 |dst, src| {
165 let change = if let Some(src) = src {
166 let (lhs_path, lhs_index, lhs_mode, lhs_id) = src.change.fields();
167 let (rhs_path, rhs_index, rhs_mode, rhs_id) = dst.change.fields();
168 ChangeRef::Rewrite {
169 source_location: Cow::Owned(lhs_path.into()),
170 source_index: lhs_index,
171 source_entry_mode: lhs_mode,
172 source_id: Cow::Owned(lhs_id.into()),
173 location: Cow::Owned(rhs_path.into()),
174 index: rhs_index,
175 entry_mode: rhs_mode,
176 id: Cow::Owned(rhs_id.into()),
177 copy: match src.kind {
178 rewrites::tracker::visit::SourceKind::Rename => false,
179 rewrites::tracker::visit::SourceKind::Copy => true,
180 },
181 }
182 } else {
183 dst.change
184 };
185 match cb(change) {
186 Ok(std::ops::ControlFlow::Continue(())) => std::ops::ControlFlow::Continue(()),
187 Ok(std::ops::ControlFlow::Break(())) => std::ops::ControlFlow::Break(()),
188 Err(err) => {
189 cb_err = Some(callback_error(err));
190 std::ops::ControlFlow::Break(())
191 }
192 }
193 },
194 resource_cache,
195 find,
196 |push| {
197 for (index, entry) in lhs.entries().iter().enumerate() {
198 let path = entry.path(rhs);
199 push(
200 ChangeRef::Modification {
201 location: Cow::Borrowed(path),
202 previous_index: 0, previous_entry_mode: entry.mode,
204 previous_id: Cow::Owned(entry.id.kind().null()),
205 index,
206 entry_mode: entry.mode,
207 id: Cow::Borrowed(entry.id.as_ref()),
208 },
209 path,
210 );
211 }
212 Ok::<_, std::convert::Infallible>(())
213 },
214 )?;
215
216 if let Some(err) = cb_err {
217 Err(err)
218 } else {
219 Ok(Some(out))
220 }
221 } else {
222 Ok(None)
223 }
224}
225
226fn emit_deletion<'rhs, 'lhs: 'rhs>(
227 (idx, path, entry): (usize, &'lhs BStr, &'lhs gix_index::Entry),
228 mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action>,
229 tracker: Option<&mut rewrites::Tracker<ChangeRef<'lhs, 'rhs>>>,
230) -> Result<Action> {
231 let change = ChangeRef::Deletion {
232 location: Cow::Borrowed(path),
233 index: idx,
234 entry_mode: entry.mode,
235 id: Cow::Borrowed(entry.id.as_ref()),
236 };
237
238 let change = match tracker {
239 None => change,
240 Some(tracker) => match tracker.try_push_change(change, path) {
241 Some(change) => change,
242 None => return Ok(std::ops::ControlFlow::Continue(())),
243 },
244 };
245
246 cb(change).map_err(callback_error)
247}
248
249fn emit_addition<'rhs, 'lhs: 'rhs>(
250 (idx, path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry),
251 mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action>,
252 tracker: Option<&mut rewrites::Tracker<ChangeRef<'lhs, 'rhs>>>,
253) -> Result<Action> {
254 if ignore_unmerged_and_intent_to_add((idx, path, entry)) {
255 return Ok(std::ops::ControlFlow::Continue(()));
256 }
257
258 let change = ChangeRef::Addition {
259 location: Cow::Borrowed(path),
260 index: idx,
261 entry_mode: entry.mode,
262 id: Cow::Borrowed(entry.id.as_ref()),
263 };
264
265 let change = match tracker {
266 None => change,
267 Some(tracker) => match tracker.try_push_change(change, path) {
268 Some(change) => change,
269 None => return Ok(std::ops::ControlFlow::Continue(())),
270 },
271 };
272
273 cb(change).map_err(callback_error)
274}
275
276fn callback_error(err: Error) -> Error {
277 err.and_raise(message("The callback indicated failure"))
278}
279
280fn ignore_unmerged_and_intent_to_add<'rhs, 'lhs: 'rhs>(
281 (_idx, _path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry),
282) -> bool {
283 let stage = entry.stage();
284 entry.flags.contains(gix_index::entry::Flags::INTENT_TO_ADD) || stage != gix_index::entry::Stage::Unconflicted
285}