Skip to main content

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;