Skip to main content

git_policy_cli/
pack_keep.rs

1//! What:
2//!  The `.keep` files that protect a landing's migrated pack until the compare-and-swap,
3//!       named by the message `cli-git <transaction-id>`.
4//! Why:
5//!  `git index-pack --keep=<message>` writes the `.keep` before the pack becomes visible,
6//!  so
7//!      no prune or repack drops the objects mid-landing;
8//!  recovery finds a dead transaction's
9//!      `.keep` by its message even before a landing record names the pack
10//!      (`src/policy-engine/commit-landing-objects.ts`).
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! await removeTransactionKeeps({ objectDirectory, transactionId });
15//! ```
16
17/// The fail-closed recovery failure.
18use super::recovery_error::{RecoveryError, io_failure};
19/// Single-file removal.
20use super::recovery_files::remove_file_if_present;
21/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
22use std::path::{Path, PathBuf};
23
24/// What:
25///  The keep message naming one transaction.
26/// Why:
27///   Recovery matches `.keep` contents against it.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// transactionKeepMessage(transactionId) // `cli-git ${transactionId}`
32/// ```
33pub fn transaction_keep_message(transaction_id: &str) -> String {
34    return format!("cli-git {transaction_id}");
35}
36
37/// What:
38///  Remove the `.keep` of one migrated pack.
39/// Why:
40///   `removePackKeep`:
41///  after `ref-updated.json`,
42///  and after a failed compare-and-swap.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// await removePackKeep({ objectDirectory, packName });
47/// ```
48pub fn remove_pack_keep(object_directory: &Path, pack_name: &str) -> Result<(), RecoveryError> {
49    return remove_file_if_present(
50        object_directory
51            .join("pack")
52            .join(format!("pack-{pack_name}.keep"))
53            .as_path(),
54    );
55}
56
57/// What:
58///  Remove every `.keep` whose message names the transaction;
59///  returns how many.
60/// Why:
61///   `removeTransactionKeeps`:
62///  a `.keep` holds the message and a newline,
63///  exactly;
64///  a file
65///       that vanished meanwhile is skipped.
66///
67/// In TS you'd write (pseudocode):
68/// ```ts
69/// async function removeTransactionKeeps({ objectDirectory, transactionId }): Promise<number>;
70/// ```
71pub fn remove_transaction_keeps(
72    object_directory: &Path,
73    transaction_id: &str,
74) -> Result<usize, RecoveryError> {
75    let pack_directory: PathBuf = object_directory.join("pack");
76    let listing: std::fs::ReadDir = match std::fs::read_dir(&pack_directory) {
77        Ok(found) => found,
78        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
79        Err(error) => return Err(io_failure("listing", pack_directory.as_path(), &error)),
80    };
81    let expected: String = format!("{}\n", transaction_keep_message(transaction_id));
82    // `mut` allows collecting owned keeps before removing them.
83    let mut owned: Vec<PathBuf> = Vec::new();
84    for item in listing {
85        let entry: std::fs::DirEntry = match item {
86            Ok(found) => found,
87            Err(error) => return Err(io_failure("listing", pack_directory.as_path(), &error)),
88        };
89        if !entry.file_name().to_string_lossy().ends_with(".keep") {
90            continue;
91        }
92        let path: PathBuf = entry.path();
93        let contents: Vec<u8> = match std::fs::read(&path) {
94            Ok(bytes) => bytes,
95            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
96            Err(error) => return Err(io_failure("reading", path.as_path(), &error)),
97        };
98        if contents == expected.as_bytes() {
99            owned.push(path);
100        }
101    }
102    for path in &owned {
103        remove_file_if_present(path.as_path())?;
104    }
105    return Ok(owned.len());
106}
107
108/// Keep-file controls stay out of the release executable.
109#[cfg(test)]
110#[path = "pack_keep_tests.rs"]
111mod tests;