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