git_policy_cli/subvolume/subvolume_entry_count.rs
1//! What:
2//! The count of the entries under a checkout: every file, directory and link below its root, counted once each.
3//! Why:
4//! The free space a migration needs is the metadata of its entries (`subvolume_preflight.rs`), so the refusal for too little
5//! space starts from how many entries the copy will write. The walk reads directory listings only, no file contents, and
6//! follows no link, as the copy pass does not.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! const entries: number = countEntries(root); // readdir on each directory, one count per name
11//! ```
12
13use std::path::{Path, PathBuf};
14
15/// What:
16/// How many entries are below `root`.
17/// `&Path` borrows the path read-only (TS: a `string` argument); `u64` is an unsigned 64-bit integer (siblings: `u32`,
18/// `usize`).
19/// Why:
20/// A directory that cannot be listed (gone, or not readable) adds nothing, because the copy reports an unreadable
21/// directory with its own message and the preflight must not duplicate it. `u64` rather than `usize` because the figure
22/// is multiplied into a byte count that is stored as `u64` in the filesystem facts.
23/// Gotcha:
24/// A link to a directory counts as one entry and its target is not walked, unlike a TS `readdir` recursion that follows
25/// `stat`; the work stack below replaces recursion so a deep tree cannot overflow the call stack.
26///
27/// In TS you'd write (pseudocode):
28/// ```ts
29/// function countEntries(root: string): number {
30/// let count = 0;
31/// const pending: string[] = [root];
32/// while (pending.length > 0) {
33/// const directory = pending.pop()!;
34/// let names: Dirent[] = [];
35/// try { names = readdirSync(directory, { withFileTypes: true }); } catch { continue; }
36/// for (const name of names) { count += 1; if (name.isDirectory()) pending.push(join(directory, name.name)); }
37/// }
38/// return count;
39/// }
40/// ```
41pub fn count_entries(root: &Path) -> u64 {
42 // What: `Vec<PathBuf>` is a growable list (TS: `string[]`) of owned paths; `vec![...]` builds it with one item.
43 // Why: The directories still to list; a list used as a stack walks the tree without recursion.
44 //
45 // In TS you'd write (pseudocode):
46 // ```ts
47 // const pending: string[] = [root];
48 // ```
49 let mut pending: Vec<PathBuf> = vec![root.to_path_buf()];
50 let mut count: u64 = 0;
51 // What: `while let Some(directory) = pending.pop()` runs while `pop()` gives `Some(...)`, the present variant of
52 // `Option`; it stops at `None`, the empty list.
53 // Why: Take the next directory off the stack until none is left.
54 //
55 // In TS you'd write (pseudocode):
56 // ```ts
57 // let directory: string | undefined;
58 // while ((directory = pending.pop()) !== undefined) { ... }
59 // ```
60 while let Some(directory) = pending.pop() {
61 // What: `let Ok(listing) = ... else { continue };` unpacks the success variant of the `Result` of `read_dir`, and
62 // runs the `else` block (skip this directory) when it is the failure variant. `&directory` lends the path.
63 // Why: An unlistable directory adds nothing and does not stop the count.
64 //
65 // In TS you'd write (pseudocode):
66 // ```ts
67 // let listing; try { listing = readdirSync(directory); } catch { continue; }
68 // ```
69 let Ok(listing) = std::fs::read_dir(&directory) else { continue };
70 // What: `listing.flatten()` walks the `Result` items of the listing and drops the failed ones, keeping the `DirEntry`
71 // of each name that was read.
72 // Why: A name that vanished between the listing and the read is not counted, as in the copy pass.
73 //
74 // In TS you'd write (pseudocode):
75 // ```ts
76 // for (const entry of safeEntries(listing)) { ... }
77 // ```
78 for entry in listing.flatten() {
79 count += 1;
80 // What: `entry.file_type()` is the type of the entry without following a link; `.ok()` turns its `Result` into an
81 // `Option` that is `None` on failure; `is_some_and(...)` is true when it is `Some` and the closure `|kind|
82 // ...` returns true.
83 // Why: Only a real directory is walked further; a link to a directory is one entry and nothing below it.
84 //
85 // In TS you'd write (pseudocode):
86 // ```ts
87 // if (entry.isDirectory()) pending.push(join(directory, entry.name)); // isDirectory() is false for a symlink
88 // ```
89 let is_directory: bool = entry.file_type().ok().is_some_and(|kind: std::fs::FileType| { return kind.is_dir(); });
90 if is_directory {
91 pending.push(entry.path());
92 }
93 }
94 }
95 // What: `return count;` hands the `u64` total back to the caller.
96 // Why: The preflight multiplies it by the cost of an entry.
97 //
98 // In TS you'd write (pseudocode):
99 // ```ts
100 // return count;
101 // ```
102 return count;
103}
104
105/// Controls for the count stay out of the release executable.
106#[cfg(test)]
107#[path = "subvolume_entry_count_tests.rs"]
108mod tests;