Skip to main content

gix_diff/index/
function.rs

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
11/// Produce an entry-by-entry diff between `lhs` and `rhs`, sending changes to `cb(change) -> Action` for consumption,
12/// which would turn `lhs` into `rhs` if applied.
13/// Use `pathspec` to reduce the set of entries to look at, and `pathspec_attributes` may be used by pathspecs that perform
14/// attribute lookups.
15///
16/// If `cb` indicated that the operation should be cancelled, no error is triggered as this isn't supposed to
17/// occur through user-interaction - this diff is typically too fast.
18///
19/// Note that rewrites will be emitted at the end, so no ordering can be assumed. They will only be tracked if
20/// `rewrite_options` is `Some`. Note that the set of entries participating in rename tracking is affected by `pathspec`.
21///
22/// Return the outcome of the rewrite tracker if it was enabled.
23///
24/// Note that only `rhs` may contain unmerged entries, as `rhs` is expected to be the index read from `.git/index`.
25/// Unmerged entries are skipped entirely.
26///
27/// Conceptually, `rhs` is *ours*, and `lhs` is *theirs*.
28/// The entries in `lhs` and `rhs` are both expected to be sorted like index entries are typically sorted.
29///
30/// Note that sparse indices aren't supported, they must be "unsparsed" before.
31pub 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, /* does not matter */
203                            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}