Skip to main content

git_policy_cli/subvolume/
subvolume_index_files.rs

1//! What:
2//!  The linked worktrees of a main checkout, found from `.git/worktrees/*/gitdir`, and the stat rewrite applied to the
3//!  index files of the checkout and of those worktrees on disk.
4//! Why:
5//!  The subvolume migration copies the main checkout and the linked worktrees inside it, so each of their indexes needs
6//!  the rewrite of `subvolume_index.rs`. A file in `.git/worktrees/<name>/gitdir` holds the path of the worktree's
7//!  `.git` file, so the worktree is its parent directory (the same reading the migration script used). An index is
8//!  replaced through `index.lock` as Git replaces it, so a running Git command is never raced: the lock is taken
9//!  before the index is read, and when it exists the index is left as copied and the report says so. The migration
10//!  holds the locks itself while it completes the new checkout, and its rewrite then runs under them.
11//!
12//! Why the lock stays:
13//!  `index.lock` is Git's protocol for replacing an index; the rewrite reads the index, computes new stat fields and renames
14//!  a file over it, which is lost if a Git writer replaces the index in between, and only Git's own lock excludes Git. The
15//!  rewrite takes it with `O_EXCL`, never waits (a lock that exists skips the index and says so), holds it for one index and
16//!  only from the read to the rename, and gives it back as Git does. Concurrency among this wrapper's own commands never
17//!  reaches this file: `single_owner_claim.rs` gives one command a checkout or a worktree without a lock.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! for (const { adminDirectory, worktree } of linkedWorktrees(root)) refreshIndexFile(join(adminDirectory, 'index'), worktree);
22//! ```
23
24use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
25use crate::parallel_runner::{Event, OrderedRun, run_ordered};
26use crate::subvolume_index::{IndexOutcome, RewriteContext, StatFields, StoredStat, rewrite_index_stat_with};
27use crate::subvolume_lock::is_ours;
28use std::ffi::OsStr;
29use std::io::Write;
30use std::os::unix::ffi::OsStrExt;
31use std::os::unix::fs::MetadataExt;
32use std::path::{Path, PathBuf};
33
34/// What:
35///  One linked worktree of the checkout.
36/// Why:
37///   The index lives in the administrative directory; the files it describes live in the worktree.
38///
39/// In TS you'd write (pseudocode):
40/// ```ts
41/// type LinkedWorktree = { adminDirectory: string; worktree: string };
42/// ```
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct LinkedWorktree {
45    /// `<root>/.git/worktrees/<name>`.
46    pub admin_directory: PathBuf,
47    /// The directory the worktree's files are in.
48    pub worktree: PathBuf,
49}
50
51/// What:
52///  The linked worktrees of `root` whose directory is inside `root` and exists.
53/// Why:
54///   Only worktrees inside the checkout are copied with it. An administrative directory without a readable `gitdir`
55///  file belongs to a worktree Git would prune, so it is skipped.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// function linkedWorktreesInside(root: string): LinkedWorktree[];
60/// ```
61pub fn linked_worktrees_inside(root: &Path) -> Vec<LinkedWorktree> {
62    let admin: PathBuf = root.join(".git").join("worktrees");
63    let Ok(names) = std::fs::read_dir(&admin) else { return Vec::new() };
64    let mut found: Vec<LinkedWorktree> = Vec::new();
65    for name in names.flatten() {
66        let Ok(recorded) = std::fs::read(name.path().join("gitdir")) else { continue };
67        // The recorded path is raw bytes: a directory named with bytes that are not UTF-8 is a valid path on Linux.
68        let Some(worktree) = Path::new(OsStr::from_bytes(recorded.trim_ascii())).parent() else { continue };
69        if worktree.starts_with(root) && worktree != root && worktree.is_dir() {
70            found.push(LinkedWorktree { admin_directory: name.path(), worktree: worktree.to_path_buf() });
71        }
72    }
73    found.sort_by(|left: &LinkedWorktree, right: &LinkedWorktree| { return left.worktree.cmp(&right.worktree); });
74    return found;
75}
76
77/// What:
78///  What `lstat` of `tree/path` reports now, in index units, or nothing for a file that cannot be examined.
79/// Why:
80///   The rewrite leaves an entry as Git recorded it when its file is missing.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// function statFields(tree: string, path: Uint8Array): StatFields | undefined;
85/// ```
86pub(crate) fn stat_fields(tree: &Path, path: &[u8]) -> Option<StatFields> {
87    let metadata: std::fs::Metadata = std::fs::symlink_metadata(tree.join(OsStr::from_bytes(path))).ok()?;
88    return Some(fields_of(&metadata));
89}
90
91/// What:
92///  The stat fields of metadata in hand, in index units.
93/// Why:
94///   A caller that holds the object it made records the fields of that object, not of whatever its path names later.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// function fieldsOf(stats: Stats): StatFields;
99/// ```
100pub(crate) fn fields_of(metadata: &std::fs::Metadata) -> StatFields {
101    return StatFields {
102        ctime_seconds: metadata.ctime() as u32,
103        ctime_nanos: metadata.ctime_nsec() as u32,
104        mtime_seconds: metadata.mtime() as u32,
105        mtime_nanos: metadata.mtime_nsec() as u32,
106        dev: metadata.dev() as u32,
107        ino: metadata.ino() as u32,
108        uid: metadata.uid(),
109        gid: metadata.gid(),
110        size: metadata.size() as u32,
111    };
112}
113
114/// What:
115///  How the rewrite of one index file is protected from a Git command that writes the same index.
116///  `&'a str` borrows the token of the migration whose lock is already held.
117/// Why:
118///   Git replaces an index through `index.lock`. A rewrite that runs by itself takes that lock, as Git does, before it
119///  reads the index and holds it until the new index is in place. The migration already holds the lock of every index
120///  while it completes the new checkout, so its rewrite uses the held lock instead of creating one.
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// type IndexLock = { kind: 'acquire' } | { kind: 'held'; token: string };
125/// ```
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum IndexLock<'a> {
128    /// Create `index.lock` for the duration of the rewrite and remove it after.
129    Acquire,
130    /// `index.lock` holds this token already; leave it in place for its owner to release.
131    Held(&'a str),
132}
133
134/// What:
135///  What a rewrite of one index file is told beyond the paths.
136///  `&'a [PathBuf]` borrows the list of trees.
137/// Why:
138///   The lock decides how the index is protected, the references decide which entries Git recorded for the files the
139///  copy was made from, and the hash length (read from the repository's configuration) decides how an index with an
140///  all-zero checksum is read.
141///
142/// In TS you'd write (pseudocode):
143/// ```ts
144/// type RefreshOptions = { lock: IndexLock; references: string[]; hashLength?: number };
145/// ```
146#[derive(Clone, Copy, Debug)]
147pub struct RefreshOptions<'a> {
148    /// How the index is locked while it is rewritten.
149    pub lock: IndexLock<'a>,
150    /// Trees whose files the index may describe (the tree the copy was made from). An entry is refreshed only if the
151    ///  file in one of them still has the stat fields the entry records; empty means every entry may be refreshed.
152    pub references: &'a [PathBuf],
153    /// The repository's hash length in bytes, when known.
154    pub hash_length: Option<usize>,
155}
156
157/// Whether a file's present stat equals what an entry recorded (the device is not compared: a subvolume has its own).
158pub(crate) fn matches_stored(now: &StatFields, stored: &StoredStat) -> bool {
159    return now.size == stored.size
160        && now.ino == stored.ino
161        && now.uid == stored.uid
162        && now.gid == stored.gid
163        && now.ctime_seconds == stored.ctime_seconds
164        && (stored.ctime_nanos == 0 || now.ctime_nanos == stored.ctime_nanos)
165        && now.mtime_seconds == stored.mtime_seconds
166        && (stored.mtime_nanos == 0 || now.mtime_nanos == stored.mtime_nanos);
167}
168
169/// What:
170///  Rewrite one index file in place and report what happened in one line.
171/// Why:
172///   A missing index is not an error (a worktree never checked out has none); a held `index.lock` and every declined
173///  shape are reported, not hidden, because the cost is a slow first `git status` there. The lock comes first, then
174///  the read, so a Git command that installs a newer index in between is never overwritten with an older one.
175///
176/// In TS you'd write (pseudocode):
177/// ```ts
178/// function refreshIndexFileWith(indexPath: string, tree: string, options: RefreshOptions): string;
179/// ```
180pub fn refresh_index_file_with(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>) -> String {
181    return refresh_index_file_seamed(index_path, tree, options, &mut || {});
182}
183
184/// `refresh_index_file_with` with a call at the moment right after the rewrite, before the lock is looked at again, where a
185///  control may let another command take the lock name.
186pub fn refresh_index_file_seamed(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, after_rewrite: &mut dyn FnMut()) -> String {
187    return refresh_index_file_report_seamed(index_path, tree, options, after_rewrite).line;
188}
189
190/// How a refresh ended, apart from the words that describe it.
191#[derive(Clone, Copy, Debug, Eq, PartialEq)]
192pub enum RefreshStatus {
193    /// The new index was installed.
194    Refreshed,
195    /// There is no index file, which is nothing to refresh and no failure.
196    NoIndex,
197    /// The index was left as it was, for the reason in the line.
198    Skipped,
199}
200
201/// A refresh's line for the person and its status for the caller.
202#[derive(Clone, Debug, Eq, PartialEq)]
203pub struct RefreshReport {
204    /// The one-line report.
205    pub line: String,
206    /// How it ended.
207    pub status: RefreshStatus,
208}
209
210/// `refresh_index_file_with` that says how it ended, so a caller that must not go on after a failed refresh can stop.
211pub fn refresh_index_file_report(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>) -> RefreshReport {
212    return refresh_index_file_report_seamed(index_path, tree, options, &mut || {});
213}
214
215/// `refresh_index_file_report` with a call at the moment right after the rewrite.
216pub fn refresh_index_file_report_seamed(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, after_rewrite: &mut dyn FnMut()) -> RefreshReport {
217    let mut lock_name: std::ffi::OsString = index_path.as_os_str().to_os_string();
218    lock_name.push(".lock");
219    let lock_path: PathBuf = PathBuf::from(lock_name);
220    let mut created: Option<std::fs::File> = None;
221    match options.lock {
222        IndexLock::Acquire => match std::fs::OpenOptions::new().write(true).create_new(true).open(&lock_path) {
223            Ok(opened) => created = Some(opened),
224            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
225                return RefreshReport { line: String::from("skipped (index.lock exists: a Git command is running there; its first status will be slow)"), status: RefreshStatus::Skipped };
226            }
227            Err(error) => return RefreshReport { line: format!("skipped (cannot create the lock: {error})"), status: RefreshStatus::Skipped },
228        },
229        IndexLock::Held(token) => {
230            if !is_ours(lock_path.as_path(), token) {
231                return RefreshReport { line: String::from("skipped (index.lock is held by another Git command; its first status will be slow)"), status: RefreshStatus::Skipped };
232            }
233        }
234    }
235    let (outcome, status): (String, RefreshStatus) = rewrite_under_lock(index_path, tree, options, created.as_mut(), &lock_path);
236    after_rewrite();
237    // Once the lock was renamed over the index the name is any Git command's to take; it is never touched again. Before that,
238    // the lock is removed only if the name still holds the file this call made.
239    if let (Some(file), true) = (created.as_ref(), status != RefreshStatus::Refreshed) {
240        remove_if_same_file(lock_path.as_path(), file);
241    }
242    return RefreshReport { line: outcome, status };
243}
244
245/// Remove the file at `path` if it is the very file `held` is open on (same device and inode).
246fn remove_if_same_file(path: &Path, held: &std::fs::File) {
247    let same: bool = match (std::fs::symlink_metadata(path), held.metadata()) {
248        (Ok(named), Ok(own)) => named.dev() == own.dev() && named.ino() == own.ino() && named.file_type().is_file(),
249        _ => false,
250    };
251    if same {
252        let _removed = std::fs::remove_file(path);
253    }
254}
255
256/// The read, rewrite and install of one index while its lock is held; `lock_file` is the lock when this call made it. The flag
257///  says whether the new index was published, after which the lock name is not this call's any more.
258fn rewrite_under_lock(index_path: &Path, tree: &Path, options: &RefreshOptions<'_>, lock_file: Option<&mut std::fs::File>, lock_path: &Path) -> (String, RefreshStatus) {
259    let index_mtime: Option<(u32, u32)> = match std::fs::metadata(index_path) {
260        Ok(metadata) => Some((metadata.mtime() as u32, metadata.mtime_nsec() as u32)),
261        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return (String::from("no index file"), RefreshStatus::NoIndex),
262        Err(error) => return (format!("skipped (cannot examine it: {error})"), RefreshStatus::Skipped),
263    };
264    let bytes: Vec<u8> = match std::fs::read(index_path) {
265        Ok(found) => found,
266        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return (String::from("no index file"), RefreshStatus::NoIndex),
267        Err(error) => return (format!("skipped (cannot read it: {error})"), RefreshStatus::Skipped),
268    };
269    return rewrite_and_install(bytes.as_slice(), index_mtime, [index_path, tree, lock_path], options, lock_file);
270}
271
272/// The rewrite of the stat fields of `bytes` and the install of the result over `index_path`, with `paths` naming the index, the tree it
273///  describes and the lock file.
274fn rewrite_and_install(bytes: &[u8], index_mtime: Option<(u32, u32)>, paths: [&Path; 3], options: &RefreshOptions<'_>, lock_file: Option<&mut std::fs::File>) -> (String, RefreshStatus) {
275    let [index_path, tree, lock_path]: [&Path; 3] = paths;
276    let context: RewriteContext = RewriteContext { hash_length: options.hash_length, index_mtime };
277    let outcome: IndexOutcome = rewrite_index_stat_with(
278        bytes,
279        &context,
280        &mut |path: &[u8]| { return stat_fields(tree, path); },
281        &mut |path: &[u8], stored: &StoredStat| {
282            if options.references.is_empty() {
283                return true;
284            }
285            return options.references.iter().any(|reference: &PathBuf| {
286                return stat_fields(reference.as_path(), path).is_some_and(|now: StatFields| { return matches_stored(&now, stored); });
287            });
288        },
289    );
290    let (rewritten, updated, total): (Vec<u8>, usize, usize) = match outcome {
291        IndexOutcome::Rewritten { bytes: new_bytes, updated, total } => (new_bytes, updated, total),
292        IndexOutcome::Skipped(reason) => return (format!("skipped ({reason})"), RefreshStatus::Skipped),
293    };
294    // With its own lock the new index is written into the lock file and the lock is renamed over the index, as Git does.
295    // Under a held lock the lock stays, and a sibling file is renamed over the index instead.
296    let installed: std::io::Result<()> = match lock_file {
297        Some(file) => file.write_all(rewritten.as_slice()).and_then(|()| { return file.sync_all(); }).and_then(|()| { return std::fs::rename(lock_path, index_path); }),
298        None => stage_and_replace(index_path, rewritten.as_slice()),
299    };
300    if let Err(error) = installed {
301        return (format!("skipped (cannot install the new index: {error})"), RefreshStatus::Skipped);
302    }
303    return (format!("{updated} of {total} entries refreshed"), RefreshStatus::Refreshed);
304}
305
306/// What:
307///  Write `bytes` to a staging file beside `index_path`, created exclusively, and rename it over the index.
308/// Why:
309///   Under a held lock no Git command writes the index, but the staging name is still checked: it is created with
310///  `O_EXCL`, which fails on an existing file or link instead of following or truncating it, written and synced through its
311///  own descriptor, and renamed only while the name still holds that file. A staging file that cannot be proven the call's
312///  is left where it is.
313///
314/// In TS you'd write (pseudocode):
315/// ```ts
316/// await stageAndReplace(indexPath, bytes);
317/// ```
318fn stage_and_replace(index_path: &Path, bytes: &[u8]) -> std::io::Result<()> {
319    let mut sibling_name: std::ffi::OsString = index_path.as_os_str().to_os_string();
320    sibling_name.push(".cli-git-new");
321    let sibling: PathBuf = PathBuf::from(sibling_name);
322    let mut staged: std::fs::File = std::fs::OpenOptions::new().write(true).create_new(true).open(&sibling)?;
323    let written: std::io::Result<()> = staged.write_all(bytes).and_then(|()| { return staged.sync_all(); }).and_then(|()| {
324        let named = std::fs::symlink_metadata(&sibling)?;
325        let own = staged.metadata()?;
326        if named.dev() != own.dev() || named.ino() != own.ino() {
327            return Err(std::io::Error::other("the staging file was replaced while the index was written"));
328        }
329        return std::fs::rename(&sibling, index_path);
330    });
331    if written.is_err() {
332        remove_if_same_file(sibling.as_path(), &staged);
333    }
334    return written;
335}
336
337/// What:
338///  Install `seed` as the index of the new worktree whose administrative directory is the open handle `admin`, with the stat
339///  fields of `stats` for the entries it knows, and nothing for the others.
340/// Why:
341///   The index of a new worktree must never be written in place and never replace one that exists: `index.lock` is created
342///  first (exclusively), the absence of an index is checked under it, the finished index is written into the lock, and
343///  that open file is linked under the name of the index without replacing anything; the lock name is released afterwards
344///  only if it still holds that file, because once it is released it is another Git process's to take. The stat fields come from the objects the caller made, so an entry whose file was
345///  replaced afterwards no longer matches its inode and Git hashes it. Everything is done through the handle.
346///
347/// In TS you'd write (pseudocode):
348/// ```ts
349/// installIndexIn(admin, seed, stats, hashLength); // throws a reason
350/// ```
351pub fn install_index_in(admin: &std::fs::File, seed: &[u8], stats: &mut dyn FnMut(&[u8]) -> Option<StatFields>, hash_length: usize) -> Result<String, String> {
352    return install_index_in_with(admin, seed, stats, hash_length, &mut InstallSeams { before_publish: &mut || {}, after_publish: &mut || {} });
353}
354
355/// The moments of an index install at which the controls move the tree.
356pub struct InstallSeams<'a> {
357    /// Called after the index was written into the lock and before it is linked under its name.
358    pub before_publish: &'a mut dyn FnMut(),
359    /// Called right after the index got its name and before the lock is released.
360    pub after_publish: &'a mut dyn FnMut(),
361}
362
363/// `install_index_in` with the moments at which a control may move the tree.
364pub fn install_index_in_with(admin: &std::fs::File, seed: &[u8], stats: &mut dyn FnMut(&[u8]) -> Option<StatFields>, hash_length: usize, seams: &mut InstallSeams<'_>) -> Result<String, String> {
365    use crate::fs_beneath::{create_file_at, entry_metadata_at, identity_of, link_open_file_at};
366    let lock: &OsStr = OsStr::new("index.lock");
367    let index: &OsStr = OsStr::new("index");
368    let mut created: std::fs::File = match create_file_at(admin, lock, 0o600) {
369        Ok(opened) => opened,
370        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => return Err(String::from("index.lock exists: a Git command is running there")),
371        Err(error) => return Err(format!("the lock cannot be created ({error})")),
372    };
373    let ours = identity_of(&created).map_err(|error| { return format!("the lock cannot be examined ({error})"); })?;
374    // Before the publication the lock is ours to remove, but only if the name still holds the file made here.
375    let give_up = |reason: String| -> Result<String, String> {
376        let _kept = crate::fs_beneath::remove_if_identical(admin, lock, ours);
377        return Err(reason);
378    };
379    match entry_metadata_at(admin, index) {
380        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
381        Err(error) => return give_up(format!("the index cannot be examined ({error})")),
382        Ok(_) => return give_up(String::from("an index appeared in the new worktree while it was made")),
383    }
384    let now: std::time::Duration = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
385    let context: RewriteContext = RewriteContext { hash_length: Some(hash_length), index_mtime: Some((now.as_secs() as u32, now.subsec_nanos())) };
386    let (rewritten, updated, total): (Vec<u8>, usize, usize) = match rewrite_index_stat_with(seed, &context, stats, &mut |_path: &[u8], _stored: &StoredStat| { return true; }) {
387        IndexOutcome::Rewritten { bytes, updated, total } => (bytes, updated, total),
388        IndexOutcome::Skipped(reason) => return give_up(format!("the index cannot be refreshed ({reason})")),
389    };
390    if let Err(error) = created.write_all(rewritten.as_slice()).and_then(|()| { return created.sync_all(); }) {
391        return give_up(format!("the new index cannot be written ({error})"));
392    }
393    (seams.before_publish)();
394    // The open file is linked under the name of the index, which fails if the name exists: what is published is the file
395    // written here, whatever the lock name refers to by now. The lock stays until the index has its name.
396    if let Err(error) = link_open_file_at(&created, admin, index) {
397        return give_up(format!("the new index cannot be put in place ({error})"));
398    }
399    (seams.after_publish)();
400    // The lock name and the index are the same file now; the lock is released only if the name still holds it, so the lock
401    // of the next Git command is never removed.
402    let _released = crate::fs_beneath::remove_if_identical(admin, lock, ours);
403    return Ok(format!("{updated} of {total} entries refreshed"));
404}
405
406/// What:
407///  Rewrite one index file in place under a lock, trusting every entry; see `refresh_index_file_with`.
408/// Why:
409///   The shape the controls use when nothing else is known about the index.
410///
411/// In TS you'd write (pseudocode):
412/// ```ts
413/// function refreshIndexFileLocked(indexPath: string, tree: string, lock: IndexLock): string;
414/// ```
415pub fn refresh_index_file_locked(index_path: &Path, tree: &Path, lock: IndexLock<'_>) -> String {
416    return refresh_index_file_with(index_path, tree, &RefreshOptions { lock, references: &[], hash_length: None });
417}
418
419/// What:
420///  Rewrite one index file in place under a lock the rewrite takes itself; see `refresh_index_file_locked`.
421/// Why:
422///   The common case: nothing holds the lock yet.
423///
424/// In TS you'd write (pseudocode):
425/// ```ts
426/// function refreshIndexFile(indexPath: string, tree: string): string;
427/// ```
428pub fn refresh_index_file(index_path: &Path, tree: &Path) -> String {
429    return refresh_index_file_locked(index_path, tree, IndexLock::Acquire);
430}
431
432/// What:
433///  The repository's hash length in bytes from its configuration file: 32 when `extensions.objectformat` is `sha256`,
434///  20 when the file has no such setting, nothing when the file cannot be read.
435/// Why:
436///   An index does not say which hash it uses; the configuration does. The reading is the plain `[section]` and
437///  `key = value` grammar, case-insensitive for section and key names, without includes.
438///
439/// In TS you'd write (pseudocode):
440/// ```ts
441/// function repositoryHashLength(gitDirectory: string): 20 | 32 | undefined;
442/// ```
443pub fn repository_hash_length(git_directory: &Path) -> Option<usize> {
444    let text: String = String::from_utf8_lossy(std::fs::read(git_directory.join("config")).ok()?.as_slice()).into_owned();
445    let mut in_extensions: bool = false;
446    for raw in text.lines() {
447        let line: &str = raw.trim();
448        if let Some(header) = line.strip_prefix('[') {
449            in_extensions = header.split(']').next().is_some_and(|name: &str| { return name.trim().eq_ignore_ascii_case("extensions"); });
450            continue;
451        }
452        if !in_extensions {
453            continue;
454        }
455        let Some((key, value)) = line.split_once('=') else { continue };
456        if key.trim().eq_ignore_ascii_case("objectformat") && value.trim().eq_ignore_ascii_case("sha256") {
457            return Some(32);
458        }
459    }
460    return Some(20);
461}
462
463/// The number of copies of an index file one rewrite holds at once: the bytes read, the parsed entries, and the bytes written.
464///  The budget multiplies the largest index file by it to estimate what one rewrite holds.
465///  Measured with `refresh_index_file` on throwaway repositories of this repository's tracked files, resident size read every 0.2 ms
466///  in a fresh process (2026-11-03): an index of 2,786,564 bytes grew the process by 13,123,584 bytes (4.7 copies) and one of
467///  1,066,894 bytes by 7,929,856 bytes (7.4 copies), which is 3.0 copies of the index plus about 4.7 MB that does not grow with
468///  it. Four copies cover the growth for an index over 4.7 MB, the indexes this estimate exists for. Kept at 4 as the coordinator's pick under the owner's delegation, open to the owner's veto (`doc/planning/cli-git-shipped-limits-audit.md`, "Picked for these follow-ups", option A).
469const INDEX_REWRITE_COPIES: u64 = 4;
470
471/// What one rewrite of an index file is: the file, the tree it describes, and the trees its entries were copied from.
472struct IndexTarget {
473    /// The index file.
474    index: PathBuf,
475    /// The tree whose files the index describes.
476    tree: PathBuf,
477    /// The trees the entries were copied from (each is the reference tree joined with the worktree's place in the checkout).
478    references: Vec<PathBuf>,
479}
480
481/// The rewrite of one index file, replaceable so a control can watch or delay it.
482pub type RewriteOne<'a> = dyn Fn(&Path, &Path, &RefreshOptions<'_>) -> String + Sync + 'a;
483
484/// What:
485///  What a rewrite of every index of a checkout is told.
486/// Why:
487///   One value names the inputs: the checkout, how its indexes are locked, the trees they were copied from, the host the
488///  budget is derived from, and the rewrite of one file.
489///
490/// In TS you'd write (pseudocode):
491/// ```ts
492/// type AllIndexes = { root: string; lock: IndexLock; references: string[]; host: HostResources; rewrite: RewriteOne };
493/// ```
494pub struct AllIndexes<'a> {
495    /// The main checkout.
496    pub root: &'a Path,
497    /// How each index is protected while it is rewritten.
498    pub lock: IndexLock<'a>,
499    /// The trees the checkout was copied from; each linked worktree is looked up under the same relative path in them.
500    pub references: &'a [PathBuf],
501    /// The CPUs and memory the budget is derived from.
502    pub host: &'a HostResources,
503    /// The rewrite of one index file.
504    pub rewrite: &'a RewriteOne<'a>,
505}
506
507/// What:
508///  Rewrite the index of the checkout and of every linked worktree inside it, reporting each through `say`.
509///  `&mut dyn FnMut(&str)` is a borrowed callback that receives one report line at a time.
510///  `references` are the trees the checkout was copied from (empty trusts every entry); each linked worktree is looked up
511///  under the same relative path in them.
512/// Why:
513///   The first `git status` of each of them is otherwise a full re-hash.
514///
515/// In TS you'd write (pseudocode):
516/// ```ts
517/// function refreshAllIndexes(root: string, lock: IndexLock, references: string[], say: (line: string) => void): void;
518/// ```
519pub fn refresh_all_indexes(root: &Path, lock: IndexLock<'_>, references: &[PathBuf], say: &mut dyn FnMut(&str)) -> usize {
520    return refresh_all_indexes_with(&AllIndexes { root, lock, references, host: &HostResources::read(), rewrite: &refresh_index_file_with }, say);
521}
522
523/// What:
524///  `refresh_all_indexes` with the host and the rewrite of one file chosen by the caller; returns how many indexes it covered.
525/// Why:
526///   Each index is its own file under its own lock describing its own tree, so the rewrites run side by side within a budget
527///  and are reported in the order the checkout's index first, then each linked worktree's by path.
528///
529/// In TS you'd write (pseudocode):
530/// ```ts
531/// const count = await refreshAllIndexesWith({ root, lock, references, host, rewrite }, say);
532/// ```
533pub fn refresh_all_indexes_with(request: &AllIndexes<'_>, say: &mut dyn FnMut(&str)) -> usize {
534    let AllIndexes { root, lock, references, host, rewrite } = *request;
535    let hash_length: Option<usize> = repository_hash_length(root.join(".git").as_path());
536    let mut targets: Vec<IndexTarget> = vec![IndexTarget { index: root.join(".git").join("index"), tree: root.to_path_buf(), references: references.to_vec() }];
537    for linked in linked_worktrees_inside(root) {
538        let relative: &Path = linked.worktree.strip_prefix(root).unwrap_or(Path::new(""));
539        targets.push(IndexTarget {
540            index: linked.admin_directory.join("index"),
541            tree: linked.worktree.clone(),
542            references: references.iter().map(|reference: &PathBuf| { return reference.join(relative); }).collect(),
543        });
544    }
545    let largest: u64 = targets.iter().map(|target: &IndexTarget| { return std::fs::metadata(&target.index).map_or(0, |found: std::fs::Metadata| { return found.len(); }); }).max().unwrap_or(0);
546    let workers: usize = worker_budget(&BudgetRequest { items: targets.len(), per_item_memory: largest.saturating_mul(INDEX_REWRITE_COPIES), ceiling: None, cap: None, host });
547    let lanes: Vec<Vec<usize>> = (0..targets.len()).map(|index: usize| { return vec![index]; }).collect();
548    run_ordered(OrderedRun {
549        items: targets.as_slice(),
550        lanes: lanes.as_slice(),
551        workers,
552        work: &|_index: usize, target: &IndexTarget, _worker_say: &mut dyn FnMut(&str)| {
553            return rewrite(target.index.as_path(), target.tree.as_path(), &RefreshOptions { lock, references: target.references.as_slice(), hash_length });
554        },
555        on_event: &mut |event: Event<'_, String>| {
556            if let Event::End(index, line) = event {
557                say(format!("index {}: {line}", targets[index].index.display()).as_str());
558            }
559        },
560    });
561    return targets.len();
562}
563
564/// Index file controls stay out of the release executable.
565#[cfg(test)]
566#[path = "subvolume_index_files_tests.rs"]
567mod tests;
568
569/// The controls for indexes rewritten side by side stay out of the release executable.
570#[cfg(test)]
571#[path = "subvolume_index_parallel_tests.rs"]
572mod parallel_tests;