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;