Skip to main content

git_policy_cli/worktree/
inner_git_words.rs

1//! What:
2//!  Where the words of a Git run the wrapper or the service started on its own go, now that none of those runs carries
3//!  `--quiet` or `-q`: one journal entry, from the service for a run it made and from the wrapper for a run it made while
4//!  carrying out a person's command.
5//! Why:
6//!  The owner's rule is "No the wrapper should never append --quiet. Letting native git emit whatever it wants to is expected",
7//!  and for the wrapper's and the service's own runs the answer was "Drop it everywhere (Recommended)". Every such run is
8//!  captured (`run_git` pipes both streams into `GitOutput`), so Git's words would be discarded with the capture; this
9//!  keeps them where a failure of the same run is already recorded (`journalctl`). A hook's standard output and the service's
10//!  protocol replies are never the destination, and no other option or redirect hides the words in the meantime.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! const words = [stdout, stderr].filter((text) => text.trim() !== '').join('\n'); if (words) journal.info(`git ${command} said: ${words}`);
15//! ```
16
17/// Masks the userinfo of any URL in Git's words, as every journal entry of the service is masked.
18use crate::lfs_output_mask::mask_userinfo_text;
19/// The entry from a wrapper a person ran, which only Linux writes.
20#[cfg(target_os = "linux")]
21use crate::client_journal::record_entry;
22/// The priority an entry carries, and the service's way to write one.
23use crate::journald_native::Priority;
24use crate::service_timing::{in_service, log_entry};
25/// The captured output of one Git start.
26use crate::transaction_git::GitOutput;
27
28/// The field a reader filters the journal on to find the entries of this module.
29pub const COMMAND_FIELD: &str = "CLI_GIT_INNER_COMMAND";
30
31/// What:
32///  The text of a Git run's words: its standard output, then its standard error, trimmed, or nothing when it said nothing.
33/// Why:
34///   A run that said nothing makes no entry, so an entry always carries something to read.
35///
36/// In TS you'd write (pseudocode):
37/// ```ts
38/// [stdout, stderr].map((text) => text.trim()).filter(Boolean).join('\n') || undefined
39/// ```
40pub fn words_of(stdout: &[u8], stderr: &[u8]) -> Option<String> {
41    // The two streams in the order they are shown: standard output, then standard error.
42    let mut parts: Vec<String> = Vec::new();
43    for bytes in [stdout, stderr] {
44        // Bytes that are not UTF-8 become the replacement character, so a word is never lost to an encoding.
45        let text: String = String::from_utf8_lossy(bytes).trim().to_owned();
46        if !text.is_empty() {
47            parts.push(text);
48        }
49    }
50    if parts.is_empty() {
51        // `None` is "this run said nothing", which makes no entry.
52        return None;
53    }
54    // `Some(...)` wraps the joined words: "there is something to record".
55    return Some(parts.join("\n"));
56}
57
58/// What:
59///  Record the words of a finished run, whichever program made it: the service's log when this is the service, the client
60///  entry otherwise.
61/// Why:
62///   The auto-push's fetch and fast-forward run in the service and in the wrapper's own stacked push, and the shadow's `init`
63///  in both, so the choice is made here once.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// recordWords(['fetch', '--no-tags', remote], `${stdout}${stderr}`)
68/// ```
69pub fn record_text<S: AsRef<str>>(arguments: &[S], text: &str) {
70    let trimmed: &str = text.trim();
71    if trimmed.is_empty() {
72        return;
73    }
74    // The command line as it reads in a terminal, so the entry names what ran.
75    let mut words: Vec<&str> = Vec::new();
76    for argument in arguments {
77        words.push(argument.as_ref());
78    }
79    let command: String = words.join(" ");
80    let message: String = mask_userinfo_text(format!("git {command} said: {trimmed}").as_str());
81    let fields: [(&str, String); 1] = [(COMMAND_FIELD, mask_userinfo_text(command.as_str()))];
82    if in_service() {
83        let _delivered = log_entry(Priority::Info, message.as_str(), &fields);
84        return;
85    }
86    deliver_from_wrapper(message.as_str(), &fields);
87}
88
89/// Send the entry of a wrapper a person ran to the journal; whether the socket took it is of no use to the command.
90#[cfg(target_os = "linux")]
91fn deliver_from_wrapper(message: &str, fields: &[(&str, String)]) {
92    let _sent: bool = record_entry(Priority::Info, message, fields);
93}
94
95/// Only Linux writes the wrapper's journal entries; elsewhere the words are not kept.
96// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
97#[cfg(not(target_os = "linux"))]
98fn deliver_from_wrapper(_message: &str, _fields: &[(&str, String)]) {}
99
100/// What:
101///  Record the words of a finished run from its captured output.
102/// Why:
103///   Most runs are captured as a `GitOutput`; this is `record_text` for them.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// recordWords(['reset', newCommit, '--', path], output)
108/// ```
109pub fn record_words<S: AsRef<str>>(arguments: &[S], output: &GitOutput) {
110    record_streams(arguments, output.stdout.as_slice(), output.stderr.as_slice());
111}
112
113/// What:
114///  Record the words of a finished run from its two streams.
115/// Why:
116///   The auto-push's queries come back as two byte streams, not as a `GitOutput`.
117///
118/// In TS you'd write (pseudocode):
119/// ```ts
120/// recordWords(['merge', '--ff-only', oid], { stdout, stderr })
121/// ```
122pub fn record_streams<S: AsRef<str>>(arguments: &[S], stdout: &[u8], stderr: &[u8]) {
123    if let Some(text) = words_of(stdout, stderr) {
124        record_text(arguments, text.as_str());
125    }
126}
127
128/// Controls for what is recorded.
129#[cfg(test)]
130#[path = "inner_git_words_tests.rs"]
131mod tests;