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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Atomic, per-issue exclusion between Pickup runs on different machines,
//! and between a Pickup run and the Architect run or Security run that marks
//! its own issue ready and dispatches it. Label edits and ordinary Issue
//! branch pushes cannot arbitrate that race. A separate origin ref is
//! created only if absent, then held through dispatch.
use anyhow::{Context, Result};
use crate::git::Git;
use crate::issue::IssueUrl;
use crate::progress;
pub struct Reservation {
launch: Git,
reference: String,
token: String,
}
impl Reservation {
/// Create a unique commit and atomically reserve `issue` with an empty
/// lease. Existing reservations are never adopted, expired or overwritten.
/// Errors with an uncertain remote outcome retain the ref for recovery.
pub fn take(launch: &Git, issue: &IssueUrl) -> Result<Option<Self>> {
Self::take_ref(launch, reference(issue.number), &issue.url)
}
/// Reserve `issue`, which this run published and is about to mark
/// ready and dispatch itself, as a Pickup run reserves its Ready issue,
/// so that an overlapping Pickup run passes it over. Fails if another
/// run already holds it.
pub fn of_published(launch: &Git, issue: &IssueUrl) -> Result<Self> {
Self::take(launch, issue)?.with_context(|| {
format!(
"#{} is already reserved on origin, at {}",
issue.number,
reference(issue.number)
)
})
}
/// Serialize CI-red Ticket publication across all Base branches and hosts.
pub fn ci_red(launch: &Git) -> Result<Option<Self>> {
Self::take_ref(
launch,
"refs/heads/thirdshift/pickup/ci-red".into(),
"CI-red Ticket filing",
)
}
fn take_ref(launch: &Git, reference: String, subject: &str) -> Result<Option<Self>> {
let nonce = tempfile::Builder::new().rand_bytes(32).tempfile()?;
// A reservation carries no source tree or parent: naming a Base branch
// permits a Launch directory with unrelated, unpublished local work.
let tree = launch.run(&["mktree"])?;
let message = format!(
"thirdshift Pickup reservation for {}\n\nToken: {}\nHost: {}\nLaunch: {}\nProcess: {}\nStarted: {}",
subject,
nonce.path().display(),
crate::host::name().unwrap_or_else(|| "unknown".into()),
launch.dir().display(),
std::process::id(),
chrono::Utc::now(),
);
let token = launch.run(&[
"-c",
"commit.gpgSign=false",
"commit-tree",
&tree,
"-m",
&message,
])?;
let pushed = launch.run(&[
"push",
"--no-verify",
&format!("--force-with-lease={reference}:"),
"origin",
&format!("{token}:{reference}"),
]);
if let Err(error) = pushed {
let remote = launch.run(&["ls-remote", "--refs", "origin", &reference])
.with_context(|| format!(
"{error:#}; could not verify reservation {reference}; check origin by hand (our token {token})"
))?;
if let Some((other, _)) = remote.split_once('\t')
&& other != token
{
return Ok(None);
}
return Err(error).with_context(|| format!(
"could not reserve {reference}; check origin by hand before retrying (our token {token})"
));
}
Ok(Some(Self {
launch: launch.completion(),
reference,
token,
}))
}
}
/// The ref that reserves issue `number` on origin.
pub fn reference(number: u64) -> String {
format!("refs/heads/thirdshift/pickup/{number}")
}
impl Drop for Reservation {
fn drop(&mut self) {
// Finish after interruption, and never delete another owner's token.
if let Err(error) = self.launch.run(&[
"push",
"--no-verify",
&format!("--force-with-lease={}:{}", self.reference, self.token),
"origin",
&format!(":{}", self.reference),
]) {
progress::warn(
&error,
format_args!(
"could not release Pickup reservation {}; check origin by hand (our token {})",
self.reference, self.token
),
);
}
}
}