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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
//! What:
//! Native `git commit` in the shadow repository against the private commit index,
//! the prepared commit read back from the shadow,
//! and `prepared.json`.
//! Why:
//! Git owns the hooks, the editor, the templates, the message cleanup and the signing;
//! this module only points Git at the shadow and the private index.
//! Every hook is off in the shadow (its config switches the commit events off and the hooks-off option comes
//! first), so Git starts no external program for a hook and never starts the wrapper.
//! A failing preparation removes the shadow so no shadow outlives the failure
//! (`src/policy-engine/commit-preparation-native.ts`, `src/shadow-repository/shadow-repository.ts`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await runNativePreparation({ gitPath, cwd, globalArgs, commitArgs, shadowPath, hookPlan, commitIndexPath });
//! ```
/// Private file write for the journal record.
use super::private_storage::write_exclusive_synced;
/// Journal record shape and its encoder.
use super::transaction_journal::{AddedPath, PreparedRecord};
use super::transaction_journal_encode::encode_prepared;
/// Invocation capture facts the shadow was built from.
use super::capture_invocation::InvocationCapture;
/// The hooks-off value Git is given again after the caller's options.
use super::hooks_off::NO_HOOKS_DIRECTORY;
/// Shadow removal, so a failed preparation leaves no shadow.
use super::shadow_git::remove_shadow_repository;
/// The shadow request and the real-Git command builder.
use super::shadow_git::shadow_request;
use super::forwarding::git_command;
use super::signal_relay::{SignalGuard, is_guard_active_refusal};
/// Context, one required Git start and failure text.
use super::transaction_git::{GitContext, GitFailure, GitOutput, run_git_checked};
/// `OsString` is owned operating-system text.
use std::ffi::OsString;
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::Path;
/// Variables native Git must not inherit, so every object it writes lands in the shadow store.
pub const NATIVE_UNSET_VARIABLES: [&str; 4] = ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY"];
/// Name of the journal record a preparation writes.
pub const PREPARED_FILENAME: &str = "prepared.json";
/// What:
/// Everything one native preparation needs from its transaction.
/// Why:
/// The incumbent passes these as separate options, and grouping them keeps the call short.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// runNativePreparation({ gitPath, cwd, globalArgs, commitArgs, shadowPath, worktreeRoot, hooksDirectory, commitIndexPath });
/// ```
#[derive(Clone, Copy, Debug)]
pub struct PreparationRequest<'request> {
/// Invocation capture the shadow was created from.
pub capture: &'request InvocationCapture,
/// Shadow repository of the transaction.
pub shadow: &'request Path,
/// Private commit index.
pub commit_index: &'request Path,
/// Kept global options of the invocation.
pub global_args: &'request [String],
/// Commit arguments without pathspecs or `--only`.
pub commit_args: &'request [String],
}
/// What:
/// Why a preparation stopped.
/// Why:
/// A nonzero native exit is the invocation's own exit code, while a start failure is an error.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class NativeCommitFailedError extends Error { exitCode: number }
/// ```
#[derive(Debug)]
pub enum PreparationError {
/// Native `git commit` exited nonzero; its code is the invocation's code.
NativeExit {
/// Git's exit code.
exit_code: i32,
},
/// A Git step around the commit failed.
Git(GitFailure),
/// `prepared.json` could not be written.
Write(String),
}
impl std::fmt::Display for PreparationError {
/// Writes the wording of the failure.
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Self::NativeExit { exit_code } => {
write!(formatter, "git commit exited {exit_code} during private preparation; nothing landed.")
}
Self::Git(error) => write!(formatter, "{error}"),
Self::Write(text) => formatter.write_str(text.as_str()),
};
}
}
/// What:
/// The prepared commit read back from the shadow `HEAD`.
/// Why:
/// Replay rebuilds every later commit from the exact object bytes,
/// and the tree and signature decide what landing may do.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PreparedCommit = { oid: string; treeOid: string; signed: boolean; raw: RawCommit };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedCommit {
/// Prepared commit, the shadow `HEAD` value.
pub oid: String,
/// Prepared commit tree.
pub tree_oid: String,
/// Whether the header block carries a `gpgsig` or `gpgsig-sha256` header.
pub signed: bool,
/// Exact commit object bytes.
pub raw: Vec<u8>,
}
/// What:
/// Builds the arguments of native `git commit` in the shadow.
/// Why:
/// The kept global options come first, then the shadow, then a hooks directory that cannot exist, which also beats a
/// `-c core.hooksPath` of the caller.
/// The shadow's own config switches every commit event off, so no external program runs for a hook.
/// No `--work-tree` is passed: the shadow's own config names the work tree.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// [...globalArgs, `--git-dir=${shadowPath}`, '-c', `core.hooksPath=${NO_HOOKS}`, 'commit', ...commitArgs]
/// ```
fn native_commit_arguments(request: &PreparationRequest) -> Vec<String> {
let mut arguments: Vec<String> = request.global_args.to_vec();
arguments.push(format!("--git-dir={}", request.shadow.display()));
arguments.push(String::from("-c"));
arguments.push(format!("core.hooksPath={NO_HOOKS_DIRECTORY}"));
arguments.push(String::from("commit"));
arguments.extend(request.commit_args.iter().cloned());
return arguments;
}
/// What:
/// Install the signal guard, waiting while another thread of this process holds it.
/// Why:
/// One wrapper process runs one Git child at a time, so the wait never happens there; library tests run several
/// preparations in threads of one process, and the guard is process-wide.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const guard = installSignalGuard();
/// ```
pub fn install_guard_when_free() -> std::io::Result<SignalGuard> {
return retry_while_active(SignalGuard::install, GUARD_WAIT_LIMIT, GUARD_WAIT_DELAY);
}
/// The most times the guard installation is retried while another thread holds the guard.
const GUARD_WAIT_LIMIT: u32 = 1_200;
/// The pause between two tries of the guard installation.
const GUARD_WAIT_DELAY: std::time::Duration = std::time::Duration::from_millis(50);
/// What:
/// Run `install` again while it fails only because another holder is active, up to `limit` more times.
/// Why:
/// Any other failure, and a holder that never lets go, end the wait with the failure itself.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// for (let attempt = 0; ; attempt += 1) { try { return install(); } catch (e) { if (!isActive(e) || attempt >= limit) throw e; await wait(delay); } }
/// ```
pub fn retry_while_active<Value>(
mut install: impl FnMut() -> std::io::Result<Value>,
limit: u32,
delay: std::time::Duration,
) -> std::io::Result<Value> {
let mut attempts: u32 = 0;
loop {
match install() {
Ok(value) => return Ok(value),
Err(error) if is_guard_active_refusal(&error) && attempts < limit => {
attempts += 1;
std::thread::sleep(delay);
}
Err(error) => return Err(error),
}
}
}
/// What:
/// Runs native `git commit` in the shadow with the terminal attached and returns its exit code.
/// Why:
/// The editor output reaches the user unchanged,
/// so the child inherits standard streams.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await runTransactionGit({ gitPath, cwd, args, indexPath, unsetEnvironment, stdio: 'inherit', allowFailure: true });
/// ```
pub fn run_native_commit(
context: &GitContext,
cwd: &Path,
request: &PreparationRequest,
) -> Result<i32, PreparationError> {
let arguments: Vec<OsString> = native_commit_arguments(request).iter().map(OsString::from).collect();
let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
command
.current_dir(cwd)
.env("GIT_INDEX_FILE", request.commit_index);
for name in NATIVE_UNSET_VARIABLES {
command.env_remove(name);
}
return run_guarded_child(&mut command);
}
/// What:
/// Run the native commit under the signal guard and return its exit code.
/// Why:
/// A signal sent to the wrapper while Git runs reaches Git when a process sent it; Git's ending decides the status.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// using guard = installSignalGuard(); return exitCode(await guard.runChild(command));
/// ```
fn run_guarded_child(command: &mut std::process::Command) -> Result<i32, PreparationError> {
let guard: SignalGuard = install_guard_when_free().map_err(|error| {
return PreparationError::Git(GitFailure(format!("installing the signal guard failed: {error}")));
})?;
let outcome = guard.run_child(command).map_err(|error| {
return PreparationError::Git(GitFailure(format!("git commit could not start: {error}")));
})?;
return Ok(super::forwarding::exit_code(outcome));
}
/// What:
/// Reads the prepared commit from the shadow `HEAD`:
/// its object, its tree and whether it is signed.
/// Why:
/// The header block ends at the first empty line; a signature header in it marks the commit signed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await readPreparedCommit({ gitPath, shadowPath });
/// ```
pub fn read_prepared_commit(context: &GitContext, shadow: &Path) -> Result<PreparedCommit, GitFailure> {
let oid_output: GitOutput = run_git_checked(
context,
&shadow_request(shadow, shadow, &["rev-parse", "--verify", "HEAD^{commit}"]),
)?;
let oid: String = String::from_utf8_lossy(oid_output.stdout.as_slice()).trim().to_string();
let raw: Vec<u8> = run_git_checked(
context,
&shadow_request(shadow, shadow, &["cat-file", "commit", oid.as_str()]),
)?
.stdout;
let header_end: usize = raw
.windows(2)
.position(|window| { return window == b"\n\n"; })
.unwrap_or(raw.len());
let header: String = String::from_utf8_lossy(&raw[..header_end]).into_owned();
let tree_oid: String = header
.lines()
.find_map(|line| { return line.strip_prefix("tree "); })
.unwrap_or("")
.to_string();
let signed: bool = header
.lines()
.any(|line| { return line.starts_with("gpgsig ") || line.starts_with("gpgsig-sha256 "); });
return Ok(PreparedCommit {
oid,
tree_oid,
signed,
raw,
});
}
/// What:
/// Writes `prepared.json` exclusively and makes it durable.
/// Why:
/// Recovery reads the prepared record to decide what a dead transaction had prepared.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await writeJournalRecord({ directory, filename: PREPARED_FILENAME, record });
/// ```
pub fn write_prepared_record(directory: &Path, record: &PreparedRecord) -> Result<(), PreparationError> {
return write_exclusive_synced(
directory.join(PREPARED_FILENAME).as_path(),
encode_prepared(record).as_bytes(),
)
.map_err(|error| { return PreparationError::Write(format!("writing {PREPARED_FILENAME} failed: {error}")); });
}
/// What:
/// The facts `prepared.json` records beyond what the shadow and the index already answer.
/// Why:
/// The landing decides what to replay from these lists, and the policy pass that fills them is a later slice.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// { committedPaths, addedPaths, selectedWorktreePaths }
/// ```
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PreparedPaths {
/// Selected and added paths the commit carries.
pub committed_paths: Vec<Vec<u8>>,
/// Added paths with the blobs their worktree copies receive after landing.
pub added_paths: Vec<AddedPath>,
/// Selected corrections and hook changes that the worktree receives after landing.
pub selected_worktree_paths: Vec<AddedPath>,
}
/// What:
/// Runs native preparation end to end:
/// native commit, the prepared commit read back, and `prepared.json`.
/// Why:
/// A failure removes the shadow before it returns, so no shadow outlives a failed preparation,
/// and the exit code of a failed native commit is the invocation's exit code.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await runNativePreparation(...); const prepared = await readPreparedCommit(...); await writeJournalRecord(...);
/// ```
pub fn prepare_native_commit(
context: &GitContext,
cwd: &Path,
request: &PreparationRequest,
journal_directory: &Path,
intended_tree_oid: &str,
paths: &PreparedPaths,
) -> Result<PreparedCommit, PreparationError> {
let exit_code: i32 = match run_native_commit(context, cwd, request) {
Ok(code) => code,
Err(error) => {
remove_failed_shadow(request.shadow);
return Err(error);
}
};
if exit_code != 0 {
remove_failed_shadow(request.shadow);
return Err(PreparationError::NativeExit { exit_code });
}
let prepared: PreparedCommit = match read_prepared_commit(context, request.shadow) {
Ok(commit) => commit,
Err(error) => {
remove_failed_shadow(request.shadow);
return Err(PreparationError::Git(error));
}
};
write_prepared_record(
journal_directory,
&PreparedRecord {
shadow_path: request.shadow.display().to_string(),
prepared_oid: prepared.oid.clone(),
signed: prepared.signed,
intended_tree_oid: intended_tree_oid.to_string(),
committed_paths: paths.committed_paths.clone(),
added_paths: paths.added_paths.clone(),
selected_worktree_paths: paths.selected_worktree_paths.clone(),
},
)?;
return Ok(prepared);
}
/// What:
/// Removes the shadow of a failed preparation.
/// Why:
/// The original failure is the answer, so a removal failure is ignored here.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await removeShadowRepository(shadowPath);
/// ```
fn remove_failed_shadow(shadow: &Path) {
let _ = remove_shadow_repository(shadow);
}
/// Native preparation controls stay out of the release executable.
#[cfg(test)]
#[path = "commit_preparation_tests.rs"]
mod tests;