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