1use super::*;
6use crate::attributes::*;
7use crate::checkout::*;
8use crate::filter::*;
9use crate::ignore::*;
10use crate::index_io::*;
11use crate::status::*;
12use crate::types_admin::*;
13
14const INDEX_FORMAT_DEFAULT: u32 = 3;
16
17fn fresh_index_default_version(git_dir: &Path) -> u32 {
24 if let Some(raw) = env::var_os("GIT_INDEX_VERSION") {
25 let raw = raw.to_string_lossy();
26 return match raw.parse::<u32>() {
27 Ok(version) if (2..=4).contains(&version) => version,
28 _ => {
29 eprintln!(
30 "warning: GIT_INDEX_VERSION set, but the value is invalid.\nUsing version {INDEX_FORMAT_DEFAULT}"
31 );
32 INDEX_FORMAT_DEFAULT
33 }
34 };
35 }
36 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
37 let mut version = if config
38 .get_bool("feature", None, "manyFiles")
39 .unwrap_or(false)
40 {
41 4
42 } else {
43 INDEX_FORMAT_DEFAULT
44 };
45 if let Some(raw) = config.get("index", None, "version") {
46 match raw.trim().parse::<i64>() {
47 Ok(value) if (2..=4).contains(&value) => version = value as u32,
48 _ => {
49 eprintln!(
50 "warning: index.version set, but the value is invalid.\nUsing version {INDEX_FORMAT_DEFAULT}"
51 );
52 return INDEX_FORMAT_DEFAULT;
53 }
54 }
55 }
56 version
57}
58
59pub fn index_skip_hash_from_config(config: &GitConfig) -> bool {
63 let many_files = config
64 .get_bool("feature", None, "manyFiles")
65 .unwrap_or(false);
66 config
67 .get_bool("index", None, "skipHash")
68 .unwrap_or(many_files)
69}
70
71fn zero_trailing_index_hash(bytes: &mut [u8], format: ObjectFormat) {
74 let raw = format.raw_len();
75 let len = bytes.len();
76 if len >= raw {
77 bytes[len - raw..].fill(0);
78 }
79}
80
81fn read_index_or_fresh(git_dir: &Path, format: ObjectFormat) -> Result<Index> {
86 match read_repository_index(git_dir, format)? {
87 Some(index) => Ok(index),
88 None => {
89 let mut index = empty_index();
90 index.version = fresh_index_default_version(git_dir);
91 Ok(index)
92 }
93 }
94}
95
96pub fn add_paths_to_index(
97 worktree_root: impl AsRef<Path>,
98 git_dir: impl AsRef<Path>,
99 format: ObjectFormat,
100 paths: &[PathBuf],
101) -> Result<UpdateIndexResult> {
102 update_index_paths(
103 worktree_root,
104 git_dir,
105 format,
106 paths,
107 UpdateIndexOptions {
108 add: true,
109 remove: false,
110 force_remove: false,
111 chmod: None,
112 info_only: false,
113 ignore_skip_worktree_entries: false,
114 allow_skip_worktree_entries: false,
115 },
116 )
117}
118
119pub fn update_index_paths(
120 worktree_root: impl AsRef<Path>,
121 git_dir: impl AsRef<Path>,
122 format: ObjectFormat,
123 paths: &[PathBuf],
124 options: UpdateIndexOptions,
125) -> Result<UpdateIndexResult> {
126 let git_dir = git_dir.as_ref();
127 let index = read_index_or_fresh(git_dir, format)?;
128 update_index_paths_with_index(worktree_root, git_dir, format, index, paths, options)
129}
130
131pub fn update_index_paths_with_index(
132 worktree_root: impl AsRef<Path>,
133 git_dir: impl AsRef<Path>,
134 format: ObjectFormat,
135 index: Index,
136 paths: &[PathBuf],
137 options: UpdateIndexOptions,
138) -> Result<UpdateIndexResult> {
139 let ordered = ordered_paths_from_plain(paths, options);
140 update_index_paths_impl(
141 worktree_root.as_ref(),
142 git_dir.as_ref(),
143 format,
144 index,
145 &ordered,
146 options,
147 None,
148 false,
149 )
150}
151
152pub(crate) fn ordered_paths_from_plain(
157 paths: &[PathBuf],
158 options: UpdateIndexOptions,
159) -> Vec<UpdateIndexPath> {
160 let mode = options.path_mode();
161 paths
162 .iter()
163 .map(|path| UpdateIndexPath {
164 path: path.clone(),
165 mode,
166 })
167 .collect()
168}
169
170pub fn update_index_ordered_paths_filtered(
176 worktree_root: impl AsRef<Path>,
177 git_dir: impl AsRef<Path>,
178 format: ObjectFormat,
179 paths: &[UpdateIndexPath],
180 options: UpdateIndexOptions,
181 config: &GitConfig,
182 verbose: bool,
183) -> Result<UpdateIndexResult> {
184 let git_dir = git_dir.as_ref();
185 let index = read_index_or_fresh(git_dir, format)?;
186 update_index_ordered_paths_filtered_with_index(
187 worktree_root,
188 git_dir,
189 format,
190 index,
191 paths,
192 options,
193 config,
194 verbose,
195 )
196}
197
198pub fn update_index_ordered_paths_filtered_with_index(
199 worktree_root: impl AsRef<Path>,
200 git_dir: impl AsRef<Path>,
201 format: ObjectFormat,
202 index: Index,
203 paths: &[UpdateIndexPath],
204 options: UpdateIndexOptions,
205 config: &GitConfig,
206 verbose: bool,
207) -> Result<UpdateIndexResult> {
208 update_index_paths_impl(
209 worktree_root.as_ref(),
210 git_dir.as_ref(),
211 format,
212 index,
213 paths,
214 options,
215 Some(config),
216 verbose,
217 )
218}
219
220pub fn add_paths_to_index_filtered(
227 worktree_root: impl AsRef<Path>,
228 git_dir: impl AsRef<Path>,
229 format: ObjectFormat,
230 paths: &[PathBuf],
231 config: &GitConfig,
232) -> Result<UpdateIndexResult> {
233 update_index_paths_filtered(
234 worktree_root,
235 git_dir,
236 format,
237 paths,
238 UpdateIndexOptions {
239 add: true,
240 remove: false,
241 force_remove: false,
242 chmod: None,
243 info_only: false,
244 ignore_skip_worktree_entries: false,
245 allow_skip_worktree_entries: false,
246 },
247 config,
248 )
249}
250
251pub fn update_index_paths_filtered(
254 worktree_root: impl AsRef<Path>,
255 git_dir: impl AsRef<Path>,
256 format: ObjectFormat,
257 paths: &[PathBuf],
258 options: UpdateIndexOptions,
259 config: &GitConfig,
260) -> Result<UpdateIndexResult> {
261 let git_dir = git_dir.as_ref();
262 let index = read_index_or_fresh(git_dir, format)?;
263 update_index_paths_filtered_with_index(
264 worktree_root,
265 git_dir,
266 format,
267 index,
268 paths,
269 options,
270 config,
271 )
272}
273
274pub fn update_index_paths_filtered_with_index(
275 worktree_root: impl AsRef<Path>,
276 git_dir: impl AsRef<Path>,
277 format: ObjectFormat,
278 index: Index,
279 paths: &[PathBuf],
280 options: UpdateIndexOptions,
281 config: &GitConfig,
282) -> Result<UpdateIndexResult> {
283 let ordered = ordered_paths_from_plain(paths, options);
284 update_index_paths_impl(
285 worktree_root.as_ref(),
286 git_dir.as_ref(),
287 format,
288 index,
289 &ordered,
290 options,
291 Some(config),
292 false,
293 )
294}
295
296pub fn add_update_all_tracked_filtered(
297 worktree_root: impl AsRef<Path>,
298 git_dir: impl AsRef<Path>,
299 format: ObjectFormat,
300 clean_config: &GitConfig,
301) -> Result<Vec<AddUpdateTrackedAction>> {
302 let worktree_root = worktree_root.as_ref();
303 let git_dir = git_dir.as_ref();
304 let index_path = repository_index_path(git_dir);
305 if !index_path.exists() {
306 return Ok(Vec::new());
307 }
308 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
309 let index_mtime = fs::metadata(&index_path)
310 .ok()
311 .and_then(|metadata| file_mtime_parts(&metadata));
312 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
313 let prechecks =
314 tracked_only_non_clean_prechecks_parallel(worktree_root, &index, &stat_cache, false)?;
315 let unmerged_paths: Vec<Vec<u8>> = {
320 let mut paths = index
321 .entries
322 .iter()
323 .filter(|entry| entry.stage() != Stage::Normal)
324 .map(|entry| entry.path.as_bytes().to_vec())
325 .collect::<Vec<_>>();
326 paths.dedup();
327 paths
328 };
329 if prechecks.is_empty() && unmerged_paths.is_empty() {
330 return Ok(Vec::new());
331 }
332
333 let pending = prechecks
334 .into_iter()
335 .map(|precheck| match precheck {
336 TrackedOnlyPrecheck::Deleted(idx) => {
337 (precheck, index.entries[idx].path.as_bytes().to_vec())
338 }
339 TrackedOnlyPrecheck::Slow(idx) => {
340 (precheck, index.entries[idx].path.as_bytes().to_vec())
341 }
342 })
343 .collect::<Vec<_>>();
344 let odb = FileObjectDatabase::from_git_dir(git_dir, format);
345 let mut actions = Vec::new();
346 let mut index_dirty = false;
347 let mut clean_filter = None;
348 let trust_filemode = trust_executable_bit(clean_config);
349 for (precheck, path) in pending {
350 match precheck {
351 TrackedOnlyPrecheck::Deleted(_) => {
352 if remove_index_entries_with_path(&mut index.entries, &path) {
353 actions.push(AddUpdateTrackedAction::Remove(path));
354 index_dirty = true;
355 }
356 }
357 TrackedOnlyPrecheck::Slow(_) => {
358 let (action, dirty) = add_update_tracked_path(
359 worktree_root,
360 git_dir,
361 format,
362 Some(clean_config),
363 trust_filemode,
364 &odb,
365 &stat_cache,
366 &mut clean_filter,
367 &mut index,
368 &path,
369 )?;
370 index_dirty |= dirty;
371 if let Some(action) = action {
372 actions.push(action);
373 }
374 }
375 }
376 }
377
378 for path in unmerged_paths {
379 let (action, dirty) = add_update_tracked_path(
380 worktree_root,
381 git_dir,
382 format,
383 Some(clean_config),
384 trust_filemode,
385 &odb,
386 &stat_cache,
387 &mut clean_filter,
388 &mut index,
389 &path,
390 )?;
391 index_dirty |= dirty;
392 if let Some(action) = action {
393 actions.push(action);
394 }
395 }
396
397 if index_dirty {
398 normalize_index_version_for_extended_flags(&mut index);
399 index.extensions = index_extensions_without_cache_tree(&index.extensions);
400 write_repository_index_ref(git_dir, format, &index)?;
401 }
402 Ok(actions)
403}
404
405pub fn add_exact_tracked_path_from_disk(
406 worktree_root: impl AsRef<Path>,
407 git_dir: impl AsRef<Path>,
408 format: ObjectFormat,
409 git_path: &[u8],
410 ignore_removal: bool,
411 config_parameters_env: Option<&str>,
412) -> Result<AddExactTrackedPathResult> {
413 let worktree_root = worktree_root.as_ref();
414 let git_dir = git_dir.as_ref();
415 let index_path = repository_index_path(git_dir);
416 let index_metadata = match fs::metadata(&index_path) {
417 Ok(metadata) => metadata,
418 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
419 return Ok(AddExactTrackedPathResult::Unsupported);
420 }
421 Err(err) => return Err(err.into()),
422 };
423 let mut index_bytes = fs::read(&index_path)?;
424 let Some(raw) = raw_exact_index_entry(&index_bytes, format, git_path)? else {
425 return Ok(AddExactTrackedPathResult::Unsupported);
426 };
427 if !raw_exact_entry_can_patch(&raw, git_path) {
428 return Ok(AddExactTrackedPathResult::Unsupported);
429 }
430 if !raw_index_extensions_are_filterable(&index_bytes, raw.entries_end, raw.checksum_offset) {
431 return Ok(AddExactTrackedPathResult::Unsupported);
432 }
433
434 let entry = raw.entry.clone();
435 if entry.stage() != Stage::Normal
436 || index_entry_skip_worktree(&entry)
437 || sley_index::is_gitlink(entry.mode)
438 {
439 return Ok(AddExactTrackedPathResult::Unsupported);
440 }
441 let absolute = worktree_root.join(repo_path_to_os_path(git_path)?);
442 let metadata = match fs::symlink_metadata(&absolute) {
443 Ok(metadata) => metadata,
444 Err(err)
445 if matches!(
446 err.kind(),
447 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
448 ) =>
449 {
450 return Ok(if ignore_removal {
451 AddExactTrackedPathResult::Handled(None)
452 } else {
453 AddExactTrackedPathResult::Unsupported
454 });
455 }
456 Err(err) => return Err(err.into()),
457 };
458 let file_type = metadata.file_type();
459 if metadata.is_dir() || !(file_type.is_file() || file_type.is_symlink()) {
460 return Ok(AddExactTrackedPathResult::Unsupported);
461 }
462 let index_mtime = file_mtime_parts(&index_metadata);
463 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
464 if stat_cache.reuse_index_entry(&entry, &metadata).is_some() {
465 return Ok(AddExactTrackedPathResult::Handled(None));
466 }
467
468 let odb = FileObjectDatabase::from_git_dir(git_dir, format);
469 let is_symlink = file_type.is_symlink();
470 let body = if is_symlink {
471 symlink_target_bytes(&absolute)?
472 } else {
473 let body = fs::read(&absolute)?;
474 let config =
479 sley_config::read_repo_config(git_dir, config_parameters_env).unwrap_or_default();
480 let mut clean_filter = None;
481 let clean_filter =
482 tracked_only_clean_filter_with_config(&mut clean_filter, worktree_root, &config);
483 clean_filter.read_attributes_for_path(worktree_root, git_path)?;
484 let checks =
485 clean_filter
486 .matcher
487 .attributes_for_path(git_path, &clean_filter.requested, false);
488 let conv_flags = ConvFlags::from_config(&clean_filter.config);
495 let index_blob = match conv_flags {
496 ConvFlags::Off => SafeCrlfIndexBlob::None,
497 _ => SafeCrlfIndexBlob::Lookup {
498 odb: &odb,
499 oid: entry.oid,
500 },
501 };
502 apply_clean_filter_cow_inner(
503 &clean_filter.config,
504 &checks,
505 git_path,
506 &body,
507 conv_flags,
508 index_blob,
509 true,
510 )?
511 .into_owned()
512 };
513 let object = EncodedObject::new(ObjectType::Blob, body);
514 let oid = object.object_id(format)?;
515 if oid != entry.oid || entry.is_intent_to_add() {
516 odb.write_object(object)?;
517 }
518
519 let config = sley_config::read_repo_config(git_dir, config_parameters_env).unwrap_or_default();
520 let trust_filemode = trust_executable_bit(&config);
521 let mut updated_entry =
522 index_entry_from_metadata_with_filemode(entry.path.clone(), oid, &metadata, trust_filemode);
523 if is_symlink {
524 updated_entry.mode = 0o120000;
525 }
526 if updated_entry == entry {
527 return Ok(AddExactTrackedPathResult::Handled(None));
528 }
529 if !raw_updated_entry_can_patch(&entry, &updated_entry, git_path) {
530 return Ok(AddExactTrackedPathResult::Unsupported);
531 }
532 patch_raw_index_entry(&mut index_bytes, format, &raw, &updated_entry)?;
533 fs::write(index_path, index_bytes)?;
534 let changed = updated_entry.oid != entry.oid || updated_entry.mode != entry.mode;
535 Ok(AddExactTrackedPathResult::Handled(
536 changed.then(|| AddUpdateTrackedAction::Add(git_path.to_vec())),
537 ))
538}
539
540pub fn add_exact_tracked_path_with_index(
541 worktree_root: impl AsRef<Path>,
542 git_dir: impl AsRef<Path>,
543 format: ObjectFormat,
544 mut index: Index,
545 git_path: &[u8],
546) -> Result<Option<AddUpdateTrackedAction>> {
547 let worktree_root = worktree_root.as_ref();
548 let git_dir = git_dir.as_ref();
549 let range = index_entries_path_range(&index.entries, git_path);
550 if range.len() != 1 {
551 return Ok(None);
552 }
553 let entry = &index.entries[range.start];
554 if entry.stage() != Stage::Normal || index_entry_skip_worktree(entry) {
555 return Ok(None);
556 }
557 let index_path = repository_index_path(git_dir);
558 let index_mtime = fs::metadata(&index_path)
559 .ok()
560 .and_then(|metadata| file_mtime_parts(&metadata));
561 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
562 let odb = FileObjectDatabase::from_git_dir(git_dir, format);
563 let trust_filemode = trust_executable_bit_from_git_dir(git_dir, None);
564 let mut clean_filter = None;
565 let (action, dirty) = add_update_tracked_path(
566 worktree_root,
567 git_dir,
568 format,
569 None,
570 trust_filemode,
571 &odb,
572 &stat_cache,
573 &mut clean_filter,
574 &mut index,
575 git_path,
576 )?;
577 if dirty {
578 normalize_index_version_for_extended_flags(&mut index);
579 index.extensions = index_extensions_without_cache_tree(&index.extensions);
580 write_repository_index_ref(git_dir, format, &index)?;
581 }
582 Ok(action)
583}
584
585pub(crate) struct RawExactIndexEntry {
586 version: u32,
587 entry: IndexEntry,
588 entry_start: usize,
589 entries_end: usize,
590 checksum_offset: usize,
591}
592
593pub(crate) fn raw_exact_index_entry(
594 bytes: &[u8],
595 format: ObjectFormat,
596 git_path: &[u8],
597) -> Result<Option<RawExactIndexEntry>> {
598 let hash_len = format.raw_len();
599 if bytes.len() < 12 + hash_len {
600 return Err(GitError::InvalidFormat("index header too short".into()));
601 }
602 let checksum_offset = bytes.len() - hash_len;
603 let actual_checksum = sley_core::digest_bytes(format, &bytes[..checksum_offset])?;
604 let expected_checksum = ObjectId::from_raw(format, &bytes[checksum_offset..])?;
605 if actual_checksum != expected_checksum {
606 return Err(GitError::InvalidFormat(format!(
607 "index checksum mismatch: expected {expected_checksum}, got {actual_checksum}"
608 )));
609 }
610 if &bytes[..4] != b"DIRC" {
611 return Err(GitError::InvalidFormat("missing DIRC signature".into()));
612 }
613 let version = u32_from_be(&bytes[4..8]);
614 if !(2..=3).contains(&version) {
615 return Ok(None);
616 }
617 let count = u32_from_be(&bytes[8..12]) as usize;
618 let mut offset = 12;
619 let mut found = None;
620 for _ in 0..count {
621 let entry_header_len = 40 + hash_len + 2;
622 if checksum_offset.saturating_sub(offset) < entry_header_len {
623 return Err(GitError::InvalidFormat("truncated index entry".into()));
624 }
625 let start = offset;
626 let oid_start = offset + 40;
627 let oid_end = oid_start + hash_len;
628 let flags = u16_from_be(&bytes[oid_end..oid_end + 2]);
629 offset = oid_end + 2;
630 let flags_extended = if flags & INDEX_FLAG_EXTENDED != 0 {
631 if checksum_offset.saturating_sub(offset) < 2 {
632 return Err(GitError::InvalidFormat(
633 "truncated index extended flags".into(),
634 ));
635 }
636 let flags_extended = u16_from_be(&bytes[offset..offset + 2]);
637 offset += 2;
638 flags_extended
639 } else {
640 0
641 };
642 let path_start = offset;
643 while bytes.get(offset).copied() != Some(0) {
644 offset += 1;
645 if offset >= checksum_offset {
646 return Err(GitError::InvalidFormat("unterminated index path".into()));
647 }
648 }
649 let path = &bytes[path_start..offset];
650 offset += 1;
651 while (offset - start) % 8 != 0 {
652 offset += 1;
653 if offset > checksum_offset {
654 return Err(GitError::InvalidFormat("truncated index padding".into()));
655 }
656 }
657 if path == git_path {
658 if found.is_some() {
659 return Ok(None);
660 }
661 let oid = ObjectId::from_raw(format, &bytes[oid_start..oid_end])?;
662 found = Some(RawExactIndexEntry {
663 version,
664 entry: IndexEntry {
665 ctime_seconds: u32_from_be(&bytes[start..start + 4]),
666 ctime_nanoseconds: u32_from_be(&bytes[start + 4..start + 8]),
667 mtime_seconds: u32_from_be(&bytes[start + 8..start + 12]),
668 mtime_nanoseconds: u32_from_be(&bytes[start + 12..start + 16]),
669 dev: u32_from_be(&bytes[start + 16..start + 20]),
670 ino: u32_from_be(&bytes[start + 20..start + 24]),
671 mode: u32_from_be(&bytes[start + 24..start + 28]),
672 uid: u32_from_be(&bytes[start + 28..start + 32]),
673 gid: u32_from_be(&bytes[start + 32..start + 36]),
674 size: u32_from_be(&bytes[start + 36..start + 40]),
675 oid,
676 flags,
677 flags_extended,
678 path: BString::from(path),
679 },
680 entry_start: start,
681 entries_end: 0,
682 checksum_offset,
683 });
684 } else if found.is_none() && path > git_path {
685 return Ok(None);
686 }
687 }
688 if let Some(mut found) = found {
689 found.entries_end = offset;
690 Ok(Some(found))
691 } else {
692 Ok(None)
693 }
694}
695
696pub(crate) fn raw_exact_entry_can_patch(raw: &RawExactIndexEntry, git_path: &[u8]) -> bool {
697 raw.version == 2
698 && raw.entry.flags_extended == 0
699 && raw.entry.flags & INDEX_FLAG_EXTENDED == 0
700 && raw.entry.flags == index_flags(git_path.len(), 0)
701 && raw.entry.path.as_bytes() == git_path
702}
703
704pub(crate) fn raw_updated_entry_can_patch(
705 previous: &IndexEntry,
706 updated: &IndexEntry,
707 git_path: &[u8],
708) -> bool {
709 updated.path.as_bytes() == git_path
710 && updated.flags_extended == 0
711 && updated.flags & INDEX_FLAG_EXTENDED == 0
712 && updated.flags == previous.flags
713}
714
715pub(crate) fn raw_index_extensions_are_filterable(
716 bytes: &[u8],
717 entries_end: usize,
718 checksum_offset: usize,
719) -> bool {
720 let mut offset = entries_end;
721 while offset < checksum_offset {
722 if checksum_offset.saturating_sub(offset) < 8 {
723 return false;
724 }
725 let size = u32_from_be(&bytes[offset + 4..offset + 8]) as usize;
726 let Some(end) = offset
727 .checked_add(8)
728 .and_then(|offset| offset.checked_add(size))
729 else {
730 return false;
731 };
732 if end > checksum_offset {
733 return false;
734 }
735 offset = end;
736 }
737 true
738}
739
740pub(crate) fn patch_raw_index_entry(
741 bytes: &mut Vec<u8>,
742 format: ObjectFormat,
743 raw: &RawExactIndexEntry,
744 entry: &IndexEntry,
745) -> Result<()> {
746 let hash_len = format.raw_len();
747 let start = raw.entry_start;
748 bytes[start..start + 4].copy_from_slice(&entry.ctime_seconds.to_be_bytes());
749 bytes[start + 4..start + 8].copy_from_slice(&entry.ctime_nanoseconds.to_be_bytes());
750 bytes[start + 8..start + 12].copy_from_slice(&entry.mtime_seconds.to_be_bytes());
751 bytes[start + 12..start + 16].copy_from_slice(&entry.mtime_nanoseconds.to_be_bytes());
752 bytes[start + 16..start + 20].copy_from_slice(&entry.dev.to_be_bytes());
753 bytes[start + 20..start + 24].copy_from_slice(&entry.ino.to_be_bytes());
754 bytes[start + 24..start + 28].copy_from_slice(&entry.mode.to_be_bytes());
755 bytes[start + 28..start + 32].copy_from_slice(&entry.uid.to_be_bytes());
756 bytes[start + 32..start + 36].copy_from_slice(&entry.gid.to_be_bytes());
757 bytes[start + 36..start + 40].copy_from_slice(&entry.size.to_be_bytes());
758 bytes[start + 40..start + 40 + hash_len].copy_from_slice(entry.oid.as_bytes());
759 bytes[start + 40 + hash_len..start + 40 + hash_len + 2]
760 .copy_from_slice(&entry.flags.to_be_bytes());
761
762 let mut extension_offset = raw.entries_end;
763 let mut removed_cache_tree = false;
764 let mut rewritten = Vec::new();
765 while extension_offset < raw.checksum_offset {
766 let signature = &bytes[extension_offset..extension_offset + 4];
767 let size = u32_from_be(&bytes[extension_offset + 4..extension_offset + 8]) as usize;
768 let end = extension_offset + 8 + size;
769 if signature == b"TREE" {
770 removed_cache_tree = true;
771 } else {
772 rewritten.extend_from_slice(&bytes[extension_offset..end]);
773 }
774 extension_offset = end;
775 }
776
777 if removed_cache_tree {
778 bytes.truncate(raw.entries_end);
779 bytes.extend_from_slice(&rewritten);
780 let checksum = sley_core::digest_bytes(format, bytes)?;
781 bytes.extend_from_slice(checksum.as_bytes());
782 } else {
783 let checksum = sley_core::digest_bytes(format, &bytes[..raw.checksum_offset])?;
784 bytes[raw.checksum_offset..raw.checksum_offset + hash_len]
785 .copy_from_slice(checksum.as_bytes());
786 }
787 Ok(())
788}
789
790pub(crate) fn u32_from_be(bytes: &[u8]) -> u32 {
791 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
792}
793
794pub(crate) fn u16_from_be(bytes: &[u8]) -> u16 {
795 u16::from_be_bytes([bytes[0], bytes[1]])
796}
797
798pub(crate) fn add_update_tracked_path(
799 worktree_root: &Path,
800 git_dir: &Path,
801 format: ObjectFormat,
802 clean_config: Option<&GitConfig>,
803 trust_filemode: bool,
804 odb: &FileObjectDatabase,
805 stat_cache: &IndexStatCache,
806 clean_filter: &mut Option<TrackedOnlyCleanFilter>,
807 index: &mut Index,
808 git_path: &[u8],
809) -> Result<(Option<AddUpdateTrackedAction>, bool)> {
810 let range = index_entries_path_range(&index.entries, git_path);
811 if range.is_empty() {
812 return Ok((None, false));
813 }
814 let entry = index.entries[range.start].clone();
815 let is_unmerged = entry.stage() != Stage::Normal;
824 let absolute = worktree_root.join(repo_path_to_os_path(git_path)?);
825 let metadata = match fs::symlink_metadata(&absolute) {
826 Ok(metadata) => metadata,
827 Err(err)
828 if matches!(
829 err.kind(),
830 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
831 ) =>
832 {
833 if remove_index_entries_with_path(&mut index.entries, git_path) {
834 return Ok((
835 Some(AddUpdateTrackedAction::Remove(git_path.to_vec())),
836 true,
837 ));
838 }
839 return Ok((None, false));
840 }
841 Err(err) => return Err(err.into()),
842 };
843 if metadata.is_dir() {
844 if !sley_index::is_gitlink(entry.mode) {
845 return Ok((None, false));
846 }
847 let oid = sley_diff_merge::gitlink_head_oid(&absolute, format).unwrap_or(entry.oid);
848 let mut updated_entry = index_entry_from_metadata_with_filemode(
849 entry.path.clone(),
850 oid,
851 &metadata,
852 trust_filemode,
853 );
854 updated_entry.mode = sley_index::GITLINK_MODE;
855 let changed =
856 is_unmerged || updated_entry.oid != entry.oid || updated_entry.mode != entry.mode;
857 if updated_entry != entry {
858 replace_index_entries_with_entry(&mut index.entries, updated_entry);
859 return Ok((
860 changed.then(|| AddUpdateTrackedAction::Add(git_path.to_vec())),
861 true,
862 ));
863 }
864 return Ok((None, false));
865 }
866 if !(metadata.is_file() || metadata.file_type().is_symlink()) {
867 return Ok((None, false));
868 }
869 if !is_unmerged && stat_cache.reuse_index_entry(&entry, &metadata).is_some() {
870 return Ok((None, false));
871 }
872
873 let is_symlink = metadata.file_type().is_symlink();
874 let body = if is_symlink {
875 symlink_target_bytes(&absolute)?
876 } else {
877 let body = fs::read(&absolute)?;
878 let clean_filter = match clean_config {
879 Some(config) => {
880 tracked_only_clean_filter_with_config(clean_filter, worktree_root, config)
881 }
882 None => tracked_only_clean_filter(clean_filter, worktree_root, git_dir),
883 };
884 clean_filter.read_attributes_for_path(worktree_root, git_path)?;
885 let checks =
886 clean_filter
887 .matcher
888 .attributes_for_path(git_path, &clean_filter.requested, false);
889 let conv_flags = ConvFlags::from_config(&clean_filter.config);
894 let index_blob = match conv_flags {
895 ConvFlags::Off => SafeCrlfIndexBlob::None,
896 _ => SafeCrlfIndexBlob::Lookup {
897 odb,
898 oid: entry.oid,
899 },
900 };
901 apply_clean_filter_cow_inner(
902 &clean_filter.config,
903 &checks,
904 git_path,
905 &body,
906 conv_flags,
907 index_blob,
908 true,
909 )?
910 .into_owned()
911 };
912 let object = EncodedObject::new(ObjectType::Blob, body);
913 let oid = object.object_id(format)?;
914 if oid != entry.oid || entry.is_intent_to_add() {
915 odb.write_object(object)?;
916 }
917 let mut updated_entry =
918 index_entry_from_metadata_with_filemode(entry.path.clone(), oid, &metadata, trust_filemode);
919 if is_symlink {
920 updated_entry.mode = 0o120000;
921 }
922 let changed = is_unmerged || updated_entry.oid != entry.oid || updated_entry.mode != entry.mode;
923 if updated_entry != entry {
924 replace_index_entries_with_entry(&mut index.entries, updated_entry);
925 return Ok((
926 changed.then(|| AddUpdateTrackedAction::Add(git_path.to_vec())),
927 true,
928 ));
929 }
930 Ok((None, false))
931}
932
933pub(crate) enum UpdateIndexCleanFilter {
934 Full(AttributeMatcher),
935 PathLocal,
936}
937
938pub(crate) fn index_entries_path_range(
939 entries: &[IndexEntry],
940 path: &[u8],
941) -> std::ops::Range<usize> {
942 let mut start = match entries.binary_search_by(|entry| entry.path.as_bytes().cmp(path)) {
943 Ok(index) => index,
944 Err(insert) => return insert..insert,
945 };
946 while start > 0 && entries[start - 1].path.as_bytes() == path {
947 start -= 1;
948 }
949 let mut end = start;
950 while end < entries.len() && entries[end].path.as_bytes() == path {
951 end += 1;
952 }
953 start..end
954}
955
956pub(crate) fn remove_index_entries_with_path(entries: &mut Vec<IndexEntry>, path: &[u8]) -> bool {
957 let range = index_entries_path_range(entries, path);
958 if range.is_empty() {
959 return false;
960 }
961 entries.drain(range);
962 true
963}
964
965pub(crate) fn remove_index_entries_under_dir(entries: &mut Vec<IndexEntry>, name: &[u8]) {
973 let start = match entries.binary_search_by(|entry| entry.path.as_bytes().cmp(name)) {
974 Ok(found) => found + 1,
975 Err(insert) => insert,
976 };
977 let mut end = start;
978 while end < entries.len() {
979 let candidate = entries[end].path.as_bytes();
980 if candidate.len() > name.len()
983 && candidate[name.len()] == b'/'
984 && candidate[..name.len()] == *name
985 {
986 end += 1;
987 } else {
988 break;
989 }
990 }
991 if end > start {
992 entries.drain(start..end);
993 }
994}
995
996pub(crate) fn remove_index_dir_name_conflicts(entries: &mut Vec<IndexEntry>, name: &[u8]) {
1005 let mut slash = name.len();
1006 while let Some(pos) = name[..slash].iter().rposition(|&byte| byte == b'/') {
1009 slash = pos;
1010 let prefix = &name[..slash];
1011 match entries.binary_search_by(|entry| entry.path.as_bytes().cmp(prefix)) {
1012 Ok(found) => {
1013 entries.remove(found);
1015 }
1016 Err(insert) => {
1017 if insert < entries.len() {
1021 let candidate = entries[insert].path.as_bytes();
1022 if candidate.len() > prefix.len()
1023 && candidate[prefix.len()] == b'/'
1024 && candidate[..prefix.len()] == *prefix
1025 {
1026 break;
1027 }
1028 }
1029 }
1030 }
1031 }
1032}
1033
1034pub(crate) fn replace_index_entries_with_entry(entries: &mut Vec<IndexEntry>, entry: IndexEntry) {
1035 let path = entry.path.as_bytes().to_vec();
1036 remove_index_entries_under_dir(entries, &path);
1043 remove_index_dir_name_conflicts(entries, &path);
1044 let range = index_entries_path_range(entries, &path);
1045 if range.is_empty() {
1046 entries.insert(range.start, entry);
1047 } else {
1048 entries.splice(range, [entry]);
1049 }
1050}
1051
1052pub(crate) fn write_index_blob_object(
1053 odb: &FileObjectDatabase,
1054 format: ObjectFormat,
1055 object: EncodedObject,
1056 large_policy: LargeObjectPolicy,
1057 pending_large: &mut Vec<(ObjectId, EncodedObject)>,
1058) -> Result<ObjectId> {
1059 let oid = object.object_id(format)?;
1060 if object.object_type == ObjectType::Blob && object.body.len() as u64 >= large_policy.threshold
1061 {
1062 if !odb.contains(&oid)? {
1063 pending_large.push((oid, object));
1064 }
1065 return Ok(oid);
1066 }
1067 odb.write_object(object)
1068}
1069
1070pub(crate) fn write_pending_large_blobs(
1071 odb: &FileObjectDatabase,
1072 objects: &[(ObjectId, EncodedObject)],
1073 policy: LargeObjectPolicy,
1074) -> Result<()> {
1075 let Some(limit) = policy.pack_size_limit else {
1076 return odb.write_blobs_as_pack(objects, policy.compression_level);
1077 };
1078 let mut start = 0usize;
1079 let mut current_size = 0u64;
1080 for (idx, (_, object)) in objects.iter().enumerate() {
1081 let estimate = object.body.len() as u64 + 32;
1082 if idx > start && current_size.saturating_add(estimate) > limit {
1083 odb.write_blobs_as_pack(&objects[start..idx], policy.compression_level)?;
1084 start = idx;
1085 current_size = 0;
1086 }
1087 current_size = current_size.saturating_add(estimate);
1088 }
1089 if start < objects.len() {
1090 odb.write_blobs_as_pack(&objects[start..], policy.compression_level)?;
1091 }
1092 Ok(())
1093}
1094
1095pub(crate) fn update_index_paths_impl(
1096 worktree_root: &Path,
1097 git_dir: &Path,
1098 format: ObjectFormat,
1099 mut index: Index,
1100 paths: &[UpdateIndexPath],
1101 options: UpdateIndexOptions,
1102 clean_config: Option<&GitConfig>,
1103 verbose: bool,
1104) -> Result<UpdateIndexResult> {
1105 let odb = FileObjectDatabase::from_git_dir(git_dir, format);
1106 let mut large_policy = LargeObjectPolicy::from_config(git_dir, None)?;
1107 if let Some(config) = clean_config {
1108 large_policy.compression_level = pack_compression_level(config);
1109 large_policy.pack_size_limit = config
1110 .get("pack", None, "packSizeLimit")
1111 .and_then(sley_config::parse_config_int)
1112 .and_then(|value| (value > 0).then_some(value as u64))
1113 .or(large_policy.pack_size_limit);
1114 }
1115 let trust_filemode = clean_config
1116 .map(trust_executable_bit)
1117 .unwrap_or_else(|| trust_executable_bit_from_git_dir(git_dir, None));
1118 let trust_symlinks = clean_config
1119 .map(trust_symlinks)
1120 .unwrap_or_else(|| trust_symlinks_from_git_dir(git_dir, None));
1121 if options.allow_skip_worktree_entries {
1122 expand_sparse_index(&mut index, &odb, format)?;
1123 }
1124 let sparse_checkout_active = sparse_checkout_config_enabled(git_dir)
1125 || index.is_sparse()
1126 || index.entries.iter().any(IndexEntry::is_sparse_dir);
1127 let clean_filter = match clean_config {
1131 Some(_) if paths.len() >= 64 => Some(UpdateIndexCleanFilter::Full(
1132 AttributeMatcher::from_worktree_root(worktree_root)?,
1133 )),
1134 Some(_) => Some(UpdateIndexCleanFilter::PathLocal),
1135 None => None,
1136 };
1137 let conv_flags = clean_config.map_or(ConvFlags::Off, ConvFlags::from_config);
1142 let non_atomic_chmod_errors = clean_config.is_some() && options.add && options.remove;
1143 let requested_filter_attrs = filter_attribute_names();
1144 let mut updated = Vec::new();
1145 let mut reports: Vec<String> = Vec::new();
1146 let mut untracked_cache_invalidation_paths = Vec::new();
1147 let mut pending_large = Vec::new();
1148 let mut chmod_error = false;
1149 for update_path in paths {
1150 let path = &update_path.path;
1151 let path_mode = update_path.mode;
1156 let path_chmod = path_mode.chmod;
1157 let absolute = if path.is_absolute() {
1158 path.clone()
1159 } else {
1160 worktree_root.join(path)
1161 };
1162 let absolute = normalize_absolute_path_lexically(&absolute);
1163 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1164 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1165 })?;
1166 let git_path = git_path_bytes(relative)?;
1167 if index_sparse_dir_contains_path(&index, &git_path) {
1168 expand_sparse_index(&mut index, &odb, format)?;
1169 }
1170 let existing_range = index_entries_path_range(&index.entries, &git_path);
1171 if path_mode.force_remove {
1172 record_resolve_undo_for_range(&mut index, format, &git_path, existing_range)?;
1173 remove_index_entries_with_path(&mut index.entries, &git_path);
1174 untracked_cache_invalidation_paths.push(git_path.clone());
1175 reports.push(format!("remove '{}'", String::from_utf8_lossy(&git_path)));
1177 continue;
1178 }
1179 let symlink_metadata = match fs::symlink_metadata(&absolute) {
1187 Ok(metadata) => Some(metadata),
1188 Err(err)
1195 if matches!(
1196 err.kind(),
1197 std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1198 ) =>
1199 {
1200 None
1201 }
1202 Err(err) => return Err(err.into()),
1203 };
1204 if !options.allow_skip_worktree_entries
1205 && index.entries[existing_range.clone()]
1206 .iter()
1207 .any(index_entry_skip_worktree)
1208 {
1209 if path_mode.remove {
1210 if !options.ignore_skip_worktree_entries {
1211 index.entries.drain(existing_range);
1212 }
1213 continue;
1214 }
1215 if symlink_metadata.is_none()
1216 || options.ignore_skip_worktree_entries
1217 || !sparse_checkout_active
1218 {
1219 continue;
1220 }
1221 }
1222 let Some(metadata) = symlink_metadata else {
1223 if path_mode.remove {
1224 record_resolve_undo_for_range(&mut index, format, &git_path, existing_range)?;
1225 remove_index_entries_with_path(&mut index.entries, &git_path);
1226 untracked_cache_invalidation_paths.push(git_path.clone());
1227 reports.push(format!("add '{}'", String::from_utf8_lossy(&git_path)));
1231 continue;
1232 }
1233 print_update_index_path_error(&git_path, "does not exist and --remove not passed");
1234 return Err(GitError::Exit(128));
1235 };
1236 if !path_mode.add && index_entries_path_range(&index.entries, &git_path).is_empty() {
1237 print_update_index_path_error(
1238 &git_path,
1239 "cannot add to the index - missing --add option?",
1240 );
1241 return Err(GitError::Exit(128));
1242 }
1243 if metadata.is_dir() {
1244 if path_mode.remove
1245 && !existing_range.is_empty()
1246 && sley_diff_merge::gitlink_head_oid(&absolute, format).is_none()
1247 {
1248 record_resolve_undo_for_range(
1249 &mut index,
1250 format,
1251 &git_path,
1252 existing_range.clone(),
1253 )?;
1254 remove_index_entries_with_path(&mut index.entries, &git_path);
1255 untracked_cache_invalidation_paths.push(git_path.clone());
1256 reports.push(format!("add '{}'", String::from_utf8_lossy(&git_path)));
1257 continue;
1258 }
1259 let display = String::from_utf8_lossy(&git_path).into_owned();
1267 let has_dot_git = absolute.join(".git").exists();
1268 if let Some(submodule_format) = embedded_repo_object_format(&absolute)
1269 && submodule_format != format
1270 {
1271 eprintln!("fatal: cannot add a submodule of a different hash algorithm");
1272 return Err(GitError::Exit(128));
1273 }
1274 let Some(head_oid) = sley_diff_merge::gitlink_head_oid(&absolute, format) else {
1275 if has_dot_git {
1276 if clean_config.is_some() {
1277 let display_dir = if display.ends_with('/') {
1278 display.clone()
1279 } else {
1280 format!("{display}/")
1281 };
1282 eprintln!("error: '{display_dir}' does not have a commit checked out");
1283 eprintln!("error: unable to index file '{display_dir}'");
1284 eprintln!("fatal: adding files failed");
1285 } else {
1286 eprintln!("error: '{display}' does not have a commit checked out");
1287 eprintln!("fatal: Unable to process path {display}");
1288 }
1289 } else {
1290 eprintln!("error: {display}: is a directory - add files inside instead");
1291 eprintln!("fatal: Unable to process path {display}");
1292 }
1293 return Err(GitError::Exit(128));
1294 };
1295 if path_chmod.is_some() {
1296 eprintln!(
1297 "fatal: git update-index: cannot chmod {}x '{display}'",
1298 if path_chmod == Some(true) { '+' } else { '-' },
1299 );
1300 return Err(GitError::Exit(128));
1301 }
1302 let mut entry = index_entry_from_metadata_with_filemode(
1303 git_path.clone(),
1304 head_oid,
1305 &metadata,
1306 trust_filemode,
1307 );
1308 entry.mode = sley_index::GITLINK_MODE;
1309 reports.push(format!("add '{display}'"));
1310 record_resolve_undo_for_range(&mut index, format, &git_path, existing_range.clone())?;
1311 replace_index_entries_with_entry(&mut index.entries, entry);
1312 untracked_cache_invalidation_paths.push(git_path.clone());
1313 updated.push(head_oid);
1314 continue;
1315 }
1316 let is_symlink = metadata.file_type().is_symlink();
1317 let body = if is_symlink {
1318 symlink_target_bytes(&absolute)?
1321 } else {
1322 let body = fs::read(&absolute)?;
1323 let index_blob = match conv_flags {
1326 ConvFlags::Off => SafeCrlfIndexBlob::None,
1327 _ => stage0_oid_in_range(&index.entries, existing_range.clone()).map_or(
1328 SafeCrlfIndexBlob::None,
1329 |oid| SafeCrlfIndexBlob::Lookup { odb: &odb, oid },
1330 ),
1331 };
1332 match (clean_config, &clean_filter) {
1333 (Some(config), Some(UpdateIndexCleanFilter::Full(matcher))) => {
1334 let checks =
1338 matcher.attributes_for_path(&git_path, &requested_filter_attrs, false);
1339 apply_clean_filter_cow_inner(
1340 config, &checks, &git_path, &body, conv_flags, index_blob, true,
1341 )?
1342 .into_owned()
1343 }
1344 (Some(config), Some(UpdateIndexCleanFilter::PathLocal)) => {
1345 let checks = filter_attribute_checks(worktree_root, &git_path)?;
1346 apply_clean_filter_cow_inner(
1347 config, &checks, &git_path, &body, conv_flags, index_blob, true,
1348 )?
1349 .into_owned()
1350 }
1351 _ => body,
1352 }
1353 };
1354 let object = EncodedObject::new(ObjectType::Blob, body);
1355 let oid = if path_mode.info_only {
1356 object.object_id(format)?
1357 } else {
1358 write_index_blob_object(&odb, format, object, large_policy, &mut pending_large)?
1359 };
1360 let mut entry = index_entry_from_metadata_with_filemode(
1361 git_path.clone(),
1362 oid,
1363 &metadata,
1364 trust_filemode,
1365 );
1366 if is_symlink {
1367 entry.mode = 0o120000;
1368 }
1369 if let Some(mode) = preferred_unmerged_mode_for_untrusted_worktree(
1370 &index.entries[existing_range.clone()],
1371 trust_filemode,
1372 trust_symlinks,
1373 ) {
1374 entry.mode = mode;
1375 }
1376 reports.push(format!("add '{}'", String::from_utf8_lossy(&git_path)));
1379 if let Some(executable) = path_chmod {
1380 if is_symlink {
1385 eprintln!(
1386 "fatal: git update-index: cannot chmod {}x '{}'",
1387 if executable { '+' } else { '-' },
1388 String::from_utf8_lossy(&git_path)
1389 );
1390 if !non_atomic_chmod_errors {
1391 return Err(GitError::Exit(128));
1392 }
1393 chmod_error = true;
1394 } else {
1395 entry.mode = if executable { 0o100755 } else { 0o100644 };
1396 reports.push(format!(
1397 "chmod {}x '{}'",
1398 if executable { '+' } else { '-' },
1399 String::from_utf8_lossy(&git_path)
1400 ));
1401 }
1402 }
1403 record_resolve_undo_for_range(&mut index, format, &git_path, existing_range.clone())?;
1404 replace_index_entries_with_entry(&mut index.entries, entry);
1405 untracked_cache_invalidation_paths.push(git_path);
1406 updated.push(oid);
1407 }
1408 normalize_index_version_for_extended_flags(&mut index);
1409 update_cache_tree_for_git_paths(
1410 &mut index,
1411 format,
1412 &odb,
1413 &untracked_cache_invalidation_paths,
1414 )?;
1415 invalidate_untracked_cache_for_git_paths(
1416 &mut index,
1417 format,
1418 &untracked_cache_invalidation_paths,
1419 )?;
1420 if !pending_large.is_empty() {
1421 write_pending_large_blobs(&odb, &pending_large, large_policy)?;
1422 }
1423 let skip_hash = clean_config
1427 .map(index_skip_hash_from_config)
1428 .unwrap_or(false);
1429 write_repository_index_ref_skip_hash(git_dir, format, &index, skip_hash)?;
1430 if verbose {
1431 let mut stdout = std::io::stdout().lock();
1432 for line in &reports {
1433 writeln!(stdout, "{line}")?;
1434 }
1435 stdout.flush()?;
1436 }
1437 if chmod_error {
1438 return Err(GitError::Exit(128));
1439 }
1440 Ok(UpdateIndexResult {
1441 entries: index.entries.len(),
1442 updated,
1443 })
1444}
1445
1446pub fn refresh_index_paths(
1447 worktree_root: impl AsRef<Path>,
1448 git_dir: impl AsRef<Path>,
1449 format: ObjectFormat,
1450 paths: &[PathBuf],
1451 quiet: bool,
1452 ignore_missing: bool,
1453 really_refresh: bool,
1454) -> Result<UpdateIndexResult> {
1455 refresh_index_paths_with_options(
1456 worktree_root,
1457 git_dir,
1458 format,
1459 paths,
1460 quiet,
1461 ignore_missing,
1462 false,
1463 false,
1464 really_refresh,
1465 )
1466}
1467
1468pub fn refresh_index_paths_with_options(
1469 worktree_root: impl AsRef<Path>,
1470 git_dir: impl AsRef<Path>,
1471 format: ObjectFormat,
1472 paths: &[PathBuf],
1473 quiet: bool,
1474 ignore_missing: bool,
1475 ignore_submodules: bool,
1476 allow_unmerged: bool,
1477 really_refresh: bool,
1478) -> Result<UpdateIndexResult> {
1479 let worktree_root = worktree_root.as_ref();
1480 let git_dir = git_dir.as_ref();
1481 let index_path = repository_index_path(git_dir);
1482 if !index_path.exists() {
1483 return Ok(UpdateIndexResult {
1484 entries: 0,
1485 updated: Vec::new(),
1486 });
1487 }
1488 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
1489 let trust_filemode = trust_executable_bit_from_git_dir(git_dir, None);
1490 let index_mtime = fs::metadata(&index_path)
1498 .ok()
1499 .and_then(|metadata| file_mtime_parts(&metadata));
1500 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1501 let selected_paths = paths
1502 .iter()
1503 .map(|path| {
1504 let absolute = if path.is_absolute() {
1505 path.clone()
1506 } else {
1507 worktree_root.join(path)
1508 };
1509 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1510 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1511 })?;
1512 git_path_bytes(relative)
1513 })
1514 .collect::<Result<Vec<_>>>()?;
1515 let selected_paths = selected_paths.into_iter().collect::<BTreeSet<_>>();
1516 if selected_paths.is_empty()
1517 && !really_refresh
1518 && !index
1519 .entries
1520 .iter()
1521 .any(|entry| entry.flags & INDEX_FLAG_ASSUME_UNCHANGED != 0)
1522 {
1523 return refresh_all_index_paths_parallel(
1524 worktree_root,
1525 git_dir,
1526 format,
1527 index,
1528 stat_cache,
1529 quiet,
1530 ignore_missing,
1531 ignore_submodules,
1532 allow_unmerged,
1533 trust_filemode,
1534 );
1535 }
1536 let mut needs_update = false;
1537 let mut index_dirty = false;
1538 for entry in &mut index.entries {
1539 if index_entry_stage(entry) != 0 {
1540 continue;
1541 }
1542 if entry.flags & INDEX_FLAG_ASSUME_UNCHANGED != 0 {
1543 if !really_refresh {
1544 continue;
1545 }
1546 entry.flags &= !INDEX_FLAG_ASSUME_UNCHANGED;
1547 index_dirty = true;
1548 }
1549 let absolute = worktree_root.join(repo_path_to_os_path(entry.path.as_bytes())?);
1550 let Ok(metadata) = fs::metadata(&absolute) else {
1551 if ignore_missing {
1552 continue;
1553 }
1554 if !quiet {
1555 print_update_index_needs_update(entry.path.as_bytes());
1556 }
1557 needs_update = true;
1558 continue;
1559 };
1560 if sley_index::is_gitlink(entry.mode) {
1570 match sley_index::gitlink_stat_verdict(&metadata) {
1571 sley_index::GitlinkStatVerdict::Populated => {
1572 if ignore_submodules {
1573 continue;
1574 }
1575 if let Some(oid) = sley_diff_merge::gitlink_head_oid(&absolute, format)
1576 && oid != entry.oid
1577 {
1578 if !quiet {
1579 print_update_index_needs_update(entry.path.as_bytes());
1580 }
1581 needs_update = true;
1582 }
1583 continue;
1584 }
1585 sley_index::GitlinkStatVerdict::TypeChanged => {
1586 if !quiet {
1587 print_update_index_needs_update(entry.path.as_bytes());
1588 }
1589 needs_update = true;
1590 continue;
1591 }
1592 }
1593 }
1594 if !metadata.is_file() {
1595 if !quiet {
1596 print_update_index_needs_update(entry.path.as_bytes());
1597 }
1598 needs_update = true;
1599 continue;
1600 }
1601 if stat_cache.reuse_index_entry(entry, &metadata).is_some() {
1608 continue;
1609 }
1610 let body = fs::read(&absolute)?;
1611 let object = EncodedObject::new(ObjectType::Blob, body);
1612 let oid = object.object_id(format)?;
1613 if oid != entry.oid || file_mode_with_trust(&metadata, trust_filemode) != entry.mode {
1614 if !quiet {
1615 print_update_index_needs_update(entry.path.as_bytes());
1616 }
1617 needs_update = true;
1618 if really_refresh
1619 && !selected_paths.is_empty()
1620 && selected_paths.contains(entry.path.as_bytes())
1621 {
1622 let updated_entry = index_entry_from_metadata_with_filemode(
1623 entry.path.clone(),
1624 oid,
1625 &metadata,
1626 trust_filemode,
1627 );
1628 if updated_entry != *entry {
1629 *entry = updated_entry;
1630 index_dirty = true;
1631 }
1632 }
1633 continue;
1634 }
1635 let updated_entry = index_entry_from_metadata_with_filemode(
1636 entry.path.clone(),
1637 oid,
1638 &metadata,
1639 trust_filemode,
1640 );
1641 if updated_entry != *entry {
1642 *entry = updated_entry;
1643 index_dirty = true;
1644 }
1645 }
1646 if index_dirty {
1647 write_repository_index_ref(git_dir, format, &index)?;
1648 }
1649 if needs_update && !quiet {
1650 return Err(GitError::Exit(1));
1651 }
1652 Ok(UpdateIndexResult {
1653 entries: index.entries.len(),
1654 updated: Vec::new(),
1655 })
1656}
1657
1658pub(crate) fn refresh_all_index_paths_parallel(
1659 worktree_root: &Path,
1660 git_dir: &Path,
1661 format: ObjectFormat,
1662 mut index: Index,
1663 stat_cache: IndexStatCache,
1664 quiet: bool,
1665 ignore_missing: bool,
1666 ignore_submodules: bool,
1667 _allow_unmerged: bool,
1668 trust_filemode: bool,
1669) -> Result<UpdateIndexResult> {
1670 let prechecks =
1671 tracked_only_non_clean_prechecks_parallel(worktree_root, &index, &stat_cache, false)?;
1672 let mut needs_update = false;
1673 let mut index_dirty = false;
1674 for precheck in prechecks {
1675 match precheck {
1676 TrackedOnlyPrecheck::Deleted(idx) => {
1677 if ignore_missing {
1678 continue;
1679 }
1680 if !quiet {
1681 print_update_index_needs_update(index.entries[idx].path.as_bytes());
1682 }
1683 needs_update = true;
1684 }
1685 TrackedOnlyPrecheck::Slow(idx) => {
1686 let entry = &mut index.entries[idx];
1687 let path = entry.path.as_bytes().to_vec();
1688 let absolute = worktree_root.join(repo_path_to_os_path(&path)?);
1689 let Ok(metadata) = fs::metadata(&absolute) else {
1690 if ignore_missing {
1691 continue;
1692 }
1693 if !quiet {
1694 print_update_index_needs_update(&path);
1695 }
1696 needs_update = true;
1697 continue;
1698 };
1699 if sley_index::is_gitlink(entry.mode) {
1703 match sley_index::gitlink_stat_verdict(&metadata) {
1704 sley_index::GitlinkStatVerdict::Populated => {
1705 if ignore_submodules {
1706 continue;
1707 }
1708 if let Some(oid) = sley_diff_merge::gitlink_head_oid(&absolute, format)
1709 && oid != entry.oid
1710 {
1711 if !quiet {
1712 print_update_index_needs_update(&path);
1713 }
1714 needs_update = true;
1715 }
1716 continue;
1717 }
1718 sley_index::GitlinkStatVerdict::TypeChanged => {
1719 if !quiet {
1720 print_update_index_needs_update(&path);
1721 }
1722 needs_update = true;
1723 continue;
1724 }
1725 }
1726 }
1727 if !metadata.is_file() {
1728 if !quiet {
1729 print_update_index_needs_update(&path);
1730 }
1731 needs_update = true;
1732 continue;
1733 }
1734 if stat_cache.reuse_index_entry(entry, &metadata).is_some() {
1735 continue;
1736 }
1737 let body = fs::read(&absolute)?;
1738 let object = EncodedObject::new(ObjectType::Blob, body);
1739 let oid = object.object_id(format)?;
1740 if oid != entry.oid || file_mode_with_trust(&metadata, trust_filemode) != entry.mode
1741 {
1742 if !quiet {
1743 print_update_index_needs_update(&path);
1744 }
1745 needs_update = true;
1746 continue;
1747 }
1748 let updated_entry = index_entry_from_metadata_with_filemode(
1749 entry.path.clone(),
1750 oid,
1751 &metadata,
1752 trust_filemode,
1753 );
1754 if updated_entry != *entry {
1755 *entry = updated_entry;
1756 index_dirty = true;
1757 }
1758 }
1759 }
1760 }
1761 if index_dirty {
1762 write_repository_index_ref(git_dir, format, &index)?;
1763 }
1764 if needs_update && !quiet {
1765 return Err(GitError::Exit(1));
1766 }
1767 Ok(UpdateIndexResult {
1768 entries: index.entries.len(),
1769 updated: Vec::new(),
1770 })
1771}
1772
1773pub fn update_index_again(
1774 worktree_root: impl AsRef<Path>,
1775 git_dir: impl AsRef<Path>,
1776 format: ObjectFormat,
1777 paths: &[PathBuf],
1778 options: UpdateIndexOptions,
1779) -> Result<UpdateIndexResult> {
1780 let worktree_root = worktree_root.as_ref();
1781 let git_dir = git_dir.as_ref();
1782 let index_path = repository_index_path(git_dir);
1783 if !index_path.exists() {
1784 return Ok(UpdateIndexResult {
1785 entries: 0,
1786 updated: Vec::new(),
1787 });
1788 }
1789 let index = Index::parse(&fs::read(&index_path)?, format)?;
1790 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1791 let head_entries = head_tree_entries(git_dir, format, &db)?;
1792 let selected_paths = selected_git_paths(worktree_root, paths)?;
1793 let mut again_paths = Vec::new();
1794 for entry in &index.entries {
1795 if index_entry_stage(entry) != 0 {
1796 continue;
1797 }
1798 if !selected_paths.is_empty() && !git_path_selected(entry.path.as_bytes(), &selected_paths)
1799 {
1800 continue;
1801 }
1802 let differs_from_head = match head_entries.get(entry.path.as_bytes()) {
1803 Some(head_entry) => head_entry.oid != entry.oid || head_entry.mode != entry.mode,
1804 None => true,
1805 };
1806 if differs_from_head {
1807 again_paths.push(worktree_root.join(repo_path_to_os_path(entry.path.as_bytes())?));
1808 }
1809 }
1810 if again_paths.is_empty() {
1811 return Ok(UpdateIndexResult {
1812 entries: index.entries.len(),
1813 updated: Vec::new(),
1814 });
1815 }
1816 update_index_paths(worktree_root, git_dir, format, &again_paths, options)
1817}
1818
1819pub fn set_index_assume_unchanged_paths(
1820 worktree_root: impl AsRef<Path>,
1821 git_dir: impl AsRef<Path>,
1822 format: ObjectFormat,
1823 paths: &[PathBuf],
1824 assume_unchanged: bool,
1825) -> Result<UpdateIndexResult> {
1826 let worktree_root = worktree_root.as_ref();
1827 let git_dir = git_dir.as_ref();
1828 let index_path = repository_index_path(git_dir);
1829 let mut index = if index_path.exists() {
1830 Index::parse(&fs::read(&index_path)?, format)?
1831 } else {
1832 Index {
1833 version: 2,
1834 entries: Vec::new(),
1835 extensions: Vec::new(),
1836 checksum: None,
1837 }
1838 };
1839 let sparse = active_sparse_checkout(git_dir)?;
1840 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1841 if index.is_sparse() {
1842 expand_sparse_index(&mut index, &db, format)?;
1843 }
1844 let selected_paths = paths
1845 .iter()
1846 .map(|path| {
1847 let absolute = if path.is_absolute() {
1848 path.clone()
1849 } else {
1850 worktree_root.join(path)
1851 };
1852 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1853 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1854 })?;
1855 git_path_bytes(relative)
1856 })
1857 .collect::<Result<Vec<_>>>()?;
1858 for path in selected_paths {
1859 if let Some(entry) = index.entries.iter_mut().find(|entry| entry.path == path) {
1860 if assume_unchanged {
1861 entry.flags |= INDEX_FLAG_ASSUME_UNCHANGED;
1862 } else {
1863 entry.flags &= !INDEX_FLAG_ASSUME_UNCHANGED;
1864 }
1865 }
1866 }
1867 normalize_index_version_for_extended_flags(&mut index);
1868 if let Some((sparse, mode)) = sparse
1869 && sparse.sparse_index
1870 {
1871 let matcher = SparseMatcher::new(&sparse, mode);
1872 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
1873 }
1874 write_repository_index_ref(git_dir, format, &index)?;
1875 Ok(UpdateIndexResult {
1876 entries: index.entries.len(),
1877 updated: Vec::new(),
1878 })
1879}
1880
1881pub(crate) fn selected_git_paths(
1882 worktree_root: &Path,
1883 paths: &[PathBuf],
1884) -> Result<BTreeSet<Vec<u8>>> {
1885 paths
1886 .iter()
1887 .map(|path| {
1888 let absolute = if path.is_absolute() {
1889 path.clone()
1890 } else {
1891 worktree_root.join(path)
1892 };
1893 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1894 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1895 })?;
1896 git_path_bytes(relative)
1897 })
1898 .collect()
1899}
1900
1901pub(crate) fn git_path_selected(path: &[u8], selected_paths: &BTreeSet<Vec<u8>>) -> bool {
1902 selected_paths
1903 .iter()
1904 .any(|selected| path == selected || index_entry_is_under_path(path, selected))
1905}
1906
1907pub fn set_index_skip_worktree_paths(
1908 worktree_root: impl AsRef<Path>,
1909 git_dir: impl AsRef<Path>,
1910 format: ObjectFormat,
1911 paths: &[PathBuf],
1912 skip_worktree: bool,
1913) -> Result<UpdateIndexResult> {
1914 let worktree_root = worktree_root.as_ref();
1915 let git_dir = git_dir.as_ref();
1916 let index_path = repository_index_path(git_dir);
1917 let mut index = if index_path.exists() {
1918 Index::parse(&fs::read(&index_path)?, format)?
1919 } else {
1920 Index {
1921 version: 2,
1922 entries: Vec::new(),
1923 extensions: Vec::new(),
1924 checksum: None,
1925 }
1926 };
1927 let sparse = active_sparse_checkout(git_dir)?;
1928 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1929 if index.is_sparse() {
1930 expand_sparse_index(&mut index, &db, format)?;
1931 }
1932 let selected_paths = paths
1933 .iter()
1934 .map(|path| {
1935 let absolute = if path.is_absolute() {
1936 path.clone()
1937 } else {
1938 worktree_root.join(path)
1939 };
1940 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1941 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1942 })?;
1943 git_path_bytes(relative)
1944 })
1945 .collect::<Result<Vec<_>>>()?;
1946 for path in selected_paths {
1947 if let Some(entry) = index.entries.iter_mut().find(|entry| entry.path == path) {
1948 if skip_worktree {
1949 entry.flags |= INDEX_FLAG_EXTENDED;
1950 entry.flags_extended |= INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
1951 } else {
1952 entry.flags_extended &= !INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
1953 if entry.flags_extended == 0 {
1954 entry.flags &= !INDEX_FLAG_EXTENDED;
1955 }
1956 }
1957 }
1958 }
1959 normalize_index_version_for_extended_flags(&mut index);
1960 if let Some((sparse, mode)) = sparse
1961 && sparse.sparse_index
1962 {
1963 let matcher = SparseMatcher::new(&sparse, mode);
1964 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
1965 }
1966 write_repository_index_ref(git_dir, format, &index)?;
1967 Ok(UpdateIndexResult {
1968 entries: index.entries.len(),
1969 updated: Vec::new(),
1970 })
1971}
1972
1973pub fn set_index_fsmonitor_valid_paths(
1974 worktree_root: impl AsRef<Path>,
1975 git_dir: impl AsRef<Path>,
1976 format: ObjectFormat,
1977 paths: &[PathBuf],
1978 _fsmonitor_valid: bool,
1979) -> Result<UpdateIndexResult> {
1980 let worktree_root = worktree_root.as_ref();
1981 let git_dir = git_dir.as_ref();
1982 let index_path = repository_index_path(git_dir);
1983 let index = if index_path.exists() {
1984 Index::parse(&fs::read(&index_path)?, format)?
1985 } else {
1986 Index {
1987 version: 2,
1988 entries: Vec::new(),
1989 extensions: Vec::new(),
1990 checksum: None,
1991 }
1992 };
1993 let selected_paths = paths
1994 .iter()
1995 .map(|path| {
1996 let absolute = if path.is_absolute() {
1997 path.clone()
1998 } else {
1999 worktree_root.join(path)
2000 };
2001 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
2002 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
2003 })?;
2004 git_path_bytes(relative)
2005 })
2006 .collect::<Result<Vec<_>>>()?;
2007 for path in selected_paths {
2008 if !index.entries.iter().any(|entry| entry.path == path) {
2009 eprintln!(
2010 "fatal: Unable to mark file {}",
2011 String::from_utf8_lossy(&path)
2012 );
2013 return Err(GitError::Exit(128));
2014 }
2015 }
2016 Ok(UpdateIndexResult {
2017 entries: index.entries.len(),
2018 updated: Vec::new(),
2019 })
2020}
2021
2022pub fn set_index_version(
2023 git_dir: impl AsRef<Path>,
2024 format: ObjectFormat,
2025 version: u32,
2026 verbose: bool,
2027) -> Result<UpdateIndexResult> {
2028 if !matches!(version, 2..=4) {
2029 return Err(GitError::Unsupported(format!(
2030 "update-index currently supports --index-version 2, 3, or 4, got {version}"
2031 )));
2032 }
2033 let git_dir = git_dir.as_ref();
2034 let index_path = repository_index_path(git_dir);
2035 let mut index = if index_path.exists() {
2036 Index::parse(&fs::read(&index_path)?, format)?
2037 } else {
2038 Index {
2039 version: 2,
2040 entries: Vec::new(),
2041 extensions: Vec::new(),
2042 checksum: None,
2043 }
2044 };
2045 let previous = index.version;
2048 if verbose {
2049 println!("index-version: was {previous}, set to {version}");
2050 }
2051 index.version = version;
2052 normalize_index_version_for_extended_flags(&mut index);
2053 write_repository_index_ref(git_dir, format, &index)?;
2054 Ok(UpdateIndexResult {
2055 entries: index.entries.len(),
2056 updated: Vec::new(),
2057 })
2058}
2059
2060pub fn force_write_index(
2061 git_dir: impl AsRef<Path>,
2062 format: ObjectFormat,
2063) -> Result<UpdateIndexResult> {
2064 let git_dir = git_dir.as_ref();
2065 let index_path = repository_index_path(git_dir);
2066 let mut index = if index_path.exists() {
2067 Index::parse(&fs::read(&index_path)?, format)?
2068 } else {
2069 Index {
2070 version: 2,
2071 entries: Vec::new(),
2072 extensions: Vec::new(),
2073 checksum: None,
2074 }
2075 };
2076 normalize_index_version_for_extended_flags(&mut index);
2077 write_repository_index_ref(git_dir, format, &index)?;
2078 Ok(UpdateIndexResult {
2079 entries: index.entries.len(),
2080 updated: Vec::new(),
2081 })
2082}
2083
2084pub fn enable_untracked_cache(
2085 worktree_root: impl AsRef<Path>,
2086 git_dir: impl AsRef<Path>,
2087 format: ObjectFormat,
2088) -> Result<()> {
2089 let worktree_root = worktree_root.as_ref();
2090 let git_dir = git_dir.as_ref();
2091 let index_path = repository_index_path(git_dir);
2092 let mut index = if index_path.exists() {
2093 Index::parse(&fs::read(&index_path)?, format)?
2094 } else {
2095 empty_index()
2096 };
2097 let ident = untracked_cache_ident(worktree_root);
2098 let dir_flags = untracked_cache_dir_flags(StatusUntrackedMode::Normal);
2099 let cache = match index.untracked_cache(format)? {
2100 Some(mut cache) if cache.ident == ident => {
2101 cache.dir_flags = dir_flags;
2102 cache
2103 }
2104 _ => UntrackedCache::new(format, ident, dir_flags),
2105 };
2106 index.set_untracked_cache(format, Some(&cache))?;
2107 write_repository_index_ref(git_dir, format, &index)?;
2108 Ok(())
2109}
2110
2111pub fn disable_untracked_cache(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<()> {
2112 let git_dir = git_dir.as_ref();
2113 let index_path = repository_index_path(git_dir);
2114 if !index_path.exists() {
2115 return Ok(());
2116 }
2117 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
2118 index.set_untracked_cache(format, None)?;
2119 write_repository_index_ref(git_dir, format, &index)?;
2120 Ok(())
2121}
2122
2123pub fn refresh_untracked_cache_after_status(
2124 worktree_root: impl AsRef<Path>,
2125 git_dir: impl AsRef<Path>,
2126 format: ObjectFormat,
2127 config: &GitConfig,
2128 untracked_mode: StatusUntrackedMode,
2129) -> Result<()> {
2130 if matches!(untracked_mode, StatusUntrackedMode::None) {
2131 return Ok(());
2132 }
2133 let worktree_root = worktree_root.as_ref();
2134 let git_dir = git_dir.as_ref();
2135 let index_path = repository_index_path(git_dir);
2136 let untracked_cache_setting = config.get("core", None, "untrackedCache");
2137 match untracked_cache_setting {
2138 Some("keep") | None => {
2139 if !repository_index_has_extension(git_dir, format, b"UNTR")? {
2140 return Ok(());
2141 }
2142 }
2143 Some("false" | "no" | "off" | "0") | Some("true" | "yes" | "on" | "1") => {}
2144 Some(_) => {
2145 if !repository_index_has_extension(git_dir, format, b"UNTR")? {
2146 return Ok(());
2147 }
2148 }
2149 }
2150 let mut index = if index_path.exists() {
2151 Index::parse(&fs::read(&index_path)?, format)?
2152 } else {
2153 empty_index()
2154 };
2155 match untracked_cache_setting {
2156 Some("false") | Some("no") | Some("off") | Some("0") => {
2157 index.set_untracked_cache(format, None)?;
2158 write_repository_index_ref(git_dir, format, &index)?;
2159 return Ok(());
2160 }
2161 Some("true") | Some("yes") | Some("on") | Some("1") => {}
2162 Some("keep") | None => {
2163 if index.untracked_cache(format)?.is_none() {
2164 return Ok(());
2165 }
2166 }
2167 Some(_) => {
2168 if index.untracked_cache(format)?.is_none() {
2169 return Ok(());
2170 }
2171 }
2172 }
2173 let old_cache = index.untracked_cache(format).ok().flatten();
2174 let ident = untracked_cache_ident(worktree_root);
2175 if old_cache.as_ref().is_some_and(|cache| cache.ident != ident) {
2176 eprintln!("warning: untracked cache is disabled on this system or location");
2177 emit_untracked_cache_bypass_trace();
2178 return Ok(());
2179 }
2180 let cache = build_untracked_cache(worktree_root, git_dir, format, &index, untracked_mode)?;
2181 emit_untracked_cache_trace(old_cache.as_ref(), &cache);
2182 index.set_untracked_cache(format, Some(&cache))?;
2183 write_repository_index_ref(git_dir, format, &index)?;
2184 Ok(())
2185}
2186
2187pub(crate) fn repository_index_has_extension(
2188 git_dir: &Path,
2189 format: ObjectFormat,
2190 signature: &[u8; 4],
2191) -> Result<bool> {
2192 let index_path = repository_index_path(git_dir);
2193 if !index_path.exists() {
2194 return Ok(false);
2195 }
2196 let bytes = read_borrowed_index_bytes(&index_path)?;
2197 sley_index::Index::bytes_have_extension(bytes.as_ref(), format, signature)
2198}
2199
2200pub fn emit_untracked_cache_bypass_trace() {
2201 sley_core::trace2::perf_read_directory_data("path", "");
2202}
2203
2204pub(crate) fn index_extensions_without_cache_tree(extensions: &[u8]) -> Vec<u8> {
2205 let mut offset = 0;
2206 let mut filtered = Vec::new();
2207 while offset < extensions.len() {
2208 if extensions.len().saturating_sub(offset) < 8 {
2209 return Vec::new();
2210 }
2211 let signature = &extensions[offset..offset + 4];
2212 let size = u32::from_be_bytes([
2213 extensions[offset + 4],
2214 extensions[offset + 5],
2215 extensions[offset + 6],
2216 extensions[offset + 7],
2217 ]) as usize;
2218 let end = offset + 8 + size;
2219 if end > extensions.len() {
2220 return Vec::new();
2221 }
2222 if signature != b"TREE" {
2223 filtered.extend_from_slice(&extensions[offset..end]);
2224 }
2225 offset = end;
2226 }
2227 filtered
2228}
2229
2230pub(crate) fn update_cache_tree_for_git_paths(
2231 index: &mut Index,
2232 format: ObjectFormat,
2233 odb: &FileObjectDatabase,
2234 paths: &[Vec<u8>],
2235) -> Result<()> {
2236 if paths.is_empty() {
2237 return Ok(());
2238 }
2239 match index.cache_tree(format) {
2240 Ok(Some(mut cache_tree)) if !cache_tree_has_invalid_untouched(&cache_tree, b"", paths) => {
2241 for path in paths {
2242 invalidate_cache_tree_path(&mut cache_tree, path);
2243 }
2244 index.set_cache_tree(Some(&cache_tree))?;
2245 return Ok(());
2246 }
2247 Ok(_) => {}
2248 Err(_) => {
2249 index.extensions = index_extensions_without_cache_tree(&index.extensions);
2250 return Ok(());
2251 }
2252 }
2253 let cache_tree = match build_cache_tree_for_index_update(&index.entries, b"", paths, odb) {
2254 Ok(cache_tree) => cache_tree,
2255 Err(_) => {
2256 index.extensions = index_extensions_without_cache_tree(&index.extensions);
2257 return Ok(());
2258 }
2259 };
2260 index.set_cache_tree(Some(&cache_tree))?;
2261 Ok(())
2262}
2263
2264pub fn refresh_cache_tree(index: &mut Index, odb: &FileObjectDatabase) {
2271 match build_cache_tree_for_index_update(&index.entries, b"", &[], odb) {
2272 Ok(cache_tree) => {
2273 if index.set_cache_tree(Some(&cache_tree)).is_err() {
2274 index.extensions = index_extensions_without_cache_tree(&index.extensions);
2275 }
2276 }
2277 Err(_) => {
2278 index.extensions = index_extensions_without_cache_tree(&index.extensions);
2279 }
2280 }
2281}
2282
2283pub fn refresh_repository_cache_tree(
2284 git_dir: &Path,
2285 format: ObjectFormat,
2286 odb: &FileObjectDatabase,
2287) -> Result<()> {
2288 let index_path = repository_index_path(git_dir);
2289 let Ok(bytes) = fs::read(&index_path) else {
2290 return Ok(());
2291 };
2292 let mut index = Index::parse(&bytes, format)?;
2293 if index.split_index_link(format)?.is_some() {
2294 return Ok(());
2295 }
2296 refresh_cache_tree(&mut index, odb);
2297 write_repository_index_ref(git_dir, format, &index)
2298}
2299
2300fn invalidate_cache_tree_path(tree: &mut CacheTree, path: &[u8]) {
2301 tree.entry_count = -1;
2302 tree.oid = None;
2303
2304 let mut rest = path;
2305 while rest.first() == Some(&b'/') {
2306 rest = &rest[1..];
2307 }
2308 if rest.is_empty() {
2309 return;
2310 }
2311 let split = rest
2312 .iter()
2313 .position(|byte| *byte == b'/')
2314 .unwrap_or(rest.len());
2315 let component = &rest[..split];
2316 let remaining = if split == rest.len() {
2317 &[][..]
2318 } else {
2319 &rest[split + 1..]
2320 };
2321 if let Some(child) = tree
2322 .subtrees
2323 .iter_mut()
2324 .find(|child| child.name.as_slice() == component)
2325 {
2326 invalidate_cache_tree_path(&mut child.tree, remaining);
2327 }
2328}
2329
2330fn cache_tree_has_invalid_untouched(tree: &CacheTree, prefix: &[u8], paths: &[Vec<u8>]) -> bool {
2331 if !cache_tree_prefix_touched(prefix, paths) && (tree.entry_count < 0 || tree.oid.is_none()) {
2332 return true;
2333 }
2334 tree.subtrees.iter().any(|child| {
2335 let mut child_prefix = Vec::with_capacity(prefix.len() + child.name.len() + 1);
2336 child_prefix.extend_from_slice(prefix);
2337 child_prefix.extend_from_slice(&child.name);
2338 child_prefix.push(b'/');
2339 cache_tree_has_invalid_untouched(&child.tree, &child_prefix, paths)
2340 })
2341}
2342
2343fn build_cache_tree_for_index_update(
2344 entries: &[IndexEntry],
2345 prefix: &[u8],
2346 invalid_paths: &[Vec<u8>],
2347 odb: &FileObjectDatabase,
2348) -> Result<CacheTree> {
2349 let mut invalid = cache_tree_prefix_touched(prefix, invalid_paths);
2350 let mut subtrees = Vec::new();
2351 let mut tree_entries = Vec::new();
2352 let mut index = 0usize;
2353 while index < entries.len() {
2354 let entry = &entries[index];
2355 if entry.stage() != Stage::Normal || entry.is_intent_to_add() {
2356 invalid = true;
2357 index += 1;
2358 continue;
2359 }
2360 let path = entry.path.as_bytes();
2361 let Some(remainder) = path.strip_prefix(prefix) else {
2362 return Err(GitError::InvalidPath(format!(
2363 "invalid index path {}",
2364 String::from_utf8_lossy(path)
2365 )));
2366 };
2367 if remainder.is_empty() || remainder[0] == b'/' {
2368 return Err(GitError::InvalidPath(format!(
2369 "invalid index path {}",
2370 String::from_utf8_lossy(path)
2371 )));
2372 }
2373 if entry.mode == SPARSE_DIR_MODE
2374 && let Some(name) = remainder.strip_suffix(b"/")
2375 && !name.is_empty()
2376 && !name.contains(&b'/')
2377 {
2378 if !invalid {
2379 tree_entries.push(TreeEntry {
2380 mode: SPARSE_DIR_MODE,
2381 name: BString::from(name),
2382 oid: entry.oid,
2383 });
2384 }
2385 index += 1;
2386 continue;
2387 }
2388 if let Some(slash) = remainder.iter().position(|byte| *byte == b'/') {
2389 let name = &remainder[..slash];
2390 if name.is_empty() {
2391 return Err(GitError::InvalidPath(format!(
2392 "invalid index path {}",
2393 String::from_utf8_lossy(path)
2394 )));
2395 }
2396 let start = index;
2397 index += 1;
2398 while index < entries.len()
2399 && same_tree_component(entries[index].path.as_bytes(), prefix, name)?
2400 {
2401 index += 1;
2402 }
2403 let mut child_prefix = Vec::with_capacity(prefix.len() + name.len() + 1);
2404 child_prefix.extend_from_slice(prefix);
2405 child_prefix.extend_from_slice(name);
2406 child_prefix.push(b'/');
2407 let child_tree = build_cache_tree_for_index_update(
2408 &entries[start..index],
2409 &child_prefix,
2410 invalid_paths,
2411 odb,
2412 )?;
2413 if !invalid {
2414 if let Some(oid) = child_tree.oid {
2415 tree_entries.push(TreeEntry {
2416 mode: 0o040000,
2417 name: BString::from(name),
2418 oid,
2419 });
2420 } else {
2421 invalid = true;
2422 }
2423 }
2424 subtrees.push(sley_index::CacheTreeChild {
2425 name: name.to_vec(),
2426 tree: child_tree,
2427 });
2428 continue;
2429 }
2430 if !invalid {
2431 tree_entries.push(TreeEntry {
2432 mode: entry.mode,
2433 name: BString::from(remainder),
2434 oid: entry.oid,
2435 });
2436 }
2437 index += 1;
2438 }
2439
2440 if invalid {
2441 return Ok(CacheTree {
2442 entry_count: -1,
2443 oid: None,
2444 subtrees,
2445 });
2446 }
2447 tree_entries.sort_by(|left, right| {
2448 git_tree_entry_cmp(
2449 left.name.as_bytes(),
2450 left.mode,
2451 right.name.as_bytes(),
2452 right.mode,
2453 )
2454 });
2455 let oid = odb.write_object(EncodedObject::new(
2456 ObjectType::Tree,
2457 Tree {
2458 entries: tree_entries,
2459 }
2460 .write(),
2461 ))?;
2462 let entry_count = i32::try_from(entries.len())
2463 .map_err(|_| GitError::InvalidFormat("cache-tree entry count overflow".into()))?;
2464 Ok(CacheTree {
2465 entry_count,
2466 oid: Some(oid),
2467 subtrees,
2468 })
2469}
2470
2471fn cache_tree_prefix_touched(prefix: &[u8], paths: &[Vec<u8>]) -> bool {
2472 if paths.is_empty() {
2473 return false;
2474 }
2475 if prefix.is_empty() {
2476 return true;
2477 }
2478 let dir = prefix.strip_suffix(b"/").unwrap_or(prefix);
2479 paths
2480 .iter()
2481 .any(|path| path.as_slice() == dir || path.starts_with(prefix))
2482}
2483
2484#[derive(Clone)]
2485pub(crate) struct ResolveUndoRecord {
2486 pub(crate) path: Vec<u8>,
2487 pub(crate) stages: [Option<(u32, ObjectId)>; 3],
2488}
2489
2490pub(crate) fn record_resolve_undo_for_path(
2491 index: &mut Index,
2492 format: ObjectFormat,
2493 path: &[u8],
2494 entries: &[IndexEntry],
2495) -> Result<()> {
2496 let mut stages = [None, None, None];
2497 for entry in entries {
2498 match entry.stage() {
2499 Stage::Base => stages[0] = Some((entry.mode, entry.oid)),
2500 Stage::Ours => stages[1] = Some((entry.mode, entry.oid)),
2501 Stage::Theirs => stages[2] = Some((entry.mode, entry.oid)),
2502 Stage::Normal => {}
2503 }
2504 }
2505 if stages.iter().all(Option::is_none) {
2506 return Ok(());
2507 }
2508 let mut records = parse_resolve_undo_records(index.extension(b"REUC")?, format)?;
2509 records.retain(|record| record.path.as_slice() != path);
2510 records.push(ResolveUndoRecord {
2511 path: path.to_vec(),
2512 stages,
2513 });
2514 records.sort_by(|left, right| left.path.cmp(&right.path));
2515 set_resolve_undo_extension(index, &records)
2516}
2517
2518pub(crate) fn record_resolve_undo_for_range(
2519 index: &mut Index,
2520 format: ObjectFormat,
2521 path: &[u8],
2522 range: Range<usize>,
2523) -> Result<()> {
2524 if range.is_empty() {
2525 return Ok(());
2526 }
2527 let entries = index.entries[range].to_vec();
2528 record_resolve_undo_for_path(index, format, path, &entries)
2529}
2530
2531pub(crate) fn parse_resolve_undo_records(
2532 body: Option<&[u8]>,
2533 format: ObjectFormat,
2534) -> Result<Vec<ResolveUndoRecord>> {
2535 let Some(body) = body else {
2536 return Ok(Vec::new());
2537 };
2538 let mut records = Vec::new();
2539 let mut offset = 0usize;
2540 while offset < body.len() {
2541 let path_end = body[offset..]
2542 .iter()
2543 .position(|byte| *byte == 0)
2544 .ok_or_else(|| GitError::InvalidFormat("truncated REUC path".into()))?
2545 + offset;
2546 let path = body[offset..path_end].to_vec();
2547 offset = path_end + 1;
2548
2549 let mut modes = [0u32; 3];
2550 for mode in &mut modes {
2551 let mode_end = body[offset..]
2552 .iter()
2553 .position(|byte| *byte == 0)
2554 .ok_or_else(|| GitError::InvalidFormat("truncated REUC mode".into()))?
2555 + offset;
2556 let text = std::str::from_utf8(&body[offset..mode_end])
2557 .map_err(|_| GitError::InvalidFormat("invalid REUC mode".into()))?;
2558 *mode = u32::from_str_radix(text, 8)
2559 .map_err(|_| GitError::InvalidFormat("invalid REUC mode".into()))?;
2560 offset = mode_end + 1;
2561 }
2562
2563 let mut stages = [None, None, None];
2564 for (idx, mode) in modes.into_iter().enumerate() {
2565 if mode == 0 {
2566 continue;
2567 }
2568 let end = offset
2569 .checked_add(format.raw_len())
2570 .ok_or_else(|| GitError::InvalidFormat("REUC oid length overflow".into()))?;
2571 if end > body.len() {
2572 return Err(GitError::InvalidFormat("truncated REUC oid".into()));
2573 }
2574 stages[idx] = Some((mode, ObjectId::from_raw(format, &body[offset..end])?));
2575 offset = end;
2576 }
2577 records.push(ResolveUndoRecord { path, stages });
2578 }
2579 Ok(records)
2580}
2581
2582pub(crate) fn set_resolve_undo_extension(
2583 index: &mut Index,
2584 records: &[ResolveUndoRecord],
2585) -> Result<()> {
2586 let mut body = Vec::new();
2587 for record in records {
2588 body.extend_from_slice(&record.path);
2589 body.push(0);
2590 for stage in record.stages {
2591 match stage {
2592 Some((mode, _)) => body.extend_from_slice(format!("{mode:o}").as_bytes()),
2593 None => body.push(b'0'),
2594 }
2595 body.push(0);
2596 }
2597 for (_, oid) in record.stages.into_iter().flatten() {
2598 body.extend_from_slice(oid.as_bytes());
2599 }
2600 }
2601
2602 let chunks = index.extension_chunks()?;
2603 let mut rebuilt = Vec::with_capacity(index.extensions.len() + body.len() + 8);
2604 let mut replaced = false;
2605 for (signature, chunk_body) in chunks {
2606 if &signature == b"REUC" {
2607 if !body.is_empty() {
2608 append_index_extension(&mut rebuilt, b"REUC", &body)?;
2609 }
2610 replaced = true;
2611 } else {
2612 append_index_extension(&mut rebuilt, &signature, chunk_body)?;
2613 }
2614 }
2615 if !replaced && !body.is_empty() {
2616 append_index_extension(&mut rebuilt, b"REUC", &body)?;
2617 }
2618 index.extensions = rebuilt;
2619 Ok(())
2620}
2621
2622pub fn clear_resolve_undo(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<()> {
2623 let git_dir = git_dir.as_ref();
2624 let index_path = repository_index_path(git_dir);
2625 match fs::read(&index_path) {
2626 Ok(bytes) => {
2627 let mut index = Index::parse(&bytes, format)?;
2628 set_resolve_undo_extension(&mut index, &[])?;
2629 write_repository_index_ref(git_dir, format, &index)
2630 }
2631 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
2632 Err(err) => Err(err.into()),
2633 }
2634}
2635
2636pub(crate) fn append_index_extension(
2637 out: &mut Vec<u8>,
2638 signature: &[u8; 4],
2639 body: &[u8],
2640) -> Result<()> {
2641 let len = u32::try_from(body.len())
2642 .map_err(|_| GitError::InvalidFormat("index extension body too large".into()))?;
2643 out.extend_from_slice(signature);
2644 out.extend_from_slice(&len.to_be_bytes());
2645 out.extend_from_slice(body);
2646 Ok(())
2647}
2648
2649pub(crate) fn index_extensions_without_split_index_link(extensions: &[u8]) -> Vec<u8> {
2650 let mut offset = 0;
2651 let mut filtered = Vec::new();
2652 while offset < extensions.len() {
2653 if extensions.len().saturating_sub(offset) < 8 {
2654 filtered.extend_from_slice(&extensions[offset..]);
2655 break;
2656 }
2657 let signature = &extensions[offset..offset + 4];
2658 let len = u32::from_be_bytes([
2659 extensions[offset + 4],
2660 extensions[offset + 5],
2661 extensions[offset + 6],
2662 extensions[offset + 7],
2663 ]) as usize;
2664 let end = offset.saturating_add(8).saturating_add(len);
2665 if end > extensions.len() {
2666 filtered.extend_from_slice(&extensions[offset..]);
2667 break;
2668 }
2669 if signature != b"link" {
2670 filtered.extend_from_slice(&extensions[offset..end]);
2671 }
2672 offset = end;
2673 }
2674 filtered
2675}
2676
2677pub(crate) fn preserved_index_extensions(git_dir: &Path, format: ObjectFormat) -> Result<Vec<u8>> {
2678 let index_path = repository_index_path(git_dir);
2679 match fs::read(&index_path) {
2680 Ok(bytes) if bytes.is_empty() => Ok(Vec::new()),
2681 Ok(bytes) => {
2682 let index = Index::parse(&bytes, format)?;
2683 Ok(index_extensions_without_cache_tree_or_resolve_undo(
2684 &index.extensions,
2685 ))
2686 }
2687 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
2688 Err(err) => Err(err.into()),
2689 }
2690}
2691
2692pub(crate) fn index_extensions_without_cache_tree_or_resolve_undo(extensions: &[u8]) -> Vec<u8> {
2693 let mut filtered = Vec::new();
2694 let mut offset = 0usize;
2695 while offset + 8 <= extensions.len() {
2696 let signature = &extensions[offset..offset + 4];
2697 let len = u32::from_be_bytes([
2698 extensions[offset + 4],
2699 extensions[offset + 5],
2700 extensions[offset + 6],
2701 extensions[offset + 7],
2702 ]) as usize;
2703 let end = offset + 8 + len;
2704 if end > extensions.len() {
2705 filtered.extend_from_slice(&extensions[offset..]);
2706 break;
2707 }
2708 if signature != b"TREE" && signature != b"REUC" {
2709 filtered.extend_from_slice(&extensions[offset..end]);
2710 }
2711 offset = end;
2712 }
2713 filtered
2714}
2715
2716pub(crate) fn repository_index_is_split(git_dir: &Path, format: ObjectFormat) -> Result<bool> {
2717 let index_path = repository_index_path(git_dir);
2718 match fs::read(index_path) {
2719 Ok(bytes) if bytes.is_empty() => Ok(false),
2720 Ok(bytes) => Ok(Index::parse(&bytes, format)?
2721 .split_index_link(format)?
2722 .is_some()),
2723 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
2724 Err(err) => Err(err.into()),
2725 }
2726}
2727
2728pub(crate) fn git_test_split_index_enabled() -> bool {
2729 env::var("GIT_TEST_SPLIT_INDEX")
2730 .ok()
2731 .is_some_and(|value| !matches!(value.as_str(), "" | "0" | "false" | "False" | "FALSE"))
2732}
2733
2734pub fn write_repository_index(git_dir: &Path, format: ObjectFormat, index: Index) -> Result<()> {
2735 let split = index.split_index_link(format)?.is_some()
2736 || repository_index_is_split(git_dir, format)?
2737 || git_test_split_index_enabled();
2738 write_repository_index_ref_with_split(git_dir, format, &index, split)
2739}
2740
2741pub fn write_repository_index_ref(
2742 git_dir: &Path,
2743 format: ObjectFormat,
2744 index: &Index,
2745) -> Result<()> {
2746 let split = index.split_index_link(format)?.is_some()
2747 || repository_index_is_split(git_dir, format)?
2748 || git_test_split_index_enabled();
2749 write_repository_index_ref_with_split(git_dir, format, index, split)
2750}
2751
2752pub fn write_repository_index_ref_skip_hash(
2755 git_dir: &Path,
2756 format: ObjectFormat,
2757 index: &Index,
2758 skip_hash: bool,
2759) -> Result<()> {
2760 let split = index.split_index_link(format)?.is_some()
2761 || repository_index_is_split(git_dir, format)?
2762 || git_test_split_index_enabled();
2763 write_repository_index_ref_with_split_skip_hash(git_dir, format, index, split, skip_hash)
2764}
2765
2766pub(crate) fn write_repository_index_ref_with_split(
2767 git_dir: &Path,
2768 format: ObjectFormat,
2769 index: &Index,
2770 split: bool,
2771) -> Result<()> {
2772 write_repository_index_ref_with_split_skip_hash(git_dir, format, index, split, false)
2773}
2774
2775pub(crate) fn write_repository_index_ref_with_split_skip_hash(
2780 git_dir: &Path,
2781 format: ObjectFormat,
2782 index: &Index,
2783 split: bool,
2784 skip_hash: bool,
2785) -> Result<()> {
2786 let index_path = repository_index_path(git_dir);
2787 if !split || alternate_index_output_path(git_dir, &index_path) {
2788 let smudged_entries = racily_clean_entry_indexes_before_write(git_dir, format, index)?;
2789 let extensions = if index.split_index_link(format)?.is_some() {
2790 Cow::Owned(index_extensions_without_split_index_link(&index.extensions))
2791 } else {
2792 Cow::Borrowed(index.extensions.as_slice())
2793 };
2794 let mut bytes = if smudged_entries.is_empty() && matches!(extensions, Cow::Borrowed(_)) {
2795 index.write(format)?
2796 } else {
2797 write_index_with_entry_size_overrides(format, index, &smudged_entries, &extensions)?
2798 };
2799 if skip_hash {
2800 zero_trailing_index_hash(&mut bytes, format);
2801 }
2802 fs::write(&index_path, bytes)?;
2803 apply_index_shared_file_mode(git_dir, &index_path, None)?;
2804 return Ok(());
2805 }
2806
2807 if let Some(link) = index.split_index_link(format)?
2808 && !link.base_oid.is_null()
2809 && let Some(base) = read_shared_index_for_link(git_dir, &index_path, format, &link)?
2810 && !split_index_delta_exceeds_threshold(git_dir, index, &base)
2811 {
2812 let (entries, link) = split_index_delta_entries(index, &base, &link)?;
2813 let extensions = index_extensions_without_split_index_link(
2814 &index_extensions_without_cache_tree(&index.extensions),
2815 );
2816 let mut primary = Index {
2817 version: index.version,
2818 entries,
2819 extensions,
2820 checksum: None,
2821 };
2822 primary.set_split_index_link(Some(&link))?;
2823 fs::write(&index_path, primary.write(format)?)?;
2824 apply_index_shared_file_mode(git_dir, &index_path, None)?;
2825 return Ok(());
2826 }
2827
2828 let mode_source = fs::metadata(&index_path)
2829 .ok()
2830 .map(|metadata| metadata.permissions());
2831 let mut shared = index.clone();
2832 smudge_racily_clean_entries_before_write(git_dir, format, &mut shared)?;
2833 shared.clear_split_index_link()?;
2834 shared.extensions = index_extensions_without_cache_tree(&shared.extensions);
2835 let shared_bytes = shared.write(format)?;
2836 let shared_oid = index_checksum_from_bytes(format, &shared_bytes)?;
2837 let shared_path = git_dir.join(format!("sharedindex.{shared_oid}"));
2838 if !shared_path.exists() {
2839 fs::write(&shared_path, &shared_bytes)?;
2840 }
2841 apply_index_shared_file_mode(git_dir, &shared_path, mode_source.as_ref())?;
2842 clean_shared_index_files(git_dir, shared_oid)?;
2843
2844 let mut primary = Index {
2845 version: index.version,
2846 entries: Vec::new(),
2847 extensions: Vec::new(),
2848 checksum: None,
2849 };
2850 primary.set_split_index_link(Some(&SplitIndexLink::new(shared_oid)))?;
2851 fs::write(&index_path, primary.write(format)?)?;
2852 apply_index_shared_file_mode(git_dir, &index_path, mode_source.as_ref())?;
2853 Ok(())
2854}
2855
2856pub(crate) fn alternate_index_output_path(git_dir: &Path, index_path: &Path) -> bool {
2857 env::var_os("GIT_INDEX_FILE").is_some() && index_path != git_dir.join("index")
2858}
2859
2860pub(crate) fn clean_shared_index_files(git_dir: &Path, current_oid: ObjectId) -> Result<()> {
2861 let Some(expire_before) = shared_index_expire_before(git_dir) else {
2862 return Ok(());
2863 };
2864 let current_name = format!("sharedindex.{current_oid}");
2865 let mut expired = Vec::new();
2866 for entry in fs::read_dir(git_dir)? {
2867 let entry = entry?;
2868 let name = entry.file_name();
2869 let Some(name) = name.to_str() else {
2870 continue;
2871 };
2872 if !name.starts_with("sharedindex.") || name == current_name {
2873 continue;
2874 }
2875 let metadata = entry.metadata()?;
2876 let Ok(modified) = metadata.modified() else {
2877 continue;
2878 };
2879 if modified <= expire_before {
2880 expired.push((modified, entry.path()));
2881 }
2882 }
2883 expired.sort_by_key(|(modified, _)| *modified);
2884 let delete_count = expired.len().saturating_sub(1);
2885 for (_, path) in expired.into_iter().take(delete_count) {
2886 let _ = fs::remove_file(path);
2887 }
2888 Ok(())
2889}
2890
2891pub(crate) fn shared_index_expire_before(git_dir: &Path) -> Option<SystemTime> {
2892 let value = sley_config::read_repo_config(git_dir, None)
2893 .ok()
2894 .and_then(|config| {
2895 config
2896 .get("splitIndex", None, "sharedIndexExpire")
2897 .map(str::to_string)
2898 })
2899 .unwrap_or_else(|| "2.weeks.ago".to_string());
2900 let value = value.trim();
2901 if value.eq_ignore_ascii_case("never") {
2902 return None;
2903 }
2904 if value.eq_ignore_ascii_case("now") {
2905 return Some(SystemTime::now());
2906 }
2907 if let Some(days) = value
2908 .strip_suffix(".days.ago")
2909 .or_else(|| value.strip_suffix(".day.ago"))
2910 .and_then(|days| days.parse::<u64>().ok())
2911 {
2912 return SystemTime::now().checked_sub(Duration::from_secs(days * 24 * 60 * 60));
2913 }
2914 if let Some(weeks) = value
2915 .strip_suffix(".weeks.ago")
2916 .or_else(|| value.strip_suffix(".week.ago"))
2917 .and_then(|weeks| weeks.parse::<u64>().ok())
2918 {
2919 return SystemTime::now().checked_sub(Duration::from_secs(weeks * 7 * 24 * 60 * 60));
2920 }
2921 SystemTime::now().checked_sub(Duration::from_secs(14 * 24 * 60 * 60))
2922}
2923
2924pub(crate) fn apply_index_shared_file_mode(
2925 git_dir: &Path,
2926 path: &Path,
2927 mode_source: Option<&fs::Permissions>,
2928) -> Result<()> {
2929 #[cfg(unix)]
2930 {
2931 use std::os::unix::fs::PermissionsExt;
2932
2933 let current = fs::metadata(path)?.permissions();
2934 let source_mode = mode_source
2935 .map(fs::Permissions::mode)
2936 .unwrap_or_else(|| current.mode());
2937 let mode = sley_config::read_repo_config(git_dir, None)
2938 .ok()
2939 .and_then(|config| {
2940 config
2941 .get("core", None, "sharedRepository")
2942 .and_then(|value| shared_repository_file_mode(value, source_mode))
2943 })
2944 .unwrap_or(source_mode & 0o7777);
2945 fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
2946 }
2947 #[cfg(not(unix))]
2948 {
2949 let _ = git_dir;
2950 let _ = path;
2951 let _ = mode_source;
2952 }
2953 Ok(())
2954}
2955
2956#[cfg(unix)]
2957pub(crate) fn shared_repository_file_mode(value: &str, source_mode: u32) -> Option<u32> {
2958 match value {
2959 "umask" | "false" | "no" | "off" | "0" => None,
2960 "group" | "true" | "yes" | "on" | "1" => Some((source_mode | 0o660) & 0o7777),
2961 "all" | "world" | "everybody" | "2" | "3" => Some((source_mode | 0o664) & 0o7777),
2962 value => {
2963 let parsed = u32::from_str_radix(value, 8).ok()?;
2964 (parsed & 0o600 == 0o600).then_some(parsed & 0o666)
2965 }
2966 }
2967}
2968
2969pub(crate) fn read_shared_index_for_link(
2970 git_dir: &Path,
2971 index_path: &Path,
2972 format: ObjectFormat,
2973 link: &SplitIndexLink,
2974) -> Result<Option<Index>> {
2975 let name = format!("sharedindex.{}", link.base_oid);
2976 let bytes = match fs::read(git_dir.join(&name)) {
2977 Ok(bytes) => bytes,
2978 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2979 let alternate = index_path
2980 .parent()
2981 .unwrap_or_else(|| Path::new("."))
2982 .join(&name);
2983 match fs::read(alternate) {
2984 Ok(bytes) => bytes,
2985 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2986 Err(err) => return Err(err.into()),
2987 }
2988 }
2989 Err(err) => return Err(err.into()),
2990 };
2991 let base = Index::parse(&bytes, format)?;
2992 if base.checksum != Some(link.base_oid) {
2993 return Ok(None);
2994 }
2995 Ok(Some(base))
2996}
2997
2998pub(crate) fn split_index_delta_exceeds_threshold(
2999 git_dir: &Path,
3000 index: &Index,
3001 base: &Index,
3002) -> bool {
3003 let max_percent = sley_config::read_repo_config(git_dir, None)
3004 .ok()
3005 .and_then(|config| {
3006 config
3007 .get("splitIndex", None, "maxPercentChange")
3008 .and_then(|value| value.parse::<i64>().ok())
3009 })
3010 .unwrap_or(20);
3011 match max_percent {
3012 0 => return true,
3013 100.. => return false,
3014 value if value < 0 => {}
3015 _ => {}
3016 }
3017 let not_shared = count_entries_not_shared_with_base(index, base);
3018 (index.entries.len() as i64) * max_percent < (not_shared as i64) * 100
3019}
3020
3021pub(crate) fn count_entries_not_shared_with_base(index: &Index, base: &Index) -> usize {
3022 index
3023 .entries
3024 .iter()
3025 .filter(|entry| {
3026 base.entries
3027 .binary_search_by(|base_entry| compare_index_key(base_entry, entry))
3028 .is_err()
3029 })
3030 .count()
3031}
3032
3033pub(crate) fn split_index_delta_entries(
3034 index: &Index,
3035 base: &Index,
3036 previous_link: &SplitIndexLink,
3037) -> Result<(Vec<IndexEntry>, SplitIndexLink)> {
3038 let mut delete_positions = Vec::new();
3039 let mut replace_positions = Vec::new();
3040 let mut replacements = Vec::new();
3041 let mut additions = Vec::new();
3042 let mut base_pos = 0usize;
3043 let mut index_pos = 0usize;
3044 while base_pos < base.entries.len() && index_pos < index.entries.len() {
3045 match compare_index_key(&base.entries[base_pos], &index.entries[index_pos]) {
3046 Ordering::Equal => {
3047 if previous_link
3048 .delete_positions
3049 .binary_search(&(base_pos as u32))
3050 .is_ok()
3051 {
3052 delete_positions.push(base_pos as u32);
3053 additions.push(index.entries[index_pos].clone());
3054 } else if !index_entry_content_eq(
3055 &base.entries[base_pos],
3056 &index.entries[index_pos],
3057 ) {
3058 replace_positions.push(base_pos as u32);
3059 let mut replacement = index.entries[index_pos].clone();
3060 replacement.path = BString::from(Vec::<u8>::new());
3061 replacement.refresh_name_length();
3062 replacements.push(replacement);
3063 }
3064 base_pos += 1;
3065 index_pos += 1;
3066 }
3067 Ordering::Less => {
3068 delete_positions.push(base_pos as u32);
3069 base_pos += 1;
3070 }
3071 Ordering::Greater => {
3072 additions.push(index.entries[index_pos].clone());
3073 index_pos += 1;
3074 }
3075 }
3076 }
3077 while base_pos < base.entries.len() {
3078 delete_positions.push(base_pos as u32);
3079 base_pos += 1;
3080 }
3081 while index_pos < index.entries.len() {
3082 additions.push(index.entries[index_pos].clone());
3083 index_pos += 1;
3084 }
3085 replacements.extend(additions);
3086 Ok((
3087 replacements,
3088 SplitIndexLink {
3089 base_oid: previous_link.base_oid,
3090 delete_positions,
3091 replace_positions,
3092 },
3093 ))
3094}
3095
3096pub(crate) fn compare_index_key(left: &IndexEntry, right: &IndexEntry) -> Ordering {
3097 left.path
3098 .as_bytes()
3099 .cmp(right.path.as_bytes())
3100 .then_with(|| left.stage().as_u16().cmp(&right.stage().as_u16()))
3101}
3102
3103pub(crate) fn index_entry_content_eq(left: &IndexEntry, right: &IndexEntry) -> bool {
3104 const ONDISK_FLAGS: u16 = sley_index::INDEX_FLAG_STAGE_MASK
3105 | sley_index::INDEX_FLAG_VALID
3106 | sley_index::INDEX_FLAG_EXTENDED;
3107 left.ctime_seconds == right.ctime_seconds
3108 && left.ctime_nanoseconds == right.ctime_nanoseconds
3109 && left.mtime_seconds == right.mtime_seconds
3110 && left.mtime_nanoseconds == right.mtime_nanoseconds
3111 && left.dev == right.dev
3112 && left.ino == right.ino
3113 && left.mode == right.mode
3114 && left.uid == right.uid
3115 && left.gid == right.gid
3116 && left.size == right.size
3117 && left.oid == right.oid
3118 && (left.flags & ONDISK_FLAGS) == (right.flags & ONDISK_FLAGS)
3119 && left.flags_extended == right.flags_extended
3120}
3121
3122pub(crate) fn write_index_with_entry_size_overrides(
3123 format: ObjectFormat,
3124 index: &Index,
3125 zero_size_entries: &[usize],
3126 extensions: &[u8],
3127) -> Result<Vec<u8>> {
3128 if !(2..=4).contains(&index.version) {
3129 return Err(GitError::Unsupported(
3130 "canonical writer currently emits index v2/v3/v4".into(),
3131 ));
3132 }
3133 let mut out = Vec::new();
3134 out.extend_from_slice(b"DIRC");
3135 out.extend_from_slice(&index.version.to_be_bytes());
3136 out.extend_from_slice(&(index.entries.len() as u32).to_be_bytes());
3137 let mut previous_path = Vec::new();
3138 for (position, entry) in index.entries.iter().enumerate() {
3139 let start = out.len();
3140 out.extend_from_slice(&entry.ctime_seconds.to_be_bytes());
3141 out.extend_from_slice(&entry.ctime_nanoseconds.to_be_bytes());
3142 out.extend_from_slice(&entry.mtime_seconds.to_be_bytes());
3143 out.extend_from_slice(&entry.mtime_nanoseconds.to_be_bytes());
3144 out.extend_from_slice(&entry.dev.to_be_bytes());
3145 out.extend_from_slice(&entry.ino.to_be_bytes());
3146 out.extend_from_slice(&entry.mode.to_be_bytes());
3147 out.extend_from_slice(&entry.uid.to_be_bytes());
3148 out.extend_from_slice(&entry.gid.to_be_bytes());
3149 let size = if zero_size_entries.binary_search(&position).is_ok() {
3150 0
3151 } else {
3152 entry.size
3153 };
3154 out.extend_from_slice(&size.to_be_bytes());
3155 if entry.oid.format() != format {
3156 return Err(GitError::Unsupported(format!(
3157 "index writer expects {} ids",
3158 format.name()
3159 )));
3160 }
3161 out.extend_from_slice(entry.oid.as_bytes());
3162 let has_extended_flags =
3163 entry.flags & INDEX_FLAG_EXTENDED != 0 || entry.flags_extended != 0;
3164 if has_extended_flags && index.version < 3 {
3165 return Err(GitError::Unsupported(
3166 "index extended flags require version 3".into(),
3167 ));
3168 }
3169 let flags = if has_extended_flags {
3170 entry.flags | INDEX_FLAG_EXTENDED
3171 } else {
3172 entry.flags & !INDEX_FLAG_EXTENDED
3173 };
3174 out.extend_from_slice(&flags.to_be_bytes());
3175 if has_extended_flags {
3176 out.extend_from_slice(&entry.flags_extended.to_be_bytes());
3177 }
3178 if index.version == 4 {
3179 let common_prefix_len = common_prefix_len(&previous_path, entry.path.as_bytes());
3180 let strip_len = previous_path.len() - common_prefix_len;
3181 encode_index_v4_path_strip_len(strip_len, &mut out);
3182 out.extend_from_slice(&entry.path.as_bytes()[common_prefix_len..]);
3183 out.push(0);
3184 previous_path = entry.path.as_bytes().to_vec();
3185 } else {
3186 out.extend_from_slice(entry.path.as_bytes());
3187 out.push(0);
3188 while (out.len() - start) % 8 != 0 {
3189 out.push(0);
3190 }
3191 }
3192 }
3193 out.extend_from_slice(extensions);
3194 let checksum = sley_core::digest_bytes(format, &out)?;
3195 out.extend_from_slice(checksum.as_bytes());
3196 Ok(out)
3197}
3198
3199pub(crate) fn encode_index_v4_path_strip_len(strip_len: usize, out: &mut Vec<u8>) {
3200 let mut bytes = Vec::new();
3201 bytes.push((strip_len & 0x7f) as u8);
3202 let mut value = strip_len >> 7;
3203 while value != 0 {
3204 value -= 1;
3205 bytes.push(((value & 0x7f) as u8) | 0x80);
3206 value >>= 7;
3207 }
3208 for byte in bytes.iter().rev() {
3209 out.push(*byte);
3210 }
3211}
3212
3213pub(crate) fn common_prefix_len(left: &[u8], right: &[u8]) -> usize {
3214 left.iter()
3215 .zip(right.iter())
3216 .take_while(|(left, right)| left == right)
3217 .count()
3218}
3219
3220pub(crate) fn index_checksum_from_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<ObjectId> {
3221 let hash_len = format.raw_len();
3222 if bytes.len() < hash_len {
3223 return Err(GitError::InvalidFormat(
3224 "index too short for checksum".into(),
3225 ));
3226 }
3227 ObjectId::from_raw(format, &bytes[bytes.len() - hash_len..])
3228}
3229
3230pub fn enable_split_index(
3231 git_dir: impl AsRef<Path>,
3232 format: ObjectFormat,
3233) -> Result<UpdateIndexResult> {
3234 let git_dir = git_dir.as_ref();
3235 let mut index = read_repository_index(git_dir, format)?.unwrap_or_else(empty_index);
3236 normalize_index_version_for_extended_flags(&mut index);
3237 write_repository_index_ref_with_split(git_dir, format, &index, true)?;
3238 Ok(UpdateIndexResult {
3239 entries: index.entries.len(),
3240 updated: Vec::new(),
3241 })
3242}
3243
3244pub fn disable_split_index(
3245 git_dir: impl AsRef<Path>,
3246 format: ObjectFormat,
3247) -> Result<UpdateIndexResult> {
3248 let git_dir = git_dir.as_ref();
3249 if !repository_index_path(git_dir).exists() {
3250 return Ok(UpdateIndexResult {
3251 entries: 0,
3252 updated: Vec::new(),
3253 });
3254 }
3255 let mut index = read_repository_index(git_dir, format)?.unwrap_or_else(empty_index);
3256 normalize_index_version_for_extended_flags(&mut index);
3257 write_repository_index_ref_with_split(git_dir, format, &index, false)?;
3258 Ok(UpdateIndexResult {
3259 entries: index.entries.len(),
3260 updated: Vec::new(),
3261 })
3262}
3263
3264pub(crate) fn smudge_racily_clean_entries_before_write(
3265 git_dir: &Path,
3266 format: ObjectFormat,
3267 index: &mut Index,
3268) -> Result<()> {
3269 for position in racily_clean_entry_indexes_before_write(git_dir, format, index)? {
3270 index.entries[position].size = 0;
3271 }
3272 Ok(())
3273}
3274
3275pub(crate) fn racily_clean_entry_indexes_before_write(
3276 git_dir: &Path,
3277 format: ObjectFormat,
3278 index: &Index,
3279) -> Result<Vec<usize>> {
3280 let index_path = repository_index_path(git_dir);
3281 let Some(index_mtime) = fs::metadata(&index_path)
3282 .ok()
3283 .and_then(|metadata| sley_index::file_mtime_parts(&metadata))
3284 else {
3285 return Ok(Vec::new());
3286 };
3287 if index_mtime == (0, 0) {
3288 return Ok(Vec::new());
3289 }
3290 let Some(worktree_root) = (match worktree_root_for_git_dir(git_dir) {
3291 Ok(worktree_root) => worktree_root,
3292 Err(_) => return Ok(Vec::new()),
3293 }) else {
3294 return Ok(Vec::new());
3295 };
3296 let mut smudged = Vec::new();
3297 for (position, entry) in index.entries.iter().enumerate() {
3298 if index_entry_stage(entry) != 0 || sley_index::is_gitlink(entry.mode) {
3299 continue;
3300 }
3301 let entry_mtime = (
3302 u64::from(entry.mtime_seconds),
3303 u64::from(entry.mtime_nanoseconds),
3304 );
3305 if entry_mtime == (0, 0) || index_mtime > entry_mtime {
3306 continue;
3307 }
3308 let absolute = worktree_root.join(repo_path_to_os_path(entry.path.as_bytes())?);
3309 let Ok(metadata) = fs::symlink_metadata(&absolute) else {
3310 continue;
3311 };
3312 if entry.mode != worktree_entry_mode(&metadata)
3313 || !worktree_entry_is_uptodate(entry, &metadata)
3314 {
3315 continue;
3316 }
3317 let body = if metadata.file_type().is_symlink() {
3318 symlink_target_bytes(&absolute)?
3319 } else if metadata.is_file() {
3320 fs::read(&absolute)?
3321 } else {
3322 continue;
3323 };
3324 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
3325 if oid != entry.oid {
3326 smudged.push(position);
3327 }
3328 }
3329 Ok(smudged)
3330}
3331
3332pub(crate) fn invalidate_untracked_cache_for_git_paths(
3333 index: &mut Index,
3334 format: ObjectFormat,
3335 paths: &[Vec<u8>],
3336) -> Result<()> {
3337 if paths.is_empty() {
3338 return Ok(());
3339 }
3340 let Some(mut cache) = index.untracked_cache(format)? else {
3341 return Ok(());
3342 };
3343 let Some(root) = cache.root.as_mut() else {
3344 return Ok(());
3345 };
3346 for path in paths {
3347 invalidate_untracked_cache_dir_for_path(root, path);
3348 }
3349 index.set_untracked_cache(format, Some(&cache))
3350}
3351
3352pub(crate) fn invalidate_untracked_cache_dir_for_path(root: &mut UntrackedCacheDir, path: &[u8]) {
3353 invalidate_untracked_cache_node(root);
3354 let mut current = root;
3355 let mut components = path.split(|byte| *byte == b'/').peekable();
3356 while let Some(component) = components.next() {
3357 if component.is_empty() || components.peek().is_none() {
3358 break;
3359 }
3360 let Some(child) = current.dirs.iter_mut().find(|dir| dir.name == component) else {
3361 break;
3362 };
3363 invalidate_untracked_cache_node(child);
3364 current = child;
3365 }
3366}
3367
3368pub(crate) fn invalidate_untracked_cache_node(node: &mut UntrackedCacheDir) {
3369 node.valid = false;
3370 node.untracked.clear();
3371}
3372
3373pub fn update_index_cacheinfo(
3374 git_dir: impl AsRef<Path>,
3375 format: ObjectFormat,
3376 entries: &[CacheInfoEntry],
3377 add: bool,
3378 verbose: bool,
3379) -> Result<UpdateIndexResult> {
3380 let git_dir = git_dir.as_ref();
3381 let index_path = repository_index_path(git_dir);
3382 let mut index = if index_path.exists() {
3383 Index::parse(&fs::read(&index_path)?, format)?
3384 } else {
3385 Index {
3386 version: 2,
3387 entries: Vec::new(),
3388 extensions: Vec::new(),
3389 checksum: None,
3390 }
3391 };
3392 let mut updated = Vec::new();
3393 let mut reports: Vec<String> = Vec::new();
3394 let mut untracked_cache_invalidation_paths = Vec::new();
3395 for cacheinfo in entries {
3396 if !add
3397 && !index
3398 .entries
3399 .iter()
3400 .any(|existing| existing.path == cacheinfo.path)
3401 {
3402 let path = String::from_utf8_lossy(&cacheinfo.path);
3403 eprintln!("error: {path}: cannot add to the index - missing --add option?");
3404 eprintln!("fatal: git update-index: --cacheinfo cannot add {path}");
3405 return Err(GitError::Exit(128));
3406 }
3407 let flags = index_flags(cacheinfo.path.len(), cacheinfo.stage);
3408 let entry = IndexEntry {
3409 ctime_seconds: 0,
3410 ctime_nanoseconds: 0,
3411 mtime_seconds: 0,
3412 mtime_nanoseconds: 0,
3413 dev: 0,
3414 ino: 0,
3415 mode: cacheinfo.mode,
3416 uid: 0,
3417 gid: 0,
3418 size: 0,
3419 oid: cacheinfo.oid,
3420 flags,
3421 flags_extended: 0,
3422 path: BString::from(cacheinfo.path.as_slice()),
3423 };
3424 index.entries.retain(|existing| {
3425 existing.path != cacheinfo.path || index_entry_stage(existing) != cacheinfo.stage
3426 });
3427 index.entries.push(entry);
3428 untracked_cache_invalidation_paths.push(cacheinfo.path.clone());
3429 updated.push(cacheinfo.oid);
3430 reports.push(format!(
3433 "add '{}'",
3434 String::from_utf8_lossy(&cacheinfo.path)
3435 ));
3436 }
3437 index
3438 .entries
3439 .sort_by(|left, right| left.path.cmp(&right.path));
3440 let null_entry = index.entries.iter().find(|entry| entry.oid.is_null());
3445 if let Some(entry) = null_entry {
3446 if verbose {
3447 flush_update_index_reports(&reports)?;
3448 }
3449 eprintln!(
3450 "error: cache entry has null sha1: {}",
3451 String::from_utf8_lossy(&entry.path)
3452 );
3453 return Err(GitError::Exit(128));
3454 }
3455 if !untracked_cache_invalidation_paths.is_empty() {
3456 index.extensions = index_extensions_without_cache_tree(&index.extensions);
3457 }
3458 invalidate_untracked_cache_for_git_paths(
3459 &mut index,
3460 format,
3461 &untracked_cache_invalidation_paths,
3462 )?;
3463 write_repository_index_ref(git_dir, format, &index)?;
3464 if verbose {
3465 flush_update_index_reports(&reports)?;
3466 }
3467 Ok(UpdateIndexResult {
3468 entries: index.entries.len(),
3469 updated,
3470 })
3471}
3472
3473pub(crate) fn flush_update_index_reports(reports: &[String]) -> Result<()> {
3474 let mut stdout = std::io::stdout().lock();
3475 for line in reports {
3476 writeln!(stdout, "{line}")?;
3477 }
3478 stdout.flush()?;
3479 Ok(())
3480}
3481
3482pub fn update_index_index_info(
3483 git_dir: impl AsRef<Path>,
3484 format: ObjectFormat,
3485 records: &[IndexInfoRecord],
3486) -> Result<UpdateIndexResult> {
3487 let git_dir = git_dir.as_ref();
3488 let index_path = repository_index_path(git_dir);
3489 let mut index = if index_path.exists() {
3490 Index::parse(&fs::read(&index_path)?, format)?
3491 } else {
3492 Index {
3493 version: 2,
3494 entries: Vec::new(),
3495 extensions: Vec::new(),
3496 checksum: None,
3497 }
3498 };
3499 let mut updated = Vec::new();
3500 let mut untracked_cache_invalidation_paths = Vec::new();
3501 for record in records {
3502 match record {
3503 IndexInfoRecord::Remove { path } => {
3504 index.entries.retain(|existing| existing.path != *path);
3505 untracked_cache_invalidation_paths.push(path.clone());
3506 }
3507 IndexInfoRecord::Add(cacheinfo) => {
3508 let flags = index_flags(cacheinfo.path.len(), cacheinfo.stage);
3509 let entry = IndexEntry {
3510 ctime_seconds: 0,
3511 ctime_nanoseconds: 0,
3512 mtime_seconds: 0,
3513 mtime_nanoseconds: 0,
3514 dev: 0,
3515 ino: 0,
3516 mode: cacheinfo.mode,
3517 uid: 0,
3518 gid: 0,
3519 size: 0,
3520 oid: cacheinfo.oid,
3521 flags,
3522 flags_extended: 0,
3523 path: BString::from(cacheinfo.path.as_slice()),
3524 };
3525 if cacheinfo.stage == 0 {
3526 index
3527 .entries
3528 .retain(|existing| existing.path != cacheinfo.path);
3529 } else {
3530 index.entries.retain(|existing| {
3531 existing.path != cacheinfo.path
3532 || index_entry_stage(existing) != cacheinfo.stage
3533 });
3534 }
3535 index.entries.push(entry);
3536 untracked_cache_invalidation_paths.push(cacheinfo.path.clone());
3537 updated.push(cacheinfo.oid);
3538 }
3539 }
3540 }
3541 index.entries.sort_by(|left, right| {
3542 left.path
3543 .cmp(&right.path)
3544 .then_with(|| index_entry_stage(left).cmp(&index_entry_stage(right)))
3545 });
3546 if !untracked_cache_invalidation_paths.is_empty() {
3547 index.extensions = index_extensions_without_cache_tree(&index.extensions);
3548 }
3549 invalidate_untracked_cache_for_git_paths(
3550 &mut index,
3551 format,
3552 &untracked_cache_invalidation_paths,
3553 )?;
3554 write_repository_index_ref(git_dir, format, &index)?;
3555 Ok(UpdateIndexResult {
3556 entries: index.entries.len(),
3557 updated,
3558 })
3559}
3560
3561pub(crate) fn index_flags(path_len: usize, stage: u16) -> u16 {
3562 ((stage & 0x3) << 12) | ((path_len.min(0xfff) as u16) & 0x0fff)
3563}
3564
3565pub(crate) const INDEX_FLAG_ASSUME_UNCHANGED: u16 = 0x8000;
3566pub(crate) const INDEX_FLAG_EXTENDED: u16 = 0x4000;
3567pub(crate) const INDEX_EXTENDED_FLAG_SKIP_WORKTREE: u16 = 0x4000;
3568
3569pub(crate) fn normalize_index_version_for_extended_flags(index: &mut Index) {
3570 let has_extended_flags = index
3571 .entries
3572 .iter()
3573 .any(|entry| entry.flags & INDEX_FLAG_EXTENDED != 0 || entry.flags_extended != 0);
3574 if has_extended_flags && index.version < 3 {
3575 index.version = 3;
3576 } else if !has_extended_flags && index.version == 3 {
3577 index.version = 2;
3578 }
3579}
3580
3581pub(crate) fn index_entry_stage(entry: &IndexEntry) -> u16 {
3582 (entry.flags >> 12) & 0x3
3583}
3584
3585pub(crate) fn stage0_oid_in_range(
3588 entries: &[IndexEntry],
3589 range: std::ops::Range<usize>,
3590) -> Option<ObjectId> {
3591 entries[range]
3592 .iter()
3593 .find(|entry| index_entry_stage(entry) == 0)
3594 .map(|entry| entry.oid)
3595}
3596
3597pub(crate) fn index_entry_skip_worktree(entry: &IndexEntry) -> bool {
3598 entry.flags & INDEX_FLAG_EXTENDED != 0
3599 && entry.flags_extended & INDEX_EXTENDED_FLAG_SKIP_WORKTREE != 0
3600}
3601
3602pub(crate) fn print_update_index_path_error(path: &[u8], message: &str) {
3603 let path = String::from_utf8_lossy(path);
3604 eprintln!("error: {path}: {message}");
3605 eprintln!("fatal: Unable to process path {path}");
3606}
3607
3608pub(crate) fn print_update_index_needs_update(path: &[u8]) {
3609 let path = String::from_utf8_lossy(path);
3610 println!("{path}: needs update");
3611}
3612
3613pub fn write_tree_from_index(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<ObjectId> {
3614 write_tree_from_index_with_options(git_dir, format, WriteTreeOptions::default())
3615}
3616
3617pub fn write_tree_from_index_with_odb(
3618 git_dir: impl AsRef<Path>,
3619 format: ObjectFormat,
3620 odb: &FileObjectDatabase,
3621) -> Result<ObjectId> {
3622 write_tree_from_index_with_options_and_odb(
3623 git_dir.as_ref(),
3624 format,
3625 WriteTreeOptions::default(),
3626 odb,
3627 )
3628}
3629
3630pub fn write_tree_from_index_with_options(
3631 git_dir: impl AsRef<Path>,
3632 format: ObjectFormat,
3633 options: WriteTreeOptions,
3634) -> Result<ObjectId> {
3635 let git_dir = git_dir.as_ref();
3636 let odb = FileObjectDatabase::from_git_dir(git_dir, format);
3637 write_tree_from_index_with_options_and_odb(git_dir, format, options, &odb)
3638}
3639
3640pub(crate) fn write_tree_from_index_with_options_and_odb(
3641 git_dir: &Path,
3642 format: ObjectFormat,
3643 options: WriteTreeOptions,
3644 odb: &FileObjectDatabase,
3645) -> Result<ObjectId> {
3646 let index_path = repository_index_path(git_dir);
3647 let index_bytes = match fs::read(&index_path) {
3651 Ok(bytes) => bytes,
3652 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3653 let mut checker = odb.presence_checker();
3654 let empty: &[WriteTreeEntry<'_>] = &[];
3655 return write_tree_entries_stream(
3656 empty,
3657 b"",
3658 None,
3659 odb,
3660 &mut checker,
3661 options.missing_ok,
3662 );
3663 }
3664 Err(err) => return Err(err.into()),
3665 };
3666 let mut checker = odb.presence_checker();
3667 let oid = if Index::bytes_have_extension(&index_bytes, format, b"link")? {
3668 let index = sley_index::read_repository_index(git_dir, format)?;
3669 write_tree_from_owned_index(&index, format, &options, odb, &mut checker)?
3670 } else {
3671 match BorrowedIndex::parse(&index_bytes, format) {
3672 Ok(index) => {
3673 write_tree_from_borrowed_index(&index, format, &options, odb, &mut checker)
3674 }
3675 Err(GitError::Unsupported(_)) => {
3676 let index = Index::parse(&index_bytes, format)?;
3677 write_tree_from_owned_index(&index, format, &options, odb, &mut checker)
3678 }
3679 Err(err) => Err(err),
3680 }?
3681 };
3682 if options.prefix.is_none() {
3683 refresh_repository_cache_tree(git_dir, format, odb)?;
3684 }
3685 Ok(oid)
3686}
3687
3688pub(crate) fn write_tree_from_borrowed_index(
3689 index: &BorrowedIndex<'_>,
3690 format: ObjectFormat,
3691 options: &WriteTreeOptions,
3692 odb: &FileObjectDatabase,
3693 checker: &mut ObjectPresenceChecker,
3694) -> Result<ObjectId> {
3695 let cache_tree = if options.prefix.is_none() {
3696 index.cache_tree(format).ok().flatten()
3697 } else {
3698 None
3699 };
3700 if options.prefix.is_none() && !index.entries.iter().any(|entry| entry.is_intent_to_add()) {
3701 return write_tree_entries_stream(
3702 &index.entries,
3703 b"",
3704 cache_tree.as_ref(),
3705 odb,
3706 checker,
3707 options.missing_ok,
3708 );
3709 }
3710 let entries = write_tree_entries_for_prefix(
3715 index
3716 .entries
3717 .iter()
3718 .filter(|entry| !entry.is_intent_to_add()),
3719 options.prefix.as_deref(),
3720 )?;
3721 write_tree_entries_stream(
3722 &entries,
3723 b"",
3724 cache_tree.as_ref(),
3725 odb,
3726 checker,
3727 options.missing_ok,
3728 )
3729}
3730
3731pub(crate) fn write_tree_from_owned_index(
3732 index: &Index,
3733 format: ObjectFormat,
3734 options: &WriteTreeOptions,
3735 odb: &FileObjectDatabase,
3736 checker: &mut ObjectPresenceChecker,
3737) -> Result<ObjectId> {
3738 let cache_tree = if options.prefix.is_none() {
3739 index.cache_tree(format).ok().flatten()
3740 } else {
3741 None
3742 };
3743 if options.prefix.is_none() && !index.entries.iter().any(|entry| entry.is_intent_to_add()) {
3744 return write_tree_entries_stream(
3745 &index.entries,
3746 b"",
3747 cache_tree.as_ref(),
3748 odb,
3749 checker,
3750 options.missing_ok,
3751 );
3752 }
3753 let entries = write_tree_entries_for_prefix(
3754 index
3755 .entries
3756 .iter()
3757 .filter(|entry| !entry.is_intent_to_add()),
3758 options.prefix.as_deref(),
3759 )?;
3760 write_tree_entries_stream(
3761 &entries,
3762 b"",
3763 cache_tree.as_ref(),
3764 odb,
3765 checker,
3766 options.missing_ok,
3767 )
3768}
3769
3770#[derive(Clone, Copy)]
3771pub(crate) struct WriteTreeEntry<'a> {
3772 pub(crate) path: &'a [u8],
3773 pub(crate) mode: u32,
3774 pub(crate) oid: ObjectId,
3775}
3776
3777pub(crate) trait WriteTreeIndexEntry {
3778 fn write_tree_path(&self) -> &[u8];
3779 fn write_tree_mode(&self) -> u32;
3780 fn write_tree_oid(&self) -> ObjectId;
3781}
3782
3783impl WriteTreeIndexEntry for IndexEntry {
3784 fn write_tree_path(&self) -> &[u8] {
3785 self.path.as_bytes()
3786 }
3787
3788 fn write_tree_mode(&self) -> u32 {
3789 self.mode
3790 }
3791
3792 fn write_tree_oid(&self) -> ObjectId {
3793 self.oid
3794 }
3795}
3796
3797impl WriteTreeIndexEntry for IndexEntryRef<'_> {
3798 fn write_tree_path(&self) -> &[u8] {
3799 self.path
3800 }
3801
3802 fn write_tree_mode(&self) -> u32 {
3803 self.mode
3804 }
3805
3806 fn write_tree_oid(&self) -> ObjectId {
3807 self.oid
3808 }
3809}
3810
3811impl WriteTreeIndexEntry for WriteTreeEntry<'_> {
3812 fn write_tree_path(&self) -> &[u8] {
3813 self.path
3814 }
3815
3816 fn write_tree_mode(&self) -> u32 {
3817 self.mode
3818 }
3819
3820 fn write_tree_oid(&self) -> ObjectId {
3821 self.oid
3822 }
3823}
3824
3825pub(crate) fn write_tree_entries_for_prefix<'a, E>(
3826 entries: impl IntoIterator<Item = &'a E>,
3827 prefix: Option<&[u8]>,
3828) -> Result<Vec<WriteTreeEntry<'a>>>
3829where
3830 E: WriteTreeIndexEntry + 'a,
3831{
3832 let Some(prefix) = prefix else {
3833 return Ok(entries
3834 .into_iter()
3835 .map(|entry| WriteTreeEntry {
3836 path: entry.write_tree_path(),
3837 mode: entry.write_tree_mode(),
3838 oid: entry.write_tree_oid(),
3839 })
3840 .collect());
3841 };
3842 let trimmed_len = prefix
3843 .iter()
3844 .rposition(|byte| *byte != b'/')
3845 .map(|idx| idx + 1)
3846 .unwrap_or(0);
3847 let trimmed = &prefix[..trimmed_len];
3848 if trimmed.is_empty() {
3849 return Ok(entries
3850 .into_iter()
3851 .map(|entry| WriteTreeEntry {
3852 path: entry.write_tree_path(),
3853 mode: entry.write_tree_mode(),
3854 oid: entry.write_tree_oid(),
3855 })
3856 .collect());
3857 }
3858 let mut prefixed = Vec::new();
3859 for entry in entries {
3860 let Some(remainder) = entry.write_tree_path().strip_prefix(trimmed) else {
3861 continue;
3862 };
3863 let Some(stripped) = remainder.strip_prefix(b"/") else {
3864 continue;
3865 };
3866 if stripped.is_empty() {
3867 continue;
3868 }
3869 prefixed.push(WriteTreeEntry {
3870 path: stripped,
3871 mode: entry.write_tree_mode(),
3872 oid: entry.write_tree_oid(),
3873 });
3874 }
3875 if prefixed.is_empty() {
3876 eprintln!(
3877 "fatal: git-write-tree: prefix {} not found",
3878 String::from_utf8_lossy(prefix)
3879 );
3880 return Err(GitError::Exit(128));
3881 }
3882 Ok(prefixed)
3883}
3884
3885pub(crate) fn write_tree_entries_stream<E>(
3886 entries: &[E],
3887 prefix: &[u8],
3888 cache_tree: Option<&CacheTree>,
3889 odb: &FileObjectDatabase,
3890 checker: &mut ObjectPresenceChecker,
3891 missing_ok: bool,
3892) -> Result<ObjectId>
3893where
3894 E: WriteTreeIndexEntry,
3895{
3896 if let Some(oid) = valid_cache_tree_oid(cache_tree, entries.len()) {
3897 return Ok(oid);
3898 }
3899
3900 let mut tree_entries = Vec::new();
3901 let mut index = 0usize;
3902 while index < entries.len() {
3903 let entry = &entries[index];
3904 let path = entry.write_tree_path();
3905 let Some(remainder) = path.strip_prefix(prefix) else {
3906 return Err(GitError::InvalidPath(format!(
3907 "invalid index path {}",
3908 String::from_utf8_lossy(path)
3909 )));
3910 };
3911 if remainder.is_empty() || remainder[0] == b'/' {
3912 return Err(GitError::InvalidPath(format!(
3913 "invalid index path {}",
3914 String::from_utf8_lossy(path)
3915 )));
3916 }
3917
3918 if entry.write_tree_mode() == SPARSE_DIR_MODE
3919 && let Some(name) = remainder.strip_suffix(b"/")
3920 && !name.is_empty()
3921 && !name.contains(&b'/')
3922 {
3923 let oid = entry.write_tree_oid();
3924 if !missing_ok && !checker.contains(&oid)? {
3925 eprintln!(
3926 "error: invalid object {:o} {} for '{}'",
3927 SPARSE_DIR_MODE,
3928 oid,
3929 String::from_utf8_lossy(path)
3930 );
3931 eprintln!("fatal: git-write-tree: error building trees");
3932 return Err(GitError::Exit(128));
3933 }
3934 tree_entries.push(TreeEntry {
3935 mode: SPARSE_DIR_MODE,
3936 name: BString::from(name),
3937 oid,
3938 });
3939 index += 1;
3940 continue;
3941 }
3942
3943 if let Some(slash) = remainder.iter().position(|byte| *byte == b'/') {
3944 let name = &remainder[..slash];
3945 if name.is_empty() {
3946 return Err(GitError::InvalidPath(format!(
3947 "invalid index path {}",
3948 String::from_utf8_lossy(path)
3949 )));
3950 }
3951 let start = index;
3952 let child_cache = cache_tree.and_then(|tree| {
3953 tree.subtrees
3954 .iter()
3955 .find(|child| child.name.as_slice() == name)
3956 .map(|child| &child.tree)
3957 });
3958 if let Some(cached_count) = valid_cache_tree_entry_count(child_cache) {
3959 let end = start.saturating_add(cached_count);
3960 if cached_count > 0
3961 && end <= entries.len()
3962 && same_tree_component(entries[end - 1].write_tree_path(), prefix, name)?
3963 && (end == entries.len()
3964 || !same_tree_component(entries[end].write_tree_path(), prefix, name)?)
3965 {
3966 index = end;
3967 } else {
3968 index += 1;
3969 while index < entries.len()
3970 && same_tree_component(entries[index].write_tree_path(), prefix, name)?
3971 {
3972 index += 1;
3973 }
3974 }
3975 } else {
3976 index += 1;
3977 while index < entries.len()
3978 && same_tree_component(entries[index].write_tree_path(), prefix, name)?
3979 {
3980 index += 1;
3981 }
3982 }
3983 if let Some(oid) = valid_cache_tree_oid(child_cache, index - start) {
3984 tree_entries.push(TreeEntry {
3985 mode: 0o040000,
3986 name: BString::from(name),
3987 oid,
3988 });
3989 continue;
3990 }
3991 let mut child_prefix = Vec::with_capacity(prefix.len() + name.len() + 1);
3992 child_prefix.extend_from_slice(prefix);
3993 child_prefix.extend_from_slice(name);
3994 child_prefix.push(b'/');
3995 let oid = write_tree_entries_stream(
3996 &entries[start..index],
3997 &child_prefix,
3998 child_cache,
3999 odb,
4000 checker,
4001 missing_ok,
4002 )?;
4003 tree_entries.push(TreeEntry {
4004 mode: 0o040000,
4005 name: BString::from(name),
4006 oid,
4007 });
4008 continue;
4009 }
4010
4011 let mode = entry.write_tree_mode();
4012 let oid = entry.write_tree_oid();
4013 if !missing_ok && !sley_index::is_gitlink(mode) && !checker.contains(&oid)? {
4014 eprintln!(
4015 "error: invalid object {:o} {} for '{}'",
4016 mode,
4017 oid,
4018 String::from_utf8_lossy(path)
4019 );
4020 eprintln!("fatal: git-write-tree: error building trees");
4021 return Err(GitError::Exit(128));
4022 }
4023 tree_entries.push(TreeEntry {
4024 mode,
4025 name: BString::from(remainder),
4026 oid,
4027 });
4028 index += 1;
4029 }
4030
4031 tree_entries.sort_by(|left, right| {
4032 git_tree_entry_cmp(
4033 left.name.as_bytes(),
4034 left.mode,
4035 right.name.as_bytes(),
4036 right.mode,
4037 )
4038 });
4039 odb.write_object(EncodedObject::new(
4040 ObjectType::Tree,
4041 Tree {
4042 entries: tree_entries,
4043 }
4044 .write(),
4045 ))
4046}
4047
4048pub(crate) fn index_entry_tree_oid_without_cache(
4049 index: &Index,
4050 format: ObjectFormat,
4051) -> Result<ObjectId> {
4052 let entries = write_tree_entries_for_prefix(
4053 index
4054 .entries
4055 .iter()
4056 .filter(|entry| entry.stage() == Stage::Normal && !entry.is_intent_to_add()),
4057 None,
4058 )?;
4059 write_tree_entries_oid_without_cache(&entries, b"", format)
4060}
4061
4062pub(crate) fn borrowed_index_entry_tree_oid_without_cache(
4063 index: &BorrowedIndex<'_>,
4064 format: ObjectFormat,
4065) -> Result<ObjectId> {
4066 let entries = write_tree_entries_for_prefix(
4067 index
4068 .entries
4069 .iter()
4070 .filter(|entry| entry.stage() == Stage::Normal && !entry.is_intent_to_add()),
4071 None,
4072 )?;
4073 write_tree_entries_oid_without_cache(&entries, b"", format)
4074}
4075
4076pub(crate) fn write_tree_entries_oid_without_cache<E>(
4077 entries: &[E],
4078 prefix: &[u8],
4079 format: ObjectFormat,
4080) -> Result<ObjectId>
4081where
4082 E: WriteTreeIndexEntry,
4083{
4084 let mut tree_entries = Vec::new();
4085 let mut index = 0usize;
4086 while index < entries.len() {
4087 let entry = &entries[index];
4088 let path = entry.write_tree_path();
4089 let Some(remainder) = path.strip_prefix(prefix) else {
4090 return Err(GitError::InvalidPath(format!(
4091 "invalid index path {}",
4092 String::from_utf8_lossy(path)
4093 )));
4094 };
4095 if remainder.is_empty() || remainder[0] == b'/' {
4096 return Err(GitError::InvalidPath(format!(
4097 "invalid index path {}",
4098 String::from_utf8_lossy(path)
4099 )));
4100 }
4101
4102 if entry.write_tree_mode() == SPARSE_DIR_MODE
4103 && let Some(name) = remainder.strip_suffix(b"/")
4104 && !name.is_empty()
4105 && !name.contains(&b'/')
4106 {
4107 tree_entries.push(TreeEntry {
4108 mode: SPARSE_DIR_MODE,
4109 name: BString::from(name),
4110 oid: entry.write_tree_oid(),
4111 });
4112 index += 1;
4113 continue;
4114 }
4115
4116 if let Some(slash) = remainder.iter().position(|byte| *byte == b'/') {
4117 let name = &remainder[..slash];
4118 if name.is_empty() {
4119 return Err(GitError::InvalidPath(format!(
4120 "invalid index path {}",
4121 String::from_utf8_lossy(path)
4122 )));
4123 }
4124 let start = index;
4125 index += 1;
4126 while index < entries.len()
4127 && same_tree_component(entries[index].write_tree_path(), prefix, name)?
4128 {
4129 index += 1;
4130 }
4131 let mut child_prefix = Vec::with_capacity(prefix.len() + name.len() + 1);
4132 child_prefix.extend_from_slice(prefix);
4133 child_prefix.extend_from_slice(name);
4134 child_prefix.push(b'/');
4135 let oid = write_tree_entries_oid_without_cache(
4136 &entries[start..index],
4137 &child_prefix,
4138 format,
4139 )?;
4140 tree_entries.push(TreeEntry {
4141 mode: 0o040000,
4142 name: BString::from(name),
4143 oid,
4144 });
4145 continue;
4146 }
4147
4148 tree_entries.push(TreeEntry {
4149 mode: entry.write_tree_mode(),
4150 name: BString::from(remainder),
4151 oid: entry.write_tree_oid(),
4152 });
4153 index += 1;
4154 }
4155
4156 tree_entries.sort_by(|left, right| {
4157 git_tree_entry_cmp(
4158 left.name.as_bytes(),
4159 left.mode,
4160 right.name.as_bytes(),
4161 right.mode,
4162 )
4163 });
4164 EncodedObject::new(
4165 ObjectType::Tree,
4166 Tree {
4167 entries: tree_entries,
4168 }
4169 .write(),
4170 )
4171 .object_id(format)
4172}
4173
4174pub(crate) fn valid_cache_tree_oid(
4175 tree: Option<&CacheTree>,
4176 entry_count: usize,
4177) -> Option<ObjectId> {
4178 let tree = tree?;
4179 if valid_cache_tree_entry_count(Some(tree))? != entry_count {
4180 return None;
4181 }
4182 tree.oid
4183}
4184
4185pub(crate) fn valid_cache_tree_entry_count(tree: Option<&CacheTree>) -> Option<usize> {
4186 let tree = tree?;
4187 if tree.entry_count < 0 || tree.oid.is_none() {
4188 return None;
4189 }
4190 Some(tree.entry_count as usize)
4191}
4192
4193pub(crate) fn same_tree_component(path: &[u8], prefix: &[u8], name: &[u8]) -> Result<bool> {
4194 let Some(remainder) = path.strip_prefix(prefix) else {
4195 return Err(GitError::InvalidPath(format!(
4196 "invalid index path {}",
4197 String::from_utf8_lossy(path)
4198 )));
4199 };
4200 Ok(remainder.starts_with(name) && remainder.get(name.len()) == Some(&b'/'))
4201}