Skip to main content

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;