Skip to main content

git_policy_cli/invocation/
entry.rs

1//! What:
2//!  Decide what one `git` invocation of the native wrapper does,
3//!  and perform it.
4//! Why:
5//!  The executable stays thin:
6//!  it gathers process facts,
7//!  asks this module for an
8//!      action,
9//!  and performs it.
10//!  The decision itself is testable without replacing
11//!      the test process.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! // const action = planInvocation(args, env, inputs); perform(action);
16//! ```
17
18/// Import the sibling modules the decision combines.
19use crate::action::{Action, ENGINE_FAILURE_EXIT_CODE, PushForward, failure, with_leading_notice};
20#[cfg(unix)]
21use crate::auto_push_trigger::{TipWatch, hand_over_if_ahead, try_watch_tip};
22use crate::push_trigger_log::{Decision, Origin, Outcome, record_decision};
23use crate::announced_query::{WRAPPER_VERSION, announced_query, announcement, printable_after};
24use crate::command_push::stackable_option_tokens;
25use crate::external_hooks::external_hooks_warning;
26use crate::lfs_pre_push::uses_lfs;
27use crate::lfs_upload_plan::settings_from_snapshot;
28use crate::config_snapshot::{ConfigSnapshot, read_config_snapshot};
29use crate::push_stacking::{STACKED_PUSH_MARKER, run_stacked_push};
30use crate::command_push::parse_push_region;
31use crate::child_environment::{
32    FORWARD_TARGET_VARIABLE, child_environment_overlay, environment_value,
33};
34use crate::effective_target::default_allowed_worktree_dirs;
35use crate::forwarded_lock_notice::{forwarded_lock_notice, is_covered};
36use crate::forwarding::{exit_code, replace_process_with_real_git, run_real_git};
37use crate::stale_ref_lock_judge::Judge;
38use crate::index_writer_forward::{CoordinatedForward, CoordinatedRun, run_coordinated_forward};
39use crate::index_writer_commands::is_index_writer;
40use crate::forwarded_command::{ResolvedCommand, Route, creates_worktrees, resolve_forwarded_command};
41use crate::invocation_config::{config_invalid_event, load_identity_config};
42use crate::repository_location::RepositoryLocation;
43use crate::transaction_git::GitContext;
44use crate::worktree_identity::WorktreeIdentity;
45use crate::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
46use crate::management::{host_filesystem_section, plan_management_with_section};
47/// The linter the executable gives the Markdown policy.
48/// The shipped checks and their constructor.
49use crate::policy_checks::{ShippedChecks, shipped_checks};
50/// A forwarded command starts with no candidate content.
51use crate::policy_content::LifecycleContent;
52use crate::real_git::{ResolutionInputs, ResolvedGit, process_resolution_inputs, resolve_real_git};
53use crate::started_as_git::refusal_when_started_as_git;
54use crate::real_git_candidate::same_file;
55use crate::repository_facts::{GitFacts, RepositoryFacts, git_facts};
56use crate::rule_require_root::{RequireRootDecision, decide_require_root};
57/// The variable that names the forbidden-strings rules list.
58use crate::scanner_selection::RULES_VARIABLE;
59use crate::wrapped_command::{WrappedOutcome, run_wrapped_command};
60use crate::wrapper_controls::{Controls, no_controls, strip_global_controls};
61use crate::worktree_add_plan::{AddPlan, copy_start_directory, parse_add_plan, parse_copy_destination, start_directory};
62use crate::worktree_remove_plan::{RemovePlan, parse_remove_plan};
63use crate::worktree_copy_forward::{CopyFacts, ForwardedRun, copy_applies_with, run_forwarded_git};
64use crate::wrapper_invocation::{StrippedInvocation, strip_wrapper_controls};
65/// What:
66///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
67///       must be UTF-8).
68/// Why:
69///   Arguments and environment values are never decoded.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// // process.argv and process.env, but byte-preserving.
74/// ```
75use std::ffi::OsString;
76/// The trait that gives output streams `.write_all(..)`.
77use std::io::Write;
78/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
79use std::path::{Path, PathBuf};
80
81/// The management namespace:
82///  `git cli-git ...` is handled by the wrapper,
83///  never by Git.
84pub const MANAGEMENT_COMMAND: &str = "cli-git";
85
86/// What:
87///  Explain that another cli-git wrapper selected this executable as real Git.
88///       `&Path` borrows this executable's path for the message.
89/// Why:
90///   Forwarding again would bounce between the two wrappers forever;
91///  stopping
92///       names this executable so the user can fix PATH.
93///
94/// In TS you'd write (pseudocode):
95/// ```ts
96/// function forwardedToSelf(ownExecutable: string): Action;
97/// ```
98fn forwarded_to_self(own_executable: &Path) -> Action {
99    // `format!` builds owned text; `.as_str()` lends it to the helper.
100    return failure(
101        format!(
102            "another cli-git wrapper selected this cli-git executable ({}) as real Git. \
103             Forwarding again would never reach Git. Remove the extra cli-git from PATH or \
104             place the real Git executable on PATH.",
105            own_executable.display()
106        )
107        .as_str(),
108    );
109}
110
111/// What:
112///  The caller's home directory from the environment,
113///  if it names one.
114///       `Option<PathBuf>` is "an owned path or nothing".
115/// Why:
116///   The tool caches exempt from linked-worktree enforcement live under the home
117///       directory;
118///  it comes from the injected environment,
119///  never from a fixed path.
120///
121/// In TS you'd write (pseudocode):
122/// ```ts
123/// const home = environment.HOME === undefined || environment.HOME === '' ? undefined : environment.HOME;
124/// ```
125fn home_directory(environment: &[(OsString, OsString)]) -> Option<PathBuf> {
126    // `if let Some(home) = ... && cond` runs only when the variable is set and not empty.
127    if let Some(home) = environment_value(environment, "HOME")
128        && !home.is_empty()
129    {
130        // `Some(...)` is the "present" case; `PathBuf::from` turns the text into a path.
131        return Some(PathBuf::from(home));
132    }
133    // `None` is the "absent" case.
134    return None;
135}
136
137/// What:
138///  Decide the action for one invocation from injected process facts.
139///       `&[OsString]` borrows the arguments after the program name;
140///       `&[(OsString, OsString)]` borrows the environment as name/value pairs.
141/// Why:
142///   Order matters.
143///  The recursion check runs before anything else.
144///  Wrapper controls
145///       written before the subcommand are removed first,
146///  because Git's own reading of
147///       the arguments stops at them and would hide the subcommand from every later
148///       decision.
149///  The management namespace is answered by the wrapper.
150///  Every other
151///       command goes through the wrapped-command lifecycle with real Git behind it.
152///
153/// In TS you'd write (pseudocode):
154/// ```ts
155/// function planInvocation(args: string[], environment: [string, string][], inputs: ResolutionInputs): Action;
156/// ```
157pub fn plan_invocation(
158    arguments: &[OsString],
159    environment: &[(OsString, OsString)],
160    inputs: &ResolutionInputs,
161) -> Action {
162    // `if let Some(target) = ... && cond` runs only when a forwarding wrapper left its
163    // marker and that marker names this executable.
164    if let Some(target) = environment_value(environment, FORWARD_TARGET_VARIABLE)
165        && same_file(Path::new(&target), inputs.own_executable.as_path())
166    {
167        return forwarded_to_self(inputs.own_executable.as_path());
168    }
169    // `mut` allows the scan to record what the removed controls asked for.
170    let mut global_controls: Controls = no_controls();
171    // `&mut global_controls` lends the record for writing.
172    let global_clean: Vec<OsString> = strip_global_controls(arguments, &mut global_controls);
173    // `.as_slice()` lends the owned list as a borrowed view.
174    let layout: GlobalLayout = global_layout(global_clean.as_slice());
175    if layout.outcome == GlobalOutcome::Command
176        && global_clean[layout.prefix_len] == MANAGEMENT_COMMAND
177    {
178        // `&list[a..]` borrows from index `a` on; `&list[..a]` borrows up to it.
179        return plan_management_with_section(
180            &global_clean[layout.prefix_len + 1..],
181            &global_clean[..layout.prefix_len],
182            &global_controls,
183            environment,
184            inputs,
185            &host_filesystem_section,
186        );
187    }
188    // What: `match` on the resolver's `Result`: keep the selection or stop with its message.
189    // Why:  Without real Git there is nothing to forward to.
190    //
191    // In TS you'd write (pseudocode):
192    // ```ts
193    // let resolved; try { resolved = resolveRealGit(inputs); } catch (e) { return failure(String(e)); }
194    // ```
195    let resolved: ResolvedGit = match resolve_real_git(inputs) {
196        Ok(found) => found,
197        Err(error) => return failure(error.to_string().as_str()),
198    };
199    // The merge push of an integrated race is planned again inside this process with the stacking marker set;
200    //  the planning of the push it came from has already given the notice.
201    let notice: Option<String> = if environment_value(environment, STACKED_PUSH_MARKER).is_some() {
202        None
203    } else {
204        resolved.notice
205    };
206    let action: Action = plan_with_real_git(
207        arguments,
208        environment,
209        resolved.path,
210        global_clean,
211        &layout,
212    );
213    // `.as_deref()` lends the optional owned notice as an optional borrowed one.
214    return with_leading_notice(action, notice.as_deref());
215}
216
217/// What:
218///  Plan an invocation that is not a management command,
219///  once the real Git is known.
220///  `&GlobalLayout` borrows where the subcommand starts in the cleaned arguments.
221/// Why:
222///  `plan_invocation` adds the notice of a recorded fallback to whatever action this returns,
223///  so every ending of the planning (a forward,
224///  a stop,
225///  a refusal) carries it without repeating it.
226///
227/// In TS you'd write (pseudocode):
228/// ```ts
229/// function planWithRealGit(args: string[], environment: [string, string][], realGit: string, globalClean: string[], layout: GlobalLayout): Action;
230/// ```
231fn plan_with_real_git(
232    arguments: &[OsString],
233    environment: &[(OsString, OsString)],
234    real_git: PathBuf,
235    global_clean: Vec<OsString>,
236    layout: &GlobalLayout,
237) -> Action {
238    let overlay: Vec<(OsString, OsString)> =
239        child_environment_overlay(environment, real_git.as_path());
240    // What: Top-level `--version`, `-v`, `--help` and `-h` run Git as a child and add the announcement.
241    // Why:  The layout is already known here, so every other invocation pays one enum comparison;
242    //       the other queries keep the exact process replacement below.
243    //
244    // In TS you'd write (pseudocode):
245    // ```ts
246    // const query = announcedQuery(args, layout); if (query) return { kind: 'forward-announced', ... };
247    // ```
248    if let Some(query) = announced_query(global_clean.as_slice(), layout) {
249        return Action::ForwardAnnounced {
250            announcement: announcement(query, WRAPPER_VERSION, real_git.as_path()),
251            real_git,
252            arguments: global_clean,
253            overlay,
254            stderr: String::new(),
255        };
256    }
257    let stripped: StrippedInvocation = strip_wrapper_controls(arguments);
258    // The global options before the subcommand select the repository for every fact query.
259    let facts: GitFacts = git_facts(
260        real_git.as_path(),
261        &stripped.arguments[..stripped.layout.prefix_len],
262        overlay.as_slice(),
263    );
264    // `mut` lets the lifecycle cache facts and state what content policies can read.
265    let mut checks: ShippedChecks<GitFacts> = shipped_checks(
266        facts,
267        // `.clone()` copies the stripped arguments for the rule cores.
268        stripped.arguments.clone(),
269        LifecycleContent::None,
270        default_allowed_worktree_dirs(
271            environment,
272            // `.as_deref()` lends the optional owned path as an optional borrowed one.
273            home_directory(environment).as_deref(),
274        ),
275    );
276    // The forbidden-strings rules variable is read from this invocation's environment.
277    checks.scanner_settings.rules_variable = environment_value(environment, RULES_VARIABLE);
278    // `match` picks by variant and binds the fields each ending carries.
279    match run_wrapped_command(&stripped, environment, &mut checks) {
280        // `arguments: forwarded` binds the field under a new name.
281        WrappedOutcome::Forward {
282            arguments: forwarded,
283            stderr,
284        } => {
285            // The opt-out, Git's other query forms (`--exec-path` and so on), and the commands that need no repository
286            // (`version`, `help`, `init`, `clone`, user-level `config`) never settle a copy.
287            let copy_bypassed: bool = stripped.controls.skip_worktree_copy
288                || stripped.layout.outcome != GlobalOutcome::Command
289                || decide_require_root(forwarded.as_slice(), &[])
290                    != RequireRootDecision::NeedsRepositoryRoot;
291            return stack_if_push(
292                forward_action(
293                    real_git,
294                    forwarded,
295                    overlay,
296                    stderr,
297                    environment,
298                    copy_bypassed,
299                    &mut checks,
300                ),
301                environment,
302            );
303        }
304        WrappedOutcome::Exit { code, stderr } => {
305            return Action::Exit {
306                code,
307                // `String::new()` is empty owned text: wrapped commands report on standard error.
308                stdout: String::new(),
309                stderr,
310            };
311        }
312    }
313}
314
315/// What:
316///  Run one invocation with the real process facts and return its exit code.
317///       `i32` is a signed 32-bit integer,
318///  the type of process exit codes.
319///       Returns only when Git was not started;
320///  a forwarded command never returns.
321/// Why:
322///   This is everything the executable's `main` does;
323///  keeping it in the library
324///       lets the same code be measured by tests and mutation runs.
325///
326/// In TS you'd write (pseudocode):
327/// ```ts
328/// function runProcess(args: string[], environment: [string, string][]): number;
329/// ```
330pub fn run_process(arguments: &[OsString], environment: &[(OsString, OsString)]) -> i32 {
331    // From here on every real-Git start of this process runs with traditional hooks off.
332    crate::hooks_off::activate();
333    let action: Action = match process_resolution_inputs(environment) {
334        // A cli-git executable that a wrapper started as Git stops before it plans anything or starts anything.
335        Ok(inputs) => match refusal_when_started_as_git(environment, inputs.own_executable.as_path()) {
336            Some(refusal) => failure(refusal.as_str()),
337            None => plan_invocation(arguments, environment, &inputs),
338        },
339        Err(error) => failure(
340            format!(
341                "cannot determine this executable or the current directory, so real Git \
342                 cannot be selected safely: {error}"
343            )
344            .as_str(),
345        ),
346    };
347    // `match` picks by variant and binds the fields each one carries.
348    match action {
349        Action::Forward {
350            real_git,
351            arguments: forwarded,
352            overlay,
353            stderr,
354        } => {
355            // Warning events reach the caller before Git's own output.
356            write_stream(&mut std::io::stderr(), stderr.as_str());
357            let error: std::io::Error = replace_process_with_real_git(
358                real_git.as_path(),
359                forwarded.as_slice(),
360                overlay.as_slice(),
361            );
362            write_stream(
363                &mut std::io::stderr(),
364                format!(
365                    "cli-git: cannot start {}: {error}. Hint: if you moved or upgraded Git, check that the file exists and is executable with `ls -l {}`, \
366                     then reinstall Git or the wrapper; no Git command ran.\n",
367                    real_git.display(),
368                    real_git.display()
369                )
370                .as_str(),
371            );
372            return ENGINE_FAILURE_EXIT_CODE;
373        }
374        Action::ForwardCoordinated { forward, stderr } => {
375            write_stream(&mut std::io::stderr(), stderr.as_str());
376            return run_coordinated_action(&forward);
377        }
378        Action::ForwardAnnounced {
379            real_git,
380            arguments: forwarded,
381            overlay,
382            announcement,
383            stderr,
384        } => {
385            write_stream(&mut std::io::stderr(), stderr.as_str());
386            return run_announced(
387                real_git.as_path(),
388                forwarded.as_slice(),
389                overlay.as_slice(),
390                announcement.as_str(),
391            );
392        }
393        Action::ForwardSettled {
394            real_git,
395            arguments: forwarded,
396            overlay,
397            copy,
398            stderr,
399        } => {
400            write_stream(&mut std::io::stderr(), stderr.as_str());
401            return finish_forwarded(run_forwarded_git(
402                real_git.as_path(),
403                forwarded.as_slice(),
404                overlay.as_slice(),
405                Some(&*copy),
406            ));
407        }
408        Action::ForwardPush { forward, stderr } => {
409            write_stream(&mut std::io::stderr(), stderr.as_str());
410            return run_stacked_push(&forward);
411        }
412        Action::Exit {
413            code,
414            stdout,
415            stderr,
416        } => {
417            // `&mut` lends each stream for writing.
418            write_stream(&mut std::io::stdout(), stdout.as_str());
419            write_stream(&mut std::io::stderr(), stderr.as_str());
420            return code;
421        }
422    }
423}
424
425/// What:
426///  Run Git as a child for an announced query,
427///  print the announcement when Git exited with status 0,
428///  and return Git's status.
429/// Why:
430///   The child shares the caller's streams, so Git's own answer is untouched and comes first;
431///  the announcement follows it only when Git answered.
432///
433/// In TS you'd write (pseudocode):
434/// ```ts
435/// const outcome = await runRealGit(realGit, args, overlay); if (outcome.exited === 0) print(text); return exitCode(outcome);
436/// ```
437fn run_announced(
438    real_git: &Path,
439    arguments: &[OsString],
440    overlay: &[(OsString, OsString)],
441    text: &str,
442) -> i32 {
443    match run_real_git(real_git, arguments, overlay) {
444        Ok(outcome) => {
445            write_stream(&mut std::io::stdout(), printable_after(outcome, text));
446            return exit_code(outcome);
447        }
448        Err(error) => {
449            write_stream(
450                &mut std::io::stderr(),
451                format!(
452                    "cli-git: cannot start {}: {error}. Hint: if you moved or upgraded Git, check that the file exists and is executable with `ls -l {}`, \
453                     then reinstall Git or the wrapper; no Git command ran.\n",
454                    real_git.display(),
455                    real_git.display()
456                )
457                .as_str(),
458            );
459            return ENGINE_FAILURE_EXIT_CODE;
460        }
461    }
462}
463
464/// What:
465///  The exit code of a forwarded run, after printing the text of a run that failed.
466/// Why:
467///   A settled run keeps the wrapper alive around Git, so the exit code returns here instead of replacing the process.
468///
469/// In TS you'd write (pseudocode):
470/// ```ts
471/// process.exitCode = outcome.exitCode;
472/// ```
473fn finish_forwarded(run: ForwardedRun) -> i32 {
474    // `match` on the ending: a status, or a failure with its own text.
475    match run {
476        ForwardedRun::Ran(code) => return code,
477        ForwardedRun::Failed { stderr, code } => {
478            write_stream(&mut std::io::stderr(), stderr.as_str());
479            return code;
480        }
481    }
482}
483
484/// What:
485///  Run a coordinated forward:
486///  print its failure,
487///  run Git under the landing lock,
488///  or replace the process when no coordination applies after all.
489/// Why:
490///   The lock must outlive Git,
491///  so only a run that takes no lock may replace the process.
492///
493/// In TS you'd write (pseudocode):
494/// ```ts
495/// const outcome = await forwardToRealGit({ ... }); process.exitCode = outcome.exitCode;
496/// ```
497fn run_coordinated_action(forward: &CoordinatedForward) -> i32 {
498    // A command that adds commits keeps the wrapper alive around Git, so the push can be handed over once it succeeded.
499    let watch: Option<TipWatch> = if adds_commits(&forward.command) { watch_forward(forward) } else { None };
500    let code: i32 = match run_coordinated_forward(forward) {
501        CoordinatedRun::Ran(code) => code,
502        CoordinatedRun::Failed { stderr, code } => {
503            write_stream(&mut std::io::stderr(), stderr.as_str());
504            code
505        }
506        CoordinatedRun::NotCoordinated if watch.is_some() => {
507            finish_forwarded(run_forwarded_git(forward.real_git.as_path(), forward.arguments.as_slice(), forward.overlay.as_slice(), None))
508        }
509        CoordinatedRun::NotCoordinated => {
510            let error: std::io::Error = replace_process_with_real_git(
511                forward.real_git.as_path(),
512                forward.arguments.as_slice(),
513                forward.overlay.as_slice(),
514            );
515            write_stream(
516                &mut std::io::stderr(),
517                format!("cli-git: cannot start {}: {error}\n", forward.real_git.display()).as_str(),
518            );
519            ENGINE_FAILURE_EXIT_CODE
520        }
521    };
522    if let Some(found) = watch.as_ref() {
523        if code == 0 {
524            hand_over_if_ahead(found, trigger_command(&forward.command).as_str(), &mut std::io::stderr());
525        } else {
526            // The watch holds the branch and its tip from before the command, which a reader needs to tell which merge failed.
527            record_decision(&Decision {
528                common_dir: Some(found.common_dir()),
529                origin: Some(found.origin()),
530                command: trigger_command(&forward.command).as_str(),
531                outcome: Outcome::Declined,
532                reason: "command-failed",
533                detail: format!("Git exited with status {code}; {} was at {} before the command", found.branch_ref(), found.tip_before()).as_str(),
534                branch: Some(found.branch_ref()),
535            });
536        }
537    }
538    return code;
539}
540
541/// Without file system readers for this platform, no push is handed over after a command that adds commits.
542#[cfg(not(unix))]
543struct TipWatch;
544
545/// Without file system readers for this platform, no branch is watched.
546#[cfg(not(unix))]
547fn watch_forward(_forward: &CoordinatedForward) -> Option<TipWatch> {
548    return None;
549}
550
551/// Without file system readers for this platform, nothing is handed over.
552#[cfg(not(unix))]
553fn hand_over_if_ahead(_watch: &TipWatch, _command: &str, _stderr: &mut dyn Write) {}
554
555/// What:
556///  Whether a resolved command adds commits to the branch that the owner wants pushed like a commit: a merge, a pull, a
557///  cherry-pick, a revert or an applied patch.
558/// Why:
559///   A rebase is left out, because a rebased branch would need a forced push; the tip check then also leaves out the
560///  rebase a pull runs.
561///
562/// In TS you'd write (pseudocode):
563/// ```ts
564/// function addsCommits(command: ResolvedGitCommand): boolean;
565/// ```
566fn adds_commits(command: &ResolvedCommand) -> bool {
567    return command.is_builtin() && matches!(command.word(), b"merge" | b"pull" | b"cherry-pick" | b"revert" | b"am");
568}
569
570/// The command word a trigger record names.
571///
572/// In TS you'd write (pseudocode):
573/// ```ts
574/// const word = command.word().toString();
575/// ```
576fn trigger_command(command: &ResolvedCommand) -> String {
577    return String::from_utf8_lossy(command.word()).into_owned();
578}
579
580/// The branch watch of a coordinated forward, started in the worktree the command runs in.
581/// A watch that cannot start leaves a record naming the step,
582/// in the common Git directory the wrapper already resolved.
583///
584/// In TS you'd write (pseudocode):
585/// ```ts
586/// function watchForward(forward: CoordinatedForward): TipWatch | undefined;
587/// ```
588#[cfg(unix)]
589fn watch_forward(forward: &CoordinatedForward) -> Option<TipWatch> {
590    let (root, common): (PathBuf, PathBuf) = match &forward.identity {
591        WorktreeIdentity::MainWorktree { worktree_root, common_dir, .. } | WorktreeIdentity::LinkedWorktree { worktree_root, common_dir, .. } => {
592            (worktree_root.clone(), common_dir.clone())
593        }
594        WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => return None,
595    };
596    return match try_watch_tip(root.as_path(), forward.real_git.as_path(), forward.overlay.as_slice(), forward.environment.as_slice()) {
597        Ok(watch) => Some(watch),
598        Err(declined) => {
599            // The watch could not start, so no branch is known; the worktree is.
600            let git_dir: PathBuf = match &forward.identity {
601                WorktreeIdentity::MainWorktree { git_dir, .. } | WorktreeIdentity::LinkedWorktree { git_dir, .. } => git_dir.clone(),
602                WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => common.clone(),
603            };
604            record_decision(&Decision {
605                common_dir: Some(common.as_path()),
606                origin: Some(Origin { worktree: root.as_path(), git_dir: git_dir.as_path() }),
607                command: trigger_command(&forward.command).as_str(),
608                outcome: Outcome::Declined,
609                reason: declined.reason,
610                detail: declined.detail.as_str(),
611                branch: None,
612            });
613            None
614        }
615    };
616}
617
618/// What:
619///  Choose how a forwarded command runs:
620///  plainly,
621///  under the landing lock when it writes the real index,
622///  and settled by the linked-worktree copy when it runs from a linked worktree or a bare repository
623///  (or creates a worktree from a main checkout whose configuration names the rules file).
624///  `copy_bypassed` is true for the wrapper's opt-out and for Git's query forms.
625/// Why:
626///   Only these commands need the wrapper to outlive Git or to hold a lock around it;
627///  everything else keeps the exact process replacement.
628///
629/// In TS you'd write (pseudocode):
630/// ```ts
631/// if (!isIndexWriter(command) && !copyApplies) forward(args); else await forwardToRealGit({ ..., unprovenOwnerTimeoutMs });
632/// ```
633fn forward_action(
634    real_git: PathBuf,
635    arguments: Vec<OsString>,
636    overlay: Vec<(OsString, OsString)>,
637    stderr: String,
638    environment: &[(OsString, OsString)],
639    copy_bypassed: bool,
640    checks: &mut ShippedChecks<GitFacts>,
641) -> Action {
642    let context: GitContext = GitContext {
643        real_git: real_git.clone(),
644        overlay: overlay.clone(),
645        global_prefix: Vec::new(),
646        // These queries send no input, so no scratch directory is needed.
647        common_dir: None,
648    };
649    let command: ResolvedCommand = resolve_forwarded_command(&context, arguments.as_slice());
650    // An alias value may carry its own `-c` options, and Git applies those after any option put before the alias word,
651    //  so the real Git gets the expansion the wrapper already resolved: the hooks-off options then follow the alias's own.
652    let forwarded_arguments: Vec<OsString> = match command.route {
653        Route::Alias => command.arguments.clone(),
654        Route::Direct | Route::ShellAlias | Route::Unresolved => arguments,
655    };
656    let writes_index: bool = is_index_writer(&command);
657    if !writes_index && copy_bypassed {
658        return Action::Forward {
659            real_git,
660            arguments: forwarded_arguments,
661            overlay,
662            stderr,
663        };
664    }
665    let location: RepositoryLocation = match checks.facts.location() {
666        Ok(found) => found,
667        Err(message) => return failure(message.as_str()),
668    };
669    if location.identity == WorktreeIdentity::OutsideWorktree {
670        return Action::Forward {
671            real_git,
672            arguments: forwarded_arguments,
673            overlay,
674            stderr,
675        };
676    }
677    // An invalid configuration refuses an index writer; a main checkout's worktree copy reads none.
678    if configuration_needed(writes_index)
679        && let Err(error) = load_identity_config(&location.identity)
680    {
681        return Action::Exit {
682            code: ENGINE_FAILURE_EXIT_CODE,
683            stdout: String::new(),
684            stderr: config_invalid_event(&error),
685        };
686    }
687    let creates: bool = creates_worktrees(&command);
688    let add_plan: Option<AddPlan> = if creates { plan_worktree_add(&command) } else { None };
689    let destination: Option<PathBuf> = if creates { plan_copy_destination(&command) } else { None };
690    let remove_plan: Option<RemovePlan> = if command.word() == b"worktree" { plan_worktree_remove(&command) } else { None };
691    let serves_ladder: bool = add_plan.is_some() || remove_plan.is_some();
692    let copy: Option<CopyFacts> =
693        if !copy_bypassed && copy_applies_with(&location.identity, creates, serves_ladder) {
694            Some(CopyFacts {
695                identity: location.identity.clone(),
696                creates,
697                destination,
698                add_plan,
699                remove_plan,
700            })
701        } else {
702            None
703        };
704    if !writes_index {
705        let Some(facts) = copy else {
706            return Action::Forward {
707                real_git,
708                arguments: forwarded_arguments,
709                overlay,
710                stderr,
711            };
712        };
713        return Action::ForwardSettled {
714            real_git,
715            arguments: forwarded_arguments,
716            overlay,
717            copy: Box::new(facts),
718            stderr,
719        };
720    }
721    return Action::ForwardCoordinated {
722        forward: Box::new(CoordinatedForward {
723            real_git,
724            arguments: forwarded_arguments,
725            overlay,
726            identity: location.identity,
727            command,
728            environment: environment.to_vec(),
729            copy,
730        }),
731        stderr,
732    };
733}
734
735/// What:
736///  The plan of a `git worktree add` in a form the cheap-worktree ladder serves, read from the resolved command and the
737///  process's working directory.
738/// Why:
739///   The destination is relative to the directory Git starts in, which the global `-C` options move; an unreadable
740///  working directory or a form outside the table leaves the command to plain Git.
741///
742/// In TS you'd write (pseudocode):
743/// ```ts
744/// function planWorktreeAdd(command: ResolvedCommand): AddPlan | undefined;
745/// ```
746fn plan_worktree_add(command: &ResolvedCommand) -> Option<AddPlan> {
747    let current: PathBuf = std::env::current_dir().ok()?;
748    let start: PathBuf = start_directory(&command.arguments[..command.subcommand_index], current.as_path())?;
749    return parse_add_plan(command.arguments.as_slice(), command.subcommand_index, start.as_path());
750}
751
752/// What:
753///  The destination of a `git worktree add` in any form of Git's documented options, read from the resolved command and the
754///  process's working directory.
755/// Why:
756///   The ignored-state copy goes to the worktree this command created, which the command line names; a form the reading
757///  does not know leaves the destination unknown, and the copy then says it did not run.
758///
759/// In TS you'd write (pseudocode):
760/// ```ts
761/// function planCopyDestination(command: ResolvedCommand): string | undefined;
762/// ```
763fn plan_copy_destination(command: &ResolvedCommand) -> Option<PathBuf> {
764    let current: PathBuf = std::env::current_dir().ok()?;
765    let start: PathBuf = copy_start_directory(&command.arguments[..command.subcommand_index], current.as_path())?;
766    return parse_copy_destination(command.arguments.as_slice(), command.subcommand_index, start.as_path());
767}
768
769/// What:
770///  The plan of a plain `git worktree remove <directory>`, read from the resolved command and the process's working
771///  directory.
772/// Why:
773///   The directory is relative to where Git starts, which the global `-C` options move; anything else is Git's.
774///
775/// In TS you'd write (pseudocode):
776/// ```ts
777/// function planWorktreeRemove(command: ResolvedCommand): RemovePlan | undefined;
778/// ```
779fn plan_worktree_remove(command: &ResolvedCommand) -> Option<RemovePlan> {
780    let current: PathBuf = std::env::current_dir().ok()?;
781    let start: PathBuf = start_directory(&command.arguments[..command.subcommand_index], current.as_path())?;
782    return parse_remove_plan(command.arguments.as_slice(), command.subcommand_index, start.as_path());
783}
784
785/// What:
786///  Whether `command` is a push that must upload LFS objects first, and the configuration warning for configured
787///  external hooks, when the same read shows any, as empty text otherwise.
788///  A dry-run push reads the configuration too, for the warning, but uploads nothing.
789/// Why:
790///   A push that sends nothing (`--dry-run`) needs no upload, and a repository without LFS keys needs none either.
791///  One configuration read answers it, made with the invocation's own global options; when Git cannot list its
792///  configuration the hook is added anyway, because it then decides with Git's own answers.
793///  The warning costs no read of its own: a push is the one forwarded command that already holds the snapshot.
794///
795/// In TS you'd write (pseudocode):
796/// ```ts
797/// function needsLfsUpload(command, realGit, overlay): [boolean, string, settings];
798/// ```
799fn needs_lfs_upload(
800    command: &ResolvedCommand,
801    real_git: &Path,
802    overlay: &[(OsString, OsString)],
803) -> (bool, String, Option<Vec<(String, String)>>) {
804    if command.word() != b"push" {
805        return (false, String::new(), None);
806    }
807    let dry_run: bool = parse_push_region(command.region(), &[]).is_ok_and(|region| {
808        return region.dry_run;
809    });
810    let context: GitContext = GitContext {
811        real_git: real_git.to_path_buf(),
812        overlay: overlay.to_vec(),
813        global_prefix: command.arguments[..command.subcommand_index].to_vec(),
814        common_dir: None,
815    };
816    let snapshot: Option<ConfigSnapshot> = match std::env::current_dir() {
817        Ok(directory) => read_config_snapshot(&context, directory.as_path()),
818        Err(_) => None,
819    };
820    let warning: String = external_hooks_warning(snapshot.as_ref()).unwrap_or_default();
821    if dry_run {
822        return (false, warning, None);
823    }
824    return match snapshot {
825        Some(found) => {
826            let uses: bool = uses_lfs(&found);
827            let settings: Option<Vec<(String, String)>> = uses.then(|| {
828                return settings_from_snapshot(&found);
829            });
830            (uses, warning, settings)
831        }
832        None => (true, warning, None),
833    };
834}
835
836/// What:
837///  Turn the forward of a plain push that may be integrated into a run that can integrate it,
838///  and put the Git LFS upload in front of every push that publishes.
839/// Why:
840///   A push that lost a race to another session cannot succeed again, and the wrapper can only look at its status
841///  while it is alive;
842///  the stacked push itself carries `STACKED_PUSH_MARKER`, so it is forwarded as it is and never stacks again.
843///   A real push in a repository with LFS keys also runs as a child that the Git LFS upload (`lfs_upload`) precedes,
844///  the stacked push too, so the parent can explain a failed upload;
845///  the integration step's own dry-run queries never upload.
846///   Every other action is returned unchanged.
847///
848/// In TS you'd write (pseudocode):
849/// ```ts
850/// function stackIfPush(action: Action, environment): Action;
851/// ```
852fn stack_if_push(action: Action, environment: &[(OsString, OsString)]) -> Action {
853    let (real_git, arguments, overlay, copy, stderr) = match action {
854        Action::Forward {
855            real_git,
856            arguments,
857            overlay,
858            stderr,
859        } => (real_git, arguments, overlay, None, stderr),
860        Action::ForwardSettled {
861            real_git,
862            arguments,
863            overlay,
864            copy,
865            stderr,
866        } => (real_git, arguments, overlay, Some(copy), stderr),
867        other => return other,
868    };
869    let context: GitContext = GitContext {
870        real_git: real_git.clone(),
871        overlay: overlay.clone(),
872        global_prefix: Vec::new(),
873        // These queries send no input, so no scratch directory is needed.
874        common_dir: None,
875    };
876    let command: ResolvedCommand = resolve_forwarded_command(&context, arguments.as_slice());
877    let (lfs, hook_warning, lfs_settings): (bool, String, Option<Vec<(String, String)>>) =
878        needs_lfs_upload(&command, real_git.as_path(), overlay.as_slice());
879    let stderr_with_warning: String = format!("{stderr}{hook_warning}");
880    let stacked_already: bool = environment_value(environment, STACKED_PUSH_MARKER).is_some();
881    let stack: bool =
882        !stacked_already && command.word() == b"push" && stackable_option_tokens(command.region()).is_some();
883    // A push that uploads LFS objects runs as a child like a stackable one: the parent that owns the push is the one
884    // that explains a failed upload, and no wrapper is started for it.
885    if stack || lfs {
886        return Action::ForwardPush {
887            forward: Box::new(PushForward {
888                real_git,
889                subcommand_index: command.subcommand_index,
890                arguments: command.arguments,
891                overlay,
892                copy,
893                environment: environment.to_vec(),
894                lfs,
895                lfs_settings,
896                stack,
897            }),
898            stderr: stderr_with_warning,
899        };
900    }
901    // A plain forwarded fetch or push becomes Git itself, so the reference locks of a killed Git are judged here, before.
902    let lock_notice: String = if is_covered(command.word()) {
903        forwarded_lock_notice(command.word(), &command.arguments[..command.subcommand_index], environment, &Judge::here())
904    } else {
905        String::new()
906    };
907    let stderr_with_locks: String = format!("{stderr_with_warning}{lock_notice}");
908    let forwarded: Vec<OsString> = arguments;
909    return match copy {
910        Some(facts) => Action::ForwardSettled {
911            real_git,
912            arguments: forwarded,
913            overlay,
914            copy: facts,
915            stderr: stderr_with_locks,
916        },
917        None => Action::Forward {
918            real_git,
919            arguments: forwarded,
920            overlay,
921            stderr: stderr_with_locks,
922        },
923    };
924}
925
926/// What:
927///  Whether the forwarded command needs the repository configuration.
928/// Why:
929///   An index writer reads its patience from it; the copy of a main checkout's ignored state reads none,
930///  so a command that only creates a worktree loads nothing.
931///
932/// In TS you'd write (pseudocode):
933/// ```ts
934/// isIndexWriter(command)
935/// ```
936fn configuration_needed(writes_index: bool) -> bool {
937    return writes_index;
938}
939
940/// What:
941///  Write text to an output stream,
942///  ignoring a closed stream.
943///       `&mut dyn Write` lends "any writable stream" (`dyn` means the concrete type is
944///       chosen at run time,
945///  like a TS interface value).
946/// Why:
947///   The exit code already carries the result;
948///  a reader that went away must not
949///       turn a diagnostic into a panic.
950///
951/// In TS you'd write (pseudocode):
952/// ```ts
953/// function writeStream(stream: Writable, text: string): void { try { stream.write(text); } catch {} }
954/// ```
955fn write_stream(stream: &mut dyn Write, text: &str) {
956    // What: `let _ = ...` deliberately discards the write's `Result`.
957    // Why:  There is nowhere left to report a failure to write a diagnostic.
958    //
959    // In TS you'd write (pseudocode):
960    // ```ts
961    // try { stream.write(text); } catch { /* reader closed */ }
962    // ```
963    let _ = stream.write_all(text.as_bytes());
964}
965
966/// Plans of the announced queries,
967///  `--version`, `-v`, `--help` and `-h`, against the queries that stay exact.
968#[cfg(test)]
969#[path = "entry_announced_tests.rs"]
970mod announced_tests;
971
972/// Plans whose real Git is the one the commit service's unit records because `PATH` offers none.
973#[cfg(test)]
974#[path = "entry_fallback_tests.rs"]
975mod fallback_tests;
976
977/// Stop controls for a looping,
978///  missing or uninterpretable real Git.
979#[cfg(test)]
980#[path = "entry_stop_tests.rs"]
981mod stop_tests;
982
983/// Decision controls and the shared fixtures stay out of the release executable.
984#[cfg(test)]
985#[path = "entry_tests.rs"]
986mod tests;