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