1use std::borrow::Cow;
19use std::collections::{BTreeMap, BTreeSet, HashMap};
20use std::fs;
21use std::io::{self, Write};
22use std::ops::Deref;
23use std::path::{Path, PathBuf};
24
25use crate::hash::verify_trailer;
26
27use crate::config::ConfigSet;
28use crate::error::{Error, Result};
29use crate::lockfile::{grit_lockfile_pid_enabled, pid_path_for_lock};
30use crate::objects::{parse_tree, HashAlgo, ObjectId, ObjectKind, TreeEntry};
31use crate::odb::Odb;
32use crate::repo::Repository;
33use crate::resolve_undo::{self, write_resolve_undo_payload, ResolveUndoRecord};
34use crate::rev_parse;
35use crate::untracked_cache;
36
37fn gitattributes_index_path(path: &[u8]) -> bool {
38 path == b".gitattributes" || path.ends_with(b"/.gitattributes")
39}
40
41pub const MODE_REGULAR: u32 = 0o100644;
43pub const MODE_EXECUTABLE: u32 = 0o100755;
45pub const MODE_SYMLINK: u32 = 0o120000;
47pub const MODE_GITLINK: u32 = 0o160000;
49pub const MODE_TREE: u32 = 0o040000;
51
52#[must_use]
54pub(crate) fn is_tree_mode(mode: u32) -> bool {
55 (mode & 0o170000) == MODE_TREE
56}
57
58#[must_use]
63pub(crate) fn canon_mode(mode: u32) -> u32 {
64 const S_IFMT: u32 = 0o170000;
65 const S_IFREG: u32 = 0o100000;
66 const S_IFLNK: u32 = 0o120000;
67 const S_IFGITLINK: u32 = 0o160000;
68 match mode & S_IFMT {
69 S_IFREG => {
70 if mode & 0o111 != 0 {
71 MODE_EXECUTABLE
72 } else {
73 MODE_REGULAR
74 }
75 }
76 S_IFLNK => MODE_SYMLINK,
77 S_IFGITLINK => MODE_GITLINK,
78 MODE_TREE => MODE_TREE,
79 _ => MODE_REGULAR,
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct StatMatchPolicy {
86 pub trust_ctime: bool,
88 pub check_stat: bool,
90 pub use_nanosec: bool,
96}
97
98impl Default for StatMatchPolicy {
99 fn default() -> Self {
100 Self {
101 trust_ctime: true,
102 check_stat: true,
103 use_nanosec: Self::DEFAULT_USE_NANOSEC,
104 }
105 }
106}
107
108impl StatMatchPolicy {
109 pub const DEFAULT_USE_NANOSEC: bool = cfg!(windows);
117
118 #[must_use]
120 pub fn from_config(config: Option<&ConfigSet>) -> Self {
121 let mut policy = Self::default();
122 let Some(cfg) = config else {
123 return policy;
124 };
125 if let Some(trust) = cfg.get_bool("core.trustctime").and_then(|r| r.ok()) {
126 policy.trust_ctime = trust;
127 }
128 if let Some(raw) = cfg.get("core.checkstat") {
129 policy.check_stat = match raw.to_ascii_lowercase().as_str() {
130 "minimal" => false,
131 "default" => true,
132 other => cfg
133 .get_bool("core.checkstat")
134 .and_then(|r| r.ok())
135 .unwrap_or(!other.eq_ignore_ascii_case("minimal")),
136 };
137 }
138 if let Some(use_ns) = cfg.get_bool("core.usenanosec").and_then(|r| r.ok()) {
139 policy.use_nanosec = use_ns;
140 }
141 policy
142 }
143}
144
145#[must_use]
150pub(crate) fn metadata_times(meta: &fs::Metadata) -> ((u32, u32), (u32, u32)) {
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::MetadataExt;
154 (
155 (meta.ctime() as u32, meta.ctime_nsec() as u32),
156 (meta.mtime() as u32, meta.mtime_nsec() as u32),
157 )
158 }
159 #[cfg(not(unix))]
160 {
161 use filetime::FileTime;
162 let mtime_ft = FileTime::from_last_modification_time(meta);
163 let ctime_ft = FileTime::from_creation_time(meta).unwrap_or(mtime_ft);
164 (
165 filetime_to_index_pair(ctime_ft),
166 filetime_to_index_pair(mtime_ft),
167 )
168 }
169}
170
171#[cfg(not(unix))]
172#[must_use]
173fn filetime_to_index_pair(ft: filetime::FileTime) -> (u32, u32) {
174 let secs = ft.unix_seconds();
175 if secs.is_negative() {
176 (0, 0)
177 } else {
178 (secs as u32, ft.nanoseconds())
179 }
180}
181
182#[must_use]
184pub fn munge_index_size(st_size: u64) -> u32 {
185 let sd_size = st_size as u32;
186 if sd_size == 0 && st_size != 0 {
187 0x8000_0000
188 } else {
189 sd_size
190 }
191}
192
193#[must_use]
195pub fn index_entries_stat_equivalent(
196 a: &IndexEntry,
197 b: &IndexEntry,
198 policy: StatMatchPolicy,
199) -> bool {
200 IndexStatFields::of_entry(a).equivalent_to(&IndexStatFields::of_entry(b), policy)
201}
202
203pub(crate) const INDEX_EXT_SPARSE_DIRECTORIES: u32 = u32::from_be_bytes(*b"sdir");
205pub(crate) const INDEX_EXT_UNTRACKED: u32 = u32::from_be_bytes(*b"UNTR");
207pub(crate) const INDEX_EXT_FSMONITOR: u32 = u32::from_be_bytes(*b"FSMN");
209const PARALLEL_TRAILER_VERIFY_MIN_BYTES: usize = 256 * 1024;
213
214const FSMONITOR_EXT_VERSION1: u32 = 1;
215const FSMONITOR_EXT_VERSION2: u32 = 2;
217pub(crate) const INDEX_EXT_RESOLVE_UNDO: u32 = u32::from_be_bytes(*b"REUC");
219pub(crate) const INDEX_EXT_LINK: u32 = u32::from_be_bytes(*b"link");
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct IndexEntry {
225 pub ctime_sec: u32,
227 pub ctime_nsec: u32,
229 pub mtime_sec: u32,
231 pub mtime_nsec: u32,
233 pub dev: u32,
235 pub ino: u32,
237 pub mode: u32,
239 pub uid: u32,
241 pub gid: u32,
243 pub size: u32,
245 pub oid: ObjectId,
247 pub flags: u16,
249 pub flags_extended: Option<u16>,
251 pub path: Vec<u8>,
253 pub base_index_pos: u32,
255}
256
257impl IndexEntry {
258 #[must_use]
260 pub fn stage(&self) -> u8 {
261 ((self.flags >> 12) & 0x3) as u8
262 }
263
264 #[must_use]
266 pub fn stat_cache_unset(&self) -> bool {
267 self.size == 0 && self.mtime_sec == 0 && self.mtime_nsec == 0
268 }
269
270 pub(crate) fn set_stage(&mut self, stage: u8) {
271 self.flags = (self.flags & 0x0FFF) | ((stage as u16 & 0x3) << 12);
272 }
273
274 #[must_use]
276 pub fn assume_unchanged(&self) -> bool {
277 self.flags & 0x8000 != 0
278 }
279
280 #[must_use]
282 pub fn skip_worktree(&self) -> bool {
283 self.flags_extended
284 .map(|f| f & 0x4000 != 0)
285 .unwrap_or(false)
286 }
287
288 pub fn set_assume_unchanged(&mut self, value: bool) {
290 if value {
291 self.flags |= 0x8000;
292 } else {
293 self.flags &= !0x8000;
294 }
295 }
296
297 pub fn set_skip_worktree(&mut self, value: bool) {
299 let fe = self.flags_extended.get_or_insert(0);
300 if value {
301 *fe |= 0x4000;
302 } else {
303 *fe &= !0x4000;
304 if *fe == 0 {
305 self.flags_extended = None;
306 }
307 }
308 }
309
310 #[must_use]
312 pub fn intent_to_add(&self) -> bool {
313 self.flags_extended
314 .map(|f| f & 0x2000 != 0)
315 .unwrap_or(false)
316 }
317
318 pub fn set_intent_to_add(&mut self, value: bool) {
320 let fe = self.flags_extended.get_or_insert(0);
321 if value {
322 *fe |= 0x2000;
323 } else {
324 *fe &= !0x2000;
325 if *fe == 0 {
326 self.flags_extended = None;
327 }
328 }
329 }
330
331 #[must_use]
333 pub fn is_sparse_directory_placeholder(&self) -> bool {
334 self.mode == MODE_TREE && self.stage() == 0 && self.skip_worktree()
335 }
336
337 const FLAG_EXT_OVERLAY_TREE_SKIP: u16 = 0x8000;
339 const FLAG_EXT_FSMONITOR_VALID: u16 = 0x1000;
341 const FLAG_EXT_CONTENT_VERIFIED: u16 = 0x0800;
343 const FLAG_EXT_ON_DISK: u16 = 0x2000 | 0x4000;
346
347 fn disk_flags_extended(fe: u16) -> u16 {
349 fe & Self::FLAG_EXT_ON_DISK
350 }
351
352 #[must_use]
354 pub fn overlay_tree_skip_output(&self) -> bool {
355 self.flags_extended
356 .is_some_and(|fe| fe & Self::FLAG_EXT_OVERLAY_TREE_SKIP != 0)
357 }
358
359 fn set_overlay_tree_skip_output(&mut self, value: bool) {
360 let fe = self.flags_extended.get_or_insert(0);
361 if value {
362 *fe |= Self::FLAG_EXT_OVERLAY_TREE_SKIP;
363 } else {
364 *fe &= !Self::FLAG_EXT_OVERLAY_TREE_SKIP;
365 if *fe == 0 {
366 self.flags_extended = None;
367 }
368 }
369 }
370
371 #[must_use]
373 pub fn fsmonitor_valid(&self) -> bool {
374 self.flags_extended
375 .is_some_and(|fe| fe & Self::FLAG_EXT_FSMONITOR_VALID != 0)
376 }
377
378 pub fn set_fsmonitor_valid(&mut self, value: bool) {
380 let fe = self.flags_extended.get_or_insert(0);
381 if value {
382 *fe |= Self::FLAG_EXT_FSMONITOR_VALID;
383 } else {
384 *fe &= !Self::FLAG_EXT_FSMONITOR_VALID;
385 if *fe == 0 {
386 self.flags_extended = None;
387 }
388 }
389 }
390
391 pub(crate) fn extended_flags_equal_ignoring_volatile(
393 left: Option<u16>,
394 right: Option<u16>,
395 ) -> bool {
396 (left.unwrap_or(0) & Self::FLAG_EXT_ON_DISK)
397 == (right.unwrap_or(0) & Self::FLAG_EXT_ON_DISK)
398 }
399
400 pub(crate) fn mark_content_verified(&mut self) {
403 *self.flags_extended.get_or_insert(0) |= Self::FLAG_EXT_CONTENT_VERIFIED;
404 }
405
406 pub(crate) fn take_content_verified(&mut self) -> bool {
408 let Some(fe) = self.flags_extended else {
409 return false;
410 };
411 if fe & Self::FLAG_EXT_CONTENT_VERIFIED == 0 {
412 return false;
413 }
414 let rest = fe & !Self::FLAG_EXT_CONTENT_VERIFIED;
415 self.flags_extended = (rest != 0).then_some(rest);
416 true
417 }
418}
419
420#[derive(Debug, Clone)]
436pub struct Index {
437 pub version: u32,
439 pub(crate) entries: Vec<IndexEntry>,
441 pub(crate) entries_sorted: bool,
443 pub sparse_directories: bool,
445 pub untracked_cache: Option<untracked_cache::UntrackedCache>,
447 pub fsmonitor_last_update: Option<String>,
449 pub(crate) fsmonitor_dirty: Option<crate::ewah_bitmap::EwahBitmap>,
451 pub(crate) fsmonitor_raw_extension: Option<Vec<u8>>,
453 pub resolve_undo: Option<BTreeMap<Vec<u8>, ResolveUndoRecord>>,
455 pub(crate) split_link: Option<crate::split_index::SplitIndexLink>,
457 pub cache_tree_root: Option<ObjectId>,
459 pub cache_tree: Option<CacheTreeNode>,
461 pub hash_algo: HashAlgo,
465 pub source_mtime: Option<(u32, u32)>,
467 pub(crate) source_checksum: Option<Vec<u8>>,
469 pub(crate) source_file_len: Option<u64>,
471 pub(crate) defer_untracked_cache_invalidation: bool,
473 pub(crate) skip_racy_smudge_on_write: bool,
475 pub(crate) content_verified: HashMap<Vec<u8>, ObjectId>,
481 pub(crate) fsmonitor_refresh_ran: bool,
483 pub(crate) suppress_pre_write_index_housekeeping: bool,
485 pub(crate) entry_modes_repaired_on_load: bool,
487}
488
489impl Default for Index {
490 fn default() -> Self {
491 Self::new()
492 }
493}
494
495#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct CacheTreeNode {
498 pub name: Vec<u8>,
500 pub entry_count: i32,
502 pub oid: Option<ObjectId>,
504 pub children: Vec<CacheTreeNode>,
506}
507
508impl CacheTreeNode {
509 #[must_use]
511 pub fn valid(
512 name: Vec<u8>,
513 entry_count: i32,
514 oid: ObjectId,
515 children: Vec<CacheTreeNode>,
516 ) -> Self {
517 Self {
518 name,
519 entry_count,
520 oid: Some(oid),
521 children,
522 }
523 }
524
525 pub fn invalidate(&mut self) {
527 self.entry_count = -1;
528 self.oid = None;
529 }
530
531 #[must_use]
533 pub fn is_valid(&self) -> bool {
534 self.entry_count >= 0 && self.oid.is_some()
535 }
536}
537
538pub use crate::index_eoie_ieot::IndexParseContext;
539
540#[derive(Debug, Clone, Copy)]
542pub struct IndexLoadOptions {
543 pub expand_sparse_directories: bool,
545 pub parse_context: IndexParseContext,
547}
548
549impl Default for IndexLoadOptions {
550 fn default() -> Self {
551 Self {
552 expand_sparse_directories: true,
553 parse_context: IndexParseContext::default(),
554 }
555 }
556}
557
558const INDEX_ENV_INVALID_FALLBACK: u32 = 3;
560const INDEX_CONFIG_INVALID_FALLBACK: u32 = 3;
562const INDEX_FORMAT_LB: u32 = 2;
564const INDEX_FORMAT_UB: u32 = 4;
567pub(crate) const INDEX_EXT_CACHE_TREE: u32 = 0x5452_4545;
569
570pub(crate) fn parse_cache_tree(data: &[u8], hash_len: usize) -> Option<CacheTreeNode> {
572 let (node, pos) = parse_cache_tree_node(data, 0, hash_len)?;
573 if pos == data.len() {
574 Some(node)
575 } else {
576 None
577 }
578}
579
580fn parse_cache_tree_node(
581 data: &[u8],
582 mut pos: usize,
583 hash_len: usize,
584) -> Option<(CacheTreeNode, usize)> {
585 let name_end = data.get(pos..)?.iter().position(|&b| b == 0)? + pos;
586 let name = data[pos..name_end].to_vec();
587 pos = name_end + 1;
588
589 let (entry_count, consumed) = parse_signed_int_prefix(&data[pos..])?;
590 pos += consumed;
591 if data.get(pos) != Some(&b' ') {
592 return None;
593 }
594 pos += 1;
595 let (subtree_count, consumed) = parse_signed_int_prefix(&data[pos..])?;
596 if subtree_count < 0 {
597 return None;
598 }
599 pos += consumed;
600 if data.get(pos) != Some(&b'\n') {
601 return None;
602 }
603 pos += 1;
604
605 let oid = if entry_count >= 0 {
606 if data.len().saturating_sub(pos) < hash_len {
607 return None;
608 }
609 let oid = ObjectId::from_bytes(&data[pos..pos + hash_len]).ok()?;
610 pos += hash_len;
611 Some(oid)
612 } else {
613 None
614 };
615
616 let mut children = Vec::with_capacity(subtree_count as usize);
617 for _ in 0..subtree_count {
618 let (child, next) = parse_cache_tree_node(data, pos, hash_len)?;
619 children.push(child);
620 pos = next;
621 }
622
623 Some((
624 CacheTreeNode {
625 name,
626 entry_count,
627 oid,
628 children,
629 },
630 pos,
631 ))
632}
633
634fn parse_signed_int_prefix(data: &[u8]) -> Option<(i32, usize)> {
635 let mut j = 0usize;
636 while j < data.len() && data[j] == b' ' {
637 j += 1;
638 }
639 let start = j;
640 if j < data.len() && data[j] == b'-' {
641 j += 1;
642 }
643 let digit_start = j;
644 while j < data.len() && data[j].is_ascii_digit() {
645 j += 1;
646 }
647 if j == digit_start {
648 return None;
649 }
650 let s = std::str::from_utf8(&data[start..j]).ok()?;
651 let v: i32 = s.parse().ok()?;
652 Some((v, j))
653}
654
655fn serialize_cache_tree_node(node: &CacheTreeNode, out: &mut Vec<u8>) {
656 out.extend_from_slice(&node.name);
657 out.push(0);
658 out.extend_from_slice(node.entry_count.to_string().as_bytes());
659 out.push(b' ');
660 out.extend_from_slice(node.children.len().to_string().as_bytes());
661 out.push(b'\n');
662 if node.entry_count >= 0 {
663 if let Some(oid) = node.oid {
664 out.extend_from_slice(oid.as_bytes());
665 }
666 }
667 for child in &node.children {
668 serialize_cache_tree_node(child, out);
669 }
670}
671
672fn format_cache_tree_node(node: &CacheTreeNode, parent_path: &str, out: &mut String) {
673 let path = if node.name.is_empty() {
674 String::new()
675 } else {
676 let name = String::from_utf8_lossy(&node.name);
677 format!("{parent_path}{name}/")
678 };
679 if node.is_valid() {
680 if let Some(oid) = node.oid {
681 out.push_str(&format!(
682 "{} {} ({} entries, {} subtrees)\n",
683 oid,
684 path,
685 node.entry_count,
686 node.children.len()
687 ));
688 }
689 } else {
690 out.push_str(&format!(
691 "{:<40} {} ({} subtrees)\n",
692 "invalid",
693 path,
694 node.children.len()
695 ));
696 }
697 for child in &node.children {
698 format_cache_tree_node(child, &path, out);
699 }
700}
701
702fn dump_one_line(node: &CacheTreeNode, pfx: &str, out: &mut String) {
704 if node.entry_count < 0 {
705 out.push_str(&format!(
706 "{:<40} {} ({} subtrees)\n",
707 "invalid",
708 pfx,
709 node.children.len()
710 ));
711 } else {
712 let oid = node.oid.unwrap_or_else(ObjectId::zero);
713 out.push_str(&format!(
714 "{} {} ({} entries, {} subtrees)\n",
715 oid,
716 pfx,
717 node.entry_count,
718 node.children.len()
719 ));
720 }
721}
722
723fn dump_cache_tree_pair(
726 it: &CacheTreeNode,
727 reference: &CacheTreeNode,
728 pfx: &str,
729 out: &mut String,
730) {
731 dump_one_line(it, pfx, out);
732
733 for child in &it.children {
734 let Some(ref_child) = reference.children.iter().find(|c| c.name == child.name) else {
735 continue;
736 };
737 let name = String::from_utf8_lossy(&child.name);
738 let child_pfx = format!("{pfx}{name}/");
739 dump_cache_tree_pair(child, ref_child, &child_pfx, out);
740 }
741}
742
743pub fn get_index_format_from_env() -> Option<u32> {
749 let val = crate::environment::Environment::empty().var("GIT_INDEX_VERSION")?;
750 if val.is_empty() {
751 return None;
752 }
753 match val.parse::<u32>() {
754 Ok(v) if (INDEX_FORMAT_LB..=INDEX_FORMAT_UB).contains(&v) => Some(v),
755 _ => Some(INDEX_ENV_INVALID_FALLBACK),
756 }
757}
758
759pub(crate) fn parse_fsmonitor_extension(
761 ext_data: &[u8],
762) -> Result<(String, crate::ewah_bitmap::EwahBitmap)> {
763 if ext_data.len() < 4 + 1 + 4 {
764 return Err(Error::IndexFsmonitorExtensionTruncated);
765 }
766 let hdr_version = u32::from_be_bytes(
767 ext_data[0..4]
768 .try_into()
769 .map_err(|_| Error::IndexFsmonitorExtensionTruncated)?,
770 );
771 let mut pos = 4usize;
772 let token = match hdr_version {
773 FSMONITOR_EXT_VERSION1 => {
774 if ext_data.len() < pos + 8 + 4 {
775 return Err(Error::IndexFsmonitorExtensionTruncated);
776 }
777 let ts = u64::from_be_bytes(
778 ext_data[pos..pos + 8]
779 .try_into()
780 .map_err(|_| Error::IndexFsmonitorExtensionTruncated)?,
781 );
782 pos += 8;
783 ts.to_string()
784 }
785 FSMONITOR_EXT_VERSION2 => {
786 let nul = ext_data[pos..]
787 .iter()
788 .position(|&b| b == 0)
789 .ok_or(Error::IndexFsmonitorExtensionTruncated)?;
790 let token = String::from_utf8_lossy(&ext_data[pos..pos + nul]).into_owned();
791 pos += nul + 1;
792 token
793 }
794 v => return Err(Error::IndexFsmonitorExtensionBadVersion(v)),
795 };
796 if ext_data.len() < pos + 4 {
797 return Err(Error::IndexFsmonitorExtensionTruncated);
798 }
799 let ewah_size = u32::from_be_bytes(
800 ext_data[pos..pos + 4]
801 .try_into()
802 .map_err(|_| Error::IndexFsmonitorExtensionTruncated)?,
803 ) as usize;
804 pos += 4;
805 if ext_data.len() < pos + ewah_size {
806 return Err(Error::IndexFsmonitorExtensionTruncated);
807 }
808 let ewah_slice = &ext_data[pos..pos + ewah_size];
809 let (dirty, consumed) = crate::ewah_bitmap::EwahBitmap::deserialize_prefix(ewah_slice)
810 .ok_or(Error::IndexFsmonitorExtensionEwahInvalid)?;
811 if consumed != ewah_size {
812 return Err(Error::IndexFsmonitorExtensionEwahInvalid);
813 }
814 Ok((token, dirty))
815}
816
817pub(crate) fn apply_fsmonitor_dirty_to_entries(
819 entries: &mut [IndexEntry],
820 dirty: &crate::ewah_bitmap::EwahBitmap,
821) {
822 for entry in entries.iter_mut() {
823 entry.set_fsmonitor_valid(true);
824 }
825 dirty.each_set_bit(|pos| {
826 if let Some(entry) = entries.get_mut(pos) {
827 entry.set_fsmonitor_valid(false);
828 }
829 });
830}
831
832impl Index {
833 #[must_use]
837 pub fn new() -> Self {
838 let version = get_index_format_from_env().unwrap_or(2);
839 Self {
840 version,
841 entries: Vec::new(),
842 entries_sorted: true,
843 sparse_directories: false,
844 untracked_cache: None,
845 fsmonitor_last_update: None,
846 fsmonitor_dirty: None,
847 fsmonitor_raw_extension: None,
848 resolve_undo: None,
849 split_link: None,
850 cache_tree_root: None,
851 cache_tree: None,
852 hash_algo: HashAlgo::Sha1,
853 source_mtime: None,
854 source_checksum: None,
855 source_file_len: None,
856 defer_untracked_cache_invalidation: false,
857 skip_racy_smudge_on_write: false,
858 content_verified: HashMap::new(),
859 fsmonitor_refresh_ran: false,
860 suppress_pre_write_index_housekeeping: false,
861 entry_modes_repaired_on_load: false,
862 }
863 }
864
865 pub(crate) fn repair_noncanonical_entry_modes(&mut self) -> bool {
867 let mut repaired = false;
868 for entry in &mut self.entries {
869 let canon = canon_mode(entry.mode);
870 if entry.mode != canon {
871 entry.mode = canon;
872 repaired = true;
873 }
874 }
875 if repaired {
876 self.entry_modes_repaired_on_load = true;
877 self.cache_tree_root = None;
878 self.cache_tree = None;
879 }
880 repaired
881 }
882
883 #[must_use]
888 pub fn gitattributes_snapshot(&self) -> Self {
889 let mut snap = Self::new();
890 snap.version = self.version;
891 snap.hash_algo = self.hash_algo;
892 for entry in &self.entries {
893 if entry.stage() != 0 {
894 continue;
895 }
896 if gitattributes_index_path(&entry.path) {
897 snap.add_or_replace(entry.clone());
898 }
899 }
900 snap
901 }
902
903 #[must_use]
911 pub fn empty(version: u32) -> Self {
912 Self {
913 version,
914 entries: Vec::new(),
915 entries_sorted: true,
916 sparse_directories: false,
917 untracked_cache: None,
918 fsmonitor_last_update: None,
919 fsmonitor_dirty: None,
920 fsmonitor_raw_extension: None,
921 resolve_undo: None,
922 split_link: None,
923 cache_tree_root: None,
924 cache_tree: None,
925 hash_algo: HashAlgo::Sha1,
926 source_mtime: None,
927 source_checksum: None,
928 source_file_len: None,
929 defer_untracked_cache_invalidation: false,
930 skip_racy_smudge_on_write: false,
931 content_verified: HashMap::new(),
932 fsmonitor_refresh_ran: false,
933 suppress_pre_write_index_housekeeping: false,
934 entry_modes_repaired_on_load: false,
935 }
936 }
937
938 pub fn new_with_config(
943 config_index_version: Option<&str>,
944 config_many_files: Option<&str>,
945 ) -> Self {
946 if let Some(v) = get_index_format_from_env() {
947 return Self {
948 version: v,
949 entries: Vec::new(),
950 entries_sorted: true,
951 sparse_directories: false,
952 untracked_cache: None,
953 fsmonitor_last_update: None,
954 fsmonitor_dirty: None,
955 fsmonitor_raw_extension: None,
956 resolve_undo: None,
957 split_link: None,
958 cache_tree_root: None,
959 cache_tree: None,
960 hash_algo: HashAlgo::default(),
961 source_mtime: None,
962 source_checksum: None,
963 source_file_len: None,
964 defer_untracked_cache_invalidation: false,
965 skip_racy_smudge_on_write: false,
966 content_verified: HashMap::new(),
967 fsmonitor_refresh_ran: false,
968 suppress_pre_write_index_housekeeping: false,
969 entry_modes_repaired_on_load: false,
970 };
971 }
972
973 let many_files = config_truthy(config_many_files);
974 let mut version = if many_files { 4 } else { 2 };
975
976 if let Some(val) = config_index_version {
977 let trimmed = val.trim();
978 if !trimmed.is_empty() {
979 match trimmed.parse::<u32>() {
980 Ok(v) if (INDEX_FORMAT_LB..=INDEX_FORMAT_UB).contains(&v) => {
981 version = v;
982 }
983 _ => {
984 version = INDEX_CONFIG_INVALID_FALLBACK;
985 }
986 }
987 }
988 }
989
990 Self {
991 version,
992 entries: Vec::new(),
993 entries_sorted: true,
994 sparse_directories: false,
995 untracked_cache: None,
996 fsmonitor_last_update: None,
997 fsmonitor_dirty: None,
998 fsmonitor_raw_extension: None,
999 resolve_undo: None,
1000 split_link: None,
1001 cache_tree_root: None,
1002 cache_tree: None,
1003 hash_algo: HashAlgo::default(),
1004 source_mtime: None,
1005 source_checksum: None,
1006 source_file_len: None,
1007 defer_untracked_cache_invalidation: false,
1008 skip_racy_smudge_on_write: false,
1009 content_verified: HashMap::new(),
1010 fsmonitor_refresh_ran: false,
1011 suppress_pre_write_index_housekeeping: false,
1012 entry_modes_repaired_on_load: false,
1013 }
1014 }
1015
1016 #[must_use]
1021 pub fn new_from_config(config: &ConfigSet) -> Self {
1022 if let Some(v) = get_index_format_from_env() {
1023 return Self {
1024 version: v,
1025 entries: Vec::new(),
1026 entries_sorted: true,
1027 sparse_directories: false,
1028 untracked_cache: None,
1029 fsmonitor_last_update: None,
1030 fsmonitor_dirty: None,
1031 fsmonitor_raw_extension: None,
1032 resolve_undo: None,
1033 split_link: None,
1034 cache_tree_root: None,
1035 cache_tree: None,
1036 hash_algo: HashAlgo::default(),
1037 source_mtime: None,
1038 source_checksum: None,
1039 source_file_len: None,
1040 defer_untracked_cache_invalidation: false,
1041 skip_racy_smudge_on_write: false,
1042 content_verified: HashMap::new(),
1043 fsmonitor_refresh_ran: false,
1044 suppress_pre_write_index_housekeeping: false,
1045 entry_modes_repaired_on_load: false,
1046 };
1047 }
1048
1049 let many_files = config
1050 .get_bool("feature.manyFiles")
1051 .and_then(|r| r.ok())
1052 .unwrap_or(false);
1053 let mut version = if many_files { 4 } else { 2 };
1054
1055 if let Some(val) = config.get("index.version") {
1056 let trimmed = val.trim();
1057 if !trimmed.is_empty() {
1058 match trimmed.parse::<u32>() {
1059 Ok(v) if (INDEX_FORMAT_LB..=INDEX_FORMAT_UB).contains(&v) => {
1060 version = v;
1061 }
1062 _ => {
1063 version = INDEX_CONFIG_INVALID_FALLBACK;
1064 }
1065 }
1066 }
1067 }
1068
1069 Self {
1070 version,
1071 entries: Vec::new(),
1072 entries_sorted: true,
1073 sparse_directories: false,
1074 untracked_cache: None,
1075 fsmonitor_last_update: None,
1076 fsmonitor_dirty: None,
1077 fsmonitor_raw_extension: None,
1078 resolve_undo: None,
1079 split_link: None,
1080 cache_tree_root: None,
1081 cache_tree: None,
1082 hash_algo: HashAlgo::default(),
1083 source_mtime: None,
1084 source_checksum: None,
1085 source_file_len: None,
1086 defer_untracked_cache_invalidation: false,
1087 skip_racy_smudge_on_write: false,
1088 content_verified: HashMap::new(),
1089 fsmonitor_refresh_ran: false,
1090 suppress_pre_write_index_housekeeping: false,
1091 entry_modes_repaired_on_load: false,
1092 }
1093 }
1094
1095 pub fn load(path: &Path) -> Result<Self> {
1103 match read_index_file(path) {
1104 Ok((data, mtime)) => {
1105 let mut idx = Self::parse(&data)?;
1106 record_loaded_index_identity(&mut idx, &data, mtime);
1107 Ok(idx)
1108 }
1109 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Self {
1110 sparse_directories: false,
1111 ..Self::new()
1112 }),
1113 Err(e) => Err(Error::Io(e)),
1114 }
1115 }
1116
1117 pub fn load_expand_sparse(path: &Path, odb: &Odb) -> Result<Self> {
1122 let mut idx = match read_index_file(path) {
1123 Ok((data, mtime)) => {
1124 let mut idx = Self::parse_with_algo(&data, odb.hash_algo())?;
1125 record_loaded_index_identity(&mut idx, &data, mtime);
1126 idx
1127 }
1128 Err(e) if e.kind() == io::ErrorKind::NotFound => Self {
1129 sparse_directories: false,
1130 hash_algo: odb.hash_algo(),
1131 ..Self::new()
1132 },
1133 Err(e) => return Err(Error::Io(e)),
1134 };
1135 idx.expand_sparse_directory_placeholders(
1136 odb,
1137 crate::path_validate::ProtectOptions::default(),
1138 )?;
1139 Ok(idx)
1140 }
1141
1142 pub fn load_expand_sparse_with_config(
1144 path: &Path,
1145 odb: &Odb,
1146 config: &ConfigSet,
1147 ) -> Result<Self> {
1148 Self::load_expand_sparse_optional_with_context(
1149 path,
1150 odb,
1151 Some(config),
1152 IndexParseContext::default(),
1153 )
1154 }
1155
1156 pub fn load_expand_sparse_optional(path: &Path, odb: &Odb) -> Result<Self> {
1160 Self::load_expand_sparse_optional_with_context(
1161 path,
1162 odb,
1163 None,
1164 IndexParseContext::default(),
1165 )
1166 }
1167
1168 pub fn load_expand_sparse_optional_with_context(
1170 path: &Path,
1171 odb: &Odb,
1172 config: Option<&ConfigSet>,
1173 parse_context: IndexParseContext,
1174 ) -> Result<Self> {
1175 let mut idx =
1176 Self::load_preserving_sparse_optional_with_context(path, odb, config, parse_context)?;
1177 let protect = config
1178 .map(crate::path_validate::ProtectOptions::from_config)
1179 .unwrap_or_default();
1180 idx.expand_sparse_directory_placeholders(odb, protect)?;
1181 Ok(idx)
1182 }
1183
1184 pub(crate) fn load_preserving_sparse_optional_with_context(
1187 path: &Path,
1188 odb: &Odb,
1189 config: Option<&ConfigSet>,
1190 parse_context: IndexParseContext,
1191 ) -> Result<Self> {
1192 let algo = odb.hash_algo();
1193 let idx = match read_index_file(path) {
1194 Ok((data, mtime)) => {
1195 let mut idx = Self::parse_with_algo_and_config(&data, algo, config, parse_context)?;
1196 record_loaded_index_identity(&mut idx, &data, mtime);
1197 idx
1198 }
1199 Err(e) if e.kind() == io::ErrorKind::NotFound => Self {
1200 hash_algo: algo,
1201 ..Self::new()
1202 },
1203 Err(e) => return Err(Error::Io(e)),
1204 };
1205 Ok(idx)
1206 }
1207
1208 #[must_use]
1210 pub fn has_sparse_directory_placeholders(&self) -> bool {
1211 self.entries
1212 .iter()
1213 .any(IndexEntry::is_sparse_directory_placeholder)
1214 }
1215
1216 pub fn expand_sparse_directory_placeholders(
1221 &mut self,
1222 odb: &Odb,
1223 protect: crate::path_validate::ProtectOptions,
1224 ) -> Result<()> {
1225 self.expand_sparse_directories_matching(odb, protect, |_| true)
1226 }
1227
1228 pub(crate) fn expand_sparse_directories_matching(
1230 &mut self,
1231 odb: &Odb,
1232 protect: crate::path_validate::ProtectOptions,
1233 mut expand: impl FnMut(&IndexEntry) -> bool,
1234 ) -> Result<()> {
1235 if !self.has_sparse_directory_placeholders() {
1236 return Ok(());
1237 }
1238 let mut out: Vec<IndexEntry> = Vec::with_capacity(self.entries.len());
1239 for entry in self.entries.drain(..) {
1240 if entry.is_sparse_directory_placeholder() && expand(&entry) {
1241 let prefix = trim_trailing_slash_bytes(&entry.path);
1242 let blobs = flatten_tree_blobs(odb, &entry.oid, prefix, protect)?;
1243 #[cfg(test)]
1244 odb.hot_path_metrics()
1245 .record_sparse_expanded_rows(blobs.len());
1246 out.extend(blobs);
1247 } else {
1248 out.push(entry);
1249 }
1250 }
1251 self.entries = out;
1252 self.sparse_directories = self.has_sparse_directory_placeholders();
1253 self.sort();
1254 Ok(())
1255 }
1256
1257 pub fn try_collapse_sparse_directories(
1263 &mut self,
1264 odb: &Odb,
1265 head_tree: &ObjectId,
1266 patterns: &[String],
1267 cone_mode: bool,
1268 enable_sparse_index: bool,
1269 protect: crate::path_validate::ProtectOptions,
1270 ) -> Result<()> {
1271 if !enable_sparse_index || !cone_mode {
1272 self.sparse_directories = false;
1273 return Ok(());
1274 }
1275
1276 self.ensure_canonical_order();
1277 let mut prefixes = BTreeSet::<Vec<u8>>::new();
1278 let mut last_dir: &[u8] = &[];
1279 for e in &self.entries {
1280 if e.stage() != 0 || e.mode == MODE_TREE || !e.skip_worktree() {
1281 continue;
1282 }
1283 let Some(slash) = e.path.iter().rposition(|&b| b == b'/') else {
1286 continue;
1287 };
1288 let dir = &e.path[..slash];
1289 if dir == last_dir {
1290 continue;
1291 }
1292 last_dir = dir;
1293 collect_directory_prefixes(&e.path, &mut prefixes);
1294 }
1295
1296 let mut ordered: Vec<Vec<u8>> = prefixes.into_iter().collect();
1298 ordered.sort_by_key(|p| std::cmp::Reverse(p.len()));
1299
1300 let mut collapsed = CollapsedDirectories::default();
1304 let mut head_trees = PrefixTreeResolver::new(*head_tree);
1305 for pref in ordered {
1306 let pref_str = String::from_utf8_lossy(&pref);
1307 if directory_in_cone(&pref_str, patterns, cone_mode) {
1308 continue;
1309 }
1310 let Some(subtree_oid) = head_trees.resolve(odb, &pref)? else {
1311 continue;
1312 };
1313 let view = SparseCollapseView {
1314 entries: &self.entries,
1315 collapsed: &collapsed,
1316 };
1317 let mut expected = collect_sparse_aware_expected_blobs(
1318 odb,
1319 &subtree_oid,
1320 &pref,
1321 patterns,
1322 cone_mode,
1323 &view,
1324 protect,
1325 )?;
1326 let mut directory = pref.clone();
1327 directory.push(b'/');
1328 let start = self.entries.partition_point(|e| e.path < directory);
1329 let end =
1330 start + self.entries[start..].partition_point(|e| e.path.starts_with(&directory));
1331 let rows = &self.entries[start..end];
1332 if expected.is_empty() {
1333 if try_collapse_directory_of_sparse_placeholders(
1334 odb,
1335 &directory,
1336 rows,
1337 &subtree_oid,
1338 )? {
1339 collapsed.insert(directory.clone(), subtree_oid);
1340 if let Ok(p) = std::str::from_utf8(&directory) {
1341 self.invalidate_untracked_cache_for_path(p);
1342 }
1343 self.invalidate_cache_tree_for_path(&directory);
1344 self.touch_fsmonitor_after_entry_change();
1345 }
1346 continue;
1347 }
1348 if rows.iter().any(|e| e.stage() != 0) {
1349 continue;
1350 }
1351 let nested = collapsed.has_descendants(&directory);
1353 let mut scratch = Vec::new();
1354 let matched: Vec<&IndexEntry> = rows
1355 .iter()
1356 .filter(|e| {
1357 e.mode != MODE_TREE && !(nested && collapsed.covers(&e.path, &mut scratch))
1358 })
1359 .collect();
1360 if matched.len() != expected.len() {
1361 continue;
1362 }
1363 expected.sort_by(|a, b| a.path.cmp(&b.path));
1364 if !matched
1365 .iter()
1366 .zip(expected.iter())
1367 .all(|(a, b)| a.path == b.path && a.oid == b.oid && a.mode == b.mode)
1368 {
1369 continue;
1370 }
1371 if !matched.iter().all(|e| e.skip_worktree()) {
1372 continue;
1373 }
1374 if matched.iter().any(|e| e.mode == MODE_GITLINK)
1378 || expected.iter().any(|e| e.mode == MODE_GITLINK)
1379 {
1380 continue;
1381 }
1382
1383 collapsed.insert(directory.clone(), subtree_oid);
1384 if let Ok(p) = std::str::from_utf8(&directory) {
1385 self.invalidate_untracked_cache_for_path(p);
1386 }
1387 self.invalidate_cache_tree_for_path(&directory);
1388 self.touch_fsmonitor_after_entry_change();
1389 }
1390
1391 if !collapsed.is_empty() {
1392 let covered = collapsed.covered_rows(&self.entries);
1393 let mut entries: Vec<IndexEntry> = std::mem::take(&mut self.entries)
1394 .into_iter()
1395 .zip(covered)
1396 .filter(|(e, covered)| e.stage() != 0 || !covered)
1397 .map(|(e, _)| e)
1398 .collect();
1399 entries.extend(collapsed.into_placeholders());
1400 self.entries = entries;
1401 self.entries_sorted = false;
1402 self.sort();
1403 }
1404 self.sparse_directories = self.has_sparse_directory_placeholders();
1405 Ok(())
1406 }
1407
1408 pub fn parse(data: &[u8]) -> Result<Self> {
1414 Self::parse_with_algo(data, detect_index_hash_algo(data))
1418 }
1419
1420 pub fn parse_with_algo(data: &[u8], hash_algo: HashAlgo) -> Result<Self> {
1424 Self::parse_with_algo_and_config(data, hash_algo, None, IndexParseContext::default())
1425 }
1426
1427 pub fn parse_with_algo_and_config(
1429 data: &[u8],
1430 hash_algo: HashAlgo,
1431 config: Option<&ConfigSet>,
1432 parse_context: IndexParseContext,
1433 ) -> Result<Self> {
1434 const INDEX_HEADER_LEN: usize = 12;
1435
1436 let hash_len = hash_algo.len();
1437 if data.len() < INDEX_HEADER_LEN + hash_len {
1438 return Err(Error::IndexError(
1439 "file too short for index header and checksum".to_owned(),
1440 ));
1441 }
1442 let (body, checksum) = data.split_at(data.len() - hash_len);
1443 if body.len() < INDEX_HEADER_LEN {
1444 return Err(Error::IndexError(
1445 "file too short for index header and checksum".to_owned(),
1446 ));
1447 }
1448 let trailer_error = |e: crate::hash::TrailerMismatch| {
1449 Error::IndexError(format!(
1450 "{} checksum mismatch: {e}",
1451 hash_algo.name().to_uppercase()
1452 ))
1453 };
1454 if checksum.iter().all(|&b| b == 0) {
1455 return Self::parse_body(body, hash_algo, config, parse_context);
1456 }
1457 let verify_in_parallel = data.len() >= PARALLEL_TRAILER_VERIFY_MIN_BYTES
1458 && parse_context.thread_policy.threads_override != Some(1);
1459 if !verify_in_parallel {
1460 verify_trailer(hash_algo, data).map_err(trailer_error)?;
1461 return Self::parse_body(body, hash_algo, config, parse_context);
1462 }
1463 std::thread::scope(|scope| {
1466 let verifier = scope.spawn(|| verify_trailer(hash_algo, data));
1467 let parsed = Self::parse_body(body, hash_algo, config, parse_context);
1468 verifier
1469 .join()
1470 .map_err(|_| Error::Message("index checksum worker thread panicked".to_owned()))?
1471 .map_err(trailer_error)?;
1472 parsed
1473 })
1474 }
1475
1476 fn parse_body(
1478 body: &[u8],
1479 hash_algo: HashAlgo,
1480 config: Option<&ConfigSet>,
1481 parse_context: IndexParseContext,
1482 ) -> Result<Self> {
1483 use crate::index_eoie_ieot::{
1484 index_load_worker_count, parallel_index_load_allowed, parse_entries_parallel,
1485 parse_entries_serial, parse_index_extensions, read_eoie_extension, read_ieot_extension,
1486 ParsedIndexExtensions,
1487 };
1488
1489 let hash_len = hash_algo.len();
1490 let magic = &body[..4];
1491 if magic != b"DIRC" {
1492 return Err(Error::IndexError("bad magic: expected DIRC".to_owned()));
1493 }
1494 let version = u32::from_be_bytes(
1495 body[4..8]
1496 .try_into()
1497 .map_err(|_| Error::IndexError("cannot read version".to_owned()))?,
1498 );
1499 if version != 2 && version != 3 && version != 4 {
1500 return Err(Error::IndexError(format!(
1501 "unsupported index version {version}"
1502 )));
1503 }
1504 let count = u32::from_be_bytes(
1505 body[8..12]
1506 .try_into()
1507 .map_err(|_| Error::IndexError("cannot read entry count".to_owned()))?,
1508 );
1509
1510 let workers = index_load_worker_count(parse_context.thread_policy, config, count as usize);
1511 let extension_offset = if workers > 1 {
1512 read_eoie_extension(body, hash_algo)
1513 } else {
1514 None
1515 };
1516
1517 let eoie_payload = 4 + hash_len;
1518 let eoie_with_header = 8 + eoie_payload;
1519 let extensions_end = body
1520 .len()
1521 .checked_sub(eoie_with_header)
1522 .filter(|_| extension_offset.is_some())
1523 .unwrap_or(body.len());
1524
1525 let try_parallel = extension_offset.is_some()
1526 && workers > 1
1527 && parallel_index_load_allowed(parse_context.thread_policy, config, count as usize);
1528
1529 let (entries, ext_fields): (Vec<IndexEntry>, ParsedIndexExtensions) = if try_parallel {
1530 let Some(ext_off) = extension_offset else {
1531 return Err(Error::IndexError(
1532 "parallel index load missing EOIE offset".to_owned(),
1533 ));
1534 };
1535 let ieot = read_ieot_extension(body, ext_off, extensions_end);
1536 let mut loaded_entries = if let Some(ref ieot) = ieot {
1537 parse_entries_parallel(body, version, count, hash_len, ieot, workers)?
1538 } else {
1539 parse_entries_serial(body, version, count, hash_len, 12)?.0
1540 };
1541 let ext_fields =
1542 parse_index_extensions(body, ext_off, extensions_end, hash_algo, None)?;
1543 if let Some(dirty) = ext_fields.fsmonitor_dirty.as_ref() {
1544 apply_fsmonitor_dirty_to_entries(&mut loaded_entries, dirty);
1545 }
1546 (loaded_entries, ext_fields)
1547 } else {
1548 let (mut entries, pos) = parse_entries_serial(body, version, count, hash_len, 12)?;
1549 let ext_fields =
1550 parse_index_extensions(body, pos, body.len(), hash_algo, Some(&mut entries))?;
1551 (entries, ext_fields)
1552 };
1553
1554 let mut index = Self {
1555 version,
1556 entries,
1557 entries_sorted: true,
1558 sparse_directories: ext_fields.sparse_directories,
1559 untracked_cache: ext_fields.untracked_cache,
1560 fsmonitor_last_update: ext_fields.fsmonitor_last_update,
1561 fsmonitor_dirty: ext_fields.fsmonitor_dirty,
1562 fsmonitor_raw_extension: ext_fields.fsmonitor_raw_extension,
1563 resolve_undo: ext_fields.resolve_undo,
1564 split_link: ext_fields.split_link,
1565 cache_tree_root: ext_fields.cache_tree_root,
1566 cache_tree: ext_fields.cache_tree,
1567 hash_algo,
1568 source_mtime: None,
1569 source_checksum: None,
1570 source_file_len: None,
1571 defer_untracked_cache_invalidation: false,
1572 skip_racy_smudge_on_write: false,
1573 content_verified: HashMap::new(),
1574 fsmonitor_refresh_ran: false,
1575 suppress_pre_write_index_housekeeping: false,
1576 entry_modes_repaired_on_load: false,
1577 };
1578 index.repair_noncanonical_entry_modes();
1579 Ok(index)
1580 }
1581
1582 pub fn write(&mut self, path: &Path) -> Result<()> {
1588 let git_dir = path.parent();
1589 let config = git_dir.and_then(|d| {
1590 ConfigSet::load(&crate::environment::Environment::empty(), Some(d), true).ok()
1591 });
1592 let skip_hash = index_skip_hash_for_write(config.as_ref());
1593 self.write_to_path(path, skip_hash)
1594 }
1595
1596 pub fn write_with_config(&mut self, path: &Path, config: &ConfigSet) -> Result<()> {
1602 self.write_with_config_and_policy(path, config, crate::parallel::ThreadPolicy::default())
1603 }
1604
1605 pub fn write_with_config_and_policy(
1607 &mut self,
1608 path: &Path,
1609 config: &ConfigSet,
1610 policy: crate::parallel::ThreadPolicy,
1611 ) -> Result<()> {
1612 let skip_hash = index_skip_hash_for_write(Some(config));
1613 self.write_to_path_with_config_and_policy(path, skip_hash, Some(config), policy)
1614 }
1615
1616 pub fn write_to_path(&mut self, path: &Path, skip_hash: bool) -> Result<()> {
1620 self.write_to_path_with_config(path, skip_hash, None)
1621 }
1622
1623 pub(crate) fn write_to_path_with_config(
1625 &mut self,
1626 path: &Path,
1627 skip_hash: bool,
1628 config: Option<&ConfigSet>,
1629 ) -> Result<()> {
1630 self.write_to_path_with_config_and_policy(
1631 path,
1632 skip_hash,
1633 config,
1634 crate::parallel::ThreadPolicy::default(),
1635 )
1636 }
1637
1638 pub(crate) fn write_to_path_with_config_and_policy(
1639 &mut self,
1640 path: &Path,
1641 skip_hash: bool,
1642 config: Option<&ConfigSet>,
1643 policy: crate::parallel::ThreadPolicy,
1644 ) -> Result<()> {
1645 self.write_to_path_with_config_and_lock_policy(path, skip_hash, config, None, policy)
1646 }
1647
1648 pub(crate) fn write_to_path_with_config_and_lock_policy(
1649 &mut self,
1650 path: &Path,
1651 skip_hash: bool,
1652 config: Option<&ConfigSet>,
1653 existing_lock: Option<&mut IndexLock>,
1654 policy: crate::parallel::ThreadPolicy,
1655 ) -> Result<()> {
1656 self.write_to_path_with_config_and_lock_mode_policy(
1657 path,
1658 skip_hash,
1659 config,
1660 existing_lock,
1661 true,
1662 policy,
1663 )
1664 }
1665
1666 pub(crate) fn write_to_path_with_config_and_lock_mode_policy(
1667 &mut self,
1668 path: &Path,
1669 skip_hash: bool,
1670 config: Option<&ConfigSet>,
1671 existing_lock: Option<&mut IndexLock>,
1672 commit: bool,
1673 policy: crate::parallel::ThreadPolicy,
1674 ) -> Result<()> {
1675 let body = self.serialize_index_body(config, policy)?;
1676
1677 if let Some(lock) = existing_lock {
1678 if lock.index_path != path {
1679 return Err(Error::Message(
1680 "index lock path does not match write target".into(),
1681 ));
1682 }
1683 let checksum: Vec<u8> = if skip_hash {
1684 vec![0u8; self.hash_algo.len()]
1685 } else {
1686 hash_index_body(self.hash_algo, &body)
1687 };
1688 lock.write_index_contents(&body, &checksum)?;
1689 if commit {
1690 lock.commit_replace_index()?;
1691 }
1692 return Ok(());
1693 }
1694
1695 let tmp_path = path.with_extension("lock");
1696 let pid_path = pid_path_for_lock(&tmp_path);
1697 let lockfile_pid_enabled = grit_lockfile_pid_enabled(config, path.parent());
1698
1699 use crate::lockfile::{LockFile, LockWait};
1700 let wait = LockWait::files_ref_lock(config);
1701 let mut lock_file = LockFile::acquire(&tmp_path, wait).map_err(Error::from)?;
1702 if lockfile_pid_enabled {
1703 if let Err(e) = lock_file
1704 .write_holder_pid()
1705 .map_err(crate::error::Error::from)
1706 {
1707 lock_file.rollback();
1708 return Err(e);
1709 }
1710 }
1711 let wrote_pid_file = lockfile_pid_enabled;
1712 let (tmp_path, mut lock_file) = lock_file.into_open_file()?;
1713
1714 if let Err(e) =
1715 write_index_body_and_trailer(&mut lock_file, self.hash_algo, skip_hash, &body)
1716 {
1717 let _ = fs::remove_file(&tmp_path);
1718 if wrote_pid_file {
1719 let _ = fs::remove_file(&pid_path);
1720 }
1721 return Err(Error::Io(e));
1722 }
1723 drop(lock_file);
1724
1725 if let Err(e) = crate::atomic_replace::replace_file(&tmp_path, path) {
1726 let _ = fs::remove_file(&tmp_path);
1727 if wrote_pid_file {
1728 let _ = fs::remove_file(&pid_path);
1729 }
1730 return Err(Error::Io(e));
1731 }
1732 if wrote_pid_file {
1733 let _ = fs::remove_file(&pid_path);
1734 }
1735 Ok(())
1736 }
1737
1738 fn serialize_index_body(
1739 &mut self,
1740 config: Option<&ConfigSet>,
1741 policy: crate::parallel::ThreadPolicy,
1742 ) -> Result<Vec<u8>> {
1743 self.ensure_canonical_order();
1744 let mut body = Vec::new();
1745 self.serialize_into_with_options(&mut body, config, policy)?;
1746 Ok(body)
1747 }
1748
1749 #[cfg(test)]
1750 pub(crate) fn serialize_into(&self, out: &mut Vec<u8>) -> Result<()> {
1754 self.serialize_into_with_options(out, None, crate::parallel::ThreadPolicy::default())
1755 }
1756
1757 pub(crate) fn serialize_into_with_options(
1758 &self,
1759 out: &mut Vec<u8>,
1760 config: Option<&ConfigSet>,
1761 policy: crate::parallel::ThreadPolicy,
1762 ) -> Result<()> {
1763 use crate::index_eoie_ieot::{
1764 entries_per_ieot_block, ieot_block_count_for_write, record_end_of_index_entries,
1765 write_ieot_payload, IeotEntry, IeotTable, INDEX_EXT_EOIE, INDEX_EXT_IEOT,
1766 };
1767 let has_extended_flags = self.entries.iter().any(|e| {
1768 e.flags_extended
1769 .is_some_and(|fe| IndexEntry::disk_flags_extended(fe) != 0)
1770 });
1771 let write_version = if self.version >= 4 {
1772 4
1773 } else if has_extended_flags {
1774 3
1775 } else if self.version >= 3 {
1776 2
1777 } else {
1778 self.version
1779 };
1780 out.reserve(self.estimate_body_capacity(write_version, config));
1781 out.extend_from_slice(b"DIRC");
1783 out.extend_from_slice(&write_version.to_be_bytes());
1784 out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1785
1786 let ieot_blocks = ieot_block_count_for_write(policy, config, self.entries.len());
1787 let ieot_stride = ieot_blocks.map(|b| entries_per_ieot_block(self.entries.len(), b));
1788 let mut ieot_table = IeotTable {
1789 entries: Vec::new(),
1790 };
1791 let mut nr_in_block = 0u32;
1792 let mut block_start_offset = out.len();
1793
1794 if write_version == 4 {
1795 let mut previous_path: Vec<u8> = Vec::new();
1796 for (i, entry) in self.entries.iter().enumerate() {
1797 if let Some(stride) = ieot_stride {
1798 if i > 0 && i % stride == 0 {
1799 ieot_table.entries.push(IeotEntry {
1800 offset: block_start_offset as u32,
1801 nr: nr_in_block,
1802 });
1803 nr_in_block = 0;
1804 if !previous_path.is_empty() {
1807 previous_path[0] = 0;
1808 }
1809 block_start_offset = out.len();
1810 }
1811 }
1812 serialize_entry_v4(entry, &mut previous_path, out);
1813 nr_in_block += 1;
1814 }
1815 } else {
1816 for (i, entry) in self.entries.iter().enumerate() {
1817 if let Some(stride) = ieot_stride {
1818 if i > 0 && i % stride == 0 {
1819 ieot_table.entries.push(IeotEntry {
1820 offset: block_start_offset as u32,
1821 nr: nr_in_block,
1822 });
1823 nr_in_block = 0;
1824 block_start_offset = out.len();
1825 }
1826 }
1827 serialize_entry(entry, write_version, out);
1828 nr_in_block += 1;
1829 }
1830 }
1831 if ieot_blocks.is_some() && nr_in_block > 0 {
1832 ieot_table.entries.push(IeotEntry {
1833 offset: block_start_offset as u32,
1834 nr: nr_in_block,
1835 });
1836 }
1837
1838 let extension_offset = out.len();
1839 let write_eoie = record_end_of_index_entries(config);
1840 let mut eoie_header_bytes: Option<Vec<u8>> = write_eoie.then(Vec::new);
1841
1842 if !ieot_table.entries.is_empty() {
1843 let mut payload = Vec::new();
1844 write_ieot_payload(&ieot_table, &mut payload);
1845 append_index_extension(out, &mut eoie_header_bytes, INDEX_EXT_IEOT, &payload);
1846 }
1847 if self.sparse_directories {
1848 append_index_extension(
1849 out,
1850 &mut eoie_header_bytes,
1851 INDEX_EXT_SPARSE_DIRECTORIES,
1852 &[],
1853 );
1854 }
1855 if let Some(uc) = &self.untracked_cache {
1856 let payload = untracked_cache::write_untracked_extension(uc);
1857 append_index_extension(out, &mut eoie_header_bytes, INDEX_EXT_UNTRACKED, &payload);
1858 }
1859 self.append_fsmonitor_extension(out, &mut eoie_header_bytes)?;
1860 if let Some(ru) = &self.resolve_undo {
1861 let payload = write_resolve_undo_payload(ru);
1862 if !payload.is_empty() {
1863 append_index_extension(
1864 out,
1865 &mut eoie_header_bytes,
1866 INDEX_EXT_RESOLVE_UNDO,
1867 &payload,
1868 );
1869 }
1870 }
1871 if let Some(sl) = &self.split_link {
1872 use crate::ewah_bitmap::EwahBitmap;
1873 let del = sl
1874 .delete_bitmap
1875 .as_ref()
1876 .cloned()
1877 .unwrap_or_else(EwahBitmap::new);
1878 let rep = sl
1879 .replace_bitmap
1880 .as_ref()
1881 .cloned()
1882 .unwrap_or_else(EwahBitmap::new);
1883 let payload =
1884 crate::split_index::serialize_link_extension_payload(&sl.base_oid, &del, &rep);
1885 append_index_extension(out, &mut eoie_header_bytes, INDEX_EXT_LINK, &payload);
1886 }
1887 if let Some(cache_tree) = &self.cache_tree {
1888 let mut payload = Vec::new();
1889 serialize_cache_tree_node(cache_tree, &mut payload);
1890 append_index_extension(out, &mut eoie_header_bytes, INDEX_EXT_CACHE_TREE, &payload);
1891 }
1892 if write_eoie {
1893 let mut eoie_payload = Vec::new();
1894 eoie_payload.extend_from_slice(&(extension_offset as u32).to_be_bytes());
1895 let headers = eoie_header_bytes.as_deref().unwrap_or(&[]);
1896 eoie_payload.extend_from_slice(&self.hash_algo.checksum_digest(headers));
1897 append_index_extension(out, &mut None, INDEX_EXT_EOIE, &eoie_payload);
1898 }
1899 Ok(())
1900 }
1901
1902 fn append_fsmonitor_extension(
1903 &self,
1904 out: &mut Vec<u8>,
1905 eoie_headers: &mut Option<Vec<u8>>,
1906 ) -> Result<()> {
1907 if let Some(raw) = &self.fsmonitor_raw_extension {
1908 if self.fsmonitor_raw_still_valid() {
1909 append_index_extension(out, eoie_headers, INDEX_EXT_FSMONITOR, raw);
1910 return Ok(());
1911 }
1912 }
1913 let Some(token) = &self.fsmonitor_last_update else {
1914 return Ok(());
1915 };
1916 let dirty = self
1917 .fsmonitor_dirty
1918 .as_ref()
1919 .cloned()
1920 .unwrap_or_else(|| self.build_fsmonitor_dirty_from_entries());
1921 if dirty.bit_size > self.fsmonitor_active_entry_count() {
1922 return Ok(());
1923 }
1924 let mut payload = Vec::new();
1925 payload.extend_from_slice(&FSMONITOR_EXT_VERSION2.to_be_bytes());
1926 payload.extend_from_slice(token.as_bytes());
1927 payload.push(0);
1928 let ewah_size_fixup = payload.len();
1929 payload.extend_from_slice(&0u32.to_be_bytes());
1930 let ewah_start = payload.len();
1931 dirty.serialize(&mut payload);
1932 let ewah_size = (payload.len() - ewah_start) as u32;
1933 payload[ewah_size_fixup..ewah_size_fixup + 4].copy_from_slice(&ewah_size.to_be_bytes());
1934 append_index_extension(out, eoie_headers, INDEX_EXT_FSMONITOR, &payload);
1935 Ok(())
1936 }
1937
1938 fn fsmonitor_active_entry_count(&self) -> usize {
1940 self.entries.len()
1941 }
1942
1943 fn build_fsmonitor_dirty_from_entries(&self) -> crate::ewah_bitmap::EwahBitmap {
1945 use crate::ewah_bitmap::EwahBitmap;
1946 let mut dirty = EwahBitmap::new();
1947 if self.fsmonitor_last_update.is_none() {
1948 return dirty;
1949 }
1950 #[cfg(test)]
1951 tests::fsmonitor_rebuild_counter::note_build();
1952 for (i, entry) in self.entries.iter().enumerate() {
1953 if !entry.fsmonitor_valid() {
1954 dirty.set_bit_extend(i);
1955 }
1956 }
1957 dirty
1958 }
1959
1960 fn fsmonitor_raw_still_valid(&self) -> bool {
1962 use crate::ewah_bitmap::ewah_bitmaps_equal;
1963 let Some(raw) = self.fsmonitor_raw_extension.as_ref() else {
1964 return false;
1965 };
1966 let Some(token) = self.fsmonitor_last_update.as_deref() else {
1967 return false;
1968 };
1969 let Ok((raw_token, raw_dirty)) = parse_fsmonitor_extension(raw) else {
1970 return false;
1971 };
1972 if raw_token != token {
1973 return false;
1974 }
1975 let current = self.build_fsmonitor_dirty_from_entries();
1976 if !ewah_bitmaps_equal(&raw_dirty, ¤t) {
1977 return false;
1978 }
1979 current.bit_size <= self.fsmonitor_active_entry_count()
1980 }
1981
1982 fn rebuild_fsmonitor_dirty_from_entries(&mut self) {
1984 if self.fsmonitor_last_update.is_none() {
1985 self.fsmonitor_dirty = None;
1986 return;
1987 }
1988 self.fsmonitor_dirty = Some(self.build_fsmonitor_dirty_from_entries());
1989 }
1990
1991 fn invalidate_fsmonitor_bitmap(&mut self) {
1993 if self.fsmonitor_last_update.is_none() {
1994 return;
1995 }
1996 self.fsmonitor_raw_extension = None;
1997 self.fsmonitor_dirty = None;
1998 }
1999
2000 pub fn set_fsmonitor_last_update(&mut self, token: Option<String>) {
2002 if self.fsmonitor_last_update.as_deref() != token.as_deref() {
2003 self.fsmonitor_raw_extension = None;
2004 }
2005 self.fsmonitor_last_update = token;
2006 self.rebuild_fsmonitor_dirty_from_entries();
2007 }
2008
2009 pub fn clear_fsmonitor_extension(&mut self) {
2011 self.fsmonitor_last_update = None;
2012 self.fsmonitor_dirty = None;
2013 self.fsmonitor_raw_extension = None;
2014 }
2015
2016 pub fn sync_fsmonitor_dirty_bitmap(&mut self) {
2018 self.rebuild_fsmonitor_dirty_from_entries();
2019 }
2020
2021 pub fn fsmonitor_validity_changed(&mut self) {
2023 self.invalidate_fsmonitor_bitmap();
2024 }
2025
2026 pub(crate) fn invalidate_all_fsmonitor_valid(&mut self) {
2028 for entry in &mut self.entries {
2029 if entry.stage() == 0 {
2030 entry.set_fsmonitor_valid(false);
2031 }
2032 }
2033 self.fsmonitor_validity_changed();
2034 }
2035
2036 pub(crate) fn invalidate_fsmonitor_for_path(&mut self, path: &str, ignorecase: bool) {
2038 if let Some(uc) = self.untracked_cache.as_mut() {
2039 crate::untracked_cache::invalidate_path(uc, path);
2040 }
2041 let mut invalidated = if path.ends_with('/') {
2042 self.invalidate_fsmonitor_directory_cone(path.trim_end_matches('/'), ignorecase)
2043 } else {
2044 self.invalidate_fsmonitor_unqualified_path(path, ignorecase)
2045 };
2046 if !invalidated && ignorecase {
2047 invalidated = self.invalidate_fsmonitor_icase_fallback(path);
2048 }
2049 if invalidated {
2050 self.fsmonitor_validity_changed();
2051 }
2052 }
2053
2054 fn invalidate_fsmonitor_unqualified_path(&mut self, path: &str, ignorecase: bool) -> bool {
2055 if let Some(entry) = self.get_mut(path.as_bytes(), 0) {
2056 entry.set_fsmonitor_valid(false);
2057 return true;
2058 }
2059 if self.has_stage0_descendant_under(path) {
2060 return self.invalidate_fsmonitor_directory_cone(path, ignorecase);
2061 }
2062 false
2063 }
2064
2065 fn invalidate_fsmonitor_directory_cone(&mut self, prefix: &str, ignorecase: bool) -> bool {
2066 if prefix.is_empty() {
2067 self.invalidate_all_fsmonitor_valid();
2068 return true;
2069 }
2070 let prefix_slash = format!("{prefix}/");
2071 let mut any = false;
2072 for entry in &mut self.entries {
2073 if entry.stage() != 0 {
2074 continue;
2075 }
2076 let Some(rel) = std::str::from_utf8(&entry.path).ok() else {
2077 continue;
2078 };
2079 if Self::fsmonitor_path_under_prefix(rel, prefix, &prefix_slash, ignorecase) {
2080 entry.set_fsmonitor_valid(false);
2081 any = true;
2082 }
2083 }
2084 any
2085 }
2086
2087 fn invalidate_fsmonitor_icase_fallback(&mut self, path: &str) -> bool {
2088 let is_dir = path.ends_with('/');
2089 let prefix = path.trim_end_matches('/');
2090 let prefix_slash = format!("{prefix}/");
2091 let mut any = false;
2092 for entry in &mut self.entries {
2093 if entry.stage() != 0 {
2094 continue;
2095 }
2096 let Some(rel) = std::str::from_utf8(&entry.path).ok() else {
2097 continue;
2098 };
2099 let matches = if is_dir {
2100 Self::fsmonitor_path_under_prefix(rel, prefix, &prefix_slash, true)
2101 } else {
2102 rel.eq_ignore_ascii_case(prefix)
2103 };
2104 if matches {
2105 entry.set_fsmonitor_valid(false);
2106 any = true;
2107 }
2108 }
2109 any
2110 }
2111
2112 fn fsmonitor_path_under_prefix(
2113 rel: &str,
2114 prefix: &str,
2115 prefix_slash: &str,
2116 ignorecase: bool,
2117 ) -> bool {
2118 if ignorecase {
2119 rel.eq_ignore_ascii_case(prefix)
2120 || (rel.len() > prefix.len()
2121 && rel.as_bytes().get(prefix.len()) == Some(&b'/')
2122 && rel[..prefix.len()].eq_ignore_ascii_case(prefix))
2123 } else {
2124 rel == prefix || rel.starts_with(prefix_slash)
2125 }
2126 }
2127
2128 fn touch_fsmonitor_after_entry_change(&mut self) {
2130 self.invalidate_fsmonitor_bitmap();
2131 }
2132
2133 #[must_use]
2135 pub fn entries(&self) -> &[IndexEntry] {
2136 &self.entries
2137 }
2138
2139 pub fn entries_mut(&mut self) -> &mut Vec<IndexEntry> {
2141 self.entries_sorted = false;
2142 &mut self.entries
2143 }
2144
2145 pub fn mark_entries_unsorted(&mut self) {
2147 self.entries_sorted = false;
2148 }
2149
2150 pub(crate) fn push_entry_unsorted(&mut self, entry: IndexEntry) {
2152 self.entries.push(entry);
2153 self.entries_sorted = false;
2154 }
2155
2156 #[must_use]
2158 pub fn entries_are_sorted(&self) -> bool {
2159 self.entries
2160 .windows(2)
2161 .all(|pair| (&pair[0].path, pair[0].stage()) <= (&pair[1].path, pair[1].stage()))
2162 }
2163
2164 fn cmp_paths(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
2166 record_path_cmp_for_tests();
2167 a.cmp(b)
2168 }
2169
2170 fn entry_matches(entry: &IndexEntry, path: &[u8], stage: u8) -> bool {
2171 entry.path == path && entry.stage() == stage
2172 }
2173
2174 pub(crate) fn find_entry_pos(&self, path: &[u8], stage: u8) -> Option<usize> {
2176 if self.entries_sorted {
2177 return match self.entries.binary_search_by(|e| {
2178 Self::cmp_paths(e.path.as_slice(), path).then_with(|| e.stage().cmp(&stage))
2179 }) {
2180 Ok(pos) if Self::entry_matches(&self.entries[pos], path, stage) => Some(pos),
2181 _ => None,
2182 };
2183 }
2184 self.entries
2185 .iter()
2186 .position(|e| Self::entry_matches(e, path, stage))
2187 }
2188
2189 fn path_run_range_sorted(&self, path: &[u8]) -> (usize, usize) {
2191 debug_assert!(self.entries_sorted);
2192 let start = self
2193 .entries
2194 .partition_point(|e| Self::cmp_paths(e.path.as_slice(), path).is_lt());
2195 let end = start
2196 + self.entries[start..]
2197 .iter()
2198 .take_while(|e| e.path == path)
2199 .count();
2200 (start, end)
2201 }
2202
2203 #[must_use]
2206 pub fn has_stage0_descendant_under(&self, rel_dir: &str) -> bool {
2207 if rel_dir.is_empty() {
2208 return self.entries.iter().any(|e| e.stage() == 0);
2209 }
2210 if self.entries_sorted {
2211 let prefix = rel_dir.as_bytes();
2212 let start = self.descendant_run_start_sorted(prefix);
2213 return self.entries[start..]
2214 .iter()
2215 .take_while(|e| path_is_under_dir(&e.path, prefix))
2216 .any(|e| e.stage() == 0);
2217 }
2218 let prefix = format!("{rel_dir}/");
2219 self.entries
2220 .iter()
2221 .any(|e| e.stage() == 0 && e.path.starts_with(prefix.as_bytes()))
2222 }
2223
2224 pub(crate) fn sorted_entries_under(&self, dir: &[u8]) -> Option<&[IndexEntry]> {
2229 if !self.entries_sorted {
2230 return None;
2231 }
2232 if dir.is_empty() {
2233 return Some(&self.entries);
2234 }
2235 let (start, end) = self.descendant_run_range_sorted(dir);
2236 Some(&self.entries[start..end])
2237 }
2238
2239 pub(crate) fn sorted_contains_path(&self, path: &[u8]) -> Option<bool> {
2242 self.entries_sorted.then(|| {
2243 self.entries
2244 .binary_search_by(|e| Self::cmp_paths(e.path.as_slice(), path))
2245 .is_ok()
2246 })
2247 }
2248
2249 fn descendant_run_range_sorted(&self, path: &[u8]) -> (usize, usize) {
2251 let start = self.descendant_run_start_sorted(path);
2252 let len = self.entries[start..]
2253 .iter()
2254 .take_while(|e| path_is_under_dir(&e.path, path))
2255 .count();
2256 (start, start + len)
2257 }
2258
2259 fn descendant_run_start_sorted(&self, path: &[u8]) -> usize {
2261 debug_assert!(self.entries_sorted);
2262 let mut prefix_slash = Vec::with_capacity(path.len() + 1);
2263 prefix_slash.extend_from_slice(path);
2264 prefix_slash.push(b'/');
2265 self.entries.partition_point(|e| {
2266 Self::cmp_paths(e.path.as_slice(), prefix_slash.as_slice()).is_lt()
2267 })
2268 }
2269
2270 fn record_resolve_undo_for_path(&mut self, path: &[u8]) {
2271 for e in &self.entries {
2272 if e.path == path && e.stage() != 0 {
2273 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
2274 }
2275 }
2276 }
2277
2278 fn record_resolve_undo_for_run(&mut self, start: usize, end: usize) {
2279 for e in &self.entries[start..end] {
2280 if e.stage() != 0 {
2281 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
2282 }
2283 }
2284 }
2285
2286 pub(crate) fn drop_derived_index_extensions(&mut self) {
2288 self.cache_tree = None;
2289 self.cache_tree_root = None;
2290 self.untracked_cache = None;
2291 }
2292
2293 pub(crate) fn invalidate_cache_tree_for_bulk_checkout<'a>(
2295 &mut self,
2296 dir_prefixes: &BTreeSet<String>,
2297 paths: impl IntoIterator<Item = &'a [u8]>,
2298 replacements: &[IndexEntry],
2299 ) {
2300 if self.cache_tree.is_none() {
2301 return;
2302 }
2303 for dir in dir_prefixes {
2304 self.invalidate_cache_tree_for_path(dir.as_bytes());
2305 }
2306 for path in paths {
2307 self.invalidate_cache_tree_for_path(path);
2308 }
2309 for entry in replacements {
2310 self.invalidate_cache_tree_for_path(&entry.path);
2311 }
2312 }
2313
2314 fn invalidate_after_path_removal(&mut self, path: &[u8]) {
2315 if let Ok(p) = std::str::from_utf8(path) {
2316 self.invalidate_untracked_cache_for_path(p);
2317 }
2318 self.invalidate_cache_tree_for_path(path);
2319 }
2320
2321 pub(crate) fn remove_prefixes_paths_and_insert<'a>(
2323 &mut self,
2324 dir_prefixes: &BTreeSet<String>,
2325 paths: impl IntoIterator<Item = &'a [u8]>,
2326 replacements: impl IntoIterator<Item = IndexEntry>,
2327 ) {
2328 let mut replacements: Vec<IndexEntry> = replacements.into_iter().collect();
2329 if !replacements.is_empty() {
2330 replacements
2331 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
2332 dedup_sorted_replacement_entries(&mut replacements);
2333 }
2334
2335 let mut paths: Vec<&[u8]> = paths.into_iter().collect();
2336 paths.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2337 paths.dedup_by(|a, b| a == b);
2338
2339 let mut prefix_bytes: Vec<&[u8]> = dir_prefixes.iter().map(|d| d.as_bytes()).collect();
2340 prefix_bytes.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2341 prefix_bytes.dedup_by(|a, b| a == b);
2342
2343 if dir_prefixes.is_empty() && paths.is_empty() {
2344 self.merge_replacements_into_sorted_index(replacements);
2345 return;
2346 }
2347
2348 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2349 let mut kept = Vec::with_capacity(self.entries.len());
2350 let skip_per_path_extension_inval = self.defer_untracked_cache_invalidation;
2351
2352 if self.entries_sorted {
2353 let mut path_idx = 0usize;
2354 for entry in self.entries.drain(..) {
2355 let ep = entry.path.as_slice();
2356 while path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_lt() {
2357 path_idx += 1;
2358 }
2359 let remove = (path_idx < paths.len()
2360 && Self::cmp_paths(paths[path_idx], ep).is_eq())
2361 || Self::entry_under_any_directory_prefix(ep, &prefix_bytes);
2362 if remove {
2363 if entry.stage() != 0 {
2364 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2365 }
2366 if !skip_per_path_extension_inval
2367 && invalidated_paths.last().is_none_or(|p| p.as_slice() != ep)
2368 {
2369 invalidated_paths.push(entry.path.clone());
2370 }
2371 } else {
2372 kept.push(entry);
2373 }
2374 }
2375 let repl_for_inval: Vec<Vec<u8>> = if skip_per_path_extension_inval {
2376 Vec::new()
2377 } else {
2378 replacements
2379 .iter()
2380 .filter(|e| e.stage() == 0)
2381 .map(|e| e.path.clone())
2382 .collect()
2383 };
2384 self.entries = merge_sorted_index_entries(kept, replacements);
2385 self.entries_sorted = true;
2386 if skip_per_path_extension_inval {
2387 self.untracked_cache = None;
2388 self.touch_fsmonitor_after_entry_change();
2389 return;
2390 }
2391 const BULK_INDEX_EXTENSION_INVALIDATE_MIN: usize = 64;
2392 let mut invalidations = dir_prefixes.len();
2393 for path in &invalidated_paths {
2394 if dir_prefixes
2395 .iter()
2396 .all(|d| !Self::entry_under_directory_prefix(path.as_slice(), d.as_bytes()))
2397 {
2398 invalidations += 1;
2399 }
2400 }
2401 invalidations += repl_for_inval.len();
2402 if invalidations >= BULK_INDEX_EXTENSION_INVALIDATE_MIN {
2403 self.drop_derived_index_extensions();
2404 } else {
2405 for dir in dir_prefixes {
2406 self.invalidate_after_path_removal(dir.as_bytes());
2407 }
2408 for path in invalidated_paths {
2409 if dir_prefixes
2410 .iter()
2411 .all(|d| !Self::entry_under_directory_prefix(path.as_slice(), d.as_bytes()))
2412 {
2413 self.invalidate_after_path_removal(&path);
2414 }
2415 }
2416 for path in repl_for_inval {
2417 if let Ok(p) = std::str::from_utf8(&path) {
2418 self.invalidate_untracked_cache_for_path(p);
2419 }
2420 self.invalidate_cache_tree_for_path(&path);
2421 }
2422 }
2423 self.touch_fsmonitor_after_entry_change();
2424 return;
2425 }
2426 if !dir_prefixes.is_empty() {
2427 self.remove_directory_prefixes(dir_prefixes);
2428 }
2429 self.remove_paths_and_insert(paths, replacements);
2430 }
2431
2432 pub(crate) fn remove_directory_prefixes(&mut self, dirs: &BTreeSet<String>) {
2434 if dirs.is_empty() {
2435 return;
2436 }
2437 let mut prefixes: Vec<&[u8]> = dirs.iter().map(|d| d.as_bytes()).collect();
2438 prefixes.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2439 prefixes.dedup_by(|a, b| a == b);
2440
2441 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2442 let mut kept = Vec::with_capacity(self.entries.len());
2443
2444 if self.entries_sorted {
2445 for entry in self.entries.drain(..) {
2446 let ep = entry.path.as_slice();
2447 let remove = Self::entry_under_any_directory_prefix(ep, &prefixes);
2448 if remove {
2449 if entry.stage() != 0 {
2450 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2451 }
2452 if invalidated_paths.last().is_none_or(|p| p.as_slice() != ep) {
2453 invalidated_paths.push(entry.path.clone());
2454 }
2455 } else {
2456 kept.push(entry);
2457 }
2458 }
2459 self.entries = kept;
2460 } else {
2461 for entry in self.entries.drain(..) {
2462 let remove =
2463 Self::entry_under_any_directory_prefix(entry.path.as_slice(), &prefixes);
2464 if remove {
2465 if entry.stage() != 0 {
2466 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2467 }
2468 if invalidated_paths
2469 .last()
2470 .is_none_or(|p| p.as_slice() != entry.path)
2471 {
2472 invalidated_paths.push(entry.path.clone());
2473 }
2474 } else {
2475 kept.push(entry);
2476 }
2477 }
2478 self.entries = kept;
2479 }
2480 if invalidated_paths.is_empty() {
2481 return;
2482 }
2483 for dir in dirs {
2484 self.invalidate_after_path_removal(dir.as_bytes());
2485 }
2486 self.touch_fsmonitor_after_entry_change();
2487 }
2488
2489 fn entry_under_directory_prefix(path: &[u8], prefix: &[u8]) -> bool {
2490 if path.len() < prefix.len() {
2491 return false;
2492 }
2493 if path[..prefix.len()] != *prefix {
2494 return false;
2495 }
2496 path.len() == prefix.len() || path[prefix.len()] == b'/'
2497 }
2498
2499 fn entry_under_any_directory_prefix(path: &[u8], prefixes: &[&[u8]]) -> bool {
2500 prefixes
2501 .iter()
2502 .any(|prefix| Self::entry_under_directory_prefix(path, prefix))
2503 }
2504
2505 pub fn remove_paths<'a>(&mut self, paths: impl IntoIterator<Item = &'a [u8]>) {
2507 use std::collections::BTreeSet;
2508
2509 let mut paths: Vec<&[u8]> = paths.into_iter().collect();
2510 if paths.is_empty() {
2511 return;
2512 }
2513 paths.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2514 paths.dedup_by(|a, b| a == b);
2515
2516 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2517 let mut kept = Vec::with_capacity(self.entries.len());
2518
2519 if self.entries_sorted {
2520 let mut path_idx = 0usize;
2521 for entry in self.entries.drain(..) {
2522 let ep = entry.path.as_slice();
2523 while path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_lt() {
2524 path_idx += 1;
2525 }
2526 let remove = path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_eq();
2527 if remove {
2528 if entry.stage() != 0 {
2529 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2530 }
2531 if invalidated_paths.last().is_none_or(|p| p.as_slice() != ep) {
2532 invalidated_paths.push(entry.path.clone());
2533 }
2534 } else {
2535 kept.push(entry);
2536 }
2537 }
2538 self.entries = kept;
2539 } else {
2540 let remove_set: BTreeSet<&[u8]> = paths.into_iter().collect();
2541 for entry in self.entries.drain(..) {
2542 if remove_set.contains(entry.path.as_slice()) {
2543 if entry.stage() != 0 {
2544 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2545 }
2546 if invalidated_paths
2547 .last()
2548 .is_none_or(|p| p.as_slice() != entry.path.as_slice())
2549 {
2550 invalidated_paths.push(entry.path.clone());
2551 }
2552 } else {
2553 kept.push(entry);
2554 }
2555 }
2556 self.entries = kept;
2557 }
2558
2559 for path in invalidated_paths {
2560 self.invalidate_after_path_removal(&path);
2561 }
2562 self.touch_fsmonitor_after_entry_change();
2563 }
2564
2565 pub(crate) fn replace_stage0_entries_batch(&mut self, replacements: Vec<IndexEntry>) {
2567 self.merge_replacements_into_sorted_index(replacements);
2568 }
2569
2570 pub(crate) fn mark_content_verified(&mut self, path: &[u8], oid: ObjectId) {
2573 self.content_verified.insert(path.to_vec(), oid);
2574 }
2575
2576 pub(crate) fn clear_content_verified(&mut self) {
2578 self.content_verified.clear();
2579 for entry in &mut self.entries {
2580 entry.take_content_verified();
2581 }
2582 }
2583
2584 pub(crate) fn overwrite_stage0_entries(&mut self, replacements: Vec<IndexEntry>) {
2590 const BULK_INVALIDATE_MIN: usize = 64;
2591 if replacements.is_empty() {
2592 return;
2593 }
2594 let defer_untracked = self.defer_untracked_cache_invalidation;
2595 let drop_caches = !defer_untracked && replacements.len() >= BULK_INVALIDATE_MIN;
2596 let mut unmatched = Vec::new();
2597 for mut entry in replacements {
2598 let pos = if entry.stage() == 0 {
2599 self.find_entry_pos(&entry.path, 0)
2600 } else {
2601 None
2602 };
2603 let Some(pos) = pos else {
2604 unmatched.push(entry);
2605 continue;
2606 };
2607 if !drop_caches {
2608 if !defer_untracked {
2609 if let Ok(p) = std::str::from_utf8(&entry.path) {
2610 self.invalidate_untracked_cache_for_path(p);
2611 }
2612 }
2613 self.invalidate_cache_tree_for_path(&entry.path);
2614 }
2615 entry.base_index_pos = self.entries[pos].base_index_pos;
2616 self.entries[pos] = entry;
2617 }
2618 if drop_caches {
2619 self.cache_tree = None;
2620 self.cache_tree_root = None;
2621 self.untracked_cache = None;
2622 }
2623 self.touch_fsmonitor_after_entry_change();
2624 if !unmatched.is_empty() {
2625 self.merge_replacements_into_sorted_index(unmatched);
2626 }
2627 }
2628
2629 fn merge_sorted_replacements_into_entries(&mut self, mut replacements: Vec<IndexEntry>) {
2631 if replacements.is_empty() {
2632 return;
2633 }
2634 if self.entries.is_empty() {
2635 self.entries = replacements;
2636 return;
2637 }
2638 self.entries.reserve(replacements.len());
2639 let mid = self.entries.len();
2640 self.entries.append(&mut replacements);
2641 merge_sorted_index_adjacent_halves(&mut self.entries, mid);
2642 }
2643
2644 fn merge_replacements_into_sorted_index(&mut self, replacements: Vec<IndexEntry>) {
2646 if replacements.is_empty() {
2647 return;
2648 }
2649 if !self.entries_sorted {
2650 for entry in replacements {
2651 self.add_or_replace(entry);
2652 }
2653 return;
2654 }
2655 const BULK_MERGE_MIN: usize = 64;
2656 if self.defer_untracked_cache_invalidation {
2657 for entry in replacements.iter().filter(|e| e.stage() == 0) {
2660 self.invalidate_cache_tree_for_path(&entry.path);
2661 }
2662 self.merge_sorted_replacements_into_entries(replacements);
2663 self.touch_fsmonitor_after_entry_change();
2664 return;
2665 }
2666 let repl_for_inval: Vec<Vec<u8>> = replacements
2667 .iter()
2668 .filter(|e| e.stage() == 0)
2669 .map(|e| e.path.clone())
2670 .collect();
2671 self.merge_sorted_replacements_into_entries(replacements);
2672 if repl_for_inval.len() >= BULK_MERGE_MIN {
2673 self.cache_tree = None;
2674 self.cache_tree_root = None;
2675 self.untracked_cache = None;
2676 self.touch_fsmonitor_after_entry_change();
2677 return;
2678 }
2679 for path in repl_for_inval {
2680 if let Ok(p) = std::str::from_utf8(&path) {
2681 self.invalidate_untracked_cache_for_path(p);
2682 }
2683 self.invalidate_cache_tree_for_path(&path);
2684 }
2685 self.touch_fsmonitor_after_entry_change();
2686 }
2687
2688 pub fn remove_paths_and_insert<'a>(
2693 &mut self,
2694 paths: impl IntoIterator<Item = &'a [u8]>,
2695 replacements: impl IntoIterator<Item = IndexEntry>,
2696 ) {
2697 let mut replacements: Vec<IndexEntry> = replacements.into_iter().collect();
2698 if replacements.is_empty() {
2699 self.remove_paths(paths);
2700 return;
2701 }
2702 replacements.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
2703 dedup_sorted_replacement_entries(&mut replacements);
2704
2705 let mut paths: Vec<&[u8]> = paths.into_iter().collect();
2706 if paths.is_empty() {
2707 self.merge_replacements_into_sorted_index(replacements);
2708 return;
2709 }
2710 paths.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2711 paths.dedup_by(|a, b| a == b);
2712
2713 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2714 let mut kept = Vec::with_capacity(self.entries.len());
2715
2716 if self.entries_sorted {
2717 let mut path_idx = 0usize;
2718 for entry in self.entries.drain(..) {
2719 let ep = entry.path.as_slice();
2720 while path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_lt() {
2721 path_idx += 1;
2722 }
2723 let remove = path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_eq();
2724 if remove {
2725 if entry.stage() != 0 {
2726 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2727 }
2728 if invalidated_paths.last().is_none_or(|p| p.as_slice() != ep) {
2729 invalidated_paths.push(entry.path.clone());
2730 }
2731 } else {
2732 kept.push(entry);
2733 }
2734 }
2735 let repl_for_inval = replacements
2736 .iter()
2737 .filter(|e| e.stage() == 0)
2738 .map(|e| e.path.clone())
2739 .collect::<Vec<_>>();
2740 self.entries = merge_sorted_index_entries(kept, replacements);
2741 self.entries_sorted = true;
2742 const BULK_INDEX_EXTENSION_INVALIDATE_MIN: usize = 64;
2743 let invalidations = invalidated_paths.len() + repl_for_inval.len();
2744 if invalidations >= BULK_INDEX_EXTENSION_INVALIDATE_MIN {
2745 self.drop_derived_index_extensions();
2746 } else {
2747 for path in invalidated_paths {
2748 self.invalidate_after_path_removal(&path);
2749 }
2750 for path in repl_for_inval {
2751 if let Ok(p) = std::str::from_utf8(&path) {
2752 self.invalidate_untracked_cache_for_path(p);
2753 }
2754 self.invalidate_cache_tree_for_path(&path);
2755 }
2756 }
2757 self.touch_fsmonitor_after_entry_change();
2758 return;
2759 }
2760 self.remove_paths(paths);
2761 for entry in replacements {
2762 self.add_or_replace(entry);
2763 }
2764 }
2765
2766 pub fn add_or_replace(&mut self, entry: IndexEntry) {
2768 let path = entry.path.clone();
2769 let stage = entry.stage();
2770 let mut inserted_stage0 = false;
2771 if self.entries_sorted {
2772 let result = self.entries.binary_search_by(|e| {
2773 e.path
2774 .as_slice()
2775 .cmp(path.as_slice())
2776 .then_with(|| e.stage().cmp(&stage))
2777 });
2778 match result {
2779 Ok(pos) => {
2780 let mut e = entry;
2781 e.base_index_pos = self.entries[pos].base_index_pos;
2782 self.entries[pos] = e;
2783 }
2784 Err(pos) => {
2785 self.entries.insert(pos, entry);
2786 inserted_stage0 = stage == 0;
2787 }
2788 }
2789 } else if let Some(pos) = self
2790 .entries
2791 .iter()
2792 .position(|e| e.path == path && e.stage() == stage)
2793 {
2794 let mut e = entry;
2795 e.base_index_pos = self.entries[pos].base_index_pos;
2796 self.entries[pos] = e;
2797 } else {
2798 self.entries.push(entry);
2799 inserted_stage0 = stage == 0;
2800 }
2801 if inserted_stage0 {
2802 if let Ok(p) = std::str::from_utf8(&path) {
2803 self.invalidate_untracked_cache_for_path(p);
2804 }
2805 }
2806 if stage == 0 {
2807 self.invalidate_cache_tree_for_path(&path);
2808 }
2809 self.touch_fsmonitor_after_entry_change();
2810 }
2811
2812 #[must_use]
2814 pub fn has_unmerged_entries(&self) -> bool {
2815 self.entries.iter().any(|e| e.stage() != 0)
2816 }
2817
2818 pub fn stage_file(&mut self, entry: IndexEntry) {
2822 let path = entry.path.clone();
2823 if self.entries_sorted {
2824 let (start, end) = self.path_run_range_sorted(&path);
2825 for i in (start..end).rev() {
2826 if self.entries[i].stage() != 0 {
2827 resolve_undo::record_resolve_undo_for_entry(
2828 &mut self.resolve_undo,
2829 &self.entries[i],
2830 );
2831 self.entries.remove(i);
2832 }
2833 }
2834 } else {
2835 for e in &self.entries {
2836 if e.path == path && e.stage() != 0 {
2837 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
2838 }
2839 }
2840 self.entries.retain(|e| e.path != path || e.stage() == 0);
2841 }
2842 self.add_or_replace(entry);
2843 }
2844
2845 pub fn clear_resolve_undo(&mut self) {
2847 self.resolve_undo = None;
2848 }
2849
2850 pub fn take_resolve_undo_record(&mut self, path: &[u8]) -> Option<ResolveUndoRecord> {
2852 let map = self.resolve_undo.as_mut()?;
2853 let ru = map.remove(path)?;
2854 if map.is_empty() {
2855 self.resolve_undo = None;
2856 }
2857 Some(ru)
2858 }
2859
2860 pub fn install_unmerged_from_resolve_undo(&mut self, path: &[u8], record: &ResolveUndoRecord) {
2862 if self.entries_sorted {
2863 let (start, end) = self.path_run_range_sorted(path);
2864 if start < end {
2865 self.entries.drain(start..end);
2866 }
2867 } else {
2868 self.entries.retain(|e| e.path != path);
2869 }
2870 for stage in 1u8..=3u8 {
2871 let i = (stage - 1) as usize;
2872 if record.modes[i] == 0 {
2873 continue;
2874 }
2875 let entry = IndexEntry {
2876 ctime_sec: 0,
2877 ctime_nsec: 0,
2878 mtime_sec: 0,
2879 mtime_nsec: 0,
2880 dev: 0,
2881 ino: 0,
2882 mode: record.modes[i],
2883 uid: 0,
2884 gid: 0,
2885 size: 0,
2886 oid: record.oids[i],
2887 flags: path.len().min(0xFFF) as u16 | ((stage as u16) << 12),
2888 flags_extended: None,
2889 path: path.to_vec(),
2890 base_index_pos: 0,
2891 };
2892 self.add_or_replace(entry);
2893 }
2894 }
2895
2896 pub fn unmerge_path_from_resolve_undo(&mut self, path: &[u8]) -> bool {
2900 let Some(record) = self.take_resolve_undo_record(path) else {
2901 return false;
2902 };
2903 self.install_unmerged_from_resolve_undo(path, &record);
2904 if let Ok(p) = std::str::from_utf8(path) {
2911 self.invalidate_untracked_cache_for_path(p);
2912 }
2913 self.invalidate_cache_tree_for_path(path);
2914 true
2915 }
2916
2917 pub fn remove(&mut self, path: &[u8]) -> bool {
2921 if self.entries_sorted {
2922 let (start, end) = self.path_run_range_sorted(path);
2923 if start < end {
2924 self.record_resolve_undo_for_run(start, end);
2925 self.entries.drain(start..end);
2926 self.invalidate_after_path_removal(path);
2927 self.touch_fsmonitor_after_entry_change();
2928 return true;
2929 }
2930 if !self.entries.iter().any(|e| e.path == path) {
2931 return false;
2932 }
2933 self.entries_sorted = false;
2934 }
2935 self.record_resolve_undo_for_path(path);
2936 let before = self.entries.len();
2937 self.entries.retain(|e| e.path != path);
2938 if self.entries.len() == before {
2939 return false;
2940 }
2941 self.invalidate_after_path_removal(path);
2942 self.touch_fsmonitor_after_entry_change();
2943 true
2944 }
2945
2946 pub fn remove_path_all_stages(&mut self, path: &[u8]) -> bool {
2950 self.remove(path)
2951 }
2952
2953 pub fn invalidate_untracked_cache_for_path(&mut self, path: &str) {
2955 if self.defer_untracked_cache_invalidation {
2956 return;
2957 }
2958 if let Some(uc) = self.untracked_cache.as_mut() {
2959 untracked_cache::invalidate_path(uc, path);
2960 }
2961 }
2962
2963 pub fn remove_descendants_under_path(&mut self, path: &str) {
2969 let prefix = path.as_bytes();
2970 if prefix.is_empty() {
2971 return;
2972 }
2973 let plen = prefix.len();
2974 if self.entries_sorted {
2975 let (start, end) = self.descendant_run_range_sorted(prefix);
2976 if start < end {
2977 self.record_resolve_undo_for_run(start, end);
2978 self.entries.drain(start..end);
2979 self.invalidate_untracked_cache_for_path(path);
2980 self.invalidate_cache_tree_for_path(path.as_bytes());
2981 return;
2982 }
2983 let any = self.entries.iter().any(|e| {
2984 let ep = e.path.as_slice();
2985 ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/'
2986 });
2987 if !any {
2988 return;
2989 }
2990 self.entries_sorted = false;
2991 }
2992 {
2993 let had_descendant = self.entries.iter().any(|e| {
2994 let ep = e.path.as_slice();
2995 ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/'
2996 });
2997 for e in self.entries.iter() {
2998 let ep = e.path.as_slice();
2999 if ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/' && e.stage() != 0 {
3000 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
3001 }
3002 }
3003 self.entries.retain(|e| {
3004 let ep = e.path.as_slice();
3005 if ep.len() <= plen {
3006 return true;
3007 }
3008 if !ep.starts_with(prefix) {
3009 return true;
3010 }
3011 ep[plen] != b'/'
3012 });
3013 if !had_descendant {
3014 return;
3015 }
3016 }
3017 self.invalidate_untracked_cache_for_path(path);
3018 self.invalidate_cache_tree_for_path(path.as_bytes());
3019 }
3020
3021 pub fn set_cache_tree(&mut self, cache_tree: CacheTreeNode) {
3023 self.cache_tree_root = cache_tree.oid.filter(|_| cache_tree.entry_count >= 0);
3024 self.cache_tree = Some(cache_tree);
3025 }
3026
3027 pub fn clear_cache_tree(&mut self) {
3029 self.cache_tree_root = None;
3030 self.cache_tree = None;
3031 }
3032
3033 pub fn invalidate_cache_tree_for_path(&mut self, path: &[u8]) {
3043 let Some(root) = self.cache_tree.as_mut() else {
3044 self.cache_tree_root = None;
3045 return;
3046 };
3047 self.cache_tree_root = None;
3048 Self::do_invalidate_cache_tree_path(root, path);
3049 }
3050
3051 fn do_invalidate_cache_tree_path(node: &mut CacheTreeNode, path: &[u8]) {
3054 node.invalidate();
3055 let slash = path.iter().position(|&b| b == b'/');
3056 match slash {
3057 None => {
3059 if !path.is_empty() {
3060 node.children.retain(|child| child.name != path);
3061 }
3062 }
3063 Some(idx) => {
3065 let (component, rest) = (&path[..idx], &path[idx + 1..]);
3066 if let Some(child) = node
3067 .children
3068 .iter_mut()
3069 .find(|child| child.name == component)
3070 {
3071 Self::do_invalidate_cache_tree_path(child, rest);
3072 }
3073 }
3074 }
3075 }
3076
3077 #[must_use]
3079 pub fn format_cache_tree_dump(&self) -> String {
3080 let Some(root) = self.cache_tree.as_ref() else {
3081 return String::new();
3082 };
3083 let mut out = String::new();
3084 format_cache_tree_node(root, "", &mut out);
3085 out
3086 }
3087
3088 pub fn dump_cache_tree(&self, odb: &Odb) -> Result<String> {
3104 let Some(it_root) = self.cache_tree.as_ref() else {
3105 return Ok(String::new());
3106 };
3107 let ref_root = crate::write_tree::build_cache_tree_from_index(odb, self)?;
3108 let mut out = String::new();
3109 dump_cache_tree_pair(it_root, &ref_root, "", &mut out);
3110 Ok(out)
3111 }
3112
3113 pub fn sort(&mut self) {
3115 self.ensure_canonical_order();
3116 }
3117
3118 pub fn ensure_canonical_order(&mut self) {
3123 if self.entries_sorted && self.entries_are_sorted() {
3124 return;
3125 }
3126 self.sort_entries_canonical();
3127 }
3128
3129 fn sort_entries_canonical(&mut self) {
3130 self.entries
3131 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
3132 self.entries_sorted = true;
3133 }
3134
3135 pub fn dedup_paths_keep_last(&mut self) {
3144 let mut seen: std::collections::HashSet<(Vec<u8>, u8)> = std::collections::HashSet::new();
3145 let mut kept: Vec<IndexEntry> = Vec::with_capacity(self.entries.len());
3146 for entry in self.entries.iter().rev() {
3148 if seen.insert((entry.path.clone(), entry.stage())) {
3149 kept.push(entry.clone());
3150 }
3151 }
3152 kept.reverse();
3153 self.entries = kept;
3154 self.entries_sorted = false;
3155 }
3156
3157 #[must_use]
3159 pub fn split_index_base_oid(&self) -> Option<ObjectId> {
3160 self.split_link.as_ref().map(|l| l.base_oid)
3161 }
3162
3163 #[must_use]
3165 pub fn get(&self, path: &[u8], stage: u8) -> Option<&IndexEntry> {
3166 self.find_entry_pos(path, stage)
3167 .map(|pos| &self.entries[pos])
3168 }
3169
3170 pub fn get_mut(&mut self, path: &[u8], stage: u8) -> Option<&mut IndexEntry> {
3172 let pos = self.find_entry_pos(path, stage)?;
3173 let entry = &mut self.entries[pos];
3174 entry.take_content_verified();
3176 Some(entry)
3177 }
3178
3179 pub(crate) fn stage0_path_positions<'a>(
3184 &'a self,
3185 ) -> std::collections::HashMap<Cow<'a, str>, usize> {
3186 let stage0_count = self.entries.iter().filter(|e| e.stage() == 0).count();
3187 let mut positions = std::collections::HashMap::with_capacity(stage0_count);
3188 for (pos, entry) in self.entries.iter().enumerate() {
3189 if entry.stage() == 0 {
3190 let key = index_path_outcome_key(&entry.path);
3191 positions.entry(key).or_insert(pos);
3192 }
3193 }
3194 positions
3195 }
3196
3197 pub fn overlay_tree_on_index(
3216 &mut self,
3217 repo: &Repository,
3218 treeish: &str,
3219 prefix: &[u8],
3220 ) -> Result<()> {
3221 let oid = rev_parse::resolve_revision(repo, treeish)?;
3222 let tree_oid = repo.odb().peel_to_tree(oid)?.ok_or_else(|| {
3223 Error::ObjectNotFound(format!("cannot peel {oid} to tree for --with-tree"))
3224 })?;
3225 for e in self.entries.iter_mut() {
3226 if e.stage() != 0 {
3227 e.set_stage(3);
3228 }
3229 }
3230 self.sort();
3231 let has_stage1 = self.entries.iter().any(|e| e.stage() == 1);
3232 let mut appended: Vec<IndexEntry> = Vec::new();
3233 let protect = crate::path_validate::ProtectOptions::from_config(&*repo.config()?);
3234 read_tree_into_overlay(
3235 repo,
3236 &tree_oid,
3237 prefix,
3238 &[],
3239 has_stage1,
3240 protect,
3241 &mut appended,
3242 )?;
3243 for e in appended {
3244 self.add_or_replace(e);
3245 }
3246 if !has_stage1 {
3247 self.sort();
3248 }
3249 let mut last_stage0: Option<&[u8]> = None;
3250 for e in &mut self.entries {
3251 match e.stage() {
3252 0 => {
3253 last_stage0 = Some(e.path.as_slice());
3254 }
3255 1 if last_stage0.is_some_and(|p| p == e.path.as_slice()) => {
3256 e.set_overlay_tree_skip_output(true);
3257 }
3258 _ => {}
3259 }
3260 }
3261 Ok(())
3262 }
3263}
3264
3265#[allow(clippy::too_many_arguments)]
3266fn read_tree_into_overlay(
3267 repo: &Repository,
3268 tree_oid: &ObjectId,
3269 prefix: &[u8],
3270 rel_base: &[u8],
3271 use_replace_path: bool,
3272 protect: crate::path_validate::ProtectOptions,
3273 out: &mut Vec<IndexEntry>,
3274) -> Result<()> {
3275 let obj = repo.odb().read(tree_oid)?;
3276 if obj.kind != ObjectKind::Tree {
3277 return Err(Error::ObjectNotFound(format!(
3278 "object {tree_oid} is not a tree"
3279 )));
3280 }
3281 let entries = parse_tree(&obj.data)?;
3282 for TreeEntry { mode, name, oid } in entries {
3283 let mut path = rel_base.to_vec();
3284 if !path.is_empty() {
3285 path.push(b'/');
3286 }
3287 path.extend_from_slice(&name);
3288 if !prefix_under_or_equal(prefix, &path) {
3289 continue;
3290 }
3291 crate::path_population::validate_tree_walk_entry(rel_base, &name, mode, protect)?;
3292 if mode == MODE_TREE {
3293 read_tree_into_overlay(repo, &oid, prefix, &path, use_replace_path, protect, out)?;
3294 continue;
3295 }
3296 if mode == MODE_GITLINK {
3297 continue;
3298 }
3299 let entry = synthetic_stage1_index_entry(mode, &path, oid);
3300 if use_replace_path {
3301 if let Some(pos) = out.iter().position(|e| e.path == path && e.stage() == 1) {
3302 out[pos] = entry;
3303 } else {
3304 out.push(entry);
3305 }
3306 } else {
3307 out.push(entry);
3308 }
3309 }
3310 Ok(())
3311}
3312
3313fn dedup_sorted_replacement_entries(replacements: &mut Vec<IndexEntry>) {
3315 if replacements.len() <= 1 {
3316 return;
3317 }
3318 let mut out: Vec<IndexEntry> = Vec::with_capacity(replacements.len());
3319 for entry in replacements.drain(..) {
3320 match out.last_mut() {
3321 Some(last) if last.path == entry.path && last.stage() == entry.stage() => {
3322 *last = entry;
3323 }
3324 _ => out.push(entry),
3325 }
3326 }
3327 *replacements = out;
3328}
3329
3330fn index_entry_path_stage_cmp(a: &IndexEntry, b: &IndexEntry) -> std::cmp::Ordering {
3331 a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage()))
3332}
3333
3334fn merge_sorted_index_entries(
3338 mut kept: Vec<IndexEntry>,
3339 mut insert: Vec<IndexEntry>,
3340) -> Vec<IndexEntry> {
3341 if insert.is_empty() {
3342 return kept;
3343 }
3344 if kept.is_empty() {
3345 return insert;
3346 }
3347 kept.reserve(insert.len());
3348 let mid = kept.len();
3349 kept.append(&mut insert);
3350 merge_sorted_index_adjacent_halves(&mut kept, mid);
3351 kept
3352}
3353
3354fn merge_sorted_index_adjacent_halves(v: &mut Vec<IndexEntry>, mid: usize) {
3356 let n = v.len();
3357 if mid == 0 || mid >= n {
3358 return;
3359 }
3360 let right_len = n - mid;
3361 let smaller_is_right = right_len <= mid;
3362 let mut aux = if smaller_is_right {
3363 v.drain(mid..).collect::<Vec<_>>()
3364 } else {
3365 v.drain(..mid).collect::<Vec<_>>()
3366 };
3367 let left_len = if smaller_is_right { v.len() } else { aux.len() };
3368 let pad = aux.len();
3369 v.reserve(pad);
3370 for _ in 0..pad {
3371 v.push(merge_padding_index_entry());
3372 }
3373 let total = v.len();
3374 merge_backward_into_place(v, left_len, total, &mut aux, smaller_is_right);
3375}
3376
3377fn merge_padding_index_entry() -> IndexEntry {
3379 IndexEntry {
3380 ctime_sec: 0,
3381 ctime_nsec: 0,
3382 mtime_sec: 0,
3383 mtime_nsec: 0,
3384 dev: 0,
3385 ino: 0,
3386 mode: 0,
3387 uid: 0,
3388 gid: 0,
3389 size: 0,
3390 oid: ObjectId::zero(),
3391 flags: 0,
3392 flags_extended: None,
3393 path: Vec::new(),
3394 base_index_pos: 0,
3395 }
3396}
3397
3398fn merge_backward_into_place(
3400 v: &mut Vec<IndexEntry>,
3401 left_len: usize,
3402 total: usize,
3403 aux: &mut Vec<IndexEntry>,
3404 smaller_is_right: bool,
3405) {
3406 let pad = aux.len();
3407 let mut li = left_len;
3408 let mut ri = if smaller_is_right {
3409 aux.len()
3410 } else {
3411 total.saturating_sub(pad)
3412 };
3413 let mut k = total;
3414 while li > 0 && ri > 0 {
3415 k -= 1;
3416 let ord = if smaller_is_right {
3417 index_entry_path_stage_cmp(&v[li - 1], &aux[ri - 1])
3418 } else {
3419 index_entry_path_stage_cmp(&aux[li - 1], &v[ri - 1])
3420 };
3421 match ord {
3422 std::cmp::Ordering::Greater => {
3423 if smaller_is_right {
3424 let entry = std::mem::replace(&mut v[li - 1], merge_padding_index_entry());
3425 drop(std::mem::replace(&mut v[k], entry));
3426 li -= 1;
3427 } else {
3428 let entry = std::mem::replace(&mut aux[li - 1], merge_padding_index_entry());
3429 li -= 1;
3430 drop(std::mem::replace(&mut v[k], entry));
3431 }
3432 }
3433 std::cmp::Ordering::Equal => {
3434 let base = if smaller_is_right {
3435 v[li - 1].base_index_pos
3436 } else {
3437 aux[li - 1].base_index_pos
3438 };
3439 let mut entry = if smaller_is_right {
3440 let Some(e) = aux.pop() else {
3441 break;
3442 };
3443 ri -= 1;
3444 e
3445 } else {
3446 ri -= 1;
3447 std::mem::replace(&mut v[ri], merge_padding_index_entry())
3448 };
3449 entry.base_index_pos = base;
3450 if smaller_is_right {
3451 drop(std::mem::replace(
3452 &mut v[li - 1],
3453 merge_padding_index_entry(),
3454 ));
3455 li -= 1;
3456 } else {
3457 drop(std::mem::replace(
3458 &mut aux[li - 1],
3459 merge_padding_index_entry(),
3460 ));
3461 li -= 1;
3462 }
3463 drop(std::mem::replace(&mut v[k], entry));
3464 }
3465 std::cmp::Ordering::Less => {
3466 if smaller_is_right {
3467 let Some(entry) = aux.pop() else {
3468 break;
3469 };
3470 ri -= 1;
3471 drop(std::mem::replace(&mut v[k], entry));
3472 } else {
3473 ri -= 1;
3474 let entry = std::mem::replace(&mut v[ri], merge_padding_index_entry());
3475 drop(std::mem::replace(&mut v[k], entry));
3476 }
3477 }
3478 }
3479 }
3480 while ri > 0 {
3481 k -= 1;
3482 if smaller_is_right {
3483 let Some(entry) = aux.pop() else {
3484 break;
3485 };
3486 ri -= 1;
3487 drop(std::mem::replace(&mut v[k], entry));
3488 } else {
3489 ri -= 1;
3490 let entry = std::mem::replace(&mut v[ri], merge_padding_index_entry());
3491 drop(std::mem::replace(&mut v[k], entry));
3492 }
3493 }
3494 if !smaller_is_right && li > 0 {
3495 for i in 0..li {
3496 let entry = std::mem::replace(&mut aux[i], merge_padding_index_entry());
3497 drop(std::mem::replace(&mut v[i], entry));
3498 }
3499 }
3500 let tail_len = total - k;
3501 if li < k && tail_len > 0 {
3502 for offset in 0..tail_len {
3503 let entry = std::mem::replace(&mut v[k + offset], merge_padding_index_entry());
3504 drop(std::mem::replace(&mut v[li + offset], entry));
3505 }
3506 }
3507 v.truncate(li + tail_len);
3508}
3509
3510fn prefix_under_or_equal(prefix: &[u8], path: &[u8]) -> bool {
3511 if prefix.is_empty() {
3512 return true;
3513 }
3514 if path == prefix {
3515 return true;
3516 }
3517 path.len() > prefix.len() && path.starts_with(prefix) && path[prefix.len()] == b'/'
3518}
3519
3520fn synthetic_stage1_index_entry(mode: u32, path: &[u8], oid: ObjectId) -> IndexEntry {
3521 let path_len = path.len().min(0xFFF) as u16;
3522 let flags = (1u16 << 12) | path_len;
3523 IndexEntry {
3524 ctime_sec: 0,
3525 ctime_nsec: 0,
3526 mtime_sec: 0,
3527 mtime_nsec: 0,
3528 dev: 0,
3529 ino: 0,
3530 mode,
3531 uid: 0,
3532 gid: 0,
3533 size: 0,
3534 oid,
3535 flags,
3536 flags_extended: None,
3537 path: path.to_vec(),
3538 base_index_pos: 0,
3539 }
3540}
3541
3542fn config_truthy(raw: Option<&str>) -> bool {
3543 let Some(val) = raw else {
3544 return false;
3545 };
3546 let lowered = val.trim().to_lowercase();
3547 matches!(lowered.as_str(), "true" | "yes" | "1" | "on")
3548}
3549
3550pub(crate) fn index_skip_hash_for_write(config: Option<&ConfigSet>) -> bool {
3555 let Some(config) = config else {
3556 return false;
3557 };
3558 let many_files = config
3559 .get_bool("feature.manyFiles")
3560 .and_then(|r| r.ok())
3561 .unwrap_or(false);
3562 if many_files {
3563 if let Some(Ok(false)) = config.get_bool("index.skipHash") {
3564 return false;
3565 }
3566 if let Some(Ok(false)) = config.get_bool("index.skiphash") {
3567 return false;
3568 }
3569 return true;
3570 }
3571 for key in ["index.skipHash", "index.skiphash"] {
3572 if let Some(Ok(true)) = config.get_bool(key) {
3573 return true;
3574 }
3575 }
3576 false
3577}
3578
3579fn trim_trailing_slash_bytes(path: &[u8]) -> &[u8] {
3580 path.strip_suffix(b"/").unwrap_or(path)
3581}
3582
3583fn directory_in_cone(dir_path: &str, patterns: &[String], cone_mode: bool) -> bool {
3584 let dir = dir_path.trim_end_matches('/');
3593 if dir.is_empty() {
3594 return true;
3595 }
3596 let with_slash = format!("{dir}/");
3597 crate::sparse_checkout::path_matches_sparse_patterns(&with_slash, patterns, cone_mode)
3598}
3599
3600fn collect_directory_prefixes(path: &[u8], out: &mut BTreeSet<Vec<u8>>) {
3601 for (i, &b) in path.iter().enumerate() {
3602 if b == b'/' && !out.contains(&path[..i]) {
3603 out.insert(path[..i].to_vec());
3604 }
3605 }
3606}
3607
3608#[derive(Default)]
3612struct CollapsedDirectories {
3613 dirs: BTreeMap<Vec<u8>, ObjectId>,
3614}
3615
3616impl CollapsedDirectories {
3617 fn is_empty(&self) -> bool {
3618 self.dirs.is_empty()
3619 }
3620
3621 fn insert(&mut self, directory: Vec<u8>, oid: ObjectId) {
3623 let nested: Vec<Vec<u8>> = self
3624 .dirs
3625 .range::<[u8], _>((
3626 std::ops::Bound::Excluded(directory.as_slice()),
3627 std::ops::Bound::Unbounded,
3628 ))
3629 .map(|(k, _)| k)
3630 .take_while(|k| k.starts_with(&directory))
3631 .cloned()
3632 .collect();
3633 for key in nested {
3634 self.dirs.remove(&key);
3635 }
3636 self.dirs.insert(directory, oid);
3637 }
3638
3639 fn get(&self, directory: &[u8]) -> Option<&ObjectId> {
3640 self.dirs.get(directory)
3641 }
3642
3643 fn has_descendants(&self, directory: &[u8]) -> bool {
3645 self.dirs
3646 .range::<[u8], _>((
3647 std::ops::Bound::Excluded(directory),
3648 std::ops::Bound::Unbounded,
3649 ))
3650 .next()
3651 .is_some_and(|(k, _)| k.starts_with(directory))
3652 }
3653
3654 fn covered_rows(&self, entries: &[IndexEntry]) -> Vec<bool> {
3657 let mut covered = vec![false; entries.len()];
3658 let mut mark = |start: usize, matches: &dyn Fn(&[u8]) -> bool| {
3659 for (row, entry) in covered[start..].iter_mut().zip(&entries[start..]) {
3660 if !matches(&entry.path) {
3661 break;
3662 }
3663 *row = true;
3664 }
3665 };
3666 for directory in self.dirs.keys() {
3667 let name = trim_trailing_slash_bytes(directory);
3668 let start = entries.partition_point(|e| e.path.as_slice() < name);
3669 mark(start, &|path| path == name);
3670 let start = entries.partition_point(|e| e.path.as_slice() < directory.as_slice());
3671 mark(start, &|path| path.starts_with(directory));
3672 }
3673 covered
3674 }
3675
3676 fn covers(&self, path: &[u8], scratch: &mut Vec<u8>) -> bool {
3679 if self.dirs.is_empty() {
3680 return false;
3681 }
3682 let under = self
3685 .dirs
3686 .range::<[u8], _>((std::ops::Bound::Unbounded, std::ops::Bound::Included(path)))
3687 .next_back()
3688 .is_some_and(|(k, _)| path.starts_with(k));
3689 if under {
3690 return true;
3691 }
3692 scratch.clear();
3693 scratch.extend_from_slice(path);
3694 scratch.push(b'/');
3695 self.dirs.contains_key(scratch.as_slice())
3696 }
3697
3698 fn into_placeholders(self) -> impl Iterator<Item = IndexEntry> {
3699 self.dirs.into_iter().map(|(path, oid)| {
3700 let mut placeholder = IndexEntry {
3701 ctime_sec: 0,
3702 ctime_nsec: 0,
3703 mtime_sec: 0,
3704 mtime_nsec: 0,
3705 dev: 0,
3706 ino: 0,
3707 mode: MODE_TREE,
3708 uid: 0,
3709 gid: 0,
3710 size: 0,
3711 oid,
3712 flags: path.len().min(0xFFF) as u16,
3713 flags_extended: Some(0),
3714 path,
3715 base_index_pos: 0,
3716 };
3717 placeholder.set_skip_worktree(true);
3718 placeholder
3719 })
3720 }
3721}
3722
3723struct SparseCollapseView<'a> {
3726 entries: &'a [IndexEntry],
3727 collapsed: &'a CollapsedDirectories,
3728}
3729
3730impl SparseCollapseView<'_> {
3731 fn has_placeholder(&self, path_slash: &[u8], oid: &ObjectId, scratch: &mut Vec<u8>) -> bool {
3733 if let Some(collapsed_oid) = self.collapsed.get(path_slash) {
3734 return collapsed_oid == oid;
3735 }
3736 if self.collapsed.covers(path_slash, scratch) {
3737 return false;
3738 }
3739 let start = self
3740 .entries
3741 .partition_point(|e| e.path.as_slice() < path_slash);
3742 self.entries[start..]
3743 .iter()
3744 .take_while(|e| e.path == path_slash)
3745 .any(|e| e.stage() == 0 && e.is_sparse_directory_placeholder() && e.oid == *oid)
3746 }
3747}
3748
3749struct PrefixTreeResolver {
3752 root: ObjectId,
3753 resolved: HashMap<Vec<u8>, Option<ObjectId>>,
3754 parsed: HashMap<ObjectId, Option<Vec<TreeEntry>>>,
3755}
3756
3757impl PrefixTreeResolver {
3758 fn new(root: ObjectId) -> Self {
3759 Self {
3760 root,
3761 resolved: HashMap::new(),
3762 parsed: HashMap::new(),
3763 }
3764 }
3765
3766 fn resolve(&mut self, odb: &Odb, prefix: &[u8]) -> Result<Option<ObjectId>> {
3769 if prefix.is_empty() {
3770 return Ok(Some(self.root));
3771 }
3772 if let Some(known) = self.resolved.get(prefix) {
3773 return Ok(*known);
3774 }
3775 let (parent, name) = match prefix.iter().rposition(|&b| b == b'/') {
3776 Some(i) => (&prefix[..i], &prefix[i + 1..]),
3777 None => (&prefix[..0], prefix),
3778 };
3779 let found = match self.resolve(odb, parent)? {
3780 Some(parent_oid) if !name.is_empty() => {
3781 let entries = match self.parsed.entry(parent_oid) {
3782 std::collections::hash_map::Entry::Occupied(known) => known.into_mut(),
3783 std::collections::hash_map::Entry::Vacant(slot) => {
3784 let obj = odb.read(&parent_oid)?;
3785 slot.insert(if obj.kind == ObjectKind::Tree {
3786 Some(parse_tree(&obj.data)?)
3787 } else {
3788 None
3789 })
3790 }
3791 };
3792 entries
3793 .as_ref()
3794 .and_then(|entries| entries.iter().find(|e| e.name == name))
3795 .filter(|e| e.mode == MODE_TREE)
3796 .map(|e| e.oid)
3797 }
3798 Some(parent_oid) => Some(parent_oid),
3800 None => None,
3801 };
3802 self.resolved.insert(prefix.to_vec(), found);
3803 Ok(found)
3804 }
3805}
3806
3807fn try_collapse_directory_of_sparse_placeholders(
3810 odb: &Odb,
3811 directory: &[u8],
3812 rows: &[IndexEntry],
3813 head_subtree: &ObjectId,
3814) -> Result<bool> {
3815 if rows.is_empty() || rows.iter().any(|e| e.stage() != 0) {
3816 return Ok(false);
3817 }
3818 if !rows.iter().all(|e| e.is_sparse_directory_placeholder()) {
3819 return Ok(false);
3820 }
3821 let obj = odb.read(head_subtree)?;
3822 if obj.kind != crate::objects::ObjectKind::Tree {
3823 return Ok(false);
3824 }
3825 let tree_entries = crate::objects::parse_tree(&obj.data)?;
3826 if tree_entries.len() != rows.len() {
3827 return Ok(false);
3828 }
3829 for te in &tree_entries {
3830 if te.mode != MODE_TREE {
3831 return Ok(false);
3832 }
3833 let mut path = directory.to_vec();
3834 path.extend_from_slice(&te.name);
3835 path.push(b'/');
3836 let Some(row) = rows.iter().find(|e| e.path == path) else {
3837 return Ok(false);
3838 };
3839 if row.oid != te.oid || !row.skip_worktree() {
3840 return Ok(false);
3841 }
3842 }
3843 Ok(true)
3844}
3845
3846fn collect_sparse_aware_expected_blobs(
3849 odb: &Odb,
3850 tree_oid: &ObjectId,
3851 prefix: &[u8],
3852 patterns: &[String],
3853 cone_mode: bool,
3854 view: &SparseCollapseView<'_>,
3855 protect: crate::path_validate::ProtectOptions,
3856) -> Result<Vec<ExpectedBlob>> {
3857 let mut out = Vec::new();
3858 let mut scratch = Vec::new();
3859 walk_sparse_aware(
3860 odb,
3861 tree_oid,
3862 prefix,
3863 patterns,
3864 cone_mode,
3865 view,
3866 protect,
3867 &mut scratch,
3868 &mut out,
3869 )?;
3870 Ok(out)
3871}
3872
3873#[allow(clippy::too_many_arguments)]
3874fn walk_sparse_aware(
3875 odb: &Odb,
3876 tree_oid: &ObjectId,
3877 prefix: &[u8],
3878 patterns: &[String],
3879 cone_mode: bool,
3880 view: &SparseCollapseView<'_>,
3881 protect: crate::path_validate::ProtectOptions,
3882 scratch: &mut Vec<u8>,
3883 out: &mut Vec<ExpectedBlob>,
3884) -> Result<()> {
3885 let tree = odb.cached_tree_for_walk(tree_oid)?;
3887 let mut visit = |name: &[u8], mode: u32, oid: ObjectId| -> Result<()> {
3888 let path = if prefix.is_empty() {
3889 name.to_vec()
3890 } else {
3891 let mut p = Vec::with_capacity(prefix.len() + 1 + name.len());
3892 p.extend_from_slice(prefix);
3893 p.push(b'/');
3894 p.extend_from_slice(name);
3895 p
3896 };
3897 crate::path_population::validate_tree_walk_entry(&path, name, mode, protect)?;
3898 if mode == MODE_TREE {
3899 let mut path_slash = path.clone();
3900 path_slash.push(b'/');
3901 if view.has_placeholder(&path_slash, &oid, scratch) {
3902 return Ok(());
3903 }
3904 return walk_sparse_aware(
3905 odb, &oid, &path, patterns, cone_mode, view, protect, scratch, out,
3906 );
3907 }
3908 let path_str = String::from_utf8_lossy(&path);
3909 if crate::sparse_checkout::path_matches_sparse_patterns(&path_str, patterns, cone_mode) {
3910 return Ok(());
3911 }
3912 out.push(ExpectedBlob { path, mode, oid });
3913 Ok(())
3914 };
3915 match &tree {
3916 crate::odb::SessionTree::Parsed(entries) => {
3917 for te in entries.iter() {
3918 visit(&te.name, te.mode, te.oid)?;
3919 }
3920 }
3921 crate::odb::SessionTree::Raw(data) => {
3922 for te in crate::objects::TreeRefIter::with_oid_len(data, odb.hash_algo().len()) {
3923 let te = te?;
3924 visit(te.name, te.mode, te.oid)?;
3925 }
3926 }
3927 }
3928 Ok(())
3929}
3930
3931struct ExpectedBlob {
3933 path: Vec<u8>,
3934 mode: u32,
3935 oid: ObjectId,
3936}
3937
3938fn flatten_tree_blobs(
3939 odb: &Odb,
3940 tree_oid: &ObjectId,
3941 prefix: &[u8],
3942 protect: crate::path_validate::ProtectOptions,
3943) -> Result<Vec<IndexEntry>> {
3944 let obj = odb.read(tree_oid)?;
3945 if obj.kind != ObjectKind::Tree {
3946 return Err(Error::IndexError(format!("expected tree at {}", tree_oid)));
3947 }
3948 let entries = parse_tree(&obj.data)?;
3949 let mut out = Vec::new();
3950 for te in entries {
3951 let path = if prefix.is_empty() {
3952 te.name.clone()
3953 } else {
3954 let mut p = prefix.to_vec();
3955 p.push(b'/');
3956 p.extend_from_slice(&te.name);
3957 p
3958 };
3959 crate::path_population::validate_tree_walk_entry(prefix, &te.name, te.mode, protect)?;
3960 if is_tree_mode(te.mode) {
3961 let sub = flatten_tree_blobs(odb, &te.oid, &path, protect)?;
3962 out.extend(sub);
3963 } else {
3964 let path_len = path.len().min(0xFFF) as u16;
3965 let mut e = IndexEntry {
3966 ctime_sec: 0,
3967 ctime_nsec: 0,
3968 mtime_sec: 0,
3969 mtime_nsec: 0,
3970 dev: 0,
3971 ino: 0,
3972 mode: canon_mode(te.mode),
3973 uid: 0,
3974 gid: 0,
3975 size: 0,
3976 oid: te.oid,
3977 flags: path_len,
3978 flags_extended: Some(0),
3979 path,
3980 base_index_pos: 0,
3981 };
3982 e.set_skip_worktree(true);
3983 out.push(e);
3984 }
3985 }
3986 Ok(out)
3987}
3988
3989pub struct IndexLock {
3996 pub(crate) index_path: PathBuf,
3997 pub(crate) lock_path: PathBuf,
3998 pub(crate) pid_path: PathBuf,
3999 pub(crate) file: fs::File,
4000 pub(crate) wrote_pid: bool,
4001 pub(crate) committed: bool,
4002}
4003
4004impl IndexLock {
4005 pub(crate) fn write_index_contents(&mut self, body: &[u8], checksum: &[u8]) -> Result<()> {
4007 self.file.write_all(body).map_err(Error::Io)?;
4008 self.file.write_all(checksum).map_err(Error::Io)?;
4009 drop(std::mem::replace(
4010 &mut self.file,
4011 fs::OpenOptions::new()
4012 .write(true)
4013 .open(&self.lock_path)
4014 .map_err(Error::Io)?,
4015 ));
4016 Ok(())
4017 }
4018
4019 pub(crate) fn commit_replace_index(&mut self) -> Result<()> {
4021 crate::atomic_replace::detach_open_handle(&mut self.file).map_err(Error::Io)?;
4022 crate::atomic_replace::replace_file(&self.lock_path, &self.index_path)
4023 .map_err(Error::Io)?;
4024 self.committed = true;
4025 if self.wrote_pid {
4026 let _ = fs::remove_file(&self.pid_path);
4027 }
4028 Ok(())
4029 }
4030
4031 pub fn acquire(index_path: &Path, config: Option<&ConfigSet>) -> Result<Self> {
4039 let lock_path = index_path.with_extension("lock");
4040 let pid_path = pid_path_for_lock(&lock_path);
4041 let lockfile_pid_enabled = grit_lockfile_pid_enabled(config, index_path.parent());
4042
4043 use crate::lockfile::{LockFile, LockWait};
4044 let wait = LockWait::files_ref_lock(config);
4045 let mut lock_file = LockFile::acquire(&lock_path, wait).map_err(Error::from)?;
4046 if lockfile_pid_enabled {
4047 if let Err(e) = lock_file
4048 .write_holder_pid()
4049 .map_err(crate::error::Error::from)
4050 {
4051 lock_file.rollback();
4052 return Err(e);
4053 }
4054 }
4055 let wrote_pid = lockfile_pid_enabled;
4056 let (lock_path, file) = lock_file.into_open_file()?;
4057
4058 Ok(Self {
4059 index_path: index_path.to_path_buf(),
4060 lock_path,
4061 pid_path,
4062 file,
4063 wrote_pid,
4064 committed: false,
4065 })
4066 }
4067}
4068
4069impl Drop for IndexLock {
4070 fn drop(&mut self) {
4071 if self.committed {
4072 return;
4073 }
4074 let _ = fs::remove_file(&self.lock_path);
4075 if self.wrote_pid {
4076 let _ = fs::remove_file(&self.pid_path);
4077 }
4078 }
4079}
4080
4081fn path_is_under_dir(path: &[u8], dir: &[u8]) -> bool {
4083 path.len() > dir.len() && path[dir.len()] == b'/' && path.starts_with(dir)
4084}
4085
4086fn hash_index_body(algo: HashAlgo, body: &[u8]) -> Vec<u8> {
4088 algo.checksum_digest(body)
4089}
4090
4091fn write_index_body_and_trailer(
4093 file: &mut fs::File,
4094 algo: HashAlgo,
4095 skip_hash: bool,
4096 body: &[u8],
4097) -> io::Result<()> {
4098 use std::io::BufWriter;
4099 let mut out = BufWriter::with_capacity(256 * 1024, file);
4100 if skip_hash {
4101 out.write_all(body)?;
4102 out.write_all(&vec![0u8; algo.len()])?;
4103 return out.flush();
4104 }
4105 let mut hasher = algo.checksum_hasher();
4106 for chunk in body.chunks(256 * 1024) {
4107 hasher.update(chunk);
4108 out.write_all(chunk)?;
4109 }
4110 out.write_all(&hasher.finalize())?;
4111 out.flush()
4112}
4113
4114impl Index {
4115 fn estimate_body_capacity(&self, write_version: u32, config: Option<&ConfigSet>) -> usize {
4117 use crate::index_eoie_ieot::record_end_of_index_entries;
4118 let hash_len = self.hash_algo.len();
4119 let mut entries_bytes = 0usize;
4120 for entry in &self.entries {
4121 let mut n = 62usize
4122 .saturating_add(hash_len)
4123 .saturating_add(entry.path.len());
4124 if write_version >= 3
4125 && entry
4126 .flags_extended
4127 .map(IndexEntry::disk_flags_extended)
4128 .filter(|fe| *fe != 0)
4129 .is_some()
4130 {
4131 n += 2;
4132 }
4133 if write_version == 4 {
4134 n += 12;
4135 } else {
4136 n = (n + 7) & !7;
4137 }
4138 entries_bytes = entries_bytes.saturating_add(n);
4139 }
4140 let mut extra = 256usize;
4141 if self.cache_tree.is_some() {
4142 extra += 4096;
4143 }
4144 if self.untracked_cache.is_some() {
4145 extra += 4096;
4146 }
4147 if self.fsmonitor_last_update.is_some() {
4148 extra += 512;
4149 }
4150 if record_end_of_index_entries(config) {
4151 extra += 64;
4152 }
4153 12 + entries_bytes + extra
4154 }
4155}
4156
4157fn append_index_extension(
4158 out: &mut Vec<u8>,
4159 eoie_headers: &mut Option<Vec<u8>>,
4160 signature: u32,
4161 payload: &[u8],
4162) {
4163 let size = payload.len() as u32;
4164 out.extend_from_slice(&signature.to_be_bytes());
4165 out.extend_from_slice(&size.to_be_bytes());
4166 if let Some(headers) = eoie_headers {
4167 headers.extend_from_slice(&signature.to_be_bytes());
4168 headers.extend_from_slice(&size.to_be_bytes());
4169 }
4170 out.extend_from_slice(payload);
4171}
4172
4173fn detect_index_hash_algo(data: &[u8]) -> HashAlgo {
4178 let sha256_len = HashAlgo::Sha256.len();
4181 if data.len() > sha256_len + 12 {
4182 let (body, checksum) = data.split_at(data.len() - sha256_len);
4183 if checksum.iter().any(|&b| b != 0) && hash_index_body(HashAlgo::Sha256, body) == checksum {
4184 return HashAlgo::Sha256;
4185 }
4186 }
4187 HashAlgo::Sha1
4188}
4189
4190pub(crate) fn parse_entry(
4194 data: &[u8],
4195 version: u32,
4196 prev_path: &[u8],
4197 hash_len: usize,
4198) -> Result<(IndexEntry, usize)> {
4199 if data.len() < 40 + hash_len + 2 {
4201 return Err(Error::IndexError("entry too short".to_owned()));
4202 }
4203
4204 let mut pos = 0;
4205
4206 macro_rules! read_u32 {
4207 () => {{
4208 let v = u32::from_be_bytes(
4209 data[pos..pos + 4]
4210 .try_into()
4211 .map_err(|_| Error::IndexError("truncated u32".to_owned()))?,
4212 );
4213 pos += 4;
4214 v
4215 }};
4216 }
4217
4218 let ctime_sec = read_u32!();
4219 let ctime_nsec = read_u32!();
4220 let mtime_sec = read_u32!();
4221 let mtime_nsec = read_u32!();
4222 let dev = read_u32!();
4223 let ino = read_u32!();
4224 let mode = read_u32!();
4225 let uid = read_u32!();
4226 let gid = read_u32!();
4227 let size = read_u32!();
4228
4229 let oid = ObjectId::from_bytes(&data[pos..pos + hash_len])?;
4230 pos += hash_len;
4231
4232 let flags = u16::from_be_bytes(
4233 data[pos..pos + 2]
4234 .try_into()
4235 .map_err(|_| Error::IndexError("truncated flags".to_owned()))?,
4236 );
4237 pos += 2;
4238
4239 let flags_extended = if version >= 3 && flags & 0x4000 != 0 {
4240 let fe = u16::from_be_bytes(
4241 data[pos..pos + 2]
4242 .try_into()
4243 .map_err(|_| Error::IndexError("truncated extended flags".to_owned()))?,
4244 );
4245 pos += 2;
4246 Some(fe)
4247 } else {
4248 None
4249 };
4250
4251 let path;
4252 if version == 4 {
4253 let (strip_len, varint_bytes) = read_varint(&data[pos..]);
4255 pos += varint_bytes;
4256 let nul = data[pos..]
4257 .iter()
4258 .position(|&b| b == 0)
4259 .ok_or_else(|| Error::IndexError("v4 entry path missing NUL".to_owned()))?;
4260 let suffix = &data[pos..pos + nul];
4261 pos += nul + 1;
4262 let keep = prev_path.len().saturating_sub(strip_len);
4263 let mut full_path = Vec::with_capacity(keep + suffix.len());
4264 full_path.extend_from_slice(&prev_path[..keep]);
4265 full_path.extend_from_slice(suffix);
4266 path = full_path;
4267 } else {
4268 let nul = data[pos..]
4270 .iter()
4271 .position(|&b| b == 0)
4272 .ok_or_else(|| Error::IndexError("entry path missing NUL terminator".to_owned()))?;
4273 path = data[pos..pos + nul].to_vec();
4274 pos += nul + 1;
4275 let entry_start = 0usize;
4276 let entry_len = pos - entry_start;
4277 let padded = (entry_len + 7) & !7;
4278 let padding = padded.saturating_sub(entry_len);
4279 pos += padding;
4280 }
4281
4282 Ok((
4283 IndexEntry {
4284 ctime_sec,
4285 ctime_nsec,
4286 mtime_sec,
4287 mtime_nsec,
4288 dev,
4289 ino,
4290 mode,
4291 uid,
4292 gid,
4293 size,
4294 oid,
4295 flags,
4296 flags_extended,
4297 path,
4298 base_index_pos: 0,
4299 },
4300 pos,
4301 ))
4302}
4303
4304fn write_varint(out: &mut Vec<u8>, mut value: usize) {
4308 loop {
4309 let mut b = (value & 0x7F) as u8;
4310 value >>= 7;
4311 if value != 0 {
4312 b |= 0x80;
4313 }
4314 out.push(b);
4315 if value == 0 {
4316 break;
4317 }
4318 }
4319}
4320
4321fn read_varint(data: &[u8]) -> (usize, usize) {
4322 let mut value: usize = 0;
4323 let mut shift = 0usize;
4324 let mut pos = 0;
4325 loop {
4326 if pos >= data.len() {
4327 break;
4328 }
4329 let byte = data[pos] as usize;
4330 pos += 1;
4331 value |= (byte & 0x7F) << shift;
4332 if byte & 0x80 == 0 {
4333 break;
4334 }
4335 shift += 7;
4336 if shift > 28 {
4338 break;
4339 }
4340 }
4341 (value, pos)
4342}
4343
4344fn serialize_entry_v4(entry: &IndexEntry, previous_path: &mut Vec<u8>, out: &mut Vec<u8>) {
4345 let write_u32 = |out: &mut Vec<u8>, v: u32| out.extend_from_slice(&v.to_be_bytes());
4346
4347 write_u32(out, entry.ctime_sec);
4348 write_u32(out, entry.ctime_nsec);
4349 write_u32(out, entry.mtime_sec);
4350 write_u32(out, entry.mtime_nsec);
4351 write_u32(out, entry.dev);
4352 write_u32(out, entry.ino);
4353 write_u32(out, entry.mode);
4354 write_u32(out, entry.uid);
4355 write_u32(out, entry.gid);
4356 write_u32(out, entry.size);
4357 out.extend_from_slice(entry.oid.as_bytes());
4358
4359 let mut flags = entry.flags;
4360 let disk_ext = entry
4361 .flags_extended
4362 .map(IndexEntry::disk_flags_extended)
4363 .filter(|fe| *fe != 0);
4364 if disk_ext.is_some() {
4365 flags |= 0x4000;
4366 } else {
4367 flags &= !0x4000;
4368 }
4369 let path_len = entry.path.len().min(0xFFF) as u16;
4370 flags = (flags & 0xF000) | path_len;
4371 out.extend_from_slice(&flags.to_be_bytes());
4372
4373 if let Some(fe) = disk_ext {
4374 out.extend_from_slice(&fe.to_be_bytes());
4375 }
4376
4377 let common = previous_path
4378 .iter()
4379 .zip(entry.path.iter())
4380 .take_while(|(a, b)| a == b)
4381 .count();
4382 let to_remove = previous_path.len().saturating_sub(common);
4383 write_varint(out, to_remove);
4384 out.extend_from_slice(&entry.path[common..]);
4385 out.push(0);
4386
4387 previous_path.clear();
4388 previous_path.extend_from_slice(&entry.path);
4389}
4390
4391fn serialize_entry(entry: &IndexEntry, version: u32, out: &mut Vec<u8>) {
4392 let start = out.len();
4393
4394 let write_u32 = |out: &mut Vec<u8>, v: u32| out.extend_from_slice(&v.to_be_bytes());
4395
4396 write_u32(out, entry.ctime_sec);
4397 write_u32(out, entry.ctime_nsec);
4398 write_u32(out, entry.mtime_sec);
4399 write_u32(out, entry.mtime_nsec);
4400 write_u32(out, entry.dev);
4401 write_u32(out, entry.ino);
4402 write_u32(out, entry.mode);
4403 write_u32(out, entry.uid);
4404 write_u32(out, entry.gid);
4405 write_u32(out, entry.size);
4406 out.extend_from_slice(entry.oid.as_bytes());
4407
4408 let mut flags = entry.flags;
4410 let disk_ext = entry
4411 .flags_extended
4412 .map(IndexEntry::disk_flags_extended)
4413 .filter(|fe| *fe != 0);
4414 if version >= 3 && disk_ext.is_some() {
4415 flags |= 0x4000;
4416 } else {
4417 flags &= !0x4000;
4418 }
4419 let path_len = entry.path.len().min(0xFFF) as u16;
4421 flags = (flags & 0xF000) | path_len;
4422 out.extend_from_slice(&flags.to_be_bytes());
4423
4424 if version >= 3 {
4425 if let Some(fe) = disk_ext {
4426 out.extend_from_slice(&fe.to_be_bytes());
4427 }
4428 }
4429
4430 out.extend_from_slice(&entry.path);
4431 out.push(0);
4432
4433 let entry_len = out.len() - start;
4435 let padded = (entry_len + 7) & !7;
4436 let padding = padded - entry_len;
4437 for _ in 0..padding {
4438 out.push(0);
4439 }
4440}
4441
4442pub fn entry_from_stat(
4455 path: &Path,
4456 rel_path: &[u8],
4457 oid: ObjectId,
4458 mode: u32,
4459) -> Result<IndexEntry> {
4460 let meta = fs::symlink_metadata(path)?;
4461 Ok(entry_from_metadata(&meta, rel_path, oid, mode))
4462}
4463
4464#[must_use]
4468pub fn index_file_mtime(path: &Path) -> Option<(u32, u32)> {
4469 let meta = fs::metadata(path).ok()?;
4470 Some(index_mtime_from_metadata(&meta))
4471}
4472
4473fn index_mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
4474 let ft = filetime::FileTime::from_last_modification_time(meta);
4475 let secs = ft.unix_seconds();
4476 if secs.is_negative() {
4477 return (0, 0);
4478 }
4479 (secs as u32, ft.nanoseconds())
4480}
4481
4482const INDEX_MMAP_MIN_BYTES: u64 = 64 * 1024;
4484
4485enum IndexFileBytes {
4487 Owned(Vec<u8>),
4488 Mapped(memmap2::Mmap),
4489}
4490
4491impl std::ops::Deref for IndexFileBytes {
4492 type Target = [u8];
4493
4494 fn deref(&self) -> &[u8] {
4495 match self {
4496 Self::Owned(bytes) => bytes,
4497 Self::Mapped(map) => map,
4498 }
4499 }
4500}
4501
4502#[must_use]
4504pub(crate) fn index_file_trailer(data: &[u8], hash_algo: HashAlgo) -> Option<Vec<u8>> {
4505 let hash_len = hash_algo.len();
4506 (data.len() >= hash_len).then(|| data[data.len() - hash_len..].to_vec())
4507}
4508
4509fn record_loaded_index_identity(index: &mut Index, data: &[u8], mtime: (u32, u32)) {
4510 index.source_mtime = Some(mtime);
4511 index.source_file_len = Some(u64::try_from(data.len()).unwrap_or(u64::MAX));
4512 index.source_checksum = index_file_trailer(data, index.hash_algo);
4513}
4514
4515#[derive(Debug, Clone, PartialEq, Eq)]
4517pub(crate) struct IndexOnDiskIdentity {
4518 pub checksum: Option<Vec<u8>>,
4519 pub mtime: Option<(u32, u32)>,
4520 pub len: u64,
4521}
4522
4523pub(crate) fn read_index_on_disk_identity(
4525 path: &Path,
4526 hash_algo: HashAlgo,
4527) -> io::Result<IndexOnDiskIdentity> {
4528 match read_index_file(path) {
4529 Ok((data, mtime)) => {
4530 let bytes = data.deref();
4531 Ok(IndexOnDiskIdentity {
4532 checksum: index_file_trailer(bytes, hash_algo),
4533 mtime: Some(mtime),
4534 len: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
4535 })
4536 }
4537 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(IndexOnDiskIdentity {
4538 checksum: None,
4539 mtime: None,
4540 len: 0,
4541 }),
4542 Err(e) => Err(e),
4543 }
4544}
4545
4546#[must_use]
4550pub(crate) fn opportunistic_index_write_still_current(
4551 loaded: &Index,
4552 on_disk: &IndexOnDiskIdentity,
4553) -> bool {
4554 let had_disk_index = loaded.source_checksum.is_some()
4555 || loaded.source_mtime.is_some()
4556 || loaded.source_file_len.is_some();
4557 if !had_disk_index {
4558 return on_disk.len == 0;
4559 }
4560 if on_disk.len == 0 {
4561 return false;
4562 }
4563 if let (Some(loaded_cs), Some(disk_cs)) = (&loaded.source_checksum, &on_disk.checksum) {
4564 if loaded_cs != disk_cs {
4565 return false;
4566 }
4567 if loaded_cs.iter().all(|&b| b == 0) {
4568 return loaded.source_mtime == on_disk.mtime
4569 && loaded.source_file_len == Some(on_disk.len);
4570 }
4571 }
4572 true
4573}
4574
4575fn read_index_file(path: &Path) -> io::Result<(IndexFileBytes, (u32, u32))> {
4580 use std::io::Read;
4581
4582 let mut file = fs::File::open(path)?;
4583 let meta = file.metadata()?;
4584 let mtime = index_mtime_from_metadata(&meta);
4585 let len = meta.len();
4586 if meta.is_file() && len >= INDEX_MMAP_MIN_BYTES {
4587 if let Ok(len) = usize::try_from(len) {
4588 if let Ok(map) = unsafe { memmap2::MmapOptions::new().len(len).map(&file) } {
4591 return Ok((IndexFileBytes::Mapped(map), mtime));
4592 }
4593 }
4594 }
4595 let mut bytes = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
4596 file.read_to_end(&mut bytes)?;
4597 Ok((IndexFileBytes::Owned(bytes), mtime))
4598}
4599
4600fn max_stage0_entry_mtime(entries: &[IndexEntry]) -> (u32, u32) {
4601 let mut max_sec = 0u32;
4602 let mut max_nsec = 0u32;
4603 for ie in entries {
4604 if ie.stage() != 0 {
4605 continue;
4606 }
4607 if ie.mtime_sec > max_sec || (ie.mtime_sec == max_sec && ie.mtime_nsec > max_nsec) {
4608 max_sec = ie.mtime_sec;
4609 max_nsec = ie.mtime_nsec;
4610 }
4611 }
4612 (max_sec, max_nsec)
4613}
4614
4615#[must_use]
4617pub(crate) fn max_stage0_mtime_pair(a: (u32, u32), b: (u32, u32)) -> (u32, u32) {
4618 if a.0 > b.0 || (a.0 == b.0 && a.1 > b.1) {
4619 a
4620 } else {
4621 b
4622 }
4623}
4624
4625pub(crate) fn stage0_mtime_max(entries: &[IndexEntry]) -> (u32, u32) {
4627 max_stage0_entry_mtime(entries)
4628}
4629
4630pub(crate) fn advance_index_file_mtime_past_entries(
4650 index: &Index,
4651 index_path: &Path,
4652 touched_only_max: Option<(u32, u32)>,
4653) -> Result<()> {
4654 use filetime::FileTime;
4655 let (max_sec, max_nsec) = match touched_only_max {
4656 Some(pair) => pair,
4657 None => max_stage0_entry_mtime(&index.entries),
4658 };
4659 if max_sec == 0 && max_nsec == 0 {
4660 return Ok(());
4661 }
4662 let needs_bump = index_file_mtime(index_path)
4663 .is_none_or(|(sec, nsec)| sec < max_sec || (sec == max_sec && nsec <= max_nsec));
4664 if !needs_bump {
4665 return Ok(());
4666 }
4667 const TICK_NSEC: u32 = if cfg!(windows) { 100 } else { 1 };
4668 if max_nsec >= 1_000_000_000 - TICK_NSEC {
4669 return Ok(());
4670 }
4671 let ft = FileTime::from_unix_time(i64::from(max_sec), max_nsec + TICK_NSEC);
4672 filetime::set_file_mtime(index_path, ft).map_err(Error::Io)
4673}
4674
4675#[must_use]
4677pub(crate) fn index_path_outcome_key(path: &[u8]) -> Cow<'_, str> {
4678 match std::str::from_utf8(path) {
4679 Ok(s) => Cow::Borrowed(s),
4680 Err(_) => Cow::Owned(String::from_utf8_lossy(path).into_owned()),
4681 }
4682}
4683
4684#[must_use]
4688pub fn worktree_path_from_index_rel(work_tree: &Path, rel: &[u8]) -> PathBuf {
4689 #[cfg(unix)]
4690 {
4691 use std::ffi::OsStr;
4692 use std::os::unix::ffi::OsStrExt;
4693 work_tree.join(OsStr::from_bytes(rel))
4694 }
4695 #[cfg(not(unix))]
4696 {
4697 let rel = std::str::from_utf8(rel)
4698 .map(Cow::Borrowed)
4699 .unwrap_or_else(|_| Cow::Owned(String::from_utf8_lossy(rel).into_owned()));
4700 work_tree.join(rel.as_ref())
4701 }
4702}
4703
4704#[derive(Debug, Clone, Copy)]
4706pub struct IndexStatFields {
4707 ctime_sec: u32,
4708 ctime_nsec: u32,
4709 mtime_sec: u32,
4710 mtime_nsec: u32,
4711 dev: u32,
4712 ino: u32,
4713 uid: u32,
4714 gid: u32,
4715 size: u32,
4716}
4717
4718impl IndexStatFields {
4719 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
4721 pub(crate) fn from_linux_stat(stat: &libc::stat) -> Self {
4722 Self {
4723 ctime_sec: stat.st_ctime as u32,
4724 ctime_nsec: stat.st_ctime_nsec as u32,
4725 mtime_sec: stat.st_mtime as u32,
4726 mtime_nsec: stat.st_mtime_nsec as u32,
4727 dev: stat.st_dev as u32,
4728 ino: stat.st_ino as u32,
4729 uid: stat.st_uid,
4730 gid: stat.st_gid,
4731 size: munge_index_size(stat.st_size as u64),
4732 }
4733 }
4734
4735 #[must_use]
4737 pub(crate) fn index_size(&self) -> u32 {
4738 self.size
4739 }
4740
4741 #[must_use]
4743 pub fn from_metadata(meta: &fs::Metadata) -> Self {
4744 #[cfg(unix)]
4745 {
4746 use std::os::unix::fs::MetadataExt;
4747 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
4748 Self {
4749 ctime_sec,
4750 ctime_nsec,
4751 mtime_sec,
4752 mtime_nsec,
4753 dev: meta.dev() as u32,
4754 ino: meta.ino() as u32,
4755 uid: meta.uid(),
4756 gid: meta.gid(),
4757 size: munge_index_size(meta.size()),
4758 }
4759 }
4760 #[cfg(not(unix))]
4761 {
4762 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
4763 Self {
4764 ctime_sec,
4765 ctime_nsec,
4766 mtime_sec,
4767 mtime_nsec,
4768 dev: 0,
4769 ino: 0,
4770 uid: 0,
4771 gid: 0,
4772 size: munge_index_size(meta.len()),
4773 }
4774 }
4775 }
4776
4777 #[must_use]
4779 pub(crate) fn of_entry(ie: &IndexEntry) -> Self {
4780 Self {
4781 ctime_sec: ie.ctime_sec,
4782 ctime_nsec: ie.ctime_nsec,
4783 mtime_sec: ie.mtime_sec,
4784 mtime_nsec: ie.mtime_nsec,
4785 dev: ie.dev,
4786 ino: ie.ino,
4787 uid: ie.uid,
4788 gid: ie.gid,
4789 size: ie.size,
4790 }
4791 }
4792
4793 #[must_use]
4795 pub(crate) fn equivalent_to(&self, other: &Self, policy: StatMatchPolicy) -> bool {
4796 if self.size != other.size || self.mtime_sec != other.mtime_sec {
4797 return false;
4798 }
4799 if policy.use_nanosec && policy.check_stat && self.mtime_nsec != other.mtime_nsec {
4800 return false;
4801 }
4802 if policy.trust_ctime && policy.check_stat {
4803 if self.ctime_sec != other.ctime_sec {
4804 return false;
4805 }
4806 if policy.use_nanosec && self.ctime_nsec != other.ctime_nsec {
4807 return false;
4808 }
4809 }
4810 !policy.check_stat
4811 || (self.dev == other.dev
4812 && self.ino == other.ino
4813 && self.uid == other.uid
4814 && self.gid == other.gid)
4815 }
4816
4817 pub(crate) fn store_in(&self, ie: &mut IndexEntry) {
4819 ie.ctime_sec = self.ctime_sec;
4820 ie.ctime_nsec = self.ctime_nsec;
4821 ie.mtime_sec = self.mtime_sec;
4822 ie.mtime_nsec = self.mtime_nsec;
4823 ie.dev = self.dev;
4824 ie.ino = self.ino;
4825 ie.uid = self.uid;
4826 ie.gid = self.gid;
4827 ie.size = self.size;
4828 }
4829
4830 #[must_use]
4832 pub fn index_entry(self, rel_path: &[u8], oid: ObjectId, mode: u32) -> IndexEntry {
4833 IndexEntry {
4834 ctime_sec: self.ctime_sec,
4835 ctime_nsec: self.ctime_nsec,
4836 mtime_sec: self.mtime_sec,
4837 mtime_nsec: self.mtime_nsec,
4838 dev: self.dev,
4839 ino: self.ino,
4840 mode,
4841 uid: self.uid,
4842 gid: self.gid,
4843 size: self.size,
4844 oid,
4845 flags: rel_path.len().min(0xFFF) as u16,
4846 flags_extended: None,
4847 path: rel_path.to_vec(),
4848 base_index_pos: 0,
4849 }
4850 }
4851}
4852
4853impl From<&fs::Metadata> for IndexStatFields {
4854 fn from(meta: &fs::Metadata) -> Self {
4855 Self::from_metadata(meta)
4856 }
4857}
4858
4859impl IndexStatFields {
4860 #[must_use]
4863 #[cfg_attr(not(windows), allow(dead_code))]
4864 pub(crate) fn size_and_times_match_index(
4865 &self,
4866 ie: &IndexEntry,
4867 policy: StatMatchPolicy,
4868 ) -> bool {
4869 if self.size != ie.size || self.mtime_sec != ie.mtime_sec {
4870 return false;
4871 }
4872 if !(policy.use_nanosec && policy.check_stat) {
4873 return true;
4874 }
4875 self.mtime_nsec == ie.mtime_nsec
4876 && (!policy.trust_ctime || self.ctime_nsec == ie.ctime_nsec)
4877 }
4878
4879 #[must_use]
4881 pub(crate) fn matches_index_entry(&self, ie: &IndexEntry, policy: StatMatchPolicy) -> bool {
4882 if self.size != ie.size {
4883 return false;
4884 }
4885 if self.mtime_sec != ie.mtime_sec {
4886 return false;
4887 }
4888 if policy.use_nanosec && policy.check_stat && self.mtime_nsec != ie.mtime_nsec {
4889 return false;
4890 }
4891 if policy.trust_ctime && policy.check_stat {
4892 if self.ctime_sec != ie.ctime_sec {
4893 return false;
4894 }
4895 if policy.use_nanosec && self.ctime_nsec != ie.ctime_nsec {
4896 return false;
4897 }
4898 }
4899 if policy.check_stat {
4900 #[cfg(windows)]
4904 {
4905 if self.ino != ie.ino && self.ino != 0 {
4906 return false;
4907 }
4908 if self.dev != ie.dev && self.dev != 0 {
4909 return false;
4910 }
4911 if self.uid != ie.uid && self.uid != 0 {
4912 return false;
4913 }
4914 if self.gid != ie.gid && self.gid != 0 {
4915 return false;
4916 }
4917 }
4918 #[cfg(not(windows))]
4919 if self.ino != ie.ino {
4920 return false;
4921 }
4922 }
4923 true
4924 }
4925}
4926
4927#[must_use]
4932pub fn entry_from_metadata(
4933 meta: &fs::Metadata,
4934 rel_path: &[u8],
4935 oid: ObjectId,
4936 mode: u32,
4937) -> IndexEntry {
4938 #[cfg(unix)]
4939 {
4940 use std::os::unix::fs::MetadataExt;
4941 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
4942 IndexEntry {
4943 ctime_sec,
4944 ctime_nsec,
4945 mtime_sec,
4946 mtime_nsec,
4947 dev: meta.dev() as u32,
4948 ino: meta.ino() as u32,
4949 mode,
4950 uid: meta.uid(),
4951 gid: meta.gid(),
4952 size: munge_index_size(meta.size()),
4953 oid,
4954 flags: rel_path.len().min(0xFFF) as u16,
4955 flags_extended: None,
4956 path: rel_path.to_vec(),
4957 base_index_pos: 0,
4958 }
4959 }
4960 #[cfg(not(unix))]
4961 {
4962 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
4963 IndexEntry {
4964 ctime_sec,
4965 ctime_nsec,
4966 mtime_sec,
4967 mtime_nsec,
4968 dev: 0,
4969 ino: 0,
4970 mode,
4971 uid: 0,
4972 gid: 0,
4973 size: munge_index_size(meta.len()),
4974 oid,
4975 flags: rel_path.len().min(0xFFF) as u16,
4976 flags_extended: None,
4977 path: rel_path.to_vec(),
4978 base_index_pos: 0,
4979 }
4980 }
4981}
4982
4983#[must_use]
4993pub fn normalize_mode(raw_mode: u32) -> u32 {
4994 const S_IFMT: u32 = 0o170000;
4995 const S_IFLNK: u32 = 0o120000;
4996 const S_IFREG: u32 = 0o100000;
4997
4998 let fmt = raw_mode & S_IFMT;
4999 if fmt == S_IFLNK {
5000 return MODE_SYMLINK;
5001 }
5002 if fmt == S_IFREG {
5003 if raw_mode & 0o111 != 0 {
5005 return MODE_EXECUTABLE;
5006 }
5007 return MODE_REGULAR;
5008 }
5009 MODE_REGULAR
5011}
5012
5013#[cfg(test)]
5014pub(crate) mod index_path_cmp {
5015 use std::cell::Cell;
5016
5017 thread_local! { static PATH_CMP_COUNT: Cell<usize> = const { Cell::new(0) };
5019 }
5020
5021 pub fn reset() {
5022 PATH_CMP_COUNT.with(|c| c.set(0));
5023 }
5024
5025 pub fn take() -> usize {
5026 PATH_CMP_COUNT.with(|c| {
5027 let n = c.get();
5028 c.set(0);
5029 n
5030 })
5031 }
5032
5033 pub fn bump() {
5034 PATH_CMP_COUNT.with(|c| c.set(c.get() + 1));
5035 }
5036}
5037
5038fn record_path_cmp_for_tests() {
5039 #[cfg(test)]
5040 index_path_cmp::bump();
5041}
5042
5043#[cfg(test)]
5044mod tests {
5045 #![allow(clippy::expect_used, clippy::unwrap_used)]
5046
5047 use super::*;
5048 use tempfile::TempDir;
5049
5050 pub(super) mod fsmonitor_rebuild_counter {
5053 use std::cell::Cell;
5054
5055 thread_local! { static COUNT: Cell<usize> = const { Cell::new(0) };
5057 }
5058
5059 pub fn reset() {
5060 COUNT.with(|c| c.set(0));
5061 }
5062
5063 pub fn count() -> usize {
5064 COUNT.with(Cell::get)
5065 }
5066
5067 pub fn note_build() {
5068 COUNT.with(|c| c.set(c.get() + 1));
5069 }
5070 }
5071
5072 fn dummy_oid() -> ObjectId {
5073 ObjectId::from_bytes(&[0u8; 20]).unwrap()
5074 }
5075
5076 fn make_entry(path: &str) -> IndexEntry {
5077 IndexEntry {
5078 ctime_sec: 0,
5079 ctime_nsec: 0,
5080 mtime_sec: 0,
5081 mtime_nsec: 0,
5082 dev: 0,
5083 ino: 0,
5084 mode: MODE_REGULAR,
5085 uid: 0,
5086 gid: 0,
5087 size: 0,
5088 oid: dummy_oid(),
5089 flags: path.len().min(0xFFF) as u16,
5090 flags_extended: None,
5091 path: path.as_bytes().to_vec(),
5092 base_index_pos: 0,
5093 }
5094 }
5095
5096 #[test]
5097 fn content_verified_bit_is_in_memory_only() {
5098 let mut index = Index::new();
5099 let mut plain = make_entry("a.txt");
5100 plain.mark_content_verified();
5101 let mut skipped = make_entry("b.txt");
5102 skipped.set_skip_worktree(true);
5103 skipped.mark_content_verified();
5104 index.add_or_replace(plain);
5105 index.add_or_replace(skipped);
5106
5107 let mut only_marked = index.clone();
5108 only_marked.remove(b"b.txt");
5109 let mut out = Vec::new();
5110 only_marked.serialize_into(&mut out).unwrap();
5111 assert_eq!(&out[4..8], &2u32.to_be_bytes(), "the bit alone keeps v2");
5112
5113 out.clear();
5114 index.serialize_into(&mut out).unwrap();
5115 let checksum = hash_index_body(index.hash_algo, &out);
5116 out.extend_from_slice(&checksum);
5117 let reread = Index::parse(&out).unwrap();
5118 assert_eq!(reread.version, 3);
5119 assert!(reread
5120 .entries
5121 .iter()
5122 .all(|e| !e.clone().take_content_verified()));
5123 assert!(reread.get(b"b.txt", 0).unwrap().skip_worktree());
5124
5125 assert!(index.get_mut(b"a.txt", 0).is_some());
5126 assert_eq!(
5127 index.entries[0].flags_extended, None,
5128 "get_mut voids the mark"
5129 );
5130 index.clear_content_verified();
5131 assert!(index.entries[1].skip_worktree());
5132 assert!(!index.entries[1].clone().take_content_verified());
5133 }
5134
5135 #[test]
5136 fn round_trip_empty_index() {
5137 let dir = TempDir::new().unwrap();
5138 let path = dir.path().join("index");
5139
5140 let mut idx = Index::new();
5141 idx.write(&path).unwrap();
5142
5143 let loaded = Index::load(&path).unwrap();
5144 assert_eq!(loaded.entries.len(), 0);
5145 }
5146
5147 #[test]
5148 fn opportunistic_write_still_current_logic() {
5149 use super::{opportunistic_index_write_still_current, IndexOnDiskIdentity};
5150
5151 let mut loaded = Index::new();
5152 loaded.source_checksum = Some(vec![1; 20]);
5153 loaded.source_mtime = Some((1, 0));
5154 loaded.source_file_len = Some(100);
5155 let same = IndexOnDiskIdentity {
5156 checksum: Some(vec![1; 20]),
5157 mtime: Some((1, 0)),
5158 len: 100,
5159 };
5160 assert!(opportunistic_index_write_still_current(&loaded, &same));
5161
5162 let changed = IndexOnDiskIdentity {
5163 checksum: Some(vec![2; 20]),
5164 mtime: Some((1, 0)),
5165 len: 100,
5166 };
5167 assert!(!opportunistic_index_write_still_current(&loaded, &changed));
5168
5169 loaded.source_checksum = Some(vec![0; 20]);
5170 assert!(!opportunistic_index_write_still_current(
5171 &loaded,
5172 &IndexOnDiskIdentity {
5173 checksum: Some(vec![0; 20]),
5174 mtime: Some((2, 0)),
5175 len: 100,
5176 }
5177 ));
5178
5179 let empty_loaded = Index::new();
5180 assert!(opportunistic_index_write_still_current(
5181 &empty_loaded,
5182 &IndexOnDiskIdentity {
5183 checksum: None,
5184 mtime: None,
5185 len: 0,
5186 }
5187 ));
5188 assert!(!opportunistic_index_write_still_current(
5189 &empty_loaded,
5190 &IndexOnDiskIdentity {
5191 checksum: Some(vec![1; 20]),
5192 mtime: Some((1, 0)),
5193 len: 50,
5194 }
5195 ));
5196 }
5197
5198 #[test]
5199 fn read_index_file_maps_only_large_files() {
5200 let dir = TempDir::new().unwrap();
5201 let threshold = usize::try_from(INDEX_MMAP_MIN_BYTES).unwrap();
5202 for (len, mapped) in [(0, false), (threshold - 1, false), (threshold, true)] {
5203 let path = dir.path().join(format!("index-{len}"));
5204 let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
5205 fs::write(&path, &contents).unwrap();
5206 let (bytes, mtime) = read_index_file(&path).unwrap();
5207 assert_eq!(matches!(bytes, IndexFileBytes::Mapped(_)), mapped, "{len}");
5208 assert_eq!(&*bytes, contents.as_slice());
5209 assert_eq!(Some(mtime), index_file_mtime(&path));
5210 }
5211 let missing = read_index_file(&dir.path().join("missing")).err().unwrap();
5212 assert_eq!(missing.kind(), io::ErrorKind::NotFound);
5213 }
5214
5215 #[test]
5216 fn round_trip_with_entries() {
5217 let dir = TempDir::new().unwrap();
5218 let path = dir.path().join("index");
5219
5220 let mut idx = Index::new();
5221 idx.add_or_replace(make_entry("foo.txt"));
5222 idx.add_or_replace(make_entry("bar/baz.txt"));
5223 idx.write(&path).unwrap(); let loaded = Index::load(&path).unwrap();
5226 assert_eq!(loaded.entries.len(), 2);
5227 assert_eq!(loaded.entries[0].path, b"bar/baz.txt");
5228 assert_eq!(loaded.entries[1].path, b"foo.txt");
5229 }
5230
5231 fn overwrite_fixture() -> Index {
5232 let mut idx = Index::new();
5233 for i in 0..200 {
5234 let mut e = make_entry(&format!("d{}/f{i:03}.txt", i % 4));
5235 e.base_index_pos = i + 1;
5236 idx.add_or_replace(e);
5237 }
5238 for stage in 1..=3u16 {
5239 let mut e = make_entry("d0/conflict.txt");
5240 e.flags |= stage << 12;
5241 idx.add_or_replace(e);
5242 }
5243 let leaf =
5244 |name: &str| CacheTreeNode::valid(name.as_bytes().to_vec(), 50, dummy_oid(), vec![]);
5245 idx.cache_tree_root = Some(dummy_oid());
5246 idx.cache_tree = Some(CacheTreeNode::valid(
5247 Vec::new(),
5248 203,
5249 dummy_oid(),
5250 vec![leaf("d0"), leaf("d1"), leaf("d2"), leaf("d3")],
5251 ));
5252 assert!(idx.entries_sorted);
5253 idx
5254 }
5255
5256 fn overwrite_replacements(count: usize) -> Vec<IndexEntry> {
5257 let changed = ObjectId::from_bytes(&[7u8; 20]).unwrap();
5258 let mut out: Vec<IndexEntry> = (0..count)
5259 .map(|i| {
5260 let mut e = make_entry(&format!("d{}/f{:03}.txt", (i * 3) % 4, i * 3));
5261 e.oid = changed;
5262 e.size = 9;
5263 e
5264 })
5265 .collect();
5266 out.sort_by(|a, b| a.path.cmp(&b.path));
5267 out
5268 }
5269
5270 #[test]
5271 fn overwrite_stage0_entries_matches_sorted_merge() {
5272 for count in [3, 64] {
5273 let mut merged = overwrite_fixture();
5274 merged.replace_stage0_entries_batch(overwrite_replacements(count));
5275 let mut overwritten = overwrite_fixture();
5276 overwritten.overwrite_stage0_entries(overwrite_replacements(count));
5277 assert_eq!(overwritten.entries, merged.entries, "count {count}");
5278 assert_eq!(overwritten.cache_tree, merged.cache_tree, "count {count}");
5279 assert_eq!(overwritten.cache_tree_root, merged.cache_tree_root);
5280 assert_eq!(overwritten.entries.len(), 203);
5281 }
5282 }
5283
5284 #[test]
5285 fn overwrite_stage0_entries_keeps_split_positions_and_invalidates_touched_dirs() {
5286 let mut idx = overwrite_fixture();
5287 let target = b"d1/f001.txt".to_vec();
5288 let pos = idx.find_entry_pos(&target, 0).unwrap();
5289 let before = idx.entries[pos].base_index_pos;
5290 let mut e = make_entry("d1/f001.txt");
5291 e.size = 42;
5292 idx.overwrite_stage0_entries(vec![e]);
5293 assert_eq!(idx.entries[pos].size, 42);
5294 assert_eq!(idx.entries[pos].base_index_pos, before);
5295 let tree = idx.cache_tree.as_ref().unwrap();
5296 assert!(!tree.is_valid());
5297 let child = |name: &[u8]| tree.children.iter().find(|c| c.name == name).unwrap();
5298 assert!(!child(b"d1").is_valid());
5299 assert!(child(b"d0").is_valid());
5300 }
5301
5302 #[test]
5303 fn overwrite_stage0_entries_merges_paths_without_a_stage0_row() {
5304 let mut idx = overwrite_fixture();
5305 let mut new_path = make_entry("d2/new.txt");
5306 new_path.size = 1;
5307 idx.overwrite_stage0_entries(vec![make_entry("d0/f000.txt"), new_path]);
5308 assert_eq!(idx.entries.len(), 204);
5309 assert!(idx.get(b"d2/new.txt", 0).is_some());
5310 assert_eq!(
5311 idx.entries
5312 .iter()
5313 .filter(|e| e.path == b"d0/conflict.txt")
5314 .count(),
5315 3
5316 );
5317 assert!(idx.entries.windows(2).all(|w| {
5318 Index::cmp_paths(&w[0].path, &w[1].path)
5319 .then_with(|| w[0].stage().cmp(&w[1].stage()))
5320 .is_lt()
5321 }));
5322 }
5323
5324 #[test]
5325 fn remove_descendants_under_path_drops_nested_only() {
5326 let mut idx = Index::new();
5327 idx.add_or_replace(make_entry("d/e"));
5328 idx.add_or_replace(make_entry("d-other"));
5329 idx.add_or_replace(make_entry("prefix/d"));
5330 idx.remove_descendants_under_path("d");
5331 let paths: Vec<_> = idx.entries.iter().map(|e| e.path.as_slice()).collect();
5332 assert_eq!(paths, vec![b"d-other".as_slice(), b"prefix/d".as_slice()]);
5333 }
5334
5335 #[test]
5336 fn fsmonitor_extension_round_trip_matches_git_layout() {
5337 let fsmn_payload: [u8; 41] = [
5339 0, 0, 0, 2, b'b', b'u', b'i', b'l', b't', b'i', b'n', b':', b'f', b'a', b'k', b'e', 0,
5340 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5341 ];
5342 let (token, dirty) = parse_fsmonitor_extension(&fsmn_payload).unwrap();
5343 assert_eq!(token, "builtin:fake");
5344 assert_eq!(dirty.bit_size, 0);
5345
5346 let dir = TempDir::new().unwrap();
5347 let path = dir.path().join("index");
5348 let mut idx = Index::new();
5349 idx.add_or_replace(make_entry("a"));
5350 if let Some(e) = idx.get_mut(b"a", 0) {
5351 e.set_fsmonitor_valid(true);
5352 }
5353 idx.fsmonitor_last_update = Some(token);
5354 idx.fsmonitor_dirty = Some(dirty);
5355 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5356 idx.write(&path).unwrap(); let loaded = Index::load(&path).unwrap();
5359 assert_eq!(
5360 loaded.fsmonitor_last_update.as_deref(),
5361 Some("builtin:fake")
5362 );
5363 assert_eq!(loaded.fsmonitor_dirty.as_ref().map(|b| b.bit_size), Some(0));
5364 assert_eq!(
5365 loaded.fsmonitor_raw_extension.as_deref(),
5366 Some(fsmn_payload.as_slice())
5367 );
5368
5369 let mut body = Vec::new();
5370 loaded.serialize_into(&mut body).unwrap();
5371 assert!(
5372 body.windows(4).any(|w| w == b"FSMN"),
5373 "serialized index must include FSMN"
5374 );
5375 let off = body.windows(4).position(|w| w == b"FSMN").unwrap();
5376 let ext_sz = u32::from_be_bytes(body[off + 4..off + 8].try_into().unwrap()) as usize;
5377 let written = &body[off + 8..off + 8 + ext_sz];
5378 assert_eq!(written, fsmn_payload);
5379 }
5380
5381 #[test]
5382 fn fsmonitor_extension_written_from_token_only_state() {
5383 let dir = TempDir::new().unwrap();
5384 let path = dir.path().join("index");
5385 let mut idx = Index::new();
5386 idx.add_or_replace(make_entry("a"));
5387 idx.set_fsmonitor_last_update(Some("builtin:fake".to_owned()));
5388 idx.write(&path).unwrap(); let data = fs::read(&path).unwrap();
5391 assert!(
5392 data.windows(4).any(|w| w == b"FSMN"),
5393 "token-only enable must emit a full FSMN extension"
5394 );
5395 let loaded = Index::load(&path).unwrap();
5396 assert_eq!(
5397 loaded.fsmonitor_last_update.as_deref(),
5398 Some("builtin:fake")
5399 );
5400 }
5401
5402 #[test]
5403 fn fsmonitor_raw_not_reused_after_token_change() {
5404 let fsmn_payload: [u8; 41] = [
5405 0, 0, 0, 2, b'b', b'u', b'i', b'l', b't', b'i', b'n', b':', b'f', b'a', b'k', b'e', 0,
5406 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5407 ];
5408 let mut idx = Index::new();
5409 idx.add_or_replace(make_entry("a"));
5410 idx.fsmonitor_last_update = Some("builtin:fake".to_owned());
5411 idx.fsmonitor_dirty = Some(parse_fsmonitor_extension(&fsmn_payload).unwrap().1);
5412 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5413
5414 idx.set_fsmonitor_last_update(Some("last_update_token".to_owned()));
5415 let mut body = Vec::new();
5416 idx.serialize_into(&mut body).unwrap();
5417 let off = body.windows(4).position(|w| w == b"FSMN").unwrap();
5418 let ext_sz = u32::from_be_bytes(body[off + 4..off + 8].try_into().unwrap()) as usize;
5419 let written = &body[off + 8..off + 8 + ext_sz];
5420 let (token, _) = parse_fsmonitor_extension(written).unwrap();
5421 assert_eq!(token, "last_update_token");
5422 assert_ne!(written, fsmn_payload);
5423 }
5424
5425 #[test]
5426 fn fsmonitor_raw_not_reused_after_validity_change() {
5427 let fsmn_payload: [u8; 41] = [
5428 0, 0, 0, 2, b'b', b'u', b'i', b'l', b't', b'i', b'n', b':', b'f', b'a', b'k', b'e', 0,
5429 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5430 ];
5431 let mut idx = Index::new();
5432 idx.add_or_replace(make_entry("a"));
5433 idx.fsmonitor_last_update = Some("builtin:fake".to_owned());
5434 idx.fsmonitor_dirty = Some(parse_fsmonitor_extension(&fsmn_payload).unwrap().1);
5435 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5436 if let Some(e) = idx.get_mut(b"a", 0) {
5437 e.set_fsmonitor_valid(false);
5438 }
5439 idx.fsmonitor_validity_changed();
5440
5441 let mut body = Vec::new();
5442 idx.serialize_into(&mut body).unwrap();
5443 let off = body.windows(4).position(|w| w == b"FSMN").unwrap();
5444 let ext_sz = u32::from_be_bytes(body[off + 4..off + 8].try_into().unwrap()) as usize;
5445 let written = &body[off + 8..off + 8 + ext_sz];
5446 assert_ne!(written, fsmn_payload);
5447 let (_, dirty) = parse_fsmonitor_extension(written).unwrap();
5448 assert!(dirty.bit_size >= 1);
5449 }
5450
5451 fn make_staged_entry(path: &str, stage: u8) -> IndexEntry {
5452 let mut e = make_entry(path);
5453 e.set_stage(stage);
5454 e
5455 }
5456
5457 struct LinearIndex {
5459 entries: Vec<IndexEntry>,
5460 resolve_undo: Option<BTreeMap<Vec<u8>, ResolveUndoRecord>>,
5461 }
5462
5463 impl LinearIndex {
5464 fn from_index(idx: &Index) -> Self {
5465 Self {
5466 entries: idx.entries.clone(),
5467 resolve_undo: idx.resolve_undo.clone(),
5468 }
5469 }
5470
5471 fn assert_matches(&self, idx: &Index) {
5472 let mut actual = idx.entries.clone();
5473 actual.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
5474 let mut expect = self.entries.clone();
5475 expect.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
5476 assert_eq!(actual, expect);
5477 assert_eq!(idx.resolve_undo, self.resolve_undo);
5478 }
5479
5480 fn add_or_replace(&mut self, entry: IndexEntry) {
5481 if let Some(pos) = self
5482 .entries
5483 .iter()
5484 .position(|e| e.path == entry.path && e.stage() == entry.stage())
5485 {
5486 self.entries[pos] = entry;
5487 } else {
5488 self.entries.push(entry);
5489 }
5490 self.entries
5491 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
5492 }
5493
5494 fn remove(&mut self, path: &[u8]) -> bool {
5495 let mut removed_any = false;
5496 for e in &self.entries {
5497 if e.path == path {
5498 if e.stage() != 0 {
5499 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
5500 }
5501 removed_any = true;
5502 }
5503 }
5504 if !removed_any {
5505 return false;
5506 }
5507 self.entries.retain(|e| e.path != path);
5508 removed_any
5509 }
5510
5511 fn stage_file(&mut self, entry: IndexEntry) {
5512 let path = entry.path.clone();
5513 for e in &self.entries {
5514 if e.path == path && e.stage() != 0 {
5515 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
5516 }
5517 }
5518 self.entries.retain(|e| e.path != path || e.stage() == 0);
5519 self.add_or_replace(entry);
5520 }
5521
5522 fn remove_descendants_under_path(&mut self, path: &str) {
5523 let prefix = path.as_bytes();
5524 if prefix.is_empty() {
5525 return;
5526 }
5527 let plen = prefix.len();
5528 for e in self.entries.iter() {
5529 let ep = e.path.as_slice();
5530 if ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/' && e.stage() != 0 {
5531 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
5532 }
5533 }
5534 self.entries.retain(|e| {
5535 let ep = e.path.as_slice();
5536 if ep.len() <= plen {
5537 return true;
5538 }
5539 if !ep.starts_with(prefix) {
5540 return true;
5541 }
5542 ep[plen] != b'/'
5543 });
5544 }
5545 }
5546
5547 #[test]
5548 fn sorted_ops_match_linear_oracle() {
5549 let paths: Vec<Vec<u8>> = [
5550 "a", "a-b", "a/b", "a/b/c", "aa", "b", "dir/x", "dir/x/y", "dir-xy",
5551 ]
5552 .into_iter()
5553 .map(|p| p.as_bytes().to_vec())
5554 .collect();
5555
5556 let mut idx = Index::new();
5557 let mut oracle = LinearIndex::from_index(&idx);
5558 let mut rng_state: u64 = 376;
5559
5560 fn next_u64(state: &mut u64) -> u64 {
5561 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
5562 *state
5563 }
5564
5565 for step in 0..400 {
5566 let op = next_u64(&mut rng_state) % 4;
5567 match op {
5568 0 => {
5569 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5570 let stage = (next_u64(&mut rng_state) % 4) as u8;
5571 let mut e = make_staged_entry(std::str::from_utf8(p).unwrap(), stage);
5572 e.oid = ObjectId::from_bytes(&[(step % 256) as u8; 20]).unwrap();
5573 idx.add_or_replace(e.clone());
5574 oracle.add_or_replace(e);
5575 }
5576 1 => {
5577 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5578 idx.remove(p);
5579 oracle.remove(p);
5580 }
5581 2 => {
5582 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5583 let e = make_entry(std::str::from_utf8(p).unwrap());
5584 idx.stage_file(e.clone());
5585 oracle.stage_file(e);
5586 }
5587 _ => {
5588 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5589 let ps = std::str::from_utf8(p).unwrap();
5590 idx.remove_descendants_under_path(ps);
5591 oracle.remove_descendants_under_path(ps);
5592 }
5593 }
5594 oracle.assert_matches(&idx);
5595 }
5596 }
5597
5598 #[test]
5599 fn stale_sorted_flag_miss_until_resorted() {
5600 let mut idx = Index::new();
5601 idx.entries.push(make_entry("z"));
5602 idx.entries.push(make_entry("a"));
5603 idx.entries.push(make_entry("b"));
5604 idx.entries_sorted = true;
5605 assert!(!idx.entries_are_sorted());
5606 assert!(idx.get(b"z", 0).is_none());
5607 idx.sort();
5608 assert!(idx.get(b"z", 0).is_some());
5609 assert!(idx.remove(b"z"));
5610 assert!(idx.get(b"z", 0).is_none());
5611 }
5612
5613 #[test]
5614 fn write_restores_canonical_order_when_sorted_flag_is_stale() {
5615 let mut idx = Index::new();
5616 idx.entries.push(make_entry("zzz.txt"));
5617 idx.entries.push(make_entry("aaa.txt"));
5618 idx.entries_sorted = true;
5619 assert!(!idx.entries_are_sorted());
5620 let dir = tempfile::tempdir().expect("tempdir");
5621 let path = dir.path().join("index");
5622 idx.write(&path).expect("write index");
5623 let parsed = Index::load(&path).expect("load index");
5624 assert!(parsed.entries_are_sorted());
5625 assert_eq!(parsed.entries[0].path, b"aaa.txt".as_slice());
5626 }
5627
5628 #[test]
5629 fn find_entry_pos_sorted_miss_is_constant() {
5630 const N: usize = 100_000;
5631 const M: usize = 1_000;
5632 let mut idx = Index::new();
5633 for i in 0..N {
5634 idx.add_or_replace(make_entry(&format!("file/{i:06}.txt")));
5635 }
5636 assert!(idx.entries_sorted);
5637 let misses: Vec<Vec<u8>> = (0..M)
5638 .map(|i| format!("missing/{i:06}.txt").into_bytes())
5639 .collect();
5640 index_path_cmp::reset();
5641 for path in &misses {
5642 assert!(idx.get(path, 0).is_none());
5643 }
5644 let cmps = index_path_cmp::take();
5645 let log_n = (N as f64).log2().ceil() as usize;
5646 let bound = 100 * M * log_n;
5647 assert!(
5648 cmps < bound,
5649 "sorted miss path comparisons {cmps} should stay under {bound} (N={N}, M={M})"
5650 );
5651 }
5652
5653 fn sorted_index(entries: Vec<IndexEntry>) -> Index {
5654 let mut idx = Index::new();
5655 idx.entries = entries;
5656 idx.sort();
5657 idx
5658 }
5659
5660 fn paths_of(entries: &[IndexEntry]) -> Vec<&str> {
5661 entries
5662 .iter()
5663 .map(|e| std::str::from_utf8(&e.path).unwrap())
5664 .collect()
5665 }
5666
5667 #[test]
5668 fn sorted_entries_under_returns_the_directory_run_at_every_stage() {
5669 let idx = sorted_index(vec![
5670 make_entry("a-b"),
5671 make_entry("a.txt"),
5672 make_entry("a/x"),
5673 make_staged_entry("a/y", 2),
5674 make_staged_entry("a/y", 3),
5675 make_entry("a/z/deep"),
5676 make_entry("a0"),
5677 make_entry("ab/c"),
5678 ]);
5679 let run = idx.sorted_entries_under(b"a").unwrap();
5680 assert_eq!(paths_of(run), vec!["a/x", "a/y", "a/y", "a/z/deep"]);
5681 assert_eq!(
5682 paths_of(idx.sorted_entries_under(b"a/z").unwrap()),
5683 vec!["a/z/deep"]
5684 );
5685 assert!(idx.sorted_entries_under(b"a.txt").unwrap().is_empty());
5686 assert!(idx.sorted_entries_under(b"missing").unwrap().is_empty());
5687 assert_eq!(idx.sorted_entries_under(b"").unwrap().len(), 8);
5688 }
5689
5690 #[test]
5691 fn sorted_lookups_decline_unsorted_indexes() {
5692 let mut idx = sorted_index(vec![make_entry("b"), make_entry("a/x")]);
5693 assert_eq!(idx.sorted_contains_path(b"a/x"), Some(true));
5694 assert_eq!(idx.sorted_contains_path(b"a"), Some(false));
5695 idx.mark_entries_unsorted();
5696 assert!(idx.sorted_entries_under(b"a").is_none());
5697 assert_eq!(idx.sorted_contains_path(b"a/x"), None);
5698 }
5699
5700 #[test]
5701 fn sorted_contains_path_matches_conflict_stages() {
5702 let idx = sorted_index(vec![
5703 make_staged_entry("both.txt", 2),
5704 make_staged_entry("both.txt", 3),
5705 make_entry("plain.txt"),
5706 ]);
5707 assert_eq!(idx.sorted_contains_path(b"both.txt"), Some(true));
5708 assert_eq!(idx.sorted_contains_path(b"plain.txt"), Some(true));
5709 assert_eq!(idx.sorted_contains_path(b"both"), Some(false));
5710 }
5711
5712 #[test]
5713 fn has_stage0_descendant_under_skips_conflict_only_directories() {
5714 let idx = sorted_index(vec![
5715 make_staged_entry("conf/f", 1),
5716 make_staged_entry("conf/f", 2),
5717 make_entry("conf.txt"),
5718 make_entry("mixed/a"),
5719 make_staged_entry("mixed/b", 3),
5720 ]);
5721 assert!(!idx.has_stage0_descendant_under("conf"));
5722 assert!(idx.has_stage0_descendant_under("mixed"));
5723 assert!(!idx.has_stage0_descendant_under("conf.txt"));
5724 assert!(!idx.has_stage0_descendant_under("mix"));
5725 }
5726
5727 #[test]
5728 fn unsorted_remove_drops_every_matching_stage_globally() {
5729 let mut idx = Index::new();
5730 idx.mark_entries_unsorted();
5731 idx.entries.push(make_staged_entry("a", 1));
5732 idx.entries.push(make_entry("z"));
5733 idx.entries.push(make_staged_entry("a", 2));
5734 assert!(idx.remove(b"a"));
5735 assert!(idx.get(b"a", 1).is_none());
5736 assert!(idx.get(b"a", 2).is_none());
5737 assert!(idx.get(b"z", 0).is_some());
5738 }
5739
5740 #[test]
5741 fn unsorted_remove_descendants_drops_noncontiguous_matches() {
5742 let mut idx = Index::new();
5743 idx.mark_entries_unsorted();
5744 idx.entries.push(make_entry("a/x"));
5745 idx.entries.push(make_entry("z"));
5746 idx.entries.push(make_entry("a/y"));
5747 idx.remove_descendants_under_path("a");
5748 let paths: Vec<_> = idx.entries.iter().map(|e| e.path.as_slice()).collect();
5749 assert_eq!(paths, vec![b"z".as_slice()]);
5750 }
5751
5752 #[test]
5753 fn sorted_ops_match_linear_oracle_unsorted_noncontiguous() {
5754 let mut idx = Index::new();
5755 idx.mark_entries_unsorted();
5756 idx.entries.push(make_entry("a/x"));
5757 idx.entries.push(make_entry("z"));
5758 idx.entries.push(make_staged_entry("a", 1));
5759 idx.remove(b"a");
5760 idx.entries.push(make_entry("a/y"));
5761 idx.sort();
5762 let mut oracle = LinearIndex::from_index(&idx);
5763 oracle.entries = idx.entries.clone();
5764 oracle.assert_matches(&idx);
5765 idx.remove_descendants_under_path("a");
5766 oracle.remove_descendants_under_path("a");
5767 oracle.assert_matches(&idx);
5768 }
5769
5770 #[test]
5771 fn merge_replacements_preserves_base_index_pos() {
5772 let mut idx = Index::new();
5773 let mut kept = make_entry("path/a.txt");
5774 kept.base_index_pos = 42;
5775 idx.add_or_replace(kept);
5776 let mut replacement = make_entry("path/a.txt");
5777 replacement.oid = dummy_oid();
5778 idx.remove_paths_and_insert([], [replacement]);
5779 assert_eq!(idx.entries[0].base_index_pos, 42);
5780 }
5781
5782 #[test]
5783 fn remove_paths_and_insert_replaces_equal_path_without_duplicates() {
5784 let mut idx = Index::new();
5785 idx.add_or_replace(make_entry("path/a.txt"));
5786 idx.add_or_replace(make_entry("path/b.txt"));
5787 let mut replacement = make_entry("path/a.txt");
5788 replacement.oid = dummy_oid();
5789 idx.remove_paths_and_insert(["ghost.txt".as_bytes()], [replacement.clone()]);
5790 assert_eq!(idx.entries.len(), 2);
5791 assert_eq!(idx.entries[0].path, b"path/a.txt");
5792 assert_eq!(idx.entries[0].oid, replacement.oid);
5793 assert_eq!(idx.entries[1].path, b"path/b.txt");
5794 }
5795
5796 #[test]
5797 fn remove_directory_prefixes_drops_nested_paths() {
5798 let mut idx = Index::new();
5799 idx.add_or_replace(make_entry("d005/f0.txt"));
5800 idx.add_or_replace(make_entry("d005/f1.txt"));
5801 idx.add_or_replace(make_entry("d006/f0.txt"));
5802 idx.add_or_replace(make_entry("keep.txt"));
5803 idx.remove_directory_prefixes(&BTreeSet::from(["d005".to_owned()]));
5804 assert!(idx.get(b"d005/f0.txt", 0).is_none());
5805 assert!(idx.get(b"d005/f1.txt", 0).is_none());
5806 assert!(idx.get(b"d006/f0.txt", 0).is_some());
5807 assert!(idx.get(b"keep.txt", 0).is_some());
5808 }
5809
5810 #[test]
5811 fn remove_prefixes_paths_and_insert_combines_prefix_exact_and_replacements() {
5812 let mut idx = Index::new();
5813 idx.add_or_replace(make_entry("d005/f0.txt"));
5814 idx.add_or_replace(make_entry("d005/f1.txt"));
5815 idx.add_or_replace(make_entry("d006/f0.txt"));
5816 idx.add_or_replace(make_entry("solo.txt"));
5817 let mut replacement = make_entry("solo.txt");
5818 replacement.oid = dummy_oid();
5819 idx.remove_prefixes_paths_and_insert(
5820 &BTreeSet::from(["d005".to_owned()]),
5821 [b"d006/f0.txt".as_slice()],
5822 [replacement.clone()],
5823 );
5824 assert!(idx.get(b"d005/f0.txt", 0).is_none());
5825 assert!(idx.get(b"d005/f1.txt", 0).is_none());
5826 assert!(idx.get(b"d006/f0.txt", 0).is_none());
5827 assert_eq!(idx.get(b"solo.txt", 0).unwrap().oid, replacement.oid);
5828 }
5829
5830 #[test]
5831 fn remove_directory_prefixes_does_not_skip_lexically_later_prefix_children() {
5832 let mut idx = Index::new();
5833 idx.add_or_replace(make_entry("d005/f0.txt"));
5834 idx.add_or_replace(make_entry("d005/f1.txt"));
5835 idx.remove_directory_prefixes(&BTreeSet::from(["d005".to_owned(), "d0050".to_owned()]));
5836 assert!(idx.get(b"d005/f0.txt", 0).is_none());
5837 assert!(idx.get(b"d005/f1.txt", 0).is_none());
5838 }
5839
5840 #[test]
5841 fn remove_is_sublinear() {
5842 const N: usize = 100_000;
5843 const M: usize = 1_000;
5844 let mut idx = Index::new();
5845 for i in 0..N {
5846 idx.add_or_replace(make_entry(&format!("file/{i:06}.txt")));
5847 }
5848 let paths: Vec<Vec<u8>> = (0..M)
5849 .map(|i| format!("file/{i:06}.txt").into_bytes())
5850 .collect();
5851 index_path_cmp::reset();
5852 idx.remove_paths(paths.iter().map(|p| p.as_slice()));
5853 let cmps = index_path_cmp::take();
5854 let log_n = (N as f64).log2().ceil() as usize;
5855 let bound = 100 * M * log_n;
5856 assert!(
5857 cmps < bound,
5858 "path comparisons {cmps} should stay under {bound} (N={N}, M={M})"
5859 );
5860 assert_eq!(idx.entries.len(), N - M);
5861 }
5862
5863 #[test]
5864 fn fsmonitor_rebuild_once_per_batch() {
5865 let fsmn_payload: [u8; 41] = [
5866 0, 0, 0, 2, b'b', b'u', b'i', b'l', b't', b'i', b'n', b':', b'f', b'a', b'k', b'e', 0,
5867 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5868 ];
5869 let (token, dirty) = parse_fsmonitor_extension(&fsmn_payload).unwrap();
5870 let dir = TempDir::new().unwrap();
5871 let path = dir.path().join("index");
5872
5873 let mut idx = Index::new();
5874 for i in 0..1000 {
5875 idx.add_or_replace(make_entry(&format!("path-{i:04}.txt")));
5876 }
5877 idx.sort();
5878 for e in &mut idx.entries {
5879 e.set_fsmonitor_valid(true);
5880 }
5881 idx.fsmonitor_last_update = Some(token);
5882 idx.fsmonitor_dirty = Some(dirty);
5883 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5884
5885 fsmonitor_rebuild_counter::reset();
5886 for i in 0..1000 {
5887 idx.add_or_replace(make_entry(&format!("mut-{i:04}.txt")));
5888 }
5889 assert_eq!(
5890 fsmonitor_rebuild_counter::count(),
5891 0,
5892 "entry mutations must not scan entries for the EWAH"
5893 );
5894 idx.write(&path).unwrap(); assert_eq!(
5896 fsmonitor_rebuild_counter::count(),
5897 1,
5898 "write must build the EWAH once for the batch"
5899 );
5900 }
5901
5902 #[test]
5903 fn requested_v4_writes_v4_on_disk() {
5904 let dir = TempDir::new().unwrap();
5905 let path = dir.path().join("index");
5906
5907 let mut idx = Index {
5908 version: 4,
5909 ..Index::default()
5910 };
5911 idx.add_or_replace(make_entry("one"));
5912 idx.add_or_replace(make_entry("two/one"));
5913 idx.write(&path).unwrap(); let data = fs::read(&path).unwrap();
5916 assert_eq!(&data[4..8], &4u32.to_be_bytes());
5917
5918 let loaded = Index::load(&path).unwrap();
5919 assert_eq!(loaded.version, 4);
5920 assert_eq!(loaded.entries[0].path, b"one");
5921 assert_eq!(loaded.entries[1].path, b"two/one");
5922 }
5923
5924 #[test]
5925 fn stat_match_policy_from_repo_config() {
5926 let dir = TempDir::new().unwrap();
5927 let git_dir = dir.path().join(".git");
5928 fs::create_dir_all(&git_dir).unwrap();
5929 fs::write(
5930 git_dir.join("config"),
5931 "[core]\n\ttrustctime = false\n\tcheckstat = minimal\n\tusenanosec = true\n",
5932 )
5933 .unwrap();
5934 let cfg = crate::config::ConfigSet::load(
5935 &crate::environment::Environment::empty(),
5936 Some(&git_dir),
5937 true,
5938 )
5939 .unwrap();
5940 let policy = StatMatchPolicy::from_config(Some(&cfg));
5941 assert!(!policy.trust_ctime);
5942 assert!(!policy.check_stat);
5943 assert!(policy.use_nanosec);
5944 }
5945
5946 fn policy_from_core_config(body: &str) -> StatMatchPolicy {
5947 let dir = TempDir::new().unwrap();
5948 let git_dir = dir.path().join(".git");
5949 fs::create_dir_all(&git_dir).unwrap();
5950 fs::write(git_dir.join("config"), format!("[core]\n{body}")).unwrap();
5951 let cfg = crate::config::ConfigSet::load(
5952 &crate::environment::Environment::empty(),
5953 Some(&git_dir),
5954 true,
5955 )
5956 .unwrap();
5957 StatMatchPolicy::from_config(Some(&cfg))
5958 }
5959
5960 #[test]
5961 fn stat_match_policy_uses_nanoseconds_by_default_on_windows_only() {
5962 assert_eq!(StatMatchPolicy::DEFAULT_USE_NANOSEC, cfg!(windows));
5963 assert_eq!(StatMatchPolicy::default().use_nanosec, cfg!(windows));
5964 assert_eq!(
5965 StatMatchPolicy::from_config(None),
5966 StatMatchPolicy::default()
5967 );
5968 assert_eq!(
5969 policy_from_core_config("\tbare = false\n"),
5970 StatMatchPolicy::default()
5971 );
5972 assert!(!policy_from_core_config("\tusenanosec = false\n").use_nanosec);
5973 assert!(policy_from_core_config("\tusenanosec = true\n").use_nanosec);
5974 }
5975
5976 const NSEC_POLICY: StatMatchPolicy = StatMatchPolicy {
5977 trust_ctime: true,
5978 check_stat: true,
5979 use_nanosec: true,
5980 };
5981 const WHOLE_SECOND_POLICY: StatMatchPolicy = StatMatchPolicy {
5982 use_nanosec: false,
5983 ..NSEC_POLICY
5984 };
5985
5986 fn entry_with_mtime(sec: u32, nsec: u32) -> IndexEntry {
5987 IndexEntry {
5988 mtime_sec: sec,
5989 mtime_nsec: nsec,
5990 ctime_sec: sec,
5991 ctime_nsec: nsec,
5992 size: 42,
5993 ..make_entry("f")
5994 }
5995 }
5996
5997 #[test]
5998 fn nanosecond_policy_trusts_same_second_entries_but_sees_newer_fractions() {
5999 let ie = entry_with_mtime(1_700_000_000, 123_456_700);
6000 let index_mtime = Some((ie.mtime_sec, ie.mtime_nsec + 1));
6001 assert!(!crate::diff::entry_is_racy_with_policy(
6002 &ie,
6003 index_mtime,
6004 NSEC_POLICY
6005 ));
6006 assert!(crate::diff::entry_is_racy_with_policy(
6007 &ie,
6008 Some((ie.mtime_sec, ie.mtime_nsec)),
6009 NSEC_POLICY
6010 ));
6011 assert!(
6012 crate::diff::entry_is_racy_with_policy(&ie, index_mtime, WHOLE_SECOND_POLICY),
6013 "the whole-second rule keeps the index's own second racy"
6014 );
6015
6016 let mut edited = IndexStatFields::of_entry(&ie);
6018 edited.mtime_nsec += 1;
6019 assert!(IndexStatFields::of_entry(&ie).matches_index_entry(&ie, NSEC_POLICY));
6020 assert!(!edited.matches_index_entry(&ie, NSEC_POLICY));
6021 assert!(edited.matches_index_entry(&ie, WHOLE_SECOND_POLICY));
6022 }
6023
6024 #[test]
6025 fn minimal_checkstat_keeps_whole_second_racy_rule_under_nanosec() {
6026 let minimal = StatMatchPolicy {
6027 check_stat: false,
6028 ..NSEC_POLICY
6029 };
6030 let ie = entry_with_mtime(1_700_000_000, 100_000_000);
6031 let index_mtime = Some((ie.mtime_sec, 500_000_000));
6032 assert!(
6033 crate::diff::entry_is_racy_with_policy(&ie, index_mtime, minimal),
6034 "without fraction equality, a same-second entry must stay racy"
6035 );
6036 let mut edited = IndexStatFields::of_entry(&ie);
6037 edited.mtime_nsec = 200_000_000;
6038 assert!(edited.matches_index_entry(&ie, minimal));
6039 assert!(!crate::diff::entry_is_racy_with_policy(
6040 &ie,
6041 Some((ie.mtime_sec + 1, 0)),
6042 minimal
6043 ));
6044 }
6045
6046 #[test]
6047 fn advancing_index_mtime_never_leaves_the_last_tick_of_a_second() {
6048 let dir = TempDir::new().unwrap();
6049 let index_path = dir.path().join("index");
6050 let sec = 1_700_000_000u32;
6051 let last_tick = if cfg!(windows) {
6052 999_999_900
6053 } else {
6054 999_999_999
6055 };
6056 let mut index = Index::new();
6057 index.add_or_replace(entry_with_mtime(sec, last_tick));
6058 index.write(&index_path).unwrap();
6059 let pinned = filetime::FileTime::from_unix_time(i64::from(sec), last_tick);
6060 filetime::set_file_mtime(&index_path, pinned).unwrap();
6061
6062 advance_index_file_mtime_past_entries(&index, &index_path, None).unwrap();
6063 assert_eq!(index_file_mtime(&index_path), Some((sec, last_tick)));
6064 for policy in [NSEC_POLICY, WHOLE_SECOND_POLICY] {
6065 assert!(crate::diff::entry_is_racy_with_policy(
6066 &index.entries[0],
6067 index_file_mtime(&index_path),
6068 policy
6069 ));
6070 }
6071 }
6072
6073 #[test]
6074 fn size_and_times_match_index_compares_fractions_only_under_nanosec_policy() {
6075 let ie = entry_with_mtime(1_700_000_000, 123_456_700);
6076 let mut scan = IndexStatFields::of_entry(&ie);
6077 scan.ctime_sec += 5;
6078 scan.ino = 99;
6079 assert!(scan.size_and_times_match_index(&ie, NSEC_POLICY));
6080
6081 let mut mtime_frac = IndexStatFields::of_entry(&ie);
6082 mtime_frac.mtime_nsec += 100;
6083 assert!(!mtime_frac.size_and_times_match_index(&ie, NSEC_POLICY));
6084 assert!(mtime_frac.size_and_times_match_index(&ie, WHOLE_SECOND_POLICY));
6085
6086 let mut ctime_frac = IndexStatFields::of_entry(&ie);
6087 ctime_frac.ctime_nsec += 100;
6088 assert!(!ctime_frac.size_and_times_match_index(&ie, NSEC_POLICY));
6089 let no_ctime = StatMatchPolicy {
6090 trust_ctime: false,
6091 ..NSEC_POLICY
6092 };
6093 assert!(ctime_frac.size_and_times_match_index(&ie, no_ctime));
6094
6095 let mut resized = IndexStatFields::of_entry(&ie);
6096 resized.size += 1;
6097 assert!(!resized.size_and_times_match_index(&ie, WHOLE_SECOND_POLICY));
6098 }
6099
6100 #[test]
6101 fn advancing_index_mtime_clears_same_tick_entries_within_their_second() {
6102 let dir = TempDir::new().unwrap();
6103 let index_path = dir.path().join("index");
6104 let (sec, nsec) = (1_700_000_000u32, 123_456_700u32);
6105 let mut index = Index::new();
6106 index.add_or_replace(entry_with_mtime(sec, nsec));
6107 index.write(&index_path).unwrap();
6108 filetime::set_file_mtime(
6109 &index_path,
6110 filetime::FileTime::from_unix_time(i64::from(sec), nsec),
6111 )
6112 .unwrap();
6113 let ie = &index.entries[0];
6114 assert!(crate::diff::entry_is_racy_with_policy(
6115 ie,
6116 index_file_mtime(&index_path),
6117 NSEC_POLICY
6118 ));
6119
6120 advance_index_file_mtime_past_entries(&index, &index_path, None).unwrap();
6121 let bumped = index_file_mtime(&index_path);
6122 assert_eq!(
6123 bumped.map(|(s, _)| s),
6124 Some(sec),
6125 "never leave the entry's second"
6126 );
6127 assert!(
6128 !crate::diff::entry_is_racy_with_policy(ie, bumped, NSEC_POLICY),
6129 "bumped index mtime {bumped:?} must be past the entry's filesystem tick"
6130 );
6131 assert!(crate::diff::entry_is_racy_with_policy(
6132 ie,
6133 bumped,
6134 WHOLE_SECOND_POLICY
6135 ));
6136 }
6137
6138 #[test]
6139 fn index_entry_extended_flags_and_placeholders() {
6140 let mut e = make_entry("p");
6141 e.set_intent_to_add(true);
6142 assert!(e.intent_to_add());
6143 e.set_assume_unchanged(true);
6144 assert!(e.assume_unchanged());
6145 e.set_skip_worktree(true);
6146 assert!(e.skip_worktree());
6147 e.mode = MODE_TREE;
6148 e.set_skip_worktree(true);
6149 assert!(e.is_sparse_directory_placeholder());
6150 e.set_overlay_tree_skip_output(true);
6151 assert!(e.overlay_tree_skip_output());
6152 }
6153
6154 #[test]
6155 fn cache_tree_formatting_and_invalidation_helpers() {
6156 let mut idx = Index::new();
6157 idx.add_or_replace(make_entry("a/b"));
6158 let child = CacheTreeNode::valid(b"a".to_vec(), 1, dummy_oid(), vec![]);
6159 let root = CacheTreeNode::valid(b"".to_vec(), 1, dummy_oid(), vec![child]);
6160 idx.set_cache_tree(root);
6161 let dump = idx.format_cache_tree_dump();
6162 assert!(dump.contains("a/") || dump.contains("invalid"));
6163 idx.invalidate_cache_tree_for_path(b"a/b");
6164 idx.clear_cache_tree();
6165 assert!(idx.cache_tree.is_none());
6166 }
6167
6168 #[test]
6169 fn dedup_paths_and_stage0_descendant_queries() {
6170 let mut idx = Index::new();
6171 idx.add_or_replace(make_entry("dir/x"));
6172 idx.add_or_replace(make_entry("dir/x"));
6173 idx.dedup_paths_keep_last();
6174 assert_eq!(idx.entries.len(), 1);
6175 assert!(idx.has_stage0_descendant_under("dir"));
6176 assert!(!idx.has_stage0_descendant_under("missing"));
6177 }
6178
6179 #[test]
6180 fn load_missing_index_is_empty() {
6181 let dir = TempDir::new().unwrap();
6182 let path = dir.path().join("no-index");
6183 let idx = Index::load(&path).unwrap();
6184 assert!(idx.entries.is_empty());
6185 }
6186
6187 #[test]
6188 fn munge_and_stat_equivalence_helpers() {
6189 assert_eq!(munge_index_size(0x1_0000_0000), 0x8000_0000);
6190 let a = make_entry("x");
6191 let b = make_entry("x");
6192 assert!(index_entries_stat_equivalent(
6193 &a,
6194 &b,
6195 StatMatchPolicy::default()
6196 ));
6197 }
6198
6199 #[test]
6200 fn new_from_config_invalid_index_version_falls_back() {
6201 let dir = TempDir::new().unwrap();
6202 let git_dir = dir.path().join(".git");
6203 fs::create_dir_all(&git_dir).unwrap();
6204 fs::write(
6205 git_dir.join("config"),
6206 "[index]\n\tversion = not-a-number\n",
6207 )
6208 .unwrap();
6209 let cfg = crate::config::ConfigSet::load(
6210 &crate::environment::Environment::empty(),
6211 Some(&git_dir),
6212 true,
6213 )
6214 .unwrap();
6215 let idx = Index::new_from_config(&cfg);
6216 assert_eq!(idx.version, INDEX_CONFIG_INVALID_FALLBACK);
6217 }
6218
6219 #[test]
6220 fn fsmonitor_invalidate_directory_prefix_without_trailing_slash() {
6221 let mut idx = Index::new();
6222 idx.add_or_replace(make_entry("dir/a.txt"));
6223 idx.add_or_replace(make_entry("dir/b.txt"));
6224 idx.set_fsmonitor_last_update(Some("tok".into()));
6225 for entry in idx.entries_mut() {
6226 entry.set_fsmonitor_valid(true);
6227 }
6228 idx.invalidate_fsmonitor_for_path("dir", false);
6229 assert!(!idx.get(b"dir/a.txt", 0).unwrap().fsmonitor_valid());
6230 assert!(!idx.get(b"dir/b.txt", 0).unwrap().fsmonitor_valid());
6231 }
6232
6233 #[test]
6234 fn parse_fsmonitor_extension_rejects_truncated_ewah() {
6235 let mut payload = vec![0, 0, 0, 2, b't', 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0];
6236 let err = parse_fsmonitor_extension(&payload).unwrap_err();
6237 assert!(matches!(
6238 err,
6239 Error::IndexFsmonitorExtensionTruncated | Error::IndexFsmonitorExtensionEwahInvalid
6240 ));
6241 payload[12..16].copy_from_slice(&2u32.to_be_bytes());
6242 let err2 = parse_fsmonitor_extension(&payload).unwrap_err();
6243 assert!(matches!(
6244 err2,
6245 Error::IndexFsmonitorExtensionTruncated | Error::IndexFsmonitorExtensionEwahInvalid
6246 ));
6247 }
6248
6249 #[test]
6250 fn unmerge_from_resolve_undo_installs_conflict_stages() {
6251 use crate::resolve_undo::ResolveUndoRecord;
6252 use std::collections::BTreeMap;
6253
6254 let mut idx = Index::new();
6255 idx.add_or_replace(make_entry("f.txt"));
6256 let oid1 = dummy_oid();
6257 let oid2 = ObjectId::from_bytes(&[1u8; 20]).unwrap();
6258 idx.resolve_undo = Some(BTreeMap::from([(
6259 b"f.txt".to_vec(),
6260 ResolveUndoRecord {
6261 modes: [MODE_REGULAR, MODE_REGULAR, 0],
6262 oids: [oid1, oid2, ObjectId::zero()],
6263 },
6264 )]));
6265 assert!(idx.unmerge_path_from_resolve_undo(b"f.txt"));
6266 assert!(idx.has_unmerged_entries());
6267 }
6268
6269 #[test]
6270 fn parse_rejects_truncated_headers() {
6271 for n in 0..12 {
6272 assert!(Index::parse(&vec![0u8; n]).is_err());
6273 }
6274 }
6275
6276 #[test]
6277 fn entry_from_stat_populates_index_metadata() {
6278 let dir = TempDir::new().unwrap();
6279 fs::write(dir.path().join("f.txt"), b"payload").unwrap();
6280 let oid = dummy_oid();
6281 let entry = entry_from_stat(dir.path(), b"f.txt", oid, MODE_REGULAR).unwrap();
6282 assert_eq!(entry.path, b"f.txt");
6283 assert!(entry.size > 0);
6284 }
6285
6286 #[test]
6287 fn write_to_path_honors_skip_hash_flag() {
6288 let dir = TempDir::new().unwrap();
6289 let path = dir.path().join("index");
6290 let mut idx = Index::new();
6291 idx.add_or_replace(make_entry("x"));
6292 idx.write_to_path(&path, true).unwrap();
6293 let bytes = fs::read(&path).unwrap();
6294 assert!(bytes.ends_with(&[0u8; 20]));
6295 }
6296
6297 #[test]
6298 fn index_lock_acquire_blocks_second_holder() {
6299 let dir = TempDir::new().unwrap();
6300 let path = dir.path().join("index");
6301 let mut idx = Index::new();
6302 idx.add_or_replace(make_entry("tracked.txt"));
6303 let lock = IndexLock::acquire(&path, None).unwrap();
6304 assert!(matches!(
6305 IndexLock::acquire(&path, None),
6306 Err(Error::LockHeld { .. })
6307 ));
6308 drop(lock);
6309 idx.write(&path).unwrap(); assert_eq!(Index::load(&path).unwrap().entries.len(), 1);
6311 }
6312
6313 #[test]
6314 fn held_index_lock_is_a_typed_error_naming_the_holder() {
6315 let dir = TempDir::new().unwrap();
6316 let path = dir.path().join("index");
6317 let lock_path = dir.path().join("index.lock");
6318 fs::write(&lock_path, b"foreign").unwrap();
6319 fs::write(
6320 dir.path().join("index~pid.lock"),
6321 format!("pid {}\n", std::process::id()),
6322 )
6323 .unwrap();
6324
6325 let Err(acquire_err) = IndexLock::acquire(&path, None) else {
6326 panic!("lock must be refused");
6327 };
6328 let write_err = Index::new().write(&path).unwrap_err();
6329 for err in [acquire_err, write_err] {
6330 match &err {
6331 Error::LockHeld { path: p, holder } => {
6332 assert_eq!(p, &lock_path);
6333 assert_eq!(
6334 *holder,
6335 Some(crate::lockfile::LockHolder {
6336 pid: u64::from(std::process::id()),
6337 running: true,
6338 })
6339 );
6340 }
6341 other => panic!("expected LockHeld, got {other:?}"),
6342 }
6343 assert!(
6344 err.to_string().contains(&lock_path.display().to_string()),
6345 "{err}"
6346 );
6347 }
6348 assert_eq!(fs::read(&lock_path).unwrap(), b"foreign");
6349 assert!(!path.exists());
6350 }
6351
6352 #[test]
6353 fn write_with_config_and_parallel_policy() {
6354 let dir = TempDir::new().unwrap();
6355 let path = dir.path().join("index");
6356 let mut idx = Index::new();
6357 idx.add_or_replace(make_entry("p"));
6358 let cfg = crate::config::ConfigSet::new();
6359 idx.write_with_config_and_policy(&path, &cfg, crate::parallel::ThreadPolicy::default())
6360 .unwrap();
6361 assert!(path.is_file());
6362 }
6363
6364 #[test]
6365 fn fsmonitor_sync_and_clear_helpers() {
6366 let mut idx = Index::new();
6367 idx.add_or_replace(make_entry("a"));
6368 idx.set_fsmonitor_last_update(Some("token".to_owned()));
6369 idx.sync_fsmonitor_dirty_bitmap();
6370 if let Some(e) = idx.get_mut(b"a", 0) {
6371 e.set_fsmonitor_valid(false);
6372 }
6373 idx.fsmonitor_validity_changed();
6374 idx.clear_fsmonitor_extension();
6375 assert!(idx.fsmonitor_last_update.is_none());
6376 }
6377
6378 #[test]
6379 fn intent_to_add_entry_forces_empty_blob_on_write_tree_path() {
6380 let dir = TempDir::new().unwrap();
6381 let odb = crate::odb::Odb::new(dir.path());
6382 let mut idx = Index::new();
6383 let mut e = make_entry("ita.txt");
6384 e.set_intent_to_add(true);
6385 idx.add_or_replace(e);
6386 crate::write_tree::write_tree_from_index(&odb, &idx, "").unwrap();
6387 }
6388
6389 #[test]
6390 fn optional_load_is_empty_only_when_the_index_is_missing() {
6391 let dir = TempDir::new().unwrap();
6392 let odb = crate::odb::Odb::new(dir.path());
6393 let path = dir.path().join("index");
6394 let missing = Index::load_expand_sparse_optional(&path, &odb).unwrap();
6395 assert!(missing.entries.is_empty());
6396
6397 for len in [0, 12, 31] {
6398 std::fs::write(&path, b"DIRC\0\0\0\x02\0\0\0\0".repeat(3)[..len].to_vec()).unwrap();
6399 assert!(
6400 matches!(
6401 Index::load_expand_sparse_optional(&path, &odb),
6402 Err(Error::IndexError(_))
6403 ),
6404 "len={len}"
6405 );
6406 }
6407 }
6408
6409 fn naive_merge_sorted(kept: &[IndexEntry], insert: &[IndexEntry]) -> Vec<IndexEntry> {
6410 let mut out: Vec<IndexEntry> = kept.iter().chain(insert.iter()).cloned().collect();
6411 out.sort_by(index_entry_path_stage_cmp);
6412 let mut deduped = Vec::with_capacity(out.len());
6413 for entry in out {
6414 if deduped
6415 .last()
6416 .is_some_and(|last| index_entry_path_stage_cmp(last, &entry).is_eq())
6417 {
6418 *deduped.last_mut().unwrap() = entry;
6419 } else {
6420 deduped.push(entry);
6421 }
6422 }
6423 deduped
6424 }
6425
6426 #[test]
6427 fn merge_sorted_index_halves_matches_naive_merge() {
6428 let paths = [
6429 ".gitattributes",
6430 "a.txt",
6431 "b.txt",
6432 "f.txt",
6433 "z.txt",
6434 "dir/nested.txt",
6435 ];
6436 let mut kept = Vec::new();
6437 let mut insert = Vec::new();
6438 for (i, path) in paths.iter().enumerate() {
6439 let mut e = make_entry(path);
6440 e.oid = ObjectId::from_bytes(&[i as u8; 20]).unwrap();
6441 kept.push(e);
6442 }
6443 for (i, path) in paths.iter().enumerate() {
6444 if i % 2 == 0 {
6445 continue;
6446 }
6447 let mut e = make_entry(path);
6448 e.oid = ObjectId::from_bytes(&[(i + 100) as u8; 20]).unwrap();
6449 insert.push(e);
6450 }
6451 kept.sort_by(index_entry_path_stage_cmp);
6452 insert.sort_by(index_entry_path_stage_cmp);
6453
6454 let expected = naive_merge_sorted(&kept, &insert);
6455 let merged = merge_sorted_index_entries(kept, insert);
6456 assert_eq!(merged, expected, "fixed scenario merge mismatch");
6457
6458 for seed in 0u64..200 {
6459 let mut kept = Vec::new();
6460 let mut insert = Vec::new();
6461 for n in 0..((seed % 7) + 1) {
6462 let path = format!("p/{seed}/{n}.txt");
6463 let mut e = make_entry(&path);
6464 e.oid = ObjectId::from_bytes(&[(seed as u8).wrapping_add(n as u8); 20]).unwrap();
6465 kept.push(e);
6466 }
6467 for n in 0..((seed % 5) + 1) {
6468 let path = format!("p/{seed}/{n}.txt");
6469 let mut e = make_entry(&path);
6470 e.oid = ObjectId::from_bytes(
6471 &[(seed as u8).wrapping_add(n as u8).wrapping_add(50); 20],
6472 )
6473 .unwrap();
6474 insert.push(e);
6475 }
6476 kept.sort_by(index_entry_path_stage_cmp);
6477 insert.sort_by(index_entry_path_stage_cmp);
6478 let expected = naive_merge_sorted(&kept, &insert);
6479 let merged = merge_sorted_index_entries(kept, insert);
6480 assert_eq!(merged, expected, "seed {seed} merge mismatch");
6481 }
6482 }
6483
6484 mod sparse_collapse {
6485 use super::*;
6486
6487 const PATTERNS: [&str; 3] = ["/*", "!/*/", "/a/"];
6488
6489 fn patterns() -> Vec<String> {
6490 PATTERNS.iter().map(|p| (*p).to_owned()).collect()
6491 }
6492
6493 fn write_tree(odb: &Odb, files: &[(&str, u32, &str)]) -> ObjectId {
6495 let mut here: BTreeMap<Vec<u8>, (u32, ObjectId)> = BTreeMap::new();
6496 let mut subdirs: BTreeMap<&str, Vec<(&str, u32, &str)>> = BTreeMap::new();
6497 for (path, mode, body) in files {
6498 match path.split_once('/') {
6499 Some((dir, rest)) => subdirs.entry(dir).or_default().push((rest, *mode, body)),
6500 None => {
6501 let oid = if *mode == MODE_GITLINK {
6502 ObjectId::from_bytes(&[7u8; 20]).unwrap()
6503 } else {
6504 odb.write(ObjectKind::Blob, body.as_bytes()).unwrap()
6505 };
6506 here.insert(path.as_bytes().to_vec(), (*mode, oid));
6507 }
6508 }
6509 }
6510 for (dir, children) in subdirs {
6511 here.insert(
6512 dir.as_bytes().to_vec(),
6513 (MODE_TREE, write_tree(odb, &children)),
6514 );
6515 }
6516 let mut entries: Vec<TreeEntry> = here
6517 .into_iter()
6518 .map(|(name, (mode, oid))| TreeEntry { mode, name, oid })
6519 .collect();
6520 entries.sort_by(|a, b| {
6521 crate::objects::tree_entry_cmp(
6522 &a.name,
6523 a.mode == MODE_TREE,
6524 &b.name,
6525 b.mode == MODE_TREE,
6526 )
6527 });
6528 odb.write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
6529 .unwrap()
6530 }
6531
6532 fn subtree(odb: &Odb, root: &ObjectId, prefix: &str) -> ObjectId {
6533 PrefixTreeResolver::new(*root)
6534 .resolve(odb, prefix.as_bytes())
6535 .unwrap()
6536 .unwrap()
6537 }
6538
6539 fn expanded_index(odb: &Odb, root: &ObjectId) -> Index {
6542 let mut index = Index::new();
6543 let rows = flatten_tree_blobs(
6544 odb,
6545 root,
6546 b"",
6547 crate::path_validate::ProtectOptions::default(),
6548 )
6549 .unwrap();
6550 for mut row in rows {
6551 let in_cone = !row.path.contains(&b'/') || row.path.starts_with(b"a/");
6552 row.set_skip_worktree(!in_cone);
6553 index.entries_mut().push(row);
6554 }
6555 index.sort();
6556 index
6557 }
6558
6559 fn collapse(index: &mut Index, odb: &Odb, root: &ObjectId) {
6560 index
6561 .try_collapse_sparse_directories(
6562 odb,
6563 root,
6564 &patterns(),
6565 true,
6566 true,
6567 crate::path_validate::ProtectOptions::default(),
6568 )
6569 .unwrap();
6570 }
6571
6572 fn rows(index: &Index) -> Vec<(String, u32, ObjectId)> {
6573 index
6574 .entries()
6575 .iter()
6576 .map(|e| (String::from_utf8_lossy(&e.path).into_owned(), e.mode, e.oid))
6577 .collect()
6578 }
6579
6580 fn placeholder_paths(index: &Index) -> Vec<String> {
6581 index
6582 .entries()
6583 .iter()
6584 .filter(|e| e.is_sparse_directory_placeholder())
6585 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
6586 .collect()
6587 }
6588
6589 const FILES: [(&str, u32, &str); 8] = [
6590 ("a/in", 0o100644, "in\n"),
6591 ("top", 0o100644, "top\n"),
6592 ("w/f4", 0o100644, "f4\n"),
6593 ("w/f5", 0o100755, "f5\n"),
6594 ("x/f1", 0o100644, "f1\n"),
6595 ("x/y/f2", 0o100644, "f2\n"),
6596 ("x/y/z/f3", 0o100644, "f3\n"),
6597 ("x/y-z", 0o100644, "dash\n"),
6598 ];
6599
6600 #[test]
6601 fn unchanged_excluded_directories_collapse_to_their_outermost_placeholder() {
6602 let dir = TempDir::new().unwrap();
6603 let odb = Odb::new(dir.path());
6604 let root = write_tree(&odb, &FILES);
6605 let mut index = expanded_index(&odb, &root);
6606 collapse(&mut index, &odb, &root);
6607
6608 assert_eq!(
6609 rows(&index),
6610 vec![
6611 ("a/in".to_owned(), 0o100644, index.entries()[0].oid),
6612 ("top".to_owned(), 0o100644, index.entries()[1].oid),
6613 ("w/".to_owned(), MODE_TREE, subtree(&odb, &root, "w")),
6614 ("x/".to_owned(), MODE_TREE, subtree(&odb, &root, "x")),
6615 ]
6616 );
6617 assert!(index.sparse_directories);
6618 assert!(index.entries_are_sorted());
6619 }
6620
6621 #[test]
6622 fn a_changed_row_keeps_its_ancestors_expanded_but_not_its_siblings() {
6623 let dir = TempDir::new().unwrap();
6624 let odb = Odb::new(dir.path());
6625 let root = write_tree(&odb, &FILES);
6626 let mut index = expanded_index(&odb, &root);
6627 let edited = odb.write(ObjectKind::Blob, b"edited\n").unwrap();
6628 let pos = index.find_entry_pos(b"x/f1", 0).unwrap();
6629 index.entries_mut()[pos].oid = edited;
6630 index.sort();
6631 collapse(&mut index, &odb, &root);
6632
6633 assert_eq!(
6634 placeholder_paths(&index),
6635 vec!["w/".to_owned(), "x/y/".to_owned()]
6636 );
6637 let kept: Vec<String> = rows(&index).into_iter().map(|(p, _, _)| p).collect();
6638 assert_eq!(kept, ["a/in", "top", "w/", "x/f1", "x/y-z", "x/y/"]);
6639 let f1 = &index.entries()[index.find_entry_pos(b"x/f1", 0).unwrap()];
6640 assert_eq!(f1.oid, edited);
6641 let y = &index.entries()[index.find_entry_pos(b"x/y/", 0).unwrap()];
6642 assert_eq!(y.oid, subtree(&odb, &root, "x/y"));
6643 }
6644
6645 #[test]
6646 fn directories_with_worktree_rows_gitlinks_or_conflicts_stay_expanded() {
6647 let dir = TempDir::new().unwrap();
6648 let odb = Odb::new(dir.path());
6649 let mut files = FILES.to_vec();
6650 files.push(("v/sub", MODE_GITLINK, ""));
6651 files.push(("u/f6", 0o100644, "f6\n"));
6652 let root = write_tree(&odb, &files);
6653 let mut index = expanded_index(&odb, &root);
6654 let pos = index.find_entry_pos(b"w/f4", 0).unwrap();
6655 index.entries_mut()[pos].set_skip_worktree(false);
6656 let pos = index.find_entry_pos(b"u/f6", 0).unwrap();
6657 let mut theirs = index.entries()[pos].clone();
6658 index.entries_mut()[pos].set_stage(2);
6659 theirs.set_stage(3);
6660 index.entries_mut().push(theirs);
6661 index.sort();
6662 collapse(&mut index, &odb, &root);
6663
6664 assert_eq!(placeholder_paths(&index), vec!["x/".to_owned()]);
6665 for path in ["u/f6", "v/sub", "w/f4", "w/f5"] {
6666 assert!(
6667 index.entries().iter().any(|e| e.path == path.as_bytes()),
6668 "{path} must stay expanded"
6669 );
6670 }
6671 }
6672
6673 #[test]
6674 fn existing_placeholders_count_only_when_they_match_head() {
6675 let dir = TempDir::new().unwrap();
6676 let odb = Odb::new(dir.path());
6677 let root = write_tree(&odb, &FILES);
6678 let y = subtree(&odb, &root, "x/y");
6679
6680 let with_y_placeholder = |oid: ObjectId| {
6681 let mut index = expanded_index(&odb, &root);
6682 index.entries_mut().retain(|e| !e.path.starts_with(b"x/y/"));
6683 let mut placeholder = index.entries()[0].clone();
6684 placeholder.path = b"x/y/".to_vec();
6685 placeholder.mode = MODE_TREE;
6686 placeholder.oid = oid;
6687 placeholder.flags = 4;
6688 placeholder.set_skip_worktree(true);
6689 index.entries_mut().push(placeholder);
6690 index.sort();
6691 index
6692 };
6693
6694 let mut matching = with_y_placeholder(y);
6695 collapse(&mut matching, &odb, &root);
6696 assert_eq!(
6697 placeholder_paths(&matching),
6698 vec!["w/".to_owned(), "x/".to_owned()]
6699 );
6700
6701 let stale = odb.write(ObjectKind::Tree, b"").unwrap();
6702 let mut outdated = with_y_placeholder(stale);
6703 collapse(&mut outdated, &odb, &root);
6704 assert_eq!(
6705 placeholder_paths(&outdated),
6706 vec!["w/".to_owned(), "x/y/".to_owned()]
6707 );
6708 let y_row = &outdated.entries()[outdated.find_entry_pos(b"x/y/", 0).unwrap()];
6709 assert_eq!(y_row.oid, stale, "an outdated placeholder is left alone");
6710 assert!(outdated.entries().iter().any(|e| e.path == b"x/f1"));
6711 }
6712
6713 #[test]
6714 fn collapsed_directories_subsume_nested_ones_and_cover_their_rows() {
6715 let oid = ObjectId::from_bytes(&[1u8; 20]).unwrap();
6716 let mut collapsed = CollapsedDirectories::default();
6717 collapsed.insert(b"a/b/".to_vec(), oid);
6718 collapsed.insert(b"a/c/".to_vec(), oid);
6719 assert!(collapsed.has_descendants(b"a/"));
6720 assert!(!collapsed.has_descendants(b"a/b/"));
6721 collapsed.insert(b"a/".to_vec(), oid);
6722 assert!(!collapsed.has_descendants(b"a/"));
6723 assert_eq!(collapsed.dirs.len(), 1);
6724
6725 let mut scratch = Vec::new();
6726 for (path, covered) in [
6727 (&b"a/x"[..], true),
6728 (b"a/b/c", true),
6729 (b"a", true),
6730 (b"a-b", false),
6731 (b"ab/c", false),
6732 (b"0", false),
6733 ] {
6734 assert_eq!(
6735 collapsed.covers(path, &mut scratch),
6736 covered,
6737 "{}",
6738 String::from_utf8_lossy(path)
6739 );
6740 }
6741
6742 let entry = |path: &str| IndexEntry {
6743 path: path.as_bytes().to_vec(),
6744 ..make_entry(path)
6745 };
6746 let entries = vec![
6747 entry("0"),
6748 entry("a"),
6749 entry("a-b"),
6750 entry("a/b/c"),
6751 entry("ab"),
6752 ];
6753 assert_eq!(
6754 collapsed.covered_rows(&entries),
6755 vec![false, true, false, true, false]
6756 );
6757 }
6758 }
6759}