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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! What:
//! The post-commit policy gate over a landed commit's own changes.
//! Why:
//! The incumbent (`src/policy-engine/post-commit-lifecycle.ts`) checks the committed bytes of
//! every path a landed commit changed, and only a gate that passes lets the commit reach auto-push. The push itself is
//! the service's work item, handed over by `commit_command.rs` after a passing gate.
//! A blocked gate keeps the commit local and names it in a `commit-landed` event; the command exits `2`.
//! The landed commit is an input, so the integrator can call this after any landing.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const result = await runPostCommitLifecycle({ rawArgs, transformedArgs, gitPath, cwd, landedOid, ... });
//! ```
/// The landed-commit object name and the candidate layer's landed-delta source.
use super::candidate_object::parse_object_id;
use super::candidate_prediction::CandidateRequest;
/// The policy configuration the gate runs with.
use super::config_schema::PolicyConfig;
/// The reading of the invocation's environment.
use super::child_environment::environment_value;
/// The failure code and the event kinds the gate emits.
use super::diagnostics::EngineFailureCode;
/// The linter the Markdown policy finds on, and runs with, the invocation's environment.
use super::policy_checks::shipped_checks;
use super::policy_content::LifecycleContent;
use super::policy_engine::{StageEnd, StageRequest, pass_exit_code};
use super::policy_events::PolicyEvent;
use super::policy_pass::run_policy_pass;
use super::policy_trigger::Trigger;
use super::repository_facts::git_facts;
/// The variable naming the forbidden-strings rules file.
use super::scanner_selection::RULES_VARIABLE;
use super::wrapper_controls::Controls;
/// Owned OS strings for the replayed global options and the child environment.
use std::ffi::OsString;
/// `Path` is a borrowed filesystem path.
use std::path::Path;
/// Exit status the command returns when the gate blocks the backup, as the incumbent's `PolicyDecisionError(2)`.
pub const POST_COMMIT_BLOCKED_EXIT_CODE: i32 = 2;
/// What:
/// The inputs of one post-commit gate.
/// Why:
/// The landed commit names the delta; the global prefix and the environment make every query read
/// the repository the commit landed in.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PostCommitInput = { gitPath; cwd; landedOid; arguments; environment; policySeverities; policyOptions };
/// ```
#[derive(Clone, Debug)]
pub struct PostCommitRequest<'request> {
/// Absolute real Git executable.
pub real_git: &'request Path,
/// The landed commit object name, as Git printed it.
pub landed_oid: &'request str,
/// The arguments the command forwarded to Git.
pub arguments: &'request [OsString],
/// The global options of the invocation, kept before every query (`-C`, `--git-dir`, `-c`).
pub global_prefix: &'request [OsString],
/// Variables added to every query.
pub overlay: &'request [(OsString, OsString)],
/// The invocation's whole environment:
/// the Markdown linter is found on its `PATH` and runs with it,
/// and the forbidden-strings rules variable is read from it.
pub environment: &'request [(OsString, OsString)],
/// The policy severities and options the gate runs with.
pub config: PolicyConfig,
/// Escapes and flags the invocation carries.
pub controls: Controls,
}
/// What:
/// The settled gate:
/// the landed commit, every event it produced, and whether the backup must not run.
/// Why:
/// `events` already carries the `commit-landed` event when `blocked` is true,
/// so the caller renders one list.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PostCommitLifecycleResult = { oid: string; events: PolicyEvent[]; blocked: boolean };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PostCommitGate {
/// The landed commit object name, as given.
pub oid: String,
/// Every event the gate produced, in order.
pub events: Vec<PolicyEvent>,
/// Whether the backup must not run.
pub blocked: bool,
}
/// What:
/// Run the post-commit policies over the landed commit's delta.
/// Why:
/// Only a gate with no blocking event, no failure and no pending correction lets the backup run;
/// a warning-only result is allowed, as in the incumbent.
/// A landed name that is not an object name is an engine failure, so the commit is kept and reported.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function runPostCommitLifecycle(input): Promise<PostCommitLifecycleResult>;
/// ```
pub fn run_post_commit_gate(request: &PostCommitRequest) -> PostCommitGate {
let oid = request.landed_oid.to_owned();
let (mut events, end): (Vec<PolicyEvent>, StageEnd) =
match parse_object_id(request.landed_oid.as_bytes()) {
Some(commit) => run_staged_gate(request, commit),
None => (
vec![PolicyEvent::EngineFailure {
code: EngineFailureCode::ContentUnavailable,
message: format!("landed commit {} is not an object name", request.landed_oid),
trigger: Some(Trigger::PostCommit),
policy: None,
path: None,
}],
StageEnd::Failed,
),
};
let blocked = pass_exit_code(events.as_slice(), end) != 0;
if blocked {
events.push(PolicyEvent::CommitLanded { oid: oid.clone() });
}
return PostCommitGate {
oid,
events,
blocked,
};
}
/// Run the post-commit stage over the landed delta of a parsed commit name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await dependencies.runPolicyEngine({ trigger: 'post-commit', gitFacts: createPostCommitGitFacts(...) })
/// ```
fn run_staged_gate(
request: &PostCommitRequest,
commit: super::candidate_object::ObjectId,
) -> (Vec<PolicyEvent>, StageEnd) {
let facts = git_facts(request.real_git, request.global_prefix, request.overlay);
let mut checks = shipped_checks(
facts,
request.arguments.to_vec(),
LifecycleContent::Requested(CandidateRequest::Landed(commit)),
Vec::new(),
);
// The scanner and Markdown options are the configuration's, as the pre-commit stage's checks hold them.
checks.scanner_settings.options = request.config.forbidden_strings.clone();
checks.markdown.options = request.config.markdown_autofix.clone();
// The forbidden-strings rules variable is read from this invocation's environment.
checks.scanner_settings.rules_variable = environment_value(request.environment, RULES_VARIABLE);
let stage: StageRequest = StageRequest {
trigger: Trigger::PostCommit,
config: request.config.clone(),
controls: request.controls.clone(),
selected: Vec::new(),
};
let pass = run_policy_pass(&stage, &mut checks);
return (pass.events, pass.end);
}
/// Post-commit gate behavior is controlled in the test module.
#[cfg(test)]
#[path = "post_commit_tests.rs"]
mod tests;