gix_status/index_as_worktree_with_renames/
mod.rs

1//! Changes between the index and the worktree along with optional rename tracking.
2mod types;
3pub use types::{Context, DirwalkContext, Entry, Error, Options, Outcome, RewriteSource, Sorting, Summary, VisitEntry};
4
5mod recorder;
6pub use recorder::Recorder;
7
8pub(super) mod function {
9    use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
10    use crate::index_as_worktree_with_renames::function::rewrite::ModificationOrDirwalkEntry;
11    use crate::index_as_worktree_with_renames::{Context, Entry, Error, Options, Outcome, RewriteSource, VisitEntry};
12    use crate::is_dir_to_mode;
13    use bstr::ByteSlice;
14    use gix_worktree::stack::State;
15    use std::borrow::Cow;
16    use std::path::Path;
17
18    /// Similar to [`index_as_worktree(…)`](crate::index_as_worktree()), except that it will automatically
19    /// track renames if enabled, while additionally providing information about untracked files
20    /// (or more, depending on the configuration).
21    ///
22    /// * `index`
23    ///     - used for checking modifications, and also for knowing which files are tracked during
24    ///       the working-dir traversal.
25    /// * `worktree`
26    ///     - The root of the worktree, in a format that respects `core.precomposeUnicode`.
27    /// * `collector`
28    ///     - A [`VisitEntry`] implementation that sees the results of this operation.
29    /// * `compare`
30    ///     - An implementation to compare two blobs for equality, used during index modification checks.
31    /// * `submodule`
32    ///     - An implementation to determine the status of a submodule when encountered during
33    ///       index modification checks.
34    /// * `objects`
35    ///     - A way to obtain objects from the git object database.
36    /// * `progress`
37    ///     - A way to send progress information for the index modification checks.
38    /// * `ctx`
39    ///    -  Additional information that will be accessed during index modification checks and traversal.
40    /// * `options`
41    ///    - a way to configure both paths of the operation.
42    #[allow(clippy::too_many_arguments)]
43    pub fn index_as_worktree_with_renames<'index, T, U, Find, E>(
44        index: &'index gix_index::State,
45        worktree: &Path,
46        collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>,
47        compare: impl CompareBlobs<Output = T> + Send + Clone,
48        submodule: impl SubmoduleStatus<Output = U, Error = E> + Send + Clone,
49        objects: Find,
50        progress: &mut dyn gix_features::progress::Progress,
51        mut ctx: Context<'_>,
52        options: Options<'_>,
53    ) -> Result<Outcome, Error>
54    where
55        T: Send + Clone,
56        U: Send + Clone,
57        E: std::error::Error + Send + Sync + 'static,
58        Find: gix_object::Find + gix_object::FindHeader + Send + Clone,
59    {
60        gix_features::parallel::threads(|scope| -> Result<Outcome, Error> {
61            let (tx, rx) = std::sync::mpsc::channel();
62            let walk_outcome = options
63                .dirwalk
64                .map(|options| {
65                    gix_features::parallel::build_thread()
66                        .name("gix_status::dirwalk".into())
67                        .spawn_scoped(scope, {
68                            let tx = tx.clone();
69                            let mut collect = dirwalk::Delegate {
70                                tx,
71                                should_interrupt: ctx.should_interrupt,
72                            };
73                            let dirwalk_ctx = ctx.dirwalk;
74                            let objects = objects.clone();
75                            let mut excludes = match ctx.resource_cache.attr_stack.state() {
76                                State::CreateDirectoryAndAttributesStack { .. } | State::AttributesStack(_) => None,
77                                State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {
78                                    Some(ctx.resource_cache.attr_stack.clone())
79                                }
80                            };
81                            let mut pathspec_attr_stack = ctx
82                                .pathspec
83                                .patterns()
84                                .any(|p| !p.attributes.is_empty())
85                                .then(|| ctx.resource_cache.attr_stack.clone());
86                            let mut pathspec = ctx.pathspec.clone();
87                            move || -> Result<_, Error> {
88                                gix_dir::walk(
89                                    worktree,
90                                    gix_dir::walk::Context {
91                                        should_interrupt: Some(ctx.should_interrupt),
92                                        git_dir_realpath: dirwalk_ctx.git_dir_realpath,
93                                        current_dir: dirwalk_ctx.current_dir,
94                                        index,
95                                        ignore_case_index_lookup: dirwalk_ctx.ignore_case_index_lookup,
96                                        pathspec: &mut pathspec,
97                                        pathspec_attributes: &mut |relative_path, case, is_dir, out| {
98                                            let stack = pathspec_attr_stack
99                                                .as_mut()
100                                                .expect("can only be called if attributes are used in patterns");
101                                            stack
102                                                .set_case(case)
103                                                .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &objects)
104                                                .is_ok_and(|platform| platform.matching_attributes(out))
105                                        },
106                                        excludes: excludes.as_mut(),
107                                        objects: &objects,
108                                        explicit_traversal_root: Some(worktree),
109                                    },
110                                    options,
111                                    &mut collect,
112                                )
113                                .map_err(Error::DirWalk)
114                            }
115                        })
116                        .map_err(Error::SpawnThread)
117                })
118                .transpose()?;
119
120            let entries = &index.entries()[index
121                .prefixed_entries_range(ctx.pathspec.common_prefix())
122                .unwrap_or(0..index.entries().len())];
123
124            let filter = options.rewrites.is_some().then(|| {
125                (
126                    ctx.resource_cache.filter.worktree_filter.clone(),
127                    ctx.resource_cache.attr_stack.clone(),
128                )
129            });
130            let tracked_modifications_outcome = gix_features::parallel::build_thread()
131                .name("gix_status::index_as_worktree".into())
132                .spawn_scoped(scope, {
133                    let mut collect = tracked_modifications::Delegate { tx };
134                    let objects = objects.clone();
135                    let stack = ctx.resource_cache.attr_stack.clone();
136                    let filter = ctx.resource_cache.filter.worktree_filter.clone();
137                    move || -> Result<_, Error> {
138                        crate::index_as_worktree(
139                            index,
140                            worktree,
141                            &mut collect,
142                            compare,
143                            submodule,
144                            objects,
145                            progress,
146                            crate::index_as_worktree::Context {
147                                pathspec: ctx.pathspec,
148                                stack,
149                                filter,
150                                should_interrupt: ctx.should_interrupt,
151                            },
152                            options.tracked_file_modifications,
153                        )
154                        .map_err(Error::TrackedFileModifications)
155                    }
156                })
157                .map_err(Error::SpawnThread)?;
158
159            let tracker = options
160                .rewrites
161                .map(gix_diff::rewrites::Tracker::<ModificationOrDirwalkEntry<'index, T, U>>::new)
162                .zip(filter);
163            let rewrite_outcome = match tracker {
164                Some((mut tracker, (mut filter, mut attrs))) => {
165                    let mut entries_for_sorting = options.sorting.map(|_| Vec::new());
166                    let mut buf = Vec::new();
167                    for event in rx {
168                        let (change, location) = match event {
169                            Event::IndexEntry(record) => {
170                                let location = Cow::Borrowed(record.relative_path);
171                                (ModificationOrDirwalkEntry::Modification(record), location)
172                            }
173                            Event::DirEntry(entry, collapsed_directory_status) => {
174                                let location = Cow::Owned(entry.rela_path.clone());
175                                (
176                                    ModificationOrDirwalkEntry::DirwalkEntry {
177                                        id: rewrite::calculate_worktree_id(
178                                            options.object_hash,
179                                            worktree,
180                                            entry.disk_kind,
181                                            entry.rela_path.as_bstr(),
182                                            &mut filter,
183                                            &mut attrs,
184                                            &objects,
185                                            &mut buf,
186                                            ctx.should_interrupt,
187                                        )?,
188                                        entry,
189                                        collapsed_directory_status,
190                                    },
191                                    location,
192                                )
193                            }
194                        };
195                        if let Some(v) = entries_for_sorting.as_mut() {
196                            v.push((change, location));
197                        } else if let Some(change) = tracker.try_push_change(change, location.as_ref()) {
198                            collector.visit_entry(rewrite::change_to_entry(change, entries));
199                        }
200                    }
201
202                    let mut entries_for_sorting = entries_for_sorting.map(|mut v| {
203                        v.sort_by(|a, b| a.1.cmp(&b.1));
204                        let mut remaining = Vec::new();
205                        for (change, location) in v {
206                            if let Some(change) = tracker.try_push_change(change, location.as_ref()) {
207                                remaining.push(rewrite::change_to_entry(change, entries));
208                            }
209                        }
210                        remaining
211                    });
212
213                    let outcome = tracker.emit(
214                        |dest, src| {
215                            match src {
216                                None => {
217                                    let entry = rewrite::change_to_entry(dest.change, entries);
218                                    if let Some(v) = entries_for_sorting.as_mut() {
219                                        v.push(entry);
220                                    } else {
221                                        collector.visit_entry(entry);
222                                    }
223                                }
224                                Some(src) => {
225                                    let ModificationOrDirwalkEntry::DirwalkEntry {
226                                        id,
227                                        entry,
228                                        collapsed_directory_status,
229                                    } = dest.change
230                                    else {
231                                        unreachable!("BUG: only possible destinations are dirwalk entries (additions)");
232                                    };
233                                    let source = match src.change {
234                                        ModificationOrDirwalkEntry::Modification(record) => {
235                                            RewriteSource::RewriteFromIndex {
236                                                index_entries: entries,
237                                                source_entry: record.entry,
238                                                source_entry_index: record.entry_index,
239                                                source_rela_path: record.relative_path,
240                                                source_status: record.status.clone(),
241                                            }
242                                        }
243                                        ModificationOrDirwalkEntry::DirwalkEntry {
244                                            id,
245                                            entry,
246                                            collapsed_directory_status,
247                                        } => RewriteSource::CopyFromDirectoryEntry {
248                                            source_dirwalk_entry: entry.clone(),
249                                            source_dirwalk_entry_collapsed_directory_status:
250                                                *collapsed_directory_status,
251                                            source_dirwalk_entry_id: *id,
252                                        },
253                                    };
254
255                                    let entry = Entry::Rewrite {
256                                        source,
257                                        dirwalk_entry: entry,
258                                        dirwalk_entry_collapsed_directory_status: collapsed_directory_status,
259                                        dirwalk_entry_id: id,
260                                        diff: src.diff,
261                                        copy: src.kind == gix_diff::rewrites::tracker::visit::SourceKind::Copy,
262                                    };
263                                    if let Some(v) = entries_for_sorting.as_mut() {
264                                        v.push(entry);
265                                    } else {
266                                        collector.visit_entry(entry);
267                                    }
268                                }
269                            }
270                            gix_diff::tree::visit::Action::Continue
271                        },
272                        &mut ctx.resource_cache,
273                        &objects,
274                        |_cb| {
275                            // NOTE: to make this work, we'd want to wait the index modification check to complete.
276                            //       Then it's possible to efficiently emit the tracked files along with what we already sent,
277                            //       i.e. untracked and ignored files.
278                            gix_features::trace::debug!("full-tree copy tracking isn't currently supported");
279                            Ok::<_, std::io::Error>(())
280                        },
281                    )?;
282
283                    if let Some(mut v) = entries_for_sorting {
284                        v.sort_by(|a, b| a.destination_rela_path().cmp(b.destination_rela_path()));
285                        for entry in v {
286                            collector.visit_entry(entry);
287                        }
288                    }
289                    Some(outcome)
290                }
291                None => {
292                    let mut entries_for_sorting = options.sorting.map(|_| Vec::new());
293                    for event in rx {
294                        let entry = match event {
295                            Event::IndexEntry(record) => Entry::Modification {
296                                entries,
297                                entry: record.entry,
298                                entry_index: record.entry_index,
299                                rela_path: record.relative_path,
300                                status: record.status,
301                            },
302                            Event::DirEntry(entry, collapsed_directory_status) => Entry::DirectoryContents {
303                                entry,
304                                collapsed_directory_status,
305                            },
306                        };
307
308                        if let Some(v) = entries_for_sorting.as_mut() {
309                            v.push(entry);
310                        } else {
311                            collector.visit_entry(entry);
312                        }
313                    }
314
315                    if let Some(mut v) = entries_for_sorting {
316                        v.sort_by(|a, b| a.destination_rela_path().cmp(b.destination_rela_path()));
317                        for entry in v {
318                            collector.visit_entry(entry);
319                        }
320                    }
321                    None
322                }
323            };
324
325            let walk_outcome = walk_outcome
326                .map(|handle| handle.join().expect("no panic"))
327                .transpose()?;
328            let tracked_modifications_outcome = tracked_modifications_outcome.join().expect("no panic")?;
329            Ok(Outcome {
330                dirwalk: walk_outcome.map(|t| t.0),
331                tracked_file_modification: tracked_modifications_outcome,
332                rewrites: rewrite_outcome,
333            })
334        })
335    }
336
337    enum Event<'index, T, U> {
338        IndexEntry(crate::index_as_worktree::Record<'index, T, U>),
339        DirEntry(gix_dir::Entry, Option<gix_dir::entry::Status>),
340    }
341
342    mod tracked_modifications {
343        use crate::index_as_worktree::{EntryStatus, Record};
344        use crate::index_as_worktree_with_renames::function::Event;
345        use bstr::BStr;
346        use gix_index::Entry;
347
348        pub(super) struct Delegate<'index, T, U> {
349            pub(super) tx: std::sync::mpsc::Sender<Event<'index, T, U>>,
350        }
351
352        impl<'index, T, U> crate::index_as_worktree::VisitEntry<'index> for Delegate<'index, T, U> {
353            type ContentChange = T;
354            type SubmoduleStatus = U;
355
356            fn visit_entry(
357                &mut self,
358                _entries: &'index [Entry],
359                entry: &'index Entry,
360                entry_index: usize,
361                rela_path: &'index BStr,
362                status: EntryStatus<Self::ContentChange, Self::SubmoduleStatus>,
363            ) {
364                self.tx
365                    .send(Event::IndexEntry(Record {
366                        entry,
367                        entry_index,
368                        relative_path: rela_path,
369                        status,
370                    }))
371                    .ok();
372            }
373        }
374    }
375
376    mod dirwalk {
377        use super::Event;
378        use gix_dir::entry::Status;
379        use gix_dir::walk::Action;
380        use gix_dir::EntryRef;
381        use std::sync::atomic::{AtomicBool, Ordering};
382
383        pub(super) struct Delegate<'index, 'a, T, U> {
384            pub(super) tx: std::sync::mpsc::Sender<Event<'index, T, U>>,
385            pub(super) should_interrupt: &'a AtomicBool,
386        }
387
388        impl<T, U> gix_dir::walk::Delegate for Delegate<'_, '_, T, U> {
389            fn emit(&mut self, entry: EntryRef<'_>, collapsed_directory_status: Option<Status>) -> Action {
390                // Status never shows untracked entries of untrackable type
391                if entry.disk_kind != Some(gix_dir::entry::Kind::Untrackable) {
392                    let entry = entry.to_owned();
393                    self.tx.send(Event::DirEntry(entry, collapsed_directory_status)).ok();
394                }
395
396                if self.should_interrupt.load(Ordering::Relaxed) {
397                    Action::Cancel
398                } else {
399                    Action::Continue
400                }
401            }
402        }
403    }
404
405    mod rewrite {
406        use crate::index_as_worktree::{Change, EntryStatus};
407        use crate::index_as_worktree_with_renames::{Entry, Error};
408        use bstr::BStr;
409        use gix_diff::rewrites::tracker::ChangeKind;
410        use gix_diff::tree::visit::Relation;
411        use gix_dir::entry::Kind;
412        use gix_filter::pipeline::convert::ToGitOutcome;
413        use gix_hash::oid;
414        use gix_object::tree::EntryMode;
415        use std::io::Read;
416        use std::path::Path;
417
418        #[derive(Clone)]
419        pub enum ModificationOrDirwalkEntry<'index, T, U>
420        where
421            T: Clone,
422            U: Clone,
423        {
424            Modification(crate::index_as_worktree::Record<'index, T, U>),
425            DirwalkEntry {
426                id: gix_hash::ObjectId,
427                entry: gix_dir::Entry,
428                collapsed_directory_status: Option<gix_dir::entry::Status>,
429            },
430        }
431
432        impl<T, U> gix_diff::rewrites::tracker::Change for ModificationOrDirwalkEntry<'_, T, U>
433        where
434            T: Clone,
435            U: Clone,
436        {
437            fn id(&self) -> &oid {
438                match self {
439                    ModificationOrDirwalkEntry::Modification(m) => &m.entry.id,
440                    ModificationOrDirwalkEntry::DirwalkEntry { id, .. } => id,
441                }
442            }
443
444            fn relation(&self) -> Option<Relation> {
445                // TODO: figure out if index or worktree can provide containerization - worktree should be possible.
446                //       index would take some processing.
447                None
448            }
449
450            fn kind(&self) -> ChangeKind {
451                match self {
452                    ModificationOrDirwalkEntry::Modification(m) => match &m.status {
453                        EntryStatus::Conflict(_) | EntryStatus::IntentToAdd | EntryStatus::NeedsUpdate(_) => {
454                            ChangeKind::Modification
455                        }
456                        EntryStatus::Change(c) => match c {
457                            Change::Removed => ChangeKind::Deletion,
458                            Change::Type { .. } | Change::Modification { .. } | Change::SubmoduleModification(_) => {
459                                ChangeKind::Modification
460                            }
461                        },
462                    },
463                    ModificationOrDirwalkEntry::DirwalkEntry { .. } => ChangeKind::Addition,
464                }
465            }
466
467            fn entry_mode(&self) -> EntryMode {
468                match self {
469                    ModificationOrDirwalkEntry::Modification(c) => c.entry.mode.to_tree_entry_mode(),
470                    ModificationOrDirwalkEntry::DirwalkEntry { entry, .. } => entry.disk_kind.map(|kind| {
471                        match kind {
472                            Kind::Untrackable => {
473                                // Trees are never tracked for rewrites, so we 'pretend'.
474                                gix_object::tree::EntryKind::Tree
475                            }
476                            Kind::File => gix_object::tree::EntryKind::Blob,
477                            Kind::Symlink => gix_object::tree::EntryKind::Link,
478                            Kind::Repository | Kind::Directory => gix_object::tree::EntryKind::Tree,
479                        }
480                        .into()
481                    }),
482                }
483                .unwrap_or(gix_object::tree::EntryKind::Blob.into())
484            }
485
486            fn id_and_entry_mode(&self) -> (&oid, EntryMode) {
487                (self.id(), self.entry_mode())
488            }
489        }
490
491        /// Note that for non-files, we always return a null-sha and assume that the rename-tracking
492        /// does nothing for these anyway.
493        #[allow(clippy::too_many_arguments)]
494        pub(super) fn calculate_worktree_id(
495            object_hash: gix_hash::Kind,
496            worktree_root: &Path,
497            disk_kind: Option<gix_dir::entry::Kind>,
498            rela_path: &BStr,
499            filter: &mut gix_filter::Pipeline,
500            attrs: &mut gix_worktree::Stack,
501            objects: &dyn gix_object::Find,
502            buf: &mut Vec<u8>,
503            should_interrupt: &std::sync::atomic::AtomicBool,
504        ) -> Result<gix_hash::ObjectId, Error> {
505            let Some(kind) = disk_kind else {
506                return Ok(object_hash.null());
507            };
508
509            Ok(match kind {
510                Kind::Untrackable => {
511                    // Go along with unreadable files, they are passed along without rename tracking.
512                    return Ok(object_hash.null());
513                }
514                Kind::File => {
515                    let platform = attrs
516                        .at_entry(rela_path, None, objects)
517                        .map_err(Error::SetAttributeContext)?;
518                    let rela_path = gix_path::from_bstr(rela_path);
519                    let file_path = worktree_root.join(rela_path.as_ref());
520                    let file = std::fs::File::open(&file_path).map_err(Error::OpenWorktreeFile)?;
521                    let out = filter.convert_to_git(
522                        file,
523                        rela_path.as_ref(),
524                        &mut |_path, attrs| {
525                            platform.matching_attributes(attrs);
526                        },
527                        &mut |_buf| Ok(None),
528                    )?;
529                    match out {
530                        ToGitOutcome::Unchanged(mut file) => gix_object::compute_stream_hash(
531                            object_hash,
532                            gix_object::Kind::Blob,
533                            &mut file,
534                            file_path.metadata().map_err(Error::OpenWorktreeFile)?.len(),
535                            &mut gix_features::progress::Discard,
536                            should_interrupt,
537                        )
538                        .map_err(Error::HashFile)?,
539                        ToGitOutcome::Buffer(buf) => gix_object::compute_hash(object_hash, gix_object::Kind::Blob, buf),
540                        ToGitOutcome::Process(mut stream) => {
541                            buf.clear();
542                            stream.read_to_end(buf).map_err(Error::HashFile)?;
543                            gix_object::compute_hash(object_hash, gix_object::Kind::Blob, buf)
544                        }
545                    }
546                }
547                Kind::Symlink => {
548                    let path = worktree_root.join(gix_path::from_bstr(rela_path));
549                    let target = gix_path::into_bstr(std::fs::read_link(path).map_err(Error::ReadLink)?);
550                    gix_object::compute_hash(object_hash, gix_object::Kind::Blob, &target)
551                }
552                Kind::Directory | Kind::Repository => object_hash.null(),
553            })
554        }
555
556        #[inline]
557        pub(super) fn change_to_entry<'index, T, U>(
558            change: ModificationOrDirwalkEntry<'index, T, U>,
559            entries: &'index [gix_index::Entry],
560        ) -> Entry<'index, T, U>
561        where
562            T: Clone,
563            U: Clone,
564        {
565            match change {
566                ModificationOrDirwalkEntry::Modification(r) => Entry::Modification {
567                    entries,
568                    entry: r.entry,
569                    entry_index: r.entry_index,
570                    rela_path: r.relative_path,
571                    status: r.status,
572                },
573                ModificationOrDirwalkEntry::DirwalkEntry {
574                    id: _,
575                    entry,
576                    collapsed_directory_status,
577                } => Entry::DirectoryContents {
578                    entry,
579                    collapsed_directory_status,
580                },
581            }
582        }
583    }
584}