Skip to main content

git_policy_cli/
standards_review_append.rs

1//! What:
2//!  How the service adds a review entry to the shadow log of a worktree that people and agents are editing at the same
3//!  time: append only, never create, never rewrite, once per commit, with a bounded repair of entries another writer
4//!  overwrote.
5//! Why:
6//!  `doc/audit/standards-review-gate-shadow.md` is a tracked file that sessions edit while the service appends to it, and
7//!  no lock may be taken. One `O_APPEND` write of the whole entry cannot clobber anyone's content and cannot interleave
8//!  with another append. What an append cannot prevent is another writer that read the file before it and writes the
9//!  whole file back after it, which loses the entry. Each entry is therefore also kept as a plain file in the Git
10//!  directory, and every delivery checks the latest few entries are still in the log and appends the missing ones again.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! if (!(await readFile(log)).includes(sha12)) await appendFile(log, text); await repairRecent(log, spool);
15//! ```
16
17use super::private_storage::{create_private_directory_all, write_private_file_unsynced};
18use super::standards_review_entry::SHORT_SHA_LENGTH;
19use std::io::Write;
20use std::path::{Path, PathBuf};
21use std::time::SystemTime;
22
23/// Spooled entries the repair checks against the log on each delivery.
24pub const REPAIR_WINDOW: usize = 16;
25
26/// Spooled entries kept; older ones are removed so the spool stays small.
27pub const SPOOL_KEPT: usize = 4 * REPAIR_WINDOW;
28
29/// What a delivery did.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum Delivery {
32    /// The entry was appended.
33    Appended,
34    /// The log already names this commit, so nothing was added.
35    AlreadyThere,
36}
37
38/// What:
39///  The spool directory of one worktree under a Git directory: entries of different worktrees are never mixed, because
40///  each worktree's log holds the history of its own branch.
41/// Why:
42///   A worktree on another branch lacks commits that a sibling worktree made, and must not be given their entries.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// join(commonDir, 'cli-git/review-entries', fnv1a(repository))
47/// ```
48pub fn spool_directory(common_dir: &Path, repository: &str) -> PathBuf {
49    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
50    for byte in repository.bytes() {
51        hash ^= u64::from(byte);
52        hash = hash.wrapping_mul(0x0100_0000_01b3);
53    }
54    return common_dir.join("cli-git").join("review-entries").join(format!("{hash:016x}"));
55}
56
57/// The text the log is searched for: the first characters of the commit name, which survive a reflow of the entry.
58fn token(sha: &str) -> String {
59    return sha.chars().take(SHORT_SHA_LENGTH).collect();
60}
61
62/// Whether the log names the commit already; a log that cannot be read names nothing.
63pub fn names_commit(log: &Path, sha: &str) -> bool {
64    return std::fs::read_to_string(log).is_ok_and(|text| return text.contains(token(sha).as_str()));
65}
66
67/// One whole-entry append to a log that must already exist.
68fn append_existing(log: &Path, text: &str) -> std::io::Result<()> {
69    let mut file: std::fs::File = std::fs::OpenOptions::new().append(true).create(false).open(log)?;
70    return file.write_all(text.as_bytes());
71}
72
73/// The spooled entries, newest first, as (commit token, path, text).
74fn spooled(spool: &Path) -> Vec<(String, PathBuf, String)> {
75    let mut found: Vec<(SystemTime, String, PathBuf)> = Vec::new();
76    if let Ok(listing) = std::fs::read_dir(spool) {
77        for entry in listing.flatten() {
78            let modified: SystemTime = entry.metadata().and_then(|meta| return meta.modified()).unwrap_or(SystemTime::UNIX_EPOCH);
79            found.push((modified, entry.file_name().to_string_lossy().into_owned(), entry.path()));
80        }
81    }
82    found.sort_by(|left, right| return right.0.cmp(&left.0).then_with(|| return right.1.cmp(&left.1)));
83    return found
84        .into_iter()
85        .map(|(_, name, path)| return (name, path.clone(), std::fs::read_to_string(path).unwrap_or_default()))
86        .collect();
87}
88
89/// What:
90///  Append again the newest spooled entries whose commit the log no longer names, and remove spooled entries beyond
91///  `SPOOL_KEPT`. Returns how many entries were appended again.
92/// Why:
93///   This is the repair for an append another writer overwrote; it looks at a bounded number of entries per call.
94///
95/// In TS you'd write (pseudocode):
96/// ```ts
97/// for (const entry of spooled.slice(0, window)) if (!log.includes(entry.token)) await appendFile(log, entry.text);
98/// ```
99pub fn repair_recent(log: &Path, spool: &Path) -> usize {
100    let entries: Vec<(String, PathBuf, String)> = spooled(spool);
101    let mut present: String = std::fs::read_to_string(log).unwrap_or_default();
102    let mut repaired: usize = 0;
103    for (name, _, text) in entries.iter().take(REPAIR_WINDOW) {
104        if text.is_empty() || present.contains(name.as_str()) {
105            continue;
106        }
107        if append_existing(log, text.as_str()).is_ok() {
108            present.push_str(text.as_str());
109            repaired += 1;
110        }
111    }
112    for (_, path, _) in entries.iter().skip(SPOOL_KEPT) {
113        let _removed = std::fs::remove_file(path);
114    }
115    return repaired;
116}
117
118/// What:
119///  Deliver one entry for `sha` to `log`: nothing if the log already names the commit, otherwise keep the entry in the
120///  spool, append it, then repair the latest few entries.
121/// Why:
122///   Keeping the commit-name test first makes a repeated item (a crash after the append, before the item was settled)
123///   add nothing.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// await deliver({ log, spool, sha, text })
128/// ```
129pub fn deliver(log: &Path, spool: Option<&Path>, sha: &str, text: &str) -> Result<Delivery, String> {
130    let current: String = std::fs::read_to_string(log).map_err(|error| return format!("reading {} failed: {error}", log.display()))?;
131    let name: String = token(sha);
132    if current.contains(name.as_str()) {
133        return Ok(Delivery::AlreadyThere);
134    }
135    if let Some(directory) = spool {
136        create_private_directory_all(directory).map_err(|error| return format!("creating the entry spool failed: {error}"))?;
137        let kept: std::io::Result<()> = write_private_file_unsynced(directory.join(name.as_str()).as_path(), text.as_bytes());
138        if let Err(error) = kept && error.kind() != std::io::ErrorKind::AlreadyExists {
139            return Err(format!("keeping the entry failed: {error}"));
140        }
141    }
142    append_existing(log, text).map_err(|error| return format!("appending to {} failed: {error}", log.display()))?;
143    if let Some(directory) = spool {
144        let _repaired = repair_recent(log, directory);
145    }
146    return Ok(Delivery::Appended);
147}
148
149/// Controls stay out of the release executable.
150#[cfg(test)]
151#[path = "standards_review_append_tests.rs"]
152mod tests;