Skip to main content

git_policy_cli/
landing_support.rs

1//! What:
2//!  Target comparison, the nonce-bearing reflog message and the compare-and-swap a landing uses
3//!  to advance its target.
4//! Why:
5//!  The reflog subject carries the transaction nonce, so recovery can tell whether the target
6//!  advanced to this landing even after a crash (`src/policy-engine/commit-landing-support.ts`).
7//!  The swap runs in the owning worktree's context, which also writes the real `HEAD` reflog.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! await compareAndSwapTarget({ gitPath, cwd, capture, transactionId, newOid, expectedOld });
12//! ```
13
14/// Journal target value.
15use super::transaction_journal::Base;
16/// Strict-enough JavaScript whitespace trimming for Git's subject text.
17use super::js_text::trim_javascript;
18/// Running real Git through the transaction runner.
19use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git};
20/// `Path` is a borrowed filesystem path.
21use std::path::Path;
22
23/// What:
24///  Reports whether two target values are equal.
25/// Why:
26///   An unborn target equals only another unborn target.
27///
28/// In TS you'd write (pseudocode):
29/// ```ts
30/// sameTarget({ left, right });
31/// ```
32pub fn same_target(left: &Base, right: &Base) -> bool {
33    return left == right;
34}
35
36/// What:
37///  The reflog subject prefix carrying the transaction nonce.
38/// Why:
39///   Recovery searches the reflog for this prefix to find an advanced target.
40///
41/// In TS you'd write (pseudocode):
42/// ```ts
43/// landingReflogPrefix(nonce); // 'commit (cli-git <id>):'
44/// ```
45pub fn landing_reflog_prefix(nonce: &str) -> String {
46    return format!("commit (cli-git {nonce}):");
47}
48
49/// What:
50///  Builds the nonce-bearing reflog message of one landed commit.
51/// Why:
52///   The message is the prefix followed by the commit's subject, as Git's own commit reflog writes it.
53///
54/// In TS you'd write (pseudocode):
55/// ```ts
56/// await landingReflogMessage({ gitPath, cwd, nonce, oid });
57/// ```
58pub fn landing_reflog_message(
59    context: &GitContext,
60    cwd: &Path,
61    nonce: &str,
62    oid: &str,
63) -> Result<String, GitFailure> {
64    let request = GitRequest::new(cwd, &["show", "--no-patch", "--format=%s", oid]);
65    let output: GitOutput = run_git(context, &request)
66        .map_err(|error| { return GitFailure(format!("git show {oid} could not start: {error}")); })?;
67    if !output.succeeded() {
68        return Err(GitFailure(format!(
69            "git show {oid} failed: {}",
70            String::from_utf8_lossy(&output.stderr).trim_end()
71        )));
72    }
73    let subject: String = String::from_utf8(output.stdout)
74        .map_err(|error| { return GitFailure(format!("git show {oid} printed non-UTF-8 text: {error}")); })?;
75    return Ok(format!("{} {}", landing_reflog_prefix(nonce), trim_javascript(&subject)));
76}
77
78/// What:
79///  One compare-and-swap of a landing's target.
80/// Why:
81///   Grouping the facts keeps the call site readable and names what the swap depends on.
82///
83/// In TS you'd write (pseudocode):
84/// ```ts
85/// { targetRef, symbolicHead, transactionId, newOid, expectedOld, emptyTreeOid }
86/// ```
87pub struct CompareAndSwap<'swap> {
88    /// Ref to move: the branch, or `HEAD` when detached.
89    pub target_ref: &'swap str,
90    /// Whether `HEAD` is detached, so `--no-deref` moves `HEAD` itself.
91    pub detached: bool,
92    /// Transaction ID written into the reflog message.
93    pub nonce: &'swap str,
94    /// Commit to land.
95    pub new_oid: &'swap str,
96    /// Value the target must still hold.
97    pub expected_old: &'swap Base,
98    /// Hash length in hex digits, which sets the width of the all-zero unborn value.
99    pub hash_length: usize,
100}
101
102/// What:
103///  Advances the target from the expected value to the new commit, in the owning worktree.
104/// Why:
105///   A nonzero exit is a lost race, not an error: the caller reads the winning value next.
106///
107/// In TS you'd write (pseudocode):
108/// ```ts
109/// await runTransactionGit({ gitPath, cwd, args: ['update-ref', '-m', message, ...], allowFailure: true });
110/// ```
111pub fn compare_and_swap_target(
112    context: &GitContext,
113    cwd: &Path,
114    swap: &CompareAndSwap<'_>,
115) -> Result<GitOutput, GitFailure> {
116    let message: String = landing_reflog_message(context, cwd, swap.nonce, swap.new_oid)?;
117    let expected: String = match swap.expected_old {
118        Base::Commit(oid) => oid.clone(),
119        Base::Unborn => "0".repeat(swap.hash_length),
120    };
121    let mut arguments: Vec<String> = vec!["update-ref".to_string(), "-m".to_string(), message];
122    if swap.detached {
123        arguments.push("--no-deref".to_string());
124    }
125    arguments.push(swap.target_ref.to_string());
126    arguments.push(swap.new_oid.to_string());
127    arguments.push(expected);
128    let request = GitRequest::new(cwd, &arguments);
129    return run_git(context, &request)
130        .map_err(|error| { return GitFailure(format!("git update-ref could not start: {error}")); });
131}
132
133/// Landing support controls stay out of the production code path.
134#[cfg(test)]
135#[path = "landing_support_tests.rs"]
136mod tests;