Skip to main content

git_policy_cli/invocation/
action.rs

1//! What:
2//!  The decision one wrapper invocation ends in.
3//! Why:
4//!  Forwarding replaces the process and cannot run inside a unit test;
5//!  returning
6//!      the decision as data lets every path be checked directly.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // type Action = { kind: 'forward'; ... } | { kind: 'exit'; code: number; stdout: string; stderr: string };
11//! ```
12
13/// What:
14///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
15///       must be UTF-8).
16/// Why:
17///   Environment additions are passed to Git without decoding.
18///
19/// In TS you'd write (pseudocode):
20/// ```ts
21/// // [string, string][] environment pairs, but byte-preserving.
22/// ```
23use std::ffi::OsString;
24/// `PathBuf` is an owned filesystem path of raw OS bytes.
25use crate::index_writer_forward::CoordinatedForward;
26use crate::worktree_copy_forward::CopyFacts;
27use std::path::PathBuf;
28
29/// What:
30///  Exit code for a usage,
31///  configuration or wrapper failure that kept Git from running.
32///       `i32` is a signed 32-bit integer,
33///  the type of process exit codes.
34/// Why:
35///   The wrapper's contract reserves 2 for these,
36///  distinct from 1 for policy findings.
37///
38/// In TS you'd write (pseudocode):
39/// ```ts
40/// const ENGINE_FAILURE_EXIT_CODE = 2;
41/// ```
42pub const ENGINE_FAILURE_EXIT_CODE: i32 = 2;
43
44/// What:
45///  What a plain push that may be integrated needs to run and, when it fails on a divergence, to merge.
46/// Why:
47///   The run keeps the caller's streams like the settled one does, so the integration step starts from Git's status
48///  and the same arguments, environment and (inside a linked worktree) settlement as an unstacked push would have had.
49///
50/// In TS you'd write (pseudocode):
51/// ```ts
52/// type PushForward = { realGit: string; args: string[]; overlay: [string, string][]; copy?: CopyFacts; subcommandIndex: number; environment: [string, string][] };
53/// ```
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct PushForward {
56    /// The selected real Git executable.
57    pub real_git: PathBuf,
58    /// The arguments Git receives, global options included.
59    pub arguments: Vec<OsString>,
60    /// Variables added to the inherited environment.
61    pub overlay: Vec<(OsString, OsString)>,
62    /// The linked-worktree settlement the run keeps when one applies.
63    pub copy: Option<Box<CopyFacts>>,
64    /// Index of `push` in `arguments`.
65    pub subcommand_index: usize,
66    /// The invocation's environment, which the integration step starts Git with.
67    pub environment: Vec<(OsString, OsString)>,
68    /// Whether this push must upload LFS objects first, by a `git-lfs push` the parent runs before the push.
69    ///  Only the push itself is preceded by the upload; the integration step's dry-run queries are not.
70    pub lfs: bool,
71    /// The configuration entries the upload plan reads, from the snapshot taken when the invocation was planned.
72    pub lfs_settings: Option<Vec<(String, String)>>,
73    /// Whether a push that fails on a divergence is integrated and pushed again.
74    ///  A push that only needs the LFS upload explained keeps its own status.
75    pub stack: bool,
76}
77
78/// What:
79///  What the executable must do for one invocation.
80///       `Vec<OsString>` is an owned argument list;
81///  `Vec<(OsString, OsString)>` is an
82///       owned list of name/value pairs;
83///  `String` is owned UTF-8 text.
84/// Why:
85///   The executable performs exactly one of these:
86///  become Git,
87///  run Git and then add text,
88///  or print and exit.
89///
90/// In TS you'd write (pseudocode):
91/// ```ts
92/// type Action = { kind: 'forward'; realGit: string; args: string[]; overlay: [string, string][]; stderr: string }
93///   | { kind: 'exit'; code: number; stdout: string; stderr: string };
94/// ```
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub enum Action {
97    /// Become real Git.
98    Forward {
99        /// The selected real Git executable.
100        real_git: PathBuf,
101        /// The arguments Git receives:
102        ///  no wrapper control,
103        ///  fixed transforms applied.
104        arguments: Vec<OsString>,
105        /// Variables added to the inherited environment.
106        overlay: Vec<(OsString, OsString)>,
107        /// Complete warning event lines,
108        ///  written to standard error before Git starts.
109        stderr: String,
110    },
111    /// Forward an index writer under its landing lock:
112    ///  coordinate,
113    ///  run Git as a child,
114    ///  release.
115    ForwardCoordinated {
116        /// What the coordinated run needs.
117        forward: Box<CoordinatedForward>,
118        /// Warning events written to standard error before the coordination starts.
119        stderr: String,
120    },
121    /// Forward a command that the linked-worktree copy settles:
122    ///  run Git as a child,
123    ///  recover a copy a dead process left first,
124    ///  copy the ignored state into the worktree the command created.
125    ForwardSettled {
126        /// The selected real Git executable.
127        real_git: PathBuf,
128        /// The arguments Git receives.
129        arguments: Vec<OsString>,
130        /// Variables added to the inherited environment.
131        overlay: Vec<(OsString, OsString)>,
132        /// What the copy needs to know about the invocation.
133        copy: Box<CopyFacts>,
134        /// Warning events written to standard error before Git starts.
135        stderr: String,
136    },
137    /// Forward a plain push as a child, so a push that lost a race to another session can be integrated:
138    ///  run Git with the caller's streams, then merge in what landed on the remote and publish the merge.
139    ForwardPush {
140        /// What the run and the integration need.
141        forward: Box<PushForward>,
142        /// Warning events written to standard error before Git starts.
143        stderr: String,
144    },
145    /// Run Git as a child for a top-level `--version`, `-v`, `--help` or `-h`,
146    ///  and print the announcement after Git's own answer when Git exited with status 0.
147    ForwardAnnounced {
148        /// The selected real Git executable.
149        real_git: PathBuf,
150        /// The arguments Git receives.
151        arguments: Vec<OsString>,
152        /// Variables added to the inherited environment.
153        overlay: Vec<(OsString, OsString)>,
154        /// Complete text for standard output after Git's answer,
155        ///  already line-terminated.
156        announcement: String,
157        /// Warning events written to standard error before Git starts.
158        stderr: String,
159    },
160    /// Stop without running the caller's command.
161    Exit {
162        /// Process exit code.
163        code: i32,
164        /// Complete text for standard output,
165        ///  already line-terminated.
166        stdout: String,
167        /// Complete text for standard error,
168        ///  already line-terminated.
169        stderr: String,
170    },
171}
172
173/// What:
174///  Put a notice in front of the standard-error text an action already carries.
175///       `Option<&str>` is the notice or nothing (`string | undefined`).
176/// Why:
177///   The notice that the real Git came from the unit's record and not from `PATH` is owed to the person
178///  before anything else the invocation says,
179///  whichever way the invocation ends;
180///  every variant carries its standard-error text,
181///  so the one prefix covers them all
182///  and no start of Git has to know about it.
183///
184/// In TS you'd write (pseudocode):
185/// ```ts
186/// function withLeadingNotice(action: Action, notice?: string): Action { return notice ? { ...action, stderr: notice + action.stderr } : action; }
187/// ```
188pub fn with_leading_notice(action: Action, notice: Option<&str>) -> Action {
189    // `let Some(x) = ... else { return ...; }` returns the action unchanged when there is no notice.
190    let Some(lead) = notice else {
191        return action;
192    };
193    // `match` picks the variant and binds the fields it carries;
194    //  `stderr: text` binds the field under a new name, `..` leaves the other fields as they are.
195    match action {
196        Action::Forward { real_git, arguments, overlay, stderr } => {
197            return Action::Forward { real_git, arguments, overlay, stderr: format!("{lead}{stderr}") };
198        }
199        Action::ForwardCoordinated { forward, stderr } => {
200            return Action::ForwardCoordinated { forward, stderr: format!("{lead}{stderr}") };
201        }
202        Action::ForwardSettled { real_git, arguments, overlay, copy, stderr } => {
203            return Action::ForwardSettled { real_git, arguments, overlay, copy, stderr: format!("{lead}{stderr}") };
204        }
205        Action::ForwardPush { forward, stderr } => {
206            return Action::ForwardPush { forward, stderr: format!("{lead}{stderr}") };
207        }
208        Action::ForwardAnnounced { real_git, arguments, overlay, announcement, stderr } => {
209            return Action::ForwardAnnounced {
210                real_git,
211                arguments,
212                overlay,
213                announcement,
214                stderr: format!("{lead}{stderr}"),
215            };
216        }
217        Action::Exit { code, stdout, stderr } => {
218            return Action::Exit { code, stdout, stderr: format!("{lead}{stderr}") };
219        }
220    }
221}
222
223/// What:
224///  Build the stop action for a wrapper failure described in prose.
225///       `&str` borrows the message;
226///  the action owns its formatted copy.
227/// Why:
228///   These failures (no real Git,
229///  a forwarding loop,
230///  an unreadable repository)
231///       print one prefixed,
232///  line-terminated diagnostic on standard error and exit 2.
233///
234/// In TS you'd write (pseudocode):
235/// ```ts
236/// function failure(message: string): Action;
237/// ```
238pub fn failure(message: &str) -> Action {
239    return Action::Exit {
240        code: ENGINE_FAILURE_EXIT_CODE,
241        // `String::new()` is empty owned text: nothing goes to standard output.
242        stdout: String::new(),
243        // `format!` builds the owned, line-terminated text.
244        stderr: format!("cli-git: {message}\n"),
245    };
246}