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