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
//! §12/§13 remote ops: `sync` (import) and `push` (publish). `sync` imports on
//! the explicit `bl sync` (and inside prime); `push` publishes after every
//! mutating op. Currency is OPTIMISTIC (mutate → push, bl-336a): there is no
//! pre-pull — a stale store surfaces atomically as the push's non-ff reject
//! (E5), and recovery is `bl sync` + retry — a forward to sync's verdict, not a
//! promise: the store may hold commits the remote never took, and then the
//! import refusal owns the exit ([`import_refused`], bl-4945). Both are no-ops in a stealth
//! (no-remote) repo: with no remote there is nothing to talk to, which is the
//! structural opt-out (§12).
use git;
use Binding;
use Env;
use cratereject_option_like;
use io;
use Path;
/// §13 `sync/pre`: the general rule — fetch the branch's UPSTREAM, **if any**,
/// then **fast-forward** THAT branch. "If any" is read from the remote
/// ([`remote_has_branch`], the same ls-remote that decides prime's
/// adopt-vs-found): an upstream-less branch — the landing by construction (§4),
/// any local-only branch — yields a no-op *for free*, no name special-cased.
/// The ff target is the branch the binding NAMES, never whatever the store
/// checkout happens to have checked out: the store's own branch integrates by
/// `merge --ff-only FETCH_HEAD` (the working tree moves with it); any other
/// branch is a pure ref move via the `<branch>:<branch>` refspec (ff-only by
/// git's own default). Either way the ff is atomically detect-and-act — a
/// non-ff IS the contention signal, so there is no separate contention probe;
/// on the checked-out branch it speaks in balls' voice ([`import_refused`],
/// bl-3129) rather than git's. Nothing is pushed, so a partial sync leaves the
/// branch at the old or the new tip, never wedged (§13 rollback).
/// A REFUSED STORE IMPORT, in balls' voice (bl-3129) — [`crate::git`]'s seal
/// rejection (bl-fa89) and `delivery_repo::acts::commit_swap`'s (bl-a3bb),
/// one layer out at the remote.
///
/// The fetched tip failed to fast-forward, which is §13 working: the import is
/// ff-only by contract (no union, no merge, no force), so the refusal means the
/// remote's history and this store's are no longer one line — and NOTHING moved,
/// neither imported nor overwritten. Raw git ("fatal: Not possible to
/// fast-forward, aborting", "Your local changes … would be overwritten") reads
/// as damage rather than as the two facts it is.
///
/// Unlike the seal's, this refusal is not always transient, and the sentence says
/// so. The optimistic §12 cycle (mutate → push, and a rejected push UN-SEALS,
/// tests/claim_race.rs) leaves the store non-diverged, so the ordinary cause is a
/// concurrent local `bl` whose seal was in flight across this fetch — a re-run
/// converges once it settles. A store that really does hold an unpublished
/// commit (a crash between seal and push, a hand-edited checkout) keeps refusing,
/// and the operator must reconcile it; naming both is the difference between an
/// instruction and a loop. No retry in core — the retry is one command (§14).
///
/// This is the ONE place that spells the EXIT (bl-4945), because it is the one
/// that detects the state: naming it without naming a way out still loops — push's
/// E5 sends the operator here, so its own sentence forwards to this verdict rather
/// than promising a convergence sync cannot always deliver. The exit is the
/// operator's, and it is stated as a choice, not a fix balls will apply: republish
/// the unpublished commits or discard them. balls never merges the two histories —
/// the ff-only contract IS the store's one-line-of-history invariant (§13), so an
/// automatic merge/rebase here would be core deciding an outcome only the operator
/// can weigh (whose ops those commits are, whether they still apply). The handles
/// are the ones the failed fetch just left in the store: `FETCH_HEAD` is the moved
/// remote tip, so `FETCH_HEAD..<branch>` is exactly the unpublished set.
/// Is `remote`'s `branch` tip NOT a store — no `tasks/` tree at its root? That
/// is the §16 migration window (bl-868d): a hub still carrying the
/// PRE-greenfield legacy JSON store on the (colliding, §16) store-branch name,
/// awaiting the runbook's one-time human cutover. Such a tip is no upstream at
/// all — every store TIP carries `tasks/` by construction (§2, the founding
/// `.gitkeep`; the §16 cutover join keeps the greenfield tree) — so a failed
/// integrate/publish against it is the window, not contention: warn (the §12
/// diagnostic-never-authority pattern) and report `true` so the caller skips,
/// keeping work local and the legacy ref intact (cutover is the runbook's
/// explicit history join + fast-forward push, never a rewrite).
/// Identification must be POSITIVE: the tip is re-fetched here (`FETCH_HEAD`),
/// and any failure to read it reports `false` — the caller's own error stands.
pub
/// The §16 migration-window warning — one spelling, shared by every site that
/// positively identified a legacy (non-store) tip.
/// Is `remote`'s `branch` tip a greenfield STORE (`tasks/` at its root, §2)?
/// `None` = the tip could not be read at all (unreachable remote, absent
/// branch) — the caller's own error stands; `Some(false)` is the §16 legacy
/// window [`not_yet_cut_over`] warns about; `Some(true)` is an ESTABLISHED
/// store, the E5 precondition. Positive identification by re-fetch
/// (`FETCH_HEAD`), shared by both reject-interpretation sites.
/// Does `remote` already carry `branch`? `git ls-remote --heads` is the one
/// round-trip that answers "an upstream, if any" — sync's no-op gate and
/// prime's adopt-vs-found / clone-vs-bootstrap signal (§12/§13).
pub
/// §12 `*/post`: publish the just-sealed balls branch to the remote — always to
/// an ESTABLISHED store (founding is `prime`'s alone, §12). A rejected push
/// (non-ff, perms revoked mid-life, a server-hook reject) means the mutation did
/// NOT land while the caller believes it is federated, so the non-zero exit
/// ABORTS the op (the push IS the optimistic mutate → push contention check;
/// re-run after `bl sync`) — it is NEVER silently degraded to stealth, which
/// would be split-brain (contrast `prime`'s founding-miss fallback, where
/// nothing existed to land on). ONE carve-out, positively identified: a reject
/// against a remote tip that is NOT a store ([`not_yet_cut_over`], bl-868d) is
/// the §16 migration window — warn and keep the work local; the legacy ref is
/// never rewritten (cutover is the runbook's explicit history join, published
/// as an ordinary fast-forward).
///
/// **A NESTED op does not publish (bl-1266).** An op publishes only if it is the
/// OUTERMOST `bl` in its invocation tree ([`Env::nested`]). A plugin that shells
/// `bl` (the shipped case: bl-chore's `claim.post` mint) inserts a whole op —
/// seal AND push — into the middle of its parent's post phase, so without this
/// the nested push publishes the PARENT's not-yet-final commit; a later
/// `claim.post` failure then un-seals only the LOCAL store (`git reset --hard`),
/// and the next `bl sync` fast-forwards the repudiated op straight back. Nothing
/// is lost by waiting: a push publishes a branch TIP, so the nested seal rides
/// the parent's own trailing push (the tracker sorts LAST, §14) — one push per op
/// TREE, still last, and §14's *"core never pushes, so there is nothing remote to
/// chase"* becomes a theorem instead of an accident of hook order.
/// §6/§13 `install/pre`: fetch the center's config branch (`balls/config`,
/// [`crate::LANDING_BRANCH`]) into the LANDING repo so core can MATERIALIZE it
/// locally and copy it in. The tracker is balls' only remote-talker — core never
/// fetches (§0) — so `prime --install`'s remote read rides this hook. It leaves
/// the config at the landing's `FETCH_HEAD` (a git-standard ref, so no invented
/// core↔plugin convention); core reads it from the same checkout. This is a READ
/// only — config adoption is destructive on the LANDING, never a push to the
/// center (publishing is `install --to`, a separate direction). Stealth (no
/// remote) is a no-op, like every handler — and so is a present remote that
/// simply LACKS the ref (bl-45fd): bl never publishes the landing (§4
/// single-owner), so a stock hub carries no `balls/config`, and a purely local
/// install must not depend on remote state. The gate is sync's own
/// [`remote_has_branch`] ("an upstream, if any", §13); an adopt that really
/// needs the center's config fails at point-of-use (no `FETCH_HEAD`).