git_policy_cli/invocation/announced_query.rs
1//! What:
2//! Recognize the four top-level spellings after which the wrapper says it is not plain Git
3//! (`--version`, `-v`, `--help`, `-h`),
4//! and build the text it prints once Git has answered.
5//! Why:
6//! A caller who types `git --version` should be able to tell that a policy wrapper sits in front of Git,
7//! without the wrapper changing a single byte of what Git itself printed.
8//! Every other query (`--exec-path`, `--html-path`, ...) stays an exact process replacement.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! // const query = announcedQuery(args, globalLayout(args));
13//! // after the child exits: if (outcome === { exited: 0 }) process.stdout.write(announcement(query, version, realGit));
14//! ```
15
16/// What:
17/// How a child ended,
18/// which decides whether the wrapper adds text.
19/// Why:
20/// Only a child that ended with exit code 0 answered the question.
21use crate::forwarding::ChildOutcome;
22/// The result of the global-option scan that says where the query token sits.
23use crate::global_arguments::{GlobalLayout, GlobalOutcome};
24/// Native argument storage;
25/// no lossy UTF-8 conversion occurs.
26use std::ffi::OsString;
27/// Borrowed filesystem path of raw OS bytes.
28use std::path::Path;
29
30/// What:
31/// The crate version printed in the banner.
32/// `env!` reads a Cargo-provided environment variable while compiling.
33/// Why:
34/// The banner must name the wrapper build that answered,
35/// and the manifest is the one place that version lives.
36///
37/// In TS you'd write (pseudocode):
38/// ```ts
39/// const WRAPPER_VERSION = packageJson.version;
40/// ```
41pub const WRAPPER_VERSION: &str = env!("CARGO_PKG_VERSION");
42
43/// Which announced query an invocation is.
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum AnnouncedQuery {
46 /// `--version` or `-v`:
47 /// one line follows Git's version line.
48 Version,
49 /// `--help` or `-h`:
50 /// a short section follows Git's usage text.
51 Help,
52}
53
54/// What:
55/// Say whether the query token of an invocation is one of the four announced spellings.
56/// `Option<AnnouncedQuery>` is "the query, or nothing".
57/// Why:
58/// The layout already told the caller this is a query and where it sits;
59/// only the exact token separates `--version` from `--exec-path`.
60/// Nothing else is read, so commands that are not queries pay one comparison.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// function announcedQuery(args: string[], layout: GlobalLayout): 'version' | 'help' | undefined;
65/// ```
66pub fn announced_query(arguments: &[OsString], layout: &GlobalLayout) -> Option<AnnouncedQuery> {
67 if layout.outcome != GlobalOutcome::Query {
68 return None;
69 }
70 // `.as_encoded_bytes()` lends the raw bytes of the query token.
71 match arguments[layout.prefix_len].as_encoded_bytes() {
72 b"--version" | b"-v" => return Some(AnnouncedQuery::Version),
73 b"--help" | b"-h" => return Some(AnnouncedQuery::Help),
74 _ => return None,
75 }
76}
77
78/// What:
79/// Build the text printed after Git's own answer.
80/// `&str` borrows the version;
81/// `&Path` borrows the resolved real Git path;
82/// the result is owned, line-terminated text.
83/// Why:
84/// The banner states which wrapper build and which Git answered;
85/// the help section also says what the wrapper adds.
86/// Building it from plain values keeps it testable without a process.
87/// A path that is not UTF-8 prints with replacement characters, because the text is for people.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// function announcement(query: AnnouncedQuery, version: string, realGit: string): string;
92/// ```
93pub fn announcement(query: AnnouncedQuery, version: &str, real_git: &Path) -> String {
94 match query {
95 AnnouncedQuery::Version => {
96 return format!(
97 "monochromatic-git-policy-cli {version}, shadowing git at {}\n",
98 real_git.display()
99 );
100 }
101 AnnouncedQuery::Help => {
102 return format!(
103 "\nmonochromatic-git-policy-cli {version} sits in front of this git\n\
104 It adds repository policies on commit, push and worktree commands,\n\
105 the `git cli-git` management command (`git cli-git --help`),\n\
106 and `git cli-git doctor`, which lists the traditional hooks a repository holds.\n\
107 real git: {}\n",
108 real_git.display()
109 );
110 }
111 }
112}
113
114/// What:
115/// The part of the announcement to print for a finished child.
116/// The result borrows from `text`.
117/// Why:
118/// Git that failed or was killed did not answer,
119/// so the caller sees Git's status and Git's own diagnostics unchanged.
120///
121/// In TS you'd write (pseudocode):
122/// ```ts
123/// function printableAfter(outcome: ChildOutcome, text: string): string { return outcome.exited === 0 ? text : ''; }
124/// ```
125pub fn printable_after(outcome: ChildOutcome, text: &str) -> &str {
126 if outcome == ChildOutcome::Exited(0) {
127 return text;
128 }
129 return "";
130}
131
132/// Layout, text and status controls stay out of the release executable.
133#[cfg(test)]
134#[path = "announced_query_tests.rs"]
135mod tests;