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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//! What:
//! The per-worktree capture store:
//! its location,
//! its sequence file and its identity file,
//! read as the incumbent reads them.
//! Why:
//! Every commit transaction captures under a short per-worktree lock and takes the next
//! sequence number,
//! so sequence numbers order the captured disk states of one worktree.
//! Recovery and pruning read the store;
//! both wrappers share it
//! (`src/policy-engine/commit-capture-order-store.ts`):
//! `<git-dir>/cli-git-captures/{capture.lock/, worktree-id, sequence, landed/<oid>.json}`.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const next = await readNextCaptureSequence(gitDir);
//! ```
/// Debug diagnostics.
use super::diagnostic_log::debug;
/// Trimming with the incumbent's `.trim()` semantics.
use super::js_text::trim_javascript;
/// The largest safe integer.
use super::json_record::MAX_SAFE_INTEGER;
/// The private-directory creation the registry uses.
use super::transaction_registry::ensure_transaction_root;
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::{Path, PathBuf};
/// Capture store directory name inside the worktree's Git directory.
pub const CAPTURE_STORE_NAME: &str = "cli-git-captures";
/// Capture lock directory name inside the store.
pub const CAPTURE_LOCK_NAME: &str = "capture.lock";
/// Worktree identity filename inside the store.
pub const WORKTREE_ID_FILENAME: &str = "worktree-id";
/// Sequence filename inside the store.
pub const SEQUENCE_FILENAME: &str = "sequence";
/// What:
/// A malformed or unreadable capture-order state,
/// with its message.
/// Why:
/// The incumbent's `CaptureOrderRecordError` and `TypeError`s of the store;
/// recovery and
/// pruning report them and continue.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class CaptureOrderRecordError extends Error {}
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CaptureError(pub String);
/// What:
/// The message as display text.
/// Why:
/// Warnings quote it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// error.message
/// ```
impl std::fmt::Display for CaptureError {
/// Writes the message.
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(self.0.as_str());
}
}
/// What:
/// The capture store directory of a worktree.
/// Why:
/// `captureStorePath`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// captureStorePath('/repo/.git') // '/repo/.git/cli-git-captures'
/// ```
pub fn capture_store_path(git_dir: &Path) -> PathBuf {
return git_dir.join(CAPTURE_STORE_NAME);
}
/// What:
/// The value of a sequence file:
/// canonical decimal digits of a safe integer,
/// then one
/// newline.
/// Why:
/// `parseSequence`:
/// `String(Number.parseInt(digits, 10)) === digits` admits no sign,
/// no
/// leading zero and no whitespace;
/// anything else is a malformed store.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function parseSequence({ text, path }): number; // throws TypeError
/// ```
pub fn parse_sequence(bytes: &[u8], path: &Path) -> Result<i64, CaptureError> {
let Some(digits) = bytes.strip_suffix(b"\n") else {
return Err(malformed_sequence(path));
};
if digits.is_empty() || (digits.len() > 1 && digits[0] == b'0') {
return Err(malformed_sequence(path));
}
// `mut` allows accumulating the value digit by digit.
let mut value: i64 = 0;
for digit in digits {
if !digit.is_ascii_digit() {
return Err(malformed_sequence(path));
}
// Sixteen digits already exceed the safe range, so the value never overflows `i64`.
value = value * 10 + i64::from(digit - b'0');
if value > MAX_SAFE_INTEGER {
return Err(malformed_sequence(path));
}
}
return Ok(value);
}
/// What:
/// The failure for a malformed sequence file.
/// Why:
/// Every malformed spelling reports the same message.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// new TypeError(`Capture sequence file is malformed: ${path}`)
/// ```
fn malformed_sequence(path: &Path) -> CaptureError {
return CaptureError(format!(
"Capture sequence file is malformed: {}",
path.display()
));
}
/// What:
/// The next capture sequence number the store would allocate,
/// read without the lock;
/// an
/// absent store or sequence file reads as 1.
/// Why:
/// `readNextCaptureSequence`:
/// landings record it after their compare-and-swap.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function readNextCaptureSequence(gitDir: string): Promise<number>;
/// ```
pub fn read_next_capture_sequence(git_dir: &Path) -> Result<i64, CaptureError> {
let path: PathBuf = capture_store_path(git_dir).join(SEQUENCE_FILENAME);
match std::fs::read(&path) {
Ok(bytes) => return Ok(parse_sequence(bytes.as_slice(), path.as_path())? + 1),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
debug(
"readNextCaptureSequence",
format!("no capture sequence yet at {}", path.display()).as_str(),
);
return Ok(1);
}
Err(error) => {
return Err(CaptureError(format!(
"reading {} failed: {error}",
path.display()
)));
}
}
}
/// What:
/// Create the store directory as a private,
/// non-linked directory when it is missing.
/// Why:
/// `ensureCaptureStore`,
/// through the registry's root creation.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await ensureCaptureStore(gitDir);
/// ```
pub fn ensure_capture_store(git_dir: &Path) -> Result<PathBuf, CaptureError> {
let store: PathBuf = capture_store_path(git_dir);
if let Err(error) = ensure_transaction_root(store.as_path()) {
return Err(CaptureError(error.0));
}
return Ok(store);
}
/// What:
/// The text of a small regular store file read without following a link,
/// decoded as
/// UTF-8 with replacement characters.
/// Why:
/// `readStoreFile` reads with `'utf8'`,
/// which never fails on bytes.
/// A missing file is
/// `Ok(None)`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function readStoreFile(path: string): Promise<string>; // ENOENT propagates
/// ```
pub fn read_store_file(path: &Path) -> Result<Option<String>, CaptureError> {
match super::private_storage::read_regular_file(path, STORE_FILE_LIMIT) {
Ok(bytes) => return Ok(Some(String::from_utf8_lossy(bytes.as_slice()).into_owned())),
Err(super::private_storage::ReadRefusal::Missing) => return Ok(None),
Err(super::private_storage::ReadRefusal::NotRegular) => {
return Err(CaptureError(format!(
"Capture store entry is not a regular file: {}",
path.display()
)));
}
Err(super::private_storage::ReadRefusal::TooLarge) => {
return Err(CaptureError(format!(
"Capture store entry is larger than {STORE_FILE_LIMIT} bytes: {}",
path.display()
)));
}
Err(super::private_storage::ReadRefusal::Io(error)) => {
return Err(CaptureError(format!(
"reading {} failed: {error}",
path.display()
)));
}
}
}
/// Largest store file read:
/// records list captured paths,
/// so they can be large.
const STORE_FILE_LIMIT: u64 = 256 * 1024 * 1024;
/// What:
/// The store's worktree identity without creating one,
/// or nothing when absent.
/// Why:
/// `readWorktreeId`:
/// a record of another store generation is unusable.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function readWorktreeId(gitDir: string): Promise<string | typeof WORKTREE_ID_ABSENT>;
/// ```
pub fn read_worktree_id(git_dir: &Path) -> Result<Option<String>, CaptureError> {
let path: PathBuf = capture_store_path(git_dir).join(WORKTREE_ID_FILENAME);
match read_store_file(path.as_path())? {
Some(text) => return Ok(Some(String::from(trim_javascript(text.as_str())))),
None => {
debug(
"readWorktreeId",
format!("no capture store identity in {}", git_dir.display()).as_str(),
);
return Ok(None);
}
}
}
/// Store controls stay out of the release executable.
#[cfg(test)]
#[path = "capture_store_tests.rs"]
mod tests;