1use std::collections::BTreeMap;
4
5use crate::error::{Error, Result};
6use crate::index::{
7 CacheTreeNode, Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR, MODE_SYMLINK,
8 MODE_TREE,
9};
10use crate::objects::{
11 oid_hash_set_with_capacity, parse_tree, serialize_tree, tree_entry_cmp, ObjectId, ObjectKind,
12 OidHashSet, TreeEntry,
13};
14use crate::odb::{Odb, WriteOptions};
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum WriteTreePersistence {
19 #[default]
21 Write,
22 DryRun,
24 Repair,
26}
27
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub struct WriteTreeFlags {
31 pub missing_ok: bool,
33 pub ignore_cache_tree: bool,
35 pub persistence: WriteTreePersistence,
37 pub silent: bool,
39}
40
41impl WriteTreeFlags {
42 #[must_use]
45 pub fn silent() -> Self {
46 Self {
47 silent: true,
48 ..Self::default()
49 }
50 }
51}
52
53#[must_use]
58pub fn index_cache_tree_inputs_match(left: &Index, right: &Index) -> bool {
59 let rows_left = cache_tree_index_rows(left);
60 let rows_right = cache_tree_index_rows(right);
61 if rows_left.len() != rows_right.len() {
62 return false;
63 }
64 rows_left.iter().zip(rows_right.iter()).all(|(a, b)| {
65 a.path == b.path
66 && a.mode == b.mode
67 && a.oid == b.oid
68 && a.intent_to_add() == b.intent_to_add()
69 && a.is_sparse_directory_placeholder() == b.is_sparse_directory_placeholder()
70 })
71}
72
73#[must_use]
78pub fn cache_tree_extension_valid(node: Option<&CacheTreeNode>) -> bool {
79 let Some(it) = node else {
80 return false;
81 };
82 if !it.is_valid() {
83 return false;
84 }
85 it.children
86 .iter()
87 .all(|child| cache_tree_extension_valid(Some(child)))
88}
89
90#[must_use]
101pub fn cache_tree_fully_valid(odb: &Odb, node: Option<&CacheTreeNode>) -> bool {
102 let Some(root) = node else {
103 return false;
104 };
105 let mut oids = Vec::new();
106 if !collect_reusable_tree_ids(root, &mut oids) {
107 return false;
108 }
109 odb.contains_many(&oids)
110 .is_ok_and(|present| present.into_iter().all(|p| p))
111}
112
113fn collect_reusable_tree_ids(node: &CacheTreeNode, out: &mut Vec<ObjectId>) -> bool {
116 let children_valid = node.children.iter().fold(true, |all, child| {
117 collect_reusable_tree_ids(child, out) & all
118 });
119 let Some(oid) = node.oid.filter(|_| children_valid && node.is_valid()) else {
120 return false;
121 };
122 out.push(oid);
123 true
124}
125
126fn present_reusable_trees(odb: &Odb, root: &CacheTreeNode) -> OidHashSet {
133 let mut oids = Vec::new();
134 collect_reusable_tree_ids(root, &mut oids);
135 let present = odb.contains_many(&oids).unwrap_or_default();
136 let mut reusable = oid_hash_set_with_capacity(oids.len());
137 reusable.extend(
138 oids.into_iter()
139 .zip(present)
140 .filter_map(|(oid, has)| has.then_some(oid)),
141 );
142 reusable
143}
144
145pub fn cache_tree_update(odb: &Odb, index: &mut Index, flags: WriteTreeFlags) -> Result<()> {
153 verify_index_for_cache_tree(index, flags.silent)?;
154
155 if flags.ignore_cache_tree {
156 index.clear_cache_tree();
157 }
158
159 let mut root = index
160 .cache_tree
161 .take()
162 .unwrap_or_else(empty_cache_tree_root);
163
164 let rows: Vec<&IndexEntry> = cache_tree_index_rows(index);
165 let ctx = CacheTreeRebuild {
166 odb,
167 flags,
168 reusable: present_reusable_trees(odb, &root),
169 };
170 let mut skipped = 0i32;
171 let (consumed, _) = rebuild_directory_node(&ctx, &mut root, &rows, b"", 0, &mut skipped)?;
172 if consumed < 0 {
173 return Err(Error::IndexError("cache-tree update failed".to_owned()));
174 }
175 index.cache_tree_root = root.oid.filter(|_| root.entry_count >= 0);
176 index.cache_tree = Some(root);
177 Ok(())
178}
179
180pub fn write_tree_update_index(
185 odb: &Odb,
186 index: &mut Index,
187 prefix: &str,
188 flags: WriteTreeFlags,
189) -> Result<ObjectId> {
190 ensure_empty_blob_for_intent_to_add(odb, index)?;
191
192 let was_valid =
193 !flags.ignore_cache_tree && cache_tree_fully_valid(odb, index.cache_tree.as_ref());
194
195 if !was_valid {
196 cache_tree_update(odb, index, flags)?;
197 }
198
199 if prefix.is_empty() {
200 let root = index.cache_tree.as_ref().ok_or_else(|| {
201 Error::IndexError("write-tree: missing cache-tree after update".to_owned())
202 })?;
203 let oid = root.oid.ok_or_else(|| {
204 Error::IndexError("write-tree: cache-tree root has no oid".to_owned())
205 })?;
206 return Ok(oid);
207 }
208
209 let subtree = find_cache_tree_subtree(index.cache_tree.as_ref(), prefix.as_bytes())
210 .ok_or_else(|| Error::IndexError(format!("write-tree: invalid prefix '{prefix}'")))?;
211 let oid = subtree
212 .oid
213 .ok_or_else(|| Error::IndexError(format!("write-tree: invalid prefix '{prefix}'")))?;
214 Ok(oid)
215}
216
217fn empty_cache_tree_root() -> CacheTreeNode {
218 CacheTreeNode {
219 name: Vec::new(),
220 entry_count: -1,
221 oid: None,
222 children: Vec::new(),
223 }
224}
225
226fn cache_tree_index_rows(index: &Index) -> Vec<&IndexEntry> {
228 index
229 .entries
230 .iter()
231 .filter(|e| e.stage() == 0 && (e.mode != MODE_TREE || e.is_sparse_directory_placeholder()))
232 .collect()
233}
234
235fn verify_index_for_cache_tree(index: &Index, _silent: bool) -> Result<()> {
236 if index.entries.iter().any(|e| e.stage() != 0) {
237 return Err(Error::IndexUnmerged);
238 }
239 let rows = cache_tree_index_rows(index);
240 for pair in rows.windows(2) {
241 let shorter = &pair[0].path;
242 let longer = &pair[1].path;
243 if longer.len() > shorter.len()
244 && longer.get(shorter.len()) == Some(&b'/')
245 && longer.starts_with(shorter.as_slice())
246 {
247 return Err(Error::IndexPathPrefixConflict {
248 directory: path_buf_from_bytes(shorter),
249 file: path_buf_from_bytes(longer),
250 });
251 }
252 }
253 Ok(())
254}
255
256fn path_buf_from_bytes(bytes: &[u8]) -> std::path::PathBuf {
257 std::path::PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
258}
259
260fn find_cache_tree_subtree<'a>(
261 root: Option<&'a CacheTreeNode>,
262 prefix: &[u8],
263) -> Option<&'a CacheTreeNode> {
264 let mut it = root?;
265 let mut path = prefix;
266 while !path.is_empty() {
267 if path[0] == b'/' {
268 path = &path[1..];
269 continue;
270 }
271 let slash = path.iter().position(|&b| b == b'/').unwrap_or(path.len());
272 let component = &path[..slash];
273 path = &path[slash..];
274 it = it
275 .children
276 .iter()
277 .find(|c| c.name.as_slice() == component)?;
278 }
279 Some(it)
280}
281
282fn find_or_create_child<'a>(
283 node: &'a mut CacheTreeNode,
284 name: &[u8],
285 create: bool,
286) -> Option<&'a mut CacheTreeNode> {
287 if let Some(pos) = node.children.iter().position(|c| c.name.as_slice() == name) {
288 return Some(&mut node.children[pos]);
289 }
290 if !create {
291 return None;
292 }
293 node.children.push(CacheTreeNode {
294 name: name.to_vec(),
295 entry_count: -1,
296 oid: None,
297 children: Vec::new(),
298 });
299 node.children.last_mut()
300}
301
302struct CacheTreeRebuild<'a> {
305 odb: &'a Odb,
306 flags: WriteTreeFlags,
307 reusable: OidHashSet,
309}
310
311fn rebuild_directory_node(
317 ctx: &CacheTreeRebuild<'_>,
318 node: &mut CacheTreeNode,
319 rows: &[&IndexEntry],
320 anchor: &[u8],
321 prefix_len: usize,
322 extra_skip: &mut i32,
323) -> Result<(i32, bool)> {
324 use std::collections::HashMap;
325
326 let odb = ctx.odb;
327 let flags = ctx.flags;
328 *extra_skip = 0;
329
330 if let Some(head) = rows.first() {
331 if head.is_sparse_directory_placeholder()
332 && head.path.len() == prefix_len
333 && entry_matches_prefix(&head.path, anchor, prefix_len)
334 {
335 node.entry_count = 1;
336 node.oid = Some(head.oid);
337 return Ok((1, false));
338 }
339 }
340
341 if subtree_reusable(node, &ctx.reusable) {
342 return Ok((node.entry_count, true));
343 }
344
345 let mut nested_sizes: HashMap<Vec<u8>, (i32, bool)> = HashMap::new();
346 let mut direct_tree_names: HashMap<Vec<u8>, ()> = HashMap::new();
347 let mut pos = 0usize;
348 while pos < rows.len() {
349 let row = rows[pos];
350 let path = row.path.as_slice();
351 if !entry_matches_prefix(path, anchor, prefix_len) {
352 break;
353 }
354
355 let tail = path_strip_prefix(path, prefix_len);
356 let Some(component_end) = tail.iter().position(|&b| b == b'/') else {
357 pos += 1;
358 continue;
359 };
360 let child_name = &tail[..component_end];
361 let child = find_or_create_child(node, child_name, true).ok_or_else(|| {
362 Error::IndexError(format!(
363 "failed to attach cache-tree child '{}'",
364 String::from_utf8_lossy(child_name)
365 ))
366 })?;
367 let nested_len = prefix_len + component_end + 1;
368 let mut nested_extra = 0i32;
369 let (covered, known_present) = rebuild_directory_node(
370 ctx,
371 child,
372 &rows[pos..],
373 path,
374 nested_len,
375 &mut nested_extra,
376 )?;
377 if covered <= 0 {
378 return Ok((covered, false));
379 }
380 nested_sizes.insert(child_name.to_vec(), (covered, known_present));
381 pos += covered as usize;
382 *extra_skip += nested_extra;
383 }
384
385 node.children.retain(|child| {
386 nested_sizes.contains_key(&child.name) || direct_tree_names.contains_key(&child.name)
387 });
388
389 let write_opts = WriteOptions {
390 silent: flags.silent,
391 ..WriteOptions::default()
392 };
393
394 let mut members: Vec<TreeEntry> = Vec::new();
395 let mut unconfirmed: Vec<(ObjectId, u32, &[u8])> = Vec::new();
397 let mut stale = false;
398 pos = 0;
399 while pos < rows.len() {
400 let row = rows[pos];
401 let path = row.path.as_slice();
402 if !entry_matches_prefix(path, anchor, prefix_len) {
403 break;
404 }
405
406 let tail = path_strip_prefix(path, prefix_len);
407 let slash = tail.iter().position(|&b| b == b'/');
408
409 let (oid, mode, name_len, advance, known_present) = if let Some(off) = slash {
410 let name = &tail[..off];
411 let child = find_or_create_child(node, name, false).ok_or_else(|| {
412 Error::IndexError(format!(
413 "cache-tree child '{}' missing for '{}'",
414 String::from_utf8_lossy(name),
415 String::from_utf8_lossy(path)
416 ))
417 })?;
418 let (covered, known_present) = *nested_sizes
419 .get(name)
420 .ok_or_else(|| Error::IndexError("cache-tree nested size missing".to_owned()))?;
421 let pending = child.entry_count < 0;
422 if pending {
423 stale = true;
424 }
425 let oid = child
426 .oid
427 .ok_or_else(|| Error::IndexError("cache-tree child oid missing".to_owned()))?;
428 if pending && is_empty_tree_oid(odb, &oid) {
429 pos += covered as usize;
430 continue;
431 }
432 (oid, MODE_TREE, off, covered as usize, known_present)
433 } else {
434 if row.intent_to_add() {
435 stale = true;
436 pos += 1;
437 continue;
438 }
439 let mode = if row.is_sparse_directory_placeholder() {
440 let name = tail.to_vec();
441 direct_tree_names.insert(name.clone(), ());
442 if let Some(child) = find_or_create_child(node, &name, true) {
443 child.entry_count = 1;
444 child.oid = Some(row.oid);
445 child.children.clear();
446 }
447 MODE_TREE
448 } else {
449 canonicalize_blob_mode(row.mode)
450 };
451 (row.oid, mode, tail.len(), 1, false)
452 };
453
454 if oid.is_zero() {
455 return Err(missing_member_error(mode, &oid, path));
456 }
457 let gitlink = mode == MODE_GITLINK;
458 let allow_missing = gitlink || flags.missing_ok || (slash.is_none() && row.intent_to_add());
459 if !allow_missing && !known_present {
460 unconfirmed.push((oid, mode, path));
461 }
462
463 members.push(TreeEntry {
464 mode,
465 name: tail[..name_len].to_vec(),
466 oid,
467 });
468 pos += advance;
469 }
470
471 if !unconfirmed.is_empty() {
472 let oids: Vec<ObjectId> = unconfirmed.iter().map(|(oid, _, _)| *oid).collect();
473 let present = odb.contains_many(&oids)?;
474 if let Some(((oid, mode, path), _)) = unconfirmed.iter().zip(present).find(|(_, has)| !has)
475 {
476 return Err(missing_member_error(*mode, oid, path));
477 }
478 }
479
480 members.sort_by(|a, b| {
481 let a_tree = a.mode == MODE_TREE;
482 let b_tree = b.mode == MODE_TREE;
483 tree_entry_cmp(&a.name, a_tree, &b.name, b_tree)
484 });
485
486 let payload = serialize_tree(&members);
487 let (tree_oid, repair_stale) = store_tree_payload(odb, &payload, flags, write_opts)?;
488 stale |= repair_stale;
489 let known_present = match flags.persistence {
490 WriteTreePersistence::Write => true,
491 WriteTreePersistence::Repair => !repair_stale,
492 WriteTreePersistence::DryRun => false,
493 };
494
495 node.oid = Some(tree_oid);
496 node.entry_count = if stale { -1 } else { pos as i32 - *extra_skip };
497 Ok((pos as i32, known_present))
498}
499
500fn missing_member_error(mode: u32, oid: &ObjectId, path: &[u8]) -> Error {
501 Error::ObjectNotFound(format!(
502 "{mode:o} {} at {}",
503 oid.to_hex(),
504 String::from_utf8_lossy(path)
505 ))
506}
507
508fn subtree_reusable(node: &CacheTreeNode, reusable: &OidHashSet) -> bool {
510 node.is_valid()
511 && node.oid.is_some_and(|oid| reusable.contains(&oid))
512 && node
513 .children
514 .iter()
515 .all(|child| subtree_reusable(child, reusable))
516}
517
518fn entry_matches_prefix(path: &[u8], anchor: &[u8], prefix_len: usize) -> bool {
519 path.len() >= prefix_len && path[..prefix_len] == anchor[..prefix_len]
520}
521
522fn path_strip_prefix(path: &[u8], prefix_len: usize) -> &[u8] {
523 path[prefix_len..]
524 .strip_prefix(b"/")
525 .unwrap_or(&path[prefix_len..])
526}
527
528#[cfg(test)]
530pub fn test_reset_tree_write_count(odb: &Odb) {
531 odb.hot_path_metrics().reset_tree_writes();
532}
533
534#[cfg(test)]
536#[must_use]
537pub fn test_tree_write_count(odb: &Odb) -> usize {
538 odb.hot_path_metrics().tree_writes()
539}
540
541fn store_tree_payload(
542 odb: &Odb,
543 payload: &[u8],
544 flags: WriteTreeFlags,
545 write_opts: WriteOptions,
546) -> Result<(ObjectId, bool)> {
547 let hashed = odb.hash(ObjectKind::Tree, payload)?;
548 match flags.persistence {
549 WriteTreePersistence::DryRun => Ok((hashed, false)),
550 WriteTreePersistence::Repair => Ok((hashed, !odb.contains(&hashed)?)),
551 WriteTreePersistence::Write => {
552 #[cfg(test)]
553 odb.hot_path_metrics().record_tree_write();
554 Ok((
555 odb.write_with_options(ObjectKind::Tree, payload, write_opts)?,
556 false,
557 ))
558 }
559 }
560}
561
562#[must_use]
564pub fn is_empty_tree_oid(odb: &Odb, oid: &ObjectId) -> bool {
565 odb.is_empty_tree(oid)
566}
567
568fn ensure_empty_blob_for_intent_to_add(odb: &Odb, index: &Index) -> Result<()> {
569 if index
570 .entries
571 .iter()
572 .any(|e| e.stage() == 0 && e.intent_to_add())
573 {
574 let _ = odb.write(ObjectKind::Blob, b"")?;
575 }
576 Ok(())
577}
578
579pub fn write_tree_from_index_subset(
585 odb: &Odb,
586 index: &Index,
587 paths: &std::collections::HashSet<Vec<u8>>,
588) -> Result<ObjectId> {
589 ensure_empty_blob_for_intent_to_add(odb, index)?;
590
591 let mut entries: Vec<&IndexEntry> = index
592 .entries
593 .iter()
594 .filter(|entry| {
595 entry.stage() == 0
596 && !entry.intent_to_add()
597 && entry.mode != MODE_TREE
598 && paths.contains(&entry.path)
599 })
600 .collect();
601 entries.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
602 build_tree(odb, &entries, b"")
603}
604
605pub fn write_tree_from_index(odb: &Odb, index: &Index, prefix: &str) -> Result<ObjectId> {
611 if prefix.is_empty() {
612 if let Some(root) = index.cache_tree_root {
613 if cache_tree_fully_valid(odb, index.cache_tree.as_ref()) {
614 return Ok(root);
615 }
616 }
617 }
618
619 ensure_empty_blob_for_intent_to_add(odb, index)?;
620
621 let prefix_bytes = prefix.as_bytes();
622 let mut entries: Vec<&IndexEntry> = index
623 .entries
624 .iter()
625 .filter(|entry| {
626 entry.stage() == 0
627 && !entry.intent_to_add()
628 && entry.mode != MODE_TREE
629 && entry.path.starts_with(prefix_bytes)
630 })
631 .collect();
632 entries.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
633 build_tree(odb, &entries, prefix_bytes)
634}
635
636pub fn build_cache_tree_from_index(odb: &Odb, index: &Index) -> Result<CacheTreeNode> {
642 ensure_empty_blob_for_intent_to_add(odb, index)?;
643 let mut entries: Vec<&IndexEntry> = index
644 .entries
645 .iter()
646 .filter(|entry| entry.stage() == 0 && !entry.intent_to_add() && entry.mode != MODE_TREE)
647 .collect();
648 entries.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
649 build_cache_tree_node(odb, b"", Vec::new(), &entries)
650}
651
652pub fn build_cache_tree_from_tree(odb: &Odb, tree_oid: &ObjectId) -> Result<CacheTreeNode> {
662 #[cfg(test)]
663 odb.hot_path_metrics().record_cache_tree_build();
664 build_cache_tree_from_tree_named(odb, tree_oid, Vec::new())
665}
666
667fn build_cache_tree_from_tree_named(
668 odb: &Odb,
669 tree_oid: &ObjectId,
670 name: Vec<u8>,
671) -> Result<CacheTreeNode> {
672 let obj = odb.read(tree_oid)?;
673 let entries = parse_tree(&obj.data)?;
674
675 let mut entry_count: i32 = 0;
676 let mut children: Vec<CacheTreeNode> = Vec::new();
677 for te in entries {
678 if te.mode == MODE_TREE {
679 let child = build_cache_tree_from_tree_named(odb, &te.oid, te.name.clone())?;
680 entry_count = entry_count.saturating_add(child.entry_count.max(0));
681 children.push(child);
682 } else {
683 entry_count = entry_count.saturating_add(1);
684 }
685 }
686 children.sort_by(|a, b| a.name.cmp(&b.name));
688 Ok(CacheTreeNode::valid(name, entry_count, *tree_oid, children))
689}
690
691pub fn verify_cache_tree(index: &Index) -> Result<()> {
701 let Some(root) = index.cache_tree.as_ref() else {
702 return Ok(());
703 };
704 let mut cache = cache_tree_index_rows(index);
706 cache.sort_by(|a, b| a.path.cmp(&b.path));
707 verify_cache_tree_one(root, &cache, &mut Vec::new())
708}
709
710fn verify_cache_tree_one(
711 node: &CacheTreeNode,
712 cache: &[&IndexEntry],
713 path: &mut Vec<u8>,
714) -> Result<()> {
715 let len = path.len();
716 for child in &node.children {
717 path.extend_from_slice(&child.name);
718 path.push(b'/');
719 verify_cache_tree_one(child, cache, path)?;
720 path.truncate(len);
721 }
722
723 if node.entry_count < 0 {
724 return Ok(());
725 }
726
727 let pos = match cache.binary_search_by(|e| e.path.as_slice().cmp(path.as_slice())) {
729 Ok(p) => p,
730 Err(p) => p,
731 };
732
733 if (node.entry_count as usize).saturating_add(pos) > cache.len() {
734 return Err(crate::error::Error::CacheTreeCorrupt);
735 }
736 Ok(())
737}
738
739fn build_tree(odb: &Odb, entries: &[&IndexEntry], dir_prefix: &[u8]) -> Result<ObjectId> {
740 let mut children: BTreeMap<Vec<u8>, ChildKind> = BTreeMap::new();
741
742 for entry in entries {
743 let path = &entry.path;
744 let rel = if dir_prefix.is_empty() {
745 path.as_slice()
746 } else {
747 path.strip_prefix(dir_prefix)
748 .and_then(|suffix| suffix.strip_prefix(b"/"))
749 .unwrap_or(path.as_slice())
750 };
751
752 if let Some(slash_pos) = rel.iter().position(|&byte| byte == b'/') {
753 let child_name = rel[..slash_pos].to_vec();
754 let sub_prefix = if dir_prefix.is_empty() {
755 child_name.clone()
756 } else {
757 let mut sub_prefix = dir_prefix.to_vec();
758 sub_prefix.push(b'/');
759 sub_prefix.extend_from_slice(&child_name);
760 sub_prefix
761 };
762 children
763 .entry(child_name)
764 .or_insert_with(|| ChildKind::Tree(sub_prefix, Vec::new()))
765 .push_entry(entry);
766 } else {
767 children
768 .entry(rel.to_vec())
769 .or_insert_with(|| ChildKind::Blob {
770 mode: canonicalize_blob_mode(entry.mode),
771 oid: entry.oid,
772 });
773 }
774 }
775
776 let mut tree_entries = Vec::with_capacity(children.len());
777 for (name, child) in children {
778 match child {
779 ChildKind::Blob { mode, oid } => tree_entries.push(TreeEntry { mode, name, oid }),
780 ChildKind::Tree(sub_prefix, sub_entries) => {
781 let sub_oid = build_tree(odb, &sub_entries, &sub_prefix)?;
782 tree_entries.push(TreeEntry {
783 mode: MODE_TREE,
784 name,
785 oid: sub_oid,
786 });
787 }
788 }
789 }
790
791 tree_entries.sort_by(|a, b| {
792 let a_tree = a.mode == MODE_TREE;
793 let b_tree = b.mode == MODE_TREE;
794 tree_entry_cmp(&a.name, a_tree, &b.name, b_tree)
795 });
796
797 let data = serialize_tree(&tree_entries);
798 odb.write(ObjectKind::Tree, &data)
799}
800
801fn build_cache_tree_node(
802 odb: &Odb,
803 dir_prefix: &[u8],
804 name: Vec<u8>,
805 entries: &[&IndexEntry],
806) -> Result<CacheTreeNode> {
807 let mut children: BTreeMap<Vec<u8>, ChildKind> = BTreeMap::new();
808
809 for entry in entries {
810 let path = &entry.path;
811 let rel = if dir_prefix.is_empty() {
812 path.as_slice()
813 } else {
814 path.strip_prefix(dir_prefix)
815 .and_then(|suffix| suffix.strip_prefix(b"/"))
816 .unwrap_or(path.as_slice())
817 };
818
819 if let Some(slash_pos) = rel.iter().position(|&byte| byte == b'/') {
820 let child_name = rel[..slash_pos].to_vec();
821 let sub_prefix = if dir_prefix.is_empty() {
822 child_name.clone()
823 } else {
824 let mut sub_prefix = dir_prefix.to_vec();
825 sub_prefix.push(b'/');
826 sub_prefix.extend_from_slice(&child_name);
827 sub_prefix
828 };
829 children
830 .entry(child_name)
831 .or_insert_with(|| ChildKind::Tree(sub_prefix, Vec::new()))
832 .push_entry(entry);
833 } else {
834 children
835 .entry(rel.to_vec())
836 .or_insert_with(|| ChildKind::Blob {
837 mode: canonicalize_blob_mode(entry.mode),
838 oid: entry.oid,
839 });
840 }
841 }
842
843 let mut tree_entries = Vec::with_capacity(children.len());
844 let mut cache_children = Vec::new();
845 for (child_name, child) in children {
846 match child {
847 ChildKind::Blob { mode, oid } => tree_entries.push(TreeEntry {
848 mode,
849 name: child_name,
850 oid,
851 }),
852 ChildKind::Tree(sub_prefix, sub_entries) => {
853 let child_node =
854 build_cache_tree_node(odb, &sub_prefix, child_name.clone(), &sub_entries)?;
855 let oid = child_node.oid.ok_or_else(|| {
856 crate::error::Error::IndexError("cache-tree child missing oid".to_owned())
857 })?;
858 tree_entries.push(TreeEntry {
859 mode: MODE_TREE,
860 name: child_name,
861 oid,
862 });
863 cache_children.push(child_node);
864 }
865 }
866 }
867
868 tree_entries.sort_by(|a, b| {
869 let a_tree = a.mode == MODE_TREE;
870 let b_tree = b.mode == MODE_TREE;
871 tree_entry_cmp(&a.name, a_tree, &b.name, b_tree)
872 });
873 cache_children.sort_by(|a, b| a.name.cmp(&b.name));
874
875 let data = serialize_tree(&tree_entries);
876 let oid = odb.write(ObjectKind::Tree, &data)?;
877 Ok(CacheTreeNode::valid(
878 name,
879 entries.len() as i32,
880 oid,
881 cache_children,
882 ))
883}
884
885pub fn write_tree_partial_from_index(
892 odb: &Odb,
893 index: &Index,
894 base_tree_oid: &ObjectId,
895 paths_from_index: &std::collections::HashSet<Vec<u8>>,
896) -> Result<ObjectId> {
897 let _ = odb.write(ObjectKind::Blob, b"");
898
899 fn full_path(prefix: &[u8], name: &[u8]) -> Vec<u8> {
900 if prefix.is_empty() {
901 name.to_vec()
902 } else {
903 let mut p = prefix.to_vec();
904 p.push(b'/');
905 p.extend_from_slice(name);
906 p
907 }
908 }
909
910 fn subtree_affected(paths_from_index: &std::collections::HashSet<Vec<u8>>, dir: &[u8]) -> bool {
911 paths_from_index
912 .iter()
913 .any(|p| p == dir || (p.starts_with(dir) && p.get(dir.len()) == Some(&b'/')))
914 }
915
916 fn index_has_entry_under(index: &Index, dir: &[u8]) -> bool {
917 index.entries.iter().any(|entry| {
918 entry.stage() == 0
919 && !entry.intent_to_add()
920 && entry.mode != MODE_TREE
921 && entry.path.starts_with(dir)
922 && entry.path.get(dir.len()) == Some(&b'/')
923 })
924 }
925
926 fn merge_level(
927 odb: &Odb,
928 index: &Index,
929 base_tree_oid: &ObjectId,
930 prefix: &[u8],
931 paths_from_index: &std::collections::HashSet<Vec<u8>>,
932 ) -> Result<ObjectId> {
933 let base_obj = odb.read(base_tree_oid)?;
934 let base_entries = parse_tree(&base_obj.data)?;
935
936 let mut by_name: BTreeMap<Vec<u8>, TreeEntry> = BTreeMap::new();
937 for te in base_entries {
938 let fp = full_path(prefix, &te.name);
939 if !subtree_affected(paths_from_index, &fp) {
940 by_name.insert(te.name.clone(), te);
941 } else if te.mode == MODE_TREE {
942 if paths_from_index.contains(&fp) && !index_has_entry_under(index, &fp) {
943 continue;
944 }
945 let sub_oid = merge_level(odb, index, &te.oid, &fp, paths_from_index)?;
946 if is_empty_tree_oid(odb, &sub_oid) {
947 continue;
948 }
949 by_name.insert(
950 te.name.clone(),
951 TreeEntry {
952 mode: MODE_TREE,
953 name: te.name,
954 oid: sub_oid,
955 },
956 );
957 } else if paths_from_index.contains(&fp) {
958 if let Some(ie) = index.entries.iter().find(|e| {
959 e.stage() == 0 && !e.intent_to_add() && e.mode != MODE_TREE && e.path == fp
960 }) {
961 by_name.insert(
962 te.name.clone(),
963 TreeEntry {
964 mode: canonicalize_blob_mode(ie.mode),
965 name: te.name,
966 oid: ie.oid,
967 },
968 );
969 }
970 } else {
972 by_name.insert(te.name.clone(), te);
973 }
974 }
975
976 for ie in &index.entries {
977 if ie.stage() != 0 || ie.intent_to_add() || ie.mode == MODE_TREE {
978 continue;
979 }
980 if !paths_from_index.contains(&ie.path) {
981 continue;
982 }
983 let rel = if prefix.is_empty() {
984 ie.path.as_slice()
985 } else if ie.path.starts_with(prefix) && ie.path.get(prefix.len()) == Some(&b'/') {
986 &ie.path[prefix.len() + 1..]
987 } else {
988 continue;
989 };
990 if rel.is_empty() {
991 continue;
992 }
993 if let Some(slash) = rel.iter().position(|&b| b == b'/') {
994 let dir_name = rel[..slash].to_vec();
995 if by_name.contains_key(&dir_name) {
996 continue;
997 }
998 let sub_prefix = full_path(prefix, &dir_name);
999 let sub_oid =
1000 write_tree_from_index(odb, index, &String::from_utf8_lossy(&sub_prefix))?;
1001 by_name.insert(
1002 dir_name.clone(),
1003 TreeEntry {
1004 mode: MODE_TREE,
1005 name: dir_name,
1006 oid: sub_oid,
1007 },
1008 );
1009 } else {
1010 let name = rel.to_vec();
1011 if !by_name.contains_key(&name) {
1012 by_name.insert(
1013 name.clone(),
1014 TreeEntry {
1015 mode: canonicalize_blob_mode(ie.mode),
1016 name,
1017 oid: ie.oid,
1018 },
1019 );
1020 }
1021 }
1022 }
1023
1024 let mut out: Vec<TreeEntry> = by_name.into_values().collect();
1025 out.sort_by(|a, b| {
1026 let a_tree = a.mode == MODE_TREE;
1027 let b_tree = b.mode == MODE_TREE;
1028 tree_entry_cmp(&a.name, a_tree, &b.name, b_tree)
1029 });
1030 let data = serialize_tree(&out);
1031 odb.write(ObjectKind::Tree, &data)
1032 }
1033
1034 merge_level(odb, index, base_tree_oid, b"", paths_from_index)
1035}
1036
1037fn canonicalize_blob_mode(mode: u32) -> u32 {
1038 match mode & 0o170000 {
1039 0o120000 => MODE_SYMLINK,
1040 0o160000 => MODE_GITLINK,
1041 0o100000 => {
1042 if mode & 0o111 != 0 {
1043 MODE_EXECUTABLE
1044 } else {
1045 MODE_REGULAR
1046 }
1047 }
1048 _ => MODE_REGULAR,
1049 }
1050}
1051
1052enum ChildKind<'a> {
1053 Blob { mode: u32, oid: ObjectId },
1054 Tree(Vec<u8>, Vec<&'a IndexEntry>),
1055}
1056
1057impl<'a> ChildKind<'a> {
1058 fn push_entry(&mut self, entry: &'a IndexEntry) {
1059 if let Self::Tree(_, entries) = self {
1060 entries.push(entry);
1061 }
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 #![allow(clippy::expect_used, clippy::unwrap_used)]
1068
1069 use super::*;
1070 use crate::index::{IndexEntry, MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK, MODE_TREE};
1071 use crate::objects::parse_tree;
1072 use tempfile::TempDir;
1073
1074 fn entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
1075 IndexEntry {
1076 ctime_sec: 0,
1077 ctime_nsec: 0,
1078 mtime_sec: 0,
1079 mtime_nsec: 0,
1080 dev: 0,
1081 ino: 0,
1082 mode,
1083 uid: 0,
1084 gid: 0,
1085 size: 0,
1086 oid,
1087 flags: path.len().min(0xFFF) as u16,
1088 flags_extended: None,
1089 path: path.as_bytes().to_vec(),
1090 base_index_pos: 0,
1091 }
1092 }
1093
1094 fn sparse_dir_placeholder(path: &str, tree_oid: ObjectId) -> IndexEntry {
1095 let mut e = entry(path, MODE_TREE, tree_oid);
1096 e.set_skip_worktree(true);
1097 e
1098 }
1099
1100 #[test]
1101 fn cache_tree_extension_valid_without_odb_io() {
1102 let dir = TempDir::new().unwrap();
1103 let odb = Odb::new(dir.path());
1104 let blob = odb.write(ObjectKind::Blob, b"x").unwrap();
1105 let tree_oid = write_tree_from_index(
1106 &odb,
1107 &Index {
1108 entries: vec![entry("x", MODE_REGULAR, blob)],
1109 ..Index::new()
1110 },
1111 "",
1112 )
1113 .unwrap();
1114 let mut root = CacheTreeNode::valid(vec![], 1, tree_oid, vec![]);
1115 assert!(cache_tree_extension_valid(Some(&root)));
1116 root.invalidate();
1117 assert!(!cache_tree_extension_valid(Some(&root)));
1118 assert!(!cache_tree_fully_valid(&odb, Some(&root)));
1119 }
1120
1121 #[test]
1122 fn cache_tree_fully_valid_requires_positive_counts_and_objects() {
1123 let dir = TempDir::new().unwrap();
1124 let odb = Odb::new(dir.path());
1125 let blob = odb.write(ObjectKind::Blob, b"x").unwrap();
1126 let tree_oid = write_tree_from_index(
1127 &odb,
1128 &Index {
1129 entries: vec![entry("x", MODE_REGULAR, blob)],
1130 ..Index::new()
1131 },
1132 "",
1133 )
1134 .unwrap();
1135
1136 assert!(!cache_tree_fully_valid(
1137 &odb,
1138 Some(&empty_cache_tree_root())
1139 ));
1140 let missing_oid = CacheTreeNode::valid(b"root".to_vec(), 1, tree_oid, vec![]);
1141 odb.test_remove_loose_object(&tree_oid);
1142 assert!(!cache_tree_fully_valid(&odb, Some(&missing_oid)));
1143 }
1144
1145 #[test]
1146 fn write_tree_update_index_reuses_valid_cache_tree() {
1147 let dir = TempDir::new().unwrap();
1148 let odb = Odb::new(dir.path());
1149 let blob = odb.write(ObjectKind::Blob, b"same").unwrap();
1150 let mut index = Index::new();
1151 index.add_or_replace(entry("same", MODE_REGULAR, blob));
1152 let first =
1153 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1154 let objects_before = std::fs::read_dir(dir.path()).unwrap().count();
1155 let second =
1156 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1157 assert_eq!(first, second);
1158 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1159 assert_eq!(
1160 std::fs::read_dir(dir.path()).unwrap().count(),
1161 objects_before
1162 );
1163 }
1164
1165 #[test]
1166 fn cache_tree_update_dry_run_hashes_without_writing_trees() {
1167 let dir = TempDir::new().unwrap();
1168 let odb = Odb::new(dir.path());
1169 let blob = odb.write(ObjectKind::Blob, b"d").unwrap();
1170 let mut index = Index::new();
1171 index.add_or_replace(entry("d", MODE_REGULAR, blob));
1172 let flags = WriteTreeFlags {
1173 persistence: WriteTreePersistence::DryRun,
1174 ..WriteTreeFlags::default()
1175 };
1176 cache_tree_update(&odb, &mut index, flags).unwrap();
1177 let root_oid = index.cache_tree.as_ref().unwrap().oid.unwrap();
1178 assert!(!odb.exists(&root_oid));
1179 assert_eq!(root_oid, write_tree_from_index(&odb, &index, "").unwrap());
1180 }
1181
1182 #[test]
1183 fn cache_tree_update_repair_reuses_existing_tree_bytes() {
1184 let dir = TempDir::new().unwrap();
1185 let odb = Odb::new(dir.path());
1186 let blob = odb.write(ObjectKind::Blob, b"r").unwrap();
1187 let mut index = Index::new();
1188 index.add_or_replace(entry("r", MODE_REGULAR, blob));
1189 let expected = write_tree_from_index(&odb, &index, "").unwrap();
1190 index.clear_cache_tree();
1191 let flags = WriteTreeFlags {
1192 persistence: WriteTreePersistence::Repair,
1193 ..WriteTreeFlags::default()
1194 };
1195 cache_tree_update(&odb, &mut index, flags).unwrap();
1196 assert_eq!(index.cache_tree.as_ref().unwrap().oid, Some(expected));
1197 }
1198
1199 #[test]
1200 fn cache_tree_update_rejects_unmerged_index() {
1201 let dir = TempDir::new().unwrap();
1202 let odb = Odb::new(dir.path());
1203 let blob = odb.write(ObjectKind::Blob, b"u").unwrap();
1204 let mut conflict = entry("conflict", MODE_REGULAR, blob);
1205 conflict.set_stage(1);
1206 let mut index = Index::new();
1207 index.add_or_replace(conflict);
1208 let err = cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap_err();
1209 assert!(matches!(err, Error::IndexUnmerged));
1210 }
1211
1212 #[test]
1213 fn cache_tree_update_rejects_path_prefix_conflict() {
1214 let dir = TempDir::new().unwrap();
1215 let odb = Odb::new(dir.path());
1216 let a = odb.write(ObjectKind::Blob, b"a").unwrap();
1217 let b = odb.write(ObjectKind::Blob, b"b").unwrap();
1218 let mut index = Index::new();
1219 index.add_or_replace(entry("dir", MODE_REGULAR, a));
1220 index.add_or_replace(entry("dir/file", MODE_REGULAR, b));
1221 let err = cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap_err();
1222 assert!(matches!(err, Error::IndexPathPrefixConflict { .. }));
1223 }
1224
1225 #[test]
1226 fn cache_tree_update_reports_missing_blob() {
1227 let dir = TempDir::new().unwrap();
1228 let odb = Odb::new(dir.path());
1229 let missing = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
1230 let mut index = Index::new();
1231 index.add_or_replace(entry("gone", MODE_REGULAR, missing));
1232 let err = cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap_err();
1233 assert!(matches!(err, Error::ObjectNotFound(_)));
1234 }
1235
1236 #[test]
1237 fn cache_tree_update_rejects_null_oid_even_with_missing_ok() {
1238 let dir = TempDir::new().unwrap();
1239 let odb = Odb::new(dir.path());
1240 let blob = odb.write(ObjectKind::Blob, b"ok").unwrap();
1241 let zero = ObjectId::zero();
1242 let mut index = Index::new();
1243 index.add_or_replace(entry("sub", MODE_GITLINK, zero));
1244 index.add_or_replace(entry("ok", MODE_REGULAR, blob));
1245 let err = cache_tree_update(
1246 &odb,
1247 &mut index,
1248 WriteTreeFlags {
1249 missing_ok: true,
1250 ..WriteTreeFlags::default()
1251 },
1252 )
1253 .unwrap_err();
1254 assert!(matches!(err, Error::ObjectNotFound(_)));
1255 }
1256
1257 #[test]
1258 fn cache_tree_update_missing_ok_skips_existence_for_non_null_gitlink() {
1259 let dir = TempDir::new().unwrap();
1260 let odb = Odb::new(dir.path());
1261 let blob = odb.write(ObjectKind::Blob, b"ok").unwrap();
1262 let missing = ObjectId::from_hex("00000000000000000000000000000000000000ab").unwrap();
1263 let mut index = Index::new();
1264 index.add_or_replace(entry("sub", MODE_GITLINK, missing));
1265 index.add_or_replace(entry("ok", MODE_REGULAR, blob));
1266 cache_tree_update(
1267 &odb,
1268 &mut index,
1269 WriteTreeFlags {
1270 missing_ok: true,
1271 ..WriteTreeFlags::default()
1272 },
1273 )
1274 .unwrap();
1275 assert!(index.cache_tree.as_ref().unwrap().oid.is_some());
1276 }
1277
1278 #[test]
1279 fn cache_tree_update_ignore_cache_tree_rebuilds_from_scratch() {
1280 let dir = TempDir::new().unwrap();
1281 let odb = Odb::new(dir.path());
1282 let blob = odb.write(ObjectKind::Blob, b"i").unwrap();
1283 let mut index = Index::new();
1284 index.add_or_replace(entry("i", MODE_REGULAR, blob));
1285 index.set_cache_tree(build_cache_tree_from_index(&odb, &index).unwrap());
1286 index.cache_tree.as_mut().unwrap().entry_count = -1;
1287 cache_tree_update(
1288 &odb,
1289 &mut index,
1290 WriteTreeFlags {
1291 ignore_cache_tree: true,
1292 ..WriteTreeFlags::default()
1293 },
1294 )
1295 .unwrap();
1296 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1297 }
1298
1299 #[test]
1300 fn cache_tree_update_marks_ita_entries_stale() {
1301 let dir = TempDir::new().unwrap();
1302 let odb = Odb::new(dir.path());
1303 let blob = odb.write(ObjectKind::Blob, b"old").unwrap();
1304 let mut index = Index::new();
1305 index.add_or_replace(entry("old", MODE_REGULAR, blob));
1306 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1307 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1308
1309 let mut ita = entry("new", MODE_REGULAR, ObjectId::zero());
1310 ita.set_intent_to_add(true);
1311 index.add_or_replace(ita);
1312 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1313 assert_eq!(index.cache_tree.as_ref().unwrap().entry_count, -1);
1314 }
1315
1316 #[test]
1317 fn sparse_placeholder_survives_ancestor_invalidation() {
1318 let dir = TempDir::new().unwrap();
1319 let odb = Odb::new(dir.path());
1320
1321 let inner_blob = odb.write(ObjectKind::Blob, b"inner").unwrap();
1322 let mut inner_index = Index::new();
1323 inner_index.add_or_replace(entry("leaf", MODE_REGULAR, inner_blob));
1324 let inner_tree = write_tree_from_index(&odb, &inner_index, "").unwrap();
1325
1326 let top_blob = odb.write(ObjectKind::Blob, b"top").unwrap();
1327 let mut index = Index::new();
1328 index.add_or_replace(entry("top", MODE_REGULAR, top_blob));
1329 index.add_or_replace(sparse_dir_placeholder("wide/sub", inner_tree));
1330 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1331 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1332
1333 index.invalidate_cache_tree_for_path(b"wide");
1334 assert_eq!(index.cache_tree.as_ref().unwrap().entry_count, -1);
1335 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1336
1337 let wide = index
1338 .cache_tree
1339 .as_ref()
1340 .unwrap()
1341 .children
1342 .iter()
1343 .find(|c| c.name == b"wide")
1344 .expect("wide subtree");
1345 let sub = wide
1346 .children
1347 .iter()
1348 .find(|c| c.name == b"sub")
1349 .expect("sparse sub placeholder");
1350 assert_eq!(sub.oid, Some(inner_tree));
1351 assert_eq!(sub.entry_count, 1);
1352 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1353 }
1354
1355 #[test]
1356 fn rebuilds_cache_tree_after_path_invalidation() {
1357 let temp_dir = TempDir::new().unwrap();
1358 let odb = Odb::new(temp_dir.path());
1359
1360 let oid_f = odb.write(ObjectKind::Blob, b"f").unwrap();
1361 let oid_deep = odb.write(ObjectKind::Blob, b"deep").unwrap();
1362
1363 let mut index = Index::new();
1364 index.add_or_replace(entry("f", MODE_REGULAR, oid_f));
1365 let root_tree = write_tree_from_index(&odb, &index, "").unwrap();
1366 index.set_cache_tree(build_cache_tree_from_index(&odb, &index).unwrap());
1367 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1368
1369 index.invalidate_cache_tree_for_path(b"deep");
1370 index.add_or_replace(entry("deep/very-long-subdir/file", MODE_REGULAR, oid_deep));
1371
1372 let oid = write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1373 assert_ne!(oid, root_tree);
1374 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1375 }
1376
1377 #[test]
1378 fn writes_sorted_tree_with_canonical_modes() {
1379 let temp_dir = TempDir::new().unwrap();
1380 let odb = Odb::new(temp_dir.path());
1381
1382 let oid_a = odb.write(ObjectKind::Blob, b"a").unwrap();
1383 let oid_exec = odb.write(ObjectKind::Blob, b"exec").unwrap();
1384 let oid_link = odb.write(ObjectKind::Blob, b"target").unwrap();
1385
1386 let mut index = Index::new();
1387 index.add_or_replace(entry("bin/run.sh", 0o100777, oid_exec));
1388 index.add_or_replace(entry("link", 0o120777, oid_link));
1389 index.add_or_replace(entry("a.txt", 0o100664, oid_a));
1390
1391 let root_oid = write_tree_from_index(&odb, &index, "").unwrap();
1392 let root_tree_obj = odb.read(&root_oid).unwrap();
1393 let root_entries = parse_tree(&root_tree_obj.data).unwrap();
1394
1395 assert_eq!(root_entries.len(), 3);
1396 assert_eq!(root_entries[0].name, b"a.txt");
1397 assert_eq!(root_entries[0].mode, MODE_REGULAR);
1398 assert_eq!(root_entries[1].name, b"bin");
1399 assert_eq!(root_entries[1].mode, MODE_TREE);
1400 assert_eq!(root_entries[2].name, b"link");
1401 assert_eq!(root_entries[2].mode, MODE_SYMLINK);
1402
1403 let bin_tree_obj = odb.read(&root_entries[1].oid).unwrap();
1404 let bin_entries = parse_tree(&bin_tree_obj.data).unwrap();
1405 assert_eq!(bin_entries.len(), 1);
1406 assert_eq!(bin_entries[0].name, b"run.sh");
1407 assert_eq!(bin_entries[0].mode, MODE_EXECUTABLE);
1408 }
1409
1410 #[test]
1411 fn write_tree_does_not_touch_entry_blobs() {
1412 use filetime::FileTime;
1413 use std::fs;
1414 use std::thread;
1415 use std::time::Duration;
1416
1417 let temp_dir = TempDir::new().unwrap();
1418 let odb = Odb::new(temp_dir.path());
1419
1420 let oid_a = odb.write(ObjectKind::Blob, b"a").unwrap();
1421 let oid_b = odb.write(ObjectKind::Blob, b"b").unwrap();
1422
1423 let path_a = odb.test_loose_object_path(&oid_a);
1424 let path_b = odb.test_loose_object_path(&oid_b);
1425 thread::sleep(Duration::from_millis(50));
1426 let stamp_a = FileTime::from_last_modification_time(&fs::metadata(&path_a).unwrap());
1427 let stamp_b = FileTime::from_last_modification_time(&fs::metadata(&path_b).unwrap());
1428 filetime::set_file_mtime(&path_a, stamp_a).unwrap();
1429 filetime::set_file_mtime(&path_b, stamp_b).unwrap();
1430
1431 let mut index = Index::new();
1432 index.add_or_replace(entry("a.txt", 0o100644, oid_a));
1433 index.add_or_replace(entry("b.txt", 0o100644, oid_b));
1434
1435 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1436
1437 let after_a = FileTime::from_last_modification_time(&fs::metadata(&path_a).unwrap());
1438 let after_b = FileTime::from_last_modification_time(&fs::metadata(&path_b).unwrap());
1439 assert_eq!(after_a, stamp_a, "write-tree must not freshen index blob a");
1440 assert_eq!(after_b, stamp_b, "write-tree must not freshen index blob b");
1441 }
1442
1443 #[test]
1444 fn write_tree_freshens_reconstructed_existing_tree() {
1445 use filetime::FileTime;
1446 use std::fs;
1447 use std::thread;
1448 use std::time::Duration;
1449
1450 let temp_dir = TempDir::new().unwrap();
1451 let odb = Odb::new(temp_dir.path());
1452
1453 let oid_a = odb.write(ObjectKind::Blob, b"a").unwrap();
1454 let oid_b = odb.write(ObjectKind::Blob, b"b").unwrap();
1455 let mut index = Index::new();
1456 index.add_or_replace(entry("a.txt", 0o100644, oid_a));
1457 index.add_or_replace(entry("b.txt", 0o100644, oid_b));
1458
1459 let root_oid =
1460 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1461 let tree_path = odb.test_loose_object_path(&root_oid);
1462
1463 let path_a = odb.test_loose_object_path(&oid_a);
1464 let path_b = odb.test_loose_object_path(&oid_b);
1465 thread::sleep(Duration::from_millis(50));
1466 let stamp_a = FileTime::from_last_modification_time(&fs::metadata(&path_a).unwrap());
1467 let stamp_b = FileTime::from_last_modification_time(&fs::metadata(&path_b).unwrap());
1468 filetime::set_file_mtime(&path_a, stamp_a).unwrap();
1469 filetime::set_file_mtime(&path_b, stamp_b).unwrap();
1470
1471 let two_days_ago = FileTime::from_unix_time(
1472 filetime::FileTime::now().unix_seconds() - time::Duration::days(2).whole_seconds(),
1473 0,
1474 );
1475 filetime::set_file_mtime(&tree_path, two_days_ago).unwrap();
1476 let aged = FileTime::from_last_modification_time(&fs::metadata(&tree_path).unwrap());
1477
1478 index.clear_cache_tree();
1479 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1480
1481 let after_tree = FileTime::from_last_modification_time(&fs::metadata(&tree_path).unwrap());
1482 assert!(
1483 after_tree > aged,
1484 "reconstructed tree must be freshened (t6501 same-tree)"
1485 );
1486 let after_a = FileTime::from_last_modification_time(&fs::metadata(&path_a).unwrap());
1487 let after_b = FileTime::from_last_modification_time(&fs::metadata(&path_b).unwrap());
1488 assert_eq!(after_a, stamp_a);
1489 assert_eq!(after_b, stamp_b);
1490 }
1491
1492 fn cache_tree_child_oid(index: &Index, name: &[u8]) -> ObjectId {
1493 index
1494 .cache_tree
1495 .as_ref()
1496 .unwrap()
1497 .children
1498 .iter()
1499 .find(|c| c.name == name)
1500 .and_then(|c| c.oid)
1501 .unwrap()
1502 }
1503
1504 fn build_nested_index(odb: &Odb) -> Index {
1505 let blobs: Vec<ObjectId> = (0..6)
1506 .map(|i| {
1507 odb.write(ObjectKind::Blob, format!("blob{i}").as_bytes())
1508 .unwrap()
1509 })
1510 .collect();
1511 let mut index = Index::new();
1512 index.add_or_replace(entry("alpha/one", MODE_REGULAR, blobs[0]));
1513 index.add_or_replace(entry("alpha/two", MODE_REGULAR, blobs[1]));
1514 index.add_or_replace(entry("beta/three", MODE_REGULAR, blobs[2]));
1515 index.add_or_replace(entry("beta/four/deep", MODE_REGULAR, blobs[3]));
1516 index.add_or_replace(entry("gamma", MODE_REGULAR, blobs[4]));
1517 index.add_or_replace(entry("wide/sub/leaf", MODE_REGULAR, blobs[5]));
1518 index
1519 }
1520
1521 #[test]
1522 fn cache_tree_update_matches_full_rebuild() {
1523 let dir = TempDir::new().unwrap();
1524 let odb = Odb::new(dir.path());
1525 let mut index = build_nested_index(&odb);
1526 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1527 let incremental_root = index.cache_tree.as_ref().unwrap().oid.unwrap();
1528
1529 let mut seed = 0xDEADBEEF_u32;
1530 for step in 0..12 {
1531 seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1532 let path: &[u8] = match step % 5 {
1533 0 => b"alpha",
1534 1 => b"beta",
1535 2 => b"beta/four",
1536 3 => b"wide",
1537 _ => b"gamma",
1538 };
1539 index.invalidate_cache_tree_for_path(path);
1540 if step % 3 == 0 {
1541 let blob = odb
1542 .write(ObjectKind::Blob, format!("mut{step}").as_bytes())
1543 .unwrap();
1544 let extra_path = format!("alpha/extra{step}");
1545 index.add_or_replace(entry(&extra_path, MODE_REGULAR, blob));
1546 }
1547 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1548 let full = build_cache_tree_from_index(&odb, &index).unwrap();
1549 assert_eq!(
1550 index.cache_tree.as_ref().unwrap().oid,
1551 full.oid,
1552 "step {step}: incremental cache-tree must match full rebuild"
1553 );
1554 }
1555 let _ = incremental_root;
1556 }
1557
1558 #[test]
1559 fn cache_tree_update_rewrites_only_invalidated() {
1560 use filetime::FileTime;
1561 use std::fs;
1562
1563 let dir = TempDir::new().unwrap();
1564 let odb = Odb::new(dir.path());
1565 let mut index = build_nested_index(&odb);
1566 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1567 let alpha_before = cache_tree_child_oid(&index, b"alpha");
1568 let wide_before = cache_tree_child_oid(&index, b"wide");
1569 let beta_before = cache_tree_child_oid(&index, b"beta");
1570 let alpha_path = odb.test_loose_object_path(&alpha_before);
1571 let wide_path = odb.test_loose_object_path(&wide_before);
1572 let alpha_mtime =
1573 FileTime::from_last_modification_time(&fs::metadata(&alpha_path).unwrap());
1574 let wide_mtime = FileTime::from_last_modification_time(&fs::metadata(&wide_path).unwrap());
1575
1576 index.invalidate_cache_tree_for_path(b"beta/four");
1577 let blob = odb.write(ObjectKind::Blob, b"updated").unwrap();
1578 index.add_or_replace(entry("beta/four/deep", MODE_REGULAR, blob));
1579
1580 odb.hot_path_metrics().reset_tree_writes();
1581 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1582 let tree_writes = odb.hot_path_metrics().tree_writes();
1583 assert!(
1584 (2..=3).contains(&tree_writes),
1585 "expected root + beta subtree tree writes, got {tree_writes}"
1586 );
1587 assert_eq!(cache_tree_child_oid(&index, b"alpha"), alpha_before);
1588 assert_eq!(cache_tree_child_oid(&index, b"wide"), wide_before);
1589 assert_ne!(cache_tree_child_oid(&index, b"beta"), beta_before);
1590 assert_eq!(
1591 FileTime::from_last_modification_time(&fs::metadata(&alpha_path).unwrap()),
1592 alpha_mtime,
1593 "unchanged alpha tree must not be rewritten"
1594 );
1595 assert_eq!(
1596 FileTime::from_last_modification_time(&fs::metadata(&wide_path).unwrap()),
1597 wide_mtime,
1598 "unchanged wide tree must not be rewritten"
1599 );
1600 }
1601
1602 #[test]
1603 fn cache_tree_update_does_not_reprobe_reused_or_written_subtrees() {
1604 let dir = TempDir::new().unwrap();
1605 let odb = Odb::new(dir.path());
1606 let dirs = 200;
1607 let mut index = Index::new();
1608 for d in 0..dirs {
1609 let blob = odb
1610 .write(ObjectKind::Blob, format!("{d}\n").as_bytes())
1611 .unwrap();
1612 index.add_or_replace(entry(&format!("d{d}/f"), MODE_REGULAR, blob));
1613 }
1614 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1615
1616 let changed = odb.write(ObjectKind::Blob, b"changed\n").unwrap();
1617 index.invalidate_cache_tree_for_path(b"d0/f");
1618 index.add_or_replace(entry("d0/f", MODE_REGULAR, changed));
1619 odb.reset_exists_probe();
1620 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1621 assert_eq!(odb.exists_probe_count(), dirs - 1 + 3);
1626 assert_eq!(
1627 index.cache_tree.as_ref().unwrap().oid,
1628 build_cache_tree_from_index(&odb, &index).unwrap().oid
1629 );
1630 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1631 }
1632
1633 #[test]
1634 fn cache_tree_update_reports_missing_blob_beside_reused_subtrees() {
1635 let dir = TempDir::new().unwrap();
1636 let odb = Odb::new(dir.path());
1637 let mut index = build_nested_index(&odb);
1638 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1639 let missing = ObjectId::from_hex("00000000000000000000000000000000000000cd").unwrap();
1640 index.invalidate_cache_tree_for_path(b"beta/three");
1641 index.add_or_replace(entry("beta/three", MODE_REGULAR, missing));
1642 let err = cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap_err();
1643 assert!(
1644 matches!(&err, Error::ObjectNotFound(msg) if msg.contains("beta/three")),
1645 "{err:?}"
1646 );
1647 }
1648
1649 #[test]
1650 fn cache_tree_update_rebuilds_missing_tree() {
1651 let dir = TempDir::new().unwrap();
1652 let odb = Odb::new(dir.path());
1653 let mut index = build_nested_index(&odb);
1654 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1655 let beta_oid = cache_tree_child_oid(&index, b"beta");
1656 let root_oid = index.cache_tree.as_ref().unwrap().oid.unwrap();
1657 odb.test_remove_loose_object(&beta_oid);
1658 assert!(!odb.exists(&beta_oid));
1659 assert!(
1660 odb.exists(&root_oid),
1661 "root tree remains while child is missing"
1662 );
1663
1664 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1665 assert!(odb.exists(&cache_tree_child_oid(&index, b"beta")));
1666 assert!(cache_tree_fully_valid(&odb, index.cache_tree.as_ref()));
1667 }
1668
1669 #[test]
1670 fn write_tree_update_recreates_missing_child_with_valid_root() {
1671 let dir = TempDir::new().unwrap();
1672 let odb = Odb::new(dir.path());
1673 let mut index = Index::new();
1674 let blob = odb.write(ObjectKind::Blob, b"x").unwrap();
1675 index.add_or_replace(entry("dir/x", MODE_REGULAR, blob));
1676 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1677 let dir_oid = cache_tree_child_oid(&index, b"dir");
1678 odb.test_remove_loose_object(&dir_oid);
1679
1680 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1681 assert!(odb.exists(&cache_tree_child_oid(&index, b"dir")));
1682 }
1683
1684 fn sha256_odb(temp: &TempDir) -> Odb {
1685 let git_dir = temp.path().join(".git");
1686 std::fs::create_dir_all(git_dir.join("objects")).unwrap();
1687 std::fs::write(
1688 git_dir.join("config"),
1689 "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectformat = sha256\n",
1690 )
1691 .unwrap();
1692 Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir)
1693 }
1694
1695 #[test]
1696 fn nested_intent_to_add_omits_empty_directory_sha256() {
1697 let temp = TempDir::new().unwrap();
1698 let odb = sha256_odb(&temp);
1699 assert_eq!(odb.hash_algo(), crate::objects::HashAlgo::Sha256);
1700
1701 let keep = odb.write(ObjectKind::Blob, b"keep").unwrap();
1702 let mut index = Index::new();
1703 index.add_or_replace(entry("keep", MODE_REGULAR, keep));
1704 let mut ita = entry("ita/new", MODE_REGULAR, ObjectId::zero());
1705 ita.set_intent_to_add(true);
1706 index.add_or_replace(ita);
1707
1708 let root =
1709 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1710 let entries = parse_tree(&odb.read(&root).unwrap().data).unwrap();
1711 assert_eq!(entries.len(), 1);
1712 assert_eq!(entries[0].name, b"keep");
1713 }
1714
1715 #[test]
1716 fn index_cache_tree_inputs_match_detects_differences() {
1717 let dir = TempDir::new().unwrap();
1718 let odb = Odb::new(dir.path());
1719 let left = build_nested_index(&odb);
1720 let mut right = left.clone();
1721 assert!(index_cache_tree_inputs_match(&left, &right));
1722 right.add_or_replace(entry(
1723 "alpha/one",
1724 MODE_REGULAR,
1725 odb.write(ObjectKind::Blob, b"changed").unwrap(),
1726 ));
1727 assert!(!index_cache_tree_inputs_match(&left, &right));
1728 }
1729
1730 #[test]
1731 fn partial_write_tree_dropping_last_file_prunes_directory() {
1732 use std::collections::HashSet;
1733
1734 let dir = TempDir::new().unwrap();
1735 let odb = Odb::new(dir.path());
1736 let blob = odb.write(ObjectKind::Blob, b"only").unwrap();
1737 let mut base_index = Index::new();
1738 base_index.add_or_replace(entry("upstream.txt", MODE_REGULAR, blob));
1739 base_index.add_or_replace(entry("dir/only.txt", MODE_REGULAR, blob));
1740 let base_tree = write_tree_from_index(&odb, &base_index, "").unwrap();
1741
1742 let mut paths = HashSet::new();
1743 paths.insert(b"dir/only.txt".to_vec());
1744 let empty_index = Index::new();
1745 let merged =
1746 write_tree_partial_from_index(&odb, &empty_index, &base_tree, &paths).expect("partial");
1747
1748 let top = parse_tree(&odb.read(&merged).unwrap().data).unwrap();
1749 assert!(
1750 top.iter().all(|e| e.name != b"dir"),
1751 "empty directory must not appear in tree"
1752 );
1753 assert!(top.iter().any(|e| e.name == b"upstream.txt"));
1754 }
1755
1756 #[test]
1757 fn partial_write_tree_merges_head_with_index_paths() {
1758 use std::collections::HashSet;
1759
1760 let dir = TempDir::new().unwrap();
1761 let odb = Odb::new(dir.path());
1762 let oid_keep = odb.write(ObjectKind::Blob, b"keep").unwrap();
1763 let oid_old = odb.write(ObjectKind::Blob, b"old").unwrap();
1764 let oid_new = odb.write(ObjectKind::Blob, b"new").unwrap();
1765
1766 let mut base_index = Index::new();
1767 base_index.add_or_replace(entry("keep.txt", MODE_REGULAR, oid_keep));
1768 base_index.add_or_replace(entry("dir/old.txt", MODE_REGULAR, oid_old));
1769 let base_tree = write_tree_from_index(&odb, &base_index, "").unwrap();
1770
1771 let mut index = base_index.clone();
1772 index.add_or_replace(entry("dir/new.txt", MODE_REGULAR, oid_new));
1773 index.add_or_replace(entry("added/sub/file.txt", MODE_REGULAR, oid_new));
1774
1775 let mut paths = HashSet::new();
1776 paths.insert(b"dir/new.txt".to_vec());
1777 paths.insert(b"added/sub/file.txt".to_vec());
1778 let merged =
1779 write_tree_partial_from_index(&odb, &index, &base_tree, &paths).expect("partial");
1780 assert_ne!(merged, base_tree);
1781
1782 let tree_obj = odb.read(&merged).unwrap();
1783 let top = parse_tree(&tree_obj.data).unwrap();
1784 assert!(top.iter().any(|e| e.name == b"keep.txt"));
1785 assert!(top.iter().any(|e| e.name == b"added"));
1786 }
1787
1788 #[test]
1789 fn write_tree_from_index_subset_and_silent_flags() {
1790 use std::collections::HashSet;
1791
1792 let dir = TempDir::new().unwrap();
1793 let odb = Odb::new(dir.path());
1794 let mut index = build_nested_index(&odb);
1795 let mut paths = HashSet::new();
1796 paths.insert(b"gamma".to_vec());
1797 let subset = write_tree_from_index_subset(&odb, &index, &paths).unwrap();
1798 assert!(odb.exists(&subset));
1799 cache_tree_update(&odb, &mut index, WriteTreeFlags::silent()).unwrap();
1800 assert!(index.cache_tree.is_some());
1801 }
1802
1803 #[test]
1804 fn cache_tree_update_prefix() {
1805 let dir = TempDir::new().unwrap();
1806 let odb = Odb::new(dir.path());
1807 let mut index = build_nested_index(&odb);
1808 cache_tree_update(&odb, &mut index, WriteTreeFlags::default()).unwrap();
1809
1810 let full =
1811 write_tree_update_index(&odb, &mut index, "", WriteTreeFlags::default()).unwrap();
1812 let sub =
1813 write_tree_update_index(&odb, &mut index, "beta", WriteTreeFlags::default()).unwrap();
1814 assert_ne!(full, sub);
1815
1816 let beta = index
1817 .cache_tree
1818 .as_ref()
1819 .unwrap()
1820 .children
1821 .iter()
1822 .find(|c| c.name == b"beta")
1823 .expect("beta cache-tree child");
1824 assert_eq!(beta.oid, Some(sub));
1825 }
1826}