Skip to main content

grit_lib/
write_tree.rs

1//! Build tree objects from index entries (`git write-tree` core logic).
2
3use 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/// How [`cache_tree_update`] persists rebuilt tree objects.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum WriteTreePersistence {
19    /// Write missing tree objects to the object store.
20    #[default]
21    Write,
22    /// Compute tree OIDs without writing (dry run).
23    DryRun,
24    /// Reuse an existing OID when serialized tree bytes already exist.
25    Repair,
26}
27
28/// Options for [`cache_tree_update`] and [`write_tree_update_index`].
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub struct WriteTreeFlags {
31    /// Allow missing blob OIDs when building trees.
32    pub missing_ok: bool,
33    /// Discard the index cache-tree and rebuild from scratch.
34    pub ignore_cache_tree: bool,
35    /// How tree objects are stored or hashed.
36    pub persistence: WriteTreePersistence,
37    /// Skip mtime freshen on existing objects when writing.
38    pub silent: bool,
39}
40
41impl WriteTreeFlags {
42    /// Like [`Self::default`], but tree writes use [`WriteOptions::silent`] so existing
43    /// objects are not freshened (used when refreshing the index cache-tree after commit).
44    #[must_use]
45    pub fn silent() -> Self {
46        Self {
47            silent: true,
48            ..Self::default()
49        }
50    }
51}
52
53/// Returns whether `left` and `right` have the same stage-0 rows that feed cache-tree / write-tree.
54///
55/// Used after commit when the on-disk index may have been mutated by hooks while the in-memory
56/// index (and its cache-tree) reflects the tree that was actually committed.
57#[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/// Returns whether every node in the index cache-tree extension has valid metadata.
74///
75/// Hot paths (`status`, index↔worktree diff) trust this the same way Git trusts a loaded
76/// `TREE` extension until write-tree invalidates it. No object-store I/O is performed.
77#[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/// Returns whether every node in `node` is valid and its tree object exists in `odb`.
91///
92/// The existence check asks the object store about all tree ids in one batch
93/// ([`ObjectStore::contains_many`](crate::odb::store::ObjectStore::contains_many)), so
94/// a database- or network-backed store answers in a single round trip. Every source
95/// [`Odb::contains`] consults counts: the primary, then any
96/// [`OdbBuilder::push_read_source`](crate::odb::OdbBuilder::push_read_source) store,
97/// then alternates, each asked only about the ids earlier sources lack. Trees present
98/// only in a promisor pack of a files primary therefore count as present too. A store
99/// failure counts as "not valid".
100#[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
113/// Push the tree id of every node of `node` whose whole subtree is valid, and return
114/// whether `node`'s own subtree is (in which case its id is the last one pushed).
115fn 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
126/// Tree ids of the cached subtrees `cache_tree_update` may reuse, confirmed present in `odb`.
127///
128/// Every candidate is probed in one batch up front instead of one store call per cached
129/// directory while rebuilding, across every source (see [`cache_tree_fully_valid`]).
130/// A store failure leaves the set empty, so everything is rebuilt (and the writes
131/// surface the failure).
132fn 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
145/// Update the index cache-tree extension and write any missing tree objects.
146///
147/// On success the index's [`Index::cache_tree`] reflects the current stage-0 entries.
148///
149/// # Errors
150///
151/// Returns an error when the index has unmerged entries, path/file conflicts, or missing objects.
152pub 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
180/// Write the index as a tree, updating the cache-tree in place when needed.
181///
182/// When the existing cache-tree is fully valid and `prefix` is empty, this returns the cached
183/// root OID without rewriting tree objects.
184pub 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
226/// Stage-0 index rows that participate in cache-tree / write-tree (includes sparse-directory placeholders).
227fn 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
302/// Per-update state shared by every [`rebuild_directory_node`] call of one
303/// [`cache_tree_update`].
304struct CacheTreeRebuild<'a> {
305    odb: &'a Odb,
306    flags: WriteTreeFlags,
307    /// Cached tree ids already confirmed present (see [`present_reusable_trees`]).
308    reusable: OidHashSet,
309}
310
311/// Rebuild one cache-tree node and the index rows it covers.
312///
313/// Returns the number of index rows consumed and whether `node.oid` is known to exist
314/// in the object store (reused, just written, or found by a repair probe), in which
315/// case the parent does not probe it again.
316fn 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    // Members whose existence is still unconfirmed, probed as one batch below.
396    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
508/// Whether `node` and every descendant are valid and their trees are in `reusable`.
509fn 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/// Reset the tree-object write counter on `odb` (unit tests only).
529#[cfg(test)]
530pub fn test_reset_tree_write_count(odb: &Odb) {
531    odb.hot_path_metrics().reset_tree_writes();
532}
533
534/// Tree objects written via [`store_tree_payload`] on `odb` since the last reset (unit tests only).
535#[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/// Returns whether `oid` is Git's canonical empty tree (including the legacy hash).
563#[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
579/// Build and write tree object(s) from index entries and return the tree OID.
580///
581/// The `prefix` argument optionally limits the write to a subtree path.
582/// Like [`write_tree_from_index`], but only index entries whose path is listed in `paths`
583/// (repository-relative, as stored in the index) are included in the tree.
584pub 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
605/// Build and write tree object(s) from index entries and return the tree OID.
606///
607/// Unlike [`write_tree_update_index`], this does not read or update the index cache-tree
608/// extension. Callers that need incremental reuse must use [`write_tree_update_index`] on a
609/// mutable index instead.
610pub 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
636/// Build a valid cache-tree extension from the index and write any missing tree objects.
637///
638/// # Errors
639///
640/// Returns an error if tree object creation fails.
641pub 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
652/// Build a cache-tree directly from a **tree object**, preserving Git's raw `entry_count`
653/// semantics. Unlike [`build_cache_tree_from_index`], the per-node `entry_count` is the recursive
654/// number of non-tree entries *as recorded in the tree* — duplicate path entries are counted
655/// separately, exactly as upstream Git's cache-tree does after reading such a tree into the index.
656///
657/// This is the cache-tree real Git attaches after `read-tree`/`reset`/`checkout` populate the index
658/// from a tree. For a tree with duplicate entries (`t4058-diff-duplicates`) the resulting
659/// `entry_count` exceeds the number of (deduplicated) index entries, so [`verify_cache_tree`] later
660/// reports "corrupted cache-tree has entries not present in index".
661pub 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    // Cache-tree children are stored sorted by name (plain byte order, no tree/dir suffix rule).
687    children.sort_by(|a, b| a.name.cmp(&b.name));
688    Ok(CacheTreeNode::valid(name, entry_count, *tree_oid, children))
689}
690
691/// Walk a cache-tree against `index`, mirroring Git's `verify_one` / `cache_tree_verify`.
692///
693/// Returns an error describing the first inconsistency. The only condition exercised by
694/// `t4058-diff-duplicates` is a node whose `entry_count` runs past the end of the index
695/// (`entry_count + pos > cache_nr`), which yields the exact upstream message
696/// "corrupted cache-tree has entries not present in index".
697///
698/// This is gated by callers behind `GIT_TEST_CHECK_CACHE_TREE`, matching upstream's
699/// `write_locked_index`.
700pub fn verify_cache_tree(index: &Index) -> Result<()> {
701    let Some(root) = index.cache_tree.as_ref() else {
702        return Ok(());
703    };
704    // Same rows as write-tree / incremental cache-tree updates (includes sparse-directory placeholders).
705    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    // Position of the first cache entry whose path is at/under this node's directory prefix.
728    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
885/// Build a tree for a **partial** commit: paths listed in `paths_from_index` (repository-relative,
886/// UTF-8 path bytes) are taken from `index`; every other path is copied from `base_tree_oid`
887/// (typically `HEAD^{tree}`).
888///
889/// This matches Git's behaviour when committing with pathspecs while the index contains additional
890/// staged paths: the commit tree merges `HEAD` with only the pathspec-selected index updates.
891pub 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                // No index entry: path was removed — omit from the merged tree.
971            } 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        // One batched reuse check of the other `dirs - 1` cached directories, then the
1622        // changed blob and the write-skip check of the two rewritten trees (`d0` and the
1623        // root). The reused directories were confirmed by that batch and the new `d0`
1624        // tree was just written, so neither is probed again.
1625        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}