1use super::*;
6use crate::attributes::*;
7use crate::checkout::*;
8use crate::filter::*;
9use crate::ignore::*;
10use crate::index::*;
11use crate::types_admin::*;
12
13pub(crate) fn restore_index_entry(
14 worktree_root: &Path,
15 git_dir: &Path,
16 format: ObjectFormat,
17 db: &FileObjectDatabase,
18 entry: &IndexEntry,
19 smudge_config: Option<&GitConfig>,
20 stat_cache: Option<&IndexStatCache>,
21) -> Result<Option<IndexEntry>> {
22 restore_index_entry_maybe_delayed(
23 worktree_root,
24 git_dir,
25 format,
26 db,
27 entry,
28 smudge_config,
29 stat_cache,
30 None,
31 )
32}
33
34pub(crate) fn restore_index_entry_maybe_delayed(
35 worktree_root: &Path,
36 git_dir: &Path,
37 format: ObjectFormat,
38 db: &FileObjectDatabase,
39 entry: &IndexEntry,
40 smudge_config: Option<&GitConfig>,
41 stat_cache: Option<&IndexStatCache>,
42 mut delayed: Option<&mut DelayedCheckoutQueue>,
43) -> Result<Option<IndexEntry>> {
44 if sley_index::is_gitlink(entry.mode) {
50 let dir_path = worktree_path(worktree_root, entry.path.as_bytes())?;
51 materialize_gitlink_dir(worktree_root, &dir_path)?;
52 return Ok(None);
53 }
54 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
55 if let Some(stat_cache) = stat_cache {
56 if let Ok(metadata) = fs::symlink_metadata(&file_path) {
57 if stat_cache
58 .reuse_index_entry_for_checkout(entry, &metadata)
59 .is_some()
60 {
61 return Ok(None);
62 }
63 }
64 }
65 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
66 let body: Cow<'_, [u8]> = match smudge_config {
67 Some(config) => {
68 let checks = smudge_attribute_checks_from_index(
69 worktree_root,
70 git_dir,
71 format,
72 entry.path.as_bytes(),
73 )?;
74 match apply_smudge_filter_with_attributes_maybe_delayed(
75 config,
76 &checks,
77 entry.path.as_bytes(),
78 &object.body,
79 format,
80 delayed.is_some() && (entry.mode & 0o170000) != 0o120000,
81 )? {
82 SmudgeFilterResult::Content(body) => body,
83 SmudgeFilterResult::Delayed { process } => {
84 let Some(queue) = delayed.as_deref_mut() else {
85 return Err(GitError::InvalidFormat(
86 "smudge filter requested delay without a checkout queue".into(),
87 ));
88 };
89 queue.enqueue(
90 process,
91 entry.path.as_bytes(),
92 &TrackedEntry {
93 mode: entry.mode,
94 oid: entry.oid,
95 },
96 );
97 return Ok(Some(unmaterialized_index_entry_from_index(entry)));
98 }
99 }
100 }
101 None => Cow::Borrowed(&object.body),
102 };
103 prepare_blob_parent_dirs(worktree_root, &file_path)?;
104 remove_existing_worktree_path(&file_path)?;
105 write_blob_body_or_symlink(&file_path, entry.mode, &body, &object.body)?;
106 let metadata = fs::symlink_metadata(&file_path)?;
107 Ok(Some(index_entry_with_refreshed_stat(entry, &metadata)))
108}
109
110pub(crate) fn index_entry_with_refreshed_stat(
111 entry: &IndexEntry,
112 metadata: &fs::Metadata,
113) -> IndexEntry {
114 let mut refreshed = index_entry_from_metadata(entry.path.clone(), entry.oid, metadata);
115 refreshed.mode = entry.mode;
116 refreshed.flags = entry.flags;
117 refreshed.flags_extended = entry.flags_extended;
118 refreshed
119}
120
121pub(crate) fn restored_head_index_entry(
122 _worktree_root: &Path,
123 _db: &FileObjectDatabase,
124 path: &[u8],
125 entry: &TrackedEntry,
126) -> Result<IndexEntry> {
127 Ok(IndexEntry {
136 ctime_seconds: 0,
137 ctime_nanoseconds: 0,
138 mtime_seconds: 0,
139 mtime_nanoseconds: 0,
140 dev: 0,
141 ino: 0,
142 mode: entry.mode,
143 uid: 0,
144 gid: 0,
145 size: 0,
146 oid: entry.oid,
147 flags: path.len().min(0x0fff) as u16,
148 flags_extended: 0,
149 path: BString::from(path),
150 })
151}
152
153pub(crate) fn index_entry_is_under_path(entry_path: &[u8], directory: &[u8]) -> bool {
154 if directory.is_empty() {
155 return true;
156 }
157 entry_path
158 .strip_prefix(directory)
159 .and_then(|rest| rest.strip_prefix(b"/"))
160 .is_some()
161}
162
163pub(crate) fn index_entry_from_metadata(
164 path: impl Into<BString>,
165 oid: ObjectId,
166 metadata: &fs::Metadata,
167) -> IndexEntry {
168 let modified = metadata.modified().ok();
169 let duration = modified
170 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
171 .unwrap_or_default();
172 let mode = file_mode(metadata);
173 let path = path.into();
174 let flags = path.len().min(0x0fff) as u16;
175 let mut entry = IndexEntry {
176 ctime_seconds: duration.as_secs().min(u32::MAX as u64) as u32,
177 ctime_nanoseconds: duration.subsec_nanos(),
178 mtime_seconds: duration.as_secs().min(u32::MAX as u64) as u32,
179 mtime_nanoseconds: duration.subsec_nanos(),
180 dev: 0,
181 ino: 0,
182 mode,
183 uid: 0,
184 gid: 0,
185 size: index_size_from_metadata(metadata),
186 oid,
187 flags,
188 flags_extended: 0,
189 path,
190 };
191 apply_unix_metadata_to_index_entry(&mut entry, metadata);
192 entry
193}
194
195pub fn fill_index_entry_stat_cache(entry: &mut IndexEntry, metadata: &fs::Metadata) {
201 let duration = metadata
202 .modified()
203 .ok()
204 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
205 .unwrap_or_default();
206 entry.mtime_seconds = duration.as_secs().min(u32::MAX as u64) as u32;
207 entry.mtime_nanoseconds = duration.subsec_nanos();
208 entry.ctime_seconds = entry.mtime_seconds;
209 entry.ctime_nanoseconds = entry.mtime_nanoseconds;
210 entry.size = index_size_from_metadata(metadata);
211 apply_unix_metadata_to_index_entry(entry, metadata);
212}
213
214pub(crate) fn index_entry_from_metadata_with_filemode(
215 path: impl Into<BString>,
216 oid: ObjectId,
217 metadata: &fs::Metadata,
218 trust_filemode: bool,
219) -> IndexEntry {
220 let mut entry = index_entry_from_metadata(path, oid, metadata);
221 entry.mode = file_mode_with_trust(metadata, trust_filemode);
222 entry
223}
224
225pub(crate) fn trust_executable_bit_from_git_dir(
226 git_dir: &Path,
227 config_parameters_env: Option<&str>,
228) -> bool {
229 sley_config::read_repo_config(git_dir, config_parameters_env)
230 .ok()
231 .as_ref()
232 .map(trust_executable_bit)
233 .unwrap_or(true)
234}
235
236pub(crate) fn trust_executable_bit(config: &GitConfig) -> bool {
237 config.get_bool("core", None, "filemode").unwrap_or(true)
238}
239
240pub(crate) fn trust_symlinks_from_git_dir(
241 git_dir: &Path,
242 config_parameters_env: Option<&str>,
243) -> bool {
244 sley_config::read_repo_config(git_dir, config_parameters_env)
245 .ok()
246 .as_ref()
247 .map(trust_symlinks)
248 .unwrap_or(true)
249}
250
251pub(crate) fn trust_symlinks(config: &GitConfig) -> bool {
252 config.get_bool("core", None, "symlinks").unwrap_or(true)
253}
254
255pub(crate) fn preferred_unmerged_mode_for_untrusted_worktree(
256 entries: &[IndexEntry],
257 trust_filemode: bool,
258 trust_symlinks: bool,
259) -> Option<u32> {
260 if trust_filemode && trust_symlinks {
261 return None;
262 }
263 let preferred = entries
264 .iter()
265 .find(|entry| entry.stage() == Stage::Ours)
266 .or_else(|| entries.iter().find(|entry| entry.stage() == Stage::Base))?;
267 if (!trust_symlinks && preferred.mode == 0o120000)
268 || (!trust_filemode && matches!(preferred.mode, 0o100644 | 0o100755))
269 {
270 Some(preferred.mode)
271 } else {
272 None
273 }
274}
275
276pub(crate) fn file_mode_with_trust(metadata: &fs::Metadata, trust_filemode: bool) -> u32 {
277 if trust_filemode {
278 file_mode(metadata)
279 } else {
280 0o100644
281 }
282}
283
284#[cfg(unix)]
285pub(crate) fn apply_unix_metadata_to_index_entry(entry: &mut IndexEntry, metadata: &fs::Metadata) {
286 use std::os::unix::fs::MetadataExt;
287
288 entry.ctime_seconds = metadata.ctime().min(u32::MAX as i64).max(0) as u32;
289 entry.ctime_nanoseconds = metadata.ctime_nsec().min(u32::MAX as i64).max(0) as u32;
290 entry.dev = metadata.dev() as u32;
291 entry.ino = metadata.ino() as u32;
292 entry.uid = metadata.uid();
293 entry.gid = metadata.gid();
294}
295
296#[cfg(not(unix))]
297pub(crate) fn apply_unix_metadata_to_index_entry(
298 _entry: &mut IndexEntry,
299 _metadata: &fs::Metadata,
300) {
301}
302
303pub(crate) fn index_size_from_metadata(metadata: &fs::Metadata) -> u32 {
304 metadata.len().min(u32::MAX as u64) as u32
305}
306
307pub(crate) fn read_expected_object(
308 db: &FileObjectDatabase,
309 oid: &ObjectId,
310 expected: ObjectType,
311) -> Result<std::sync::Arc<EncodedObject>> {
312 let object = db
313 .read_object(oid)
314 .map_err(|err| expect_missing_object_kind(err, *oid, missing_kind_for_type(expected)))?;
315 if object.object_type != expected {
316 return Err(GitError::InvalidObject(format!(
317 "expected {} {}, found {}",
318 expected.as_str(),
319 oid,
320 object.object_type.as_str()
321 )));
322 }
323 Ok(object)
324}
325
326pub(crate) fn expect_missing_object_kind(
327 err: GitError,
328 oid: ObjectId,
329 expected: MissingObjectKind,
330) -> GitError {
331 match err.not_found_kind() {
332 Some(sley_core::NotFoundKind::Object { .. }) => GitError::object_kind_not_found_in(
333 oid,
334 expected,
335 MissingObjectContext::WorktreeMaterialize,
336 ),
337 _ => err,
338 }
339}
340
341pub(crate) fn missing_kind_for_type(object_type: ObjectType) -> MissingObjectKind {
342 match object_type {
343 ObjectType::Blob => MissingObjectKind::Blob,
344 ObjectType::Tree => MissingObjectKind::Tree,
345 ObjectType::Commit => MissingObjectKind::Commit,
346 ObjectType::Tag => MissingObjectKind::Tag,
347 }
348}
349
350pub(crate) fn read_commit(
351 db: &FileObjectDatabase,
352 format: ObjectFormat,
353 oid: &ObjectId,
354) -> Result<Commit> {
355 let object = read_expected_object(db, oid, ObjectType::Commit)?;
356 Commit::parse(format, &object.body)
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub(crate) struct TrackedEntry {
361 pub(crate) mode: u32,
362 pub(crate) oid: ObjectId,
363}
364
365pub(crate) fn status_change_code(old_mode: u32, new_mode: u32) -> u8 {
372 if sley_diff_merge::is_type_change(old_mode, new_mode) {
373 b'T'
374 } else {
375 b'M'
376 }
377}
378
379#[derive(Debug, Clone, Default)]
392pub(crate) struct IndexStatCache {
393 pub(crate) entries: HashMap<Vec<u8>, IndexEntry>,
394 pub(crate) index_mtime: Option<(u64, u64)>,
397}
398
399impl IndexStatCache {
400 pub(crate) fn from_index(index: &Index, index_path: &Path) -> Self {
404 let index_mtime = fs::metadata(index_path)
405 .ok()
406 .and_then(|metadata| file_mtime_parts(&metadata));
407 Self::from_index_mtime(index, index_mtime)
408 }
409
410 pub(crate) fn from_index_mtime(index: &Index, index_mtime: Option<(u64, u64)>) -> Self {
411 IndexStatCache {
412 entries: stage0_index_entries(index),
413 index_mtime,
414 }
415 }
416
417 pub(crate) fn from_index_mtime_only(index_mtime: Option<(u64, u64)>) -> Self {
418 IndexStatCache {
419 entries: HashMap::new(),
420 index_mtime,
421 }
422 }
423
424 fn is_racily_clean(&self, entry: &IndexEntry) -> bool {
434 let Some(index_mtime) = self.index_mtime else {
435 return true;
436 };
437 if index_mtime == (0, 0) {
438 return true;
439 }
440 let entry_mtime = (
441 u64::from(entry.mtime_seconds),
442 u64::from(entry.mtime_nanoseconds),
443 );
444 if entry_mtime == (0, 0) {
445 return true;
446 }
447 index_mtime <= entry_mtime
449 }
450
451 fn is_racily_clean_ref(&self, entry: &IndexEntryRef<'_>) -> bool {
452 let Some(index_mtime) = self.index_mtime else {
453 return true;
454 };
455 if index_mtime == (0, 0) {
456 return true;
457 }
458 let entry_mtime = (
459 u64::from(entry.mtime_seconds),
460 u64::from(entry.mtime_nanoseconds),
461 );
462 if entry_mtime == (0, 0) {
463 return true;
464 }
465 index_mtime <= entry_mtime
466 }
467
468 fn contains(&self, git_path: &[u8]) -> bool {
471 self.entries.contains_key(git_path)
472 }
473
474 fn tracked_entry(&self, git_path: &[u8]) -> Option<TrackedEntry> {
475 self.entries.get(git_path).map(|entry| TrackedEntry {
476 mode: entry.mode,
477 oid: entry.oid,
478 })
479 }
480
481 pub(crate) fn index_entry(&self, git_path: &[u8]) -> Option<&IndexEntry> {
482 self.entries.get(git_path)
483 }
484
485 pub(crate) fn reuse_tracked_entry(
493 &self,
494 git_path: &[u8],
495 worktree_metadata: &fs::Metadata,
496 ) -> Option<TrackedEntry> {
497 let entry = self.entries.get(git_path)?;
498 self.reuse_index_entry(entry, worktree_metadata)
499 }
500
501 pub(crate) fn reuse_index_entry(
502 &self,
503 entry: &IndexEntry,
504 worktree_metadata: &fs::Metadata,
505 ) -> Option<TrackedEntry> {
506 if sley_index::is_gitlink(entry.mode) {
511 return match sley_index::gitlink_stat_verdict(worktree_metadata) {
512 sley_index::GitlinkStatVerdict::Populated => Some(TrackedEntry {
513 mode: entry.mode,
514 oid: entry.oid,
515 }),
516 sley_index::GitlinkStatVerdict::TypeChanged => None,
517 };
518 }
519 if entry.mode != worktree_entry_mode(worktree_metadata) {
520 return None;
521 }
522 if !worktree_entry_is_uptodate(entry, worktree_metadata) {
523 return None;
524 }
525 if self.is_racily_clean(entry) {
526 return None;
527 }
528 Some(TrackedEntry {
529 mode: entry.mode,
530 oid: entry.oid,
531 })
532 }
533
534 fn reuse_index_entry_for_checkout(
535 &self,
536 entry: &IndexEntry,
537 worktree_metadata: &fs::Metadata,
538 ) -> Option<TrackedEntry> {
539 if let Some(tracked) = self.reuse_index_entry(entry, worktree_metadata) {
540 return Some(tracked);
541 }
542 if u64::from(entry.size) != 0 || worktree_metadata.len() == 0 {
543 return None;
544 }
545 if entry.mode != worktree_entry_mode(worktree_metadata) {
546 return None;
547 }
548 let (mtime_seconds, mtime_nanoseconds) = file_mtime_parts(worktree_metadata)?;
549 if u64::from(entry.mtime_seconds) != mtime_seconds
550 || u64::from(entry.mtime_nanoseconds) != mtime_nanoseconds
551 {
552 return None;
553 }
554 if self.is_racily_clean(entry) {
555 return None;
556 }
557 Some(TrackedEntry {
558 mode: entry.mode,
559 oid: entry.oid,
560 })
561 }
562
563 pub(crate) fn reuse_index_entry_ref(
564 &self,
565 entry: &IndexEntryRef<'_>,
566 worktree_metadata: &fs::Metadata,
567 ) -> Option<TrackedEntry> {
568 if sley_index::is_gitlink(entry.mode) {
569 return match sley_index::gitlink_stat_verdict(worktree_metadata) {
570 sley_index::GitlinkStatVerdict::Populated => Some(TrackedEntry {
571 mode: entry.mode,
572 oid: entry.oid,
573 }),
574 sley_index::GitlinkStatVerdict::TypeChanged => None,
575 };
576 }
577 if entry.mode != worktree_entry_mode(worktree_metadata) {
578 return None;
579 }
580 if !worktree_entry_ref_is_uptodate(entry, worktree_metadata) {
581 return None;
582 }
583 if self.is_racily_clean_ref(entry) {
584 return None;
585 }
586 Some(TrackedEntry {
587 mode: entry.mode,
588 oid: entry.oid,
589 })
590 }
591
592 fn gitlink_entry(&self, git_path: &[u8]) -> Option<&IndexEntry> {
594 self.entries
595 .get(git_path)
596 .filter(|entry| sley_index::is_gitlink(entry.mode))
597 }
598}
599
600pub(crate) fn read_index_entries(
601 git_dir: &Path,
602 format: ObjectFormat,
603) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
604 let db = FileObjectDatabase::from_git_dir(git_dir, format);
605 Ok(read_index_entries_with_stat_cache(git_dir, format, &db)?.0)
606}
607
608pub(crate) fn read_all_index_paths(
609 git_dir: &Path,
610 format: ObjectFormat,
611) -> Result<BTreeSet<Vec<u8>>> {
612 let index_path = repository_index_path(git_dir);
613 let bytes = match fs::read(index_path) {
614 Ok(bytes) => bytes,
615 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
616 Err(err) => return Err(err.into()),
617 };
618 let index = Index::parse(&bytes, format)?;
619 Ok(index
620 .entries
621 .into_iter()
622 .map(|entry| entry.path.into_bytes())
623 .collect())
624}
625
626pub(crate) fn resolve_head_tree_oid(
627 git_dir: &Path,
628 format: ObjectFormat,
629 db: &FileObjectDatabase,
630) -> Result<Option<ObjectId>> {
631 let Some(commit_oid) = resolve_head_commit_oid(git_dir, format)? else {
632 return Ok(None);
633 };
634 if let Some(tree_oid) = sley_rev::commit_graph_tree_oid(git_dir, format, &commit_oid)? {
635 return Ok(Some(tree_oid));
636 }
637 let object = read_expected_object(db, &commit_oid, ObjectType::Commit)?;
638 let commit = Commit::parse_ref(format, &object.body)?;
639 Ok(Some(commit.tree))
640}
641
642pub(crate) fn resolve_head_commit_oid(
643 git_dir: &Path,
644 format: ObjectFormat,
645) -> Result<Option<ObjectId>> {
646 let refs = FileRefStore::new(git_dir, format);
647 sley_refs::resolve_ref_peeled(&refs, "HEAD")
648}
649
650pub(crate) fn status_row_is_untracked_or_ignored(entry: ShortStatusRow<'_>) -> bool {
651 matches!((entry.index, entry.worktree), (b'?', b'?') | (b'!', b'!'))
652}
653
654pub(crate) fn checkout_switch_head_symbolic(
655 refs: &FileRefStore,
656 branch_ref: String,
657 committer: Vec<u8>,
658 branch: &str,
659 old_oid: Option<ObjectId>,
660 new_oid: Option<ObjectId>,
661) -> Result<()> {
662 let from = match refs.read_ref("HEAD") {
666 Ok(Some(RefTarget::Symbolic(name))) => name
667 .strip_prefix("refs/heads/")
668 .unwrap_or(&name)
669 .to_string(),
670 Ok(Some(RefTarget::Direct(oid))) => oid.to_hex(),
671 _ => "HEAD".to_string(),
672 };
673 let mut tx = refs.transaction();
674 let reflog = match (old_oid, new_oid) {
675 (Some(old_oid), Some(new_oid)) => Some(ReflogEntry {
676 old_oid,
677 new_oid,
678 committer,
679 message: format!("checkout: moving from {from} to {branch}").into_bytes(),
680 }),
681 _ => None,
682 };
683 tx.update(RefUpdate {
684 name: "HEAD".into(),
685 expected: None,
686 new: RefTarget::Symbolic(branch_ref),
687 reflog,
688 });
689 tx.commit()
690}
691
692pub(crate) fn head_matches_index_from_entries(
693 index: &Index,
694 format: ObjectFormat,
695 head_tree_oid: &ObjectId,
696) -> Result<bool> {
697 if index
698 .entries
699 .iter()
700 .any(|entry| entry.stage() != Stage::Normal)
701 {
702 return Ok(false);
703 }
704 let index_tree = index_entry_tree_oid_without_cache(index, format)?;
705 Ok(&index_tree == head_tree_oid)
706}
707
708pub(crate) fn head_matches_borrowed_index_from_entries(
709 index: &BorrowedIndex<'_>,
710 format: ObjectFormat,
711 head_tree_oid: &ObjectId,
712) -> Result<bool> {
713 if index
714 .entries
715 .iter()
716 .any(|entry| entry.stage() != Stage::Normal)
717 {
718 return Ok(false);
719 }
720 let index_tree = borrowed_index_entry_tree_oid_without_cache(index, format)?;
721 Ok(&index_tree == head_tree_oid)
722}
723
724pub(crate) fn read_index_entries_with_stat_cache(
728 git_dir: &Path,
729 format: ObjectFormat,
730 db: &FileObjectDatabase,
731) -> Result<(BTreeMap<Vec<u8>, TrackedEntry>, IndexStatCache, bool)> {
732 let (index, stat_cache, head_matches_index) = read_index_with_stat_cache(git_dir, format, db)?;
733 let tracked = index_entries_from_index(index);
734 Ok((tracked, stat_cache, head_matches_index))
735}
736
737pub(crate) fn index_entries_from_index(index: Index) -> BTreeMap<Vec<u8>, TrackedEntry> {
738 index
739 .entries
740 .into_iter()
741 .filter(|entry| entry.stage() == Stage::Normal)
742 .map(|entry| {
743 (
744 entry.path.into_bytes(),
745 TrackedEntry {
746 mode: entry.mode,
747 oid: entry.oid,
748 },
749 )
750 })
751 .collect()
752}
753
754pub(crate) fn read_index_with_stat_cache(
755 git_dir: &Path,
756 format: ObjectFormat,
757 db: &FileObjectDatabase,
758) -> Result<(Index, IndexStatCache, bool)> {
759 read_index_with_stat_cache_entries(git_dir, format, db, true)
760}
761
762pub(crate) fn read_index_with_stat_cache_entries(
763 git_dir: &Path,
764 format: ObjectFormat,
765 db: &FileObjectDatabase,
766 include_entries: bool,
767) -> Result<(Index, IndexStatCache, bool)> {
768 let index_path = repository_index_path(git_dir);
769 let index_metadata = match fs::metadata(&index_path) {
770 Ok(metadata) => metadata,
771 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
772 return Ok((
773 Index {
774 version: 2,
775 entries: Vec::new(),
776 extensions: Vec::new(),
777 checksum: None,
778 },
779 IndexStatCache::default(),
780 false,
781 ));
782 }
783 Err(err) => return Err(err.into()),
784 };
785 let index = sley_index::read_repository_index(git_dir, format)?;
786 let index_mtime = file_mtime_parts(&index_metadata);
787 let stat_cache = if include_entries {
788 IndexStatCache::from_index_mtime(&index, index_mtime)
789 } else {
790 IndexStatCache::from_index_mtime_only(index_mtime)
791 };
792 let head_matches_index = match resolve_head_tree_oid(git_dir, format, db)? {
793 Some(head_tree_oid) => head_matches_index_from_entries(&index, format, &head_tree_oid)?,
794 None => false,
795 };
796 Ok((index, stat_cache, head_matches_index))
797}
798
799pub(crate) fn head_tree_entries(
800 git_dir: &Path,
801 format: ObjectFormat,
802 db: &FileObjectDatabase,
803) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
804 let refs = FileRefStore::new(git_dir, format);
805 let Some(head) = refs.read_ref("HEAD")? else {
806 return Ok(BTreeMap::new());
807 };
808 let commit_oid = match head {
809 RefTarget::Direct(oid) => Some(oid),
810 RefTarget::Symbolic(name) => match refs.read_ref(&name)? {
811 Some(RefTarget::Direct(oid)) => Some(oid),
812 _ => None,
813 },
814 };
815 let Some(commit_oid) = commit_oid else {
816 return Ok(BTreeMap::new());
817 };
818 let object = read_expected_object(db, &commit_oid, ObjectType::Commit)?;
819 let commit = Commit::parse_ref(format, &object.body)?;
820 let mut entries = BTreeMap::new();
821 collect_tree_entries(db, format, &commit.tree, &mut entries)?;
822 Ok(entries)
823}
824
825pub(crate) fn tree_entries(
826 db: &FileObjectDatabase,
827 format: ObjectFormat,
828 tree_oid: &ObjectId,
829) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
830 let mut entries = BTreeMap::new();
831 collect_tree_entries(db, format, tree_oid, &mut entries)?;
832 Ok(entries)
833}
834
835pub(crate) fn collect_tree_entries(
843 db: &FileObjectDatabase,
844 format: ObjectFormat,
845 tree_oid: &ObjectId,
846 entries: &mut BTreeMap<Vec<u8>, TrackedEntry>,
847) -> Result<()> {
848 for (path, (mode, oid)) in sley_diff_merge::flatten_tree(db, format, tree_oid)? {
849 entries.insert(path, TrackedEntry { mode, oid });
850 }
851 Ok(())
852}
853
854pub(crate) fn worktree_entries_with_stat_cache(
859 worktree_root: &Path,
860 git_dir: &Path,
861 format: ObjectFormat,
862 stat_cache: Option<&IndexStatCache>,
863 tracked_paths: Option<&BTreeSet<Vec<u8>>>,
864 ignores: Option<&mut IgnoreMatcher>,
865) -> Result<BTreeMap<Vec<u8>, TrackedEntry>> {
866 Ok(worktree_entries_with_submodule_dirt(
867 worktree_root,
868 git_dir,
869 format,
870 stat_cache,
871 tracked_paths,
872 ignores,
873 )?
874 .0)
875}
876
877pub(crate) type WorktreeEntriesWithDirt = (BTreeMap<Vec<u8>, TrackedEntry>, BTreeMap<Vec<u8>, u8>);
881
882pub(crate) type StatusWorktreeSnapshot = (
885 BTreeMap<Vec<u8>, TrackedEntry>,
886 BTreeMap<Vec<u8>, u8>,
887 HashSet<Vec<u8>>,
888);
889
890pub(crate) fn worktree_entries_with_submodule_dirt(
894 worktree_root: &Path,
895 git_dir: &Path,
896 format: ObjectFormat,
897 stat_cache: Option<&IndexStatCache>,
898 tracked_paths: Option<&BTreeSet<Vec<u8>>>,
899 ignores: Option<&mut IgnoreMatcher>,
900) -> Result<WorktreeEntriesWithDirt> {
901 let mut entries = BTreeMap::new();
902 let mut submodule_dirt_map = BTreeMap::new();
903 let mut tracked_presence = HashSet::new();
904 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
909 let mut attr_matcher = AttributeMatcher::from_worktree_base(worktree_root);
914 let attr_requested = filter_attribute_names();
915 let mut context = WorktreeEntriesWalk {
916 git_dir,
917 format,
918 config: &config,
919 matcher: &mut attr_matcher,
920 requested: &attr_requested,
921 stat_cache,
922 known_tracked_paths: tracked_paths,
923 tracked_paths,
924 ignores,
925 entries: &mut entries,
926 submodule_dirt: &mut submodule_dirt_map,
927 tracked_presence: &mut tracked_presence,
928 record_clean_tracked: true,
929 };
930 collect_worktree_entries(&mut context, worktree_root, &[])?;
931 Ok((entries, submodule_dirt_map))
932}
933
934pub(crate) fn status_worktree_entries_with_submodule_dirt(
935 worktree_root: &Path,
936 git_dir: &Path,
937 format: ObjectFormat,
938 stat_cache: &IndexStatCache,
939 known_tracked_paths: Option<&BTreeSet<Vec<u8>>>,
940 tracked_paths: Option<&BTreeSet<Vec<u8>>>,
941 ignores: Option<&mut IgnoreMatcher>,
942) -> Result<StatusWorktreeSnapshot> {
943 let mut entries = BTreeMap::new();
944 let mut submodule_dirt_map = BTreeMap::new();
945 let mut tracked_presence = HashSet::new();
946 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
947 let mut attr_matcher = AttributeMatcher::from_worktree_base(worktree_root);
948 let attr_requested = filter_attribute_names();
949 let mut context = WorktreeEntriesWalk {
950 git_dir,
951 format,
952 config: &config,
953 matcher: &mut attr_matcher,
954 requested: &attr_requested,
955 stat_cache: Some(stat_cache),
956 known_tracked_paths,
957 tracked_paths,
958 ignores,
959 entries: &mut entries,
960 submodule_dirt: &mut submodule_dirt_map,
961 tracked_presence: &mut tracked_presence,
962 record_clean_tracked: false,
963 };
964 collect_worktree_entries(&mut context, worktree_root, &[])?;
965 Ok((entries, submodule_dirt_map, tracked_presence))
966}
967
968pub(crate) fn worktree_entry_for_git_path(
969 worktree_root: &Path,
970 git_dir: &Path,
971 format: ObjectFormat,
972 git_path: &[u8],
973 expected_oid: &ObjectId,
974 expected_mode: u32,
975 stat_cache: Option<&IndexStatCache>,
976) -> Result<Option<TrackedEntry>> {
977 if git_path_has_symlink_parent(worktree_root, git_path)? {
978 return Ok(None);
979 }
980 let absolute = worktree_root.join(repo_path_to_os_path(git_path)?);
981 let metadata = match fs::symlink_metadata(&absolute) {
982 Ok(metadata) => metadata,
983 Err(err)
984 if matches!(
985 err.kind(),
986 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
987 ) =>
988 {
989 return Ok(None);
990 }
991 Err(err) => return Err(err.into()),
992 };
993
994 if sley_index::is_gitlink(expected_mode) {
995 if !metadata.is_dir() {
996 return Ok(Some(TrackedEntry {
997 mode: worktree_entry_mode(&metadata),
998 oid: ObjectId::null(format),
999 }));
1000 }
1001 let oid = sley_diff_merge::gitlink_head_oid(&absolute, format).unwrap_or(*expected_oid);
1002 return Ok(Some(TrackedEntry {
1003 mode: sley_index::GITLINK_MODE,
1004 oid,
1005 }));
1006 }
1007
1008 if metadata.is_dir() {
1009 return Ok(Some(TrackedEntry {
1010 mode: worktree_entry_mode(&metadata),
1011 oid: ObjectId::null(format),
1012 }));
1013 }
1014
1015 if !(metadata.is_file() || metadata.file_type().is_symlink()) {
1016 return Ok(Some(TrackedEntry {
1017 mode: worktree_entry_mode(&metadata),
1018 oid: ObjectId::null(format),
1019 }));
1020 }
1021
1022 if let Some(tracked) =
1023 stat_cache.and_then(|cache| cache.reuse_tracked_entry(git_path, &metadata))
1024 {
1025 return Ok(Some(tracked));
1026 }
1027
1028 let mode = worktree_entry_mode(&metadata);
1029 let body = if metadata.file_type().is_symlink() {
1030 symlink_target_bytes(&absolute)?
1031 } else {
1032 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1033 let body = fs::read(&absolute)?;
1034 let clean = apply_clean_filter(worktree_root, git_dir, &config, git_path, &body)?;
1035 let oid = match stat_cache.and_then(|cache| cache.index_entry(git_path)) {
1036 Some(index_entry) => clean_filtered_oid_for_status(
1037 format,
1038 &body,
1039 clean,
1040 index_entry.oid,
1041 index_entry.size,
1042 &metadata,
1043 )?,
1044 None => EncodedObject::new(ObjectType::Blob, clean).object_id(format)?,
1045 };
1046 return Ok(Some(TrackedEntry { mode, oid }));
1047 };
1048 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
1049 Ok(Some(TrackedEntry { mode, oid }))
1050}
1051
1052pub(crate) fn worktree_entry_for_index_entry_with_attributes(
1053 worktree_root: &Path,
1054 git_dir: &Path,
1055 format: ObjectFormat,
1056 index_entry: &IndexEntry,
1057 stat_cache: &IndexStatCache,
1058 clean_filter: &mut Option<TrackedOnlyCleanFilter>,
1059) -> Result<Option<TrackedEntry>> {
1060 let git_path = index_entry.path.as_bytes();
1061 let expected_mode = index_entry.mode;
1062 if git_path_has_symlink_parent(worktree_root, git_path)? {
1063 return Ok(None);
1064 }
1065 let absolute = worktree_root.join(repo_path_to_os_path(git_path)?);
1066 let metadata = match fs::symlink_metadata(&absolute) {
1067 Ok(metadata) => metadata,
1068 Err(err)
1069 if matches!(
1070 err.kind(),
1071 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1072 ) =>
1073 {
1074 return Ok(None);
1075 }
1076 Err(err) => return Err(err.into()),
1077 };
1078 let file_type = metadata.file_type();
1079
1080 if sley_index::is_gitlink(expected_mode) {
1081 if !file_type.is_dir() {
1082 return Ok(Some(TrackedEntry {
1083 mode: worktree_entry_mode(&metadata),
1084 oid: ObjectId::null(format),
1085 }));
1086 }
1087 let oid = sley_diff_merge::gitlink_head_oid(&absolute, format).unwrap_or(index_entry.oid);
1088 return Ok(Some(TrackedEntry {
1089 mode: sley_index::GITLINK_MODE,
1090 oid,
1091 }));
1092 }
1093
1094 if file_type.is_dir() {
1095 if expected_mode != 0o040000 {
1096 return Ok(None);
1097 }
1098 return Ok(Some(TrackedEntry {
1099 mode: worktree_entry_mode(&metadata),
1100 oid: ObjectId::null(format),
1101 }));
1102 }
1103
1104 if !(file_type.is_file() || file_type.is_symlink()) {
1105 return Ok(Some(TrackedEntry {
1106 mode: worktree_entry_mode(&metadata),
1107 oid: ObjectId::null(format),
1108 }));
1109 }
1110
1111 if let Some(tracked) = stat_cache.reuse_index_entry(index_entry, &metadata) {
1112 return Ok(Some(tracked));
1113 }
1114
1115 let mode = worktree_entry_mode(&metadata);
1116 let body = if file_type.is_symlink() {
1117 symlink_target_bytes(&absolute)?
1118 } else {
1119 let body = fs::read(&absolute)?;
1120 let clean_filter = tracked_only_clean_filter(clean_filter, worktree_root, git_dir);
1121 clean_filter.read_attributes_for_path(worktree_root, git_path)?;
1122 let checks =
1123 clean_filter
1124 .matcher
1125 .attributes_for_path(git_path, &clean_filter.requested, false);
1126 let clean =
1127 apply_clean_filter_with_attributes(&clean_filter.config, &checks, git_path, &body)?;
1128 let oid = clean_filtered_oid_for_status(
1129 format,
1130 &body,
1131 clean,
1132 index_entry.oid,
1133 index_entry.size,
1134 &metadata,
1135 )?;
1136 return Ok(Some(TrackedEntry { mode, oid }));
1137 };
1138 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
1139 Ok(Some(TrackedEntry { mode, oid }))
1140}
1141
1142pub(crate) fn worktree_entry_for_index_entry_ref_with_attributes(
1143 worktree_root: &Path,
1144 git_dir: &Path,
1145 format: ObjectFormat,
1146 index_entry: &IndexEntryRef<'_>,
1147 stat_cache: &IndexStatCache,
1148 clean_filter: &mut Option<TrackedOnlyCleanFilter>,
1149) -> Result<Option<TrackedEntry>> {
1150 let git_path = index_entry.path;
1151 let expected_mode = index_entry.mode;
1152 if git_path_has_symlink_parent(worktree_root, git_path)? {
1153 return Ok(None);
1154 }
1155 let absolute = worktree_root.join(repo_path_to_os_path(git_path)?);
1156 let metadata = match fs::symlink_metadata(&absolute) {
1157 Ok(metadata) => metadata,
1158 Err(err)
1159 if matches!(
1160 err.kind(),
1161 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1162 ) =>
1163 {
1164 return Ok(None);
1165 }
1166 Err(err) => return Err(err.into()),
1167 };
1168 let file_type = metadata.file_type();
1169
1170 if sley_index::is_gitlink(expected_mode) {
1171 if !file_type.is_dir() {
1172 return Ok(Some(TrackedEntry {
1173 mode: worktree_entry_mode(&metadata),
1174 oid: ObjectId::null(format),
1175 }));
1176 }
1177 let oid = sley_diff_merge::gitlink_head_oid(&absolute, format).unwrap_or(index_entry.oid);
1178 return Ok(Some(TrackedEntry {
1179 mode: sley_index::GITLINK_MODE,
1180 oid,
1181 }));
1182 }
1183
1184 if file_type.is_dir() {
1185 if expected_mode != 0o040000 {
1186 return Ok(None);
1187 }
1188 return Ok(Some(TrackedEntry {
1189 mode: worktree_entry_mode(&metadata),
1190 oid: ObjectId::null(format),
1191 }));
1192 }
1193
1194 if !(file_type.is_file() || file_type.is_symlink()) {
1195 return Ok(Some(TrackedEntry {
1196 mode: worktree_entry_mode(&metadata),
1197 oid: ObjectId::null(format),
1198 }));
1199 }
1200
1201 if let Some(tracked) = stat_cache.reuse_index_entry_ref(index_entry, &metadata) {
1202 return Ok(Some(tracked));
1203 }
1204
1205 let mode = worktree_entry_mode(&metadata);
1206 let body = if file_type.is_symlink() {
1207 symlink_target_bytes(&absolute)?
1208 } else {
1209 let body = fs::read(&absolute)?;
1210 let clean_filter = tracked_only_clean_filter(clean_filter, worktree_root, git_dir);
1211 clean_filter.read_attributes_for_path(worktree_root, git_path)?;
1212 let checks =
1213 clean_filter
1214 .matcher
1215 .attributes_for_path(git_path, &clean_filter.requested, false);
1216 let clean =
1217 apply_clean_filter_with_attributes(&clean_filter.config, &checks, git_path, &body)?;
1218 let oid = clean_filtered_oid_for_status(
1219 format,
1220 &body,
1221 clean,
1222 index_entry.oid,
1223 index_entry.size,
1224 &metadata,
1225 )?;
1226 return Ok(Some(TrackedEntry { mode, oid }));
1227 };
1228 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
1229 Ok(Some(TrackedEntry { mode, oid }))
1230}
1231
1232pub(crate) fn clean_filtered_oid_for_status(
1233 format: ObjectFormat,
1234 raw_body: &[u8],
1235 clean_body: Vec<u8>,
1236 index_oid: ObjectId,
1237 index_size: u32,
1238 metadata: &fs::Metadata,
1239) -> Result<ObjectId> {
1240 let clean_oid = EncodedObject::new(ObjectType::Blob, clean_body).object_id(format)?;
1241 let metadata_size = index_size_from_metadata(metadata);
1242 if clean_oid == index_oid && index_size != 0 && index_size != metadata_size {
1243 return EncodedObject::new(ObjectType::Blob, raw_body.to_vec()).object_id(format);
1244 }
1245 Ok(clean_oid)
1246}
1247
1248#[derive(Default)]
1249pub struct StatCleanFilterValidator {
1250 clean_filter: Option<TrackedOnlyCleanFilter>,
1251}
1252
1253impl StatCleanFilterValidator {
1254 pub fn new() -> Self {
1255 Self::default()
1256 }
1257
1258 pub fn validate_path(
1259 &mut self,
1260 worktree_root: &Path,
1261 git_dir: &Path,
1262 format: ObjectFormat,
1263 _index_mode: u32,
1264 index_oid: ObjectId,
1265 _index_size: u32,
1266 git_path: &[u8],
1267 absolute_path: &Path,
1268 metadata: &fs::Metadata,
1269 ) -> Result<Option<sley_diff_merge::IndexWorktreeValidatedEntry>> {
1270 if !metadata.is_file() {
1271 return Ok(None);
1272 }
1273 let clean_filter =
1274 tracked_only_clean_filter(&mut self.clean_filter, worktree_root, git_dir);
1275 clean_filter.read_attributes_for_path(worktree_root, git_path)?;
1276 let checks =
1277 clean_filter
1278 .matcher
1279 .attributes_for_path(git_path, &clean_filter.requested, false);
1280 let body = fs::read(absolute_path)?;
1281 if !clean_encoding_needs_stat_match_validation(&clean_filter.config, &checks) {
1282 let clean =
1283 apply_clean_filter_with_attributes(&clean_filter.config, &checks, git_path, &body)?;
1284 let oid = EncodedObject::new(ObjectType::Blob, clean).object_id(format)?;
1285 let oid = clean_or_raw_oid_for_index(format, &body, oid, index_oid)?;
1286 return Ok(Some(sley_diff_merge::IndexWorktreeValidatedEntry {
1287 mode: worktree_entry_mode(metadata),
1288 oid,
1289 }));
1290 }
1291 validate_clean_encoding_for_stat_match(&clean_filter.config, &checks, git_path, &body)?;
1292 let clean =
1293 apply_clean_filter_with_attributes(&clean_filter.config, &checks, git_path, &body)?;
1294 let oid = EncodedObject::new(ObjectType::Blob, clean).object_id(format)?;
1295 let oid = clean_or_raw_oid_for_index(format, &body, oid, index_oid)?;
1296 Ok(Some(sley_diff_merge::IndexWorktreeValidatedEntry {
1297 mode: worktree_entry_mode(metadata),
1298 oid,
1299 }))
1300 }
1301}
1302
1303fn clean_or_raw_oid_for_index(
1304 format: ObjectFormat,
1305 raw_body: &[u8],
1306 clean_oid: ObjectId,
1307 index_oid: ObjectId,
1308) -> Result<ObjectId> {
1309 if clean_oid == index_oid {
1310 return Ok(clean_oid);
1311 }
1312 let raw_oid = EncodedObject::new(ObjectType::Blob, raw_body.to_vec()).object_id(format)?;
1313 if raw_oid == index_oid {
1314 Ok(raw_oid)
1315 } else {
1316 Ok(clean_oid)
1317 }
1318}
1319
1320pub(crate) struct TrackedOnlyCleanFilter {
1321 pub(crate) config: GitConfig,
1322 pub(crate) matcher: AttributeMatcher,
1323 pub(crate) requested: Vec<Vec<u8>>,
1324 pub(crate) attribute_dirs: BTreeSet<Vec<u8>>,
1325}
1326
1327impl TrackedOnlyCleanFilter {
1328 pub(crate) fn read_attributes_for_path(
1329 &mut self,
1330 worktree_root: &Path,
1331 git_path: &[u8],
1332 ) -> Result<()> {
1333 self.read_attribute_dir(worktree_root, &[])?;
1334 let mut prefix = Vec::new();
1335 let mut parts = git_path.split(|byte| *byte == b'/').peekable();
1336 while let Some(part) = parts.next() {
1337 if parts.peek().is_none() {
1338 break;
1339 }
1340 if !prefix.is_empty() {
1341 prefix.push(b'/');
1342 }
1343 prefix.extend_from_slice(part);
1344 self.read_attribute_dir(worktree_root, &prefix)?;
1345 }
1346 Ok(())
1347 }
1348
1349 fn read_attribute_dir(&mut self, worktree_root: &Path, git_path: &[u8]) -> Result<()> {
1350 if !self.attribute_dirs.insert(git_path.to_vec()) {
1351 return Ok(());
1352 }
1353 let dir = if git_path.is_empty() {
1354 worktree_root.to_path_buf()
1355 } else {
1356 worktree_root.join(repo_path_to_os_path(git_path)?)
1357 };
1358 read_dir_attribute_patterns(worktree_root, &dir, &mut self.matcher)
1359 }
1360}
1361
1362pub(crate) fn tracked_only_clean_filter<'a>(
1363 clean_filter: &'a mut Option<TrackedOnlyCleanFilter>,
1364 worktree_root: &Path,
1365 git_dir: &Path,
1366) -> &'a mut TrackedOnlyCleanFilter {
1367 clean_filter.get_or_insert_with(|| TrackedOnlyCleanFilter {
1368 config: sley_config::read_repo_config(git_dir, None).unwrap_or_default(),
1369 matcher: AttributeMatcher::from_worktree_base(worktree_root),
1370 requested: filter_attribute_names(),
1371 attribute_dirs: BTreeSet::new(),
1372 })
1373}
1374
1375pub(crate) fn tracked_only_clean_filter_with_config<'a>(
1376 clean_filter: &'a mut Option<TrackedOnlyCleanFilter>,
1377 worktree_root: &Path,
1378 config: &GitConfig,
1379) -> &'a mut TrackedOnlyCleanFilter {
1380 clean_filter.get_or_insert_with(|| TrackedOnlyCleanFilter {
1381 config: config.clone(),
1382 matcher: AttributeMatcher::from_worktree_base(worktree_root),
1383 requested: filter_attribute_names(),
1384 attribute_dirs: BTreeSet::new(),
1385 })
1386}
1387
1388pub(crate) struct WorktreeEntriesWalk<'a> {
1389 git_dir: &'a Path,
1390 format: ObjectFormat,
1391 config: &'a GitConfig,
1392 matcher: &'a mut AttributeMatcher,
1393 requested: &'a [Vec<u8>],
1394 stat_cache: Option<&'a IndexStatCache>,
1395 known_tracked_paths: Option<&'a BTreeSet<Vec<u8>>>,
1396 tracked_paths: Option<&'a BTreeSet<Vec<u8>>>,
1397 ignores: Option<&'a mut IgnoreMatcher>,
1398 entries: &'a mut BTreeMap<Vec<u8>, TrackedEntry>,
1399 submodule_dirt: &'a mut BTreeMap<Vec<u8>, u8>,
1401 tracked_presence: &'a mut HashSet<Vec<u8>>,
1402 record_clean_tracked: bool,
1403}
1404
1405impl WorktreeEntriesWalk<'_> {
1406 fn mark_tracked_present(&mut self, git_path: &[u8]) {
1407 self.tracked_presence.insert(git_path.to_vec());
1408 }
1409
1410 fn tracked_entry_for(&self, git_path: &[u8]) -> Option<TrackedEntry> {
1411 self.stat_cache
1412 .and_then(|cache| cache.tracked_entry(git_path))
1413 }
1414
1415 fn should_record_tracked_entry(&self, git_path: &[u8], entry: &TrackedEntry) -> bool {
1416 self.record_clean_tracked
1417 || self.is_intent_to_add(git_path)
1418 || self
1419 .tracked_entry_for(git_path)
1420 .is_none_or(|tracked| tracked != *entry)
1421 }
1422
1423 fn is_intent_to_add(&self, git_path: &[u8]) -> bool {
1428 self.stat_cache
1429 .and_then(|cache| cache.index_entry(git_path))
1430 .is_some_and(IndexEntry::is_intent_to_add)
1431 }
1432}
1433
1434pub(crate) fn git_path_append_component(parent: &[u8], component: &std::ffi::OsStr) -> Vec<u8> {
1435 let component = os_str_component_bytes(component);
1436 let separator = usize::from(!parent.is_empty());
1437 let mut path = Vec::with_capacity(parent.len() + separator + component.len());
1438 if !parent.is_empty() {
1439 path.extend_from_slice(parent);
1440 path.push(b'/');
1441 }
1442 path.extend_from_slice(component.as_ref());
1443 path
1444}
1445
1446pub(crate) fn git_path_push_component(path: &mut Vec<u8>, component: &std::ffi::OsStr) -> usize {
1447 let original_len = path.len();
1448 let component = os_str_component_bytes(component);
1449 if !path.is_empty() {
1450 path.push(b'/');
1451 }
1452 path.extend_from_slice(component.as_ref());
1453 original_len
1454}
1455
1456#[cfg(unix)]
1457pub(crate) fn os_str_component_bytes(component: &std::ffi::OsStr) -> Cow<'_, [u8]> {
1458 use std::os::unix::ffi::OsStrExt;
1459
1460 Cow::Borrowed(component.as_bytes())
1461}
1462
1463#[cfg(not(unix))]
1464pub(crate) fn os_str_component_bytes(component: &std::ffi::OsStr) -> Cow<'_, [u8]> {
1465 Cow::Owned(component.to_string_lossy().into_owned().into_bytes())
1466}
1467
1468pub(crate) fn collect_worktree_entries(
1469 context: &mut WorktreeEntriesWalk<'_>,
1470 dir: &Path,
1471 dir_git_path: &[u8],
1472) -> Result<()> {
1473 if is_same_path(dir, context.git_dir) {
1474 return Ok(());
1475 }
1476 read_dir_attribute_patterns_for_base(dir, dir_git_path, context.matcher)?;
1480 if let Some(ignores) = context.ignores.as_deref_mut() {
1481 read_dir_ignore_patterns_for_base(dir, dir_git_path, ignores)?;
1482 }
1483 let mut dir_entries = fs::read_dir(dir)?.collect::<std::result::Result<Vec<_>, _>>()?;
1484 dir_entries.sort_by_key(|entry| entry.file_name());
1485 for entry in dir_entries {
1486 let file_name = entry.file_name();
1487 let path = entry.path();
1488 if is_dot_git_entry(&path) {
1489 continue;
1490 }
1491 if is_same_path(&path, context.git_dir) {
1492 continue;
1493 }
1494 let metadata = entry.metadata()?;
1495 let git_path = git_path_append_component(dir_git_path, &file_name);
1496 if context
1497 .ignores
1498 .as_ref()
1499 .is_some_and(|ignores| ignores.is_ignored(&git_path, metadata.is_dir()))
1500 {
1501 let tracked = context.known_tracked_paths.is_some_and(|tracked_paths| {
1502 if metadata.is_dir() {
1503 tracked_paths_may_contain(tracked_paths, &git_path)
1504 } else {
1505 tracked_paths.contains(&git_path)
1506 }
1507 });
1508 if !tracked {
1509 continue;
1510 }
1511 if metadata.is_dir() {
1512 collect_worktree_entries(context, &path, &git_path)?;
1513 continue;
1514 }
1515 }
1516 if metadata.is_dir() {
1517 if let Some(index_entry) = context
1525 .stat_cache
1526 .and_then(|cache| cache.gitlink_entry(&git_path))
1527 {
1528 context.mark_tracked_present(&git_path);
1529 let oid = sley_diff_merge::gitlink_head_oid(&path, context.format)
1530 .unwrap_or(index_entry.oid);
1531 let dirt = submodule_dirt_checked(&path)?;
1532 if dirt != 0 {
1533 context.submodule_dirt.insert(git_path.clone(), dirt);
1534 }
1535 let tracked = TrackedEntry {
1536 mode: sley_index::GITLINK_MODE,
1537 oid,
1538 };
1539 if dirt != 0 || context.should_record_tracked_entry(&git_path, &tracked) {
1540 context.entries.insert(git_path, tracked);
1541 }
1542 continue;
1543 }
1544 if is_nested_repository_boundary(&path, context.git_dir) {
1545 if let Some(tracked_paths) = context.tracked_paths
1546 && !tracked_paths_may_contain(tracked_paths, &git_path)
1547 {
1548 continue;
1549 }
1550 context.entries.insert(
1551 git_path,
1552 TrackedEntry {
1553 mode: 0o040000,
1554 oid: ObjectId::null(context.format),
1555 },
1556 );
1557 continue;
1558 }
1559 if let Some(tracked_paths) = context.tracked_paths
1560 && !tracked_paths_may_contain(tracked_paths, &git_path)
1561 {
1562 continue;
1563 }
1564 collect_worktree_entries(context, &path, &git_path)?;
1565 } else if metadata.is_file() || metadata.file_type().is_symlink() {
1566 if let Some(tracked_paths) = context.tracked_paths
1567 && !tracked_paths.contains(&git_path)
1568 {
1569 continue;
1570 }
1571 let entry_mode = worktree_entry_mode(&metadata);
1572 if let Some(tracked) = context
1579 .stat_cache
1580 .and_then(|cache| cache.reuse_tracked_entry(&git_path, &metadata))
1581 {
1582 context.mark_tracked_present(&git_path);
1583 if context.record_clean_tracked || context.is_intent_to_add(&git_path) {
1584 context.entries.insert(git_path, tracked);
1585 }
1586 continue;
1587 }
1588 if context
1595 .stat_cache
1596 .is_some_and(|cache| !cache.contains(&git_path))
1597 {
1598 context.entries.insert(
1599 git_path,
1600 TrackedEntry {
1601 mode: entry_mode,
1602 oid: ObjectId::null(context.format),
1603 },
1604 );
1605 continue;
1606 }
1607 let body = if metadata.file_type().is_symlink() {
1608 symlink_target_bytes(&path)?
1611 } else {
1612 let body = fs::read(&path)?;
1613 let checks =
1618 context
1619 .matcher
1620 .attributes_for_path(&git_path, context.requested, false);
1621 let clean =
1622 apply_clean_filter_with_attributes(context.config, &checks, &git_path, &body)?;
1623 let oid = match context
1624 .stat_cache
1625 .and_then(|cache| cache.index_entry(&git_path))
1626 {
1627 Some(index_entry) => clean_filtered_oid_for_status(
1628 context.format,
1629 &body,
1630 clean,
1631 index_entry.oid,
1632 index_entry.size,
1633 &metadata,
1634 )?,
1635 None => {
1636 EncodedObject::new(ObjectType::Blob, clean).object_id(context.format)?
1637 }
1638 };
1639 let tracked = TrackedEntry {
1640 mode: entry_mode,
1641 oid,
1642 };
1643 if context
1644 .stat_cache
1645 .is_some_and(|cache| cache.contains(&git_path))
1646 {
1647 context.mark_tracked_present(&git_path);
1648 if context.should_record_tracked_entry(&git_path, &tracked) {
1649 context.entries.insert(git_path, tracked);
1650 }
1651 } else {
1652 context.entries.insert(git_path, tracked);
1653 }
1654 continue;
1655 };
1656 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(context.format)?;
1657 let tracked = TrackedEntry {
1658 mode: entry_mode,
1659 oid,
1660 };
1661 if context
1662 .stat_cache
1663 .is_some_and(|cache| cache.contains(&git_path))
1664 {
1665 context.mark_tracked_present(&git_path);
1666 if context.should_record_tracked_entry(&git_path, &tracked) {
1667 context.entries.insert(git_path, tracked);
1668 }
1669 } else {
1670 context.entries.insert(git_path, tracked);
1671 }
1672 }
1673 }
1674 Ok(())
1675}
1676
1677pub(crate) fn tracked_paths_may_contain(
1678 tracked_paths: &BTreeSet<Vec<u8>>,
1679 directory: &[u8],
1680) -> bool {
1681 if tracked_paths.contains(directory) {
1682 return true;
1683 }
1684 let mut prefix = Vec::with_capacity(directory.len() + 1);
1685 prefix.extend_from_slice(directory);
1686 prefix.push(b'/');
1687 tracked_paths
1688 .range::<[u8], _>((
1689 std::ops::Bound::Included(prefix.as_slice()),
1690 std::ops::Bound::Unbounded,
1691 ))
1692 .next()
1693 .is_some_and(|path| path.starts_with(&prefix))
1694}
1695
1696pub(crate) fn is_same_path(left: &Path, right: &Path) -> bool {
1697 left == right
1698}
1699
1700pub(crate) fn is_dot_git_entry(path: &Path) -> bool {
1704 path.file_name() == Some(std::ffi::OsStr::new(".git"))
1705}
1706
1707pub(crate) fn is_nested_repository_boundary(path: &Path, git_dir: &Path) -> bool {
1714 let dot_git = path.join(".git");
1715 if dot_git.is_dir() {
1716 if is_same_path(&dot_git, git_dir) {
1717 return false;
1718 }
1719 return true;
1720 }
1721 sley_diff_merge::gitlink_git_dir(path).is_some_and(|embedded| !is_same_path(&embedded, git_dir))
1722}
1723
1724pub(crate) fn active_repository_worktree_dir(path: &Path, git_dir: &Path) -> bool {
1725 sley_diff_merge::gitlink_git_dir(path).is_some_and(|embedded| is_same_path(&embedded, git_dir))
1726}
1727
1728pub(crate) fn is_embedded_git_internals(root: &Path, path: &Path) -> bool {
1730 let Ok(relative) = path.strip_prefix(root) else {
1731 return false;
1732 };
1733 let mut current = root.to_path_buf();
1734 for component in relative.components() {
1735 if matches!(component, std::path::Component::Normal(name) if name == ".git")
1736 && current != root
1737 && current.join(".git").is_dir()
1738 {
1739 return true;
1740 }
1741 current.push(component);
1742 }
1743 false
1744}
1745
1746pub(crate) fn git_path_has_symlink_parent(worktree_root: &Path, git_path: &[u8]) -> Result<bool> {
1747 let text =
1748 std::str::from_utf8(git_path).map_err(|err| GitError::InvalidPath(err.to_string()))?;
1749 let mut current = worktree_root.to_path_buf();
1750 let mut components = text.split('/').peekable();
1751 while let Some(component) = components.next() {
1752 if components.peek().is_none() {
1753 break;
1754 }
1755 current.push(component);
1756 let metadata = match fs::symlink_metadata(¤t) {
1757 Ok(metadata) => metadata,
1758 Err(err)
1759 if matches!(
1760 err.kind(),
1761 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1762 ) =>
1763 {
1764 return Ok(false);
1765 }
1766 Err(err) => return Err(err.into()),
1767 };
1768 if metadata.file_type().is_symlink() {
1769 return Ok(true);
1770 }
1771 }
1772 Ok(false)
1773}
1774
1775pub(crate) fn worktree_entry_mode(metadata: &fs::Metadata) -> u32 {
1776 if metadata.file_type().is_symlink() {
1777 0o120000
1778 } else if metadata.is_dir() {
1779 0o040000
1780 } else {
1781 file_mode(metadata)
1782 }
1783}
1784
1785pub(crate) fn worktree_path(root: &Path, path: &[u8]) -> Result<PathBuf> {
1786 let text = std::str::from_utf8(path).map_err(|err| GitError::InvalidPath(err.to_string()))?;
1787 let relative = PathBuf::from(text);
1788 if relative.is_absolute()
1789 || relative.components().any(|component| {
1790 matches!(
1791 component,
1792 std::path::Component::ParentDir | std::path::Component::Prefix(_)
1793 )
1794 })
1795 {
1796 return Err(GitError::InvalidPath(format!(
1797 "invalid worktree path {text}"
1798 )));
1799 }
1800 Ok(root.join(relative))
1801}
1802
1803pub(crate) fn remove_worktree_file(root: &Path, path: &[u8]) -> Result<()> {
1804 let file = worktree_path(root, path)?;
1805 if !file.exists() {
1806 return Ok(());
1807 }
1808 if file.is_dir() {
1809 match fs::remove_dir(&file) {
1814 Ok(()) => prune_empty_parents(root, file.parent())?,
1815 Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {}
1816 Err(err) => return Err(err.into()),
1817 }
1818 return Ok(());
1819 }
1820 fs::remove_file(&file)?;
1821 prune_empty_parents(root, file.parent())?;
1822 Ok(())
1823}
1824
1825pub(crate) fn prune_empty_parents(root: &Path, mut dir: Option<&Path>) -> Result<()> {
1826 while let Some(path) = dir {
1827 if path == root || path_is_original_cwd(path) {
1828 break;
1829 }
1830 match fs::remove_dir(path) {
1831 Ok(()) => dir = path.parent(),
1832 Err(err) if err.kind() == std::io::ErrorKind::NotFound => dir = path.parent(),
1833 Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => break,
1834 Err(err) => return Err(err.into()),
1835 }
1836 }
1837 Ok(())
1838}
1839
1840pub(crate) fn original_cwd_absolute() -> Option<PathBuf> {
1841 let cwd = sley_core::original_cwd().or_else(|| env::current_dir().ok())?;
1842 Some(fs::canonicalize(&cwd).unwrap_or(cwd))
1843}
1844
1845pub(crate) fn path_is_original_cwd(path: &Path) -> bool {
1846 let Some(cwd) = original_cwd_absolute() else {
1847 return false;
1848 };
1849 let path = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1850 path == cwd
1851}
1852
1853pub(crate) fn original_cwd_is_inside(path: &Path) -> bool {
1854 let Some(cwd) = original_cwd_absolute() else {
1855 return false;
1856 };
1857 let path = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1858 cwd == path || cwd.starts_with(&path)
1859}
1860
1861pub(crate) fn refuse_if_current_working_directory_becomes_file(
1862 worktree_root: &Path,
1863 target_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1864) -> Result<()> {
1865 for (path, entry) in target_entries {
1866 if sley_index::is_gitlink(entry.mode) || (entry.mode & 0o170000) == 0o040000 {
1867 continue;
1868 }
1869 let path = worktree_path(worktree_root, path)?;
1870 if path_is_original_cwd(&path)
1871 && fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.is_dir())
1872 {
1873 return refuse_remove_current_working_directory(&path);
1874 }
1875 }
1876 Ok(())
1877}
1878
1879pub(crate) fn refuse_remove_current_working_directory(path: &Path) -> Result<()> {
1880 eprintln!(
1881 "error: Refusing to remove the current working directory:\n{}",
1882 path.display()
1883 );
1884 Err(GitError::Exit(128))
1885}
1886
1887pub(crate) fn git_tree_entry_cmp(
1888 left_name: &[u8],
1889 left_mode: u32,
1890 right_name: &[u8],
1891 right_mode: u32,
1892) -> Ordering {
1893 let shared = left_name.len().min(right_name.len());
1894 let name_order = left_name[..shared].cmp(&right_name[..shared]);
1895 if name_order != Ordering::Equal {
1896 return name_order;
1897 }
1898 let left_end = left_name.len() == shared;
1899 let right_end = right_name.len() == shared;
1900 match (left_end, right_end) {
1901 (true, true) => Ordering::Equal,
1902 (true, false) => tree_name_terminator(left_mode).cmp(&right_name[shared]),
1903 (false, true) => left_name[shared].cmp(&tree_name_terminator(right_mode)),
1904 (false, false) => Ordering::Equal,
1905 }
1906}
1907
1908pub(crate) fn tree_name_terminator(mode: u32) -> u8 {
1909 if mode == 0o040000 { b'/' } else { 0 }
1910}
1911
1912#[cfg(unix)]
1913pub(crate) fn file_mode(metadata: &fs::Metadata) -> u32 {
1914 use std::os::unix::fs::PermissionsExt;
1915 if metadata.permissions().mode() & 0o111 != 0 {
1916 0o100755
1917 } else {
1918 0o100644
1919 }
1920}
1921
1922#[cfg(not(unix))]
1923pub(crate) fn file_mode(_metadata: &fs::Metadata) -> u32 {
1924 0o100644
1925}
1926
1927#[cfg(unix)]
1932pub(crate) fn symlink_target_bytes(path: &Path) -> Result<Vec<u8>> {
1933 use std::os::unix::ffi::OsStrExt;
1934 let target = fs::read_link(path)?;
1935 Ok(target.as_os_str().as_bytes().to_vec())
1936}
1937
1938#[cfg(not(unix))]
1939pub(crate) fn symlink_target_bytes(path: &Path) -> Result<Vec<u8>> {
1940 let target = fs::read_link(path)?;
1941 Ok(target.to_string_lossy().replace('\\', "/").into_bytes())
1944}
1945
1946pub(crate) fn git_path_bytes(path: &Path) -> Result<Vec<u8>> {
1947 if path.components().any(|component| {
1948 matches!(
1949 component,
1950 std::path::Component::ParentDir | std::path::Component::Prefix(_)
1951 )
1952 }) {
1953 return Err(GitError::InvalidPath(format!(
1954 "invalid index path {}",
1955 path.display()
1956 )));
1957 }
1958 Ok(path
1959 .components()
1960 .filter_map(|component| match component {
1961 std::path::Component::Normal(value) => Some(value.to_string_lossy().into_owned()),
1962 _ => None,
1963 })
1964 .collect::<Vec<_>>()
1965 .join("/")
1966 .into_bytes())
1967}
1968
1969pub(crate) fn normalize_absolute_path_lexically(path: &Path) -> PathBuf {
1970 let mut normalized = PathBuf::new();
1971 for component in path.components() {
1972 match component {
1973 std::path::Component::CurDir => {}
1974 std::path::Component::ParentDir => {
1975 normalized.pop();
1976 }
1977 std::path::Component::Normal(_)
1978 | std::path::Component::RootDir
1979 | std::path::Component::Prefix(_) => normalized.push(component.as_os_str()),
1980 }
1981 }
1982 normalized
1983}
1984
1985pub(crate) fn absolute_path_lexically(path: &Path, cwd: &Path) -> PathBuf {
1986 if path.is_absolute() {
1987 normalize_absolute_path_lexically(path)
1988 } else {
1989 normalize_absolute_path_lexically(&cwd.join(path))
1990 }
1991}
1992
1993pub(crate) fn repo_path_to_os_path(path: &[u8]) -> Result<PathBuf> {
1994 #[cfg(unix)]
1995 {
1996 use std::os::unix::ffi::OsStrExt;
1997
1998 Ok(PathBuf::from(std::ffi::OsStr::from_bytes(path)))
1999 }
2000
2001 #[cfg(not(unix))]
2002 {
2003 let path = std::str::from_utf8(path)
2004 .map_err(|_| GitError::InvalidPath("index path is not utf8".into()))?;
2005 Ok(path.split('/').collect())
2006 }
2007}
2008
2009pub(crate) fn git_path_to_relative_path(path: &[u8]) -> Result<PathBuf> {
2010 let path = std::str::from_utf8(path)
2011 .map_err(|err| GitError::InvalidPath(format!("invalid utf-8 index path: {err}")))?;
2012 Ok(path.split('/').collect())
2013}
2014
2015pub(crate) fn path_has_trailing_separator(path: &Path) -> bool {
2016 path.as_os_str()
2017 .to_string_lossy()
2018 .ends_with(std::path::MAIN_SEPARATOR)
2019}