Skip to main content

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;