Skip to main content

git_policy_cli/
policy_patch.rs

1//! What:
2//!  One policy-proposed patch applied to the private commit index with `git apply --cached --3way`.
3//! Why:
4//!  Corrections land in the private index, never the real one.
5//!  A patch that no longer applies is a `patch-conflict` for the transaction, not a silent skip.
6//!  The incumbent applies the same command in `applyPrivatePatch`
7//!  (`src/policy-engine/commit-transaction-git.ts`).
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! await applyPrivatePatch({ workspace, gitPath, cwd, patch, candidateRevision, ordinal });
12//! ```
13
14use super::policy_patch_validate::{PatchCheck, validate_policy_patch};
15use super::private_storage::write_exclusive_synced;
16use super::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
17use std::path::{Path, PathBuf};
18
19/// What:
20///  A policy patch proposal for one ordinary text path.
21/// Why:
22///   The path and bytes are exactly what the policy proposed, and are validated before Git runs.
23///
24/// In TS you'd write (pseudocode):
25/// ```ts
26/// type PolicyPatch = { kind: 'git-unified'; targetId: string; path: string; bytes: Uint8Array };
27/// ```
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct PolicyPatch {
30    /// Exact candidate target ID the patch corrects.
31    pub target_id: String,
32    /// Repository path the patch names.
33    pub path: String,
34    /// Unified patch bytes.
35    pub bytes: Vec<u8>,
36}
37
38/// What:
39///  Why a policy patch was not applied.
40/// Why:
41///   The convergence loop maps `Rejected` to a grammar failure and `Conflict` to `patch-conflict`.
42///
43/// In TS you'd write (pseudocode):
44/// ```ts
45/// class PatchApplicationError extends Error {}
46/// ```
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub enum PatchError {
49    /// The patch broke the supported grammar; Git never saw it.
50    Rejected(String),
51    /// Git refused the patch against the private index.
52    Conflict(String),
53    /// The patch file or the Git process failed.
54    Failed(String),
55}
56
57/// What:
58///  The text of a patch failure.
59/// Why:
60///   Diagnostics print the reason unchanged.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// error.message;
65/// ```
66impl std::fmt::Display for PatchError {
67    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        let message: &String = match self {
69            Self::Rejected(message) | Self::Conflict(message) | Self::Failed(message) => message,
70        };
71        return write!(formatter, "{message}");
72    }
73}
74
75/// What:
76///  Inputs of one private patch application.
77/// Why:
78///   One object keeps the Git context, the private index and the patch separate at the call site.
79///
80/// In TS you'd write (pseudocode):
81/// ```ts
82/// type PrivatePatchInput = { context; cwd; index; objectDirectory; directory; patch; expectedRevision; ordinal };
83/// ```
84pub struct PrivatePatchInput<'a> {
85    /// Real Git executable and the invocation's overlay.
86    pub context: &'a GitContext,
87    /// Effective repository directory the patch is applied from.
88    pub cwd: &'a Path,
89    /// Private commit index receiving the correction.
90    pub index: &'a Path,
91    /// Object store receiving written blobs; the real store when absent.
92    pub object_directory: Option<&'a Path>,
93    /// Private transaction directory holding the patch file.
94    pub directory: &'a Path,
95    /// Patch to apply.
96    pub patch: &'a PolicyPatch,
97    /// Exact candidate blob revision the patch's index line names as its base.
98    pub expected_revision: &'a str,
99    /// Position of this patch in the pass, which names its patch file.
100    pub ordinal: u32,
101}
102
103/// What:
104///  Validate a policy patch, write it privately, and apply it to the private index.
105/// Why:
106///   The patch file is created exclusively with private permissions, so a re-run never overwrites an earlier one.
107///
108/// In TS you'd write (pseudocode):
109/// ```ts
110/// validatePolicyPatch({ patch, expectedRevision });
111/// await runTransactionGit({ args: ['apply', '--cached', '--3way', patchPath], indexPath });
112/// ```
113pub fn apply_private_patch(input: PrivatePatchInput<'_>) -> Result<(), PatchError> {
114    validate_policy_patch(PatchCheck {
115        patch: input.patch,
116        expected_revision: input.expected_revision,
117    })?;
118    let patch_path: PathBuf = input.directory.join(format!("patch-{}.diff", input.ordinal));
119    if let Err(error) = write_exclusive_synced(patch_path.as_path(), input.patch.bytes.as_slice()) {
120        return Err(PatchError::Failed(format!("writing {}: {error}", patch_path.display())));
121    }
122    let mut request: GitRequest = GitRequest::new(input.cwd, &["apply", "--cached", "--3way"]);
123    request.arguments.push(patch_path.as_os_str().to_owned());
124    request.without_prefix = true;
125    request.index = Some(input.index.to_path_buf());
126    request.object_directory = input.object_directory.map(Path::to_path_buf);
127    let output: GitOutput = match run_git(input.context, &request) {
128        Ok(found) => found,
129        Err(error) => return Err(PatchError::Failed(format!("starting git apply: {error}"))),
130    };
131    if output.succeeded() {
132        return Ok(());
133    }
134    return Err(PatchError::Conflict(output.error_text()));
135}
136
137#[cfg(test)]
138#[path = "policy_patch_tests.rs"]
139mod tests;