git_policy_cli/index_writer_forward.rs
1//! What:
2//! Running a forwarded index writer under its landing lock:
3//! coordinate,
4//! run real Git as a child
5//! that inherits the landing lease,
6//! release the lock once Git returns.
7//! Why:
8//! Replacing the process would drop the lock with the process image,
9//! so a coordinated writer
10//! waits for Git instead.
11//! The nested wrappers Git starts see the lease and do not wait on it
12//! (`src/forward-real-git.ts`).
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! await using coordination = await coordinateIndexWriter({ command, gitPath, timeoutMs });
17//! await runGitWithWorktreeCopy({ args, gitPath, environment: coordination.environment });
18//! ```
19
20/// Coordination with landings.
21use super::index_writer_coordination::{CoordinationFailure, CoordinationInput, coordinate_index_writer};
22/// Running real Git, settling the linked-worktree copy around it when one applies.
23use super::worktree_copy_forward::{CopyFacts, ForwardedRun, run_forwarded_git};
24/// The resolved forwarded command.
25use super::forwarded_command::ResolvedCommand;
26/// The repository shape the invocation selects.
27use super::worktree_identity::WorktreeIdentity;
28/// `OsString` is owned operating-system text of raw bytes.
29use std::ffi::OsString;
30/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
31use std::path::PathBuf;
32
33/// What:
34/// Everything a coordinated forward needs,
35/// owned,
36/// so the planned action can carry it.
37/// Why:
38/// The plan decides that coordination is needed;
39/// the run performs it.
40///
41/// In TS you'd write (pseudocode):
42/// ```ts
43/// type ForwardToRealGit = { realGit: string; args: string[]; overlay: [string, string][]; identity; command; environment; timeoutMs };
44/// ```
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CoordinatedForward {
47 /// Real Git executable.
48 pub real_git: PathBuf,
49 /// Arguments Git receives,
50 /// as the caller gave them.
51 pub arguments: Vec<OsString>,
52 /// Environment variables added to Git's inherited environment.
53 pub overlay: Vec<(OsString, OsString)>,
54 /// Repository shape of the invocation.
55 pub identity: WorktreeIdentity,
56 /// The command after alias expansion,
57 /// which decides the coordination.
58 pub command: ResolvedCommand,
59 /// The environment the invocation inherited,
60 /// which may carry an ancestor's lease.
61 pub environment: Vec<(OsString, OsString)>,
62 /// `indexLock.unprovenOwnerTimeoutMs`.
63 pub timeout_ms: u64,
64 /// The linked-worktree copy to settle around Git, when it applies to this invocation.
65 pub copy: Option<CopyFacts>,
66}
67
68/// What:
69/// How a coordinated forward ended.
70/// Why:
71/// Only a run that was not coordinated may replace the process.
72///
73/// In TS you'd write (pseudocode):
74/// ```ts
75/// type ForwardOutcome = 'not-coordinated' | { exitCode: number } | { stderr: string; exitCode: number };
76/// ```
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum CoordinatedRun {
79 /// No coordination applies;
80 /// the caller replaces its process with Git.
81 NotCoordinated,
82 /// Git ran under the landing lock and exited with this status.
83 Ran(i32),
84 /// The command did not run;
85 /// its standard error and exit status.
86 Failed {
87 /// Text for standard error.
88 stderr: String,
89 /// Exit status.
90 code: i32,
91 },
92}
93
94/// What:
95/// Coordinate,
96/// run Git as a child with the lease,
97/// release the lock,
98/// and report the status.
99/// Why:
100/// `forwardToRealGit`:
101/// the lock is held until real Git returns.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// async function forwardToRealGit({ args, gitPath, identity, unprovenOwnerTimeoutMs }): Promise<void>;
106/// ```
107pub fn run_coordinated_forward(forward: &CoordinatedForward) -> CoordinatedRun {
108 // The context runs the coordination's own Git queries with the same overlay as the command.
109 let context: crate::transaction_git::GitContext = crate::transaction_git::GitContext {
110 real_git: forward.real_git.clone(),
111 overlay: forward.overlay.clone(),
112 global_prefix: Vec::new(),
113 };
114 let input: CoordinationInput<'_> = CoordinationInput {
115 context: &context,
116 identity: &forward.identity,
117 command: &forward.command,
118 environment: forward.environment.as_slice(),
119 timeout_ms: forward.timeout_ms,
120 };
121 let coordination = match coordinate_index_writer(&input) {
122 Ok(Some(held)) => held,
123 Ok(None) => {
124 // A copy keeps the wrapper alive around Git; otherwise the caller may replace the process.
125 let Some(facts) = forward.copy.as_ref() else {
126 return CoordinatedRun::NotCoordinated;
127 };
128 return from_forwarded(run_forwarded_git(
129 forward.real_git.as_path(),
130 forward.arguments.as_slice(),
131 forward.overlay.as_slice(),
132 Some(facts),
133 ));
134 }
135 Err(failure) => return failed_with(&failure),
136 };
137 let mut child_overlay: Vec<(OsString, OsString)> = forward.overlay.clone();
138 child_overlay.push(coordination.lease.clone());
139 let outcome: ForwardedRun = run_forwarded_git(
140 forward.real_git.as_path(),
141 forward.arguments.as_slice(),
142 child_overlay.as_slice(),
143 forward.copy.as_ref(),
144 );
145 let released: Result<(), String> = coordination.release();
146 return finish(outcome, released);
147}
148
149/// What:
150/// The run's result once Git returned and the lock was released.
151/// Why:
152/// Git's status is the answer unless the lock could not be released,
153/// which is reported.
154///
155/// In TS you'd write (pseudocode):
156/// ```ts
157/// try { return { exitCode } } finally { await coordination[Symbol.asyncDispose](); }
158/// ```
159fn finish(outcome: ForwardedRun, released: Result<(), String>) -> CoordinatedRun {
160 if let Err(reason) = released {
161 return CoordinatedRun::Failed {
162 stderr: format!("cli-git: the landing lock could not be released: {reason}\n"),
163 code: super::action::ENGINE_FAILURE_EXIT_CODE,
164 };
165 }
166 return from_forwarded(outcome);
167}
168
169/// What:
170/// The coordinated run that a forwarded run stands for.
171/// Why:
172/// Both report the same two endings, so the status and the failure text pass through unchanged.
173///
174/// In TS you'd write (pseudocode):
175/// ```ts
176/// // the same exit status or the same failure text
177/// ```
178fn from_forwarded(run: ForwardedRun) -> CoordinatedRun {
179 // `match` on the ending: a status, or a failure with its own text.
180 match run {
181 ForwardedRun::Ran(code) => return CoordinatedRun::Ran(code),
182 ForwardedRun::Failed { stderr, code } => return CoordinatedRun::Failed { stderr, code },
183 }
184}
185
186/// What:
187/// The engine failure a refused coordination reports.
188/// Why:
189/// The unproven owner and read failures each keep their own event.
190///
191/// In TS you'd write (pseudocode):
192/// ```ts
193/// renderPolicyEvents([createEngineFailureEvent({ code, message })])
194/// ```
195fn failed_with(failure: &CoordinationFailure) -> CoordinatedRun {
196 return CoordinatedRun::Failed {
197 stderr: failure.stderr(),
198 code: super::action::ENGINE_FAILURE_EXIT_CODE,
199 };
200}
201
202/// Coordinated forwarding controls stay out of the release executable.
203#[cfg(test)]
204#[path = "index_writer_forward_tests.rs"]
205mod tests;