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
//! Candidate arithmetic for the speculator (bl-d0c2, design
//! docs/design/bl-24e7-speculative-merge-queue.md) — the git acts that make a
//! queue prefix buildable without creating anything that can leak.
//!
//! A candidate is computed, never stored: `git merge-tree --write-tree` yields
//! the merged TREE with no branch, no index and no worktree involved, and
//! [`commit_tree`] wraps it in an UNREFERENCED merge commit (parents: the
//! previous candidate and the sealed tip) purely so the next `merge-tree` in
//! the chain has a committish with the right ancestry. Nothing points at these
//! objects — they are `git gc` food by construction, which is the design's
//! cleanup section made structural.
//!
//! The one materialization is [`build_dir`]/[`remove_build_dir`]: a DETACHED
//! worktree that exists only for the duration of one gate run. The invariant a
//! consumer may assert between rounds is that `git worktree list` shows only
//! real claims.
use std::io;
use std::path::Path;
use crate::safegit;
/// The outcome of merging one sealed tip onto the chain so far.
#[derive(Debug, PartialEq, Eq)]
pub enum Merge {
/// A clean merge — the resulting tree OID.
Tree(String),
/// The merge conflicts. The candidate (and every deeper one) is
/// unbuildable; the ball falls back to fold-at-close, where the branch
/// owner — the only one whose judgment belongs in the resolution —
/// resolves it (settles the design's open question 2).
Conflict,
}
/// `git merge-tree --write-tree base tip` — the real merge machinery (rename
/// detection included), no side effects at all. Exit 1 is ambiguous: a
/// CONFLICTED merge still writes the tree OID as its first stdout line, while
/// "not something we can merge" writes a message — only the former is a
/// conflict; everything else is an error with git's voice.
pub fn merge_tree(repo: &Path, base: &str, tip: &str) -> io::Result<Merge> {
let mut cmd = safegit::at(repo);
cmd.args(["merge-tree", "--write-tree", base, tip]);
let out = cmd.output()?;
let stdout = String::from_utf8_lossy(&out.stdout);
let first = stdout.lines().next().unwrap_or("").trim().to_string();
let is_oid = first.len() == 40 && first.bytes().all(|b| b.is_ascii_hexdigit());
match out.status.code() {
Some(0) => Ok(Merge::Tree(first)),
Some(1) if is_oid => Ok(Merge::Conflict),
_ => Err(io::Error::other(format!(
"git merge-tree: {} {}",
stdout.trim(),
String::from_utf8_lossy(&out.stderr).trim()
))),
}
}
/// Wrap a candidate tree in an unreferenced merge commit so the chain's next
/// merge sees real ancestry. Identity is pinned and mechanical, like the
/// queue's tags — these commits carry no authorship, only structure.
pub fn commit_tree(repo: &Path, tree: &str, parents: &[&str]) -> io::Result<String> {
let mut cmd = safegit::at(repo);
cmd.args(["-c", "user.name=bl-speculate", "-c", "user.email=speculate@balls"]);
cmd.args(["commit-tree", tree, "-m", "speculative candidate (bl-24e7)"]);
for p in parents {
cmd.args(["-p", p]);
}
let out = cmd.output()?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(io::Error::other(format!(
"git commit-tree: {}",
String::from_utf8_lossy(&out.stderr).trim()
)))
}
}
/// Materialize a candidate commit as a detached worktree at `dir` for one
/// gate run. The caller MUST pair this with [`remove_build_dir`].
pub fn build_dir(repo: &Path, commit: &str, dir: &Path) -> io::Result<()> {
let mut cmd = safegit::at(repo);
cmd.args(["worktree", "add", "--detach"]).arg(dir).arg(commit);
let out = cmd.output()?;
if out.status.success() {
Ok(())
} else {
Err(io::Error::other(format!(
"git worktree add: {}",
String::from_utf8_lossy(&out.stderr).trim()
)))
}
}
/// Tear the build worktree down, force included — a gate leaves debris
/// (target/, caches) and the worktree must go regardless.
pub fn remove_build_dir(repo: &Path, dir: &Path) -> io::Result<()> {
let mut cmd = safegit::at(repo);
cmd.args(["worktree", "remove", "--force"]).arg(dir);
let out = cmd.output()?;
if out.status.success() {
Ok(())
} else {
Err(io::Error::other(format!(
"git worktree remove: {}",
String::from_utf8_lossy(&out.stderr).trim()
)))
}
}
#[cfg(test)]
#[path = "speculate_candidate_tests.rs"]
mod speculate_candidate_tests;