Skip to main content

git_policy_cli/lock/
push_lock_clearing.rs

1//! What:
2//!  The wrapper's own fetch and push, run after the reference locks of a killed Git in their repository have been judged
3//!  and the provably stale ones removed.
4//! Why:
5//!  The commit service pushes unattended, and the reconcile fetch runs inside it, so a lock that a killed Git left blocks
6//!  them with nobody there to clear it. Clearing before every start also covers every repetition of a push (each repetition
7//!  starts through here), and nothing is waited for. A failed attempt carries the notice, so the person who reads the
8//!  failure (or the refusal Git prints and still exits 0 on) finds what was removed, what was left, and the command that clears a lock the wrapper will not remove.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! const outcome = clearStaleRefLocks(rootsOfCommand(command), judge);
13//! const attempt = await runBoundedPushOnce(command);
14//! if (attempt.exitCode !== 0) attempt.output += outcome.notice();
15//! ```
16
17use crate::auto_push_single_flight::{PushAttempt, SingleFlightError};
18use crate::bounded_push::{Patience, run_bounded_push_once};
19use crate::diagnostic_log::warn;
20use crate::git_directory_roots::roots_of_command;
21use crate::stale_ref_lock_judge::Judge;
22use crate::stale_ref_locks::clear_stale_ref_locks;
23use std::ffi::OsString;
24use std::process::Command;
25use std::time::Duration;
26
27/// Tag used for diagnostics from this module.
28const TAG: &str = "cli-git";
29
30/// What Git prints when another process holds the lock of a reference it updates, also from a push that exits 0.
31const REFERENCE_LOCK_REFUSAL: &str = "cannot lock ref";
32
33/// Run `command` as `run_bounded_push_once` does, after the stale reference locks of its repository are cleared by `judge`.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// async function runClearingLocks({ command, patience, grace, environment, judge }): Promise<PushAttempt>;
38/// ```
39pub fn run_clearing_locks(
40    command: Command,
41    patience: Patience,
42    grace: Duration,
43    environment: &[(OsString, OsString)],
44    judge: &Judge,
45) -> Result<PushAttempt, SingleFlightError> {
46    let outcome = clear_stale_ref_locks(roots_of_command(&command).as_slice(), judge);
47    let notice: String = outcome.notice();
48    let mut attempt: PushAttempt = run_bounded_push_once(command, patience, grace, environment)?;
49    // A push that delivered but could not update its tracking reference prints the refusal and exits 0, so the refusal is
50    //  looked for as well as the exit status (the service repeats such a push on the same words).
51    let failed: bool = attempt.exit_code != 0 || attempt.output.contains(REFERENCE_LOCK_REFUSAL);
52    if failed && !notice.is_empty() {
53        // The service shows this output, so the notice is not logged a second time.
54        attempt.output = if attempt.output.is_empty() { notice.trim_end().to_owned() } else { format!("{}\n{}", attempt.output, notice.trim_end()) };
55    } else if !notice.is_empty() {
56        warn(TAG, notice.trim_end());
57    }
58    return Ok(attempt);
59}
60
61/// Controls for the clearing before the wrapper's own fetch and push.
62#[cfg(test)]
63#[path = "push_lock_clearing_tests.rs"]
64mod tests;