Skip to main content

git_policy_cli/commit/
post_commit.rs

1//! What:
2//!  The post-commit policy gate over a landed commit's own changes.
3//! Why:
4//!  The incumbent (`src/policy-engine/post-commit-lifecycle.ts`) checks the committed bytes of
5//!  every path a landed commit changed, and only a gate that passes lets the commit reach auto-push. The push itself is
6//!  the service's work item, handed over by `commit_command.rs` after a passing gate.
7//!  A blocked gate keeps the commit local and names it in a `commit-landed` event; the command exits `2`.
8//!  The landed commit is an input, so the integrator can call this after any landing.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! const result = await runPostCommitLifecycle({ rawArgs, transformedArgs, gitPath, cwd, landedOid, ... });
13//! ```
14
15/// The landed-commit object name and the candidate layer's landed-delta source.
16use crate::candidate_object::parse_object_id;
17use crate::candidate_prediction::CandidateRequest;
18/// The policy configuration the gate runs with.
19use crate::config_schema::PolicyConfig;
20/// The reading of the invocation's environment.
21use crate::child_environment::environment_value;
22/// The failure code and the event kinds the gate emits.
23use crate::diagnostics::EngineFailureCode;
24/// The linter the Markdown policy finds on, and runs with, the invocation's environment.
25use crate::policy_checks::shipped_checks;
26use crate::policy_content::LifecycleContent;
27use crate::policy_engine::{StageEnd, StageRequest, pass_exit_code};
28use crate::policy_events::PolicyEvent;
29use crate::policy_pass::run_policy_pass;
30use crate::policy_trigger::Trigger;
31use crate::repository_facts::git_facts;
32/// The variable naming the forbidden-strings rules file.
33use crate::scanner_selection::RULES_VARIABLE;
34use crate::wrapper_controls::Controls;
35/// Owned OS strings for the replayed global options and the child environment.
36use std::ffi::OsString;
37/// `Path` is a borrowed filesystem path.
38use std::path::Path;
39
40/// Exit status the command returns when the gate blocks the backup, as the incumbent's `PolicyDecisionError(2)`.
41pub const POST_COMMIT_BLOCKED_EXIT_CODE: i32 = 2;
42
43/// What:
44///  The inputs of one post-commit gate.
45/// Why:
46///   The landed commit names the delta; the global prefix and the environment make every query read
47///  the repository the commit landed in.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// type PostCommitInput = { gitPath; cwd; landedOid; arguments; environment; policySeverities; policyOptions };
52/// ```
53#[derive(Clone, Debug)]
54pub struct PostCommitRequest<'request> {
55    /// Absolute real Git executable.
56    pub real_git: &'request Path,
57    /// The landed commit object name, as Git printed it.
58    pub landed_oid: &'request str,
59    /// The arguments the command forwarded to Git.
60    pub arguments: &'request [OsString],
61    /// The global options of the invocation, kept before every query (`-C`, `--git-dir`, `-c`).
62    pub global_prefix: &'request [OsString],
63    /// Variables added to every query.
64    pub overlay: &'request [(OsString, OsString)],
65    /// The invocation's whole environment:
66    ///  the Markdown linter is found on its `PATH` and runs with it,
67    ///  and the forbidden-strings rules variable is read from it.
68    pub environment: &'request [(OsString, OsString)],
69    /// The policy severities and options the gate runs with.
70    pub config: PolicyConfig,
71    /// Escapes and flags the invocation carries.
72    pub controls: Controls,
73}
74
75/// What:
76///  The settled gate:
77///  the landed commit, every event it produced, and whether the backup must not run.
78/// Why:
79///   `events` already carries the `commit-landed` event when `blocked` is true,
80///  so the caller renders one list.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// type PostCommitLifecycleResult = { oid: string; events: PolicyEvent[]; blocked: boolean };
85/// ```
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct PostCommitGate {
88    /// The landed commit object name, as given.
89    pub oid: String,
90    /// Every event the gate produced, in order.
91    pub events: Vec<PolicyEvent>,
92    /// Whether the backup must not run.
93    pub blocked: bool,
94}
95
96/// What:
97///  Run the post-commit policies over the landed commit's delta.
98/// Why:
99///   Only a gate with no blocking event, no failure and no pending correction lets the backup run;
100///  a warning-only result is allowed, as in the incumbent.
101///   A landed name that is not an object name is an engine failure, so the commit is kept and reported.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// async function runPostCommitLifecycle(input): Promise<PostCommitLifecycleResult>;
106/// ```
107pub fn run_post_commit_gate(request: &PostCommitRequest) -> PostCommitGate {
108    let oid = request.landed_oid.to_owned();
109    let (mut events, end): (Vec<PolicyEvent>, StageEnd) =
110        match parse_object_id(request.landed_oid.as_bytes()) {
111            Some(commit) => run_staged_gate(request, commit),
112            None => (
113                vec![PolicyEvent::EngineFailure {
114                    code: EngineFailureCode::ContentUnavailable,
115                    hint: crate::policy_events::EngineFailureHint::OfCode,
116                    message: format!("landed commit {} is not an object name", request.landed_oid),
117                    trigger: Some(Trigger::PostCommit),
118                    policy: None,
119                    path: None,
120                }],
121                StageEnd::Failed,
122            ),
123        };
124    let blocked = pass_exit_code(events.as_slice(), end) != 0;
125    if blocked {
126        events.push(PolicyEvent::CommitLanded { oid: oid.clone() });
127    }
128    return PostCommitGate {
129        oid,
130        events,
131        blocked,
132    };
133}
134
135/// Run the post-commit stage over the landed delta of a parsed commit name.
136///
137/// In TS you'd write (pseudocode):
138/// ```ts
139/// await dependencies.runPolicyEngine({ trigger: 'post-commit', gitFacts: createPostCommitGitFacts(...) })
140/// ```
141fn run_staged_gate(
142    request: &PostCommitRequest,
143    commit: crate::candidate_object::ObjectId,
144) -> (Vec<PolicyEvent>, StageEnd) {
145    let facts = git_facts(request.real_git, request.global_prefix, request.overlay);
146    let mut checks = shipped_checks(
147        facts,
148        request.arguments.to_vec(),
149        LifecycleContent::Requested(CandidateRequest::Landed(commit)),
150        Vec::new(),
151    );
152    // The scanner and Markdown options are the configuration's, as the pre-commit stage's checks hold them.
153    checks.scanner_settings.options = request.config.forbidden_strings.clone();
154    checks.markdown.options = request.config.markdown_autofix.clone();
155    // The forbidden-strings rules variable is read from this invocation's environment.
156    checks.scanner_settings.rules_variable = environment_value(request.environment, RULES_VARIABLE);
157    let stage: StageRequest = StageRequest {
158        trigger: Trigger::PostCommit,
159        config: request.config.clone(),
160        controls: request.controls.clone(),
161        selected: Vec::new(),
162    };
163    let pass = run_policy_pass(&stage, &mut checks);
164    return (pass.events, pass.end);
165}
166
167/// Post-commit gate behavior is controlled in the test module.
168#[cfg(test)]
169#[path = "post_commit_tests.rs"]
170mod tests;