gix_diff/index/
function.rs

1use super::{Action, ChangeRef, Error, RewriteOptions};
2use crate::rewrites;
3use bstr::BStr;
4use gix_filter::attributes::glob::pattern::Case;
5use std::borrow::Cow;
6use std::cell::RefCell;
7use std::cmp::Ordering;
8
9/// Produce an entry-by-entry diff between `lhs` and `rhs`, sending changes to `cb(change) -> Action` for consumption,
10/// which would turn `lhs` into `rhs` if applied.
11/// Use `pathspec` to reduce the set of entries to look at, and `pathspec_attributes` may be used by pathspecs that perform
12/// attribute lookups.
13///
14/// If `cb` indicated that the operation should be cancelled, no error is triggered as this isn't supposed to
15/// occur through user-interaction - this diff is typically too fast.
16///
17/// Note that rewrites will be emitted at the end, so no ordering can be assumed. They will only be tracked if
18/// `rewrite_options` is `Some`. Note that the set of entries participating in rename tracking is affected by `pathspec`.
19///
20/// Return the outcome of the rewrite tracker if it was enabled.
21///
22/// Note that only `rhs` may contain unmerged entries, as `rhs` is expected to be the index read from `.git/index`.
23/// Unmerged entries are skipped entirely.
24///
25/// Conceptually, `rhs` is *ours*, and `lhs` is *theirs*.
26/// The entries in `lhs` and `rhs` are both expected to be sorted like index entries are typically sorted.
27///
28/// Note that sparse indices aren't supported, they must be "unsparsed" before.
29pub fn diff<'rhs, 'lhs: 'rhs, E, Find>(
30    lhs: &'lhs gix_index::State,
31    rhs: &'rhs gix_index::State,
32    mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action, E>,
33    rewrite_options: Option<RewriteOptions<'_, Find>>,
34    pathspec: &mut gix_pathspec::Search,
35    pathspec_attributes: &mut dyn FnMut(&BStr, Case, bool, &mut gix_attributes::search::Outcome) -> bool,
36) -> Result<Option<rewrites::Outcome>, Error>
37where
38    E: Into<Box<dyn std::error::Error + Send + Sync>>,
39    Find: gix_object::FindObjectOrHeader,
40{
41    if lhs.is_sparse() || rhs.is_sparse() {
42        return Err(Error::IsSparse);
43    }
44    if lhs
45        .entries()
46        .iter()
47        .any(|e| e.stage() != gix_index::entry::Stage::Unconflicted)
48    {
49        return Err(Error::LhsHasUnmerged);
50    }
51
52    let lhs_range = lhs
53        .prefixed_entries_range(pathspec.common_prefix())
54        .unwrap_or_else(|| 0..lhs.entries().len());
55    let rhs_range = rhs
56        .prefixed_entries_range(pathspec.common_prefix())
57        .unwrap_or_else(|| 0..rhs.entries().len());
58
59    let pattern_matches = RefCell::new(|relative_path, entry: &gix_index::Entry| {
60        pathspec
61            .pattern_matching_relative_path(relative_path, Some(entry.mode.is_submodule()), pathspec_attributes)
62            .is_some_and(|m| !m.is_excluded())
63    });
64
65    let (mut lhs_iter, mut rhs_iter) = (
66        lhs.entries()[lhs_range.clone()]
67            .iter()
68            .enumerate()
69            .map(|(idx, e)| (idx + lhs_range.start, e.path(lhs), e))
70            .filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)),
71        rhs.entries()[rhs_range.clone()]
72            .iter()
73            .enumerate()
74            .map(|(idx, e)| (idx + rhs_range.start, e.path(rhs), e))
75            .filter(|(_, path, e)| pattern_matches.borrow_mut()(path, e)),
76    );
77
78    let mut resource_cache_storage = None;
79    let mut tracker = rewrite_options.map(
80        |RewriteOptions {
81             resource_cache,
82             rewrites,
83             find,
84         }| {
85            resource_cache_storage = Some((resource_cache, find));
86            rewrites::Tracker::<ChangeRef<'lhs, 'rhs>>::new(rewrites)
87        },
88    );
89
90    let (mut lhs_storage, mut rhs_storage) = (lhs_iter.next(), rhs_iter.next());
91    loop {
92        match (lhs_storage, rhs_storage) {
93            (Some(lhs), Some(rhs)) => {
94                let (lhs_idx, lhs_path, lhs_entry) = lhs;
95                let (rhs_idx, rhs_path, rhs_entry) = rhs;
96                match lhs_path.cmp(rhs_path) {
97                    Ordering::Less => match emit_deletion(lhs, &mut cb, tracker.as_mut())? {
98                        Action::Continue => {
99                            lhs_storage = lhs_iter.next();
100                        }
101                        Action::Cancel => return Ok(None),
102                    },
103                    Ordering::Equal => {
104                        if ignore_unmerged_and_intent_to_add(rhs) {
105                            rhs_storage = rhs_iter.next();
106                            lhs_storage = lhs_iter.next();
107                            continue;
108                        }
109                        if lhs_entry.id != rhs_entry.id || lhs_entry.mode != rhs_entry.mode {
110                            let change = ChangeRef::Modification {
111                                location: Cow::Borrowed(rhs_path),
112                                previous_index: lhs_idx,
113                                previous_entry_mode: lhs_entry.mode,
114                                previous_id: Cow::Borrowed(lhs_entry.id.as_ref()),
115                                index: rhs_idx,
116                                entry_mode: rhs_entry.mode,
117                                id: Cow::Borrowed(rhs_entry.id.as_ref()),
118                            };
119
120                            let change = match tracker.as_mut() {
121                                None => Some(change),
122                                Some(tracker) => tracker.try_push_change(change, rhs_path),
123                            };
124                            if let Some(change) = change {
125                                match cb(change).map_err(|err| Error::Callback(err.into()))? {
126                                    Action::Continue => {}
127                                    Action::Cancel => return Ok(None),
128                                }
129                            }
130                        }
131                        lhs_storage = lhs_iter.next();
132                        rhs_storage = rhs_iter.next();
133                    }
134                    Ordering::Greater => match emit_addition(rhs, &mut cb, tracker.as_mut())? {
135                        Action::Continue => {
136                            rhs_storage = rhs_iter.next();
137                        }
138                        Action::Cancel => return Ok(None),
139                    },
140                }
141            }
142            (Some(lhs), None) => match emit_deletion(lhs, &mut cb, tracker.as_mut())? {
143                Action::Cancel => return Ok(None),
144                Action::Continue => {
145                    lhs_storage = lhs_iter.next();
146                }
147            },
148            (None, Some(rhs)) => match emit_addition(rhs, &mut cb, tracker.as_mut())? {
149                Action::Cancel => return Ok(None),
150                Action::Continue => {
151                    rhs_storage = rhs_iter.next();
152                }
153            },
154            (None, None) => break,
155        }
156    }
157
158    if let Some((mut tracker, (resource_cache, find))) = tracker.zip(resource_cache_storage) {
159        let mut cb_err = None;
160        let out = tracker.emit(
161            |dst, src| {
162                let change = if let Some(src) = src {
163                    let (lhs_path, lhs_index, lhs_mode, lhs_id) = src.change.fields();
164                    let (rhs_path, rhs_index, rhs_mode, rhs_id) = dst.change.fields();
165                    ChangeRef::Rewrite {
166                        source_location: Cow::Owned(lhs_path.into()),
167                        source_index: lhs_index,
168                        source_entry_mode: lhs_mode,
169                        source_id: Cow::Owned(lhs_id.into()),
170                        location: Cow::Owned(rhs_path.into()),
171                        index: rhs_index,
172                        entry_mode: rhs_mode,
173                        id: Cow::Owned(rhs_id.into()),
174                        copy: match src.kind {
175                            rewrites::tracker::visit::SourceKind::Rename => false,
176                            rewrites::tracker::visit::SourceKind::Copy => true,
177                        },
178                    }
179                } else {
180                    dst.change
181                };
182                match cb(change) {
183                    Ok(Action::Continue) => crate::tree::visit::Action::Continue,
184                    Ok(Action::Cancel) => crate::tree::visit::Action::Cancel,
185                    Err(err) => {
186                        cb_err = Some(Error::Callback(err.into()));
187                        crate::tree::visit::Action::Cancel
188                    }
189                }
190            },
191            resource_cache,
192            find,
193            |push| {
194                for (index, entry) in lhs.entries().iter().enumerate() {
195                    let path = entry.path(rhs);
196                    push(
197                        ChangeRef::Modification {
198                            location: Cow::Borrowed(path),
199                            previous_index: 0, /* does not matter */
200                            previous_entry_mode: entry.mode,
201                            previous_id: Cow::Owned(entry.id.kind().null()),
202                            index,
203                            entry_mode: entry.mode,
204                            id: Cow::Borrowed(entry.id.as_ref()),
205                        },
206                        path,
207                    );
208                }
209                Ok::<_, std::convert::Infallible>(())
210            },
211        )?;
212
213        if let Some(err) = cb_err {
214            Err(err)
215        } else {
216            Ok(Some(out))
217        }
218    } else {
219        Ok(None)
220    }
221}
222
223fn emit_deletion<'rhs, 'lhs: 'rhs, E>(
224    (idx, path, entry): (usize, &'lhs BStr, &'lhs gix_index::Entry),
225    mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action, E>,
226    tracker: Option<&mut rewrites::Tracker<ChangeRef<'lhs, 'rhs>>>,
227) -> Result<Action, Error>
228where
229    E: Into<Box<dyn std::error::Error + Send + Sync>>,
230{
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(Action::Continue),
243        },
244    };
245
246    cb(change).map_err(|err| Error::Callback(err.into()))
247}
248
249fn emit_addition<'rhs, 'lhs: 'rhs, E>(
250    (idx, path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry),
251    mut cb: impl FnMut(ChangeRef<'lhs, 'rhs>) -> Result<Action, E>,
252    tracker: Option<&mut rewrites::Tracker<ChangeRef<'lhs, 'rhs>>>,
253) -> Result<Action, Error>
254where
255    E: Into<Box<dyn std::error::Error + Send + Sync>>,
256{
257    if ignore_unmerged_and_intent_to_add((idx, path, entry)) {
258        return Ok(Action::Continue);
259    }
260
261    let change = ChangeRef::Addition {
262        location: Cow::Borrowed(path),
263        index: idx,
264        entry_mode: entry.mode,
265        id: Cow::Borrowed(entry.id.as_ref()),
266    };
267
268    let change = match tracker {
269        None => change,
270        Some(tracker) => match tracker.try_push_change(change, path) {
271            Some(change) => change,
272            None => return Ok(Action::Continue),
273        },
274    };
275
276    cb(change).map_err(|err| Error::Callback(err.into()))
277}
278
279fn ignore_unmerged_and_intent_to_add<'rhs, 'lhs: 'rhs>(
280    (_idx, _path, entry): (usize, &'rhs BStr, &'rhs gix_index::Entry),
281) -> bool {
282    let stage = entry.stage();
283    entry.flags.contains(gix_index::entry::Flags::INTENT_TO_ADD) || stage != gix_index::entry::Stage::Unconflicted
284}