Skip to main content

git_policy_cli/
push_trigger_log.rs

1//! What:
2//!  The record of every decision of the push trigger after a command that adds commits:
3//! handed over,
4//! or declined with the step that declined and why.
5//! Why:
6//!  A merge that was never pushed left nothing to look at,
7//! because the trigger returned silently at eight places
8//! and the reason sat only in the code.
9//! The record is one JSON line appended to a size-capped file in the common Git directory beside the commit timing records,
10//! and a debug line under `MONOCHROMATIC_VERBOSE=true`;
11//! the person's terminal shows neither.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! await appendFile(join(commonDir, 'cli-git-push-trigger.jsonl'), JSON.stringify({ command, outcome, reason }) + '\n');
16//! ```
17
18use super::diagnostic_log::debug;
19use super::discard_audit::append_line;
20use super::iso_time::now_iso;
21use super::json_record::ObjectWriter;
22use super::service_protocol::RELEASE;
23use std::path::Path;
24
25/// The file inside the common Git directory that collects the trigger records of every worktree of the repository.
26pub const TRIGGER_FILE: &str = "cli-git-push-trigger.jsonl";
27
28/// The version of the record's fields;
29/// it changes only when a field's meaning changes,
30/// never when a field is added.
31pub const TRIGGER_SCHEMA_VERSION: i64 = 1;
32
33/// Module tag for diagnostics.
34const TAG: &str = "pushTrigger";
35
36/// How the trigger ended for one command.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum Outcome {
39    /// The push was handed to the service.
40    HandedOver,
41    /// The trigger did nothing,
42    /// for the reason the record names.
43    Declined,
44}
45
46impl Outcome {
47    /// The word in the record.
48    pub fn word(self) -> &'static str {
49        return match self {
50            Self::HandedOver => "handed-over",
51            Self::Declined => "declined",
52        };
53    }
54}
55
56/// What:
57///  The line for one decision.
58/// Why:
59///   The fields say which command,
60/// which release,
61/// which branch and tips,
62/// and the one reason,
63/// so a missing push is diagnosed from the file alone.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// JSON.stringify({ schemaVersion, time, pid, release, command, outcome, reason, detail, branch }) + '\n'
68/// ```
69pub fn trigger_line(command: &str, outcome: Outcome, reason: &str, detail: &str, branch: Option<&str>) -> String {
70    let mut record: ObjectWriter = ObjectWriter::new();
71    record
72        .integer("schemaVersion", TRIGGER_SCHEMA_VERSION)
73        .string("time", now_iso().as_str())
74        .integer("pid", i64::from(std::process::id()))
75        .string("release", RELEASE)
76        .string("command", command)
77        .string("outcome", outcome.word())
78        .string("reason", reason)
79        .string("detail", detail);
80    if let Some(name) = branch {
81        record.string("branch", name);
82    }
83    return format!("{}\n", record.finish());
84}
85
86/// What:
87///  Records one decision:
88/// a debug line always (when verbose),
89/// and one appended line when the common Git directory is known.
90/// Why:
91///   The command's result never depends on the record:
92/// a file that cannot be written is only a debug line.
93///
94/// In TS you'd write (pseudocode):
95/// ```ts
96/// recordDecision(commonDir, 'merge', 'declined', 'upstream-equals-tip', detail, branch)
97/// ```
98pub fn record_decision(common_dir: Option<&Path>, command: &str, outcome: Outcome, reason: &str, detail: &str, branch: Option<&str>) {
99    debug(TAG, &format!("{command}: {} ({reason}) {detail}", outcome.word()));
100    let Some(directory) = common_dir else { return };
101    let target = directory.join(TRIGGER_FILE);
102    let line: String = trigger_line(command, outcome, reason, detail, branch);
103    if let Err(error) = append_line(target.as_path(), line.as_bytes()) {
104        debug(TAG, &format!("the trigger record could not be written to {}: {error}", target.display()));
105    }
106}