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 if let Some(lock) = existing_lock {
1676 if lock.index_path != path {
1677 return Err(Error::Message(
1678 "index lock path does not match write target".into(),
1679 ));
1680 }
1681 self.stream_index_file(&mut lock.file, skip_hash, config, policy)?;
1682 lock.reopen_after_write()?;
1683 if commit {
1684 lock.commit_replace_index()?;
1685 }
1686 return Ok(());
1687 }
1688
1689 let tmp_path = path.with_extension("lock");
1690 let pid_path = pid_path_for_lock(&tmp_path);
1691 let lockfile_pid_enabled = grit_lockfile_pid_enabled(config, path.parent());
1692
1693 use crate::lockfile::{LockFile, LockWait};
1694 let wait = LockWait::files_ref_lock(config);
1695 let mut lock_file = LockFile::acquire(&tmp_path, wait).map_err(Error::from)?;
1696 if lockfile_pid_enabled {
1697 if let Err(e) = lock_file
1698 .write_holder_pid()
1699 .map_err(crate::error::Error::from)
1700 {
1701 lock_file.rollback();
1702 return Err(e);
1703 }
1704 }
1705 let wrote_pid_file = lockfile_pid_enabled;
1706 let (tmp_path, mut lock_file) = lock_file.into_open_file()?;
1707
1708 if let Err(e) = self.stream_index_file(&mut lock_file, skip_hash, config, policy) {
1709 drop(lock_file);
1710 let _ = fs::remove_file(&tmp_path);
1711 if wrote_pid_file {
1712 let _ = fs::remove_file(&pid_path);
1713 }
1714 return Err(e);
1715 }
1716 drop(lock_file);
1717
1718 if let Err(e) = crate::atomic_replace::replace_file(&tmp_path, path) {
1719 let _ = fs::remove_file(&tmp_path);
1720 if wrote_pid_file {
1721 let _ = fs::remove_file(&pid_path);
1722 }
1723 return Err(Error::Io(e));
1724 }
1725 if wrote_pid_file {
1726 let _ = fs::remove_file(&pid_path);
1727 }
1728 Ok(())
1729 }
1730
1731 fn stream_index_file(
1737 &mut self,
1738 file: &mut fs::File,
1739 skip_hash: bool,
1740 config: Option<&ConfigSet>,
1741 policy: crate::parallel::ThreadPolicy,
1742 ) -> Result<()> {
1743 self.ensure_canonical_order();
1744 let mut hasher = (!skip_hash).then(|| self.hash_algo.checksum_hasher());
1745 let mut sink = |chunk: &[u8]| -> io::Result<()> {
1746 if let Some(hasher) = hasher.as_mut() {
1747 hasher.update(chunk);
1748 }
1749 file.write_all(chunk)
1750 };
1751 let mut body = IndexBodyOut {
1752 buf: Vec::new(),
1753 drained: 0,
1754 sink: Some(&mut sink),
1755 };
1756 self.serialize_body(&mut body, config, policy)?;
1757 let checksum = match hasher {
1758 Some(hasher) => hasher.finalize(),
1759 None => vec![0u8; self.hash_algo.len()],
1760 };
1761 file.write_all(&checksum)?;
1762 Ok(())
1763 }
1764
1765 #[cfg(test)]
1766 pub(crate) fn serialize_into(&self, out: &mut Vec<u8>) -> Result<()> {
1770 let mut body = IndexBodyOut {
1771 buf: std::mem::take(out),
1772 drained: 0,
1773 sink: None,
1774 };
1775 let result = self.serialize_body(&mut body, None, crate::parallel::ThreadPolicy::default());
1776 *out = body.buf;
1777 result
1778 }
1779
1780 fn serialize_body(
1782 &self,
1783 out: &mut IndexBodyOut<'_>,
1784 config: Option<&ConfigSet>,
1785 policy: crate::parallel::ThreadPolicy,
1786 ) -> Result<()> {
1787 use crate::index_eoie_ieot::{
1788 entries_per_ieot_block, ieot_block_count_for_write, record_end_of_index_entries,
1789 write_ieot_payload, IeotEntry, IeotTable, INDEX_EXT_EOIE, INDEX_EXT_IEOT,
1790 };
1791 let has_extended_flags = self.entries.iter().any(|e| {
1792 e.flags_extended
1793 .is_some_and(|fe| IndexEntry::disk_flags_extended(fe) != 0)
1794 });
1795 let write_version = if self.version >= 4 {
1796 4
1797 } else if has_extended_flags {
1798 3
1799 } else if self.version >= 3 {
1800 2
1801 } else {
1802 self.version
1803 };
1804 let estimate = self.estimate_body_capacity(write_version, config);
1805 out.buf.reserve(if out.sink.is_some() {
1806 estimate.min(INDEX_WRITE_CHUNK + INDEX_WRITE_CHUNK_SLACK)
1807 } else {
1808 estimate
1809 });
1810 out.buf.extend_from_slice(b"DIRC");
1812 out.buf.extend_from_slice(&write_version.to_be_bytes());
1813 out.buf
1814 .extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1815
1816 let ieot_blocks = ieot_block_count_for_write(policy, config, self.entries.len());
1817 let ieot_stride = ieot_blocks.map(|b| entries_per_ieot_block(self.entries.len(), b));
1818 let mut ieot_table = IeotTable {
1819 entries: Vec::new(),
1820 };
1821 let mut nr_in_block = 0u32;
1822 let mut block_start_offset = out.position();
1823
1824 if write_version == 4 {
1825 let mut previous_path: Vec<u8> = Vec::new();
1826 for (i, entry) in self.entries.iter().enumerate() {
1827 if let Some(stride) = ieot_stride {
1828 if i > 0 && i % stride == 0 {
1829 ieot_table.entries.push(IeotEntry {
1830 offset: block_start_offset as u32,
1831 nr: nr_in_block,
1832 });
1833 nr_in_block = 0;
1834 if !previous_path.is_empty() {
1837 previous_path[0] = 0;
1838 }
1839 block_start_offset = out.position();
1840 }
1841 }
1842 serialize_entry_v4(entry, &mut previous_path, &mut out.buf);
1843 nr_in_block += 1;
1844 out.drain_if_full()?;
1845 }
1846 } else {
1847 for (i, entry) in self.entries.iter().enumerate() {
1848 if let Some(stride) = ieot_stride {
1849 if i > 0 && i % stride == 0 {
1850 ieot_table.entries.push(IeotEntry {
1851 offset: block_start_offset as u32,
1852 nr: nr_in_block,
1853 });
1854 nr_in_block = 0;
1855 block_start_offset = out.position();
1856 }
1857 }
1858 serialize_entry(entry, write_version, &mut out.buf);
1859 nr_in_block += 1;
1860 out.drain_if_full()?;
1861 }
1862 }
1863 if ieot_blocks.is_some() && nr_in_block > 0 {
1864 ieot_table.entries.push(IeotEntry {
1865 offset: block_start_offset as u32,
1866 nr: nr_in_block,
1867 });
1868 }
1869
1870 let extension_offset = out.position();
1871 let write_eoie = record_end_of_index_entries(config);
1872 let mut eoie_header_bytes: Option<Vec<u8>> = write_eoie.then(Vec::new);
1873
1874 if !ieot_table.entries.is_empty() {
1875 let mut payload = Vec::new();
1876 write_ieot_payload(&ieot_table, &mut payload);
1877 append_index_extension(
1878 &mut out.buf,
1879 &mut eoie_header_bytes,
1880 INDEX_EXT_IEOT,
1881 &payload,
1882 );
1883 }
1884 if self.sparse_directories {
1885 append_index_extension(
1886 &mut out.buf,
1887 &mut eoie_header_bytes,
1888 INDEX_EXT_SPARSE_DIRECTORIES,
1889 &[],
1890 );
1891 }
1892 if let Some(uc) = &self.untracked_cache {
1893 let payload = untracked_cache::write_untracked_extension(uc);
1894 append_index_extension(
1895 &mut out.buf,
1896 &mut eoie_header_bytes,
1897 INDEX_EXT_UNTRACKED,
1898 &payload,
1899 );
1900 }
1901 self.append_fsmonitor_extension(&mut out.buf, &mut eoie_header_bytes)?;
1902 if let Some(ru) = &self.resolve_undo {
1903 let payload = write_resolve_undo_payload(ru);
1904 if !payload.is_empty() {
1905 append_index_extension(
1906 &mut out.buf,
1907 &mut eoie_header_bytes,
1908 INDEX_EXT_RESOLVE_UNDO,
1909 &payload,
1910 );
1911 }
1912 }
1913 if let Some(sl) = &self.split_link {
1914 use crate::ewah_bitmap::EwahBitmap;
1915 let del = sl
1916 .delete_bitmap
1917 .as_ref()
1918 .cloned()
1919 .unwrap_or_else(EwahBitmap::new);
1920 let rep = sl
1921 .replace_bitmap
1922 .as_ref()
1923 .cloned()
1924 .unwrap_or_else(EwahBitmap::new);
1925 let payload =
1926 crate::split_index::serialize_link_extension_payload(&sl.base_oid, &del, &rep);
1927 append_index_extension(
1928 &mut out.buf,
1929 &mut eoie_header_bytes,
1930 INDEX_EXT_LINK,
1931 &payload,
1932 );
1933 }
1934 if let Some(cache_tree) = &self.cache_tree {
1935 let mut payload = Vec::new();
1936 serialize_cache_tree_node(cache_tree, &mut payload);
1937 append_index_extension(
1938 &mut out.buf,
1939 &mut eoie_header_bytes,
1940 INDEX_EXT_CACHE_TREE,
1941 &payload,
1942 );
1943 }
1944 if write_eoie {
1945 let mut eoie_payload = Vec::new();
1946 eoie_payload.extend_from_slice(&(extension_offset as u32).to_be_bytes());
1947 let headers = eoie_header_bytes.as_deref().unwrap_or(&[]);
1948 eoie_payload.extend_from_slice(&self.hash_algo.checksum_digest(headers));
1949 append_index_extension(&mut out.buf, &mut None, INDEX_EXT_EOIE, &eoie_payload);
1950 }
1951 out.drain()?;
1952 Ok(())
1953 }
1954
1955 fn append_fsmonitor_extension(
1956 &self,
1957 out: &mut Vec<u8>,
1958 eoie_headers: &mut Option<Vec<u8>>,
1959 ) -> Result<()> {
1960 if let Some(raw) = &self.fsmonitor_raw_extension {
1961 if self.fsmonitor_raw_still_valid() {
1962 append_index_extension(out, eoie_headers, INDEX_EXT_FSMONITOR, raw);
1963 return Ok(());
1964 }
1965 }
1966 let Some(token) = &self.fsmonitor_last_update else {
1967 return Ok(());
1968 };
1969 let dirty = self
1970 .fsmonitor_dirty
1971 .as_ref()
1972 .cloned()
1973 .unwrap_or_else(|| self.build_fsmonitor_dirty_from_entries());
1974 if dirty.bit_size > self.fsmonitor_active_entry_count() {
1975 return Ok(());
1976 }
1977 let mut payload = Vec::new();
1978 payload.extend_from_slice(&FSMONITOR_EXT_VERSION2.to_be_bytes());
1979 payload.extend_from_slice(token.as_bytes());
1980 payload.push(0);
1981 let ewah_size_fixup = payload.len();
1982 payload.extend_from_slice(&0u32.to_be_bytes());
1983 let ewah_start = payload.len();
1984 dirty.serialize(&mut payload);
1985 let ewah_size = (payload.len() - ewah_start) as u32;
1986 payload[ewah_size_fixup..ewah_size_fixup + 4].copy_from_slice(&ewah_size.to_be_bytes());
1987 append_index_extension(out, eoie_headers, INDEX_EXT_FSMONITOR, &payload);
1988 Ok(())
1989 }
1990
1991 fn fsmonitor_active_entry_count(&self) -> usize {
1993 self.entries.len()
1994 }
1995
1996 fn build_fsmonitor_dirty_from_entries(&self) -> crate::ewah_bitmap::EwahBitmap {
1998 use crate::ewah_bitmap::EwahBitmap;
1999 let mut dirty = EwahBitmap::new();
2000 if self.fsmonitor_last_update.is_none() {
2001 return dirty;
2002 }
2003 #[cfg(test)]
2004 tests::fsmonitor_rebuild_counter::note_build();
2005 for (i, entry) in self.entries.iter().enumerate() {
2006 if !entry.fsmonitor_valid() {
2007 dirty.set_bit_extend(i);
2008 }
2009 }
2010 dirty
2011 }
2012
2013 fn fsmonitor_raw_still_valid(&self) -> bool {
2015 use crate::ewah_bitmap::ewah_bitmaps_equal;
2016 let Some(raw) = self.fsmonitor_raw_extension.as_ref() else {
2017 return false;
2018 };
2019 let Some(token) = self.fsmonitor_last_update.as_deref() else {
2020 return false;
2021 };
2022 let Ok((raw_token, raw_dirty)) = parse_fsmonitor_extension(raw) else {
2023 return false;
2024 };
2025 if raw_token != token {
2026 return false;
2027 }
2028 let current = self.build_fsmonitor_dirty_from_entries();
2029 if !ewah_bitmaps_equal(&raw_dirty, ¤t) {
2030 return false;
2031 }
2032 current.bit_size <= self.fsmonitor_active_entry_count()
2033 }
2034
2035 fn rebuild_fsmonitor_dirty_from_entries(&mut self) {
2037 if self.fsmonitor_last_update.is_none() {
2038 self.fsmonitor_dirty = None;
2039 return;
2040 }
2041 self.fsmonitor_dirty = Some(self.build_fsmonitor_dirty_from_entries());
2042 }
2043
2044 fn invalidate_fsmonitor_bitmap(&mut self) {
2046 if self.fsmonitor_last_update.is_none() {
2047 return;
2048 }
2049 self.fsmonitor_raw_extension = None;
2050 self.fsmonitor_dirty = None;
2051 }
2052
2053 pub fn set_fsmonitor_last_update(&mut self, token: Option<String>) {
2055 if self.fsmonitor_last_update.as_deref() != token.as_deref() {
2056 self.fsmonitor_raw_extension = None;
2057 }
2058 self.fsmonitor_last_update = token;
2059 self.rebuild_fsmonitor_dirty_from_entries();
2060 }
2061
2062 pub fn clear_fsmonitor_extension(&mut self) {
2064 self.fsmonitor_last_update = None;
2065 self.fsmonitor_dirty = None;
2066 self.fsmonitor_raw_extension = None;
2067 }
2068
2069 pub fn sync_fsmonitor_dirty_bitmap(&mut self) {
2071 self.rebuild_fsmonitor_dirty_from_entries();
2072 }
2073
2074 pub fn fsmonitor_validity_changed(&mut self) {
2076 self.invalidate_fsmonitor_bitmap();
2077 }
2078
2079 pub(crate) fn invalidate_all_fsmonitor_valid(&mut self) {
2081 for entry in &mut self.entries {
2082 if entry.stage() == 0 {
2083 entry.set_fsmonitor_valid(false);
2084 }
2085 }
2086 self.fsmonitor_validity_changed();
2087 }
2088
2089 pub(crate) fn invalidate_fsmonitor_for_path(&mut self, path: &str, ignorecase: bool) {
2091 if let Some(uc) = self.untracked_cache.as_mut() {
2092 crate::untracked_cache::invalidate_path(uc, path);
2093 }
2094 let mut invalidated = if path.ends_with('/') {
2095 self.invalidate_fsmonitor_directory_cone(path.trim_end_matches('/'), ignorecase)
2096 } else {
2097 self.invalidate_fsmonitor_unqualified_path(path, ignorecase)
2098 };
2099 if !invalidated && ignorecase {
2100 invalidated = self.invalidate_fsmonitor_icase_fallback(path);
2101 }
2102 if invalidated {
2103 self.fsmonitor_validity_changed();
2104 }
2105 }
2106
2107 fn invalidate_fsmonitor_unqualified_path(&mut self, path: &str, ignorecase: bool) -> bool {
2108 if let Some(entry) = self.get_mut(path.as_bytes(), 0) {
2109 entry.set_fsmonitor_valid(false);
2110 return true;
2111 }
2112 if self.has_stage0_descendant_under(path) {
2113 return self.invalidate_fsmonitor_directory_cone(path, ignorecase);
2114 }
2115 false
2116 }
2117
2118 fn invalidate_fsmonitor_directory_cone(&mut self, prefix: &str, ignorecase: bool) -> bool {
2119 if prefix.is_empty() {
2120 self.invalidate_all_fsmonitor_valid();
2121 return true;
2122 }
2123 let prefix_slash = format!("{prefix}/");
2124 let mut any = false;
2125 for entry in &mut self.entries {
2126 if entry.stage() != 0 {
2127 continue;
2128 }
2129 let Some(rel) = std::str::from_utf8(&entry.path).ok() else {
2130 continue;
2131 };
2132 if Self::fsmonitor_path_under_prefix(rel, prefix, &prefix_slash, ignorecase) {
2133 entry.set_fsmonitor_valid(false);
2134 any = true;
2135 }
2136 }
2137 any
2138 }
2139
2140 fn invalidate_fsmonitor_icase_fallback(&mut self, path: &str) -> bool {
2141 let is_dir = path.ends_with('/');
2142 let prefix = path.trim_end_matches('/');
2143 let prefix_slash = format!("{prefix}/");
2144 let mut any = false;
2145 for entry in &mut self.entries {
2146 if entry.stage() != 0 {
2147 continue;
2148 }
2149 let Some(rel) = std::str::from_utf8(&entry.path).ok() else {
2150 continue;
2151 };
2152 let matches = if is_dir {
2153 Self::fsmonitor_path_under_prefix(rel, prefix, &prefix_slash, true)
2154 } else {
2155 rel.eq_ignore_ascii_case(prefix)
2156 };
2157 if matches {
2158 entry.set_fsmonitor_valid(false);
2159 any = true;
2160 }
2161 }
2162 any
2163 }
2164
2165 fn fsmonitor_path_under_prefix(
2166 rel: &str,
2167 prefix: &str,
2168 prefix_slash: &str,
2169 ignorecase: bool,
2170 ) -> bool {
2171 if ignorecase {
2172 rel.eq_ignore_ascii_case(prefix)
2173 || (rel.len() > prefix.len()
2174 && rel.as_bytes().get(prefix.len()) == Some(&b'/')
2175 && rel[..prefix.len()].eq_ignore_ascii_case(prefix))
2176 } else {
2177 rel == prefix || rel.starts_with(prefix_slash)
2178 }
2179 }
2180
2181 fn touch_fsmonitor_after_entry_change(&mut self) {
2183 self.invalidate_fsmonitor_bitmap();
2184 }
2185
2186 #[must_use]
2188 pub fn entries(&self) -> &[IndexEntry] {
2189 &self.entries
2190 }
2191
2192 pub fn entries_mut(&mut self) -> &mut Vec<IndexEntry> {
2194 self.entries_sorted = false;
2195 &mut self.entries
2196 }
2197
2198 pub fn mark_entries_unsorted(&mut self) {
2200 self.entries_sorted = false;
2201 }
2202
2203 pub(crate) fn push_entry_unsorted(&mut self, entry: IndexEntry) {
2205 self.entries.push(entry);
2206 self.entries_sorted = false;
2207 }
2208
2209 #[must_use]
2211 pub fn entries_are_sorted(&self) -> bool {
2212 self.entries
2213 .windows(2)
2214 .all(|pair| (&pair[0].path, pair[0].stage()) <= (&pair[1].path, pair[1].stage()))
2215 }
2216
2217 fn cmp_paths(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
2219 record_path_cmp_for_tests();
2220 a.cmp(b)
2221 }
2222
2223 fn entry_matches(entry: &IndexEntry, path: &[u8], stage: u8) -> bool {
2224 entry.path == path && entry.stage() == stage
2225 }
2226
2227 pub(crate) fn find_entry_pos(&self, path: &[u8], stage: u8) -> Option<usize> {
2229 if self.entries_sorted {
2230 return match self.entries.binary_search_by(|e| {
2231 Self::cmp_paths(e.path.as_slice(), path).then_with(|| e.stage().cmp(&stage))
2232 }) {
2233 Ok(pos) if Self::entry_matches(&self.entries[pos], path, stage) => Some(pos),
2234 _ => None,
2235 };
2236 }
2237 self.entries
2238 .iter()
2239 .position(|e| Self::entry_matches(e, path, stage))
2240 }
2241
2242 fn path_run_range_sorted(&self, path: &[u8]) -> (usize, usize) {
2244 debug_assert!(self.entries_sorted);
2245 let start = self
2246 .entries
2247 .partition_point(|e| Self::cmp_paths(e.path.as_slice(), path).is_lt());
2248 let end = start
2249 + self.entries[start..]
2250 .iter()
2251 .take_while(|e| e.path == path)
2252 .count();
2253 (start, end)
2254 }
2255
2256 #[must_use]
2259 pub fn has_stage0_descendant_under(&self, rel_dir: &str) -> bool {
2260 if rel_dir.is_empty() {
2261 return self.entries.iter().any(|e| e.stage() == 0);
2262 }
2263 if self.entries_sorted {
2264 let prefix = rel_dir.as_bytes();
2265 let start = self.descendant_run_start_sorted(prefix);
2266 return self.entries[start..]
2267 .iter()
2268 .take_while(|e| path_is_under_dir(&e.path, prefix))
2269 .any(|e| e.stage() == 0);
2270 }
2271 let prefix = format!("{rel_dir}/");
2272 self.entries
2273 .iter()
2274 .any(|e| e.stage() == 0 && e.path.starts_with(prefix.as_bytes()))
2275 }
2276
2277 pub(crate) fn sorted_entries_under(&self, dir: &[u8]) -> Option<&[IndexEntry]> {
2282 if !self.entries_sorted {
2283 return None;
2284 }
2285 if dir.is_empty() {
2286 return Some(&self.entries);
2287 }
2288 let (start, end) = self.descendant_run_range_sorted(dir);
2289 Some(&self.entries[start..end])
2290 }
2291
2292 pub(crate) fn sorted_contains_path(&self, path: &[u8]) -> Option<bool> {
2295 self.entries_sorted.then(|| {
2296 self.entries
2297 .binary_search_by(|e| Self::cmp_paths(e.path.as_slice(), path))
2298 .is_ok()
2299 })
2300 }
2301
2302 fn descendant_run_range_sorted(&self, path: &[u8]) -> (usize, usize) {
2304 let start = self.descendant_run_start_sorted(path);
2305 let len = self.entries[start..]
2306 .iter()
2307 .take_while(|e| path_is_under_dir(&e.path, path))
2308 .count();
2309 (start, start + len)
2310 }
2311
2312 fn descendant_run_start_sorted(&self, path: &[u8]) -> usize {
2314 debug_assert!(self.entries_sorted);
2315 let mut prefix_slash = Vec::with_capacity(path.len() + 1);
2316 prefix_slash.extend_from_slice(path);
2317 prefix_slash.push(b'/');
2318 self.entries.partition_point(|e| {
2319 Self::cmp_paths(e.path.as_slice(), prefix_slash.as_slice()).is_lt()
2320 })
2321 }
2322
2323 fn record_resolve_undo_for_path(&mut self, path: &[u8]) {
2324 for e in &self.entries {
2325 if e.path == path && e.stage() != 0 {
2326 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
2327 }
2328 }
2329 }
2330
2331 fn record_resolve_undo_for_run(&mut self, start: usize, end: usize) {
2332 for e in &self.entries[start..end] {
2333 if e.stage() != 0 {
2334 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
2335 }
2336 }
2337 }
2338
2339 pub(crate) fn drop_derived_index_extensions(&mut self) {
2341 self.cache_tree = None;
2342 self.cache_tree_root = None;
2343 self.untracked_cache = None;
2344 }
2345
2346 pub(crate) fn invalidate_cache_tree_for_bulk_checkout<'a>(
2348 &mut self,
2349 dir_prefixes: &BTreeSet<String>,
2350 paths: impl IntoIterator<Item = &'a [u8]>,
2351 replacements: &[IndexEntry],
2352 ) {
2353 if self.cache_tree.is_none() {
2354 return;
2355 }
2356 for dir in dir_prefixes {
2357 self.invalidate_cache_tree_for_path(dir.as_bytes());
2358 }
2359 for path in paths {
2360 self.invalidate_cache_tree_for_path(path);
2361 }
2362 for entry in replacements {
2363 self.invalidate_cache_tree_for_path(&entry.path);
2364 }
2365 }
2366
2367 fn invalidate_after_path_removal(&mut self, path: &[u8]) {
2368 if let Ok(p) = std::str::from_utf8(path) {
2369 self.invalidate_untracked_cache_for_path(p);
2370 }
2371 self.invalidate_cache_tree_for_path(path);
2372 }
2373
2374 pub(crate) fn remove_prefixes_paths_and_insert<'a>(
2376 &mut self,
2377 dir_prefixes: &BTreeSet<String>,
2378 paths: impl IntoIterator<Item = &'a [u8]>,
2379 replacements: impl IntoIterator<Item = IndexEntry>,
2380 ) {
2381 let mut replacements: Vec<IndexEntry> = replacements.into_iter().collect();
2382 if !replacements.is_empty() {
2383 replacements
2384 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
2385 dedup_sorted_replacement_entries(&mut replacements);
2386 }
2387
2388 let mut paths: Vec<&[u8]> = paths.into_iter().collect();
2389 paths.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2390 paths.dedup_by(|a, b| a == b);
2391
2392 let mut prefix_bytes: Vec<&[u8]> = dir_prefixes.iter().map(|d| d.as_bytes()).collect();
2393 prefix_bytes.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2394 prefix_bytes.dedup_by(|a, b| a == b);
2395
2396 if dir_prefixes.is_empty() && paths.is_empty() {
2397 self.merge_replacements_into_sorted_index(replacements);
2398 return;
2399 }
2400
2401 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2402 let mut kept = Vec::with_capacity(self.entries.len());
2403 let skip_per_path_extension_inval = self.defer_untracked_cache_invalidation;
2404
2405 if self.entries_sorted {
2406 let mut path_idx = 0usize;
2407 for entry in self.entries.drain(..) {
2408 let ep = entry.path.as_slice();
2409 while path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_lt() {
2410 path_idx += 1;
2411 }
2412 let remove = (path_idx < paths.len()
2413 && Self::cmp_paths(paths[path_idx], ep).is_eq())
2414 || Self::entry_under_any_directory_prefix(ep, &prefix_bytes);
2415 if remove {
2416 if entry.stage() != 0 {
2417 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2418 }
2419 if !skip_per_path_extension_inval
2420 && invalidated_paths.last().is_none_or(|p| p.as_slice() != ep)
2421 {
2422 invalidated_paths.push(entry.path.clone());
2423 }
2424 } else {
2425 kept.push(entry);
2426 }
2427 }
2428 let repl_for_inval: Vec<Vec<u8>> = if skip_per_path_extension_inval {
2429 Vec::new()
2430 } else {
2431 replacements
2432 .iter()
2433 .filter(|e| e.stage() == 0)
2434 .map(|e| e.path.clone())
2435 .collect()
2436 };
2437 self.entries = merge_sorted_index_entries(kept, replacements);
2438 self.entries_sorted = true;
2439 if skip_per_path_extension_inval {
2440 self.untracked_cache = None;
2441 self.touch_fsmonitor_after_entry_change();
2442 return;
2443 }
2444 const BULK_INDEX_EXTENSION_INVALIDATE_MIN: usize = 64;
2445 let mut invalidations = dir_prefixes.len();
2446 for path in &invalidated_paths {
2447 if dir_prefixes
2448 .iter()
2449 .all(|d| !Self::entry_under_directory_prefix(path.as_slice(), d.as_bytes()))
2450 {
2451 invalidations += 1;
2452 }
2453 }
2454 invalidations += repl_for_inval.len();
2455 if invalidations >= BULK_INDEX_EXTENSION_INVALIDATE_MIN {
2456 self.drop_derived_index_extensions();
2457 } else {
2458 for dir in dir_prefixes {
2459 self.invalidate_after_path_removal(dir.as_bytes());
2460 }
2461 for path in invalidated_paths {
2462 if dir_prefixes
2463 .iter()
2464 .all(|d| !Self::entry_under_directory_prefix(path.as_slice(), d.as_bytes()))
2465 {
2466 self.invalidate_after_path_removal(&path);
2467 }
2468 }
2469 for path in repl_for_inval {
2470 if let Ok(p) = std::str::from_utf8(&path) {
2471 self.invalidate_untracked_cache_for_path(p);
2472 }
2473 self.invalidate_cache_tree_for_path(&path);
2474 }
2475 }
2476 self.touch_fsmonitor_after_entry_change();
2477 return;
2478 }
2479 if !dir_prefixes.is_empty() {
2480 self.remove_directory_prefixes(dir_prefixes);
2481 }
2482 self.remove_paths_and_insert(paths, replacements);
2483 }
2484
2485 pub(crate) fn remove_directory_prefixes(&mut self, dirs: &BTreeSet<String>) {
2487 if dirs.is_empty() {
2488 return;
2489 }
2490 let mut prefixes: Vec<&[u8]> = dirs.iter().map(|d| d.as_bytes()).collect();
2491 prefixes.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2492 prefixes.dedup_by(|a, b| a == b);
2493
2494 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2495 let mut kept = Vec::with_capacity(self.entries.len());
2496
2497 if self.entries_sorted {
2498 for entry in self.entries.drain(..) {
2499 let ep = entry.path.as_slice();
2500 let remove = Self::entry_under_any_directory_prefix(ep, &prefixes);
2501 if remove {
2502 if entry.stage() != 0 {
2503 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2504 }
2505 if invalidated_paths.last().is_none_or(|p| p.as_slice() != ep) {
2506 invalidated_paths.push(entry.path.clone());
2507 }
2508 } else {
2509 kept.push(entry);
2510 }
2511 }
2512 self.entries = kept;
2513 } else {
2514 for entry in self.entries.drain(..) {
2515 let remove =
2516 Self::entry_under_any_directory_prefix(entry.path.as_slice(), &prefixes);
2517 if remove {
2518 if entry.stage() != 0 {
2519 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2520 }
2521 if invalidated_paths
2522 .last()
2523 .is_none_or(|p| p.as_slice() != entry.path)
2524 {
2525 invalidated_paths.push(entry.path.clone());
2526 }
2527 } else {
2528 kept.push(entry);
2529 }
2530 }
2531 self.entries = kept;
2532 }
2533 if invalidated_paths.is_empty() {
2534 return;
2535 }
2536 for dir in dirs {
2537 self.invalidate_after_path_removal(dir.as_bytes());
2538 }
2539 self.touch_fsmonitor_after_entry_change();
2540 }
2541
2542 fn entry_under_directory_prefix(path: &[u8], prefix: &[u8]) -> bool {
2543 if path.len() < prefix.len() {
2544 return false;
2545 }
2546 if path[..prefix.len()] != *prefix {
2547 return false;
2548 }
2549 path.len() == prefix.len() || path[prefix.len()] == b'/'
2550 }
2551
2552 fn entry_under_any_directory_prefix(path: &[u8], prefixes: &[&[u8]]) -> bool {
2553 prefixes
2554 .iter()
2555 .any(|prefix| Self::entry_under_directory_prefix(path, prefix))
2556 }
2557
2558 pub fn remove_paths<'a>(&mut self, paths: impl IntoIterator<Item = &'a [u8]>) {
2560 use std::collections::BTreeSet;
2561
2562 let mut paths: Vec<&[u8]> = paths.into_iter().collect();
2563 if paths.is_empty() {
2564 return;
2565 }
2566 paths.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2567 paths.dedup_by(|a, b| a == b);
2568
2569 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2570 let mut kept = Vec::with_capacity(self.entries.len());
2571
2572 if self.entries_sorted {
2573 let mut path_idx = 0usize;
2574 for entry in self.entries.drain(..) {
2575 let ep = entry.path.as_slice();
2576 while path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_lt() {
2577 path_idx += 1;
2578 }
2579 let remove = path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_eq();
2580 if remove {
2581 if entry.stage() != 0 {
2582 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2583 }
2584 if invalidated_paths.last().is_none_or(|p| p.as_slice() != ep) {
2585 invalidated_paths.push(entry.path.clone());
2586 }
2587 } else {
2588 kept.push(entry);
2589 }
2590 }
2591 self.entries = kept;
2592 } else {
2593 let remove_set: BTreeSet<&[u8]> = paths.into_iter().collect();
2594 for entry in self.entries.drain(..) {
2595 if remove_set.contains(entry.path.as_slice()) {
2596 if entry.stage() != 0 {
2597 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2598 }
2599 if invalidated_paths
2600 .last()
2601 .is_none_or(|p| p.as_slice() != entry.path.as_slice())
2602 {
2603 invalidated_paths.push(entry.path.clone());
2604 }
2605 } else {
2606 kept.push(entry);
2607 }
2608 }
2609 self.entries = kept;
2610 }
2611
2612 for path in invalidated_paths {
2613 self.invalidate_after_path_removal(&path);
2614 }
2615 self.touch_fsmonitor_after_entry_change();
2616 }
2617
2618 pub(crate) fn replace_stage0_entries_batch(&mut self, replacements: Vec<IndexEntry>) {
2620 self.merge_replacements_into_sorted_index(replacements);
2621 }
2622
2623 pub(crate) fn mark_content_verified(&mut self, path: &[u8], oid: ObjectId) {
2626 self.content_verified.insert(path.to_vec(), oid);
2627 }
2628
2629 pub(crate) fn clear_content_verified(&mut self) {
2631 self.content_verified.clear();
2632 for entry in &mut self.entries {
2633 entry.take_content_verified();
2634 }
2635 }
2636
2637 pub(crate) fn overwrite_stage0_entries(&mut self, replacements: Vec<IndexEntry>) {
2643 const BULK_INVALIDATE_MIN: usize = 64;
2644 if replacements.is_empty() {
2645 return;
2646 }
2647 let defer_untracked = self.defer_untracked_cache_invalidation;
2648 let drop_caches = !defer_untracked && replacements.len() >= BULK_INVALIDATE_MIN;
2649 let mut unmatched = Vec::new();
2650 for mut entry in replacements {
2651 let pos = if entry.stage() == 0 {
2652 self.find_entry_pos(&entry.path, 0)
2653 } else {
2654 None
2655 };
2656 let Some(pos) = pos else {
2657 unmatched.push(entry);
2658 continue;
2659 };
2660 if !drop_caches {
2661 if !defer_untracked {
2662 if let Ok(p) = std::str::from_utf8(&entry.path) {
2663 self.invalidate_untracked_cache_for_path(p);
2664 }
2665 }
2666 self.invalidate_cache_tree_for_path(&entry.path);
2667 }
2668 entry.base_index_pos = self.entries[pos].base_index_pos;
2669 self.entries[pos] = entry;
2670 }
2671 if drop_caches {
2672 self.cache_tree = None;
2673 self.cache_tree_root = None;
2674 self.untracked_cache = None;
2675 }
2676 self.touch_fsmonitor_after_entry_change();
2677 if !unmatched.is_empty() {
2678 self.merge_replacements_into_sorted_index(unmatched);
2679 }
2680 }
2681
2682 fn merge_sorted_replacements_into_entries(&mut self, mut replacements: Vec<IndexEntry>) {
2684 if replacements.is_empty() {
2685 return;
2686 }
2687 if self.entries.is_empty() {
2688 self.entries = replacements;
2689 return;
2690 }
2691 self.entries.reserve(replacements.len());
2692 let mid = self.entries.len();
2693 self.entries.append(&mut replacements);
2694 merge_sorted_index_adjacent_halves(&mut self.entries, mid);
2695 }
2696
2697 fn merge_replacements_into_sorted_index(&mut self, replacements: Vec<IndexEntry>) {
2699 if replacements.is_empty() {
2700 return;
2701 }
2702 if !self.entries_sorted {
2703 for entry in replacements {
2704 self.add_or_replace(entry);
2705 }
2706 return;
2707 }
2708 const BULK_MERGE_MIN: usize = 64;
2709 if self.defer_untracked_cache_invalidation {
2710 for entry in replacements.iter().filter(|e| e.stage() == 0) {
2713 self.invalidate_cache_tree_for_path(&entry.path);
2714 }
2715 self.merge_sorted_replacements_into_entries(replacements);
2716 self.touch_fsmonitor_after_entry_change();
2717 return;
2718 }
2719 let repl_for_inval: Vec<Vec<u8>> = replacements
2720 .iter()
2721 .filter(|e| e.stage() == 0)
2722 .map(|e| e.path.clone())
2723 .collect();
2724 self.merge_sorted_replacements_into_entries(replacements);
2725 if repl_for_inval.len() >= BULK_MERGE_MIN {
2726 self.cache_tree = None;
2727 self.cache_tree_root = None;
2728 self.untracked_cache = None;
2729 self.touch_fsmonitor_after_entry_change();
2730 return;
2731 }
2732 for path in repl_for_inval {
2733 if let Ok(p) = std::str::from_utf8(&path) {
2734 self.invalidate_untracked_cache_for_path(p);
2735 }
2736 self.invalidate_cache_tree_for_path(&path);
2737 }
2738 self.touch_fsmonitor_after_entry_change();
2739 }
2740
2741 pub fn remove_paths_and_insert<'a>(
2746 &mut self,
2747 paths: impl IntoIterator<Item = &'a [u8]>,
2748 replacements: impl IntoIterator<Item = IndexEntry>,
2749 ) {
2750 let mut replacements: Vec<IndexEntry> = replacements.into_iter().collect();
2751 if replacements.is_empty() {
2752 self.remove_paths(paths);
2753 return;
2754 }
2755 replacements.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
2756 dedup_sorted_replacement_entries(&mut replacements);
2757
2758 let mut paths: Vec<&[u8]> = paths.into_iter().collect();
2759 if paths.is_empty() {
2760 self.merge_replacements_into_sorted_index(replacements);
2761 return;
2762 }
2763 paths.sort_unstable_by(|a, b| Self::cmp_paths(a, b));
2764 paths.dedup_by(|a, b| a == b);
2765
2766 let mut invalidated_paths: Vec<Vec<u8>> = Vec::new();
2767 let mut kept = Vec::with_capacity(self.entries.len());
2768
2769 if self.entries_sorted {
2770 let mut path_idx = 0usize;
2771 for entry in self.entries.drain(..) {
2772 let ep = entry.path.as_slice();
2773 while path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_lt() {
2774 path_idx += 1;
2775 }
2776 let remove = path_idx < paths.len() && Self::cmp_paths(paths[path_idx], ep).is_eq();
2777 if remove {
2778 if entry.stage() != 0 {
2779 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, &entry);
2780 }
2781 if invalidated_paths.last().is_none_or(|p| p.as_slice() != ep) {
2782 invalidated_paths.push(entry.path.clone());
2783 }
2784 } else {
2785 kept.push(entry);
2786 }
2787 }
2788 let repl_for_inval = replacements
2789 .iter()
2790 .filter(|e| e.stage() == 0)
2791 .map(|e| e.path.clone())
2792 .collect::<Vec<_>>();
2793 self.entries = merge_sorted_index_entries(kept, replacements);
2794 self.entries_sorted = true;
2795 const BULK_INDEX_EXTENSION_INVALIDATE_MIN: usize = 64;
2796 let invalidations = invalidated_paths.len() + repl_for_inval.len();
2797 if invalidations >= BULK_INDEX_EXTENSION_INVALIDATE_MIN {
2798 self.drop_derived_index_extensions();
2799 } else {
2800 for path in invalidated_paths {
2801 self.invalidate_after_path_removal(&path);
2802 }
2803 for path in repl_for_inval {
2804 if let Ok(p) = std::str::from_utf8(&path) {
2805 self.invalidate_untracked_cache_for_path(p);
2806 }
2807 self.invalidate_cache_tree_for_path(&path);
2808 }
2809 }
2810 self.touch_fsmonitor_after_entry_change();
2811 return;
2812 }
2813 self.remove_paths(paths);
2814 for entry in replacements {
2815 self.add_or_replace(entry);
2816 }
2817 }
2818
2819 pub fn add_or_replace(&mut self, entry: IndexEntry) {
2821 let path = entry.path.clone();
2822 let stage = entry.stage();
2823 let mut inserted_stage0 = false;
2824 if self.entries_sorted {
2825 let result = self.entries.binary_search_by(|e| {
2826 e.path
2827 .as_slice()
2828 .cmp(path.as_slice())
2829 .then_with(|| e.stage().cmp(&stage))
2830 });
2831 match result {
2832 Ok(pos) => {
2833 let mut e = entry;
2834 e.base_index_pos = self.entries[pos].base_index_pos;
2835 self.entries[pos] = e;
2836 }
2837 Err(pos) => {
2838 self.entries.insert(pos, entry);
2839 inserted_stage0 = stage == 0;
2840 }
2841 }
2842 } else if let Some(pos) = self
2843 .entries
2844 .iter()
2845 .position(|e| e.path == path && e.stage() == stage)
2846 {
2847 let mut e = entry;
2848 e.base_index_pos = self.entries[pos].base_index_pos;
2849 self.entries[pos] = e;
2850 } else {
2851 self.entries.push(entry);
2852 inserted_stage0 = stage == 0;
2853 }
2854 if inserted_stage0 {
2855 if let Ok(p) = std::str::from_utf8(&path) {
2856 self.invalidate_untracked_cache_for_path(p);
2857 }
2858 }
2859 if stage == 0 {
2860 self.invalidate_cache_tree_for_path(&path);
2861 }
2862 self.touch_fsmonitor_after_entry_change();
2863 }
2864
2865 #[must_use]
2867 pub fn has_unmerged_entries(&self) -> bool {
2868 self.entries.iter().any(|e| e.stage() != 0)
2869 }
2870
2871 pub fn stage_file(&mut self, entry: IndexEntry) {
2875 let path = entry.path.clone();
2876 if self.entries_sorted {
2877 let (start, end) = self.path_run_range_sorted(&path);
2878 for i in (start..end).rev() {
2879 if self.entries[i].stage() != 0 {
2880 resolve_undo::record_resolve_undo_for_entry(
2881 &mut self.resolve_undo,
2882 &self.entries[i],
2883 );
2884 self.entries.remove(i);
2885 }
2886 }
2887 } else {
2888 for e in &self.entries {
2889 if e.path == path && e.stage() != 0 {
2890 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
2891 }
2892 }
2893 self.entries.retain(|e| e.path != path || e.stage() == 0);
2894 }
2895 self.add_or_replace(entry);
2896 }
2897
2898 pub fn clear_resolve_undo(&mut self) {
2900 self.resolve_undo = None;
2901 }
2902
2903 pub fn take_resolve_undo_record(&mut self, path: &[u8]) -> Option<ResolveUndoRecord> {
2905 let map = self.resolve_undo.as_mut()?;
2906 let ru = map.remove(path)?;
2907 if map.is_empty() {
2908 self.resolve_undo = None;
2909 }
2910 Some(ru)
2911 }
2912
2913 pub fn install_unmerged_from_resolve_undo(&mut self, path: &[u8], record: &ResolveUndoRecord) {
2915 if self.entries_sorted {
2916 let (start, end) = self.path_run_range_sorted(path);
2917 if start < end {
2918 self.entries.drain(start..end);
2919 }
2920 } else {
2921 self.entries.retain(|e| e.path != path);
2922 }
2923 for stage in 1u8..=3u8 {
2924 let i = (stage - 1) as usize;
2925 if record.modes[i] == 0 {
2926 continue;
2927 }
2928 let entry = IndexEntry {
2929 ctime_sec: 0,
2930 ctime_nsec: 0,
2931 mtime_sec: 0,
2932 mtime_nsec: 0,
2933 dev: 0,
2934 ino: 0,
2935 mode: record.modes[i],
2936 uid: 0,
2937 gid: 0,
2938 size: 0,
2939 oid: record.oids[i],
2940 flags: path.len().min(0xFFF) as u16 | ((stage as u16) << 12),
2941 flags_extended: None,
2942 path: path.to_vec(),
2943 base_index_pos: 0,
2944 };
2945 self.add_or_replace(entry);
2946 }
2947 }
2948
2949 pub fn unmerge_path_from_resolve_undo(&mut self, path: &[u8]) -> bool {
2953 let Some(record) = self.take_resolve_undo_record(path) else {
2954 return false;
2955 };
2956 self.install_unmerged_from_resolve_undo(path, &record);
2957 if let Ok(p) = std::str::from_utf8(path) {
2964 self.invalidate_untracked_cache_for_path(p);
2965 }
2966 self.invalidate_cache_tree_for_path(path);
2967 true
2968 }
2969
2970 pub fn remove(&mut self, path: &[u8]) -> bool {
2974 if self.entries_sorted {
2975 let (start, end) = self.path_run_range_sorted(path);
2976 if start < end {
2977 self.record_resolve_undo_for_run(start, end);
2978 self.entries.drain(start..end);
2979 self.invalidate_after_path_removal(path);
2980 self.touch_fsmonitor_after_entry_change();
2981 return true;
2982 }
2983 if !self.entries.iter().any(|e| e.path == path) {
2984 return false;
2985 }
2986 self.entries_sorted = false;
2987 }
2988 self.record_resolve_undo_for_path(path);
2989 let before = self.entries.len();
2990 self.entries.retain(|e| e.path != path);
2991 if self.entries.len() == before {
2992 return false;
2993 }
2994 self.invalidate_after_path_removal(path);
2995 self.touch_fsmonitor_after_entry_change();
2996 true
2997 }
2998
2999 pub fn remove_path_all_stages(&mut self, path: &[u8]) -> bool {
3003 self.remove(path)
3004 }
3005
3006 pub fn invalidate_untracked_cache_for_path(&mut self, path: &str) {
3008 if self.defer_untracked_cache_invalidation {
3009 return;
3010 }
3011 if let Some(uc) = self.untracked_cache.as_mut() {
3012 untracked_cache::invalidate_path(uc, path);
3013 }
3014 }
3015
3016 pub fn remove_descendants_under_path(&mut self, path: &str) {
3022 let prefix = path.as_bytes();
3023 if prefix.is_empty() {
3024 return;
3025 }
3026 let plen = prefix.len();
3027 if self.entries_sorted {
3028 let (start, end) = self.descendant_run_range_sorted(prefix);
3029 if start < end {
3030 self.record_resolve_undo_for_run(start, end);
3031 self.entries.drain(start..end);
3032 self.invalidate_untracked_cache_for_path(path);
3033 self.invalidate_cache_tree_for_path(path.as_bytes());
3034 return;
3035 }
3036 let any = self.entries.iter().any(|e| {
3037 let ep = e.path.as_slice();
3038 ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/'
3039 });
3040 if !any {
3041 return;
3042 }
3043 self.entries_sorted = false;
3044 }
3045 {
3046 let had_descendant = self.entries.iter().any(|e| {
3047 let ep = e.path.as_slice();
3048 ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/'
3049 });
3050 for e in self.entries.iter() {
3051 let ep = e.path.as_slice();
3052 if ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/' && e.stage() != 0 {
3053 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
3054 }
3055 }
3056 self.entries.retain(|e| {
3057 let ep = e.path.as_slice();
3058 if ep.len() <= plen {
3059 return true;
3060 }
3061 if !ep.starts_with(prefix) {
3062 return true;
3063 }
3064 ep[plen] != b'/'
3065 });
3066 if !had_descendant {
3067 return;
3068 }
3069 }
3070 self.invalidate_untracked_cache_for_path(path);
3071 self.invalidate_cache_tree_for_path(path.as_bytes());
3072 }
3073
3074 pub fn set_cache_tree(&mut self, cache_tree: CacheTreeNode) {
3076 self.cache_tree_root = cache_tree.oid.filter(|_| cache_tree.entry_count >= 0);
3077 self.cache_tree = Some(cache_tree);
3078 }
3079
3080 pub fn clear_cache_tree(&mut self) {
3082 self.cache_tree_root = None;
3083 self.cache_tree = None;
3084 }
3085
3086 pub fn invalidate_cache_tree_for_path(&mut self, path: &[u8]) {
3096 let Some(root) = self.cache_tree.as_mut() else {
3097 self.cache_tree_root = None;
3098 return;
3099 };
3100 self.cache_tree_root = None;
3101 Self::do_invalidate_cache_tree_path(root, path);
3102 }
3103
3104 fn do_invalidate_cache_tree_path(node: &mut CacheTreeNode, path: &[u8]) {
3107 node.invalidate();
3108 let slash = path.iter().position(|&b| b == b'/');
3109 match slash {
3110 None => {
3112 if !path.is_empty() {
3113 node.children.retain(|child| child.name != path);
3114 }
3115 }
3116 Some(idx) => {
3118 let (component, rest) = (&path[..idx], &path[idx + 1..]);
3119 if let Some(child) = node
3120 .children
3121 .iter_mut()
3122 .find(|child| child.name == component)
3123 {
3124 Self::do_invalidate_cache_tree_path(child, rest);
3125 }
3126 }
3127 }
3128 }
3129
3130 #[must_use]
3132 pub fn format_cache_tree_dump(&self) -> String {
3133 let Some(root) = self.cache_tree.as_ref() else {
3134 return String::new();
3135 };
3136 let mut out = String::new();
3137 format_cache_tree_node(root, "", &mut out);
3138 out
3139 }
3140
3141 pub fn dump_cache_tree(&self, odb: &Odb) -> Result<String> {
3157 let Some(it_root) = self.cache_tree.as_ref() else {
3158 return Ok(String::new());
3159 };
3160 let ref_root = crate::write_tree::build_cache_tree_from_index(odb, self)?;
3161 let mut out = String::new();
3162 dump_cache_tree_pair(it_root, &ref_root, "", &mut out);
3163 Ok(out)
3164 }
3165
3166 pub fn sort(&mut self) {
3168 self.ensure_canonical_order();
3169 }
3170
3171 pub fn ensure_canonical_order(&mut self) {
3176 if self.entries_sorted && self.entries_are_sorted() {
3177 return;
3178 }
3179 self.sort_entries_canonical();
3180 }
3181
3182 fn sort_entries_canonical(&mut self) {
3183 self.entries
3184 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
3185 self.entries_sorted = true;
3186 }
3187
3188 pub fn dedup_paths_keep_last(&mut self) {
3197 let mut seen: std::collections::HashSet<(Vec<u8>, u8)> = std::collections::HashSet::new();
3198 let mut kept: Vec<IndexEntry> = Vec::with_capacity(self.entries.len());
3199 for entry in self.entries.iter().rev() {
3201 if seen.insert((entry.path.clone(), entry.stage())) {
3202 kept.push(entry.clone());
3203 }
3204 }
3205 kept.reverse();
3206 self.entries = kept;
3207 self.entries_sorted = false;
3208 }
3209
3210 #[must_use]
3212 pub fn split_index_base_oid(&self) -> Option<ObjectId> {
3213 self.split_link.as_ref().map(|l| l.base_oid)
3214 }
3215
3216 #[must_use]
3218 pub fn get(&self, path: &[u8], stage: u8) -> Option<&IndexEntry> {
3219 self.find_entry_pos(path, stage)
3220 .map(|pos| &self.entries[pos])
3221 }
3222
3223 pub fn get_mut(&mut self, path: &[u8], stage: u8) -> Option<&mut IndexEntry> {
3225 let pos = self.find_entry_pos(path, stage)?;
3226 let entry = &mut self.entries[pos];
3227 entry.take_content_verified();
3229 Some(entry)
3230 }
3231
3232 pub(crate) fn stage0_path_positions<'a>(
3237 &'a self,
3238 ) -> std::collections::HashMap<Cow<'a, str>, usize> {
3239 let stage0_count = self.entries.iter().filter(|e| e.stage() == 0).count();
3240 let mut positions = std::collections::HashMap::with_capacity(stage0_count);
3241 for (pos, entry) in self.entries.iter().enumerate() {
3242 if entry.stage() == 0 {
3243 let key = index_path_outcome_key(&entry.path);
3244 positions.entry(key).or_insert(pos);
3245 }
3246 }
3247 positions
3248 }
3249
3250 pub fn overlay_tree_on_index(
3269 &mut self,
3270 repo: &Repository,
3271 treeish: &str,
3272 prefix: &[u8],
3273 ) -> Result<()> {
3274 let oid = rev_parse::resolve_revision(repo, treeish)?;
3275 let tree_oid = repo.odb().peel_to_tree(oid)?.ok_or_else(|| {
3276 Error::ObjectNotFound(format!("cannot peel {oid} to tree for --with-tree"))
3277 })?;
3278 for e in self.entries.iter_mut() {
3279 if e.stage() != 0 {
3280 e.set_stage(3);
3281 }
3282 }
3283 self.sort();
3284 let has_stage1 = self.entries.iter().any(|e| e.stage() == 1);
3285 let mut appended: Vec<IndexEntry> = Vec::new();
3286 let protect = crate::path_validate::ProtectOptions::from_config(&*repo.config()?);
3287 read_tree_into_overlay(
3288 repo,
3289 &tree_oid,
3290 prefix,
3291 &[],
3292 has_stage1,
3293 protect,
3294 &mut appended,
3295 )?;
3296 for e in appended {
3297 self.add_or_replace(e);
3298 }
3299 if !has_stage1 {
3300 self.sort();
3301 }
3302 let mut last_stage0: Option<&[u8]> = None;
3303 for e in &mut self.entries {
3304 match e.stage() {
3305 0 => {
3306 last_stage0 = Some(e.path.as_slice());
3307 }
3308 1 if last_stage0.is_some_and(|p| p == e.path.as_slice()) => {
3309 e.set_overlay_tree_skip_output(true);
3310 }
3311 _ => {}
3312 }
3313 }
3314 Ok(())
3315 }
3316}
3317
3318#[allow(clippy::too_many_arguments)]
3319fn read_tree_into_overlay(
3320 repo: &Repository,
3321 tree_oid: &ObjectId,
3322 prefix: &[u8],
3323 rel_base: &[u8],
3324 use_replace_path: bool,
3325 protect: crate::path_validate::ProtectOptions,
3326 out: &mut Vec<IndexEntry>,
3327) -> Result<()> {
3328 let obj = repo.odb().read(tree_oid)?;
3329 if obj.kind != ObjectKind::Tree {
3330 return Err(Error::ObjectNotFound(format!(
3331 "object {tree_oid} is not a tree"
3332 )));
3333 }
3334 let entries = parse_tree(&obj.data)?;
3335 for TreeEntry { mode, name, oid } in entries {
3336 let mut path = rel_base.to_vec();
3337 if !path.is_empty() {
3338 path.push(b'/');
3339 }
3340 path.extend_from_slice(&name);
3341 if !prefix_under_or_equal(prefix, &path) {
3342 continue;
3343 }
3344 crate::path_population::validate_tree_walk_entry(rel_base, &name, mode, protect)?;
3345 if mode == MODE_TREE {
3346 read_tree_into_overlay(repo, &oid, prefix, &path, use_replace_path, protect, out)?;
3347 continue;
3348 }
3349 if mode == MODE_GITLINK {
3350 continue;
3351 }
3352 let entry = synthetic_stage1_index_entry(mode, &path, oid);
3353 if use_replace_path {
3354 if let Some(pos) = out.iter().position(|e| e.path == path && e.stage() == 1) {
3355 out[pos] = entry;
3356 } else {
3357 out.push(entry);
3358 }
3359 } else {
3360 out.push(entry);
3361 }
3362 }
3363 Ok(())
3364}
3365
3366fn dedup_sorted_replacement_entries(replacements: &mut Vec<IndexEntry>) {
3368 if replacements.len() <= 1 {
3369 return;
3370 }
3371 let mut out: Vec<IndexEntry> = Vec::with_capacity(replacements.len());
3372 for entry in replacements.drain(..) {
3373 match out.last_mut() {
3374 Some(last) if last.path == entry.path && last.stage() == entry.stage() => {
3375 *last = entry;
3376 }
3377 _ => out.push(entry),
3378 }
3379 }
3380 *replacements = out;
3381}
3382
3383fn index_entry_path_stage_cmp(a: &IndexEntry, b: &IndexEntry) -> std::cmp::Ordering {
3384 a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage()))
3385}
3386
3387fn merge_sorted_index_entries(
3391 mut kept: Vec<IndexEntry>,
3392 mut insert: Vec<IndexEntry>,
3393) -> Vec<IndexEntry> {
3394 if insert.is_empty() {
3395 return kept;
3396 }
3397 if kept.is_empty() {
3398 return insert;
3399 }
3400 kept.reserve(insert.len());
3401 let mid = kept.len();
3402 kept.append(&mut insert);
3403 merge_sorted_index_adjacent_halves(&mut kept, mid);
3404 kept
3405}
3406
3407fn merge_sorted_index_adjacent_halves(v: &mut Vec<IndexEntry>, mid: usize) {
3409 let n = v.len();
3410 if mid == 0 || mid >= n {
3411 return;
3412 }
3413 let right_len = n - mid;
3414 let smaller_is_right = right_len <= mid;
3415 let mut aux = if smaller_is_right {
3416 v.drain(mid..).collect::<Vec<_>>()
3417 } else {
3418 v.drain(..mid).collect::<Vec<_>>()
3419 };
3420 let left_len = if smaller_is_right { v.len() } else { aux.len() };
3421 let pad = aux.len();
3422 v.reserve(pad);
3423 for _ in 0..pad {
3424 v.push(merge_padding_index_entry());
3425 }
3426 let total = v.len();
3427 merge_backward_into_place(v, left_len, total, &mut aux, smaller_is_right);
3428}
3429
3430fn merge_padding_index_entry() -> IndexEntry {
3432 IndexEntry {
3433 ctime_sec: 0,
3434 ctime_nsec: 0,
3435 mtime_sec: 0,
3436 mtime_nsec: 0,
3437 dev: 0,
3438 ino: 0,
3439 mode: 0,
3440 uid: 0,
3441 gid: 0,
3442 size: 0,
3443 oid: ObjectId::zero(),
3444 flags: 0,
3445 flags_extended: None,
3446 path: Vec::new(),
3447 base_index_pos: 0,
3448 }
3449}
3450
3451fn merge_backward_into_place(
3453 v: &mut Vec<IndexEntry>,
3454 left_len: usize,
3455 total: usize,
3456 aux: &mut Vec<IndexEntry>,
3457 smaller_is_right: bool,
3458) {
3459 let pad = aux.len();
3460 let mut li = left_len;
3461 let mut ri = if smaller_is_right {
3462 aux.len()
3463 } else {
3464 total.saturating_sub(pad)
3465 };
3466 let mut k = total;
3467 while li > 0 && ri > 0 {
3468 k -= 1;
3469 let ord = if smaller_is_right {
3470 index_entry_path_stage_cmp(&v[li - 1], &aux[ri - 1])
3471 } else {
3472 index_entry_path_stage_cmp(&aux[li - 1], &v[ri - 1])
3473 };
3474 match ord {
3475 std::cmp::Ordering::Greater => {
3476 if smaller_is_right {
3477 let entry = std::mem::replace(&mut v[li - 1], merge_padding_index_entry());
3478 drop(std::mem::replace(&mut v[k], entry));
3479 li -= 1;
3480 } else {
3481 let entry = std::mem::replace(&mut aux[li - 1], merge_padding_index_entry());
3482 li -= 1;
3483 drop(std::mem::replace(&mut v[k], entry));
3484 }
3485 }
3486 std::cmp::Ordering::Equal => {
3487 let base = if smaller_is_right {
3488 v[li - 1].base_index_pos
3489 } else {
3490 aux[li - 1].base_index_pos
3491 };
3492 let mut entry = if smaller_is_right {
3493 let Some(e) = aux.pop() else {
3494 break;
3495 };
3496 ri -= 1;
3497 e
3498 } else {
3499 ri -= 1;
3500 std::mem::replace(&mut v[ri], merge_padding_index_entry())
3501 };
3502 entry.base_index_pos = base;
3503 if smaller_is_right {
3504 drop(std::mem::replace(
3505 &mut v[li - 1],
3506 merge_padding_index_entry(),
3507 ));
3508 li -= 1;
3509 } else {
3510 drop(std::mem::replace(
3511 &mut aux[li - 1],
3512 merge_padding_index_entry(),
3513 ));
3514 li -= 1;
3515 }
3516 drop(std::mem::replace(&mut v[k], entry));
3517 }
3518 std::cmp::Ordering::Less => {
3519 if smaller_is_right {
3520 let Some(entry) = aux.pop() else {
3521 break;
3522 };
3523 ri -= 1;
3524 drop(std::mem::replace(&mut v[k], entry));
3525 } else {
3526 ri -= 1;
3527 let entry = std::mem::replace(&mut v[ri], merge_padding_index_entry());
3528 drop(std::mem::replace(&mut v[k], entry));
3529 }
3530 }
3531 }
3532 }
3533 while ri > 0 {
3534 k -= 1;
3535 if smaller_is_right {
3536 let Some(entry) = aux.pop() else {
3537 break;
3538 };
3539 ri -= 1;
3540 drop(std::mem::replace(&mut v[k], entry));
3541 } else {
3542 ri -= 1;
3543 let entry = std::mem::replace(&mut v[ri], merge_padding_index_entry());
3544 drop(std::mem::replace(&mut v[k], entry));
3545 }
3546 }
3547 if !smaller_is_right && li > 0 {
3548 for i in 0..li {
3549 let entry = std::mem::replace(&mut aux[i], merge_padding_index_entry());
3550 drop(std::mem::replace(&mut v[i], entry));
3551 }
3552 }
3553 let tail_len = total - k;
3554 if li < k && tail_len > 0 {
3555 for offset in 0..tail_len {
3556 let entry = std::mem::replace(&mut v[k + offset], merge_padding_index_entry());
3557 drop(std::mem::replace(&mut v[li + offset], entry));
3558 }
3559 }
3560 v.truncate(li + tail_len);
3561}
3562
3563fn prefix_under_or_equal(prefix: &[u8], path: &[u8]) -> bool {
3564 if prefix.is_empty() {
3565 return true;
3566 }
3567 if path == prefix {
3568 return true;
3569 }
3570 path.len() > prefix.len() && path.starts_with(prefix) && path[prefix.len()] == b'/'
3571}
3572
3573fn synthetic_stage1_index_entry(mode: u32, path: &[u8], oid: ObjectId) -> IndexEntry {
3574 let path_len = path.len().min(0xFFF) as u16;
3575 let flags = (1u16 << 12) | path_len;
3576 IndexEntry {
3577 ctime_sec: 0,
3578 ctime_nsec: 0,
3579 mtime_sec: 0,
3580 mtime_nsec: 0,
3581 dev: 0,
3582 ino: 0,
3583 mode,
3584 uid: 0,
3585 gid: 0,
3586 size: 0,
3587 oid,
3588 flags,
3589 flags_extended: None,
3590 path: path.to_vec(),
3591 base_index_pos: 0,
3592 }
3593}
3594
3595fn config_truthy(raw: Option<&str>) -> bool {
3596 let Some(val) = raw else {
3597 return false;
3598 };
3599 let lowered = val.trim().to_lowercase();
3600 matches!(lowered.as_str(), "true" | "yes" | "1" | "on")
3601}
3602
3603pub(crate) fn index_skip_hash_for_write(config: Option<&ConfigSet>) -> bool {
3608 let Some(config) = config else {
3609 return false;
3610 };
3611 let many_files = config
3612 .get_bool("feature.manyFiles")
3613 .and_then(|r| r.ok())
3614 .unwrap_or(false);
3615 if many_files {
3616 if let Some(Ok(false)) = config.get_bool("index.skipHash") {
3617 return false;
3618 }
3619 if let Some(Ok(false)) = config.get_bool("index.skiphash") {
3620 return false;
3621 }
3622 return true;
3623 }
3624 for key in ["index.skipHash", "index.skiphash"] {
3625 if let Some(Ok(true)) = config.get_bool(key) {
3626 return true;
3627 }
3628 }
3629 false
3630}
3631
3632fn trim_trailing_slash_bytes(path: &[u8]) -> &[u8] {
3633 path.strip_suffix(b"/").unwrap_or(path)
3634}
3635
3636fn directory_in_cone(dir_path: &str, patterns: &[String], cone_mode: bool) -> bool {
3637 let dir = dir_path.trim_end_matches('/');
3646 if dir.is_empty() {
3647 return true;
3648 }
3649 let with_slash = format!("{dir}/");
3650 crate::sparse_checkout::path_matches_sparse_patterns(&with_slash, patterns, cone_mode)
3651}
3652
3653fn collect_directory_prefixes(path: &[u8], out: &mut BTreeSet<Vec<u8>>) {
3654 for (i, &b) in path.iter().enumerate() {
3655 if b == b'/' && !out.contains(&path[..i]) {
3656 out.insert(path[..i].to_vec());
3657 }
3658 }
3659}
3660
3661#[derive(Default)]
3665struct CollapsedDirectories {
3666 dirs: BTreeMap<Vec<u8>, ObjectId>,
3667}
3668
3669impl CollapsedDirectories {
3670 fn is_empty(&self) -> bool {
3671 self.dirs.is_empty()
3672 }
3673
3674 fn insert(&mut self, directory: Vec<u8>, oid: ObjectId) {
3676 let nested: Vec<Vec<u8>> = self
3677 .dirs
3678 .range::<[u8], _>((
3679 std::ops::Bound::Excluded(directory.as_slice()),
3680 std::ops::Bound::Unbounded,
3681 ))
3682 .map(|(k, _)| k)
3683 .take_while(|k| k.starts_with(&directory))
3684 .cloned()
3685 .collect();
3686 for key in nested {
3687 self.dirs.remove(&key);
3688 }
3689 self.dirs.insert(directory, oid);
3690 }
3691
3692 fn get(&self, directory: &[u8]) -> Option<&ObjectId> {
3693 self.dirs.get(directory)
3694 }
3695
3696 fn has_descendants(&self, directory: &[u8]) -> bool {
3698 self.dirs
3699 .range::<[u8], _>((
3700 std::ops::Bound::Excluded(directory),
3701 std::ops::Bound::Unbounded,
3702 ))
3703 .next()
3704 .is_some_and(|(k, _)| k.starts_with(directory))
3705 }
3706
3707 fn covered_rows(&self, entries: &[IndexEntry]) -> Vec<bool> {
3710 let mut covered = vec![false; entries.len()];
3711 let mut mark = |start: usize, matches: &dyn Fn(&[u8]) -> bool| {
3712 for (row, entry) in covered[start..].iter_mut().zip(&entries[start..]) {
3713 if !matches(&entry.path) {
3714 break;
3715 }
3716 *row = true;
3717 }
3718 };
3719 for directory in self.dirs.keys() {
3720 let name = trim_trailing_slash_bytes(directory);
3721 let start = entries.partition_point(|e| e.path.as_slice() < name);
3722 mark(start, &|path| path == name);
3723 let start = entries.partition_point(|e| e.path.as_slice() < directory.as_slice());
3724 mark(start, &|path| path.starts_with(directory));
3725 }
3726 covered
3727 }
3728
3729 fn covers(&self, path: &[u8], scratch: &mut Vec<u8>) -> bool {
3732 if self.dirs.is_empty() {
3733 return false;
3734 }
3735 let under = self
3738 .dirs
3739 .range::<[u8], _>((std::ops::Bound::Unbounded, std::ops::Bound::Included(path)))
3740 .next_back()
3741 .is_some_and(|(k, _)| path.starts_with(k));
3742 if under {
3743 return true;
3744 }
3745 scratch.clear();
3746 scratch.extend_from_slice(path);
3747 scratch.push(b'/');
3748 self.dirs.contains_key(scratch.as_slice())
3749 }
3750
3751 fn into_placeholders(self) -> impl Iterator<Item = IndexEntry> {
3752 self.dirs.into_iter().map(|(path, oid)| {
3753 let mut placeholder = IndexEntry {
3754 ctime_sec: 0,
3755 ctime_nsec: 0,
3756 mtime_sec: 0,
3757 mtime_nsec: 0,
3758 dev: 0,
3759 ino: 0,
3760 mode: MODE_TREE,
3761 uid: 0,
3762 gid: 0,
3763 size: 0,
3764 oid,
3765 flags: path.len().min(0xFFF) as u16,
3766 flags_extended: Some(0),
3767 path,
3768 base_index_pos: 0,
3769 };
3770 placeholder.set_skip_worktree(true);
3771 placeholder
3772 })
3773 }
3774}
3775
3776struct SparseCollapseView<'a> {
3779 entries: &'a [IndexEntry],
3780 collapsed: &'a CollapsedDirectories,
3781}
3782
3783impl SparseCollapseView<'_> {
3784 fn has_placeholder(&self, path_slash: &[u8], oid: &ObjectId, scratch: &mut Vec<u8>) -> bool {
3786 if let Some(collapsed_oid) = self.collapsed.get(path_slash) {
3787 return collapsed_oid == oid;
3788 }
3789 if self.collapsed.covers(path_slash, scratch) {
3790 return false;
3791 }
3792 let start = self
3793 .entries
3794 .partition_point(|e| e.path.as_slice() < path_slash);
3795 self.entries[start..]
3796 .iter()
3797 .take_while(|e| e.path == path_slash)
3798 .any(|e| e.stage() == 0 && e.is_sparse_directory_placeholder() && e.oid == *oid)
3799 }
3800}
3801
3802struct PrefixTreeResolver {
3805 root: ObjectId,
3806 resolved: HashMap<Vec<u8>, Option<ObjectId>>,
3807 parsed: HashMap<ObjectId, Option<Vec<TreeEntry>>>,
3808}
3809
3810impl PrefixTreeResolver {
3811 fn new(root: ObjectId) -> Self {
3812 Self {
3813 root,
3814 resolved: HashMap::new(),
3815 parsed: HashMap::new(),
3816 }
3817 }
3818
3819 fn resolve(&mut self, odb: &Odb, prefix: &[u8]) -> Result<Option<ObjectId>> {
3822 if prefix.is_empty() {
3823 return Ok(Some(self.root));
3824 }
3825 if let Some(known) = self.resolved.get(prefix) {
3826 return Ok(*known);
3827 }
3828 let (parent, name) = match prefix.iter().rposition(|&b| b == b'/') {
3829 Some(i) => (&prefix[..i], &prefix[i + 1..]),
3830 None => (&prefix[..0], prefix),
3831 };
3832 let found = match self.resolve(odb, parent)? {
3833 Some(parent_oid) if !name.is_empty() => {
3834 let entries = match self.parsed.entry(parent_oid) {
3835 std::collections::hash_map::Entry::Occupied(known) => known.into_mut(),
3836 std::collections::hash_map::Entry::Vacant(slot) => {
3837 let obj = odb.read(&parent_oid)?;
3838 slot.insert(if obj.kind == ObjectKind::Tree {
3839 Some(parse_tree(&obj.data)?)
3840 } else {
3841 None
3842 })
3843 }
3844 };
3845 entries
3846 .as_ref()
3847 .and_then(|entries| entries.iter().find(|e| e.name == name))
3848 .filter(|e| e.mode == MODE_TREE)
3849 .map(|e| e.oid)
3850 }
3851 Some(parent_oid) => Some(parent_oid),
3853 None => None,
3854 };
3855 self.resolved.insert(prefix.to_vec(), found);
3856 Ok(found)
3857 }
3858}
3859
3860fn try_collapse_directory_of_sparse_placeholders(
3863 odb: &Odb,
3864 directory: &[u8],
3865 rows: &[IndexEntry],
3866 head_subtree: &ObjectId,
3867) -> Result<bool> {
3868 if rows.is_empty() || rows.iter().any(|e| e.stage() != 0) {
3869 return Ok(false);
3870 }
3871 if !rows.iter().all(|e| e.is_sparse_directory_placeholder()) {
3872 return Ok(false);
3873 }
3874 let obj = odb.read(head_subtree)?;
3875 if obj.kind != crate::objects::ObjectKind::Tree {
3876 return Ok(false);
3877 }
3878 let tree_entries = crate::objects::parse_tree(&obj.data)?;
3879 if tree_entries.len() != rows.len() {
3880 return Ok(false);
3881 }
3882 for te in &tree_entries {
3883 if te.mode != MODE_TREE {
3884 return Ok(false);
3885 }
3886 let mut path = directory.to_vec();
3887 path.extend_from_slice(&te.name);
3888 path.push(b'/');
3889 let Some(row) = rows.iter().find(|e| e.path == path) else {
3890 return Ok(false);
3891 };
3892 if row.oid != te.oid || !row.skip_worktree() {
3893 return Ok(false);
3894 }
3895 }
3896 Ok(true)
3897}
3898
3899fn collect_sparse_aware_expected_blobs(
3902 odb: &Odb,
3903 tree_oid: &ObjectId,
3904 prefix: &[u8],
3905 patterns: &[String],
3906 cone_mode: bool,
3907 view: &SparseCollapseView<'_>,
3908 protect: crate::path_validate::ProtectOptions,
3909) -> Result<Vec<ExpectedBlob>> {
3910 let mut out = Vec::new();
3911 let mut scratch = Vec::new();
3912 walk_sparse_aware(
3913 odb,
3914 tree_oid,
3915 prefix,
3916 patterns,
3917 cone_mode,
3918 view,
3919 protect,
3920 &mut scratch,
3921 &mut out,
3922 )?;
3923 Ok(out)
3924}
3925
3926#[allow(clippy::too_many_arguments)]
3927fn walk_sparse_aware(
3928 odb: &Odb,
3929 tree_oid: &ObjectId,
3930 prefix: &[u8],
3931 patterns: &[String],
3932 cone_mode: bool,
3933 view: &SparseCollapseView<'_>,
3934 protect: crate::path_validate::ProtectOptions,
3935 scratch: &mut Vec<u8>,
3936 out: &mut Vec<ExpectedBlob>,
3937) -> Result<()> {
3938 let tree = odb.cached_tree_for_walk(tree_oid)?;
3940 let mut visit = |name: &[u8], mode: u32, oid: ObjectId| -> Result<()> {
3941 let path = if prefix.is_empty() {
3942 name.to_vec()
3943 } else {
3944 let mut p = Vec::with_capacity(prefix.len() + 1 + name.len());
3945 p.extend_from_slice(prefix);
3946 p.push(b'/');
3947 p.extend_from_slice(name);
3948 p
3949 };
3950 crate::path_population::validate_tree_walk_entry(&path, name, mode, protect)?;
3951 if mode == MODE_TREE {
3952 let mut path_slash = path.clone();
3953 path_slash.push(b'/');
3954 if view.has_placeholder(&path_slash, &oid, scratch) {
3955 return Ok(());
3956 }
3957 return walk_sparse_aware(
3958 odb, &oid, &path, patterns, cone_mode, view, protect, scratch, out,
3959 );
3960 }
3961 let path_str = String::from_utf8_lossy(&path);
3962 if crate::sparse_checkout::path_matches_sparse_patterns(&path_str, patterns, cone_mode) {
3963 return Ok(());
3964 }
3965 out.push(ExpectedBlob { path, mode, oid });
3966 Ok(())
3967 };
3968 match &tree {
3969 crate::odb::SessionTree::Parsed(entries) => {
3970 for te in entries.iter() {
3971 visit(&te.name, te.mode, te.oid)?;
3972 }
3973 }
3974 crate::odb::SessionTree::Raw(data) => {
3975 for te in crate::objects::TreeRefIter::with_oid_len(data, odb.hash_algo().len()) {
3976 let te = te?;
3977 visit(te.name, te.mode, te.oid)?;
3978 }
3979 }
3980 }
3981 Ok(())
3982}
3983
3984struct ExpectedBlob {
3986 path: Vec<u8>,
3987 mode: u32,
3988 oid: ObjectId,
3989}
3990
3991fn flatten_tree_blobs(
3992 odb: &Odb,
3993 tree_oid: &ObjectId,
3994 prefix: &[u8],
3995 protect: crate::path_validate::ProtectOptions,
3996) -> Result<Vec<IndexEntry>> {
3997 let obj = odb.read(tree_oid)?;
3998 if obj.kind != ObjectKind::Tree {
3999 return Err(Error::IndexError(format!("expected tree at {}", tree_oid)));
4000 }
4001 let entries = parse_tree(&obj.data)?;
4002 let mut out = Vec::new();
4003 for te in entries {
4004 let path = if prefix.is_empty() {
4005 te.name.clone()
4006 } else {
4007 let mut p = prefix.to_vec();
4008 p.push(b'/');
4009 p.extend_from_slice(&te.name);
4010 p
4011 };
4012 crate::path_population::validate_tree_walk_entry(prefix, &te.name, te.mode, protect)?;
4013 if is_tree_mode(te.mode) {
4014 let sub = flatten_tree_blobs(odb, &te.oid, &path, protect)?;
4015 out.extend(sub);
4016 } else {
4017 let path_len = path.len().min(0xFFF) as u16;
4018 let mut e = IndexEntry {
4019 ctime_sec: 0,
4020 ctime_nsec: 0,
4021 mtime_sec: 0,
4022 mtime_nsec: 0,
4023 dev: 0,
4024 ino: 0,
4025 mode: canon_mode(te.mode),
4026 uid: 0,
4027 gid: 0,
4028 size: 0,
4029 oid: te.oid,
4030 flags: path_len,
4031 flags_extended: Some(0),
4032 path,
4033 base_index_pos: 0,
4034 };
4035 e.set_skip_worktree(true);
4036 out.push(e);
4037 }
4038 }
4039 Ok(out)
4040}
4041
4042pub struct IndexLock {
4049 pub(crate) index_path: PathBuf,
4050 pub(crate) lock_path: PathBuf,
4051 pub(crate) pid_path: PathBuf,
4052 pub(crate) file: fs::File,
4053 pub(crate) wrote_pid: bool,
4054 pub(crate) committed: bool,
4055}
4056
4057impl IndexLock {
4058 pub(crate) fn reopen_after_write(&mut self) -> Result<()> {
4060 drop(std::mem::replace(
4061 &mut self.file,
4062 fs::OpenOptions::new()
4063 .write(true)
4064 .open(&self.lock_path)
4065 .map_err(Error::Io)?,
4066 ));
4067 Ok(())
4068 }
4069
4070 pub(crate) fn commit_replace_index(&mut self) -> Result<()> {
4072 crate::atomic_replace::detach_open_handle(&mut self.file).map_err(Error::Io)?;
4073 crate::atomic_replace::replace_file(&self.lock_path, &self.index_path)
4074 .map_err(Error::Io)?;
4075 self.committed = true;
4076 if self.wrote_pid {
4077 let _ = fs::remove_file(&self.pid_path);
4078 }
4079 Ok(())
4080 }
4081
4082 pub fn acquire(index_path: &Path, config: Option<&ConfigSet>) -> Result<Self> {
4090 let lock_path = index_path.with_extension("lock");
4091 let pid_path = pid_path_for_lock(&lock_path);
4092 let lockfile_pid_enabled = grit_lockfile_pid_enabled(config, index_path.parent());
4093
4094 use crate::lockfile::{LockFile, LockWait};
4095 let wait = LockWait::files_ref_lock(config);
4096 let mut lock_file = LockFile::acquire(&lock_path, wait).map_err(Error::from)?;
4097 if lockfile_pid_enabled {
4098 if let Err(e) = lock_file
4099 .write_holder_pid()
4100 .map_err(crate::error::Error::from)
4101 {
4102 lock_file.rollback();
4103 return Err(e);
4104 }
4105 }
4106 let wrote_pid = lockfile_pid_enabled;
4107 let (lock_path, file) = lock_file.into_open_file()?;
4108
4109 Ok(Self {
4110 index_path: index_path.to_path_buf(),
4111 lock_path,
4112 pid_path,
4113 file,
4114 wrote_pid,
4115 committed: false,
4116 })
4117 }
4118}
4119
4120impl Drop for IndexLock {
4121 fn drop(&mut self) {
4122 if self.committed {
4123 return;
4124 }
4125 let _ = fs::remove_file(&self.lock_path);
4126 if self.wrote_pid {
4127 let _ = fs::remove_file(&self.pid_path);
4128 }
4129 }
4130}
4131
4132fn path_is_under_dir(path: &[u8], dir: &[u8]) -> bool {
4134 path.len() > dir.len() && path[dir.len()] == b'/' && path.starts_with(dir)
4135}
4136
4137fn hash_index_body(algo: HashAlgo, body: &[u8]) -> Vec<u8> {
4139 algo.checksum_digest(body)
4140}
4141
4142const INDEX_WRITE_CHUNK: usize = 128 * 1024;
4144
4145const INDEX_WRITE_CHUNK_SLACK: usize = 8 * 1024;
4147
4148type IndexChunkSink<'a> = &'a mut dyn FnMut(&[u8]) -> io::Result<()>;
4150
4151struct IndexBodyOut<'a> {
4158 buf: Vec<u8>,
4159 drained: usize,
4160 sink: Option<IndexChunkSink<'a>>,
4161}
4162
4163impl IndexBodyOut<'_> {
4164 fn position(&self) -> usize {
4166 self.drained + self.buf.len()
4167 }
4168
4169 fn drain_if_full(&mut self) -> io::Result<()> {
4171 if self.buf.len() < INDEX_WRITE_CHUNK {
4172 return Ok(());
4173 }
4174 self.drain()
4175 }
4176
4177 fn drain(&mut self) -> io::Result<()> {
4179 let Some(sink) = self.sink.as_mut() else {
4180 return Ok(());
4181 };
4182 if self.buf.is_empty() {
4183 return Ok(());
4184 }
4185 sink(&self.buf)?;
4186 self.drained += self.buf.len();
4187 self.buf.clear();
4188 Ok(())
4189 }
4190}
4191
4192impl Index {
4193 fn estimate_body_capacity(&self, write_version: u32, config: Option<&ConfigSet>) -> usize {
4195 use crate::index_eoie_ieot::record_end_of_index_entries;
4196 let hash_len = self.hash_algo.len();
4197 let mut entries_bytes = 0usize;
4198 for entry in &self.entries {
4199 let mut n = 62usize
4200 .saturating_add(hash_len)
4201 .saturating_add(entry.path.len());
4202 if write_version >= 3
4203 && entry
4204 .flags_extended
4205 .map(IndexEntry::disk_flags_extended)
4206 .filter(|fe| *fe != 0)
4207 .is_some()
4208 {
4209 n += 2;
4210 }
4211 if write_version == 4 {
4212 n += 12;
4213 } else {
4214 n = (n + 7) & !7;
4215 }
4216 entries_bytes = entries_bytes.saturating_add(n);
4217 }
4218 let mut extra = 256usize;
4219 if self.cache_tree.is_some() {
4220 extra += 4096;
4221 }
4222 if self.untracked_cache.is_some() {
4223 extra += 4096;
4224 }
4225 if self.fsmonitor_last_update.is_some() {
4226 extra += 512;
4227 }
4228 if record_end_of_index_entries(config) {
4229 extra += 64;
4230 }
4231 12 + entries_bytes + extra
4232 }
4233}
4234
4235fn append_index_extension(
4236 out: &mut Vec<u8>,
4237 eoie_headers: &mut Option<Vec<u8>>,
4238 signature: u32,
4239 payload: &[u8],
4240) {
4241 let size = payload.len() as u32;
4242 out.extend_from_slice(&signature.to_be_bytes());
4243 out.extend_from_slice(&size.to_be_bytes());
4244 if let Some(headers) = eoie_headers {
4245 headers.extend_from_slice(&signature.to_be_bytes());
4246 headers.extend_from_slice(&size.to_be_bytes());
4247 }
4248 out.extend_from_slice(payload);
4249}
4250
4251fn detect_index_hash_algo(data: &[u8]) -> HashAlgo {
4256 let sha256_len = HashAlgo::Sha256.len();
4259 if data.len() > sha256_len + 12 {
4260 let (body, checksum) = data.split_at(data.len() - sha256_len);
4261 if checksum.iter().any(|&b| b != 0) && hash_index_body(HashAlgo::Sha256, body) == checksum {
4262 return HashAlgo::Sha256;
4263 }
4264 }
4265 HashAlgo::Sha1
4266}
4267
4268pub(crate) fn parse_entry(
4272 data: &[u8],
4273 version: u32,
4274 prev_path: &[u8],
4275 hash_len: usize,
4276) -> Result<(IndexEntry, usize)> {
4277 if data.len() < 40 + hash_len + 2 {
4279 return Err(Error::IndexError("entry too short".to_owned()));
4280 }
4281
4282 let mut pos = 0;
4283
4284 macro_rules! read_u32 {
4285 () => {{
4286 let v = u32::from_be_bytes(
4287 data[pos..pos + 4]
4288 .try_into()
4289 .map_err(|_| Error::IndexError("truncated u32".to_owned()))?,
4290 );
4291 pos += 4;
4292 v
4293 }};
4294 }
4295
4296 let ctime_sec = read_u32!();
4297 let ctime_nsec = read_u32!();
4298 let mtime_sec = read_u32!();
4299 let mtime_nsec = read_u32!();
4300 let dev = read_u32!();
4301 let ino = read_u32!();
4302 let mode = read_u32!();
4303 let uid = read_u32!();
4304 let gid = read_u32!();
4305 let size = read_u32!();
4306
4307 let oid = ObjectId::from_bytes(&data[pos..pos + hash_len])?;
4308 pos += hash_len;
4309
4310 let flags = u16::from_be_bytes(
4311 data[pos..pos + 2]
4312 .try_into()
4313 .map_err(|_| Error::IndexError("truncated flags".to_owned()))?,
4314 );
4315 pos += 2;
4316
4317 let flags_extended = if version >= 3 && flags & 0x4000 != 0 {
4318 let fe = u16::from_be_bytes(
4319 data[pos..pos + 2]
4320 .try_into()
4321 .map_err(|_| Error::IndexError("truncated extended flags".to_owned()))?,
4322 );
4323 pos += 2;
4324 Some(fe)
4325 } else {
4326 None
4327 };
4328
4329 let path;
4330 if version == 4 {
4331 let (strip_len, varint_bytes) = read_varint(&data[pos..]);
4333 pos += varint_bytes;
4334 let nul = data[pos..]
4335 .iter()
4336 .position(|&b| b == 0)
4337 .ok_or_else(|| Error::IndexError("v4 entry path missing NUL".to_owned()))?;
4338 let suffix = &data[pos..pos + nul];
4339 pos += nul + 1;
4340 let keep = prev_path.len().saturating_sub(strip_len);
4341 let mut full_path = Vec::with_capacity(keep + suffix.len());
4342 full_path.extend_from_slice(&prev_path[..keep]);
4343 full_path.extend_from_slice(suffix);
4344 path = full_path;
4345 } else {
4346 let nul = data[pos..]
4348 .iter()
4349 .position(|&b| b == 0)
4350 .ok_or_else(|| Error::IndexError("entry path missing NUL terminator".to_owned()))?;
4351 path = data[pos..pos + nul].to_vec();
4352 pos += nul + 1;
4353 let entry_start = 0usize;
4354 let entry_len = pos - entry_start;
4355 let padded = (entry_len + 7) & !7;
4356 let padding = padded.saturating_sub(entry_len);
4357 pos += padding;
4358 }
4359
4360 Ok((
4361 IndexEntry {
4362 ctime_sec,
4363 ctime_nsec,
4364 mtime_sec,
4365 mtime_nsec,
4366 dev,
4367 ino,
4368 mode,
4369 uid,
4370 gid,
4371 size,
4372 oid,
4373 flags,
4374 flags_extended,
4375 path,
4376 base_index_pos: 0,
4377 },
4378 pos,
4379 ))
4380}
4381
4382fn write_varint(out: &mut Vec<u8>, mut value: usize) {
4386 loop {
4387 let mut b = (value & 0x7F) as u8;
4388 value >>= 7;
4389 if value != 0 {
4390 b |= 0x80;
4391 }
4392 out.push(b);
4393 if value == 0 {
4394 break;
4395 }
4396 }
4397}
4398
4399fn read_varint(data: &[u8]) -> (usize, usize) {
4400 let mut value: usize = 0;
4401 let mut shift = 0usize;
4402 let mut pos = 0;
4403 loop {
4404 if pos >= data.len() {
4405 break;
4406 }
4407 let byte = data[pos] as usize;
4408 pos += 1;
4409 value |= (byte & 0x7F) << shift;
4410 if byte & 0x80 == 0 {
4411 break;
4412 }
4413 shift += 7;
4414 if shift > 28 {
4416 break;
4417 }
4418 }
4419 (value, pos)
4420}
4421
4422fn serialize_entry_v4(entry: &IndexEntry, previous_path: &mut Vec<u8>, out: &mut Vec<u8>) {
4423 let write_u32 = |out: &mut Vec<u8>, v: u32| out.extend_from_slice(&v.to_be_bytes());
4424
4425 write_u32(out, entry.ctime_sec);
4426 write_u32(out, entry.ctime_nsec);
4427 write_u32(out, entry.mtime_sec);
4428 write_u32(out, entry.mtime_nsec);
4429 write_u32(out, entry.dev);
4430 write_u32(out, entry.ino);
4431 write_u32(out, entry.mode);
4432 write_u32(out, entry.uid);
4433 write_u32(out, entry.gid);
4434 write_u32(out, entry.size);
4435 out.extend_from_slice(entry.oid.as_bytes());
4436
4437 let mut flags = entry.flags;
4438 let disk_ext = entry
4439 .flags_extended
4440 .map(IndexEntry::disk_flags_extended)
4441 .filter(|fe| *fe != 0);
4442 if disk_ext.is_some() {
4443 flags |= 0x4000;
4444 } else {
4445 flags &= !0x4000;
4446 }
4447 let path_len = entry.path.len().min(0xFFF) as u16;
4448 flags = (flags & 0xF000) | path_len;
4449 out.extend_from_slice(&flags.to_be_bytes());
4450
4451 if let Some(fe) = disk_ext {
4452 out.extend_from_slice(&fe.to_be_bytes());
4453 }
4454
4455 let common = previous_path
4456 .iter()
4457 .zip(entry.path.iter())
4458 .take_while(|(a, b)| a == b)
4459 .count();
4460 let to_remove = previous_path.len().saturating_sub(common);
4461 write_varint(out, to_remove);
4462 out.extend_from_slice(&entry.path[common..]);
4463 out.push(0);
4464
4465 previous_path.clear();
4466 previous_path.extend_from_slice(&entry.path);
4467}
4468
4469fn serialize_entry(entry: &IndexEntry, version: u32, out: &mut Vec<u8>) {
4470 let start = out.len();
4471
4472 let write_u32 = |out: &mut Vec<u8>, v: u32| out.extend_from_slice(&v.to_be_bytes());
4473
4474 write_u32(out, entry.ctime_sec);
4475 write_u32(out, entry.ctime_nsec);
4476 write_u32(out, entry.mtime_sec);
4477 write_u32(out, entry.mtime_nsec);
4478 write_u32(out, entry.dev);
4479 write_u32(out, entry.ino);
4480 write_u32(out, entry.mode);
4481 write_u32(out, entry.uid);
4482 write_u32(out, entry.gid);
4483 write_u32(out, entry.size);
4484 out.extend_from_slice(entry.oid.as_bytes());
4485
4486 let mut flags = entry.flags;
4488 let disk_ext = entry
4489 .flags_extended
4490 .map(IndexEntry::disk_flags_extended)
4491 .filter(|fe| *fe != 0);
4492 if version >= 3 && disk_ext.is_some() {
4493 flags |= 0x4000;
4494 } else {
4495 flags &= !0x4000;
4496 }
4497 let path_len = entry.path.len().min(0xFFF) as u16;
4499 flags = (flags & 0xF000) | path_len;
4500 out.extend_from_slice(&flags.to_be_bytes());
4501
4502 if version >= 3 {
4503 if let Some(fe) = disk_ext {
4504 out.extend_from_slice(&fe.to_be_bytes());
4505 }
4506 }
4507
4508 out.extend_from_slice(&entry.path);
4509 out.push(0);
4510
4511 let entry_len = out.len() - start;
4513 let padded = (entry_len + 7) & !7;
4514 let padding = padded - entry_len;
4515 for _ in 0..padding {
4516 out.push(0);
4517 }
4518}
4519
4520pub fn entry_from_stat(
4533 path: &Path,
4534 rel_path: &[u8],
4535 oid: ObjectId,
4536 mode: u32,
4537) -> Result<IndexEntry> {
4538 let meta = fs::symlink_metadata(path)?;
4539 Ok(entry_from_metadata(&meta, rel_path, oid, mode))
4540}
4541
4542#[must_use]
4546pub fn index_file_mtime(path: &Path) -> Option<(u32, u32)> {
4547 let meta = fs::metadata(path).ok()?;
4548 Some(index_mtime_from_metadata(&meta))
4549}
4550
4551fn index_mtime_from_metadata(meta: &fs::Metadata) -> (u32, u32) {
4552 let ft = filetime::FileTime::from_last_modification_time(meta);
4553 let secs = ft.unix_seconds();
4554 if secs.is_negative() {
4555 return (0, 0);
4556 }
4557 (secs as u32, ft.nanoseconds())
4558}
4559
4560const INDEX_MMAP_MIN_BYTES: u64 = 64 * 1024;
4562
4563enum IndexFileBytes {
4565 Owned(Vec<u8>),
4566 Mapped(memmap2::Mmap),
4567}
4568
4569impl std::ops::Deref for IndexFileBytes {
4570 type Target = [u8];
4571
4572 fn deref(&self) -> &[u8] {
4573 match self {
4574 Self::Owned(bytes) => bytes,
4575 Self::Mapped(map) => map,
4576 }
4577 }
4578}
4579
4580#[must_use]
4582pub(crate) fn index_file_trailer(data: &[u8], hash_algo: HashAlgo) -> Option<Vec<u8>> {
4583 let hash_len = hash_algo.len();
4584 (data.len() >= hash_len).then(|| data[data.len() - hash_len..].to_vec())
4585}
4586
4587fn record_loaded_index_identity(index: &mut Index, data: &[u8], mtime: (u32, u32)) {
4588 index.source_mtime = Some(mtime);
4589 index.source_file_len = Some(u64::try_from(data.len()).unwrap_or(u64::MAX));
4590 index.source_checksum = index_file_trailer(data, index.hash_algo);
4591}
4592
4593#[derive(Debug, Clone, PartialEq, Eq)]
4595pub(crate) struct IndexOnDiskIdentity {
4596 pub checksum: Option<Vec<u8>>,
4597 pub mtime: Option<(u32, u32)>,
4598 pub len: u64,
4599}
4600
4601pub(crate) fn read_index_on_disk_identity(
4603 path: &Path,
4604 hash_algo: HashAlgo,
4605) -> io::Result<IndexOnDiskIdentity> {
4606 match read_index_file(path) {
4607 Ok((data, mtime)) => {
4608 let bytes = data.deref();
4609 Ok(IndexOnDiskIdentity {
4610 checksum: index_file_trailer(bytes, hash_algo),
4611 mtime: Some(mtime),
4612 len: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
4613 })
4614 }
4615 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(IndexOnDiskIdentity {
4616 checksum: None,
4617 mtime: None,
4618 len: 0,
4619 }),
4620 Err(e) => Err(e),
4621 }
4622}
4623
4624#[must_use]
4628pub(crate) fn opportunistic_index_write_still_current(
4629 loaded: &Index,
4630 on_disk: &IndexOnDiskIdentity,
4631) -> bool {
4632 let had_disk_index = loaded.source_checksum.is_some()
4633 || loaded.source_mtime.is_some()
4634 || loaded.source_file_len.is_some();
4635 if !had_disk_index {
4636 return on_disk.len == 0;
4637 }
4638 if on_disk.len == 0 {
4639 return false;
4640 }
4641 if let (Some(loaded_cs), Some(disk_cs)) = (&loaded.source_checksum, &on_disk.checksum) {
4642 if loaded_cs != disk_cs {
4643 return false;
4644 }
4645 if loaded_cs.iter().all(|&b| b == 0) {
4646 return loaded.source_mtime == on_disk.mtime
4647 && loaded.source_file_len == Some(on_disk.len);
4648 }
4649 }
4650 true
4651}
4652
4653fn read_index_file(path: &Path) -> io::Result<(IndexFileBytes, (u32, u32))> {
4658 use std::io::Read;
4659
4660 let mut file = fs::File::open(path)?;
4661 let meta = file.metadata()?;
4662 let mtime = index_mtime_from_metadata(&meta);
4663 let len = meta.len();
4664 if meta.is_file() && len >= INDEX_MMAP_MIN_BYTES {
4665 if let Ok(len) = usize::try_from(len) {
4666 if let Ok(map) = unsafe { memmap2::MmapOptions::new().len(len).map(&file) } {
4669 return Ok((IndexFileBytes::Mapped(map), mtime));
4670 }
4671 }
4672 }
4673 let mut bytes = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
4674 file.read_to_end(&mut bytes)?;
4675 Ok((IndexFileBytes::Owned(bytes), mtime))
4676}
4677
4678fn max_stage0_entry_mtime(entries: &[IndexEntry]) -> (u32, u32) {
4679 let mut max_sec = 0u32;
4680 let mut max_nsec = 0u32;
4681 for ie in entries {
4682 if ie.stage() != 0 {
4683 continue;
4684 }
4685 if ie.mtime_sec > max_sec || (ie.mtime_sec == max_sec && ie.mtime_nsec > max_nsec) {
4686 max_sec = ie.mtime_sec;
4687 max_nsec = ie.mtime_nsec;
4688 }
4689 }
4690 (max_sec, max_nsec)
4691}
4692
4693#[must_use]
4695pub(crate) fn max_stage0_mtime_pair(a: (u32, u32), b: (u32, u32)) -> (u32, u32) {
4696 if a.0 > b.0 || (a.0 == b.0 && a.1 > b.1) {
4697 a
4698 } else {
4699 b
4700 }
4701}
4702
4703pub(crate) fn stage0_mtime_max(entries: &[IndexEntry]) -> (u32, u32) {
4705 max_stage0_entry_mtime(entries)
4706}
4707
4708pub(crate) fn advance_index_file_mtime_past_entries(
4728 index: &Index,
4729 index_path: &Path,
4730 touched_only_max: Option<(u32, u32)>,
4731) -> Result<()> {
4732 use filetime::FileTime;
4733 let (max_sec, max_nsec) = match touched_only_max {
4734 Some(pair) => pair,
4735 None => max_stage0_entry_mtime(&index.entries),
4736 };
4737 if max_sec == 0 && max_nsec == 0 {
4738 return Ok(());
4739 }
4740 let needs_bump = index_file_mtime(index_path)
4741 .is_none_or(|(sec, nsec)| sec < max_sec || (sec == max_sec && nsec <= max_nsec));
4742 if !needs_bump {
4743 return Ok(());
4744 }
4745 const TICK_NSEC: u32 = if cfg!(windows) { 100 } else { 1 };
4746 if max_nsec >= 1_000_000_000 - TICK_NSEC {
4747 return Ok(());
4748 }
4749 let ft = FileTime::from_unix_time(i64::from(max_sec), max_nsec + TICK_NSEC);
4750 filetime::set_file_mtime(index_path, ft).map_err(Error::Io)
4751}
4752
4753#[must_use]
4755pub(crate) fn index_path_outcome_key(path: &[u8]) -> Cow<'_, str> {
4756 match std::str::from_utf8(path) {
4757 Ok(s) => Cow::Borrowed(s),
4758 Err(_) => Cow::Owned(String::from_utf8_lossy(path).into_owned()),
4759 }
4760}
4761
4762#[must_use]
4766pub fn worktree_path_from_index_rel(work_tree: &Path, rel: &[u8]) -> PathBuf {
4767 #[cfg(unix)]
4768 {
4769 use std::ffi::OsStr;
4770 use std::os::unix::ffi::OsStrExt;
4771 work_tree.join(OsStr::from_bytes(rel))
4772 }
4773 #[cfg(not(unix))]
4774 {
4775 let rel = std::str::from_utf8(rel)
4776 .map(Cow::Borrowed)
4777 .unwrap_or_else(|_| Cow::Owned(String::from_utf8_lossy(rel).into_owned()));
4778 work_tree.join(rel.as_ref())
4779 }
4780}
4781
4782#[derive(Debug, Clone, Copy)]
4784pub struct IndexStatFields {
4785 ctime_sec: u32,
4786 ctime_nsec: u32,
4787 mtime_sec: u32,
4788 mtime_nsec: u32,
4789 dev: u32,
4790 ino: u32,
4791 uid: u32,
4792 gid: u32,
4793 size: u32,
4794}
4795
4796impl IndexStatFields {
4797 #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
4799 pub(crate) fn from_linux_stat(stat: &libc::stat) -> Self {
4800 Self {
4801 ctime_sec: stat.st_ctime as u32,
4802 ctime_nsec: stat.st_ctime_nsec as u32,
4803 mtime_sec: stat.st_mtime as u32,
4804 mtime_nsec: stat.st_mtime_nsec as u32,
4805 dev: stat.st_dev as u32,
4806 ino: stat.st_ino as u32,
4807 uid: stat.st_uid,
4808 gid: stat.st_gid,
4809 size: munge_index_size(stat.st_size as u64),
4810 }
4811 }
4812
4813 #[must_use]
4815 pub(crate) fn index_size(&self) -> u32 {
4816 self.size
4817 }
4818
4819 #[must_use]
4821 pub fn from_metadata(meta: &fs::Metadata) -> Self {
4822 #[cfg(unix)]
4823 {
4824 use std::os::unix::fs::MetadataExt;
4825 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
4826 Self {
4827 ctime_sec,
4828 ctime_nsec,
4829 mtime_sec,
4830 mtime_nsec,
4831 dev: meta.dev() as u32,
4832 ino: meta.ino() as u32,
4833 uid: meta.uid(),
4834 gid: meta.gid(),
4835 size: munge_index_size(meta.size()),
4836 }
4837 }
4838 #[cfg(not(unix))]
4839 {
4840 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
4841 Self {
4842 ctime_sec,
4843 ctime_nsec,
4844 mtime_sec,
4845 mtime_nsec,
4846 dev: 0,
4847 ino: 0,
4848 uid: 0,
4849 gid: 0,
4850 size: munge_index_size(meta.len()),
4851 }
4852 }
4853 }
4854
4855 #[must_use]
4857 pub(crate) fn of_entry(ie: &IndexEntry) -> Self {
4858 Self {
4859 ctime_sec: ie.ctime_sec,
4860 ctime_nsec: ie.ctime_nsec,
4861 mtime_sec: ie.mtime_sec,
4862 mtime_nsec: ie.mtime_nsec,
4863 dev: ie.dev,
4864 ino: ie.ino,
4865 uid: ie.uid,
4866 gid: ie.gid,
4867 size: ie.size,
4868 }
4869 }
4870
4871 #[must_use]
4873 pub(crate) fn equivalent_to(&self, other: &Self, policy: StatMatchPolicy) -> bool {
4874 if self.size != other.size || self.mtime_sec != other.mtime_sec {
4875 return false;
4876 }
4877 if policy.use_nanosec && policy.check_stat && self.mtime_nsec != other.mtime_nsec {
4878 return false;
4879 }
4880 if policy.trust_ctime && policy.check_stat {
4881 if self.ctime_sec != other.ctime_sec {
4882 return false;
4883 }
4884 if policy.use_nanosec && self.ctime_nsec != other.ctime_nsec {
4885 return false;
4886 }
4887 }
4888 !policy.check_stat
4889 || (self.dev == other.dev
4890 && self.ino == other.ino
4891 && self.uid == other.uid
4892 && self.gid == other.gid)
4893 }
4894
4895 pub(crate) fn store_in(&self, ie: &mut IndexEntry) {
4897 ie.ctime_sec = self.ctime_sec;
4898 ie.ctime_nsec = self.ctime_nsec;
4899 ie.mtime_sec = self.mtime_sec;
4900 ie.mtime_nsec = self.mtime_nsec;
4901 ie.dev = self.dev;
4902 ie.ino = self.ino;
4903 ie.uid = self.uid;
4904 ie.gid = self.gid;
4905 ie.size = self.size;
4906 }
4907
4908 #[must_use]
4910 pub fn index_entry(self, rel_path: &[u8], oid: ObjectId, mode: u32) -> IndexEntry {
4911 IndexEntry {
4912 ctime_sec: self.ctime_sec,
4913 ctime_nsec: self.ctime_nsec,
4914 mtime_sec: self.mtime_sec,
4915 mtime_nsec: self.mtime_nsec,
4916 dev: self.dev,
4917 ino: self.ino,
4918 mode,
4919 uid: self.uid,
4920 gid: self.gid,
4921 size: self.size,
4922 oid,
4923 flags: rel_path.len().min(0xFFF) as u16,
4924 flags_extended: None,
4925 path: rel_path.to_vec(),
4926 base_index_pos: 0,
4927 }
4928 }
4929}
4930
4931impl From<&fs::Metadata> for IndexStatFields {
4932 fn from(meta: &fs::Metadata) -> Self {
4933 Self::from_metadata(meta)
4934 }
4935}
4936
4937impl IndexStatFields {
4938 #[must_use]
4941 #[cfg_attr(not(windows), allow(dead_code))]
4942 pub(crate) fn size_and_times_match_index(
4943 &self,
4944 ie: &IndexEntry,
4945 policy: StatMatchPolicy,
4946 ) -> bool {
4947 if self.size != ie.size || self.mtime_sec != ie.mtime_sec {
4948 return false;
4949 }
4950 if !(policy.use_nanosec && policy.check_stat) {
4951 return true;
4952 }
4953 self.mtime_nsec == ie.mtime_nsec
4954 && (!policy.trust_ctime || self.ctime_nsec == ie.ctime_nsec)
4955 }
4956
4957 #[must_use]
4959 pub(crate) fn matches_index_entry(&self, ie: &IndexEntry, policy: StatMatchPolicy) -> bool {
4960 if self.size != ie.size {
4961 return false;
4962 }
4963 if self.mtime_sec != ie.mtime_sec {
4964 return false;
4965 }
4966 if policy.use_nanosec && policy.check_stat && self.mtime_nsec != ie.mtime_nsec {
4967 return false;
4968 }
4969 if policy.trust_ctime && policy.check_stat {
4970 if self.ctime_sec != ie.ctime_sec {
4971 return false;
4972 }
4973 if policy.use_nanosec && self.ctime_nsec != ie.ctime_nsec {
4974 return false;
4975 }
4976 }
4977 if policy.check_stat {
4978 #[cfg(windows)]
4982 {
4983 if self.ino != ie.ino && self.ino != 0 {
4984 return false;
4985 }
4986 if self.dev != ie.dev && self.dev != 0 {
4987 return false;
4988 }
4989 if self.uid != ie.uid && self.uid != 0 {
4990 return false;
4991 }
4992 if self.gid != ie.gid && self.gid != 0 {
4993 return false;
4994 }
4995 }
4996 #[cfg(not(windows))]
4997 if self.ino != ie.ino {
4998 return false;
4999 }
5000 }
5001 true
5002 }
5003}
5004
5005#[must_use]
5010pub fn entry_from_metadata(
5011 meta: &fs::Metadata,
5012 rel_path: &[u8],
5013 oid: ObjectId,
5014 mode: u32,
5015) -> IndexEntry {
5016 #[cfg(unix)]
5017 {
5018 use std::os::unix::fs::MetadataExt;
5019 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
5020 IndexEntry {
5021 ctime_sec,
5022 ctime_nsec,
5023 mtime_sec,
5024 mtime_nsec,
5025 dev: meta.dev() as u32,
5026 ino: meta.ino() as u32,
5027 mode,
5028 uid: meta.uid(),
5029 gid: meta.gid(),
5030 size: munge_index_size(meta.size()),
5031 oid,
5032 flags: rel_path.len().min(0xFFF) as u16,
5033 flags_extended: None,
5034 path: rel_path.to_vec(),
5035 base_index_pos: 0,
5036 }
5037 }
5038 #[cfg(not(unix))]
5039 {
5040 let ((ctime_sec, ctime_nsec), (mtime_sec, mtime_nsec)) = metadata_times(meta);
5041 IndexEntry {
5042 ctime_sec,
5043 ctime_nsec,
5044 mtime_sec,
5045 mtime_nsec,
5046 dev: 0,
5047 ino: 0,
5048 mode,
5049 uid: 0,
5050 gid: 0,
5051 size: munge_index_size(meta.len()),
5052 oid,
5053 flags: rel_path.len().min(0xFFF) as u16,
5054 flags_extended: None,
5055 path: rel_path.to_vec(),
5056 base_index_pos: 0,
5057 }
5058 }
5059}
5060
5061#[must_use]
5071pub fn normalize_mode(raw_mode: u32) -> u32 {
5072 const S_IFMT: u32 = 0o170000;
5073 const S_IFLNK: u32 = 0o120000;
5074 const S_IFREG: u32 = 0o100000;
5075
5076 let fmt = raw_mode & S_IFMT;
5077 if fmt == S_IFLNK {
5078 return MODE_SYMLINK;
5079 }
5080 if fmt == S_IFREG {
5081 if raw_mode & 0o111 != 0 {
5083 return MODE_EXECUTABLE;
5084 }
5085 return MODE_REGULAR;
5086 }
5087 MODE_REGULAR
5089}
5090
5091#[cfg(test)]
5092pub(crate) mod index_path_cmp {
5093 use std::cell::Cell;
5094
5095 thread_local! { static PATH_CMP_COUNT: Cell<usize> = const { Cell::new(0) };
5097 }
5098
5099 pub fn reset() {
5100 PATH_CMP_COUNT.with(|c| c.set(0));
5101 }
5102
5103 pub fn take() -> usize {
5104 PATH_CMP_COUNT.with(|c| {
5105 let n = c.get();
5106 c.set(0);
5107 n
5108 })
5109 }
5110
5111 pub fn bump() {
5112 PATH_CMP_COUNT.with(|c| c.set(c.get() + 1));
5113 }
5114}
5115
5116fn record_path_cmp_for_tests() {
5117 #[cfg(test)]
5118 index_path_cmp::bump();
5119}
5120
5121#[cfg(test)]
5122mod tests {
5123 #![allow(clippy::expect_used, clippy::unwrap_used)]
5124
5125 use super::*;
5126 use tempfile::TempDir;
5127
5128 pub(super) mod fsmonitor_rebuild_counter {
5131 use std::cell::Cell;
5132
5133 thread_local! { static COUNT: Cell<usize> = const { Cell::new(0) };
5135 }
5136
5137 pub fn reset() {
5138 COUNT.with(|c| c.set(0));
5139 }
5140
5141 pub fn count() -> usize {
5142 COUNT.with(Cell::get)
5143 }
5144
5145 pub fn note_build() {
5146 COUNT.with(|c| c.set(c.get() + 1));
5147 }
5148 }
5149
5150 fn dummy_oid() -> ObjectId {
5151 ObjectId::from_bytes(&[0u8; 20]).unwrap()
5152 }
5153
5154 fn make_entry(path: &str) -> IndexEntry {
5155 IndexEntry {
5156 ctime_sec: 0,
5157 ctime_nsec: 0,
5158 mtime_sec: 0,
5159 mtime_nsec: 0,
5160 dev: 0,
5161 ino: 0,
5162 mode: MODE_REGULAR,
5163 uid: 0,
5164 gid: 0,
5165 size: 0,
5166 oid: dummy_oid(),
5167 flags: path.len().min(0xFFF) as u16,
5168 flags_extended: None,
5169 path: path.as_bytes().to_vec(),
5170 base_index_pos: 0,
5171 }
5172 }
5173
5174 #[test]
5175 fn content_verified_bit_is_in_memory_only() {
5176 let mut index = Index::new();
5177 let mut plain = make_entry("a.txt");
5178 plain.mark_content_verified();
5179 let mut skipped = make_entry("b.txt");
5180 skipped.set_skip_worktree(true);
5181 skipped.mark_content_verified();
5182 index.add_or_replace(plain);
5183 index.add_or_replace(skipped);
5184
5185 let mut only_marked = index.clone();
5186 only_marked.remove(b"b.txt");
5187 let mut out = Vec::new();
5188 only_marked.serialize_into(&mut out).unwrap();
5189 assert_eq!(&out[4..8], &2u32.to_be_bytes(), "the bit alone keeps v2");
5190
5191 out.clear();
5192 index.serialize_into(&mut out).unwrap();
5193 let checksum = hash_index_body(index.hash_algo, &out);
5194 out.extend_from_slice(&checksum);
5195 let reread = Index::parse(&out).unwrap();
5196 assert_eq!(reread.version, 3);
5197 assert!(reread
5198 .entries
5199 .iter()
5200 .all(|e| !e.clone().take_content_verified()));
5201 assert!(reread.get(b"b.txt", 0).unwrap().skip_worktree());
5202
5203 assert!(index.get_mut(b"a.txt", 0).is_some());
5204 assert_eq!(
5205 index.entries[0].flags_extended, None,
5206 "get_mut voids the mark"
5207 );
5208 index.clear_content_verified();
5209 assert!(index.entries[1].skip_worktree());
5210 assert!(!index.entries[1].clone().take_content_verified());
5211 }
5212
5213 #[test]
5214 fn round_trip_empty_index() {
5215 let dir = TempDir::new().unwrap();
5216 let path = dir.path().join("index");
5217
5218 let mut idx = Index::new();
5219 idx.write(&path).unwrap();
5220
5221 let loaded = Index::load(&path).unwrap();
5222 assert_eq!(loaded.entries.len(), 0);
5223 }
5224
5225 #[test]
5226 fn opportunistic_write_still_current_logic() {
5227 use super::{opportunistic_index_write_still_current, IndexOnDiskIdentity};
5228
5229 let mut loaded = Index::new();
5230 loaded.source_checksum = Some(vec![1; 20]);
5231 loaded.source_mtime = Some((1, 0));
5232 loaded.source_file_len = Some(100);
5233 let same = IndexOnDiskIdentity {
5234 checksum: Some(vec![1; 20]),
5235 mtime: Some((1, 0)),
5236 len: 100,
5237 };
5238 assert!(opportunistic_index_write_still_current(&loaded, &same));
5239
5240 let changed = IndexOnDiskIdentity {
5241 checksum: Some(vec![2; 20]),
5242 mtime: Some((1, 0)),
5243 len: 100,
5244 };
5245 assert!(!opportunistic_index_write_still_current(&loaded, &changed));
5246
5247 loaded.source_checksum = Some(vec![0; 20]);
5248 assert!(!opportunistic_index_write_still_current(
5249 &loaded,
5250 &IndexOnDiskIdentity {
5251 checksum: Some(vec![0; 20]),
5252 mtime: Some((2, 0)),
5253 len: 100,
5254 }
5255 ));
5256
5257 let empty_loaded = Index::new();
5258 assert!(opportunistic_index_write_still_current(
5259 &empty_loaded,
5260 &IndexOnDiskIdentity {
5261 checksum: None,
5262 mtime: None,
5263 len: 0,
5264 }
5265 ));
5266 assert!(!opportunistic_index_write_still_current(
5267 &empty_loaded,
5268 &IndexOnDiskIdentity {
5269 checksum: Some(vec![1; 20]),
5270 mtime: Some((1, 0)),
5271 len: 50,
5272 }
5273 ));
5274 }
5275
5276 #[test]
5277 fn read_index_file_maps_only_large_files() {
5278 let dir = TempDir::new().unwrap();
5279 let threshold = usize::try_from(INDEX_MMAP_MIN_BYTES).unwrap();
5280 for (len, mapped) in [(0, false), (threshold - 1, false), (threshold, true)] {
5281 let path = dir.path().join(format!("index-{len}"));
5282 let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
5283 fs::write(&path, &contents).unwrap();
5284 let (bytes, mtime) = read_index_file(&path).unwrap();
5285 assert_eq!(matches!(bytes, IndexFileBytes::Mapped(_)), mapped, "{len}");
5286 assert_eq!(&*bytes, contents.as_slice());
5287 assert_eq!(Some(mtime), index_file_mtime(&path));
5288 }
5289 let missing = read_index_file(&dir.path().join("missing")).err().unwrap();
5290 assert_eq!(missing.kind(), io::ErrorKind::NotFound);
5291 }
5292
5293 #[test]
5294 fn round_trip_with_entries() {
5295 let dir = TempDir::new().unwrap();
5296 let path = dir.path().join("index");
5297
5298 let mut idx = Index::new();
5299 idx.add_or_replace(make_entry("foo.txt"));
5300 idx.add_or_replace(make_entry("bar/baz.txt"));
5301 idx.write(&path).unwrap(); let loaded = Index::load(&path).unwrap();
5304 assert_eq!(loaded.entries.len(), 2);
5305 assert_eq!(loaded.entries[0].path, b"bar/baz.txt");
5306 assert_eq!(loaded.entries[1].path, b"foo.txt");
5307 }
5308
5309 fn overwrite_fixture() -> Index {
5310 let mut idx = Index::new();
5311 for i in 0..200 {
5312 let mut e = make_entry(&format!("d{}/f{i:03}.txt", i % 4));
5313 e.base_index_pos = i + 1;
5314 idx.add_or_replace(e);
5315 }
5316 for stage in 1..=3u16 {
5317 let mut e = make_entry("d0/conflict.txt");
5318 e.flags |= stage << 12;
5319 idx.add_or_replace(e);
5320 }
5321 let leaf =
5322 |name: &str| CacheTreeNode::valid(name.as_bytes().to_vec(), 50, dummy_oid(), vec![]);
5323 idx.cache_tree_root = Some(dummy_oid());
5324 idx.cache_tree = Some(CacheTreeNode::valid(
5325 Vec::new(),
5326 203,
5327 dummy_oid(),
5328 vec![leaf("d0"), leaf("d1"), leaf("d2"), leaf("d3")],
5329 ));
5330 assert!(idx.entries_sorted);
5331 idx
5332 }
5333
5334 fn overwrite_replacements(count: usize) -> Vec<IndexEntry> {
5335 let changed = ObjectId::from_bytes(&[7u8; 20]).unwrap();
5336 let mut out: Vec<IndexEntry> = (0..count)
5337 .map(|i| {
5338 let mut e = make_entry(&format!("d{}/f{:03}.txt", (i * 3) % 4, i * 3));
5339 e.oid = changed;
5340 e.size = 9;
5341 e
5342 })
5343 .collect();
5344 out.sort_by(|a, b| a.path.cmp(&b.path));
5345 out
5346 }
5347
5348 #[test]
5349 fn overwrite_stage0_entries_matches_sorted_merge() {
5350 for count in [3, 64] {
5351 let mut merged = overwrite_fixture();
5352 merged.replace_stage0_entries_batch(overwrite_replacements(count));
5353 let mut overwritten = overwrite_fixture();
5354 overwritten.overwrite_stage0_entries(overwrite_replacements(count));
5355 assert_eq!(overwritten.entries, merged.entries, "count {count}");
5356 assert_eq!(overwritten.cache_tree, merged.cache_tree, "count {count}");
5357 assert_eq!(overwritten.cache_tree_root, merged.cache_tree_root);
5358 assert_eq!(overwritten.entries.len(), 203);
5359 }
5360 }
5361
5362 #[test]
5363 fn overwrite_stage0_entries_keeps_split_positions_and_invalidates_touched_dirs() {
5364 let mut idx = overwrite_fixture();
5365 let target = b"d1/f001.txt".to_vec();
5366 let pos = idx.find_entry_pos(&target, 0).unwrap();
5367 let before = idx.entries[pos].base_index_pos;
5368 let mut e = make_entry("d1/f001.txt");
5369 e.size = 42;
5370 idx.overwrite_stage0_entries(vec![e]);
5371 assert_eq!(idx.entries[pos].size, 42);
5372 assert_eq!(idx.entries[pos].base_index_pos, before);
5373 let tree = idx.cache_tree.as_ref().unwrap();
5374 assert!(!tree.is_valid());
5375 let child = |name: &[u8]| tree.children.iter().find(|c| c.name == name).unwrap();
5376 assert!(!child(b"d1").is_valid());
5377 assert!(child(b"d0").is_valid());
5378 }
5379
5380 #[test]
5381 fn overwrite_stage0_entries_merges_paths_without_a_stage0_row() {
5382 let mut idx = overwrite_fixture();
5383 let mut new_path = make_entry("d2/new.txt");
5384 new_path.size = 1;
5385 idx.overwrite_stage0_entries(vec![make_entry("d0/f000.txt"), new_path]);
5386 assert_eq!(idx.entries.len(), 204);
5387 assert!(idx.get(b"d2/new.txt", 0).is_some());
5388 assert_eq!(
5389 idx.entries
5390 .iter()
5391 .filter(|e| e.path == b"d0/conflict.txt")
5392 .count(),
5393 3
5394 );
5395 assert!(idx.entries.windows(2).all(|w| {
5396 Index::cmp_paths(&w[0].path, &w[1].path)
5397 .then_with(|| w[0].stage().cmp(&w[1].stage()))
5398 .is_lt()
5399 }));
5400 }
5401
5402 #[test]
5403 fn remove_descendants_under_path_drops_nested_only() {
5404 let mut idx = Index::new();
5405 idx.add_or_replace(make_entry("d/e"));
5406 idx.add_or_replace(make_entry("d-other"));
5407 idx.add_or_replace(make_entry("prefix/d"));
5408 idx.remove_descendants_under_path("d");
5409 let paths: Vec<_> = idx.entries.iter().map(|e| e.path.as_slice()).collect();
5410 assert_eq!(paths, vec![b"d-other".as_slice(), b"prefix/d".as_slice()]);
5411 }
5412
5413 #[test]
5414 fn fsmonitor_extension_round_trip_matches_git_layout() {
5415 let fsmn_payload: [u8; 41] = [
5417 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,
5418 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5419 ];
5420 let (token, dirty) = parse_fsmonitor_extension(&fsmn_payload).unwrap();
5421 assert_eq!(token, "builtin:fake");
5422 assert_eq!(dirty.bit_size, 0);
5423
5424 let dir = TempDir::new().unwrap();
5425 let path = dir.path().join("index");
5426 let mut idx = Index::new();
5427 idx.add_or_replace(make_entry("a"));
5428 if let Some(e) = idx.get_mut(b"a", 0) {
5429 e.set_fsmonitor_valid(true);
5430 }
5431 idx.fsmonitor_last_update = Some(token);
5432 idx.fsmonitor_dirty = Some(dirty);
5433 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5434 idx.write(&path).unwrap(); let loaded = Index::load(&path).unwrap();
5437 assert_eq!(
5438 loaded.fsmonitor_last_update.as_deref(),
5439 Some("builtin:fake")
5440 );
5441 assert_eq!(loaded.fsmonitor_dirty.as_ref().map(|b| b.bit_size), Some(0));
5442 assert_eq!(
5443 loaded.fsmonitor_raw_extension.as_deref(),
5444 Some(fsmn_payload.as_slice())
5445 );
5446
5447 let mut body = Vec::new();
5448 loaded.serialize_into(&mut body).unwrap();
5449 assert!(
5450 body.windows(4).any(|w| w == b"FSMN"),
5451 "serialized index must include FSMN"
5452 );
5453 let off = body.windows(4).position(|w| w == b"FSMN").unwrap();
5454 let ext_sz = u32::from_be_bytes(body[off + 4..off + 8].try_into().unwrap()) as usize;
5455 let written = &body[off + 8..off + 8 + ext_sz];
5456 assert_eq!(written, fsmn_payload);
5457 }
5458
5459 #[test]
5460 fn fsmonitor_extension_written_from_token_only_state() {
5461 let dir = TempDir::new().unwrap();
5462 let path = dir.path().join("index");
5463 let mut idx = Index::new();
5464 idx.add_or_replace(make_entry("a"));
5465 idx.set_fsmonitor_last_update(Some("builtin:fake".to_owned()));
5466 idx.write(&path).unwrap(); let data = fs::read(&path).unwrap();
5469 assert!(
5470 data.windows(4).any(|w| w == b"FSMN"),
5471 "token-only enable must emit a full FSMN extension"
5472 );
5473 let loaded = Index::load(&path).unwrap();
5474 assert_eq!(
5475 loaded.fsmonitor_last_update.as_deref(),
5476 Some("builtin:fake")
5477 );
5478 }
5479
5480 #[test]
5481 fn fsmonitor_raw_not_reused_after_token_change() {
5482 let fsmn_payload: [u8; 41] = [
5483 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,
5484 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5485 ];
5486 let mut idx = Index::new();
5487 idx.add_or_replace(make_entry("a"));
5488 idx.fsmonitor_last_update = Some("builtin:fake".to_owned());
5489 idx.fsmonitor_dirty = Some(parse_fsmonitor_extension(&fsmn_payload).unwrap().1);
5490 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5491
5492 idx.set_fsmonitor_last_update(Some("last_update_token".to_owned()));
5493 let mut body = Vec::new();
5494 idx.serialize_into(&mut body).unwrap();
5495 let off = body.windows(4).position(|w| w == b"FSMN").unwrap();
5496 let ext_sz = u32::from_be_bytes(body[off + 4..off + 8].try_into().unwrap()) as usize;
5497 let written = &body[off + 8..off + 8 + ext_sz];
5498 let (token, _) = parse_fsmonitor_extension(written).unwrap();
5499 assert_eq!(token, "last_update_token");
5500 assert_ne!(written, fsmn_payload);
5501 }
5502
5503 #[test]
5504 fn fsmonitor_raw_not_reused_after_validity_change() {
5505 let fsmn_payload: [u8; 41] = [
5506 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,
5507 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5508 ];
5509 let mut idx = Index::new();
5510 idx.add_or_replace(make_entry("a"));
5511 idx.fsmonitor_last_update = Some("builtin:fake".to_owned());
5512 idx.fsmonitor_dirty = Some(parse_fsmonitor_extension(&fsmn_payload).unwrap().1);
5513 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5514 if let Some(e) = idx.get_mut(b"a", 0) {
5515 e.set_fsmonitor_valid(false);
5516 }
5517 idx.fsmonitor_validity_changed();
5518
5519 let mut body = Vec::new();
5520 idx.serialize_into(&mut body).unwrap();
5521 let off = body.windows(4).position(|w| w == b"FSMN").unwrap();
5522 let ext_sz = u32::from_be_bytes(body[off + 4..off + 8].try_into().unwrap()) as usize;
5523 let written = &body[off + 8..off + 8 + ext_sz];
5524 assert_ne!(written, fsmn_payload);
5525 let (_, dirty) = parse_fsmonitor_extension(written).unwrap();
5526 assert!(dirty.bit_size >= 1);
5527 }
5528
5529 fn make_staged_entry(path: &str, stage: u8) -> IndexEntry {
5530 let mut e = make_entry(path);
5531 e.set_stage(stage);
5532 e
5533 }
5534
5535 struct LinearIndex {
5537 entries: Vec<IndexEntry>,
5538 resolve_undo: Option<BTreeMap<Vec<u8>, ResolveUndoRecord>>,
5539 }
5540
5541 impl LinearIndex {
5542 fn from_index(idx: &Index) -> Self {
5543 Self {
5544 entries: idx.entries.clone(),
5545 resolve_undo: idx.resolve_undo.clone(),
5546 }
5547 }
5548
5549 fn assert_matches(&self, idx: &Index) {
5550 let mut actual = idx.entries.clone();
5551 actual.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
5552 let mut expect = self.entries.clone();
5553 expect.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
5554 assert_eq!(actual, expect);
5555 assert_eq!(idx.resolve_undo, self.resolve_undo);
5556 }
5557
5558 fn add_or_replace(&mut self, entry: IndexEntry) {
5559 if let Some(pos) = self
5560 .entries
5561 .iter()
5562 .position(|e| e.path == entry.path && e.stage() == entry.stage())
5563 {
5564 self.entries[pos] = entry;
5565 } else {
5566 self.entries.push(entry);
5567 }
5568 self.entries
5569 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
5570 }
5571
5572 fn remove(&mut self, path: &[u8]) -> bool {
5573 let mut removed_any = false;
5574 for e in &self.entries {
5575 if e.path == path {
5576 if e.stage() != 0 {
5577 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
5578 }
5579 removed_any = true;
5580 }
5581 }
5582 if !removed_any {
5583 return false;
5584 }
5585 self.entries.retain(|e| e.path != path);
5586 removed_any
5587 }
5588
5589 fn stage_file(&mut self, entry: IndexEntry) {
5590 let path = entry.path.clone();
5591 for e in &self.entries {
5592 if e.path == path && e.stage() != 0 {
5593 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
5594 }
5595 }
5596 self.entries.retain(|e| e.path != path || e.stage() == 0);
5597 self.add_or_replace(entry);
5598 }
5599
5600 fn remove_descendants_under_path(&mut self, path: &str) {
5601 let prefix = path.as_bytes();
5602 if prefix.is_empty() {
5603 return;
5604 }
5605 let plen = prefix.len();
5606 for e in self.entries.iter() {
5607 let ep = e.path.as_slice();
5608 if ep.len() > plen && ep.starts_with(prefix) && ep[plen] == b'/' && e.stage() != 0 {
5609 resolve_undo::record_resolve_undo_for_entry(&mut self.resolve_undo, e);
5610 }
5611 }
5612 self.entries.retain(|e| {
5613 let ep = e.path.as_slice();
5614 if ep.len() <= plen {
5615 return true;
5616 }
5617 if !ep.starts_with(prefix) {
5618 return true;
5619 }
5620 ep[plen] != b'/'
5621 });
5622 }
5623 }
5624
5625 #[test]
5626 fn sorted_ops_match_linear_oracle() {
5627 let paths: Vec<Vec<u8>> = [
5628 "a", "a-b", "a/b", "a/b/c", "aa", "b", "dir/x", "dir/x/y", "dir-xy",
5629 ]
5630 .into_iter()
5631 .map(|p| p.as_bytes().to_vec())
5632 .collect();
5633
5634 let mut idx = Index::new();
5635 let mut oracle = LinearIndex::from_index(&idx);
5636 let mut rng_state: u64 = 376;
5637
5638 fn next_u64(state: &mut u64) -> u64 {
5639 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
5640 *state
5641 }
5642
5643 for step in 0..400 {
5644 let op = next_u64(&mut rng_state) % 4;
5645 match op {
5646 0 => {
5647 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5648 let stage = (next_u64(&mut rng_state) % 4) as u8;
5649 let mut e = make_staged_entry(std::str::from_utf8(p).unwrap(), stage);
5650 e.oid = ObjectId::from_bytes(&[(step % 256) as u8; 20]).unwrap();
5651 idx.add_or_replace(e.clone());
5652 oracle.add_or_replace(e);
5653 }
5654 1 => {
5655 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5656 idx.remove(p);
5657 oracle.remove(p);
5658 }
5659 2 => {
5660 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5661 let e = make_entry(std::str::from_utf8(p).unwrap());
5662 idx.stage_file(e.clone());
5663 oracle.stage_file(e);
5664 }
5665 _ => {
5666 let p = &paths[(next_u64(&mut rng_state) as usize) % paths.len()];
5667 let ps = std::str::from_utf8(p).unwrap();
5668 idx.remove_descendants_under_path(ps);
5669 oracle.remove_descendants_under_path(ps);
5670 }
5671 }
5672 oracle.assert_matches(&idx);
5673 }
5674 }
5675
5676 #[test]
5677 fn stale_sorted_flag_miss_until_resorted() {
5678 let mut idx = Index::new();
5679 idx.entries.push(make_entry("z"));
5680 idx.entries.push(make_entry("a"));
5681 idx.entries.push(make_entry("b"));
5682 idx.entries_sorted = true;
5683 assert!(!idx.entries_are_sorted());
5684 assert!(idx.get(b"z", 0).is_none());
5685 idx.sort();
5686 assert!(idx.get(b"z", 0).is_some());
5687 assert!(idx.remove(b"z"));
5688 assert!(idx.get(b"z", 0).is_none());
5689 }
5690
5691 #[test]
5692 fn write_restores_canonical_order_when_sorted_flag_is_stale() {
5693 let mut idx = Index::new();
5694 idx.entries.push(make_entry("zzz.txt"));
5695 idx.entries.push(make_entry("aaa.txt"));
5696 idx.entries_sorted = true;
5697 assert!(!idx.entries_are_sorted());
5698 let dir = tempfile::tempdir().expect("tempdir");
5699 let path = dir.path().join("index");
5700 idx.write(&path).expect("write index");
5701 let parsed = Index::load(&path).expect("load index");
5702 assert!(parsed.entries_are_sorted());
5703 assert_eq!(parsed.entries[0].path, b"aaa.txt".as_slice());
5704 }
5705
5706 #[test]
5707 fn find_entry_pos_sorted_miss_is_constant() {
5708 const N: usize = 100_000;
5709 const M: usize = 1_000;
5710 let mut idx = Index::new();
5711 for i in 0..N {
5712 idx.add_or_replace(make_entry(&format!("file/{i:06}.txt")));
5713 }
5714 assert!(idx.entries_sorted);
5715 let misses: Vec<Vec<u8>> = (0..M)
5716 .map(|i| format!("missing/{i:06}.txt").into_bytes())
5717 .collect();
5718 index_path_cmp::reset();
5719 for path in &misses {
5720 assert!(idx.get(path, 0).is_none());
5721 }
5722 let cmps = index_path_cmp::take();
5723 let log_n = (N as f64).log2().ceil() as usize;
5724 let bound = 100 * M * log_n;
5725 assert!(
5726 cmps < bound,
5727 "sorted miss path comparisons {cmps} should stay under {bound} (N={N}, M={M})"
5728 );
5729 }
5730
5731 fn sorted_index(entries: Vec<IndexEntry>) -> Index {
5732 let mut idx = Index::new();
5733 idx.entries = entries;
5734 idx.sort();
5735 idx
5736 }
5737
5738 fn paths_of(entries: &[IndexEntry]) -> Vec<&str> {
5739 entries
5740 .iter()
5741 .map(|e| std::str::from_utf8(&e.path).unwrap())
5742 .collect()
5743 }
5744
5745 #[test]
5746 fn sorted_entries_under_returns_the_directory_run_at_every_stage() {
5747 let idx = sorted_index(vec![
5748 make_entry("a-b"),
5749 make_entry("a.txt"),
5750 make_entry("a/x"),
5751 make_staged_entry("a/y", 2),
5752 make_staged_entry("a/y", 3),
5753 make_entry("a/z/deep"),
5754 make_entry("a0"),
5755 make_entry("ab/c"),
5756 ]);
5757 let run = idx.sorted_entries_under(b"a").unwrap();
5758 assert_eq!(paths_of(run), vec!["a/x", "a/y", "a/y", "a/z/deep"]);
5759 assert_eq!(
5760 paths_of(idx.sorted_entries_under(b"a/z").unwrap()),
5761 vec!["a/z/deep"]
5762 );
5763 assert!(idx.sorted_entries_under(b"a.txt").unwrap().is_empty());
5764 assert!(idx.sorted_entries_under(b"missing").unwrap().is_empty());
5765 assert_eq!(idx.sorted_entries_under(b"").unwrap().len(), 8);
5766 }
5767
5768 #[test]
5769 fn sorted_lookups_decline_unsorted_indexes() {
5770 let mut idx = sorted_index(vec![make_entry("b"), make_entry("a/x")]);
5771 assert_eq!(idx.sorted_contains_path(b"a/x"), Some(true));
5772 assert_eq!(idx.sorted_contains_path(b"a"), Some(false));
5773 idx.mark_entries_unsorted();
5774 assert!(idx.sorted_entries_under(b"a").is_none());
5775 assert_eq!(idx.sorted_contains_path(b"a/x"), None);
5776 }
5777
5778 #[test]
5779 fn sorted_contains_path_matches_conflict_stages() {
5780 let idx = sorted_index(vec![
5781 make_staged_entry("both.txt", 2),
5782 make_staged_entry("both.txt", 3),
5783 make_entry("plain.txt"),
5784 ]);
5785 assert_eq!(idx.sorted_contains_path(b"both.txt"), Some(true));
5786 assert_eq!(idx.sorted_contains_path(b"plain.txt"), Some(true));
5787 assert_eq!(idx.sorted_contains_path(b"both"), Some(false));
5788 }
5789
5790 #[test]
5791 fn has_stage0_descendant_under_skips_conflict_only_directories() {
5792 let idx = sorted_index(vec![
5793 make_staged_entry("conf/f", 1),
5794 make_staged_entry("conf/f", 2),
5795 make_entry("conf.txt"),
5796 make_entry("mixed/a"),
5797 make_staged_entry("mixed/b", 3),
5798 ]);
5799 assert!(!idx.has_stage0_descendant_under("conf"));
5800 assert!(idx.has_stage0_descendant_under("mixed"));
5801 assert!(!idx.has_stage0_descendant_under("conf.txt"));
5802 assert!(!idx.has_stage0_descendant_under("mix"));
5803 }
5804
5805 #[test]
5806 fn unsorted_remove_drops_every_matching_stage_globally() {
5807 let mut idx = Index::new();
5808 idx.mark_entries_unsorted();
5809 idx.entries.push(make_staged_entry("a", 1));
5810 idx.entries.push(make_entry("z"));
5811 idx.entries.push(make_staged_entry("a", 2));
5812 assert!(idx.remove(b"a"));
5813 assert!(idx.get(b"a", 1).is_none());
5814 assert!(idx.get(b"a", 2).is_none());
5815 assert!(idx.get(b"z", 0).is_some());
5816 }
5817
5818 #[test]
5819 fn unsorted_remove_descendants_drops_noncontiguous_matches() {
5820 let mut idx = Index::new();
5821 idx.mark_entries_unsorted();
5822 idx.entries.push(make_entry("a/x"));
5823 idx.entries.push(make_entry("z"));
5824 idx.entries.push(make_entry("a/y"));
5825 idx.remove_descendants_under_path("a");
5826 let paths: Vec<_> = idx.entries.iter().map(|e| e.path.as_slice()).collect();
5827 assert_eq!(paths, vec![b"z".as_slice()]);
5828 }
5829
5830 #[test]
5831 fn sorted_ops_match_linear_oracle_unsorted_noncontiguous() {
5832 let mut idx = Index::new();
5833 idx.mark_entries_unsorted();
5834 idx.entries.push(make_entry("a/x"));
5835 idx.entries.push(make_entry("z"));
5836 idx.entries.push(make_staged_entry("a", 1));
5837 idx.remove(b"a");
5838 idx.entries.push(make_entry("a/y"));
5839 idx.sort();
5840 let mut oracle = LinearIndex::from_index(&idx);
5841 oracle.entries = idx.entries.clone();
5842 oracle.assert_matches(&idx);
5843 idx.remove_descendants_under_path("a");
5844 oracle.remove_descendants_under_path("a");
5845 oracle.assert_matches(&idx);
5846 }
5847
5848 #[test]
5849 fn merge_replacements_preserves_base_index_pos() {
5850 let mut idx = Index::new();
5851 let mut kept = make_entry("path/a.txt");
5852 kept.base_index_pos = 42;
5853 idx.add_or_replace(kept);
5854 let mut replacement = make_entry("path/a.txt");
5855 replacement.oid = dummy_oid();
5856 idx.remove_paths_and_insert([], [replacement]);
5857 assert_eq!(idx.entries[0].base_index_pos, 42);
5858 }
5859
5860 #[test]
5861 fn remove_paths_and_insert_replaces_equal_path_without_duplicates() {
5862 let mut idx = Index::new();
5863 idx.add_or_replace(make_entry("path/a.txt"));
5864 idx.add_or_replace(make_entry("path/b.txt"));
5865 let mut replacement = make_entry("path/a.txt");
5866 replacement.oid = dummy_oid();
5867 idx.remove_paths_and_insert(["ghost.txt".as_bytes()], [replacement.clone()]);
5868 assert_eq!(idx.entries.len(), 2);
5869 assert_eq!(idx.entries[0].path, b"path/a.txt");
5870 assert_eq!(idx.entries[0].oid, replacement.oid);
5871 assert_eq!(idx.entries[1].path, b"path/b.txt");
5872 }
5873
5874 #[test]
5875 fn remove_directory_prefixes_drops_nested_paths() {
5876 let mut idx = Index::new();
5877 idx.add_or_replace(make_entry("d005/f0.txt"));
5878 idx.add_or_replace(make_entry("d005/f1.txt"));
5879 idx.add_or_replace(make_entry("d006/f0.txt"));
5880 idx.add_or_replace(make_entry("keep.txt"));
5881 idx.remove_directory_prefixes(&BTreeSet::from(["d005".to_owned()]));
5882 assert!(idx.get(b"d005/f0.txt", 0).is_none());
5883 assert!(idx.get(b"d005/f1.txt", 0).is_none());
5884 assert!(idx.get(b"d006/f0.txt", 0).is_some());
5885 assert!(idx.get(b"keep.txt", 0).is_some());
5886 }
5887
5888 #[test]
5889 fn remove_prefixes_paths_and_insert_combines_prefix_exact_and_replacements() {
5890 let mut idx = Index::new();
5891 idx.add_or_replace(make_entry("d005/f0.txt"));
5892 idx.add_or_replace(make_entry("d005/f1.txt"));
5893 idx.add_or_replace(make_entry("d006/f0.txt"));
5894 idx.add_or_replace(make_entry("solo.txt"));
5895 let mut replacement = make_entry("solo.txt");
5896 replacement.oid = dummy_oid();
5897 idx.remove_prefixes_paths_and_insert(
5898 &BTreeSet::from(["d005".to_owned()]),
5899 [b"d006/f0.txt".as_slice()],
5900 [replacement.clone()],
5901 );
5902 assert!(idx.get(b"d005/f0.txt", 0).is_none());
5903 assert!(idx.get(b"d005/f1.txt", 0).is_none());
5904 assert!(idx.get(b"d006/f0.txt", 0).is_none());
5905 assert_eq!(idx.get(b"solo.txt", 0).unwrap().oid, replacement.oid);
5906 }
5907
5908 #[test]
5909 fn remove_directory_prefixes_does_not_skip_lexically_later_prefix_children() {
5910 let mut idx = Index::new();
5911 idx.add_or_replace(make_entry("d005/f0.txt"));
5912 idx.add_or_replace(make_entry("d005/f1.txt"));
5913 idx.remove_directory_prefixes(&BTreeSet::from(["d005".to_owned(), "d0050".to_owned()]));
5914 assert!(idx.get(b"d005/f0.txt", 0).is_none());
5915 assert!(idx.get(b"d005/f1.txt", 0).is_none());
5916 }
5917
5918 #[test]
5919 fn remove_is_sublinear() {
5920 const N: usize = 100_000;
5921 const M: usize = 1_000;
5922 let mut idx = Index::new();
5923 for i in 0..N {
5924 idx.add_or_replace(make_entry(&format!("file/{i:06}.txt")));
5925 }
5926 let paths: Vec<Vec<u8>> = (0..M)
5927 .map(|i| format!("file/{i:06}.txt").into_bytes())
5928 .collect();
5929 index_path_cmp::reset();
5930 idx.remove_paths(paths.iter().map(|p| p.as_slice()));
5931 let cmps = index_path_cmp::take();
5932 let log_n = (N as f64).log2().ceil() as usize;
5933 let bound = 100 * M * log_n;
5934 assert!(
5935 cmps < bound,
5936 "path comparisons {cmps} should stay under {bound} (N={N}, M={M})"
5937 );
5938 assert_eq!(idx.entries.len(), N - M);
5939 }
5940
5941 #[test]
5942 fn fsmonitor_rebuild_once_per_batch() {
5943 let fsmn_payload: [u8; 41] = [
5944 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,
5945 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
5946 ];
5947 let (token, dirty) = parse_fsmonitor_extension(&fsmn_payload).unwrap();
5948 let dir = TempDir::new().unwrap();
5949 let path = dir.path().join("index");
5950
5951 let mut idx = Index::new();
5952 for i in 0..1000 {
5953 idx.add_or_replace(make_entry(&format!("path-{i:04}.txt")));
5954 }
5955 idx.sort();
5956 for e in &mut idx.entries {
5957 e.set_fsmonitor_valid(true);
5958 }
5959 idx.fsmonitor_last_update = Some(token);
5960 idx.fsmonitor_dirty = Some(dirty);
5961 idx.fsmonitor_raw_extension = Some(fsmn_payload.to_vec());
5962
5963 fsmonitor_rebuild_counter::reset();
5964 for i in 0..1000 {
5965 idx.add_or_replace(make_entry(&format!("mut-{i:04}.txt")));
5966 }
5967 assert_eq!(
5968 fsmonitor_rebuild_counter::count(),
5969 0,
5970 "entry mutations must not scan entries for the EWAH"
5971 );
5972 idx.write(&path).unwrap(); assert_eq!(
5974 fsmonitor_rebuild_counter::count(),
5975 1,
5976 "write must build the EWAH once for the batch"
5977 );
5978 }
5979
5980 #[test]
5981 fn requested_v4_writes_v4_on_disk() {
5982 let dir = TempDir::new().unwrap();
5983 let path = dir.path().join("index");
5984
5985 let mut idx = Index {
5986 version: 4,
5987 ..Index::default()
5988 };
5989 idx.add_or_replace(make_entry("one"));
5990 idx.add_or_replace(make_entry("two/one"));
5991 idx.write(&path).unwrap(); let data = fs::read(&path).unwrap();
5994 assert_eq!(&data[4..8], &4u32.to_be_bytes());
5995
5996 let loaded = Index::load(&path).unwrap();
5997 assert_eq!(loaded.version, 4);
5998 assert_eq!(loaded.entries[0].path, b"one");
5999 assert_eq!(loaded.entries[1].path, b"two/one");
6000 }
6001
6002 #[test]
6003 fn stat_match_policy_from_repo_config() {
6004 let dir = TempDir::new().unwrap();
6005 let git_dir = dir.path().join(".git");
6006 fs::create_dir_all(&git_dir).unwrap();
6007 fs::write(
6008 git_dir.join("config"),
6009 "[core]\n\ttrustctime = false\n\tcheckstat = minimal\n\tusenanosec = true\n",
6010 )
6011 .unwrap();
6012 let cfg = crate::config::ConfigSet::load(
6013 &crate::environment::Environment::empty(),
6014 Some(&git_dir),
6015 true,
6016 )
6017 .unwrap();
6018 let policy = StatMatchPolicy::from_config(Some(&cfg));
6019 assert!(!policy.trust_ctime);
6020 assert!(!policy.check_stat);
6021 assert!(policy.use_nanosec);
6022 }
6023
6024 fn policy_from_core_config(body: &str) -> StatMatchPolicy {
6025 let dir = TempDir::new().unwrap();
6026 let git_dir = dir.path().join(".git");
6027 fs::create_dir_all(&git_dir).unwrap();
6028 fs::write(git_dir.join("config"), format!("[core]\n{body}")).unwrap();
6029 let cfg = crate::config::ConfigSet::load(
6030 &crate::environment::Environment::empty(),
6031 Some(&git_dir),
6032 true,
6033 )
6034 .unwrap();
6035 StatMatchPolicy::from_config(Some(&cfg))
6036 }
6037
6038 #[test]
6039 fn stat_match_policy_uses_nanoseconds_by_default_on_windows_only() {
6040 assert_eq!(StatMatchPolicy::DEFAULT_USE_NANOSEC, cfg!(windows));
6041 assert_eq!(StatMatchPolicy::default().use_nanosec, cfg!(windows));
6042 assert_eq!(
6043 StatMatchPolicy::from_config(None),
6044 StatMatchPolicy::default()
6045 );
6046 assert_eq!(
6047 policy_from_core_config("\tbare = false\n"),
6048 StatMatchPolicy::default()
6049 );
6050 assert!(!policy_from_core_config("\tusenanosec = false\n").use_nanosec);
6051 assert!(policy_from_core_config("\tusenanosec = true\n").use_nanosec);
6052 }
6053
6054 const NSEC_POLICY: StatMatchPolicy = StatMatchPolicy {
6055 trust_ctime: true,
6056 check_stat: true,
6057 use_nanosec: true,
6058 };
6059 const WHOLE_SECOND_POLICY: StatMatchPolicy = StatMatchPolicy {
6060 use_nanosec: false,
6061 ..NSEC_POLICY
6062 };
6063
6064 fn entry_with_mtime(sec: u32, nsec: u32) -> IndexEntry {
6065 IndexEntry {
6066 mtime_sec: sec,
6067 mtime_nsec: nsec,
6068 ctime_sec: sec,
6069 ctime_nsec: nsec,
6070 size: 42,
6071 ..make_entry("f")
6072 }
6073 }
6074
6075 #[test]
6076 fn nanosecond_policy_trusts_same_second_entries_but_sees_newer_fractions() {
6077 let ie = entry_with_mtime(1_700_000_000, 123_456_700);
6078 let index_mtime = Some((ie.mtime_sec, ie.mtime_nsec + 1));
6079 assert!(!crate::diff::entry_is_racy_with_policy(
6080 &ie,
6081 index_mtime,
6082 NSEC_POLICY
6083 ));
6084 assert!(crate::diff::entry_is_racy_with_policy(
6085 &ie,
6086 Some((ie.mtime_sec, ie.mtime_nsec)),
6087 NSEC_POLICY
6088 ));
6089 assert!(
6090 crate::diff::entry_is_racy_with_policy(&ie, index_mtime, WHOLE_SECOND_POLICY),
6091 "the whole-second rule keeps the index's own second racy"
6092 );
6093
6094 let mut edited = IndexStatFields::of_entry(&ie);
6096 edited.mtime_nsec += 1;
6097 assert!(IndexStatFields::of_entry(&ie).matches_index_entry(&ie, NSEC_POLICY));
6098 assert!(!edited.matches_index_entry(&ie, NSEC_POLICY));
6099 assert!(edited.matches_index_entry(&ie, WHOLE_SECOND_POLICY));
6100 }
6101
6102 #[test]
6103 fn minimal_checkstat_keeps_whole_second_racy_rule_under_nanosec() {
6104 let minimal = StatMatchPolicy {
6105 check_stat: false,
6106 ..NSEC_POLICY
6107 };
6108 let ie = entry_with_mtime(1_700_000_000, 100_000_000);
6109 let index_mtime = Some((ie.mtime_sec, 500_000_000));
6110 assert!(
6111 crate::diff::entry_is_racy_with_policy(&ie, index_mtime, minimal),
6112 "without fraction equality, a same-second entry must stay racy"
6113 );
6114 let mut edited = IndexStatFields::of_entry(&ie);
6115 edited.mtime_nsec = 200_000_000;
6116 assert!(edited.matches_index_entry(&ie, minimal));
6117 assert!(!crate::diff::entry_is_racy_with_policy(
6118 &ie,
6119 Some((ie.mtime_sec + 1, 0)),
6120 minimal
6121 ));
6122 }
6123
6124 #[test]
6125 fn advancing_index_mtime_never_leaves_the_last_tick_of_a_second() {
6126 let dir = TempDir::new().unwrap();
6127 let index_path = dir.path().join("index");
6128 let sec = 1_700_000_000u32;
6129 let last_tick = if cfg!(windows) {
6130 999_999_900
6131 } else {
6132 999_999_999
6133 };
6134 let mut index = Index::new();
6135 index.add_or_replace(entry_with_mtime(sec, last_tick));
6136 index.write(&index_path).unwrap();
6137 let pinned = filetime::FileTime::from_unix_time(i64::from(sec), last_tick);
6138 filetime::set_file_mtime(&index_path, pinned).unwrap();
6139
6140 advance_index_file_mtime_past_entries(&index, &index_path, None).unwrap();
6141 assert_eq!(index_file_mtime(&index_path), Some((sec, last_tick)));
6142 for policy in [NSEC_POLICY, WHOLE_SECOND_POLICY] {
6143 assert!(crate::diff::entry_is_racy_with_policy(
6144 &index.entries[0],
6145 index_file_mtime(&index_path),
6146 policy
6147 ));
6148 }
6149 }
6150
6151 #[test]
6152 fn size_and_times_match_index_compares_fractions_only_under_nanosec_policy() {
6153 let ie = entry_with_mtime(1_700_000_000, 123_456_700);
6154 let mut scan = IndexStatFields::of_entry(&ie);
6155 scan.ctime_sec += 5;
6156 scan.ino = 99;
6157 assert!(scan.size_and_times_match_index(&ie, NSEC_POLICY));
6158
6159 let mut mtime_frac = IndexStatFields::of_entry(&ie);
6160 mtime_frac.mtime_nsec += 100;
6161 assert!(!mtime_frac.size_and_times_match_index(&ie, NSEC_POLICY));
6162 assert!(mtime_frac.size_and_times_match_index(&ie, WHOLE_SECOND_POLICY));
6163
6164 let mut ctime_frac = IndexStatFields::of_entry(&ie);
6165 ctime_frac.ctime_nsec += 100;
6166 assert!(!ctime_frac.size_and_times_match_index(&ie, NSEC_POLICY));
6167 let no_ctime = StatMatchPolicy {
6168 trust_ctime: false,
6169 ..NSEC_POLICY
6170 };
6171 assert!(ctime_frac.size_and_times_match_index(&ie, no_ctime));
6172
6173 let mut resized = IndexStatFields::of_entry(&ie);
6174 resized.size += 1;
6175 assert!(!resized.size_and_times_match_index(&ie, WHOLE_SECOND_POLICY));
6176 }
6177
6178 #[test]
6179 fn advancing_index_mtime_clears_same_tick_entries_within_their_second() {
6180 let dir = TempDir::new().unwrap();
6181 let index_path = dir.path().join("index");
6182 let (sec, nsec) = (1_700_000_000u32, 123_456_700u32);
6183 let mut index = Index::new();
6184 index.add_or_replace(entry_with_mtime(sec, nsec));
6185 index.write(&index_path).unwrap();
6186 filetime::set_file_mtime(
6187 &index_path,
6188 filetime::FileTime::from_unix_time(i64::from(sec), nsec),
6189 )
6190 .unwrap();
6191 let ie = &index.entries[0];
6192 assert!(crate::diff::entry_is_racy_with_policy(
6193 ie,
6194 index_file_mtime(&index_path),
6195 NSEC_POLICY
6196 ));
6197
6198 advance_index_file_mtime_past_entries(&index, &index_path, None).unwrap();
6199 let bumped = index_file_mtime(&index_path);
6200 assert_eq!(
6201 bumped.map(|(s, _)| s),
6202 Some(sec),
6203 "never leave the entry's second"
6204 );
6205 assert!(
6206 !crate::diff::entry_is_racy_with_policy(ie, bumped, NSEC_POLICY),
6207 "bumped index mtime {bumped:?} must be past the entry's filesystem tick"
6208 );
6209 assert!(crate::diff::entry_is_racy_with_policy(
6210 ie,
6211 bumped,
6212 WHOLE_SECOND_POLICY
6213 ));
6214 }
6215
6216 #[test]
6217 fn index_entry_extended_flags_and_placeholders() {
6218 let mut e = make_entry("p");
6219 e.set_intent_to_add(true);
6220 assert!(e.intent_to_add());
6221 e.set_assume_unchanged(true);
6222 assert!(e.assume_unchanged());
6223 e.set_skip_worktree(true);
6224 assert!(e.skip_worktree());
6225 e.mode = MODE_TREE;
6226 e.set_skip_worktree(true);
6227 assert!(e.is_sparse_directory_placeholder());
6228 e.set_overlay_tree_skip_output(true);
6229 assert!(e.overlay_tree_skip_output());
6230 }
6231
6232 #[test]
6233 fn cache_tree_formatting_and_invalidation_helpers() {
6234 let mut idx = Index::new();
6235 idx.add_or_replace(make_entry("a/b"));
6236 let child = CacheTreeNode::valid(b"a".to_vec(), 1, dummy_oid(), vec![]);
6237 let root = CacheTreeNode::valid(b"".to_vec(), 1, dummy_oid(), vec![child]);
6238 idx.set_cache_tree(root);
6239 let dump = idx.format_cache_tree_dump();
6240 assert!(dump.contains("a/") || dump.contains("invalid"));
6241 idx.invalidate_cache_tree_for_path(b"a/b");
6242 idx.clear_cache_tree();
6243 assert!(idx.cache_tree.is_none());
6244 }
6245
6246 #[test]
6247 fn dedup_paths_and_stage0_descendant_queries() {
6248 let mut idx = Index::new();
6249 idx.add_or_replace(make_entry("dir/x"));
6250 idx.add_or_replace(make_entry("dir/x"));
6251 idx.dedup_paths_keep_last();
6252 assert_eq!(idx.entries.len(), 1);
6253 assert!(idx.has_stage0_descendant_under("dir"));
6254 assert!(!idx.has_stage0_descendant_under("missing"));
6255 }
6256
6257 #[test]
6258 fn load_missing_index_is_empty() {
6259 let dir = TempDir::new().unwrap();
6260 let path = dir.path().join("no-index");
6261 let idx = Index::load(&path).unwrap();
6262 assert!(idx.entries.is_empty());
6263 }
6264
6265 #[test]
6266 fn munge_and_stat_equivalence_helpers() {
6267 assert_eq!(munge_index_size(0x1_0000_0000), 0x8000_0000);
6268 let a = make_entry("x");
6269 let b = make_entry("x");
6270 assert!(index_entries_stat_equivalent(
6271 &a,
6272 &b,
6273 StatMatchPolicy::default()
6274 ));
6275 }
6276
6277 #[test]
6278 fn new_from_config_invalid_index_version_falls_back() {
6279 let dir = TempDir::new().unwrap();
6280 let git_dir = dir.path().join(".git");
6281 fs::create_dir_all(&git_dir).unwrap();
6282 fs::write(
6283 git_dir.join("config"),
6284 "[index]\n\tversion = not-a-number\n",
6285 )
6286 .unwrap();
6287 let cfg = crate::config::ConfigSet::load(
6288 &crate::environment::Environment::empty(),
6289 Some(&git_dir),
6290 true,
6291 )
6292 .unwrap();
6293 let idx = Index::new_from_config(&cfg);
6294 assert_eq!(idx.version, INDEX_CONFIG_INVALID_FALLBACK);
6295 }
6296
6297 #[test]
6298 fn fsmonitor_invalidate_directory_prefix_without_trailing_slash() {
6299 let mut idx = Index::new();
6300 idx.add_or_replace(make_entry("dir/a.txt"));
6301 idx.add_or_replace(make_entry("dir/b.txt"));
6302 idx.set_fsmonitor_last_update(Some("tok".into()));
6303 for entry in idx.entries_mut() {
6304 entry.set_fsmonitor_valid(true);
6305 }
6306 idx.invalidate_fsmonitor_for_path("dir", false);
6307 assert!(!idx.get(b"dir/a.txt", 0).unwrap().fsmonitor_valid());
6308 assert!(!idx.get(b"dir/b.txt", 0).unwrap().fsmonitor_valid());
6309 }
6310
6311 #[test]
6312 fn parse_fsmonitor_extension_rejects_truncated_ewah() {
6313 let mut payload = vec![0, 0, 0, 2, b't', 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0];
6314 let err = parse_fsmonitor_extension(&payload).unwrap_err();
6315 assert!(matches!(
6316 err,
6317 Error::IndexFsmonitorExtensionTruncated | Error::IndexFsmonitorExtensionEwahInvalid
6318 ));
6319 payload[12..16].copy_from_slice(&2u32.to_be_bytes());
6320 let err2 = parse_fsmonitor_extension(&payload).unwrap_err();
6321 assert!(matches!(
6322 err2,
6323 Error::IndexFsmonitorExtensionTruncated | Error::IndexFsmonitorExtensionEwahInvalid
6324 ));
6325 }
6326
6327 #[test]
6328 fn unmerge_from_resolve_undo_installs_conflict_stages() {
6329 use crate::resolve_undo::ResolveUndoRecord;
6330 use std::collections::BTreeMap;
6331
6332 let mut idx = Index::new();
6333 idx.add_or_replace(make_entry("f.txt"));
6334 let oid1 = dummy_oid();
6335 let oid2 = ObjectId::from_bytes(&[1u8; 20]).unwrap();
6336 idx.resolve_undo = Some(BTreeMap::from([(
6337 b"f.txt".to_vec(),
6338 ResolveUndoRecord {
6339 modes: [MODE_REGULAR, MODE_REGULAR, 0],
6340 oids: [oid1, oid2, ObjectId::zero()],
6341 },
6342 )]));
6343 assert!(idx.unmerge_path_from_resolve_undo(b"f.txt"));
6344 assert!(idx.has_unmerged_entries());
6345 }
6346
6347 #[test]
6348 fn parse_rejects_truncated_headers() {
6349 for n in 0..12 {
6350 assert!(Index::parse(&vec![0u8; n]).is_err());
6351 }
6352 }
6353
6354 #[test]
6355 fn entry_from_stat_populates_index_metadata() {
6356 let dir = TempDir::new().unwrap();
6357 fs::write(dir.path().join("f.txt"), b"payload").unwrap();
6358 let oid = dummy_oid();
6359 let entry = entry_from_stat(dir.path(), b"f.txt", oid, MODE_REGULAR).unwrap();
6360 assert_eq!(entry.path, b"f.txt");
6361 assert!(entry.size > 0);
6362 }
6363
6364 #[test]
6365 fn write_to_path_honors_skip_hash_flag() {
6366 let dir = TempDir::new().unwrap();
6367 let path = dir.path().join("index");
6368 let mut idx = Index::new();
6369 idx.add_or_replace(make_entry("x"));
6370 idx.write_to_path(&path, true).unwrap();
6371 let bytes = fs::read(&path).unwrap();
6372 assert!(bytes.ends_with(&[0u8; 20]));
6373 }
6374
6375 #[test]
6376 fn index_lock_acquire_blocks_second_holder() {
6377 let dir = TempDir::new().unwrap();
6378 let path = dir.path().join("index");
6379 let mut idx = Index::new();
6380 idx.add_or_replace(make_entry("tracked.txt"));
6381 let lock = IndexLock::acquire(&path, None).unwrap();
6382 assert!(matches!(
6383 IndexLock::acquire(&path, None),
6384 Err(Error::LockHeld { .. })
6385 ));
6386 drop(lock);
6387 idx.write(&path).unwrap(); assert_eq!(Index::load(&path).unwrap().entries.len(), 1);
6389 }
6390
6391 #[test]
6392 fn held_index_lock_is_a_typed_error_naming_the_holder() {
6393 let dir = TempDir::new().unwrap();
6394 let path = dir.path().join("index");
6395 let lock_path = dir.path().join("index.lock");
6396 fs::write(&lock_path, b"foreign").unwrap();
6397 fs::write(
6398 dir.path().join("index~pid.lock"),
6399 format!("pid {}\n", std::process::id()),
6400 )
6401 .unwrap();
6402
6403 let Err(acquire_err) = IndexLock::acquire(&path, None) else {
6404 panic!("lock must be refused");
6405 };
6406 let write_err = Index::new().write(&path).unwrap_err();
6407 for err in [acquire_err, write_err] {
6408 match &err {
6409 Error::LockHeld { path: p, holder } => {
6410 assert_eq!(p, &lock_path);
6411 assert_eq!(
6412 *holder,
6413 Some(crate::lockfile::LockHolder {
6414 pid: u64::from(std::process::id()),
6415 running: true,
6416 })
6417 );
6418 }
6419 other => panic!("expected LockHeld, got {other:?}"),
6420 }
6421 assert!(
6422 err.to_string().contains(&lock_path.display().to_string()),
6423 "{err}"
6424 );
6425 }
6426 assert_eq!(fs::read(&lock_path).unwrap(), b"foreign");
6427 assert!(!path.exists());
6428 }
6429
6430 fn streaming_fixture(version: u32) -> Index {
6431 let mut idx = Index::new();
6432 idx.version = version;
6433 for i in 0..6000 {
6434 idx.add_or_replace(make_entry(&format!("dir{:02}/sub/file-{i:05}.txt", i % 37)));
6435 }
6436 idx
6437 }
6438
6439 fn streaming_config() -> crate::config::ConfigSet {
6440 use crate::config::{ConfigFile, ConfigScope};
6441 let file = ConfigFile::parse(
6442 Path::new("config"),
6443 "[index]\n\tthreads = 4\n",
6444 ConfigScope::Local,
6445 )
6446 .unwrap();
6447 let mut cfg = crate::config::ConfigSet::new();
6448 cfg.merge(&file);
6449 cfg
6450 }
6451
6452 fn in_memory_index_file(
6453 idx: &Index,
6454 cfg: &crate::config::ConfigSet,
6455 policy: crate::parallel::ThreadPolicy,
6456 skip_hash: bool,
6457 ) -> Vec<u8> {
6458 let mut body = IndexBodyOut {
6459 buf: Vec::new(),
6460 drained: 0,
6461 sink: None,
6462 };
6463 idx.serialize_body(&mut body, Some(cfg), policy).unwrap();
6464 let mut file = body.buf;
6465 let checksum = if skip_hash {
6466 vec![0u8; idx.hash_algo.len()]
6467 } else {
6468 idx.hash_algo.checksum_digest(&file)
6469 };
6470 file.extend_from_slice(&checksum);
6471 file
6472 }
6473
6474 #[test]
6475 fn streamed_write_matches_in_memory_serialization_across_chunks() {
6476 let policy = crate::parallel::ThreadPolicy::with_threads_override(Some(4));
6477 let cfg = streaming_config();
6478 for version in [2, 4] {
6479 for skip_hash in [false, true] {
6480 let dir = TempDir::new().unwrap();
6481 let path = dir.path().join("index");
6482 let mut idx = streaming_fixture(version);
6483 let expected = in_memory_index_file(&idx, &cfg, policy, skip_hash);
6484 assert!(
6485 expected.len() > 3 * INDEX_WRITE_CHUNK,
6486 "fixture must span several chunks"
6487 );
6488 idx.write_to_path_with_config_and_policy(&path, skip_hash, Some(&cfg), policy)
6489 .unwrap();
6490 let written = fs::read(&path).unwrap();
6491 assert!(
6492 written == expected,
6493 "v{version} skip_hash={skip_hash}: streamed index differs from in-memory body"
6494 );
6495 assert!(!path.with_extension("lock").exists());
6496
6497 let ctx = crate::index::IndexParseContext::from_policy(policy);
6498 let loaded =
6499 Index::parse_with_algo_and_config(&written, idx.hash_algo, Some(&cfg), ctx)
6500 .unwrap();
6501 assert_eq!(loaded.entries.len(), idx.entries.len());
6502 assert!(loaded
6503 .entries
6504 .iter()
6505 .zip(&idx.entries)
6506 .all(|(a, b)| a.path == b.path && a.oid == b.oid));
6507 }
6508 }
6509 }
6510
6511 #[test]
6512 fn streamed_write_through_held_lock_matches_in_memory_serialization() {
6513 let policy = crate::parallel::ThreadPolicy::with_threads_override(Some(4));
6514 let cfg = streaming_config();
6515 let dir = TempDir::new().unwrap();
6516 let path = dir.path().join("index");
6517 let mut idx = streaming_fixture(2);
6518 let expected = in_memory_index_file(&idx, &cfg, policy, false);
6519
6520 let mut lock = IndexLock::acquire(&path, Some(&cfg)).unwrap();
6521 idx.write_to_path_with_config_and_lock_mode_policy(
6522 &path,
6523 false,
6524 Some(&cfg),
6525 Some(&mut lock),
6526 false,
6527 policy,
6528 )
6529 .unwrap();
6530 assert!(
6531 !path.exists(),
6532 "uncommitted lock must not replace the index"
6533 );
6534 assert!(fs::read(path.with_extension("lock")).unwrap() == expected);
6535 lock.commit_replace_index().unwrap();
6536 assert!(fs::read(&path).unwrap() == expected);
6537 assert!(!path.with_extension("lock").exists());
6538 }
6539
6540 #[test]
6541 fn index_body_out_drains_only_when_full_and_tracks_offsets() {
6542 let mut chunks: Vec<usize> = Vec::new();
6543 let mut sink = |chunk: &[u8]| -> io::Result<()> {
6544 chunks.push(chunk.len());
6545 Ok(())
6546 };
6547 let mut out = IndexBodyOut {
6548 buf: Vec::new(),
6549 drained: 0,
6550 sink: Some(&mut sink),
6551 };
6552 out.buf.resize(INDEX_WRITE_CHUNK - 1, 0);
6553 out.drain_if_full().unwrap();
6554 assert_eq!(out.position(), INDEX_WRITE_CHUNK - 1);
6555 out.buf.extend_from_slice(&[1, 2]);
6556 out.drain_if_full().unwrap();
6557 assert!(out.buf.is_empty());
6558 assert_eq!(out.position(), INDEX_WRITE_CHUNK + 1);
6559 out.buf.push(3);
6560 out.drain().unwrap();
6561 out.drain().unwrap();
6562 assert_eq!(out.position(), INDEX_WRITE_CHUNK + 2);
6563 assert_eq!(chunks, vec![INDEX_WRITE_CHUNK + 1, 1]);
6564
6565 let mut in_memory = IndexBodyOut {
6566 buf: vec![0; INDEX_WRITE_CHUNK * 2],
6567 drained: 0,
6568 sink: None,
6569 };
6570 in_memory.drain_if_full().unwrap();
6571 in_memory.drain().unwrap();
6572 assert_eq!(in_memory.buf.len(), INDEX_WRITE_CHUNK * 2);
6573 assert_eq!(in_memory.position(), INDEX_WRITE_CHUNK * 2);
6574 }
6575
6576 #[test]
6577 fn write_with_config_and_parallel_policy() {
6578 let dir = TempDir::new().unwrap();
6579 let path = dir.path().join("index");
6580 let mut idx = Index::new();
6581 idx.add_or_replace(make_entry("p"));
6582 let cfg = crate::config::ConfigSet::new();
6583 idx.write_with_config_and_policy(&path, &cfg, crate::parallel::ThreadPolicy::default())
6584 .unwrap();
6585 assert!(path.is_file());
6586 }
6587
6588 #[test]
6589 fn fsmonitor_sync_and_clear_helpers() {
6590 let mut idx = Index::new();
6591 idx.add_or_replace(make_entry("a"));
6592 idx.set_fsmonitor_last_update(Some("token".to_owned()));
6593 idx.sync_fsmonitor_dirty_bitmap();
6594 if let Some(e) = idx.get_mut(b"a", 0) {
6595 e.set_fsmonitor_valid(false);
6596 }
6597 idx.fsmonitor_validity_changed();
6598 idx.clear_fsmonitor_extension();
6599 assert!(idx.fsmonitor_last_update.is_none());
6600 }
6601
6602 #[test]
6603 fn intent_to_add_entry_forces_empty_blob_on_write_tree_path() {
6604 let dir = TempDir::new().unwrap();
6605 let odb = crate::odb::Odb::new(dir.path());
6606 let mut idx = Index::new();
6607 let mut e = make_entry("ita.txt");
6608 e.set_intent_to_add(true);
6609 idx.add_or_replace(e);
6610 crate::write_tree::write_tree_from_index(&odb, &idx, "").unwrap();
6611 }
6612
6613 #[test]
6614 fn optional_load_is_empty_only_when_the_index_is_missing() {
6615 let dir = TempDir::new().unwrap();
6616 let odb = crate::odb::Odb::new(dir.path());
6617 let path = dir.path().join("index");
6618 let missing = Index::load_expand_sparse_optional(&path, &odb).unwrap();
6619 assert!(missing.entries.is_empty());
6620
6621 for len in [0, 12, 31] {
6622 std::fs::write(&path, b"DIRC\0\0\0\x02\0\0\0\0".repeat(3)[..len].to_vec()).unwrap();
6623 assert!(
6624 matches!(
6625 Index::load_expand_sparse_optional(&path, &odb),
6626 Err(Error::IndexError(_))
6627 ),
6628 "len={len}"
6629 );
6630 }
6631 }
6632
6633 fn naive_merge_sorted(kept: &[IndexEntry], insert: &[IndexEntry]) -> Vec<IndexEntry> {
6634 let mut out: Vec<IndexEntry> = kept.iter().chain(insert.iter()).cloned().collect();
6635 out.sort_by(index_entry_path_stage_cmp);
6636 let mut deduped = Vec::with_capacity(out.len());
6637 for entry in out {
6638 if deduped
6639 .last()
6640 .is_some_and(|last| index_entry_path_stage_cmp(last, &entry).is_eq())
6641 {
6642 *deduped.last_mut().unwrap() = entry;
6643 } else {
6644 deduped.push(entry);
6645 }
6646 }
6647 deduped
6648 }
6649
6650 #[test]
6651 fn merge_sorted_index_halves_matches_naive_merge() {
6652 let paths = [
6653 ".gitattributes",
6654 "a.txt",
6655 "b.txt",
6656 "f.txt",
6657 "z.txt",
6658 "dir/nested.txt",
6659 ];
6660 let mut kept = Vec::new();
6661 let mut insert = Vec::new();
6662 for (i, path) in paths.iter().enumerate() {
6663 let mut e = make_entry(path);
6664 e.oid = ObjectId::from_bytes(&[i as u8; 20]).unwrap();
6665 kept.push(e);
6666 }
6667 for (i, path) in paths.iter().enumerate() {
6668 if i % 2 == 0 {
6669 continue;
6670 }
6671 let mut e = make_entry(path);
6672 e.oid = ObjectId::from_bytes(&[(i + 100) as u8; 20]).unwrap();
6673 insert.push(e);
6674 }
6675 kept.sort_by(index_entry_path_stage_cmp);
6676 insert.sort_by(index_entry_path_stage_cmp);
6677
6678 let expected = naive_merge_sorted(&kept, &insert);
6679 let merged = merge_sorted_index_entries(kept, insert);
6680 assert_eq!(merged, expected, "fixed scenario merge mismatch");
6681
6682 for seed in 0u64..200 {
6683 let mut kept = Vec::new();
6684 let mut insert = Vec::new();
6685 for n in 0..((seed % 7) + 1) {
6686 let path = format!("p/{seed}/{n}.txt");
6687 let mut e = make_entry(&path);
6688 e.oid = ObjectId::from_bytes(&[(seed as u8).wrapping_add(n as u8); 20]).unwrap();
6689 kept.push(e);
6690 }
6691 for n in 0..((seed % 5) + 1) {
6692 let path = format!("p/{seed}/{n}.txt");
6693 let mut e = make_entry(&path);
6694 e.oid = ObjectId::from_bytes(
6695 &[(seed as u8).wrapping_add(n as u8).wrapping_add(50); 20],
6696 )
6697 .unwrap();
6698 insert.push(e);
6699 }
6700 kept.sort_by(index_entry_path_stage_cmp);
6701 insert.sort_by(index_entry_path_stage_cmp);
6702 let expected = naive_merge_sorted(&kept, &insert);
6703 let merged = merge_sorted_index_entries(kept, insert);
6704 assert_eq!(merged, expected, "seed {seed} merge mismatch");
6705 }
6706 }
6707
6708 mod sparse_collapse {
6709 use super::*;
6710
6711 const PATTERNS: [&str; 3] = ["/*", "!/*/", "/a/"];
6712
6713 fn patterns() -> Vec<String> {
6714 PATTERNS.iter().map(|p| (*p).to_owned()).collect()
6715 }
6716
6717 fn write_tree(odb: &Odb, files: &[(&str, u32, &str)]) -> ObjectId {
6719 let mut here: BTreeMap<Vec<u8>, (u32, ObjectId)> = BTreeMap::new();
6720 let mut subdirs: BTreeMap<&str, Vec<(&str, u32, &str)>> = BTreeMap::new();
6721 for (path, mode, body) in files {
6722 match path.split_once('/') {
6723 Some((dir, rest)) => subdirs.entry(dir).or_default().push((rest, *mode, body)),
6724 None => {
6725 let oid = if *mode == MODE_GITLINK {
6726 ObjectId::from_bytes(&[7u8; 20]).unwrap()
6727 } else {
6728 odb.write(ObjectKind::Blob, body.as_bytes()).unwrap()
6729 };
6730 here.insert(path.as_bytes().to_vec(), (*mode, oid));
6731 }
6732 }
6733 }
6734 for (dir, children) in subdirs {
6735 here.insert(
6736 dir.as_bytes().to_vec(),
6737 (MODE_TREE, write_tree(odb, &children)),
6738 );
6739 }
6740 let mut entries: Vec<TreeEntry> = here
6741 .into_iter()
6742 .map(|(name, (mode, oid))| TreeEntry { mode, name, oid })
6743 .collect();
6744 entries.sort_by(|a, b| {
6745 crate::objects::tree_entry_cmp(
6746 &a.name,
6747 a.mode == MODE_TREE,
6748 &b.name,
6749 b.mode == MODE_TREE,
6750 )
6751 });
6752 odb.write(ObjectKind::Tree, &crate::objects::serialize_tree(&entries))
6753 .unwrap()
6754 }
6755
6756 fn subtree(odb: &Odb, root: &ObjectId, prefix: &str) -> ObjectId {
6757 PrefixTreeResolver::new(*root)
6758 .resolve(odb, prefix.as_bytes())
6759 .unwrap()
6760 .unwrap()
6761 }
6762
6763 fn expanded_index(odb: &Odb, root: &ObjectId) -> Index {
6766 let mut index = Index::new();
6767 let rows = flatten_tree_blobs(
6768 odb,
6769 root,
6770 b"",
6771 crate::path_validate::ProtectOptions::default(),
6772 )
6773 .unwrap();
6774 for mut row in rows {
6775 let in_cone = !row.path.contains(&b'/') || row.path.starts_with(b"a/");
6776 row.set_skip_worktree(!in_cone);
6777 index.entries_mut().push(row);
6778 }
6779 index.sort();
6780 index
6781 }
6782
6783 fn collapse(index: &mut Index, odb: &Odb, root: &ObjectId) {
6784 index
6785 .try_collapse_sparse_directories(
6786 odb,
6787 root,
6788 &patterns(),
6789 true,
6790 true,
6791 crate::path_validate::ProtectOptions::default(),
6792 )
6793 .unwrap();
6794 }
6795
6796 fn rows(index: &Index) -> Vec<(String, u32, ObjectId)> {
6797 index
6798 .entries()
6799 .iter()
6800 .map(|e| (String::from_utf8_lossy(&e.path).into_owned(), e.mode, e.oid))
6801 .collect()
6802 }
6803
6804 fn placeholder_paths(index: &Index) -> Vec<String> {
6805 index
6806 .entries()
6807 .iter()
6808 .filter(|e| e.is_sparse_directory_placeholder())
6809 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
6810 .collect()
6811 }
6812
6813 const FILES: [(&str, u32, &str); 8] = [
6814 ("a/in", 0o100644, "in\n"),
6815 ("top", 0o100644, "top\n"),
6816 ("w/f4", 0o100644, "f4\n"),
6817 ("w/f5", 0o100755, "f5\n"),
6818 ("x/f1", 0o100644, "f1\n"),
6819 ("x/y/f2", 0o100644, "f2\n"),
6820 ("x/y/z/f3", 0o100644, "f3\n"),
6821 ("x/y-z", 0o100644, "dash\n"),
6822 ];
6823
6824 #[test]
6825 fn unchanged_excluded_directories_collapse_to_their_outermost_placeholder() {
6826 let dir = TempDir::new().unwrap();
6827 let odb = Odb::new(dir.path());
6828 let root = write_tree(&odb, &FILES);
6829 let mut index = expanded_index(&odb, &root);
6830 collapse(&mut index, &odb, &root);
6831
6832 assert_eq!(
6833 rows(&index),
6834 vec![
6835 ("a/in".to_owned(), 0o100644, index.entries()[0].oid),
6836 ("top".to_owned(), 0o100644, index.entries()[1].oid),
6837 ("w/".to_owned(), MODE_TREE, subtree(&odb, &root, "w")),
6838 ("x/".to_owned(), MODE_TREE, subtree(&odb, &root, "x")),
6839 ]
6840 );
6841 assert!(index.sparse_directories);
6842 assert!(index.entries_are_sorted());
6843 }
6844
6845 #[test]
6846 fn a_changed_row_keeps_its_ancestors_expanded_but_not_its_siblings() {
6847 let dir = TempDir::new().unwrap();
6848 let odb = Odb::new(dir.path());
6849 let root = write_tree(&odb, &FILES);
6850 let mut index = expanded_index(&odb, &root);
6851 let edited = odb.write(ObjectKind::Blob, b"edited\n").unwrap();
6852 let pos = index.find_entry_pos(b"x/f1", 0).unwrap();
6853 index.entries_mut()[pos].oid = edited;
6854 index.sort();
6855 collapse(&mut index, &odb, &root);
6856
6857 assert_eq!(
6858 placeholder_paths(&index),
6859 vec!["w/".to_owned(), "x/y/".to_owned()]
6860 );
6861 let kept: Vec<String> = rows(&index).into_iter().map(|(p, _, _)| p).collect();
6862 assert_eq!(kept, ["a/in", "top", "w/", "x/f1", "x/y-z", "x/y/"]);
6863 let f1 = &index.entries()[index.find_entry_pos(b"x/f1", 0).unwrap()];
6864 assert_eq!(f1.oid, edited);
6865 let y = &index.entries()[index.find_entry_pos(b"x/y/", 0).unwrap()];
6866 assert_eq!(y.oid, subtree(&odb, &root, "x/y"));
6867 }
6868
6869 #[test]
6870 fn directories_with_worktree_rows_gitlinks_or_conflicts_stay_expanded() {
6871 let dir = TempDir::new().unwrap();
6872 let odb = Odb::new(dir.path());
6873 let mut files = FILES.to_vec();
6874 files.push(("v/sub", MODE_GITLINK, ""));
6875 files.push(("u/f6", 0o100644, "f6\n"));
6876 let root = write_tree(&odb, &files);
6877 let mut index = expanded_index(&odb, &root);
6878 let pos = index.find_entry_pos(b"w/f4", 0).unwrap();
6879 index.entries_mut()[pos].set_skip_worktree(false);
6880 let pos = index.find_entry_pos(b"u/f6", 0).unwrap();
6881 let mut theirs = index.entries()[pos].clone();
6882 index.entries_mut()[pos].set_stage(2);
6883 theirs.set_stage(3);
6884 index.entries_mut().push(theirs);
6885 index.sort();
6886 collapse(&mut index, &odb, &root);
6887
6888 assert_eq!(placeholder_paths(&index), vec!["x/".to_owned()]);
6889 for path in ["u/f6", "v/sub", "w/f4", "w/f5"] {
6890 assert!(
6891 index.entries().iter().any(|e| e.path == path.as_bytes()),
6892 "{path} must stay expanded"
6893 );
6894 }
6895 }
6896
6897 #[test]
6898 fn existing_placeholders_count_only_when_they_match_head() {
6899 let dir = TempDir::new().unwrap();
6900 let odb = Odb::new(dir.path());
6901 let root = write_tree(&odb, &FILES);
6902 let y = subtree(&odb, &root, "x/y");
6903
6904 let with_y_placeholder = |oid: ObjectId| {
6905 let mut index = expanded_index(&odb, &root);
6906 index.entries_mut().retain(|e| !e.path.starts_with(b"x/y/"));
6907 let mut placeholder = index.entries()[0].clone();
6908 placeholder.path = b"x/y/".to_vec();
6909 placeholder.mode = MODE_TREE;
6910 placeholder.oid = oid;
6911 placeholder.flags = 4;
6912 placeholder.set_skip_worktree(true);
6913 index.entries_mut().push(placeholder);
6914 index.sort();
6915 index
6916 };
6917
6918 let mut matching = with_y_placeholder(y);
6919 collapse(&mut matching, &odb, &root);
6920 assert_eq!(
6921 placeholder_paths(&matching),
6922 vec!["w/".to_owned(), "x/".to_owned()]
6923 );
6924
6925 let stale = odb.write(ObjectKind::Tree, b"").unwrap();
6926 let mut outdated = with_y_placeholder(stale);
6927 collapse(&mut outdated, &odb, &root);
6928 assert_eq!(
6929 placeholder_paths(&outdated),
6930 vec!["w/".to_owned(), "x/y/".to_owned()]
6931 );
6932 let y_row = &outdated.entries()[outdated.find_entry_pos(b"x/y/", 0).unwrap()];
6933 assert_eq!(y_row.oid, stale, "an outdated placeholder is left alone");
6934 assert!(outdated.entries().iter().any(|e| e.path == b"x/f1"));
6935 }
6936
6937 #[test]
6938 fn collapsed_directories_subsume_nested_ones_and_cover_their_rows() {
6939 let oid = ObjectId::from_bytes(&[1u8; 20]).unwrap();
6940 let mut collapsed = CollapsedDirectories::default();
6941 collapsed.insert(b"a/b/".to_vec(), oid);
6942 collapsed.insert(b"a/c/".to_vec(), oid);
6943 assert!(collapsed.has_descendants(b"a/"));
6944 assert!(!collapsed.has_descendants(b"a/b/"));
6945 collapsed.insert(b"a/".to_vec(), oid);
6946 assert!(!collapsed.has_descendants(b"a/"));
6947 assert_eq!(collapsed.dirs.len(), 1);
6948
6949 let mut scratch = Vec::new();
6950 for (path, covered) in [
6951 (&b"a/x"[..], true),
6952 (b"a/b/c", true),
6953 (b"a", true),
6954 (b"a-b", false),
6955 (b"ab/c", false),
6956 (b"0", false),
6957 ] {
6958 assert_eq!(
6959 collapsed.covers(path, &mut scratch),
6960 covered,
6961 "{}",
6962 String::from_utf8_lossy(path)
6963 );
6964 }
6965
6966 let entry = |path: &str| IndexEntry {
6967 path: path.as_bytes().to_vec(),
6968 ..make_entry(path)
6969 };
6970 let entries = vec![
6971 entry("0"),
6972 entry("a"),
6973 entry("a-b"),
6974 entry("a/b/c"),
6975 entry("ab"),
6976 ];
6977 assert_eq!(
6978 collapsed.covered_rows(&entries),
6979 vec![false, true, false, true, false]
6980 );
6981 }
6982 }
6983}