1mod 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 #[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 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 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 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 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 #[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 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}