Skip to main content

git_policy_cli/
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 super::action::{Action, ENGINE_FAILURE_EXIT_CODE, PushForward, failure};
20use super::command_push::stackable_option_tokens;
21use super::lfs_pre_push::{hook_entry_arguments, run_hook_entry, uses_lfs, with_lfs_hook};
22use super::config_snapshot::{ConfigSnapshot, read_config_snapshot};
23use super::push_stacking::{STACKED_PUSH_MARKER, run_stacked_push};
24use super::command_push::parse_push_region;
25use super::child_environment::{
26    FORWARD_TARGET_VARIABLE, child_environment_overlay, environment_value,
27};
28use super::effective_target::default_allowed_worktree_dirs;
29use super::forwarding::replace_process_with_real_git;
30use super::index_writer_forward::{CoordinatedForward, CoordinatedRun, run_coordinated_forward};
31use super::index_writer_commands::is_index_writer;
32use super::forwarded_command::{ResolvedCommand, creates_or_moves_worktrees, resolve_forwarded_command};
33use super::invocation_config::{config_invalid_event, load_identity_config};
34use super::config_file::LoadedConfig;
35use super::repository_location::RepositoryLocation;
36use super::transaction_git::GitContext;
37use super::worktree_identity::WorktreeIdentity;
38use super::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
39use super::management::plan_management;
40/// The linter the executable gives the Markdown policy.
41use super::markdown_linter::executable_linter;
42/// The shipped checks and their constructor.
43use super::policy_checks::{ShippedChecks, shipped_checks};
44/// A forwarded command starts with no candidate content.
45use super::policy_content::LifecycleContent;
46use super::real_git::{ResolutionInputs, process_resolution_inputs, resolve_real_git};
47use super::real_git_candidate::same_file;
48use super::repository_facts::{GitFacts, RepositoryFacts, git_facts};
49use super::rule_require_root::{RequireRootDecision, decide_require_root};
50/// The variable that names the forbidden-strings rules list.
51use super::scanner_selection::RULES_VARIABLE;
52use super::wrapped_command::{WrappedOutcome, run_wrapped_command};
53use super::wrapper_controls::{Controls, no_controls, strip_global_controls};
54use super::worktree_copy_forward::{CopyFacts, ForwardedRun, copy_applies, run_forwarded_git};
55use super::wrapper_invocation::{StrippedInvocation, strip_wrapper_controls};
56/// What:
57///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
58///       must be UTF-8).
59/// Why:
60///   Arguments and environment values are never decoded.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// // process.argv and process.env, but byte-preserving.
65/// ```
66use std::ffi::OsString;
67/// The trait that gives output streams `.write_all(..)`.
68use std::io::Write;
69/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
70use std::path::{Path, PathBuf};
71
72/// The management namespace:
73///  `git cli-git ...` is handled by the wrapper,
74///  never by Git.
75pub const MANAGEMENT_COMMAND: &str = "cli-git";
76
77/// What:
78///  Explain that another cli-git wrapper selected this executable as real Git.
79///       `&Path` borrows this executable's path for the message.
80/// Why:
81///   Forwarding again would bounce between the two wrappers forever;
82///  stopping
83///       names this executable so the user can fix PATH.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// function forwardedToSelf(ownExecutable: string): Action;
88/// ```
89fn forwarded_to_self(own_executable: &Path) -> Action {
90    // `format!` builds owned text; `.as_str()` lends it to the helper.
91    return failure(
92        format!(
93            "another cli-git wrapper selected this cli-git executable ({}) as real Git. \
94             Forwarding again would never reach Git. Remove the extra cli-git from PATH or \
95             place the real Git executable on PATH.",
96            own_executable.display()
97        )
98        .as_str(),
99    );
100}
101
102/// What:
103///  The caller's home directory from the environment,
104///  if it names one.
105///       `Option<PathBuf>` is "an owned path or nothing".
106/// Why:
107///   The tool caches exempt from linked-worktree enforcement live under the home
108///       directory;
109///  it comes from the injected environment,
110///  never from a fixed path.
111///
112/// In TS you'd write (pseudocode):
113/// ```ts
114/// const home = environment.HOME === undefined || environment.HOME === '' ? undefined : environment.HOME;
115/// ```
116fn home_directory(environment: &[(OsString, OsString)]) -> Option<PathBuf> {
117    // `if let Some(home) = ... && cond` runs only when the variable is set and not empty.
118    if let Some(home) = environment_value(environment, "HOME")
119        && !home.is_empty()
120    {
121        // `Some(...)` is the "present" case; `PathBuf::from` turns the text into a path.
122        return Some(PathBuf::from(home));
123    }
124    // `None` is the "absent" case.
125    return None;
126}
127
128/// What:
129///  Decide the action for one invocation from injected process facts.
130///       `&[OsString]` borrows the arguments after the program name;
131///       `&[(OsString, OsString)]` borrows the environment as name/value pairs.
132/// Why:
133///   Order matters.
134///  The recursion check runs before anything else.
135///  Wrapper controls
136///       written before the subcommand are removed first,
137///  because Git's own reading of
138///       the arguments stops at them and would hide the subcommand from every later
139///       decision.
140///  The management namespace is answered by the wrapper.
141///  Every other
142///       command goes through the wrapped-command lifecycle with real Git behind it.
143///
144/// In TS you'd write (pseudocode):
145/// ```ts
146/// function planInvocation(args: string[], environment: [string, string][], inputs: ResolutionInputs): Action;
147/// ```
148pub fn plan_invocation(
149    arguments: &[OsString],
150    environment: &[(OsString, OsString)],
151    inputs: &ResolutionInputs,
152) -> Action {
153    // `if let Some(target) = ... && cond` runs only when a forwarding wrapper left its
154    // marker and that marker names this executable.
155    if let Some(target) = environment_value(environment, FORWARD_TARGET_VARIABLE)
156        && same_file(Path::new(&target), inputs.own_executable.as_path())
157    {
158        return forwarded_to_self(inputs.own_executable.as_path());
159    }
160    // `mut` allows the scan to record what the removed controls asked for.
161    let mut global_controls: Controls = no_controls();
162    // `&mut global_controls` lends the record for writing.
163    let global_clean: Vec<OsString> = strip_global_controls(arguments, &mut global_controls);
164    // `.as_slice()` lends the owned list as a borrowed view.
165    let layout: GlobalLayout = global_layout(global_clean.as_slice());
166    if layout.outcome == GlobalOutcome::Command
167        && global_clean[layout.prefix_len] == MANAGEMENT_COMMAND
168    {
169        // `&list[a..]` borrows from index `a` on; `&list[..a]` borrows up to it.
170        return plan_management(
171            &global_clean[layout.prefix_len + 1..],
172            &global_clean[..layout.prefix_len],
173            &global_controls,
174            environment,
175            inputs,
176        );
177    }
178    // What: `match` on the resolver's `Result`: keep the path or stop with its message.
179    // Why:  Without real Git there is nothing to forward to.
180    //
181    // In TS you'd write (pseudocode):
182    // ```ts
183    // let realGit; try { realGit = resolveRealGit(inputs); } catch (e) { return failure(String(e)); }
184    // ```
185    let real_git: PathBuf = match resolve_real_git(inputs) {
186        Ok(found) => found,
187        Err(error) => return failure(error.to_string().as_str()),
188    };
189    let overlay: Vec<(OsString, OsString)> =
190        child_environment_overlay(environment, real_git.as_path());
191    let stripped: StrippedInvocation = strip_wrapper_controls(arguments);
192    // The global options before the subcommand select the repository for every fact query.
193    let facts: GitFacts = git_facts(
194        real_git.as_path(),
195        &stripped.arguments[..stripped.layout.prefix_len],
196        overlay.as_slice(),
197    );
198    // `mut` lets the lifecycle cache facts and state what content policies can read.
199    let mut checks: ShippedChecks<GitFacts> = shipped_checks(
200        facts,
201        // `.clone()` copies the stripped arguments for the rule cores.
202        stripped.arguments.clone(),
203        LifecycleContent::None,
204        default_allowed_worktree_dirs(
205            environment,
206            // `.as_deref()` lends the optional owned path as an optional borrowed one.
207            home_directory(environment).as_deref(),
208        ),
209    );
210    // The forbidden-strings rules variable is read from this invocation's environment.
211    checks.scanner_settings.rules_variable = environment_value(environment, RULES_VARIABLE);
212    // The Markdown linter is found on, and runs with, this invocation's environment.
213    checks.markdown.linter = Box::new(executable_linter(environment));
214    // `match` picks by variant and binds the fields each ending carries.
215    match run_wrapped_command(&stripped, environment, &mut checks) {
216        // `arguments: forwarded` binds the field under a new name.
217        WrappedOutcome::Forward {
218            arguments: forwarded,
219            stderr,
220        } => {
221            // The opt-out, Git's help and version forms, and the commands that need no repository
222            // (`version`, `help`, `init`, `clone`, user-level `config`) never settle a copy.
223            let copy_bypassed: bool = stripped.controls.skip_worktree_copy
224                || stripped.layout.outcome != GlobalOutcome::Command
225                || decide_require_root(forwarded.as_slice(), &[])
226                    != RequireRootDecision::NeedsRepositoryRoot;
227            return stack_if_push(
228                forward_action(
229                    real_git,
230                    forwarded,
231                    overlay,
232                    stderr,
233                    environment,
234                    copy_bypassed,
235                    &mut checks,
236                ),
237                environment,
238                inputs.own_executable.as_path(),
239            );
240        }
241        WrappedOutcome::Exit { code, stderr } => {
242            return Action::Exit {
243                code,
244                // `String::new()` is empty owned text: wrapped commands report on standard error.
245                stdout: String::new(),
246                stderr,
247            };
248        }
249    }
250}
251
252/// What:
253///  Run one invocation with the real process facts and return its exit code.
254///       `i32` is a signed 32-bit integer,
255///  the type of process exit codes.
256///       Returns only when Git was not started;
257///  a forwarded command never returns.
258/// Why:
259///   This is everything the executable's `main` does;
260///  keeping it in the library
261///       lets the same code be measured by tests and mutation runs.
262///
263/// In TS you'd write (pseudocode):
264/// ```ts
265/// function runProcess(args: string[], environment: [string, string][]): number;
266/// ```
267pub fn run_process(arguments: &[OsString], environment: &[(OsString, OsString)]) -> i32 {
268    // From here on every real-Git start of this process runs with traditional hooks off.
269    super::hooks_off::activate();
270    // Git starts the LFS pre-push hook as `cli-git lfs-pre-push <remote> <url>`; it needs no repository facts.
271    if let Some(hook_arguments) = hook_entry_arguments(arguments) {
272        return run_hook_entry(hook_arguments);
273    }
274    let action: Action = match process_resolution_inputs(environment) {
275        Ok(inputs) => plan_invocation(arguments, environment, &inputs),
276        Err(error) => failure(
277            format!(
278                "cannot determine this executable or the current directory, so real Git \
279                 cannot be selected safely: {error}"
280            )
281            .as_str(),
282        ),
283    };
284    // `match` picks by variant and binds the fields each one carries.
285    match action {
286        Action::Forward {
287            real_git,
288            arguments: forwarded,
289            overlay,
290            stderr,
291        } => {
292            // Warning events reach the caller before Git's own output.
293            write_stream(&mut std::io::stderr(), stderr.as_str());
294            let error: std::io::Error = replace_process_with_real_git(
295                real_git.as_path(),
296                forwarded.as_slice(),
297                overlay.as_slice(),
298            );
299            write_stream(
300                &mut std::io::stderr(),
301                format!(
302                    "cli-git: cannot start {}: {error}. Hint: if you moved or upgraded Git, check that the file exists and is executable with `ls -l {}`, \
303                     then reinstall Git or the wrapper; no Git command ran.\n",
304                    real_git.display(),
305                    real_git.display()
306                )
307                .as_str(),
308            );
309            return ENGINE_FAILURE_EXIT_CODE;
310        }
311        Action::ForwardCoordinated { forward, stderr } => {
312            write_stream(&mut std::io::stderr(), stderr.as_str());
313            return run_coordinated_action(&forward);
314        }
315        Action::ForwardSettled {
316            real_git,
317            arguments: forwarded,
318            overlay,
319            copy,
320            stderr,
321        } => {
322            write_stream(&mut std::io::stderr(), stderr.as_str());
323            return finish_forwarded(run_forwarded_git(
324                real_git.as_path(),
325                forwarded.as_slice(),
326                overlay.as_slice(),
327                Some(&*copy),
328            ));
329        }
330        Action::ForwardPush { forward, stderr } => {
331            write_stream(&mut std::io::stderr(), stderr.as_str());
332            return run_stacked_push(&forward);
333        }
334        Action::Exit {
335            code,
336            stdout,
337            stderr,
338        } => {
339            // `&mut` lends each stream for writing.
340            write_stream(&mut std::io::stdout(), stdout.as_str());
341            write_stream(&mut std::io::stderr(), stderr.as_str());
342            return code;
343        }
344    }
345}
346
347/// What:
348///  The exit code of a forwarded run, after printing the text of a run that failed.
349/// Why:
350///   A settled run keeps the wrapper alive around Git, so the exit code returns here instead of replacing the process.
351///
352/// In TS you'd write (pseudocode):
353/// ```ts
354/// process.exitCode = outcome.exitCode;
355/// ```
356fn finish_forwarded(run: ForwardedRun) -> i32 {
357    // `match` on the ending: a status, or a failure with its own text.
358    match run {
359        ForwardedRun::Ran(code) => return code,
360        ForwardedRun::Failed { stderr, code } => {
361            write_stream(&mut std::io::stderr(), stderr.as_str());
362            return code;
363        }
364    }
365}
366
367/// What:
368///  Run a coordinated forward:
369///  print its failure,
370///  run Git under the landing lock,
371///  or replace the process when no coordination applies after all.
372/// Why:
373///   The lock must outlive Git,
374///  so only a run that takes no lock may replace the process.
375///
376/// In TS you'd write (pseudocode):
377/// ```ts
378/// const outcome = await forwardToRealGit({ ... }); process.exitCode = outcome.exitCode;
379/// ```
380fn run_coordinated_action(forward: &CoordinatedForward) -> i32 {
381    match run_coordinated_forward(forward) {
382        CoordinatedRun::Ran(code) => return code,
383        CoordinatedRun::Failed { stderr, code } => {
384            write_stream(&mut std::io::stderr(), stderr.as_str());
385            return code;
386        }
387        CoordinatedRun::NotCoordinated => {
388            let error: std::io::Error = replace_process_with_real_git(
389                forward.real_git.as_path(),
390                forward.arguments.as_slice(),
391                forward.overlay.as_slice(),
392            );
393            write_stream(
394                &mut std::io::stderr(),
395                format!("cli-git: cannot start {}: {error}\n", forward.real_git.display()).as_str(),
396            );
397            return ENGINE_FAILURE_EXIT_CODE;
398        }
399    }
400}
401
402/// What:
403///  Choose how a forwarded command runs:
404///  plainly,
405///  under the landing lock when it writes the real index,
406///  and settled by the linked-worktree copy when it runs from a linked worktree or a bare repository
407///  (or creates a worktree from a main checkout whose configuration names the rules file).
408///  `copy_bypassed` is true for the wrapper's opt-out and for Git's help and version forms.
409/// Why:
410///   Only these commands need the wrapper to outlive Git or to hold a lock around it;
411///  everything else keeps the exact process replacement.
412///
413/// In TS you'd write (pseudocode):
414/// ```ts
415/// if (!isIndexWriter(command) && !copyApplies) forward(args); else await forwardToRealGit({ ..., unprovenOwnerTimeoutMs });
416/// ```
417fn forward_action(
418    real_git: PathBuf,
419    arguments: Vec<OsString>,
420    overlay: Vec<(OsString, OsString)>,
421    stderr: String,
422    environment: &[(OsString, OsString)],
423    copy_bypassed: bool,
424    checks: &mut ShippedChecks<GitFacts>,
425) -> Action {
426    let context: GitContext = GitContext {
427        real_git: real_git.clone(),
428        overlay: overlay.clone(),
429        global_prefix: Vec::new(),
430        // These queries send no input, so no scratch directory is needed.
431        common_dir: None,
432    };
433    let command: ResolvedCommand = resolve_forwarded_command(&context, arguments.as_slice());
434    let writes_index: bool = is_index_writer(&command);
435    if !writes_index && copy_bypassed {
436        return Action::Forward {
437            real_git,
438            arguments,
439            overlay,
440            stderr,
441        };
442    }
443    let location: RepositoryLocation = match checks.facts.location() {
444        Ok(found) => found,
445        Err(message) => return failure(message.as_str()),
446    };
447    if location.identity == WorktreeIdentity::OutsideWorktree {
448        return Action::Forward {
449            real_git,
450            arguments,
451            overlay,
452            stderr,
453        };
454    }
455    let loaded: Option<LoadedConfig> = match configuration_needed(&location.identity, &command, writes_index) {
456        false => None,
457        true => match load_identity_config(&location.identity) {
458            Ok(found) => Some(found),
459            Err(error) => {
460                return Action::Exit {
461                    code: ENGINE_FAILURE_EXIT_CODE,
462                    stdout: String::new(),
463                    stderr: config_invalid_event(&error),
464                };
465            }
466        },
467    };
468    let creates_or_moves: bool = creates_or_moves_worktrees(&command);
469    // Only a main checkout hands over the configured rules files; `copy_applies` limits it to a creating command.
470    let main_checkout_paths: Vec<String> = match (&location.identity, &loaded) {
471        // Every configured file, required or optional: the copier skips a file Git does not ignore or that is absent.
472        (WorktreeIdentity::MainWorktree { .. }, Some(found)) => found
473            .config
474            .policies
475            .forbidden_strings
476            .rules_files
477            .iter()
478            .flatten()
479            .map(|file| return file.path.clone())
480            .collect(),
481        _ => Vec::new(),
482    };
483    let copy: Option<CopyFacts> =
484        if !copy_bypassed && copy_applies(&location.identity, creates_or_moves, main_checkout_paths.as_slice()) {
485            Some(CopyFacts {
486                identity: location.identity.clone(),
487                creates_or_moves,
488                main_checkout_paths,
489                global_prefix: checks.facts.transaction_context().global_prefix,
490                environment: environment.to_vec(),
491            })
492        } else {
493            None
494        };
495    if !writes_index {
496        let Some(facts) = copy else {
497            return Action::Forward {
498                real_git,
499                arguments,
500                overlay,
501                stderr,
502            };
503        };
504        return Action::ForwardSettled {
505            real_git,
506            arguments,
507            overlay,
508            copy: Box::new(facts),
509            stderr,
510        };
511    }
512    let Some(found) = loaded else {
513        return failure("the repository configuration was not loaded for an index writer");
514    };
515    return Action::ForwardCoordinated {
516        forward: Box::new(CoordinatedForward {
517            real_git,
518            arguments,
519            overlay,
520            identity: location.identity,
521            command,
522            environment: environment.to_vec(),
523            timeout_ms: found.config.concurrency.index_lock.unproven_owner_timeout_ms,
524            copy,
525        }),
526        stderr,
527    };
528}
529
530/// What:
531///  The executable the Git LFS pre-push hook names, when `command` is a push that must upload LFS objects first.
532///  `Option<&Path>` is "the executable, or nothing to add".
533/// Why:
534///   A push that sends nothing (`--dry-run`) needs no upload, and a repository without LFS keys needs none either.
535///  One configuration read answers it, made with the invocation's own global options; when Git cannot list its
536///  configuration the hook is added anyway, because it then decides with Git's own answers.
537///
538/// In TS you'd write (pseudocode):
539/// ```ts
540/// function lfsHookExecutable(command, realGit, overlay, ownExecutable): string | undefined;
541/// ```
542fn lfs_hook_executable<'executable>(
543    command: &ResolvedCommand,
544    real_git: &Path,
545    overlay: &[(OsString, OsString)],
546    own_executable: &'executable Path,
547) -> Option<&'executable Path> {
548    if command.word() != b"push" {
549        return None;
550    }
551    if parse_push_region(command.region(), &[]).is_ok_and(|region| {
552        return region.dry_run;
553    }) {
554        return None;
555    }
556    let context: GitContext = GitContext {
557        real_git: real_git.to_path_buf(),
558        overlay: overlay.to_vec(),
559        global_prefix: command.arguments[..command.subcommand_index].to_vec(),
560        common_dir: None,
561    };
562    let snapshot: Option<ConfigSnapshot> = match std::env::current_dir() {
563        Ok(directory) => read_config_snapshot(&context, directory.as_path()),
564        Err(_) => None,
565    };
566    return match snapshot {
567        Some(found) if !uses_lfs(&found) => None,
568        Some(_) | None => Some(own_executable),
569    };
570}
571
572/// What:
573///  Turn the forward of a plain push that may be integrated into a run that can integrate it,
574///  and put the Git LFS upload in front of every push that publishes.
575/// Why:
576///   A push that lost a race to another session cannot succeed again, and the wrapper can only look at its status
577///  while it is alive;
578///  the stacked push itself carries `STACKED_PUSH_MARKER`, so it is forwarded as it is and never stacks again.
579///   A real push in a repository with LFS keys carries the pre-push hook of `lfs_pre_push`, the stacked push too;
580///  the integration step's own dry-run queries run without it, because they would upload objects.
581///   Every other action is returned unchanged.
582///
583/// In TS you'd write (pseudocode):
584/// ```ts
585/// function stackIfPush(action: Action, environment, ownExecutable): Action;
586/// ```
587fn stack_if_push(action: Action, environment: &[(OsString, OsString)], own_executable: &Path) -> Action {
588    let (real_git, arguments, overlay, copy, stderr) = match action {
589        Action::Forward {
590            real_git,
591            arguments,
592            overlay,
593            stderr,
594        } => (real_git, arguments, overlay, None, stderr),
595        Action::ForwardSettled {
596            real_git,
597            arguments,
598            overlay,
599            copy,
600            stderr,
601        } => (real_git, arguments, overlay, Some(copy), stderr),
602        other => return other,
603    };
604    let context: GitContext = GitContext {
605        real_git: real_git.clone(),
606        overlay: overlay.clone(),
607        global_prefix: Vec::new(),
608        // These queries send no input, so no scratch directory is needed.
609        common_dir: None,
610    };
611    let command: ResolvedCommand = resolve_forwarded_command(&context, arguments.as_slice());
612    let lfs_executable: Option<&Path> =
613        lfs_hook_executable(&command, real_git.as_path(), overlay.as_slice(), own_executable);
614    let stacked_already: bool = environment_value(environment, STACKED_PUSH_MARKER).is_some();
615    if !stacked_already && command.word() == b"push" && stackable_option_tokens(command.region()).is_some() {
616        return Action::ForwardPush {
617            forward: Box::new(PushForward {
618                real_git,
619                subcommand_index: command.subcommand_index,
620                arguments: command.arguments,
621                overlay,
622                copy,
623                environment: environment.to_vec(),
624                lfs_pre_push: lfs_executable.map(Path::to_path_buf),
625            }),
626            stderr,
627        };
628    }
629    let forwarded: Vec<OsString> = match lfs_executable {
630        Some(executable) => with_lfs_hook(arguments.as_slice(), executable),
631        None => arguments,
632    };
633    return match copy {
634        Some(facts) => Action::ForwardSettled {
635            real_git,
636            arguments: forwarded,
637            overlay,
638            copy: facts,
639            stderr,
640        },
641        None => Action::Forward {
642            real_git,
643            arguments: forwarded,
644            overlay,
645            stderr,
646        },
647    };
648}
649
650/// What:
651///  Whether the forwarded command needs the repository configuration.
652/// Why:
653///   An index writer reads its patience from it, and a main checkout reads the rules file name from it;
654///  linked worktrees and bare repositories need no configuration for the copy, so their commands load nothing new.
655///
656/// In TS you'd write (pseudocode):
657/// ```ts
658/// isIndexWriter(command) || (identity.kind === 'main-worktree' && createsOrMovesWorktrees(command))
659/// ```
660fn configuration_needed(identity: &WorktreeIdentity, command: &ResolvedCommand, writes_index: bool) -> bool {
661    return writes_index
662        || (matches!(identity, WorktreeIdentity::MainWorktree { .. })
663            && creates_or_moves_worktrees(command));
664}
665
666/// What:
667///  Write text to an output stream,
668///  ignoring a closed stream.
669///       `&mut dyn Write` lends "any writable stream" (`dyn` means the concrete type is
670///       chosen at run time,
671///  like a TS interface value).
672/// Why:
673///   The exit code already carries the result;
674///  a reader that went away must not
675///       turn a diagnostic into a panic.
676///
677/// In TS you'd write (pseudocode):
678/// ```ts
679/// function writeStream(stream: Writable, text: string): void { try { stream.write(text); } catch {} }
680/// ```
681fn write_stream(stream: &mut dyn Write, text: &str) {
682    // What: `let _ = ...` deliberately discards the write's `Result`.
683    // Why:  There is nowhere left to report a failure to write a diagnostic.
684    //
685    // In TS you'd write (pseudocode):
686    // ```ts
687    // try { stream.write(text); } catch { /* reader closed */ }
688    // ```
689    let _ = stream.write_all(text.as_bytes());
690}
691
692/// Stop controls for a looping,
693///  missing or uninterpretable real Git.
694#[cfg(test)]
695#[path = "entry_stop_tests.rs"]
696mod stop_tests;
697
698/// Decision controls and the shared fixtures stay out of the release executable.
699#[cfg(test)]
700#[path = "entry_tests.rs"]
701mod tests;