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 index.extensions = index_extensions_without_cache_tree(&index.extensions);
1410 invalidate_untracked_cache_for_git_paths(
1411 &mut index,
1412 format,
1413 &untracked_cache_invalidation_paths,
1414 )?;
1415 if !pending_large.is_empty() {
1416 write_pending_large_blobs(&odb, &pending_large, large_policy)?;
1417 }
1418 let skip_hash = clean_config
1422 .map(index_skip_hash_from_config)
1423 .unwrap_or(false);
1424 write_repository_index_ref_skip_hash(git_dir, format, &index, skip_hash)?;
1425 if verbose {
1426 let mut stdout = std::io::stdout().lock();
1427 for line in &reports {
1428 writeln!(stdout, "{line}")?;
1429 }
1430 stdout.flush()?;
1431 }
1432 if chmod_error {
1433 return Err(GitError::Exit(128));
1434 }
1435 Ok(UpdateIndexResult {
1436 entries: index.entries.len(),
1437 updated,
1438 })
1439}
1440
1441pub fn refresh_index_paths(
1442 worktree_root: impl AsRef<Path>,
1443 git_dir: impl AsRef<Path>,
1444 format: ObjectFormat,
1445 paths: &[PathBuf],
1446 quiet: bool,
1447 ignore_missing: bool,
1448 really_refresh: bool,
1449) -> Result<UpdateIndexResult> {
1450 let worktree_root = worktree_root.as_ref();
1451 let git_dir = git_dir.as_ref();
1452 let index_path = repository_index_path(git_dir);
1453 if !index_path.exists() {
1454 return Ok(UpdateIndexResult {
1455 entries: 0,
1456 updated: Vec::new(),
1457 });
1458 }
1459 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
1460 let trust_filemode = trust_executable_bit_from_git_dir(git_dir, None);
1461 let index_mtime = fs::metadata(&index_path)
1469 .ok()
1470 .and_then(|metadata| file_mtime_parts(&metadata));
1471 let stat_cache = IndexStatCache::from_index_mtime_only(index_mtime);
1472 let selected_paths = paths
1473 .iter()
1474 .map(|path| {
1475 let absolute = if path.is_absolute() {
1476 path.clone()
1477 } else {
1478 worktree_root.join(path)
1479 };
1480 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1481 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1482 })?;
1483 git_path_bytes(relative)
1484 })
1485 .collect::<Result<Vec<_>>>()?;
1486 let selected_paths = selected_paths.into_iter().collect::<BTreeSet<_>>();
1487 if selected_paths.is_empty()
1488 && !really_refresh
1489 && !index
1490 .entries
1491 .iter()
1492 .any(|entry| entry.flags & INDEX_FLAG_ASSUME_UNCHANGED != 0)
1493 {
1494 return refresh_all_index_paths_parallel(
1495 worktree_root,
1496 git_dir,
1497 format,
1498 index,
1499 stat_cache,
1500 quiet,
1501 ignore_missing,
1502 trust_filemode,
1503 );
1504 }
1505 let mut needs_update = false;
1506 let mut index_dirty = false;
1507 for entry in &mut index.entries {
1508 if index_entry_stage(entry) != 0 {
1509 continue;
1510 }
1511 if entry.flags & INDEX_FLAG_ASSUME_UNCHANGED != 0 {
1512 if !really_refresh {
1513 continue;
1514 }
1515 entry.flags &= !INDEX_FLAG_ASSUME_UNCHANGED;
1516 index_dirty = true;
1517 }
1518 let absolute = worktree_root.join(repo_path_to_os_path(entry.path.as_bytes())?);
1519 let Ok(metadata) = fs::metadata(&absolute) else {
1520 if ignore_missing {
1521 continue;
1522 }
1523 if !quiet {
1524 print_update_index_needs_update(entry.path.as_bytes());
1525 }
1526 needs_update = true;
1527 continue;
1528 };
1529 if sley_index::is_gitlink(entry.mode) {
1539 match sley_index::gitlink_stat_verdict(&metadata) {
1540 sley_index::GitlinkStatVerdict::Populated => continue,
1541 sley_index::GitlinkStatVerdict::TypeChanged => {
1542 if !quiet {
1543 print_update_index_needs_update(entry.path.as_bytes());
1544 }
1545 needs_update = true;
1546 continue;
1547 }
1548 }
1549 }
1550 if !metadata.is_file() {
1551 if !quiet {
1552 print_update_index_needs_update(entry.path.as_bytes());
1553 }
1554 needs_update = true;
1555 continue;
1556 }
1557 if stat_cache.reuse_index_entry(entry, &metadata).is_some() {
1564 continue;
1565 }
1566 let body = fs::read(&absolute)?;
1567 let object = EncodedObject::new(ObjectType::Blob, body);
1568 let oid = object.object_id(format)?;
1569 if oid != entry.oid || file_mode_with_trust(&metadata, trust_filemode) != entry.mode {
1570 if !quiet {
1571 print_update_index_needs_update(entry.path.as_bytes());
1572 }
1573 needs_update = true;
1574 if really_refresh
1575 && !selected_paths.is_empty()
1576 && selected_paths.contains(entry.path.as_bytes())
1577 {
1578 let updated_entry = index_entry_from_metadata_with_filemode(
1579 entry.path.clone(),
1580 oid,
1581 &metadata,
1582 trust_filemode,
1583 );
1584 if updated_entry != *entry {
1585 *entry = updated_entry;
1586 index_dirty = true;
1587 }
1588 }
1589 continue;
1590 }
1591 let updated_entry = index_entry_from_metadata_with_filemode(
1592 entry.path.clone(),
1593 oid,
1594 &metadata,
1595 trust_filemode,
1596 );
1597 if updated_entry != *entry {
1598 *entry = updated_entry;
1599 index_dirty = true;
1600 }
1601 }
1602 if index_dirty {
1603 write_repository_index_ref(git_dir, format, &index)?;
1604 }
1605 if needs_update && !quiet {
1606 return Err(GitError::Exit(1));
1607 }
1608 Ok(UpdateIndexResult {
1609 entries: index.entries.len(),
1610 updated: Vec::new(),
1611 })
1612}
1613
1614pub(crate) fn refresh_all_index_paths_parallel(
1615 worktree_root: &Path,
1616 git_dir: &Path,
1617 format: ObjectFormat,
1618 mut index: Index,
1619 stat_cache: IndexStatCache,
1620 quiet: bool,
1621 ignore_missing: bool,
1622 trust_filemode: bool,
1623) -> Result<UpdateIndexResult> {
1624 let prechecks =
1625 tracked_only_non_clean_prechecks_parallel(worktree_root, &index, &stat_cache, false)?;
1626 let mut needs_update = false;
1627 let mut index_dirty = false;
1628 for precheck in prechecks {
1629 match precheck {
1630 TrackedOnlyPrecheck::Deleted(idx) => {
1631 if ignore_missing {
1632 continue;
1633 }
1634 if !quiet {
1635 print_update_index_needs_update(index.entries[idx].path.as_bytes());
1636 }
1637 needs_update = true;
1638 }
1639 TrackedOnlyPrecheck::Slow(idx) => {
1640 let entry = &mut index.entries[idx];
1641 let path = entry.path.as_bytes().to_vec();
1642 let absolute = worktree_root.join(repo_path_to_os_path(&path)?);
1643 let Ok(metadata) = fs::metadata(&absolute) else {
1644 if ignore_missing {
1645 continue;
1646 }
1647 if !quiet {
1648 print_update_index_needs_update(&path);
1649 }
1650 needs_update = true;
1651 continue;
1652 };
1653 if sley_index::is_gitlink(entry.mode) {
1657 match sley_index::gitlink_stat_verdict(&metadata) {
1658 sley_index::GitlinkStatVerdict::Populated => continue,
1659 sley_index::GitlinkStatVerdict::TypeChanged => {
1660 if !quiet {
1661 print_update_index_needs_update(&path);
1662 }
1663 needs_update = true;
1664 continue;
1665 }
1666 }
1667 }
1668 if !metadata.is_file() {
1669 if !quiet {
1670 print_update_index_needs_update(&path);
1671 }
1672 needs_update = true;
1673 continue;
1674 }
1675 if stat_cache.reuse_index_entry(entry, &metadata).is_some() {
1676 continue;
1677 }
1678 let body = fs::read(&absolute)?;
1679 let object = EncodedObject::new(ObjectType::Blob, body);
1680 let oid = object.object_id(format)?;
1681 if oid != entry.oid || file_mode_with_trust(&metadata, trust_filemode) != entry.mode
1682 {
1683 if !quiet {
1684 print_update_index_needs_update(&path);
1685 }
1686 needs_update = true;
1687 continue;
1688 }
1689 let updated_entry = index_entry_from_metadata_with_filemode(
1690 entry.path.clone(),
1691 oid,
1692 &metadata,
1693 trust_filemode,
1694 );
1695 if updated_entry != *entry {
1696 *entry = updated_entry;
1697 index_dirty = true;
1698 }
1699 }
1700 }
1701 }
1702 if index_dirty {
1703 write_repository_index_ref(git_dir, format, &index)?;
1704 }
1705 if needs_update && !quiet {
1706 return Err(GitError::Exit(1));
1707 }
1708 Ok(UpdateIndexResult {
1709 entries: index.entries.len(),
1710 updated: Vec::new(),
1711 })
1712}
1713
1714pub fn update_index_again(
1715 worktree_root: impl AsRef<Path>,
1716 git_dir: impl AsRef<Path>,
1717 format: ObjectFormat,
1718 paths: &[PathBuf],
1719 options: UpdateIndexOptions,
1720) -> Result<UpdateIndexResult> {
1721 let worktree_root = worktree_root.as_ref();
1722 let git_dir = git_dir.as_ref();
1723 let index_path = repository_index_path(git_dir);
1724 if !index_path.exists() {
1725 return Ok(UpdateIndexResult {
1726 entries: 0,
1727 updated: Vec::new(),
1728 });
1729 }
1730 let index = Index::parse(&fs::read(&index_path)?, format)?;
1731 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1732 let head_entries = head_tree_entries(git_dir, format, &db)?;
1733 let selected_paths = selected_git_paths(worktree_root, paths)?;
1734 let mut again_paths = Vec::new();
1735 for entry in &index.entries {
1736 if index_entry_stage(entry) != 0 {
1737 continue;
1738 }
1739 if !selected_paths.is_empty() && !git_path_selected(entry.path.as_bytes(), &selected_paths)
1740 {
1741 continue;
1742 }
1743 let differs_from_head = match head_entries.get(entry.path.as_bytes()) {
1744 Some(head_entry) => head_entry.oid != entry.oid || head_entry.mode != entry.mode,
1745 None => true,
1746 };
1747 if differs_from_head {
1748 again_paths.push(worktree_root.join(repo_path_to_os_path(entry.path.as_bytes())?));
1749 }
1750 }
1751 if again_paths.is_empty() {
1752 return Ok(UpdateIndexResult {
1753 entries: index.entries.len(),
1754 updated: Vec::new(),
1755 });
1756 }
1757 update_index_paths(worktree_root, git_dir, format, &again_paths, options)
1758}
1759
1760pub fn set_index_assume_unchanged_paths(
1761 worktree_root: impl AsRef<Path>,
1762 git_dir: impl AsRef<Path>,
1763 format: ObjectFormat,
1764 paths: &[PathBuf],
1765 assume_unchanged: bool,
1766) -> Result<UpdateIndexResult> {
1767 let worktree_root = worktree_root.as_ref();
1768 let git_dir = git_dir.as_ref();
1769 let index_path = repository_index_path(git_dir);
1770 let mut index = if index_path.exists() {
1771 Index::parse(&fs::read(&index_path)?, format)?
1772 } else {
1773 Index {
1774 version: 2,
1775 entries: Vec::new(),
1776 extensions: Vec::new(),
1777 checksum: None,
1778 }
1779 };
1780 let sparse = active_sparse_checkout(git_dir)?;
1781 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1782 if index.is_sparse() {
1783 expand_sparse_index(&mut index, &db, format)?;
1784 }
1785 let selected_paths = paths
1786 .iter()
1787 .map(|path| {
1788 let absolute = if path.is_absolute() {
1789 path.clone()
1790 } else {
1791 worktree_root.join(path)
1792 };
1793 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1794 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1795 })?;
1796 git_path_bytes(relative)
1797 })
1798 .collect::<Result<Vec<_>>>()?;
1799 for path in selected_paths {
1800 if let Some(entry) = index.entries.iter_mut().find(|entry| entry.path == path) {
1801 if assume_unchanged {
1802 entry.flags |= INDEX_FLAG_ASSUME_UNCHANGED;
1803 } else {
1804 entry.flags &= !INDEX_FLAG_ASSUME_UNCHANGED;
1805 }
1806 }
1807 }
1808 normalize_index_version_for_extended_flags(&mut index);
1809 if let Some((sparse, mode)) = sparse
1810 && sparse.sparse_index
1811 {
1812 let matcher = SparseMatcher::new(&sparse, mode);
1813 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
1814 }
1815 write_repository_index_ref(git_dir, format, &index)?;
1816 Ok(UpdateIndexResult {
1817 entries: index.entries.len(),
1818 updated: Vec::new(),
1819 })
1820}
1821
1822pub(crate) fn selected_git_paths(
1823 worktree_root: &Path,
1824 paths: &[PathBuf],
1825) -> Result<BTreeSet<Vec<u8>>> {
1826 paths
1827 .iter()
1828 .map(|path| {
1829 let absolute = if path.is_absolute() {
1830 path.clone()
1831 } else {
1832 worktree_root.join(path)
1833 };
1834 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1835 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1836 })?;
1837 git_path_bytes(relative)
1838 })
1839 .collect()
1840}
1841
1842pub(crate) fn git_path_selected(path: &[u8], selected_paths: &BTreeSet<Vec<u8>>) -> bool {
1843 selected_paths
1844 .iter()
1845 .any(|selected| path == selected || index_entry_is_under_path(path, selected))
1846}
1847
1848pub fn set_index_skip_worktree_paths(
1849 worktree_root: impl AsRef<Path>,
1850 git_dir: impl AsRef<Path>,
1851 format: ObjectFormat,
1852 paths: &[PathBuf],
1853 skip_worktree: bool,
1854) -> Result<UpdateIndexResult> {
1855 let worktree_root = worktree_root.as_ref();
1856 let git_dir = git_dir.as_ref();
1857 let index_path = repository_index_path(git_dir);
1858 let mut index = if index_path.exists() {
1859 Index::parse(&fs::read(&index_path)?, format)?
1860 } else {
1861 Index {
1862 version: 2,
1863 entries: Vec::new(),
1864 extensions: Vec::new(),
1865 checksum: None,
1866 }
1867 };
1868 let sparse = active_sparse_checkout(git_dir)?;
1869 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1870 if index.is_sparse() {
1871 expand_sparse_index(&mut index, &db, format)?;
1872 }
1873 let selected_paths = paths
1874 .iter()
1875 .map(|path| {
1876 let absolute = if path.is_absolute() {
1877 path.clone()
1878 } else {
1879 worktree_root.join(path)
1880 };
1881 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1882 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1883 })?;
1884 git_path_bytes(relative)
1885 })
1886 .collect::<Result<Vec<_>>>()?;
1887 for path in selected_paths {
1888 if let Some(entry) = index.entries.iter_mut().find(|entry| entry.path == path) {
1889 if skip_worktree {
1890 entry.flags |= INDEX_FLAG_EXTENDED;
1891 entry.flags_extended |= INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
1892 } else {
1893 entry.flags_extended &= !INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
1894 if entry.flags_extended == 0 {
1895 entry.flags &= !INDEX_FLAG_EXTENDED;
1896 }
1897 }
1898 }
1899 }
1900 normalize_index_version_for_extended_flags(&mut index);
1901 if let Some((sparse, mode)) = sparse
1902 && sparse.sparse_index
1903 {
1904 let matcher = SparseMatcher::new(&sparse, mode);
1905 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
1906 }
1907 write_repository_index_ref(git_dir, format, &index)?;
1908 Ok(UpdateIndexResult {
1909 entries: index.entries.len(),
1910 updated: Vec::new(),
1911 })
1912}
1913
1914pub fn set_index_fsmonitor_valid_paths(
1915 worktree_root: impl AsRef<Path>,
1916 git_dir: impl AsRef<Path>,
1917 format: ObjectFormat,
1918 paths: &[PathBuf],
1919 _fsmonitor_valid: bool,
1920) -> Result<UpdateIndexResult> {
1921 let worktree_root = worktree_root.as_ref();
1922 let git_dir = git_dir.as_ref();
1923 let index_path = repository_index_path(git_dir);
1924 let index = if index_path.exists() {
1925 Index::parse(&fs::read(&index_path)?, format)?
1926 } else {
1927 Index {
1928 version: 2,
1929 entries: Vec::new(),
1930 extensions: Vec::new(),
1931 checksum: None,
1932 }
1933 };
1934 let selected_paths = paths
1935 .iter()
1936 .map(|path| {
1937 let absolute = if path.is_absolute() {
1938 path.clone()
1939 } else {
1940 worktree_root.join(path)
1941 };
1942 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1943 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1944 })?;
1945 git_path_bytes(relative)
1946 })
1947 .collect::<Result<Vec<_>>>()?;
1948 for path in selected_paths {
1949 if !index.entries.iter().any(|entry| entry.path == path) {
1950 eprintln!(
1951 "fatal: Unable to mark file {}",
1952 String::from_utf8_lossy(&path)
1953 );
1954 return Err(GitError::Exit(128));
1955 }
1956 }
1957 Ok(UpdateIndexResult {
1958 entries: index.entries.len(),
1959 updated: Vec::new(),
1960 })
1961}
1962
1963pub fn set_index_version(
1964 git_dir: impl AsRef<Path>,
1965 format: ObjectFormat,
1966 version: u32,
1967 verbose: bool,
1968) -> Result<UpdateIndexResult> {
1969 if !matches!(version, 2..=4) {
1970 return Err(GitError::Unsupported(format!(
1971 "update-index currently supports --index-version 2, 3, or 4, got {version}"
1972 )));
1973 }
1974 let git_dir = git_dir.as_ref();
1975 let index_path = repository_index_path(git_dir);
1976 let mut index = if index_path.exists() {
1977 Index::parse(&fs::read(&index_path)?, format)?
1978 } else {
1979 Index {
1980 version: 2,
1981 entries: Vec::new(),
1982 extensions: Vec::new(),
1983 checksum: None,
1984 }
1985 };
1986 let previous = index.version;
1989 if verbose {
1990 println!("index-version: was {previous}, set to {version}");
1991 }
1992 index.version = version;
1993 normalize_index_version_for_extended_flags(&mut index);
1994 write_repository_index_ref(git_dir, format, &index)?;
1995 Ok(UpdateIndexResult {
1996 entries: index.entries.len(),
1997 updated: Vec::new(),
1998 })
1999}
2000
2001pub fn force_write_index(
2002 git_dir: impl AsRef<Path>,
2003 format: ObjectFormat,
2004) -> Result<UpdateIndexResult> {
2005 let git_dir = git_dir.as_ref();
2006 let index_path = repository_index_path(git_dir);
2007 let mut index = if index_path.exists() {
2008 Index::parse(&fs::read(&index_path)?, format)?
2009 } else {
2010 Index {
2011 version: 2,
2012 entries: Vec::new(),
2013 extensions: Vec::new(),
2014 checksum: None,
2015 }
2016 };
2017 normalize_index_version_for_extended_flags(&mut index);
2018 write_repository_index_ref(git_dir, format, &index)?;
2019 Ok(UpdateIndexResult {
2020 entries: index.entries.len(),
2021 updated: Vec::new(),
2022 })
2023}
2024
2025pub fn enable_untracked_cache(
2026 worktree_root: impl AsRef<Path>,
2027 git_dir: impl AsRef<Path>,
2028 format: ObjectFormat,
2029) -> Result<()> {
2030 let worktree_root = worktree_root.as_ref();
2031 let git_dir = git_dir.as_ref();
2032 let index_path = repository_index_path(git_dir);
2033 let mut index = if index_path.exists() {
2034 Index::parse(&fs::read(&index_path)?, format)?
2035 } else {
2036 empty_index()
2037 };
2038 let ident = untracked_cache_ident(worktree_root);
2039 let dir_flags = untracked_cache_dir_flags(StatusUntrackedMode::Normal);
2040 let cache = match index.untracked_cache(format)? {
2041 Some(mut cache) if cache.ident == ident => {
2042 cache.dir_flags = dir_flags;
2043 cache
2044 }
2045 _ => UntrackedCache::new(format, ident, dir_flags),
2046 };
2047 index.set_untracked_cache(format, Some(&cache))?;
2048 write_repository_index_ref(git_dir, format, &index)?;
2049 Ok(())
2050}
2051
2052pub fn disable_untracked_cache(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<()> {
2053 let git_dir = git_dir.as_ref();
2054 let index_path = repository_index_path(git_dir);
2055 if !index_path.exists() {
2056 return Ok(());
2057 }
2058 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
2059 index.set_untracked_cache(format, None)?;
2060 write_repository_index_ref(git_dir, format, &index)?;
2061 Ok(())
2062}
2063
2064pub fn refresh_untracked_cache_after_status(
2065 worktree_root: impl AsRef<Path>,
2066 git_dir: impl AsRef<Path>,
2067 format: ObjectFormat,
2068 config: &GitConfig,
2069 untracked_mode: StatusUntrackedMode,
2070) -> Result<()> {
2071 if matches!(untracked_mode, StatusUntrackedMode::None) {
2072 return Ok(());
2073 }
2074 let worktree_root = worktree_root.as_ref();
2075 let git_dir = git_dir.as_ref();
2076 let index_path = repository_index_path(git_dir);
2077 let untracked_cache_setting = config.get("core", None, "untrackedCache");
2078 match untracked_cache_setting {
2079 Some("keep") | None => {
2080 if !repository_index_has_extension(git_dir, format, b"UNTR")? {
2081 return Ok(());
2082 }
2083 }
2084 Some("false" | "no" | "off" | "0") | Some("true" | "yes" | "on" | "1") => {}
2085 Some(_) => {
2086 if !repository_index_has_extension(git_dir, format, b"UNTR")? {
2087 return Ok(());
2088 }
2089 }
2090 }
2091 let mut index = if index_path.exists() {
2092 Index::parse(&fs::read(&index_path)?, format)?
2093 } else {
2094 empty_index()
2095 };
2096 match untracked_cache_setting {
2097 Some("false") | Some("no") | Some("off") | Some("0") => {
2098 index.set_untracked_cache(format, None)?;
2099 write_repository_index_ref(git_dir, format, &index)?;
2100 return Ok(());
2101 }
2102 Some("true") | Some("yes") | Some("on") | Some("1") => {}
2103 Some("keep") | None => {
2104 if index.untracked_cache(format)?.is_none() {
2105 return Ok(());
2106 }
2107 }
2108 Some(_) => {
2109 if index.untracked_cache(format)?.is_none() {
2110 return Ok(());
2111 }
2112 }
2113 }
2114 let old_cache = index.untracked_cache(format).ok().flatten();
2115 let ident = untracked_cache_ident(worktree_root);
2116 if old_cache.as_ref().is_some_and(|cache| cache.ident != ident) {
2117 eprintln!("warning: untracked cache is disabled on this system or location");
2118 emit_untracked_cache_bypass_trace();
2119 return Ok(());
2120 }
2121 let cache = build_untracked_cache(worktree_root, git_dir, format, &index, untracked_mode)?;
2122 emit_untracked_cache_trace(old_cache.as_ref(), &cache);
2123 index.set_untracked_cache(format, Some(&cache))?;
2124 write_repository_index_ref(git_dir, format, &index)?;
2125 Ok(())
2126}
2127
2128pub(crate) fn repository_index_has_extension(
2129 git_dir: &Path,
2130 format: ObjectFormat,
2131 signature: &[u8; 4],
2132) -> Result<bool> {
2133 let index_path = repository_index_path(git_dir);
2134 if !index_path.exists() {
2135 return Ok(false);
2136 }
2137 let bytes = read_borrowed_index_bytes(&index_path)?;
2138 sley_index::Index::bytes_have_extension(bytes.as_ref(), format, signature)
2139}
2140
2141pub fn emit_untracked_cache_bypass_trace() {
2142 sley_core::trace2::perf_read_directory_data("path", "");
2143}
2144
2145pub(crate) fn index_extensions_without_cache_tree(extensions: &[u8]) -> Vec<u8> {
2146 let mut offset = 0;
2147 let mut filtered = Vec::new();
2148 while offset < extensions.len() {
2149 if extensions.len().saturating_sub(offset) < 8 {
2150 return Vec::new();
2151 }
2152 let signature = &extensions[offset..offset + 4];
2153 let size = u32::from_be_bytes([
2154 extensions[offset + 4],
2155 extensions[offset + 5],
2156 extensions[offset + 6],
2157 extensions[offset + 7],
2158 ]) as usize;
2159 let end = offset + 8 + size;
2160 if end > extensions.len() {
2161 return Vec::new();
2162 }
2163 if signature != b"TREE" {
2164 filtered.extend_from_slice(&extensions[offset..end]);
2165 }
2166 offset = end;
2167 }
2168 filtered
2169}
2170
2171#[derive(Clone)]
2172pub(crate) struct ResolveUndoRecord {
2173 pub(crate) path: Vec<u8>,
2174 pub(crate) stages: [Option<(u32, ObjectId)>; 3],
2175}
2176
2177pub(crate) fn record_resolve_undo_for_path(
2178 index: &mut Index,
2179 format: ObjectFormat,
2180 path: &[u8],
2181 entries: &[IndexEntry],
2182) -> Result<()> {
2183 let mut stages = [None, None, None];
2184 for entry in entries {
2185 match entry.stage() {
2186 Stage::Base => stages[0] = Some((entry.mode, entry.oid)),
2187 Stage::Ours => stages[1] = Some((entry.mode, entry.oid)),
2188 Stage::Theirs => stages[2] = Some((entry.mode, entry.oid)),
2189 Stage::Normal => {}
2190 }
2191 }
2192 if stages.iter().all(Option::is_none) {
2193 return Ok(());
2194 }
2195 let mut records = parse_resolve_undo_records(index.extension(b"REUC")?, format)?;
2196 records.retain(|record| record.path.as_slice() != path);
2197 records.push(ResolveUndoRecord {
2198 path: path.to_vec(),
2199 stages,
2200 });
2201 records.sort_by(|left, right| left.path.cmp(&right.path));
2202 set_resolve_undo_extension(index, &records)
2203}
2204
2205pub(crate) fn record_resolve_undo_for_range(
2206 index: &mut Index,
2207 format: ObjectFormat,
2208 path: &[u8],
2209 range: Range<usize>,
2210) -> Result<()> {
2211 if range.is_empty() {
2212 return Ok(());
2213 }
2214 let entries = index.entries[range].to_vec();
2215 record_resolve_undo_for_path(index, format, path, &entries)
2216}
2217
2218pub(crate) fn parse_resolve_undo_records(
2219 body: Option<&[u8]>,
2220 format: ObjectFormat,
2221) -> Result<Vec<ResolveUndoRecord>> {
2222 let Some(body) = body else {
2223 return Ok(Vec::new());
2224 };
2225 let mut records = Vec::new();
2226 let mut offset = 0usize;
2227 while offset < body.len() {
2228 let path_end = body[offset..]
2229 .iter()
2230 .position(|byte| *byte == 0)
2231 .ok_or_else(|| GitError::InvalidFormat("truncated REUC path".into()))?
2232 + offset;
2233 let path = body[offset..path_end].to_vec();
2234 offset = path_end + 1;
2235
2236 let mut modes = [0u32; 3];
2237 for mode in &mut modes {
2238 let mode_end = body[offset..]
2239 .iter()
2240 .position(|byte| *byte == 0)
2241 .ok_or_else(|| GitError::InvalidFormat("truncated REUC mode".into()))?
2242 + offset;
2243 let text = std::str::from_utf8(&body[offset..mode_end])
2244 .map_err(|_| GitError::InvalidFormat("invalid REUC mode".into()))?;
2245 *mode = u32::from_str_radix(text, 8)
2246 .map_err(|_| GitError::InvalidFormat("invalid REUC mode".into()))?;
2247 offset = mode_end + 1;
2248 }
2249
2250 let mut stages = [None, None, None];
2251 for (idx, mode) in modes.into_iter().enumerate() {
2252 if mode == 0 {
2253 continue;
2254 }
2255 let end = offset
2256 .checked_add(format.raw_len())
2257 .ok_or_else(|| GitError::InvalidFormat("REUC oid length overflow".into()))?;
2258 if end > body.len() {
2259 return Err(GitError::InvalidFormat("truncated REUC oid".into()));
2260 }
2261 stages[idx] = Some((mode, ObjectId::from_raw(format, &body[offset..end])?));
2262 offset = end;
2263 }
2264 records.push(ResolveUndoRecord { path, stages });
2265 }
2266 Ok(records)
2267}
2268
2269pub(crate) fn set_resolve_undo_extension(
2270 index: &mut Index,
2271 records: &[ResolveUndoRecord],
2272) -> Result<()> {
2273 let mut body = Vec::new();
2274 for record in records {
2275 body.extend_from_slice(&record.path);
2276 body.push(0);
2277 for stage in record.stages {
2278 match stage {
2279 Some((mode, _)) => body.extend_from_slice(format!("{mode:o}").as_bytes()),
2280 None => body.push(b'0'),
2281 }
2282 body.push(0);
2283 }
2284 for (_, oid) in record.stages.into_iter().flatten() {
2285 body.extend_from_slice(oid.as_bytes());
2286 }
2287 }
2288
2289 let chunks = index.extension_chunks()?;
2290 let mut rebuilt = Vec::with_capacity(index.extensions.len() + body.len() + 8);
2291 let mut replaced = false;
2292 for (signature, chunk_body) in chunks {
2293 if &signature == b"REUC" {
2294 if !body.is_empty() {
2295 append_index_extension(&mut rebuilt, b"REUC", &body)?;
2296 }
2297 replaced = true;
2298 } else {
2299 append_index_extension(&mut rebuilt, &signature, chunk_body)?;
2300 }
2301 }
2302 if !replaced && !body.is_empty() {
2303 append_index_extension(&mut rebuilt, b"REUC", &body)?;
2304 }
2305 index.extensions = rebuilt;
2306 Ok(())
2307}
2308
2309pub fn clear_resolve_undo(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<()> {
2310 let git_dir = git_dir.as_ref();
2311 let index_path = repository_index_path(git_dir);
2312 match fs::read(&index_path) {
2313 Ok(bytes) => {
2314 let mut index = Index::parse(&bytes, format)?;
2315 set_resolve_undo_extension(&mut index, &[])?;
2316 write_repository_index_ref(git_dir, format, &index)
2317 }
2318 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
2319 Err(err) => Err(err.into()),
2320 }
2321}
2322
2323pub(crate) fn append_index_extension(
2324 out: &mut Vec<u8>,
2325 signature: &[u8; 4],
2326 body: &[u8],
2327) -> Result<()> {
2328 let len = u32::try_from(body.len())
2329 .map_err(|_| GitError::InvalidFormat("index extension body too large".into()))?;
2330 out.extend_from_slice(signature);
2331 out.extend_from_slice(&len.to_be_bytes());
2332 out.extend_from_slice(body);
2333 Ok(())
2334}
2335
2336pub(crate) fn index_extensions_without_split_index_link(extensions: &[u8]) -> Vec<u8> {
2337 let mut offset = 0;
2338 let mut filtered = Vec::new();
2339 while offset < extensions.len() {
2340 if extensions.len().saturating_sub(offset) < 8 {
2341 filtered.extend_from_slice(&extensions[offset..]);
2342 break;
2343 }
2344 let signature = &extensions[offset..offset + 4];
2345 let len = u32::from_be_bytes([
2346 extensions[offset + 4],
2347 extensions[offset + 5],
2348 extensions[offset + 6],
2349 extensions[offset + 7],
2350 ]) as usize;
2351 let end = offset.saturating_add(8).saturating_add(len);
2352 if end > extensions.len() {
2353 filtered.extend_from_slice(&extensions[offset..]);
2354 break;
2355 }
2356 if signature != b"link" {
2357 filtered.extend_from_slice(&extensions[offset..end]);
2358 }
2359 offset = end;
2360 }
2361 filtered
2362}
2363
2364pub(crate) fn preserved_index_extensions(git_dir: &Path, format: ObjectFormat) -> Result<Vec<u8>> {
2365 let index_path = repository_index_path(git_dir);
2366 match fs::read(&index_path) {
2367 Ok(bytes) => {
2368 let index = Index::parse(&bytes, format)?;
2369 Ok(index_extensions_without_cache_tree_or_resolve_undo(
2370 &index.extensions,
2371 ))
2372 }
2373 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
2374 Err(err) => Err(err.into()),
2375 }
2376}
2377
2378pub(crate) fn index_extensions_without_cache_tree_or_resolve_undo(extensions: &[u8]) -> Vec<u8> {
2379 let mut filtered = Vec::new();
2380 let mut offset = 0usize;
2381 while offset + 8 <= extensions.len() {
2382 let signature = &extensions[offset..offset + 4];
2383 let len = u32::from_be_bytes([
2384 extensions[offset + 4],
2385 extensions[offset + 5],
2386 extensions[offset + 6],
2387 extensions[offset + 7],
2388 ]) as usize;
2389 let end = offset + 8 + len;
2390 if end > extensions.len() {
2391 filtered.extend_from_slice(&extensions[offset..]);
2392 break;
2393 }
2394 if signature != b"TREE" && signature != b"REUC" {
2395 filtered.extend_from_slice(&extensions[offset..end]);
2396 }
2397 offset = end;
2398 }
2399 filtered
2400}
2401
2402pub(crate) fn repository_index_is_split(git_dir: &Path, format: ObjectFormat) -> Result<bool> {
2403 let index_path = repository_index_path(git_dir);
2404 match fs::read(index_path) {
2405 Ok(bytes) => Ok(Index::parse(&bytes, format)?
2406 .split_index_link(format)?
2407 .is_some()),
2408 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
2409 Err(err) => Err(err.into()),
2410 }
2411}
2412
2413pub(crate) fn git_test_split_index_enabled() -> bool {
2414 env::var("GIT_TEST_SPLIT_INDEX")
2415 .ok()
2416 .is_some_and(|value| !matches!(value.as_str(), "" | "0" | "false" | "False" | "FALSE"))
2417}
2418
2419pub fn write_repository_index(git_dir: &Path, format: ObjectFormat, index: Index) -> Result<()> {
2420 let split = index.split_index_link(format)?.is_some()
2421 || repository_index_is_split(git_dir, format)?
2422 || git_test_split_index_enabled();
2423 write_repository_index_ref_with_split(git_dir, format, &index, split)
2424}
2425
2426pub fn write_repository_index_ref(
2427 git_dir: &Path,
2428 format: ObjectFormat,
2429 index: &Index,
2430) -> Result<()> {
2431 let split = index.split_index_link(format)?.is_some()
2432 || repository_index_is_split(git_dir, format)?
2433 || git_test_split_index_enabled();
2434 write_repository_index_ref_with_split(git_dir, format, index, split)
2435}
2436
2437pub fn write_repository_index_ref_skip_hash(
2440 git_dir: &Path,
2441 format: ObjectFormat,
2442 index: &Index,
2443 skip_hash: bool,
2444) -> Result<()> {
2445 let split = index.split_index_link(format)?.is_some()
2446 || repository_index_is_split(git_dir, format)?
2447 || git_test_split_index_enabled();
2448 write_repository_index_ref_with_split_skip_hash(git_dir, format, index, split, skip_hash)
2449}
2450
2451pub(crate) fn write_repository_index_ref_with_split(
2452 git_dir: &Path,
2453 format: ObjectFormat,
2454 index: &Index,
2455 split: bool,
2456) -> Result<()> {
2457 write_repository_index_ref_with_split_skip_hash(git_dir, format, index, split, false)
2458}
2459
2460pub(crate) fn write_repository_index_ref_with_split_skip_hash(
2465 git_dir: &Path,
2466 format: ObjectFormat,
2467 index: &Index,
2468 split: bool,
2469 skip_hash: bool,
2470) -> Result<()> {
2471 let index_path = repository_index_path(git_dir);
2472 if !split || alternate_index_output_path(git_dir, &index_path) {
2473 let smudged_entries = racily_clean_entry_indexes_before_write(git_dir, format, index)?;
2474 let extensions = if index.split_index_link(format)?.is_some() {
2475 Cow::Owned(index_extensions_without_split_index_link(&index.extensions))
2476 } else {
2477 Cow::Borrowed(index.extensions.as_slice())
2478 };
2479 let mut bytes = if smudged_entries.is_empty() && matches!(extensions, Cow::Borrowed(_)) {
2480 index.write(format)?
2481 } else {
2482 write_index_with_entry_size_overrides(format, index, &smudged_entries, &extensions)?
2483 };
2484 if skip_hash {
2485 zero_trailing_index_hash(&mut bytes, format);
2486 }
2487 fs::write(&index_path, bytes)?;
2488 apply_index_shared_file_mode(git_dir, &index_path, None)?;
2489 return Ok(());
2490 }
2491
2492 if let Some(link) = index.split_index_link(format)?
2493 && !link.base_oid.is_null()
2494 && let Some(base) = read_shared_index_for_link(git_dir, &index_path, format, &link)?
2495 && !split_index_delta_exceeds_threshold(git_dir, index, &base)
2496 {
2497 let (entries, link) = split_index_delta_entries(index, &base, &link)?;
2498 let extensions = index_extensions_without_split_index_link(
2499 &index_extensions_without_cache_tree(&index.extensions),
2500 );
2501 let mut primary = Index {
2502 version: index.version,
2503 entries,
2504 extensions,
2505 checksum: None,
2506 };
2507 primary.set_split_index_link(Some(&link))?;
2508 fs::write(&index_path, primary.write(format)?)?;
2509 apply_index_shared_file_mode(git_dir, &index_path, None)?;
2510 return Ok(());
2511 }
2512
2513 let mode_source = fs::metadata(&index_path)
2514 .ok()
2515 .map(|metadata| metadata.permissions());
2516 let mut shared = index.clone();
2517 smudge_racily_clean_entries_before_write(git_dir, format, &mut shared)?;
2518 shared.clear_split_index_link()?;
2519 shared.extensions = index_extensions_without_cache_tree(&shared.extensions);
2520 let shared_bytes = shared.write(format)?;
2521 let shared_oid = index_checksum_from_bytes(format, &shared_bytes)?;
2522 let shared_path = git_dir.join(format!("sharedindex.{shared_oid}"));
2523 if !shared_path.exists() {
2524 fs::write(&shared_path, &shared_bytes)?;
2525 }
2526 apply_index_shared_file_mode(git_dir, &shared_path, mode_source.as_ref())?;
2527 clean_shared_index_files(git_dir, shared_oid)?;
2528
2529 let mut primary = Index {
2530 version: index.version,
2531 entries: Vec::new(),
2532 extensions: Vec::new(),
2533 checksum: None,
2534 };
2535 primary.set_split_index_link(Some(&SplitIndexLink::new(shared_oid)))?;
2536 fs::write(&index_path, primary.write(format)?)?;
2537 apply_index_shared_file_mode(git_dir, &index_path, mode_source.as_ref())?;
2538 Ok(())
2539}
2540
2541pub(crate) fn alternate_index_output_path(git_dir: &Path, index_path: &Path) -> bool {
2542 env::var_os("GIT_INDEX_FILE").is_some() && index_path != git_dir.join("index")
2543}
2544
2545pub(crate) fn clean_shared_index_files(git_dir: &Path, current_oid: ObjectId) -> Result<()> {
2546 let Some(expire_before) = shared_index_expire_before(git_dir) else {
2547 return Ok(());
2548 };
2549 let current_name = format!("sharedindex.{current_oid}");
2550 let mut expired = Vec::new();
2551 for entry in fs::read_dir(git_dir)? {
2552 let entry = entry?;
2553 let name = entry.file_name();
2554 let Some(name) = name.to_str() else {
2555 continue;
2556 };
2557 if !name.starts_with("sharedindex.") || name == current_name {
2558 continue;
2559 }
2560 let metadata = entry.metadata()?;
2561 let Ok(modified) = metadata.modified() else {
2562 continue;
2563 };
2564 if modified <= expire_before {
2565 expired.push((modified, entry.path()));
2566 }
2567 }
2568 expired.sort_by_key(|(modified, _)| *modified);
2569 let delete_count = expired.len().saturating_sub(1);
2570 for (_, path) in expired.into_iter().take(delete_count) {
2571 let _ = fs::remove_file(path);
2572 }
2573 Ok(())
2574}
2575
2576pub(crate) fn shared_index_expire_before(git_dir: &Path) -> Option<SystemTime> {
2577 let value = sley_config::read_repo_config(git_dir, None)
2578 .ok()
2579 .and_then(|config| {
2580 config
2581 .get("splitIndex", None, "sharedIndexExpire")
2582 .map(str::to_string)
2583 })
2584 .unwrap_or_else(|| "2.weeks.ago".to_string());
2585 let value = value.trim();
2586 if value.eq_ignore_ascii_case("never") {
2587 return None;
2588 }
2589 if value.eq_ignore_ascii_case("now") {
2590 return Some(SystemTime::now());
2591 }
2592 if let Some(days) = value
2593 .strip_suffix(".days.ago")
2594 .or_else(|| value.strip_suffix(".day.ago"))
2595 .and_then(|days| days.parse::<u64>().ok())
2596 {
2597 return SystemTime::now().checked_sub(Duration::from_secs(days * 24 * 60 * 60));
2598 }
2599 if let Some(weeks) = value
2600 .strip_suffix(".weeks.ago")
2601 .or_else(|| value.strip_suffix(".week.ago"))
2602 .and_then(|weeks| weeks.parse::<u64>().ok())
2603 {
2604 return SystemTime::now().checked_sub(Duration::from_secs(weeks * 7 * 24 * 60 * 60));
2605 }
2606 SystemTime::now().checked_sub(Duration::from_secs(14 * 24 * 60 * 60))
2607}
2608
2609pub(crate) fn apply_index_shared_file_mode(
2610 git_dir: &Path,
2611 path: &Path,
2612 mode_source: Option<&fs::Permissions>,
2613) -> Result<()> {
2614 #[cfg(unix)]
2615 {
2616 use std::os::unix::fs::PermissionsExt;
2617
2618 let current = fs::metadata(path)?.permissions();
2619 let source_mode = mode_source
2620 .map(fs::Permissions::mode)
2621 .unwrap_or_else(|| current.mode());
2622 let mode = sley_config::read_repo_config(git_dir, None)
2623 .ok()
2624 .and_then(|config| {
2625 config
2626 .get("core", None, "sharedRepository")
2627 .and_then(|value| shared_repository_file_mode(value, source_mode))
2628 })
2629 .unwrap_or(source_mode & 0o7777);
2630 fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
2631 }
2632 #[cfg(not(unix))]
2633 {
2634 let _ = git_dir;
2635 let _ = path;
2636 let _ = mode_source;
2637 }
2638 Ok(())
2639}
2640
2641#[cfg(unix)]
2642pub(crate) fn shared_repository_file_mode(value: &str, source_mode: u32) -> Option<u32> {
2643 match value {
2644 "umask" | "false" | "no" | "off" | "0" => None,
2645 "group" | "true" | "yes" | "on" | "1" => Some((source_mode | 0o660) & 0o7777),
2646 "all" | "world" | "everybody" | "2" | "3" => Some((source_mode | 0o664) & 0o7777),
2647 value => {
2648 let parsed = u32::from_str_radix(value, 8).ok()?;
2649 (parsed & 0o600 == 0o600).then_some(parsed & 0o666)
2650 }
2651 }
2652}
2653
2654pub(crate) fn read_shared_index_for_link(
2655 git_dir: &Path,
2656 index_path: &Path,
2657 format: ObjectFormat,
2658 link: &SplitIndexLink,
2659) -> Result<Option<Index>> {
2660 let name = format!("sharedindex.{}", link.base_oid);
2661 let bytes = match fs::read(git_dir.join(&name)) {
2662 Ok(bytes) => bytes,
2663 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2664 let alternate = index_path
2665 .parent()
2666 .unwrap_or_else(|| Path::new("."))
2667 .join(&name);
2668 match fs::read(alternate) {
2669 Ok(bytes) => bytes,
2670 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2671 Err(err) => return Err(err.into()),
2672 }
2673 }
2674 Err(err) => return Err(err.into()),
2675 };
2676 let base = Index::parse(&bytes, format)?;
2677 if base.checksum != Some(link.base_oid) {
2678 return Ok(None);
2679 }
2680 Ok(Some(base))
2681}
2682
2683pub(crate) fn split_index_delta_exceeds_threshold(
2684 git_dir: &Path,
2685 index: &Index,
2686 base: &Index,
2687) -> bool {
2688 let max_percent = sley_config::read_repo_config(git_dir, None)
2689 .ok()
2690 .and_then(|config| {
2691 config
2692 .get("splitIndex", None, "maxPercentChange")
2693 .and_then(|value| value.parse::<i64>().ok())
2694 })
2695 .unwrap_or(20);
2696 match max_percent {
2697 0 => return true,
2698 100.. => return false,
2699 value if value < 0 => {}
2700 _ => {}
2701 }
2702 let not_shared = count_entries_not_shared_with_base(index, base);
2703 (index.entries.len() as i64) * max_percent < (not_shared as i64) * 100
2704}
2705
2706pub(crate) fn count_entries_not_shared_with_base(index: &Index, base: &Index) -> usize {
2707 index
2708 .entries
2709 .iter()
2710 .filter(|entry| {
2711 base.entries
2712 .binary_search_by(|base_entry| compare_index_key(base_entry, entry))
2713 .is_err()
2714 })
2715 .count()
2716}
2717
2718pub(crate) fn split_index_delta_entries(
2719 index: &Index,
2720 base: &Index,
2721 previous_link: &SplitIndexLink,
2722) -> Result<(Vec<IndexEntry>, SplitIndexLink)> {
2723 let mut delete_positions = Vec::new();
2724 let mut replace_positions = Vec::new();
2725 let mut replacements = Vec::new();
2726 let mut additions = Vec::new();
2727 let mut base_pos = 0usize;
2728 let mut index_pos = 0usize;
2729 while base_pos < base.entries.len() && index_pos < index.entries.len() {
2730 match compare_index_key(&base.entries[base_pos], &index.entries[index_pos]) {
2731 Ordering::Equal => {
2732 if previous_link
2733 .delete_positions
2734 .binary_search(&(base_pos as u32))
2735 .is_ok()
2736 {
2737 delete_positions.push(base_pos as u32);
2738 additions.push(index.entries[index_pos].clone());
2739 } else if !index_entry_content_eq(
2740 &base.entries[base_pos],
2741 &index.entries[index_pos],
2742 ) {
2743 replace_positions.push(base_pos as u32);
2744 let mut replacement = index.entries[index_pos].clone();
2745 replacement.path = BString::from(Vec::<u8>::new());
2746 replacement.refresh_name_length();
2747 replacements.push(replacement);
2748 }
2749 base_pos += 1;
2750 index_pos += 1;
2751 }
2752 Ordering::Less => {
2753 delete_positions.push(base_pos as u32);
2754 base_pos += 1;
2755 }
2756 Ordering::Greater => {
2757 additions.push(index.entries[index_pos].clone());
2758 index_pos += 1;
2759 }
2760 }
2761 }
2762 while base_pos < base.entries.len() {
2763 delete_positions.push(base_pos as u32);
2764 base_pos += 1;
2765 }
2766 while index_pos < index.entries.len() {
2767 additions.push(index.entries[index_pos].clone());
2768 index_pos += 1;
2769 }
2770 replacements.extend(additions);
2771 Ok((
2772 replacements,
2773 SplitIndexLink {
2774 base_oid: previous_link.base_oid,
2775 delete_positions,
2776 replace_positions,
2777 },
2778 ))
2779}
2780
2781pub(crate) fn compare_index_key(left: &IndexEntry, right: &IndexEntry) -> Ordering {
2782 left.path
2783 .as_bytes()
2784 .cmp(right.path.as_bytes())
2785 .then_with(|| left.stage().as_u16().cmp(&right.stage().as_u16()))
2786}
2787
2788pub(crate) fn index_entry_content_eq(left: &IndexEntry, right: &IndexEntry) -> bool {
2789 const ONDISK_FLAGS: u16 = sley_index::INDEX_FLAG_STAGE_MASK
2790 | sley_index::INDEX_FLAG_VALID
2791 | sley_index::INDEX_FLAG_EXTENDED;
2792 left.ctime_seconds == right.ctime_seconds
2793 && left.ctime_nanoseconds == right.ctime_nanoseconds
2794 && left.mtime_seconds == right.mtime_seconds
2795 && left.mtime_nanoseconds == right.mtime_nanoseconds
2796 && left.dev == right.dev
2797 && left.ino == right.ino
2798 && left.mode == right.mode
2799 && left.uid == right.uid
2800 && left.gid == right.gid
2801 && left.size == right.size
2802 && left.oid == right.oid
2803 && (left.flags & ONDISK_FLAGS) == (right.flags & ONDISK_FLAGS)
2804 && left.flags_extended == right.flags_extended
2805}
2806
2807pub(crate) fn write_index_with_entry_size_overrides(
2808 format: ObjectFormat,
2809 index: &Index,
2810 zero_size_entries: &[usize],
2811 extensions: &[u8],
2812) -> Result<Vec<u8>> {
2813 if !(2..=4).contains(&index.version) {
2814 return Err(GitError::Unsupported(
2815 "canonical writer currently emits index v2/v3/v4".into(),
2816 ));
2817 }
2818 let mut out = Vec::new();
2819 out.extend_from_slice(b"DIRC");
2820 out.extend_from_slice(&index.version.to_be_bytes());
2821 out.extend_from_slice(&(index.entries.len() as u32).to_be_bytes());
2822 let mut previous_path = Vec::new();
2823 for (position, entry) in index.entries.iter().enumerate() {
2824 let start = out.len();
2825 out.extend_from_slice(&entry.ctime_seconds.to_be_bytes());
2826 out.extend_from_slice(&entry.ctime_nanoseconds.to_be_bytes());
2827 out.extend_from_slice(&entry.mtime_seconds.to_be_bytes());
2828 out.extend_from_slice(&entry.mtime_nanoseconds.to_be_bytes());
2829 out.extend_from_slice(&entry.dev.to_be_bytes());
2830 out.extend_from_slice(&entry.ino.to_be_bytes());
2831 out.extend_from_slice(&entry.mode.to_be_bytes());
2832 out.extend_from_slice(&entry.uid.to_be_bytes());
2833 out.extend_from_slice(&entry.gid.to_be_bytes());
2834 let size = if zero_size_entries.binary_search(&position).is_ok() {
2835 0
2836 } else {
2837 entry.size
2838 };
2839 out.extend_from_slice(&size.to_be_bytes());
2840 if entry.oid.format() != format {
2841 return Err(GitError::Unsupported(format!(
2842 "index writer expects {} ids",
2843 format.name()
2844 )));
2845 }
2846 out.extend_from_slice(entry.oid.as_bytes());
2847 let has_extended_flags =
2848 entry.flags & INDEX_FLAG_EXTENDED != 0 || entry.flags_extended != 0;
2849 if has_extended_flags && index.version < 3 {
2850 return Err(GitError::Unsupported(
2851 "index extended flags require version 3".into(),
2852 ));
2853 }
2854 let flags = if has_extended_flags {
2855 entry.flags | INDEX_FLAG_EXTENDED
2856 } else {
2857 entry.flags & !INDEX_FLAG_EXTENDED
2858 };
2859 out.extend_from_slice(&flags.to_be_bytes());
2860 if has_extended_flags {
2861 out.extend_from_slice(&entry.flags_extended.to_be_bytes());
2862 }
2863 if index.version == 4 {
2864 let common_prefix_len = common_prefix_len(&previous_path, entry.path.as_bytes());
2865 let strip_len = previous_path.len() - common_prefix_len;
2866 encode_index_v4_path_strip_len(strip_len, &mut out);
2867 out.extend_from_slice(&entry.path.as_bytes()[common_prefix_len..]);
2868 out.push(0);
2869 previous_path = entry.path.as_bytes().to_vec();
2870 } else {
2871 out.extend_from_slice(entry.path.as_bytes());
2872 out.push(0);
2873 while (out.len() - start) % 8 != 0 {
2874 out.push(0);
2875 }
2876 }
2877 }
2878 out.extend_from_slice(extensions);
2879 let checksum = sley_core::digest_bytes(format, &out)?;
2880 out.extend_from_slice(checksum.as_bytes());
2881 Ok(out)
2882}
2883
2884pub(crate) fn encode_index_v4_path_strip_len(strip_len: usize, out: &mut Vec<u8>) {
2885 let mut bytes = Vec::new();
2886 bytes.push((strip_len & 0x7f) as u8);
2887 let mut value = strip_len >> 7;
2888 while value != 0 {
2889 value -= 1;
2890 bytes.push(((value & 0x7f) as u8) | 0x80);
2891 value >>= 7;
2892 }
2893 for byte in bytes.iter().rev() {
2894 out.push(*byte);
2895 }
2896}
2897
2898pub(crate) fn common_prefix_len(left: &[u8], right: &[u8]) -> usize {
2899 left.iter()
2900 .zip(right.iter())
2901 .take_while(|(left, right)| left == right)
2902 .count()
2903}
2904
2905pub(crate) fn index_checksum_from_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<ObjectId> {
2906 let hash_len = format.raw_len();
2907 if bytes.len() < hash_len {
2908 return Err(GitError::InvalidFormat(
2909 "index too short for checksum".into(),
2910 ));
2911 }
2912 ObjectId::from_raw(format, &bytes[bytes.len() - hash_len..])
2913}
2914
2915pub fn enable_split_index(
2916 git_dir: impl AsRef<Path>,
2917 format: ObjectFormat,
2918) -> Result<UpdateIndexResult> {
2919 let git_dir = git_dir.as_ref();
2920 let mut index = read_repository_index(git_dir, format)?.unwrap_or_else(empty_index);
2921 normalize_index_version_for_extended_flags(&mut index);
2922 write_repository_index_ref_with_split(git_dir, format, &index, true)?;
2923 Ok(UpdateIndexResult {
2924 entries: index.entries.len(),
2925 updated: Vec::new(),
2926 })
2927}
2928
2929pub fn disable_split_index(
2930 git_dir: impl AsRef<Path>,
2931 format: ObjectFormat,
2932) -> Result<UpdateIndexResult> {
2933 let git_dir = git_dir.as_ref();
2934 if !repository_index_path(git_dir).exists() {
2935 return Ok(UpdateIndexResult {
2936 entries: 0,
2937 updated: Vec::new(),
2938 });
2939 }
2940 let mut index = read_repository_index(git_dir, format)?.unwrap_or_else(empty_index);
2941 normalize_index_version_for_extended_flags(&mut index);
2942 write_repository_index_ref_with_split(git_dir, format, &index, false)?;
2943 Ok(UpdateIndexResult {
2944 entries: index.entries.len(),
2945 updated: Vec::new(),
2946 })
2947}
2948
2949pub(crate) fn smudge_racily_clean_entries_before_write(
2950 git_dir: &Path,
2951 format: ObjectFormat,
2952 index: &mut Index,
2953) -> Result<()> {
2954 for position in racily_clean_entry_indexes_before_write(git_dir, format, index)? {
2955 index.entries[position].size = 0;
2956 }
2957 Ok(())
2958}
2959
2960pub(crate) fn racily_clean_entry_indexes_before_write(
2961 git_dir: &Path,
2962 format: ObjectFormat,
2963 index: &Index,
2964) -> Result<Vec<usize>> {
2965 let index_path = repository_index_path(git_dir);
2966 let Some(index_mtime) = fs::metadata(&index_path)
2967 .ok()
2968 .and_then(|metadata| sley_index::file_mtime_parts(&metadata))
2969 else {
2970 return Ok(Vec::new());
2971 };
2972 if index_mtime == (0, 0) {
2973 return Ok(Vec::new());
2974 }
2975 let Some(worktree_root) = (match worktree_root_for_git_dir(git_dir) {
2976 Ok(worktree_root) => worktree_root,
2977 Err(_) => return Ok(Vec::new()),
2978 }) else {
2979 return Ok(Vec::new());
2980 };
2981 let mut smudged = Vec::new();
2982 for (position, entry) in index.entries.iter().enumerate() {
2983 if index_entry_stage(entry) != 0 || sley_index::is_gitlink(entry.mode) {
2984 continue;
2985 }
2986 let entry_mtime = (
2987 u64::from(entry.mtime_seconds),
2988 u64::from(entry.mtime_nanoseconds),
2989 );
2990 if entry_mtime == (0, 0) || index_mtime > entry_mtime {
2991 continue;
2992 }
2993 let absolute = worktree_root.join(repo_path_to_os_path(entry.path.as_bytes())?);
2994 let Ok(metadata) = fs::symlink_metadata(&absolute) else {
2995 continue;
2996 };
2997 if entry.mode != worktree_entry_mode(&metadata)
2998 || !worktree_entry_is_uptodate(entry, &metadata)
2999 {
3000 continue;
3001 }
3002 let body = if metadata.file_type().is_symlink() {
3003 symlink_target_bytes(&absolute)?
3004 } else if metadata.is_file() {
3005 fs::read(&absolute)?
3006 } else {
3007 continue;
3008 };
3009 let oid = EncodedObject::new(ObjectType::Blob, body).object_id(format)?;
3010 if oid != entry.oid {
3011 smudged.push(position);
3012 }
3013 }
3014 Ok(smudged)
3015}
3016
3017pub(crate) fn invalidate_untracked_cache_for_git_paths(
3018 index: &mut Index,
3019 format: ObjectFormat,
3020 paths: &[Vec<u8>],
3021) -> Result<()> {
3022 if paths.is_empty() {
3023 return Ok(());
3024 }
3025 let Some(mut cache) = index.untracked_cache(format)? else {
3026 return Ok(());
3027 };
3028 let Some(root) = cache.root.as_mut() else {
3029 return Ok(());
3030 };
3031 for path in paths {
3032 invalidate_untracked_cache_dir_for_path(root, path);
3033 }
3034 index.set_untracked_cache(format, Some(&cache))
3035}
3036
3037pub(crate) fn invalidate_untracked_cache_dir_for_path(root: &mut UntrackedCacheDir, path: &[u8]) {
3038 invalidate_untracked_cache_node(root);
3039 let mut current = root;
3040 let mut components = path.split(|byte| *byte == b'/').peekable();
3041 while let Some(component) = components.next() {
3042 if component.is_empty() || components.peek().is_none() {
3043 break;
3044 }
3045 let Some(child) = current.dirs.iter_mut().find(|dir| dir.name == component) else {
3046 break;
3047 };
3048 invalidate_untracked_cache_node(child);
3049 current = child;
3050 }
3051}
3052
3053pub(crate) fn invalidate_untracked_cache_node(node: &mut UntrackedCacheDir) {
3054 node.valid = false;
3055 node.untracked.clear();
3056}
3057
3058pub fn update_index_cacheinfo(
3059 git_dir: impl AsRef<Path>,
3060 format: ObjectFormat,
3061 entries: &[CacheInfoEntry],
3062 add: bool,
3063 verbose: bool,
3064) -> Result<UpdateIndexResult> {
3065 let git_dir = git_dir.as_ref();
3066 let index_path = repository_index_path(git_dir);
3067 let mut index = if index_path.exists() {
3068 Index::parse(&fs::read(&index_path)?, format)?
3069 } else {
3070 Index {
3071 version: 2,
3072 entries: Vec::new(),
3073 extensions: Vec::new(),
3074 checksum: None,
3075 }
3076 };
3077 let mut updated = Vec::new();
3078 let mut reports: Vec<String> = Vec::new();
3079 let mut untracked_cache_invalidation_paths = Vec::new();
3080 for cacheinfo in entries {
3081 if !add
3082 && !index
3083 .entries
3084 .iter()
3085 .any(|existing| existing.path == cacheinfo.path)
3086 {
3087 let path = String::from_utf8_lossy(&cacheinfo.path);
3088 eprintln!("error: {path}: cannot add to the index - missing --add option?");
3089 eprintln!("fatal: git update-index: --cacheinfo cannot add {path}");
3090 return Err(GitError::Exit(128));
3091 }
3092 let flags = index_flags(cacheinfo.path.len(), cacheinfo.stage);
3093 let entry = IndexEntry {
3094 ctime_seconds: 0,
3095 ctime_nanoseconds: 0,
3096 mtime_seconds: 0,
3097 mtime_nanoseconds: 0,
3098 dev: 0,
3099 ino: 0,
3100 mode: cacheinfo.mode,
3101 uid: 0,
3102 gid: 0,
3103 size: 0,
3104 oid: cacheinfo.oid,
3105 flags,
3106 flags_extended: 0,
3107 path: BString::from(cacheinfo.path.as_slice()),
3108 };
3109 index.entries.retain(|existing| {
3110 existing.path != cacheinfo.path || index_entry_stage(existing) != cacheinfo.stage
3111 });
3112 index.entries.push(entry);
3113 untracked_cache_invalidation_paths.push(cacheinfo.path.clone());
3114 updated.push(cacheinfo.oid);
3115 reports.push(format!(
3118 "add '{}'",
3119 String::from_utf8_lossy(&cacheinfo.path)
3120 ));
3121 }
3122 index
3123 .entries
3124 .sort_by(|left, right| left.path.cmp(&right.path));
3125 let null_entry = index.entries.iter().find(|entry| entry.oid.is_null());
3130 if let Some(entry) = null_entry {
3131 if verbose {
3132 flush_update_index_reports(&reports)?;
3133 }
3134 eprintln!(
3135 "error: cache entry has null sha1: {}",
3136 String::from_utf8_lossy(&entry.path)
3137 );
3138 return Err(GitError::Exit(128));
3139 }
3140 invalidate_untracked_cache_for_git_paths(
3141 &mut index,
3142 format,
3143 &untracked_cache_invalidation_paths,
3144 )?;
3145 write_repository_index_ref(git_dir, format, &index)?;
3146 if verbose {
3147 flush_update_index_reports(&reports)?;
3148 }
3149 Ok(UpdateIndexResult {
3150 entries: index.entries.len(),
3151 updated,
3152 })
3153}
3154
3155pub(crate) fn flush_update_index_reports(reports: &[String]) -> Result<()> {
3156 let mut stdout = std::io::stdout().lock();
3157 for line in reports {
3158 writeln!(stdout, "{line}")?;
3159 }
3160 stdout.flush()?;
3161 Ok(())
3162}
3163
3164pub fn update_index_index_info(
3165 git_dir: impl AsRef<Path>,
3166 format: ObjectFormat,
3167 records: &[IndexInfoRecord],
3168) -> Result<UpdateIndexResult> {
3169 let git_dir = git_dir.as_ref();
3170 let index_path = repository_index_path(git_dir);
3171 let mut index = if index_path.exists() {
3172 Index::parse(&fs::read(&index_path)?, format)?
3173 } else {
3174 Index {
3175 version: 2,
3176 entries: Vec::new(),
3177 extensions: Vec::new(),
3178 checksum: None,
3179 }
3180 };
3181 let mut updated = Vec::new();
3182 let mut untracked_cache_invalidation_paths = Vec::new();
3183 for record in records {
3184 match record {
3185 IndexInfoRecord::Remove { path } => {
3186 index.entries.retain(|existing| existing.path != *path);
3187 untracked_cache_invalidation_paths.push(path.clone());
3188 }
3189 IndexInfoRecord::Add(cacheinfo) => {
3190 let flags = index_flags(cacheinfo.path.len(), cacheinfo.stage);
3191 let entry = IndexEntry {
3192 ctime_seconds: 0,
3193 ctime_nanoseconds: 0,
3194 mtime_seconds: 0,
3195 mtime_nanoseconds: 0,
3196 dev: 0,
3197 ino: 0,
3198 mode: cacheinfo.mode,
3199 uid: 0,
3200 gid: 0,
3201 size: 0,
3202 oid: cacheinfo.oid,
3203 flags,
3204 flags_extended: 0,
3205 path: BString::from(cacheinfo.path.as_slice()),
3206 };
3207 if cacheinfo.stage == 0 {
3208 index
3209 .entries
3210 .retain(|existing| existing.path != cacheinfo.path);
3211 } else {
3212 index.entries.retain(|existing| {
3213 existing.path != cacheinfo.path
3214 || index_entry_stage(existing) != cacheinfo.stage
3215 });
3216 }
3217 index.entries.push(entry);
3218 untracked_cache_invalidation_paths.push(cacheinfo.path.clone());
3219 updated.push(cacheinfo.oid);
3220 }
3221 }
3222 }
3223 index.entries.sort_by(|left, right| {
3224 left.path
3225 .cmp(&right.path)
3226 .then_with(|| index_entry_stage(left).cmp(&index_entry_stage(right)))
3227 });
3228 invalidate_untracked_cache_for_git_paths(
3229 &mut index,
3230 format,
3231 &untracked_cache_invalidation_paths,
3232 )?;
3233 write_repository_index_ref(git_dir, format, &index)?;
3234 Ok(UpdateIndexResult {
3235 entries: index.entries.len(),
3236 updated,
3237 })
3238}
3239
3240pub(crate) fn index_flags(path_len: usize, stage: u16) -> u16 {
3241 ((stage & 0x3) << 12) | ((path_len.min(0xfff) as u16) & 0x0fff)
3242}
3243
3244pub(crate) const INDEX_FLAG_ASSUME_UNCHANGED: u16 = 0x8000;
3245pub(crate) const INDEX_FLAG_EXTENDED: u16 = 0x4000;
3246pub(crate) const INDEX_EXTENDED_FLAG_SKIP_WORKTREE: u16 = 0x4000;
3247
3248pub(crate) fn normalize_index_version_for_extended_flags(index: &mut Index) {
3249 let has_extended_flags = index
3250 .entries
3251 .iter()
3252 .any(|entry| entry.flags & INDEX_FLAG_EXTENDED != 0 || entry.flags_extended != 0);
3253 if has_extended_flags && index.version < 3 {
3254 index.version = 3;
3255 } else if !has_extended_flags && index.version == 3 {
3256 index.version = 2;
3257 }
3258}
3259
3260pub(crate) fn index_entry_stage(entry: &IndexEntry) -> u16 {
3261 (entry.flags >> 12) & 0x3
3262}
3263
3264pub(crate) fn stage0_oid_in_range(
3267 entries: &[IndexEntry],
3268 range: std::ops::Range<usize>,
3269) -> Option<ObjectId> {
3270 entries[range]
3271 .iter()
3272 .find(|entry| index_entry_stage(entry) == 0)
3273 .map(|entry| entry.oid)
3274}
3275
3276pub(crate) fn index_entry_skip_worktree(entry: &IndexEntry) -> bool {
3277 entry.flags & INDEX_FLAG_EXTENDED != 0
3278 && entry.flags_extended & INDEX_EXTENDED_FLAG_SKIP_WORKTREE != 0
3279}
3280
3281pub(crate) fn print_update_index_path_error(path: &[u8], message: &str) {
3282 let path = String::from_utf8_lossy(path);
3283 eprintln!("error: {path}: {message}");
3284 eprintln!("fatal: Unable to process path {path}");
3285}
3286
3287pub(crate) fn print_update_index_needs_update(path: &[u8]) {
3288 let path = String::from_utf8_lossy(path);
3289 println!("{path}: needs update");
3290}
3291
3292pub fn write_tree_from_index(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Result<ObjectId> {
3293 write_tree_from_index_with_options(git_dir, format, WriteTreeOptions::default())
3294}
3295
3296pub fn write_tree_from_index_with_odb(
3297 git_dir: impl AsRef<Path>,
3298 format: ObjectFormat,
3299 odb: &FileObjectDatabase,
3300) -> Result<ObjectId> {
3301 write_tree_from_index_with_options_and_odb(
3302 git_dir.as_ref(),
3303 format,
3304 WriteTreeOptions::default(),
3305 odb,
3306 )
3307}
3308
3309pub fn write_tree_from_index_with_options(
3310 git_dir: impl AsRef<Path>,
3311 format: ObjectFormat,
3312 options: WriteTreeOptions,
3313) -> Result<ObjectId> {
3314 let git_dir = git_dir.as_ref();
3315 let odb = FileObjectDatabase::from_git_dir(git_dir, format);
3316 write_tree_from_index_with_options_and_odb(git_dir, format, options, &odb)
3317}
3318
3319pub(crate) fn write_tree_from_index_with_options_and_odb(
3320 git_dir: &Path,
3321 format: ObjectFormat,
3322 options: WriteTreeOptions,
3323 odb: &FileObjectDatabase,
3324) -> Result<ObjectId> {
3325 let index_path = repository_index_path(git_dir);
3326 let index_bytes = match fs::read(&index_path) {
3330 Ok(bytes) => bytes,
3331 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
3332 let mut checker = odb.presence_checker();
3333 let empty: &[WriteTreeEntry<'_>] = &[];
3334 return write_tree_entries_stream(
3335 empty,
3336 b"",
3337 None,
3338 odb,
3339 &mut checker,
3340 options.missing_ok,
3341 );
3342 }
3343 Err(err) => return Err(err.into()),
3344 };
3345 let mut checker = odb.presence_checker();
3346 if Index::bytes_have_extension(&index_bytes, format, b"link")? {
3347 let index = sley_index::read_repository_index(git_dir, format)?;
3348 return write_tree_from_owned_index(&index, format, &options, odb, &mut checker);
3349 }
3350 match BorrowedIndex::parse(&index_bytes, format) {
3351 Ok(index) => write_tree_from_borrowed_index(&index, format, &options, odb, &mut checker),
3352 Err(GitError::Unsupported(_)) => {
3353 let index = Index::parse(&index_bytes, format)?;
3354 write_tree_from_owned_index(&index, format, &options, odb, &mut checker)
3355 }
3356 Err(err) => Err(err),
3357 }
3358}
3359
3360pub(crate) fn write_tree_from_borrowed_index(
3361 index: &BorrowedIndex<'_>,
3362 format: ObjectFormat,
3363 options: &WriteTreeOptions,
3364 odb: &FileObjectDatabase,
3365 checker: &mut ObjectPresenceChecker,
3366) -> Result<ObjectId> {
3367 let cache_tree = if options.prefix.is_none() {
3368 index.cache_tree(format).ok().flatten()
3369 } else {
3370 None
3371 };
3372 if options.prefix.is_none() && !index.entries.iter().any(|entry| entry.is_intent_to_add()) {
3373 return write_tree_entries_stream(
3374 &index.entries,
3375 b"",
3376 cache_tree.as_ref(),
3377 odb,
3378 checker,
3379 options.missing_ok,
3380 );
3381 }
3382 let entries = write_tree_entries_for_prefix(
3387 index
3388 .entries
3389 .iter()
3390 .filter(|entry| !entry.is_intent_to_add()),
3391 options.prefix.as_deref(),
3392 )?;
3393 write_tree_entries_stream(
3394 &entries,
3395 b"",
3396 cache_tree.as_ref(),
3397 odb,
3398 checker,
3399 options.missing_ok,
3400 )
3401}
3402
3403pub(crate) fn write_tree_from_owned_index(
3404 index: &Index,
3405 format: ObjectFormat,
3406 options: &WriteTreeOptions,
3407 odb: &FileObjectDatabase,
3408 checker: &mut ObjectPresenceChecker,
3409) -> Result<ObjectId> {
3410 let cache_tree = if options.prefix.is_none() {
3411 index.cache_tree(format).ok().flatten()
3412 } else {
3413 None
3414 };
3415 if options.prefix.is_none() && !index.entries.iter().any(|entry| entry.is_intent_to_add()) {
3416 return write_tree_entries_stream(
3417 &index.entries,
3418 b"",
3419 cache_tree.as_ref(),
3420 odb,
3421 checker,
3422 options.missing_ok,
3423 );
3424 }
3425 let entries = write_tree_entries_for_prefix(
3426 index
3427 .entries
3428 .iter()
3429 .filter(|entry| !entry.is_intent_to_add()),
3430 options.prefix.as_deref(),
3431 )?;
3432 write_tree_entries_stream(
3433 &entries,
3434 b"",
3435 cache_tree.as_ref(),
3436 odb,
3437 checker,
3438 options.missing_ok,
3439 )
3440}
3441
3442#[derive(Clone, Copy)]
3443pub(crate) struct WriteTreeEntry<'a> {
3444 pub(crate) path: &'a [u8],
3445 pub(crate) mode: u32,
3446 pub(crate) oid: ObjectId,
3447}
3448
3449pub(crate) trait WriteTreeIndexEntry {
3450 fn write_tree_path(&self) -> &[u8];
3451 fn write_tree_mode(&self) -> u32;
3452 fn write_tree_oid(&self) -> ObjectId;
3453}
3454
3455impl WriteTreeIndexEntry for IndexEntry {
3456 fn write_tree_path(&self) -> &[u8] {
3457 self.path.as_bytes()
3458 }
3459
3460 fn write_tree_mode(&self) -> u32 {
3461 self.mode
3462 }
3463
3464 fn write_tree_oid(&self) -> ObjectId {
3465 self.oid
3466 }
3467}
3468
3469impl WriteTreeIndexEntry for IndexEntryRef<'_> {
3470 fn write_tree_path(&self) -> &[u8] {
3471 self.path
3472 }
3473
3474 fn write_tree_mode(&self) -> u32 {
3475 self.mode
3476 }
3477
3478 fn write_tree_oid(&self) -> ObjectId {
3479 self.oid
3480 }
3481}
3482
3483impl WriteTreeIndexEntry for WriteTreeEntry<'_> {
3484 fn write_tree_path(&self) -> &[u8] {
3485 self.path
3486 }
3487
3488 fn write_tree_mode(&self) -> u32 {
3489 self.mode
3490 }
3491
3492 fn write_tree_oid(&self) -> ObjectId {
3493 self.oid
3494 }
3495}
3496
3497pub(crate) fn write_tree_entries_for_prefix<'a, E>(
3498 entries: impl IntoIterator<Item = &'a E>,
3499 prefix: Option<&[u8]>,
3500) -> Result<Vec<WriteTreeEntry<'a>>>
3501where
3502 E: WriteTreeIndexEntry + 'a,
3503{
3504 let Some(prefix) = prefix else {
3505 return Ok(entries
3506 .into_iter()
3507 .map(|entry| WriteTreeEntry {
3508 path: entry.write_tree_path(),
3509 mode: entry.write_tree_mode(),
3510 oid: entry.write_tree_oid(),
3511 })
3512 .collect());
3513 };
3514 let trimmed_len = prefix
3515 .iter()
3516 .rposition(|byte| *byte != b'/')
3517 .map(|idx| idx + 1)
3518 .unwrap_or(0);
3519 let trimmed = &prefix[..trimmed_len];
3520 if trimmed.is_empty() {
3521 return Ok(entries
3522 .into_iter()
3523 .map(|entry| WriteTreeEntry {
3524 path: entry.write_tree_path(),
3525 mode: entry.write_tree_mode(),
3526 oid: entry.write_tree_oid(),
3527 })
3528 .collect());
3529 }
3530 let mut prefixed = Vec::new();
3531 for entry in entries {
3532 let Some(remainder) = entry.write_tree_path().strip_prefix(trimmed) else {
3533 continue;
3534 };
3535 let Some(stripped) = remainder.strip_prefix(b"/") else {
3536 continue;
3537 };
3538 if stripped.is_empty() {
3539 continue;
3540 }
3541 prefixed.push(WriteTreeEntry {
3542 path: stripped,
3543 mode: entry.write_tree_mode(),
3544 oid: entry.write_tree_oid(),
3545 });
3546 }
3547 if prefixed.is_empty() {
3548 eprintln!(
3549 "fatal: git-write-tree: prefix {} not found",
3550 String::from_utf8_lossy(prefix)
3551 );
3552 return Err(GitError::Exit(128));
3553 }
3554 Ok(prefixed)
3555}
3556
3557pub(crate) fn write_tree_entries_stream<E>(
3558 entries: &[E],
3559 prefix: &[u8],
3560 cache_tree: Option<&CacheTree>,
3561 odb: &FileObjectDatabase,
3562 checker: &mut ObjectPresenceChecker,
3563 missing_ok: bool,
3564) -> Result<ObjectId>
3565where
3566 E: WriteTreeIndexEntry,
3567{
3568 if let Some(oid) = valid_cache_tree_oid(cache_tree, entries.len()) {
3569 return Ok(oid);
3570 }
3571
3572 let mut tree_entries = Vec::new();
3573 let mut index = 0usize;
3574 while index < entries.len() {
3575 let entry = &entries[index];
3576 let path = entry.write_tree_path();
3577 let Some(remainder) = path.strip_prefix(prefix) else {
3578 return Err(GitError::InvalidPath(format!(
3579 "invalid index path {}",
3580 String::from_utf8_lossy(path)
3581 )));
3582 };
3583 if remainder.is_empty() || remainder[0] == b'/' {
3584 return Err(GitError::InvalidPath(format!(
3585 "invalid index path {}",
3586 String::from_utf8_lossy(path)
3587 )));
3588 }
3589
3590 if entry.write_tree_mode() == SPARSE_DIR_MODE
3591 && let Some(name) = remainder.strip_suffix(b"/")
3592 && !name.is_empty()
3593 && !name.contains(&b'/')
3594 {
3595 let oid = entry.write_tree_oid();
3596 if !missing_ok && !checker.contains(&oid)? {
3597 eprintln!(
3598 "error: invalid object {:o} {} for '{}'",
3599 SPARSE_DIR_MODE,
3600 oid,
3601 String::from_utf8_lossy(path)
3602 );
3603 eprintln!("fatal: git-write-tree: error building trees");
3604 return Err(GitError::Exit(128));
3605 }
3606 tree_entries.push(TreeEntry {
3607 mode: SPARSE_DIR_MODE,
3608 name: BString::from(name),
3609 oid,
3610 });
3611 index += 1;
3612 continue;
3613 }
3614
3615 if let Some(slash) = remainder.iter().position(|byte| *byte == b'/') {
3616 let name = &remainder[..slash];
3617 if name.is_empty() {
3618 return Err(GitError::InvalidPath(format!(
3619 "invalid index path {}",
3620 String::from_utf8_lossy(path)
3621 )));
3622 }
3623 let start = index;
3624 let child_cache = cache_tree.and_then(|tree| {
3625 tree.subtrees
3626 .iter()
3627 .find(|child| child.name.as_slice() == name)
3628 .map(|child| &child.tree)
3629 });
3630 if let Some(cached_count) = valid_cache_tree_entry_count(child_cache) {
3631 let end = start.saturating_add(cached_count);
3632 if cached_count > 0
3633 && end <= entries.len()
3634 && same_tree_component(entries[end - 1].write_tree_path(), prefix, name)?
3635 && (end == entries.len()
3636 || !same_tree_component(entries[end].write_tree_path(), prefix, name)?)
3637 {
3638 index = end;
3639 } else {
3640 index += 1;
3641 while index < entries.len()
3642 && same_tree_component(entries[index].write_tree_path(), prefix, name)?
3643 {
3644 index += 1;
3645 }
3646 }
3647 } else {
3648 index += 1;
3649 while index < entries.len()
3650 && same_tree_component(entries[index].write_tree_path(), prefix, name)?
3651 {
3652 index += 1;
3653 }
3654 }
3655 if let Some(oid) = valid_cache_tree_oid(child_cache, index - start) {
3656 tree_entries.push(TreeEntry {
3657 mode: 0o040000,
3658 name: BString::from(name),
3659 oid,
3660 });
3661 continue;
3662 }
3663 let mut child_prefix = Vec::with_capacity(prefix.len() + name.len() + 1);
3664 child_prefix.extend_from_slice(prefix);
3665 child_prefix.extend_from_slice(name);
3666 child_prefix.push(b'/');
3667 let oid = write_tree_entries_stream(
3668 &entries[start..index],
3669 &child_prefix,
3670 child_cache,
3671 odb,
3672 checker,
3673 missing_ok,
3674 )?;
3675 tree_entries.push(TreeEntry {
3676 mode: 0o040000,
3677 name: BString::from(name),
3678 oid,
3679 });
3680 continue;
3681 }
3682
3683 let mode = entry.write_tree_mode();
3684 let oid = entry.write_tree_oid();
3685 if !missing_ok && !sley_index::is_gitlink(mode) && !checker.contains(&oid)? {
3686 eprintln!(
3687 "error: invalid object {:o} {} for '{}'",
3688 mode,
3689 oid,
3690 String::from_utf8_lossy(path)
3691 );
3692 eprintln!("fatal: git-write-tree: error building trees");
3693 return Err(GitError::Exit(128));
3694 }
3695 tree_entries.push(TreeEntry {
3696 mode,
3697 name: BString::from(remainder),
3698 oid,
3699 });
3700 index += 1;
3701 }
3702
3703 tree_entries.sort_by(|left, right| {
3704 git_tree_entry_cmp(
3705 left.name.as_bytes(),
3706 left.mode,
3707 right.name.as_bytes(),
3708 right.mode,
3709 )
3710 });
3711 odb.write_object(EncodedObject::new(
3712 ObjectType::Tree,
3713 Tree {
3714 entries: tree_entries,
3715 }
3716 .write(),
3717 ))
3718}
3719
3720pub(crate) fn valid_cache_tree_oid(
3721 tree: Option<&CacheTree>,
3722 entry_count: usize,
3723) -> Option<ObjectId> {
3724 let tree = tree?;
3725 if valid_cache_tree_entry_count(Some(tree))? != entry_count {
3726 return None;
3727 }
3728 tree.oid
3729}
3730
3731pub(crate) fn valid_cache_tree_entry_count(tree: Option<&CacheTree>) -> Option<usize> {
3732 let tree = tree?;
3733 if tree.entry_count < 0 || tree.oid.is_none() {
3734 return None;
3735 }
3736 Some(tree.entry_count as usize)
3737}
3738
3739pub(crate) fn same_tree_component(path: &[u8], prefix: &[u8], name: &[u8]) -> Result<bool> {
3740 let Some(remainder) = path.strip_prefix(prefix) else {
3741 return Err(GitError::InvalidPath(format!(
3742 "invalid index path {}",
3743 String::from_utf8_lossy(path)
3744 )));
3745 };
3746 Ok(remainder.starts_with(name) && remainder.get(name.len()) == Some(&b'/'))
3747}