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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! §11 delivery plugin — the real project-repo git seam ([`Project`]).
//!
//! [`Project`] is the production [`crate::delivery::Repo`]: it shells out to git
//! against the PROJECT repo at the invocation path, owning the `work/<id>` code
//! worktree and the direct (local-squash) delivery onto the integration branch.
//! Every act is idempotent — it recomputes from `(path, branch)` and checks the
//! filesystem/refs first, so a re-run is a no-op rather than an error (§11). The
//! squash itself is plumbing (`commit-tree` + `update-ref`) so it never disturbs
//! a checked-out integration working tree — the work happens in the code
//! worktree, whose tree delivery runs the repo's own pre-commit gate against
//! before anything lands (bl-ee85). That tree must ALREADY carry the target
//! (bl-a1a4): delivery validates and advances, it never reconciles. The squash
//! is the BINDING
//! commit point (§14): an abort never resets it — a retried close detects it
//! by its delivery tag and converges ([`crate::delivery_standing`]).
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// The production [`crate::delivery::Repo`]: git against one project-repo root.
#[derive(Debug)]
pub struct Project {
pub(crate) root: PathBuf,
}
impl Project {
/// Operate against the project repo rooted at `root` (the §7 invocation path).
#[must_use]
pub fn at(root: &Path) -> Self {
Self { root: root.to_path_buf() }
}
/// `git -C <cwd> <args>` as an unspawned [`Command`] — the one place the
/// delivery environment, binary name and cwd are constructed. Ambient Git
/// controls cannot cross [`crate::safegit::delivery_at`]; repo-local config
/// and author/committer identity still do. Callers set only their own stdio
/// and exit policy ([`Self::run`] captures, [`Self::ok`] discards, `standing`
/// pipes for stdout).
pub(crate) fn git(cwd: &Path, args: &[&str]) -> Command {
let mut cmd = crate::safegit::delivery_at(cwd);
cmd.args(args);
cmd
}
/// Run `git -C <cwd> <args>`, returning stdout; a non-zero exit becomes an
/// [`io::Error`] carrying git's stderr (the one failure funnel).
pub(crate) fn run(cwd: &Path, args: &[&str]) -> io::Result<String> {
Self::feed(cwd, args, None)
}
/// [`Self::run`] with `stdin` piped to the child — the ARGV-FREE message
/// channel (bl-a500). A commit message is unbounded author text: the
/// composed delivery message concatenates every `work/<id>` commit body, and
/// spelling it as a `-m` argument dies at the kernel's per-argument ceiling
/// (`MAX_ARG_STRLEN`, 128 KiB on Linux) with a bare `Argument list too long
/// (os error 7)` — observed at 142 KB on a real close, with the delivery
/// already gated and nothing landed. Every git call that carries a MESSAGE
/// therefore takes `-F -` and passes it here; argv carries only refs, paths
/// and the single-line reflog label. Safe against the 64 KiB pipe buffer
/// because the commands fed this way (`commit`, `commit-tree`) drain stdin
/// before writing their own short output.
pub(crate) fn feed(cwd: &Path, args: &[&str], stdin: Option<&str>) -> io::Result<String> {
let mut cmd = Self::git(cwd, args);
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() });
let mut child = cmd.spawn()?;
// TAKEN, not borrowed: the pipe must CLOSE at the end of this block, or
// the child waits forever for an EOF that never comes.
if let (Some(text), Some(mut pipe)) = (stdin, child.stdin.take()) {
pipe.write_all(text.as_bytes())?;
}
let out = child.wait_with_output()?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
} else {
Err(io::Error::other(format!(
"git {}: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
)))
}
}
/// Run `git -C <cwd> <args>` purely for its exit code — a predicate (does a
/// ref exist? do two trees differ?). `Ok(true)` on exit 0, `Ok(false)` on
/// any non-zero; only a spawn failure is an error.
pub(crate) fn ok(cwd: &Path, args: &[&str]) -> io::Result<bool> {
Ok(Self::git(cwd, args).stdout(Stdio::null()).stderr(Stdio::null()).status()?.success())
}
/// Does local branch `branch` exist?
pub(crate) fn branch_exists(&self, branch: &str) -> io::Result<bool> {
Self::ok(&self.root, &["rev-parse", "--verify", "--quiet", &format!("refs/heads/{branch}")])
}
/// EVERY root-commit reachable from HEAD, newest-first:
/// `git rev-list --max-parents=0 HEAD` prints one root per line, and a
/// multi-root repo (an unrelated history merged in — vendoring) has more than
/// one. These are the project identities this checkout answers to; the claim
/// guard (bl-0161) admits a ball whose recorded root is ANY of them, so
/// merging an unrelated history never flips identity and strands earlier
/// balls. Empty when `root` is not a git repo, carries no commit yet, or any
/// git call fails — fail-open, the guard withholds nothing it cannot prove.
/// The set-returning read is the reusable primitive: root-aware `list`
/// (bl-5965) scopes on the same call.
#[must_use]
pub fn root_commits(&self) -> Vec<String> {
Self::run(&self.root, &["rev-list", "--max-parents=0", "HEAD"])
.map(|out| out.lines().map(str::to_string).collect())
.unwrap_or_default()
}
/// This project's canonical, REMOTE-FREE root-commit stamp: the first
/// (newest) of [`Self::root_commits`]. Intrinsic to history and identical
/// across clones/hosts, it is what `create` records on a ball (bl-1ce7).
/// `None` off a non-repo / commitless checkout — a ball created there records
/// nothing and is unconstrained (back-compat). The stamp stays singular; the
/// SET is the read side (the guard, and future list scope).
#[must_use]
pub fn root_commit(&self) -> Option<String> {
self.root_commits().into_iter().next()
}
/// Capture any pending worktree work onto `branch` as a commit (squashed
/// away later), so an uncommitted change is never lost at delivery.
/// `--no-verify`: the delivery gate ([`Self::gate`]) runs ONCE, later, on
/// the final delivered tree — not here, where it would fire only when the
/// worktree happened to be dirty (the bl-ee85 asymmetry). The caller has
/// already run the strict-fold guard
/// ([`crate::delivery_fold::ensure_no_merge_in_progress`]) — over a
/// half-merge, this `add -A` + commit would CONCLUDE the merge with a
/// silent work-side resolution (bl-a04a). Capture runs BEFORE the ancestry
/// precondition ([`crate::delivery_fold::ensure_target_incorporated`]) and
/// survives its refusal on purpose: the remedy the refusal prescribes is
/// `git merge <target>` in this very worktree, and git refuses that over
/// local modifications. Committing the closer's own pending work onto their
/// own branch is not reconciliation — it moves no target and merges nothing.
///
/// `subject` is the delivery message's SUBJECT LINE, never the whole
/// message (bl-a500). The capture commit is bookkeeping that the squash
/// erases, but it survives on `work/<id>` across an ABORTED close — and the
/// next attempt reads it back through [`crate::delivery::Repo::work_messages`]. Labelling it
/// with the composed message therefore folded that message into the next
/// composition, which the next capture folded in again: the delivery
/// message DOUBLED per retry, reaching 142 KB after four (the reported
/// blow-up). One line is a label; it cannot compound.
pub(crate) fn capture(path: &Path, subject: &str) -> io::Result<()> {
Self::run(path, &["add", "-A"])?;
if Self::ok(path, &["diff", "--cached", "--quiet"])? {
return Ok(()); // nothing staged — the worktree is clean
}
Self::feed(path, &["commit", "--no-verify", "-F", "-"], Some(subject))?;
Ok(())
}
/// The delivery gate (bl-ee85): run the project repo's own `pre-commit`
/// hook — resolved exactly as git resolves it (`--git-path` honors
/// `core.hooksPath`), skipped exactly as git skips it (absent or
/// non-executable) — against the worktree holding the to-be-delivered tree.
/// The squash is plumbing and would silently bypass the hook every porcelain
/// commit runs; this restores that gate at the one moment it is
/// representative: after capture, on a source tree the ancestry precondition
/// has already proved carries the target (bl-a1a4). A failure aborts the
/// close BEFORE the seal, so the task stays claimed and the worktree stays
/// up for the fix. The hook receives the same rebuilt environment as the
/// Git children, so its own nested Git cannot recover caller-supplied
/// config/redirect controls. The hook's stdout joins stderr — diagnostics,
/// never the product channel (§6).
pub(crate) fn gate(path: &Path) -> io::Result<()> {
let printed = Self::run(path, &["rev-parse", "--git-path", "hooks/pre-commit"])?;
let hook = path.join(printed.trim());
let Ok(meta) = fs::metadata(&hook) else {
return Ok(()); // no hook → an ungated project delivers as before
};
if !is_executable(&meta) {
return Ok(()); // git's rule: a non-executable hook is ignored
}
let mut cmd = Command::new(&hook);
crate::safegit::delivery_env(&mut cmd);
let status = cmd
.current_dir(path)
.stdout(Stdio::from(io::stderr()))
.status()?;
if status.success() {
Ok(())
} else {
Err(io::Error::other(format!("delivery gate {} failed: {status}", hook.display())))
}
}
/// The `marker`-tagged commits reachable from `revs` (a ref or a range),
/// NEWEST FIRST — the one tag-scan the retry standing ([`Project::standing`])
/// reads through, and the derived "where was `<id>` delivered?" query (§11):
/// no stored field. Recency order resolves the id-reuse ambiguity bl-d7a5
/// deferred — a reused id only begins after the prior incarnation CLOSED, so
/// deliveries are monotonic with incarnations and the k-th-most-recent
/// incarnation maps to the k-th element, the same live-first-else-most-recent
/// walk §9 applies to the ball file. The `--grep` is `--fixed-strings` so the
/// `[`/`]` match literally, not as a regex. Empty when `marker` is absent.
/// (`git log`'s default order IS recency, so this is "do not reverse it".)
pub(crate) fn marked(&self, revs: &str, marker: &str) -> io::Result<Vec<String>> {
let grep = format!("--grep={marker}");
let out = Self::run(&self.root, &["log", "--format=%H", "--fixed-strings", &grep, revs])?;
Ok(out.lines().map(str::to_string).collect())
}
}
/// Whether `meta` describes an executable regular file. On Unix this is the
/// owner-or-group-or-other `+x` bit — git's own rule for hook execution. On
/// Windows there is no executable bit and git-for-Windows resolves hook
/// runnability at launch time (extension / shebang), so we report every file
/// as executable here and let [`Command::new`] surface a real failure if the
/// hook can't actually be launched.
#[cfg(unix)]
fn is_executable(meta: &fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode() & 0o111 != 0
}
#[cfg(windows)]
fn is_executable(_meta: &fs::Metadata) -> bool {
true
}
// The [`crate::delivery::Repo`] trait impl (worktree lifecycle + squash
// delivery) lives in a sibling; an `impl` block registers on [`Project`]
// regardless of module, so no re-export is needed.
#[path = "delivery_repo_acts.rs"]
mod acts;
#[cfg(test)]
#[path = "delivery_repo_tests.rs"]
pub(crate) mod tests;
#[cfg(test)]
#[path = "delivery_repo_deliver_tests.rs"]
mod deliver_tests;
#[cfg(test)]
#[path = "delivery_repo_gate_tests.rs"]
mod gate_tests;