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