Skip to main content

git_policy_cli/
commit_revalidation.rs

1//! What:
2//!  Revalidation of a replayed tree, outside both landing locks:
3//!  a private index for the replayed tree, the shadow `HEAD` at the replay parent, the policies converged again over
4//!  that index, and `pre-commit` re-run whenever the tree differs from the one it last approved.
5//! Why:
6//!  A replay merges changes the policies and the hook never saw together, so the merged tree must pass the same gates
7//!  as the prepared one (`src/policy-engine/commit-revalidation.ts`).
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const revalidation = await revalidateReplay({ context, replay, mergedTree, onto, approvedTree });
12//! ```
13
14use super::candidate_object::{ObjectId, parse_object_id};
15use super::capture_invocation::InvocationCapture;
16use super::commit_convergence::{CommitConvergence, CommitConvergenceInput, converge_commit_policies};
17use super::commit_facts::PrivateIndexFacts;
18use super::commit_index::{list_changed_index_paths, write_private_tree};
19use super::commit_transaction::{CommitInvocation, TransactionStop, failed, settled_passes, staged_at_base};
20use super::commit_workspace::CommitWorkspace;
21use super::diagnostic_log::debug;
22use super::policy_checks::ShippedChecks;
23use super::policy_content::{ContentState, LifecycleContent};
24use super::policy_engine::{StageRequest, pass_exit_code};
25use super::policy_events::PolicyEvent;
26use super::policy_pass::{PassResult, run_policy_pass};
27use super::private_storage::create_private_directory;
28use super::replay_revalidation::{move_shadow_head, read_replayed_index, replayed_index_path};
29use super::repository_facts::RepositoryFacts;
30use super::transaction_git::GitContext;
31use super::transaction_journal::SymbolicHead;
32use std::path::{Path, PathBuf};
33
34/// Module tag for diagnostics.
35const TAG: &str = "revalidateReplay";
36
37/// What:
38///  The result of revalidating a replayed tree.
39/// Why:
40///   A blocked tree stops the commit with the policies' events; a revalidated one is rebuilt and landed.
41///
42/// In TS you'd write (pseudocode):
43/// ```ts
44/// type Revalidation = { kind: 'revalidated'; ... } | { kind: 'blocked'; result };
45/// ```
46pub enum Revalidation {
47    /// The replayed commit may land.
48    Revalidated {
49        /// The tree to commit, with the policy changes.
50        tree: String,
51        /// The private index holding that tree.
52        index: PathBuf,
53        /// The settled pass's events, with the fix summary the corrections earned.
54        events: Vec<PolicyEvent>,
55        /// The paths the replayed commit changes against its new parent.
56        committed_paths: Vec<Vec<u8>>,
57    },
58    /// A policy blocked the replayed commit.
59    Blocked {
60        /// The blocking pass's events.
61        events: Vec<PolicyEvent>,
62        /// The exit status of the block.
63        exit_code: i32,
64    },
65}
66
67/// What:
68///  What revalidation reads besides the policies.
69/// Why:
70///   The policies need the capture and the kept global options.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// type RevalidationContext = { gitPath; cwd; capture; workspace; args; policyOptions; readOnlySelection; repositoryRoot; globalArgs; noVerify };
75/// ```
76pub struct RevalidationContext<'a> {
77    /// The invocation's fixed facts.
78    pub invocation: &'a CommitInvocation<'a>,
79    /// The invocation capture.
80    pub capture: &'a InvocationCapture,
81    /// Whether interactive or include selection forbids automatic patches.
82    pub read_only_selection: bool,
83}
84
85/// What:
86///  Revalidate a replayed tree.
87/// Why:
88///   The policies converge over the replayed index with the replay parent as their baseline, and `pre-commit` sees
89///   the tree when the policies changed what it last approved.
90///
91/// In TS you'd write (pseudocode):
92/// ```ts
93/// await revalidateReplay({ context, replay, mergedTree, onto, approvedTree });
94/// ```
95pub fn revalidate_replay<F: RepositoryFacts>(
96    context: &RevalidationContext<'_>,
97    workspace: &CommitWorkspace,
98    request: &StageRequest,
99    checks: &mut ShippedChecks<PrivateIndexFacts<F>>,
100    replay: u64,
101    merged_tree: &str,
102    onto: &str,
103) -> Result<Revalidation, TransactionStop> {
104    let git: &GitContext = context.invocation.context;
105    let cwd: &Path = context.invocation.cwd;
106    let index: PathBuf = replayed_index_path(workspace.landing.directory.as_path(), replay);
107    let directory: PathBuf = workspace.landing.directory.join(format!("replay-{replay}"));
108    ensure_replay_directory(directory.as_path())
109        .map_err(|error| { return failed(format!("creating {} failed: {error}", directory.display())); })?;
110    read_replayed_index(git, cwd, workspace.object_directory.as_path(), index.as_path(), merged_tree)?;
111    move_shadow_head(
112        git,
113        workspace.landing.shadow.as_path(),
114        cwd,
115        context.capture.layout.target_ref.as_str(),
116        onto,
117        is_detached(&context.capture.layout.symbolic_head),
118    )?;
119    let candidate_paths: Vec<Vec<u8>> =
120        list_changed_index_paths(git, cwd, index.as_path(), onto, workspace.object_directory.as_path())?;
121    let base: Option<ObjectId> = parse_object_id(onto.as_bytes());
122    checks.facts.use_index(index.as_path(), workspace.object_directory.as_path());
123    checks.content = ContentState::new();
124    checks.candidates = LifecycleContent::Requested(staged_at_base(base, candidate_paths.as_slice()));
125    let (pass, changed_passes, changed_paths): (PassResult, u64, Vec<Vec<u8>>) = if context.read_only_selection {
126        checks.can_apply_patches = false;
127        (run_policy_pass(request, checks), 0, Vec::new())
128    } else {
129        let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
130            request,
131            checks,
132            stage: super::private_index_stage::PrivateStage {
133                context: git,
134                cwd,
135                index: index.as_path(),
136                object_directory: Some(workspace.object_directory.as_path()),
137            },
138        });
139        let passes: u64 = settled_passes(&outcome.convergence);
140        (outcome.pass, passes, outcome.changed_paths)
141    };
142    let code: i32 = pass_exit_code(pass.events.as_slice(), pass.end);
143    if code != 0 {
144        return Ok(Revalidation::Blocked {
145            events: pass.events,
146            exit_code: code,
147        });
148    }
149    let policy_tree: String =
150        write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?;
151    let tree: String = policy_tree;
152    debug(TAG, format!("revalidated replay {replay} onto {onto}: tree {tree}").as_str());
153    return Ok(Revalidation::Revalidated {
154        tree,
155        index,
156        events: super::commit_conclusion::events_with_changed_summary(pass.events, changed_passes, changed_paths.as_slice()),
157        committed_paths: candidate_paths,
158    });
159}
160
161/// What:
162///  Make the directory of one replay, accepting one that subsumption already made for its base index.
163/// Why:
164///   Every other failure leaves the replay without a place for its index.
165///
166/// In TS you'd write (pseudocode):
167/// ```ts
168/// await mkdir(directory, { mode: 0o700 }).catch((error) => { if (error.code !== 'EEXIST') throw error; });
169/// ```
170pub fn ensure_replay_directory(directory: &Path) -> std::io::Result<()> {
171    match create_private_directory(directory) {
172        Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => return Err(error),
173        Ok(()) | Err(_) => return Ok(()),
174    }
175}
176
177/// What:
178///  Whether the invocation started on a detached `HEAD`.
179/// Why:
180///   A detached replay moves the shadow `HEAD` itself; a branch replay moves the branch.
181///
182/// In TS you'd write (pseudocode):
183/// ```ts
184/// symbolicHead.kind === 'detached'
185/// ```
186pub fn is_detached(head: &SymbolicHead) -> bool {
187    return *head == SymbolicHead::Detached;
188}
189
190/// Controls for this module stay out of the release executable.
191#[cfg(test)]
192#[path = "commit_revalidation_tests.rs"]
193mod tests;