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
//! The merging queue — order as a query over git tags (bl-5c5f, design
//! docs/design/bl-24e7-speculative-merge-queue.md).
//!
//! A ball enters the merge queue by planting an annotated tag
//! `merging/<id>` on the tip of its `work/<id>` branch. That one ref is the
//! whole mechanism, three facts in one object, none stored anywhere else:
//!
//! - **membership** — the tag exists;
//! - **position** — the taggerdate orders the queue (refname breaks
//! same-second ties, deterministically);
//! - **the seal** — the tag's target names the exact commit sealed. A branch
//! that moves past its tag is UNSEALED by derivation: fixing a gate failure
//! means committing, committing moves the tip off the tag, and re-entering
//! means re-tagging — which is a new date, the bottom of the queue. Eviction
//! has no mechanism of its own.
//!
//! Reads never mutate ([`queue`] reports unsealed entries rather than reaping
//! them — sweeping is the speculator's job, bl-d0c2, backstopped by prime's
//! debris pass), and nothing here touches a remote: the queue is a
//! single-substrate coordination surface, like every other derived status in
//! balls (§3 — status is computed on read, absence is the record).
//!
//! Env-free like [`crate::speculate`]: the repo path and the optional
//! timestamp arrive as arguments; `bl-speculate` is the env-reading edge.
use std::io;
use std::path::Path;
use crate::safegit;
/// One queue entry, as derived from a `merging/<id>` tag.
#[derive(Debug, PartialEq, Eq)]
pub struct Entry {
/// The ball id — the tag name with `merging/` stripped.
pub id: String,
/// The sealed commit — the tag's peeled target.
pub tip: String,
/// TRUE iff `work/<id>` still points at the sealed commit. An unsealed
/// entry holds no position: its branch moved (or vanished — a landed
/// close deletes it), so it is out of the queue in every consumer's eyes.
pub sealed: bool,
}
/// Seal `work/<id>` into the queue: plant `merging/<id>` on its tip, replacing
/// any earlier tag — so re-enqueue IS requeue-at-bottom (a fresh taggerdate),
/// the invariant the design leans on. `date`, when given, becomes the
/// taggerdate (tests order deterministically with it; the edge passes None).
/// The tagger identity is fixed and mechanical — the tag is a seal marker,
/// not authorship; builder identity lives in the verdict records.
pub fn enqueue(repo: &Path, id: &str, date: Option<&str>) -> io::Result<String> {
let mut resolve = safegit::at(repo);
resolve.args(["rev-parse", "--verify"]).arg(format!("refs/heads/work/{id}"));
let tip = ok_stdout(&resolve.output()?, "git rev-parse work branch")?;
let mut tag = safegit::at(repo);
tag.args(["-c", "user.name=bl-speculate", "-c", "user.email=speculate@balls"]);
tag.args(["tag", "--force", "-a", "-m", "sealed for the merge queue (bl-24e7)"]);
tag.arg(format!("merging/{id}")).arg(&tip);
if let Some(d) = date {
tag.env("GIT_COMMITTER_DATE", d);
}
ok_stdout(&tag.output()?, "git tag")?;
Ok(tip)
}
/// Leave the queue: delete `merging/<id>`. Landing, abandoning, and eviction
/// cleanup are all this one act — the tag's absence is the record.
pub fn dequeue(repo: &Path, id: &str) -> io::Result<()> {
let mut cmd = safegit::at(repo);
cmd.args(["tag", "-d"]).arg(format!("merging/{id}"));
ok_stdout(&cmd.output()?, "git tag -d").map(|_| ())
}
/// The queue, in position order: every `merging/*` tag sorted by taggerdate
/// (refname on ties), each checked against its `work/<id>` tip for the seal.
/// Unsealed entries are still REPORTED — a consumer deciding what to sweep
/// needs to see them — but hold no position a speculator may build on.
pub fn queue(repo: &Path) -> io::Result<Vec<Entry>> {
let mut cmd = safegit::at(repo);
cmd.args(["for-each-ref", "--sort=refname", "--sort=taggerdate"]);
cmd.args(["--format=%(refname:short) %(*objectname)", "refs/tags/merging/"]);
let listing = ok_stdout(&cmd.output()?, "git for-each-ref")?;
listing.lines().map(|line| entry(repo, line)).collect()
}
/// Parse one for-each-ref line and derive the seal from the live branch tip.
fn entry(repo: &Path, line: &str) -> io::Result<Entry> {
let (name, tip) = line
.split_once(' ')
.ok_or_else(|| io::Error::other(format!("unparseable for-each-ref line: {line:?}")))?;
let id = name.strip_prefix("merging/").unwrap_or(name).to_string();
let mut resolve = safegit::at(repo);
resolve.args(["rev-parse", "--verify", "--quiet"]).arg(format!("refs/heads/work/{id}"));
let branch = resolve.output()?;
let sealed = branch.status.success()
&& String::from_utf8_lossy(&branch.stdout).trim() == tip;
Ok(Entry { id, tip: tip.to_string(), sealed })
}
/// Success → trimmed stdout; failure → an error carrying git's stderr voice.
fn ok_stdout(out: &std::process::Output, what: &str) -> io::Result<String> {
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
let voice = String::from_utf8_lossy(&out.stderr);
Err(io::Error::other(format!("{what}: {}", voice.trim())))
}
}
#[cfg(test)]
#[path = "speculate_queue_tests.rs"]
mod speculate_queue_tests;