can you rebase soph/progress-indicators onto soph/smart-subdivision

Is there an easy way to show progress including data throughput? or is this a huge effort?
Not a huge effort — the plumbing is already in place. countingReadCloser.Read (internal/syncer/stats.go:115) already sees every byte flowing through both source and target HTTP bodies; right now we only surface the total on Close. The two flavors:
- End-of-run throughput (tiny):
--statsalready gives bytes per service and--measure-memoryalready gives elapsed ms. Adding a derivedbytes/elapsedline to the human output is a few lines in the print path. ~30 min of work. - Live progress on stderr (small-medium): add an atomic byte counter shared across the counting readers, plus a ticker goroutine that prints
\rfetch: 42.1 MB @ 5.3 MB/son stderr behind a--progressflag (auto-off when--jsonor non-TTY). ~50–100 LOC, mostly instats.go+ a small renderer + flag wiring incmd/git-sync. The tradeoff: a true % bar is hard because git smart-HTTP responses are chunked with no Content-Length, so it'd be throughput + bytes-so-far, not a percentage.
I'd start with the end-of-run number since it's nearly free, and only add the live ticker if you actually want feedback during long bootstraps. Want me to do either?
Yeah I want both, ideally also separated for source and target, like how fast did we get data from the source and how fast did target take it (might be bound to network but still)
So also when doing a live ticket can we show both?
Yes — both is doable on one stderr line. Here's the approach I'd take, want to confirm before I start:
Per-side accounting: "source" = bytes streamed through the source HTTP transport (dominated by upload-pack response — what we got from the source). "target" = bytes streamed through the target transport (dominated by receive-pack request body — what we pushed to the target). I'll wrap both req.Body and resp.Body in the existing countingRoundTripper, with atomic byte counters per label so a ticker can sample without locking.
End-of-run lines (under --stats): add throughput: source=42.1 MB @ 5.3 MB/s | target=38.7 MB @ 4.9 MB/s derived from per-side bytes ÷ wall-clock elapsed.
Live ticker (--progress): goroutine prints \rsource: 12.3 MB @ 4.5 MB/s · target: 8.1 MB @ 3.2 MB/s to stderr every 200ms; auto-suppressed when stderr isn't a TTY or when --json is on.
Sound right? If yes I'll implement it — should be ~250-300 LOC across internal/syncer/stats.go, a new progress.go, syncer wiring, and flag plumbing.
All green. Here's what shipped:
End-of-run (under --stats):
"source" = bytes that flowed through the source HTTP transport (dominated by upload-pack response — what we got). "target" = bytes through the target transport (dominated by receive-pack request body — what we pushed). Computed against the stats-collector's own wall clock, so it works without --measure-memory.
Live ticker (under --progress):
Stderr-only, suppressed automatically when stderr isn't a TTY (so JSON pipes / CI logs stay clean), 200ms refresh, single in-place line that's terminated with a newline so subsequent output isn't mangled.
Files touched:
internal/syncer/stats.go— per-side atomic byte counters; wraps bothreq.Bodyandresp.Bodyso upload bytes count toward the target sideinternal/syncer/progress.go(new) — ticker, TTY check, byte/rate formattersinternal/syncer/syncer.go—Config.Progress,syncSession.finish()deferred from the four entry points, newthroughputLinein stats outputinternalbridge/model.go,unstable/client.go— plumbSides/ElapsedNanos/Progressthrough public APIcmd/git-sync/{sync,replicate,plan,bootstrap,fetch}.go—--progressflag- Tests:
progress_test.go+throughput_test.go(verifies real bootstrap populates per-side bytes, prints the throughput line, and renders both sides on the live ticker)
Race + lint pass.
Two commits, each self-contained:
-
1db3bcfTrack per-side transport bytes for source and target — pure plumbing. Wraps both request and response bodies in the existingcountingRoundTripper, adds atomic per-label counters and a wall-clock sample toStatssnapshots, plus the matchinginternalbridgefields. Builds, tests, and lints standalone with no callers — counters are just sitting there waiting to be consumed. -
633c57eShow per-side throughput at end of run and via --progress ticker — surfacing. Adds thethroughput: source=… · target=…line under--stats, the--progresslive ticker on stderr (TTY-gated),Config.Progress+syncSession.finish()plumbing, theunstable.AdvancedOptions.Progressfield, the four CLI flags, and tests.
Each commit builds and tests cleanly on its own; nothing on main is touched.
git-sync on soph/progress-indicators via 🐹 v1.26.2 ❯ go run cmd/git-sync/main.go
command-line-arguments
cmd/git-sync/main.go:25:13: undefined: newRootCmd
That's a Go invocation issue, not anything from this branch. go run cmd/git-sync/main.go only compiles that single file, so the other files in the package (root.go, syncplan.go, etc. where newRootCmd lives) are missing.
Use the package path instead:
Or build once and run:
Same problem would have hit you before this branch — nothing about the throughput/progress work changed how main.go is wired.
❯ go run ./cmd/git-sync sync --progress https://github.com/entireio/cli.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-cli.git source: 14.2 KB @ 2.15 KB/sxame for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': source: 14.2 KB @ 1.45 KB/ssignal: interrupt55b07276caf3.artifacts.cloudflare.net':
Two things going on, both around credentials:
-
Mangled line. Your target (Cloudflare) is asking for auth — go-git prompts on stderr (you can see the tail of
Username for 'https://…':poking through), and the progress ticker is also drawing on stderr. They clobber each other. Fix: provide credentials so no prompt fires, e.g. -
Only
source:shown. Target hasn't completed a single HTTP round trip yet — it's blocked on that credential prompt — so the target counter was never registered. As soon as the first target request fires, you'd see both lines.
If you want, I can land a small follow-up that pre-registers both source and target at session start, so the line reads source: … · target: 0 B @ 0 B/s from the first frame, even before target's first HTTP request. Doesn't fix the prompt collision but it removes the "where's target?" surprise. Want me to?
Pushed up as 2e3bd39. Re-run your earlier command and the credential prompt should fire cleanly before any ticker drawing — once you've answered it (or once the helper caches the creds for next time), the ticker takes over for the actual transfer phase.
Caveat to be aware of: if a credential prompt ever fires mid-transfer (essentially never in HTTP git, but theoretically possible), it would still collide. Fixing that case would require intercepting the git credential fill subprocess's stderr through a coordinator that pauses the ticker — much more invasive. Happy to tackle that if you ever hit it, but it's not worth the complexity preemptively.
can we do:
github.com -> 168 MB @ 4.35 MB/s · 168 MB @ 4.35 MB/s -> 8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net
basically take the host names from both sides?
Maybe we can use something better then "·" in the middle? any ideas?
Yes, doable. Layout would be:
Arrows on each side make the data-flow direction self-evident, so the middle separator just needs to mark "two different things". A few options:
| Separator | Look |
|---|---|
│ (U+2502, light vertical) | … 4.35 MB/s │ 168 MB @ … |
┃ (U+2503, heavy vertical) | … 4.35 MB/s ┃ 168 MB @ … |
| double space | … 4.35 MB/s 168 MB @ … |
⇢ (dashed arrow) | … 4.35 MB/s ⇢ 168 MB @ … |
My pick is │ — clean "this-and-that" divider, doesn't compete with the directional arrows, no risk of being confused for a flow indicator. Alternative I'd accept: just double space and let the arrows carry the visual structure.
One thing to flag: long hostnames (your Cloudflare host is ~57 chars) push the line past 80 cols. If it wraps, \r redraws break. I'd suggest just printing the full hostname and adding smart truncation later if it actually bites — most real-world hostnames fit. Alternative is something like 8b04592e…cloudflare.net from day one.
Which separator do you want, and do you want hostname truncation now or later?
Done. With your Cloudflare URL it would now render as:
(That hostname is 57 chars; truncated to a 30-char budget with the apex preserved.) Re-run with credentials configured and you should see the new format. If a particular hostname's truncation looks off in practice, the threshold (maxHostnameWidth = 30 in progress.go) is a one-line tweak away.
this is from a run before (will test the new layout next):
❯ go run ./cmd/git-sync sync --branch main --tags --progress https://github.com/entireio/cli.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': source: 11.3 MB @ 367 KB/s · target: 11.3 MB @ 366 KB/s CREATE refs/heads/main - create refs/heads/main at 0ec0d032 CREATE refs/tags/v0.3.0 - create refs/tags/v0.3.0 at e72157d0 CREATE refs/tags/v0.3.1 - create refs/tags/v0.3.1 at e72157d0 CREATE refs/tags/v0.3.10 - create refs/tags/v0.3.10 at c05553be CREATE refs/tags/v0.3.11 - create refs/tags/v0.3.11 at 01e95ba0 CREATE refs/tags/v0.3.12 - create refs/tags/v0.3.12 at bc58b21e CREATE refs/tags/v0.3.13 - create refs/tags/v0.3.13 at 6e349234 CREATE refs/tags/v0.3.2 - create refs/tags/v0.3.2 at ce1c563e CREATE refs/tags/v0.3.3 - create refs/tags/v0.3.3 at c080f3ee CREATE refs/tags/v0.3.4 - create refs/tags/v0.3.4 at 504ba492 CREATE refs/tags/v0.3.5 - create refs/tags/v0.3.5 at 9450c8a1 CREATE refs/tags/v0.3.6 - create refs/tags/v0.3.6 at 3cc27f94 CREATE refs/tags/v0.3.7 - create refs/tags/v0.3.7 at 7d83d898 CREATE refs/tags/v0.3.8 - create refs/tags/v0.3.8 at 38e81516 CREATE refs/tags/v0.3.9 - create refs/tags/v0.3.9 at f70f2b9c CREATE refs/tags/v0.4.2 - create refs/tags/v0.4.2 at 2a9998e5 CREATE refs/tags/v0.4.3 - create refs/tags/v0.4.3 at 59943565 CREATE refs/tags/v0.4.4 - create refs/tags/v0.4.4 at 2f0ad9ab CREATE refs/tags/v0.4.5 - create refs/tags/v0.4.5 at a92c0d06 CREATE refs/tags/v0.4.6 - create refs/tags/v0.4.6 at 48fe29da CREATE refs/tags/v0.4.7 - create refs/tags/v0.4.7 at 80695ef9 CREATE refs/tags/v0.4.8 - create refs/tags/v0.4.8 at 81ddee25 CREATE refs/tags/v0.4.9 - create refs/tags/v0.4.9 at 14b1c440 CREATE refs/tags/v0.5.0 - create refs/tags/v0.5.0 at 0fa52951 CREATE refs/tags/v0.5.1 - create refs/tags/v0.5.1 at d46fdc24 CREATE refs/tags/v0.5.2 - create refs/tags/v0.5.2 at 5affe173 CREATE refs/tags/v0.5.3 - create refs/tags/v0.5.3 at f2ddf9f7 CREATE refs/tags/v0.5.4 - create refs/tags/v0.5.4 at 746a74cd CREATE refs/tags/v0.5.4-nightly.202604091732.bf7bee9a - create refs/tags/v0.5.4-nightly.202604091732.bf7bee9a at bf7bee9a CREATE refs/tags/v0.5.4-nightly.202604100645.7b0903ab - create refs/tags/v0.5.4-nightly.202604100645.7b0903ab at 7b0903ab CREATE refs/tags/v0.5.5 - create refs/tags/v0.5.5 at 90bb1c50 CREATE refs/tags/v0.5.5-nightly.202604101410.746a74cd0 - create refs/tags/v0.5.5-nightly.202604101410.746a74cd0 at 746a74cd CREATE refs/tags/v0.5.5-nightly.202604110629.ceb09882 - create refs/tags/v0.5.5-nightly.202604110629.ceb09882 at ceb09882 CREATE refs/tags/v0.5.5-nightly.202604120639.3c6b56a7 - create refs/tags/v0.5.5-nightly.202604120639.3c6b56a7 at 3c6b56a7 CREATE refs/tags/v0.5.6 - create refs/tags/v0.5.6 at c9fedb4b CREATE refs/tags/v0.5.6-nightly.202604140645.a4fc0020 - create refs/tags/v0.5.6-nightly.202604140645.a4fc0020 at a4fc0020 CREATE refs/tags/v0.5.6-nightly.202604150645.0fe261c8 - create refs/tags/v0.5.6-nightly.202604150645.0fe261c8 at 0fe261c8 CREATE refs/tags/v0.5.6-nightly.202604160646.5bc86155 - create refs/tags/v0.5.6-nightly.202604160646.5bc86155 at 5bc86155 CREATE refs/tags/v0.5.6-nightly.202604170646.96867cdc - create refs/tags/v0.5.6-nightly.202604170646.96867cdc at 96867cdc CREATE refs/tags/v0.5.6-nightly.202604180633.957f073f - create refs/tags/v0.5.6-nightly.202604180633.957f073f at 957f073f CREATE refs/tags/v0.5.6-nightly.202604190642.fcda1cf7 - create refs/tags/v0.5.6-nightly.202604190642.fcda1cf7 at fcda1cf7 CREATE refs/tags/v0.5.6-nightly.202604210647.ef25b3c9 - create refs/tags/v0.5.6-nightly.202604210647.ef25b3c9 at ef25b3c9 CREATE refs/tags/v0.5.6-nightly.202604220646.cbf5d9b3 - create refs/tags/v0.5.6-nightly.202604220646.cbf5d9b3 at cbf5d9b3 CREATE refs/tags/v0.5.6-nightly.202604230647.17fa870e - create refs/tags/v0.5.6-nightly.202604230647.17fa870e at 17fa870e CREATE refs/tags/v0.5.6-nightly.202604240648.48e32b28 - create refs/tags/v0.5.6-nightly.202604240648.48e32b28 at 48e32b28 CREATE refs/tags/v0.5.7-nightly.202604250638.f53b923a - create refs/tags/v0.5.7-nightly.202604250638.f53b923a at f53b923a CREATE refs/tags/v0.5.7-nightly.202604280703.51ccd662 - create refs/tags/v0.5.7-nightly.202604280703.51ccd662 at 51ccd662 CREATE refs/tags/v0.5.7-nightly.202604290658.a01814a9 - create refs/tags/v0.5.7-nightly.202604290658.a01814a9 at a01814a9 CREATE refs/tags/v0.5.7-nightly.202604300701.6368942b - create refs/tags/v0.5.7-nightly.202604300701.6368942b at 6368942b CREATE refs/tags/v0.5.7-nightly.202605010703.da8fc8dd - create refs/tags/v0.5.7-nightly.202605010703.da8fc8dd at da8fc8dd CREATE refs/tags/v0.5.7-nightly.202605020647.589aba5b - create refs/tags/v0.5.7-nightly.202605020647.589aba5b at 589aba5b CREATE refs/tags/v0.6.0 - create refs/tags/v0.6.0 at 0ec0d032 summary: pushed=52 deleted=0 skipped=0 blocked=0 mode=sync protocol=v2 relay=true relay-mode=bootstrap relay-reason=empty-target-managed-refs batching=false batch-count=0 planned-batches=0
But a few things:
- once the transfer was done I think processing happened but that meant the transfer speed started dropping since it was still calculated, we basically knew it's done, right? Can we show it differently?
- let's say I use a bigger repo that needs a packfile split, can we surface which packfile we are currently sending?
Both shipped:
Point 1 (ed2dd56) — Rate now freezes at the active-window value (bytes / (lastByteAt − start)) instead of decaying as wall clock keeps moving past the last byte. Once a side has been idle for >750ms it gets a ✓ marker. The final frame force-marks all sides. So your earlier transfer would now read e.g. github.com → 11.3 MB @ 367 KB/s ✓ │ 11.3 MB @ 366 KB/s ✓ → cloudflare.net and stay there during the post-transfer ref-creation phase.
Point 2 (6fcd650) — Bootstrap publishes its current activity through a new OnPhase callback on bstrap.Params. The renderer reads it and appends (pack 3/8) to live frames. Covers all three bootstrap shapes:
- one-shot:
(pushing pack) - batched checkpoints:
(pack N/M)per checkpoint - post-batch tag push:
(pushing tags)
The phase suffix is suppressed on the final frame so it doesn't read as still-running.
Try a real run with a chunky repo and you should see the pack counter advancing while the target rate is active, then both sides settling with ✓ once the dust clears.
any idea why this is happening: ❯ go run ./cmd/git-sync sync --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': github.com → 14.2 KB @ 26.7 KB/s ✓ │ 4.01 KB @ 606 B/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: no source refs matched exit status 1
No branch named entire/checkpoints/v1 exists on the source. Closest matches:
refs/heads/entire/trails/v1— sameentire/<thing>/v1shape, possibly what you meantrefs/heads/feat/checkpoints-v2-validationrefs/heads/soph/copy-checkpoints-from-forks
So this is just a typo / wrong name — the planner correctly threw "no source refs matched" because nothing on source is called that.
That said, looking at this output reveals two genuinely useful UX things we could improve:
-
Better error. "no source refs matched" is terse. We could add: "requested branches:
entire/checkpoints/v1; available:main,entire-activity,entire/trails/v1, …" so a typo is obvious without dropping intogit ls-remote. -
Suggest near-matches. When the user asks for
entire/checkpoints/v1and we seeentire/trails/v1exists, we could surface that explicitly.
Want me to land #1? It's a 5-line change in the planner — useful in the same scenario you just hit, and unrelated to the progress work so easy to keep separate.
github.com → 1.63 GB @ 5.17 MB/s │ 1.63 GB @ 5.16 MB/s → 8b04592ed74a5cc…cloudflare.net (pack 1/4)
this went from without to 1/2 and now 1/4 maybe we can add a log line above, that we hit the pack size limit, and have to split?
Done. Re-running your big sync, when bootstrap goes from 1 → 2 → 4 packs you'd now see something like:
Three triggers now produce notices:
target rejected pack — switching to batched mode (limit 512 MB)— when the very first one-shot push gets refused and we fall back to batchedpack would exceed target limit — splitting N → M packs— when the pre-push header estimate already shows the pack is too bigtarget rejected pack — splitting N → M packs— when the actual push gets rejected (your case)
When --progress is off, the notices fall through to plain stderr so the information isn't lost.
Now your subdivision notices carry actual numbers. For your earlier sync the sequence would read like:
The pre-push estimate uses the actual PACK header object count × 750 bytes/object (already in the codebase). The post-rejection one parses the body limit out of the server error and falls back to a bare "target rejected pack" message if the limit isn't extractable. Per-pack estimates are estimated_total / new_count — slightly approximate since real distribution varies, but useful as a sanity check.
also let me guess, so it fails with a 500 but due to packfile size we try to shrink it until it fully fails: error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f68f6390e5ad2af-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error exit status 1
Actually no — your guess is half-right. Walk through what happened with the code as it stands:
-
First push (one-shot) failed with something that matched
isTargetBodyLimitError— a 413, or text containingbody exceeded size limit/request body too large. That's the only path that flips us into batched mode and setsTargetMaxPack. HTTP 500 alone wouldn't. -
Second push (batched, 1/2) — once we're in batched mode, the pre-push header estimate takes over. It doesn't look at the server response at all; it just multiplies the PACK header's object count by 750 bytes/object and compares to
TargetMaxPack. If the estimate exceeds the limit, it splits proactively — that's how 1 went to 2 and 2 went to 4 without anything actually being rejected. -
Push at 1/4 — header estimate now fits the limit, so we send the pack. Cloudflare returns HTTP 500 +
Internal Server Error. OurisTargetBodyLimitErrorchecks for"body exceeded size limit","request body" + "too large","payload" + "too large","http 413"— none of those match. So the post-rejection subdivide branch correctly doesn't trigger, the error propagates, and the sync dies.
So the code isn't shrinking because of the 500. The shrinking already happened earlier in the run from real body-size errors; the 500 at 1/4 is something else and we surface it cleanly.
That said, the 500 at 1/4 might still be Cloudflare's way of saying "too big" — they're inconsistent about it. Two genuinely useful fixes if you want to chase this:
- Treat HTTP 500 from this Cloudflare endpoint as a body-size signal (matches
Server=cloudflareheader + 5xx) and let it trigger another subdivide. Risky — it would also retry actual server bugs as size issues. - Add a max-subdivision guard so we don't shrink forever if subdivisions stop helping. E.g., bail after N subdivisions of the same range with no progress.
Want me to look at the actual response body / headers next time it 500s by running a curl against that endpoint with a mock pack, or just add a --verbose slog line for each push attempt + status code so you can see the sequence directly? My recommendation: add the verbose log first, run once more, then we know whether to widen the body-limit heuristic.
A little, yes — --verbose already logs the high-level bootstrap events (fetching, pushing, subdividing, switching to batched), but it's missing two things that would actually answer "is Cloudflare's 500 a size issue":
-
The estimated pack size at every push attempt — we compute it in
checkPackSizeAndSubdividebut only log it when we do subdivide. I'd log it on every push so you see the trajectory:push attempt: ~600 MB → split, thenpush attempt: ~300 MB → split, thenpush attempt: ~150 MB → 500. If that final 500 fires at 150 MB while the limit is 256 MB, it's almost certainly not a body-size issue. If it fires at 240 MB just under the 256 limit, Cloudflare is probably masking body-size as 500. -
The HTTP error from every failed push, not just the propagated final one. Right now we only see the last error's body+headers because that's what gets returned. The intermediate ones that triggered subdivisions get logged with their
err.Error()only on the subdivide path; if a push fails for some other reason in the middle of a batch we wouldn't see its details. Loggingerr.Error()(which already includes the Cf-Ray and response body) on the failure path before propagating would make the sequence transparent.
Both are 2-3 line additions in internal/strategy/bootstrap/bootstrap.go — no new infrastructure, just p.log(...) calls in the right places. Want me to land them so you can re-run with --verbose and we have actual data?
sorry, continue and add those, but also make sure --verbose works with the progress things:
Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': github.com → 487 B @ 926 B/s ✓ │ 4.01 KB @ 389 B/s → 8b04592ed74a5cc…cloudflare.nettime=2026-05-04T19:26:38.514+02:00 level=INFO msg="bootstrap fetching refs from source" ref_count=1 github.com → 6.76 KB @ 633 B/s │ 4.01 KB @ 389 B/s → 8b04592ed74a5cc…cloudflare.nettime=2026-05-04T19:26:38.803+02:00 level=INFO msg="bootstrap pushing refs to target" ref_count=1 source: Enumerating objects: 64652, done. source: Counting objects: 100% (392/392), done. source: Compressing objects: 100% (271/271), done.B/s ✓ → 8b04592ed74a5cc…cloudflare.net (pushing pack)source: Compressing objects: 36% (98/271)) github.com → 44.1 MB @ 2.12 MB/s │ 44.0 MB @ 2.12 MB/s → 8b04592ed74a5cc…cloudflare.net (pushing pack)
Both shipped:
52d3f19 — --verbose + --progress no longer fight for stderr. The slog handler and go-git's sideband progress sink (the source: Enumerating objects… lines) both write through a sessionStderr that routes via progressReporter.notify() when the ticker is active — clearing the frame, printing the line, re-drawing on the next tick. Both \n and \r are treated as line ends so Compressing 36%\rCompressing 50%\rCompressing 100% becomes three clean lines above the ticker instead of clobbering it.
4a759ed — Two new verbose log lines that answer your earlier diagnostic question:
If will_subdivide=false on the failed line and estimated_bytes is well under target_limit_bytes, the 500 isn't a body-size issue. If it's right at the boundary, Cloudflare is probably masking body-size as 500 and we should widen the heuristic. Re-run with --verbose --progress and the trajectory will be there to inspect.
github.com → 1.96 GB @ 5.07 MB/s │ 1.96 GB @ 5.06 MB/s → 8b04592ed74a5cc…cloudflare.net (pack 1/4)time=2026-05-04T19:33:03.451+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=4 new_remaining=8 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f693d41afacd2af-FRA, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>"
Re-run with --verbose --progress and the slog/notice lines should now land cleanly on their own row above the ticker. The ticker's \r\x1b[2K clears the entire previous row before drawing each frame, and notify does the same before printing a one-time message — so multi-byte UTF-8 in the progress line can no longer bleed through.
If you still see overlap after this, it's almost certainly your terminal not honoring \x1b[2K — but every modern macOS terminal (Terminal.app, iTerm2, alacritty, Ghostty, kitty, WezTerm) supports it, so this should be it.
❯ go run ./cmd/git-sync sync --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-04T20:17:25.873+02:00 level=INFO msg="bootstrap fetching refs from source" ref_count=1 time=2026-05-04T20:17:26.132+02:00 level=INFO msg="bootstrap pushing refs to target" ref_count=1 source: Enumerating objects: 64686, done. source: Counting objects: 0% (1/426) source: Counting objects: 1% (5/426) source: Counting objects: 2% (9/426) source: Counting objects: 3% (13/426) source: Counting objects: 4% (18/426) source: Counting objects: 5% (22/426) source: Counting objects: 6% (26/426) source: Counting objects: 7% (30/426) source: Counting objects: 8% (35/426) source: Counting objects: 9% (39/426) source: Counting objects: 10% (43/426) source: Counting objects: 11% (47/426) source: Counting objects: 12% (52/426) source: Counting objects: 13% (56/426) source: Counting objects: 14% (60/426) source: Counting objects: 15% (64/426) source: Counting objects: 16% (69/426) source: Counting objects: 17% (73/426) source: Counting objects: 18% (77/426) source: Counting objects: 19% (81/426) source: Counting objects: 20% (86/426) source: Counting objects: 21% (90/426) source: Counting objects: 22% (94/426) source: Counting objects: 23% (98/426) source: Counting objects: 24% (103/426) source: Counting objects: 25% (107/426) source: Counting objects: 26% (111/426) source: Counting objects: 27% (116/426) source: Counting objects: 28% (120/426) source: Counting objects: 29% (124/426) source: Counting objects: 30% (128/426) source: Counting objects: 31% (133/426) source: Counting objects: 32% (137/426) source: Counting objects: 33% (141/426) source: Counting objects: 34% (145/426) source: Counting objects: 35% (150/426) source: Counting objects: 36% (154/426) source: Counting objects: 37% (158/426) source: Counting objects: 38% (162/4 26) source: Counting objects: 39% (167/426) source: Counting objects: 40% (171/426) source: Counting objects: 41% (175/426) source: Counting objects: 42% (1 79/426) source: Counting objects: 43% (184/426) source: Counting objects: 44% (188/426) source: Counting objects: 45% (192/426) source: Counting objects: 46% (196/426) source: Counting objects: 47% (201/426) source: Counting objects: 48% (205/426) source: Counting objects: 49% (209/426) source: Counting objects: 50% (213/426) source: Counting objects: 51% (218/426) source: Counting objects: 52% (222/426) source: Counting objects: 53% (226/426) source: Counting objects: 54% (231/426) source: Counting objects: 55% (235/426) source: Counting objects: 56% (239/426) source: Counting objects: 57% (243/426) source: Counting objects: 58% (248/426) source: Counting objects: 59% (252/426) source: Counting objects: 60% (256/426) source: Counting objects: 61% (260/426) source: Counting objects: 62% (265/426) source: Counting objects: 63% (269/426) source: Counting objects: 64% (273/426) source: Counting objects: 65% (277/426) source: Counting objects: 66% (282/426) source: Counting objects: 67% (286/426) source: Counting objects: 68% (290/426) source: Counting objects: 69% (294/426) source: Counting objects: 70% (299/426) source: Counting objects: 71% (303/426) source: Counting objects: 72% (307/426) source: Counting objects: 73% (311/426) source: Counting objects: 74% (316/426) source: Counting objects: 75% (320/426) source: Counting objects: 76% (324/426) source: Counting objects: 77% (329/426) source: Counting objects: 78% (333/426) source: Counting objects: 79% (337/426) source: Counting objects: 80% (341/426) source: Counting objects: 81% (346/426) source: Counting objects: 82% (350/4 26) source: Counting objects: 83% (354/426) source: Counting objects: 84% (358/426) source: Counting objects: 85% (363/426) source: Counting objects: 86% (367/426) source: Counting objects: 87% (371/426) source: Counting objects: 88% (375/426) source: Counting objects: 89% (380/426) source: Counting objects: 90% (384/426) source: Counting objects: 91% (388/426) source: Counting objects: 92% (392/426) source: Counting objects: 93% (397/426) source: Counting objects: 94% (401/426) source: Counting objects: 95% (405/426) source: Counting objects: 96% (409/426) source: Counting objects: 97% (414/426) source: Counting objects: 98% (418/426) source: Counting objects: 99% (422/426) source: Counting objects: 100% (426/426) source: Counting objects: 100% (426/426), done. source: Compressing objects: 0% (1/295) source: Compressing objects: 1% (3/295) source: Compressing objects: 2% (6/295) source: Compressing objects: 3% (9/295) source: Compressing objects: 4% (12/295) source: Compressing objects: 5% (15/295) source: Compressing objects: 6% (18/295) source: Compressing objects: 7% (21/295) source: Compressing objects: 8% (24/295) source: Compressing objects: 9% (27/295) source: Compressing objects: 10% (30/295) source: Compressing objects: 11% (33/295) source: Compressing objects: 12% (36/295) source: Compressing objects: 13% (39/295) source: Compressing objects: 14% (42/295) source: Compressing objects: 15% (45/295) source: Compressing objects: 16% (48/295) source: Compressing objects: 17% (51/295) source: Compressing objects: 18% (54/295) source: Compressing objects: 19% (57/295) source: Compressing objects: 20% (59/295) source: Compressing objects: 21% (62/295) source: Compressing objects: 22% (65/295) source: Compressing objects: 23% (68/295) source: Compressing objects: 24% (71/295) source: Compressing objects: 25% (74/295) source: Compressing objects: 26% (77/295) source: Compressing objects: 27% (80/295) source: Compressing objects: 28% (83/295) source: Compressing objects: 29% (86/295) source: Compressing objects: 30% (89/295) source: Compressing objects: 31% (92/295) source: Compressing objects: 32% (95/295) source: Compressing objects: 33% (98/295) source: Compressing objects: 34% (101/295) source: Compressing objects: 35% (104/295) source: Compressing objects: 35% (105/295) source: Compressing objects: 36% (107/295) source: Compressing objects: 37% (110/295) source: Compressing objects: 38% (113/295) source: Compressing objects: 39% (116/295) source: Compressing objects: 40% (118/295) source: Compressing objects: 41% (121/295) source: Compressing objects: 42% (124/295) source: Compressing objects: 43% (127/295) source: Compressing objects: 44% (130/295) source: Compressing objects: 45% (133/295) source: Compressing objects: 46% (136/295) source: Compressing objects: 47% (139/295) source: Compressing objects: 48% (142/295) source: Compressing objects: 49% (145/295) source: Compressing objects: 50% (148/295) source: Compressing objects: 51% (151/295) source: Compressing objects: 52% (154/295) source: Compressing objects: 53% (157/295) source: Compressing objects: 54% (160/295) source: Compressing objects: 55% (163/295) source: Compressing objects: 56% (166/295) source: Compressing objects: 57% (169/295) source: Compressing objects: 58% (172/295) source: Compressing objects: 59% (175/295) source: Compressing objects: 60% (177/295) source: Compressing objects: 61% (180/295) source: Compressing objects: 62% (183/295) source: Compressing objects: 63% (186/295) source: Compressing objects: 64% (189/295) source: Compressing objects: 65% (192/295) source: Compressing objects: 66% (195/295) source: Compressing objects: 67% (198/295) source: Compressing objects: 68% (201/295) source: Compressing objects: 69% (204/295) source: Compressing objects: 70% (207/295) source: Compressing objects: 71% (210/295) source: Compressing objects: 72% (213/295) source: Compressing objects: 73% (216/295) source: Compressing objects: 74% (219/295) source: Compressing objects: 75% (222/295) source: Compressing objects: 76% (225/295) source: Compressing objects: 77% (228/295) source: Compressing objects: 78% (231/295) source: Compressing objects: 79% (234/295) source: Compressing objects: 80% (236/295) source: Compressing objects: 81% (239/295) source: Compressing objects: 82% (242/295) source: Compressing objects: 83% (245/295) source: Compressing objects: 84% (248/295) source: Compressing objects: 85% (251/295) source: Compressing objects: 86% (254/295) source: Compressing objects: 87% (257/295) source: Compressing objects: 88% (260/295) source: Compressing objects: 89% (263/295) source: Compressing objects: 90% (266/295) source: Compressing objects: 91% (269/295) source: Compressing objects: 92% (272/295) source: Compressing objects: 93% (275/295) source: Compressing objects: 94% (278/295) source: Compressing objects: 95% (281/295) source: Compressing objects: 96% (284/295) source: Compressing objects: 97% (287/295) source: Compressing objects: 98% (290/295) source: Compressing objects: 99% (293/295) source: Compressing objects: 100% (295/295) source: Compressing objects: 100% (295/295), done.
Re-run with --verbose --progress and the sideband progress should now read as proper single-row updates (source: Counting objects: 10% (43/426)) instead of the prefix being broken onto its own row.
Quick caveat to flag: even with this fix, server-side progress at high frequency (the percentage updates every few hundred ms) will scroll past pretty fast under --verbose. If the noise is too much in practice, there are two simple follow-ups:
- Throttle the sideband sink so we don't pass every 1% update through (drop frames within e.g. 250ms of the previous one).
- Send sideband progress through
notifyonly when it differs from the previous frame (skip duplicates).
Mention if either becomes annoying in real use.
but could we not make it so it's updating in a single line? source: Compressing objects: 89% (263/295) source: Compressing objects: 90% (266/295) source: Compressing objects: 91% (269/295) source: Compressing objects: 92% (272/295) source: Compressing objects: 93% (275/295) source: Compressing objects: 94% (278/295) source: Compressing objects: 95% (281/295) source: Compressing objects: 96% (284/295) source: Compressing objects: 97% (287/295) source: Compressing objects: 98% (290/295) source: Compressing objects: 99% (293/295) source: Compressing objects: 100% (295/295) source: Compressing objects: 100% (295/295), done.
Re-run with --verbose --progress and you'll see:
When the sideband phase finishes (Compressing 100%, done.\n), that line scrolls into the permanent log above and the transient row goes empty until the next phase starts (e.g., Resolving deltas:). The throughput ticker keeps running on its own row underneath the whole time.
Caveat: this only works on terminals that honor \x1b[%dA (cursor-up) and \x1b[J (erase-to-end-of-screen) — that's every modern macOS/Linux/Windows terminal but no-op pipes/log files (which we already auto-suppress via the TTY check, so no change there).
ok, looks good, next thing:
time=2026-05-04T20:19:04.242+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48514500 target_limit_bytes=536870912 time=2026-05-04T20:20:39.240+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48514500 target_limit_bytes=536870912 will_subdivide=true error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6982f7a94fd2af-FRA, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" time=2026-05-04T20:20:39.240+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=1 new_remaining=2 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6982f7a94fd2af-FRA, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" target rejected pack — splitting 1 → 2 packs
So it gives us this after first try. And if I let it run it gets until 32 parts and then the size is enough. I wonder now: If I do a mirror push with the git cli, can I somehow also turn on verbose or monitoring to figure out how it does the handshakes? it would otherwise run in the same size limit issues, right?
Good question — and the short answer is: plain git push --mirror would absolutely hit the same 413, because git itself does not auto-subdivide packfiles. There's no native fallback to "split into smaller packs and push them sequentially" — that incremental-checkpoint behavior is exactly the value-add git-sync's batched bootstrap is providing.
What http.postBuffer does not do, despite the common belief: it's just a local memory buffer for the request body. Bumping it lets git send a larger pack in one POST, but it doesn't chunk to fit a server limit. If the pack exceeds Cloudflare's body cap, git push fails with 413 and stops.
To verify and study the handshake, the relevant env vars on git CLI are:
So a useful comparison run would be:
You'll see:
- the
info/refs?service=git-receive-packGET with caps advertised back - the single
git-receive-packPOST containing the full pack - on a sufficiently large repo, the response will be
HTTP/1.1 413 Payload Too Largeand git will just exit non-zero — no retry logic kicks in
If you want to confirm the body-size threshold is identical to what git-sync hit, the GIT_TRACE_CURL=1 output shows Content-Length: … on the POST. Compare that to the target_limit_bytes that git-sync logged. If they match (or git's number is just over Cloudflare's parsed limit), it's the same wall.
If you do want to know where Cloudflare's actual cap is to set --target-max-pack-bytes upfront (skipping the 1 → 2 → 4 → … 32 dance), running GIT_TRACE_CURL=1 git push --mirror … once and grepping the failure response for the limit, or pulling the parsed limit out of git-sync's target_limit_bytes log line on the first 413, both work. Then git-sync sync --target-max-pack-bytes 50000000 (or whatever the cap is) will start at the right size on the next run.
that is to much tracing:
20:36:30.460389 http.c:878 => Send data: ....D$......7.."..<.....N,7h...Df.....4<..........q.TQ4.|F. 20:36:30.460390 http.c:878 => Send data: ..N........RU.@..\.gx.psx .|..T...<..,C:Q..%.$...0.S..@..?~. 20:36:30.460391 http.c:878 => Send data: #+..(.....A..}F.46R....V.....6.+b.....=..!,.H.....5.b..a..6
20:36:30.460391 http.c:878 => Send data: 0......n..j.l...eC/..}.&..-.C.8?{.oJZf..V.0.."........NQ5.
20:36:30.460392 http.c:878 => Send data: .%...LT>..UsB*.......1.).0..x...X...O8.:.r..6.!c.Af.....krS.
20:36:30.460393 http.c:878 => Send data: .@.....6XE...O.eT..... v..B...6C..X..Dq.... ?......m...... 20:36:30.460394 http.c:878 => Send data: ...O..1~Q.......X....*.,)-<....e..p.w..Y......&...c;S......r 20:36:30.460395 http.c:878 => Send data: Ua.ju..~...P....Y.....w..E..tq...L.uX.7....,..)..l.l{..[...
20:36:30.460396 http.c:878 => Send data: ..#...s'Mf[^..l.....f}........b)....-...........O...?A+.|..@
That's the binary packfile body. Suppress it with GIT_TRACE_CURL_NO_DATA=1 — keeps every header line and protocol message but drops the body dumps:
You'll still see the => Send header: … lines (request method, URL, content-type, content-length) and <= Recv header: … (status code, response headers), but => Send data: blobs become => Send data, NNNN bytes (0xXXXX) size-only stubs. That's exactly the level you need for the handshake — which capabilities both sides advertised, what content-length actually went up, what status code came back.
If you want to dump the response body but not the request body (so you can read Cloudflare's HTML 413 explanation without the gigabyte of pack going up), there isn't a separate flag for that — GIT_TRACE_CURL_NO_DATA=1 suppresses both directions. But the response is small enough that even with GIT_TRACE_CURL=1 and no _NO_DATA, the <= Recv data: for a 413 is tiny; the noise is all on the upload side. So the practical workflow is:
- First run:
GIT_TRACE_CURL_NO_DATA=1to see headers - If you want the response body too:
teethe entire trace and grep for<= Recvlines after the fact
20:37:05.989210 http.c:890 == Info: Connection #0 to host 8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net left intact 20:37:05.989320 http.c:890 == Info: Found bundle for host: 0x101a53230 [can multiplex] 20:37:05.989346 http.c:890 == Info: Re-using existing connection with host 8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net 20:37:05.989373 http.c:890 == Info: Server auth using Basic with user 'x' 20:37:05.989469 http.c:890 == Info: [HTTP/2] [7] OPENED stream for https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack 20:37:05.989487 http.c:890 == Info: [HTTP/2] [7] [:method: POST] 20:37:05.989494 http.c:890 == Info: [HTTP/2] [7] [:scheme: https] 20:37:05.989502 http.c:890 == Info: [HTTP/2] [7] [:authority: 8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net] 20:37:05.989509 http.c:890 == Info: [HTTP/2] [7] [:path: /git/soph/test-repo-bench1.git/git-receive-pack] 20:37:05.989516 http.c:890 == Info: [HTTP/2] [7] [authorization: Basic <redacted>] 20:37:05.989523 http.c:890 == Info: [HTTP/2] [7] [user-agent: git/2.52.0] 20:37:05.989529 http.c:890 == Info: [HTTP/2] [7] [accept-encoding: deflate, gzip] 20:37:05.989535 http.c:890 == Info: [HTTP/2] [7] [content-type: application/x-git-receive-pack-request] 20:37:05.989542 http.c:890 == Info: [HTTP/2] [7] [accept: application/x-git-receive-pack-result] 20:37:05.989548 http.c:890 == Info: [HTTP/2] [7] [accept-language: en-GB, *;q=0.9] 20:37:05.990540 http.c:837 => Send header, 0000000409 bytes (0x00000199) 20:37:05.990563 http.c:849 => Send header: POST /git/soph/test-repo-bench1.git/git-receive-pack HTTP/2 20:37:05.990570 http.c:849 => Send header: Host: 8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net 20:37:05.990576 http.c:849 => Send header: Authorization: Basic <redacted> 20:37:05.990581 http.c:849 => Send header: User-Agent: git/2.52.0 20:37:05.990586 http.c:849 => Send header: Accept-Encoding: deflate, gzip 20:37:05.990592 http.c:849 => Send header: Content-Type: application/x-git-receive-pack-request 20:37:05.990597 http.c:849 => Send header: Accept: application/x-git-receive-pack-result 20:37:05.990603 http.c:849 => Send header: Accept-Language: en-GB, *;q=0.9 20:37:05.990607 http.c:849 => Send header: 20:38:37.689437 http.c:837 <= Recv header, 0000000013 bytes (0x0000000d) 20:38:37.689488 http.c:849 <= Recv header: HTTP/2 413 20:38:37.689519 http.c:837 <= Recv header, 0000000037 bytes (0x00000025) 20:38:37.689528 http.c:849 <= Recv header: date: Mon, 04 May 2026 18:38:37 GMT 20:38:37.689544 http.c:837 <= Recv header, 0000000040 bytes (0x00000028) 20:38:37.689551 http.c:849 <= Recv header: content-type: text/html; charset=UTF-8 20:38:37.689569 http.c:837 <= Recv header, 0000000021 bytes (0x00000015) 20:38:37.689578 http.c:849 <= Recv header: content-length: 167 20:38:37.689593 http.c:837 <= Recv header, 0000000099 bytes (0x00000063) 20:38:37.689601 http.c:849 <= Recv header: cache-control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0 20:38:37.689614 http.c:837 <= Recv header, 0000000030 bytes (0x0000001e) 20:38:37.689620 http.c:849 <= Recv header: referrer-policy: same-origin 20:38:37.689630 http.c:837 <= Recv header, 0000000040 bytes (0x00000028) 20:38:37.689636 http.c:849 <= Recv header: expires: Thu, 01 Jan 1970 00:00:01 GMT 20:38:37.689646 http.c:837 <= Recv header, 0000000029 bytes (0x0000001d) 20:38:37.689652 http.c:849 <= Recv header: x-frame-options: SAMEORIGIN 20:38:37.689660 http.c:837 <= Recv header, 0000000020 bytes (0x00000014) 20:38:37.689666 http.c:849 <= Recv header: server: cloudflare 20:38:37.689674 http.c:837 <= Recv header, 0000000030 bytes (0x0000001e) 20:38:37.689680 http.c:849 <= Recv header: cf-ray: 9f699d60989cd2af-FRA 20:38:37.689688 http.c:837 <= Recv header, 0000000002 bytes (0x00000002) 20:38:37.689694 http.c:849 <= Recv header: 20:38:37.689744 http.c:890 == Info: HTTP error before end of send, stop sending 20:38:37.689767 http.c:890 == Info: abort upload after having sent 524576868 bytes 20:38:37.690059 http.c:890 == Info: Connection #0 to host 8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net left intact error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413 send-pack: unexpected disconnect while reading sideband packet Writing objects: 100% (64686/64686), 960.23 MiB | 10.31 MiB/s, done. Total 64686 (delta 34487), reused 63908 (delta 33711), pack-reused 0 (from 0) fatal: the remote end hung up unexpectedly Everything up-to-date
Perfect — this is exactly the data point we needed. Three concrete things from that trace:
-
Cloudflare's actual cap is ~500 MiB, not 512. Look at this line:
That's 524,576,868 bytes ≈ 500.27 MiB. Cloudflare cuts the request mid-upload (sends the 413 response before git is done writing) right around the 500 MiB mark. git-sync's default
defaultTargetMaxPackBytes = 512 MiBis just over that wall, which is why batched mode still hits 413 once and has to split 1 → 2 → 4 → … the first time. -
Plain
git push --mirroris dead in the water at this point. No retry, no chunking, no fallback — justerror: RPC failed; HTTP 413andfatal: the remote end hung up unexpectedly. The "Everything up-to-date" line at the bottom is a quirk of git-push's exit handling, not a success. So git-sync's incremental checkpoint behavior really is what's making this repo syncable at all to this endpoint. -
No Cloudflare-specific size header in the response — just
HTTP/2 413and a 167-byte HTML body. There's nothing in the headers we could parse to learn the cap programmatically; we'd need a hardcoded heuristic or trial-and-error.
So the immediate practical fix for your re-runs: pass --target-max-pack-bytes upfront and skip the dance entirely. Two reasonable values:
If you want a small follow-up code change: bump defaultTargetMaxPackBytes in internal/strategy/bootstrap/bootstrap.go from 512 * 1024 * 1024 to 480 * 1024 * 1024 (480 MiB). That gives a safety margin against the Cloudflare wall and will succeed on the first batched attempt for everyone, not just you. The constant is already a "best-guess fallback" used only when we can't parse the limit from the error — which is exactly Cloudflare's case. Want me to land that?
not sure I understand this part now, shouldn't it be more then one now:
time=2026-05-04T20:42:36.452+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48514500 target_limit_bytes=524288000 github.com → 78.0 MB @ 3.49 MB/s │ 76.5 MB @ 3.42 MB/s → 8b04592ed74a5cc…cloudflare.net (pack 1/1)
Different repo this time, that's why. Previously you were syncing cli.git (the ~960 MiB one that needed 32 splits); this run is cli-checkpoints.git and the pack is way smaller. Let me decode the numbers:
estimated_bytes=48514500= 64,686 objects × 750 bytes/object (the constantestimatedBytesPerObjectinbootstrap.go:703). That's a rough average — for compressed objects, real values can swing from ~200 bytes (tiny commits) to ~2 KiB (blob-heavy). Your live counter shows ~78 MB and rising, so reality is roughly 60% larger than the heuristic — typical for repos with large blobs.target_limit_bytes=524288000= the 500 MiB you passed via--target-max-pack-bytes.batch_total=1means the planner calculated one checkpoint will fit. With ~78 MB actual vs a 500 MiB limit, you have ~6× headroom — well within the wall, so a single pack is correct here.
The 1 → 2 → 4 → … → 32 dance you saw earlier was for cli.git, which has a real pack around 960 MiB (you can see this in your earlier git push --mirror trace: Writing objects: 100% (64686/64686), 960.23 MiB). With a 500 MiB cap, that needs ~2 chunks at minimum — but because the planner's first estimate undershot real bytes (per-object × 750 said ~48 MB but reality was ~960 MB), it planned 1 pack, fetched it, peeked the header → estimate said 48 MB → tried to push → 413 from Cloudflare at 524 MB → subdivide 1→2 → re-fetch each half → still over → subdivide 2→4 → ... until each piece was small enough to slip under 500 MiB. That's the dance.
For the current repo, you're done at pack 1/1. If the actual push completes around 78 MB without a 413, that confirms it.
If you want to make the planner's first estimate much closer to truth (and avoid the dance for cli.git-shaped repos), we could:
- Bump
estimatedBytesPerObjectfrom 750 → ~1500 to be more conservative for blob-heavy repos. Trade-off: skinny repos plan more chunks than needed (cheap; extra batches finish fast). - Or sample the actual pack size on a probe fetch first before committing to a chunk count. Heavier — adds a roundtrip — but precise.
(1) is a one-line change. Want me to do it?
it was cli-checkpoints for a while now, sorry, and I saw the dance for this one, the git push --mirror was also for that repo
OK that changes the picture — same repo for everything, so the dance was on this one too. You're right that batch_total=1 is misleading. Here's what's actually happening:
The heuristic is wildly wrong for this repo. Real numbers:
- Actual pack size (from your
git push --mirrortrace): 960.23 MiB ≈ 1,006 MB - Object count: 64,686
- Real bytes/object: ~15,562
- Our hardcoded estimate (
estimatedBytesPerObject = 750): 20× too low
So the planner does this:
estimateBatchCount(chainLen, 524 MB)says "1 batch is fine" because chain × 65 KiB/commit < 524 MB- After fetching, header peek says
48 MB(64,686 × 750) → "definitely fine, push it" - Cloudflare's actual cut is around 524 MB; real pack is ~960 MB → 413
- Post-rejection subdivide halves:
1 → 2. Re-fetch each half, header still says ~24 MB, push, fail again. - Repeat:
2 → 4 → 8 → 16 → 32. Each iteration also re-fetches the full chain from source.
Setting --target-max-pack-bytes 524288000 didn't avoid the dance — it just set the limit. The dance is driven by reactive halving on a too-optimistic estimate.
The real fix: use actual data from the failed push to subdivide intelligently. We already have it — the target-side byte counter tells us exactly how many bytes were transmitted before Cloudflare cut us off. If 524 MB went up before the 413 and the limit is 500 MB, and we know there are N more checkpoints remaining, we can compute the real needed pack count in one step:
Jump straight from 1 → 32 (or whatever the actual ratio demands) on the first 413, instead of dancing for log₂(32) = 5 rounds. That's 5 wasted source re-fetches per branch.
Two options for the fix:
-
Smart subdivision from observed pack size — adds ~30 lines to the post-rejection branch. Snapshot
target.bytesbeforePushPack, compute the delta on failure, divide remaining by(delta / limit). One-shot jump to the right count. -
Cheaper alternative: just bump
estimatedBytesPerObjectfrom 750 → ~3000 (4× more conservative). Pre-flight check would catch the fat pack and subdivide pre-push without the wasted upload. Skinny repos plan a few extra batches, harmless. Doesn't solve every case but handles the common "blob-heavy" failure mode without code complexity.
Want me to do (1)? It's the right fix and gives a meaningful diagnostic line ("rejected at 524 MB, jumping to 32 packs based on observed size").
let's do 1, can we do 1 in a separate PR easily? then I would suggest we push the progress changes, then do the other change in a new branch (I'll open a PR) and we rebase the progress on top of it for now
Both branches pushed:
soph/progress-indicators(12 commits, offmain) → https://github.com/entireio/git-sync/pull/new/soph/progress-indicatorssoph/smart-subdivision(1 commit, offmain) → https://github.com/entireio/git-sync/pull/new/soph/smart-subdivision
Once you've opened the PR for smart-subdivision, ping me and I'll rebase progress-indicators on top of it. If you want, I can do the rebase now anyway — it's two independent commit ranges with no overlap, so it should be a clean rebase regardless of merge order.
Quick recap of what's in the smart-subdivision commit:
packReadCounterwraps the pack stream;counter.nreports bytes the HTTP transport actually pulled before the server returned 413.observedSubdivisionFactor(sent, limit)returnsceil(sent × 2 / limit)(minimum 2). The× 2is the safety multiplier for the unsent-but-real portion of the pack.subdivideToFactorkeeps halving until the count reaches the requested factor; rounds up to the next power of 2.- The post-rejection branch in
bootstrap.gonow uses both. For your Cloudflare scenario (524 MiB sent, 500 MiB limit, real pack 960 MiB), factor = 3 → first split jumps 1 → 4 packs, each ~240 MiB, well within the cap. One round instead of five. - Slog line now carries
sent_bytes,limit_bytes, andfactorso verbose runs show the arithmetic.
Done. Layout now:
main(752b6e3)soph/smart-subdivision(1 commit:f794e73) — base for the next stacksoph/progress-indicators(12 commits, rebased on top of smart-subdivision) — head is now2cd03f6
Everything builds, tests pass with -race, lint clean. Three conflicts resolved during the rebase, all in bootstrap.go around the post-rejection subdivide block — each resolution kept smart-subdivision's structure (pushErr variable, factor-based splitting, the sent_bytes / limit_bytes / factor slog fields) and folded in the progress branch's additions (the notice() call, the bootstrap batch push failed log line, the limit-text in the user-facing notice). The bootstrap batch push failed log now also carries sent_bytes since that's available now — useful diagnostic when verifying the smart-subdivision math in a real run.
Subdivision skipped when factor ≤ remaining checkpoint count
High Severity
subdivideToFactor uses len(expanded) < targetCount as its loop guard, so when the remaining checkpoint count already meets or exceeds factor, it returns unchanged. The caller then checks len(expanded) > len(batch.Checkpoints[idx:]), which evaluates to false, turning a recoverable 413 into a hard failure. This is a regression from the old code, which always called subdivideCheckpoints at least once. When a reverse proxy cuts the connection at its body limit, sentBytes ≈ limit, so factor is always 2. The first rejection subdivides 1→2, but the second rejection sees 2 remaining ≥ factor 2, skips subdivision entirely, and fails.
Fix verified. Now I need to land it on the soph/smart-subdivision branch (since that's where subdivideToFactor lives logically), then re-rebase progress on top. Let me stash my changes, switch, amend smart-subdivision, then re-rebase.
Done. Final state:
-
soph/smart-subdivision(1 commit,763ad6d) — amended to fix the regression. The new testTestSubdivideToFactorAlwaysProgressesexercises the exact scenario you described (factor=2 stuck at limit boundary, second rejection on a 2-piece batch). Tests + lint clean. Force-pushed. -
soph/progress-indicators(13 commits, heada35cb87) — re-rebased on top of the corrected smart-subdivision. Usedgit rebase --onto soph/smart-subdivision f794e73 …to skip the duplicated original-smart-subdivision commit and replay only the progress commits. Clean replay, no conflicts. Tests + lint clean. Force-pushed.
The fix in one paragraph: subdivideToFactor now unconditionally calls subdivideCheckpoints once before checking the targetCount loop, so a call with len(remaining) ≥ targetCount still produces forward progress instead of returning the input unchanged. The only path that still returns the input is when subdivision is genuinely impossible (every gap is already 1 commit) — guarded by the len(expanded) <= len(remaining) check on the first round.
one question: I have a network stats tool on my mac, it instantly says 44MB/s up/down each, but the git-sync out put slowly moves up from 13 to 27 but never gets past that
That's expected behavior — and fixable. The displayed rate is bytes ÷ (last_byte_time − stats_start_time). stats_start_time is when the syncer session was created, before any bytes flow. So the active window includes the time spent on auth, info-refs, ls-refs, and ref planning — all of which are tiny but slow setup phases. The actual pack streaming happens later, at the wire-rate your network tool sees, but the displayed average dilutes it across the whole session.
Concretely, for a transfer that goes:
t = 0–3s: setup (auth + ref listing, ~hundreds of KB)t = 3–13s: pack streaming at 44 MB/s, ~440 MB total
At t=13s you'd see 441 MB / 13s = 33.9 MB/s displayed, slowly climbing toward 44 MB/s as the proportion of streaming-time grows but never reaching it. That matches your "13 → 27, never past 27" — the streaming hasn't run long enough yet for the average to converge.
There's also a smaller secondary factor: macOS network tools count wire bytes (TCP/TLS framing, HTTP headers, retransmissions, plus both directions on each socket). Our counter wraps the HTTP body, so we miss the framing overhead. That's maybe 5–10% though — not the bulk of your gap.
Two easy fixes, depending on what you want the number to mean:
-
"Streaming throughput" — track
firstByteAtper side and compute rate overlastByteAt − firstByteAtinstead oflastByteAt − sessionStart. Number jumps to roughly the actual pack-streaming rate (within ~10% of the network tool). Stable, no jitter. Trivial change: 1 field onsideCounter, 1 line inRead, 1 conditional inliveSides. -
"Instantaneous throughput" — sliding window of bytes-per-second over the last ~2 seconds. Closer to what the network tool shows and bouncier (you'd see it ramp up and back down as packs flush). More work: ring buffer + sampler.
I'd recommend (1). The "average over the streaming phase" is what most users actually want — stable enough to read, and matches the headline question "how fast was this transfer?". Want me to land it?
Maybe 2–3 hours total. Concretely:
Core mechanics (~80 LOC):
- Per-side ring buffer of
(timestamp, cumulative_bytes)samples — fixed size, e.g. 20 slots for a 2-second window at 100 ms cadence - A sampler goroutine that ticks every 100 ms and snapshots
bytes.Load()into the ring (decoupled fromRead, so no hot-path overhead) - A
windowRate()helper that returns(latest_bytes − oldest_bytes_within_window) / window_seconds
Wiring (~40 LOC):
- Add
InstantBytesPerSec int64toSideBytes - Populate it in
liveSides()from the ring formatSidechooses between active-window rate and instant rate (probably show both: "44 MB/s now · 33 MB/s avg")
Decisions that add scope:
- Jitter smoothing: raw window rate flickers as packs flush in bursts. EWMA dampens that — another ~15 LOC.
- End-of-transfer behavior: instant rate naturally drops to 0 once bytes stop, which conflicts with the "✓ frozen rate" we already have. Either freeze the instant rate at the last non-zero value when idle, or hide the instant rate after the side goes idle (and only show frozen average). Another ~10 LOC.
- Tests: ring-buffer unit tests + integration test that exercises a synthetic burst → idle → burst sequence to verify both the live rate and the freeze. ~60 LOC.
So realistic total is 150–200 LOC vs 10–15 LOC for option (1) — roughly 10× the work for arguably more accurate UX.
My honest take: option (1) is the better default for a one-shot batch sync tool. The user's question after a sync is usually "how fast did that transfer go?" — for which a stable streaming-window average is more readable than a flickering instant rate. Option (2)'s jitter is more useful for tools you watch continuously (curl, rsync). Land (1) now, leave (2) for later if "I want it to match my network monitor live" turns out to be a real recurring desire.
Want me to do (1)? It's a 5-minute change.
Pushed (no force needed since this is a fast-forward). Re-run with --progress and the rate displayed during the streaming phase should match what your network tool shows much more closely — it's now (latest_bytes − oldest_bytes_within_2s_window) / window_duration, so it converges to the wire rate within ~2 seconds of streaming starting and tracks short-term variation as packs flush.
Behavior summary:
- Active transfer: rolling window (instant rate), ticks at 200 ms with a 2 s window
- Side goes idle for >750 ms (or final frame): switches to the active-window average and adds the ✓ marker — that's the "how fast was this transfer?" headline you keep in scrollback
- End-of-run
throughput:line: still the active-window average — appropriate for the post-mortem summary
Two caveats to flag, since (2) trades stability for responsiveness:
- Jitter: pack flushes are bursty. You'll see the live number bounce around — typical range is ±20% within the window. If it gets distracting we can layer EWMA smoothing on top (~15 LOC).
- Wire-vs-payload gap: the network monitor still counts TCP/TLS framing and HTTP headers; our counter sees HTTP body bytes. So expect git-sync's number to read maybe 5–10% lower than your monitor even at peak, even though the streaming-phase math is now correct.
ok, works thanks, next issue, it's still splitting:
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 524288000 --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git time=2026-05-04T22:13:47.067+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-04T22:13:47.067+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-04T22:13:47.067+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-04T22:13:47.623+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=2682 estimated_batches=1 time=2026-05-04T22:13:47.623+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=1 resume_hash=<zero> time=2026-05-04T22:13:47.623+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 from=<zero> to=072f0f7a source: Enumerating objects: 64696, done. source: Counting objects: 100% (5308/5308), done. source: Compressing objects: 100% (238/238), done. time=2026-05-04T22:13:48.151+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48522000 target_limit_bytes=524288000 time=2026-05-04T22:14:03.203+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48522000 target_limit_bytes=524288000 sent_bytes=526909452 will_subdivide=true error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a2b084d234f3e-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" time=2026-05-04T22:14:03.203+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=1 new_remaining=4 sent_bytes=526909452 limit_bytes=524288000 factor=3 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a2b084d234f3e-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" target rejected pack (target limit 500 MB) — splitting 1 → 4 packs time=2026-05-04T22:14:03.203+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=4 from=<zero> to=c57d0306 source: Enumerating objects: 48115, done. source: Counting objects: 100% (9281/9281), done. source: Compressing objects: 100% (911/911), done. time=2026-05-04T22:14:04.591+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=4 estimated_bytes=36086250 target_limit_bytes=524288000 time=2026-05-04T22:14:20.837+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=4 estimated_bytes=36086250 target_limit_bytes=524288000 sent_bytes=527958028 will_subdivide=true error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a2b6f08924f3e-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" time=2026-05-04T22:14:20.837+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=4 new_remaining=8 sent_bytes=527958028 limit_bytes=524288000 factor=3 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a2b6f08924f3e-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" target rejected pack (target limit 500 MB) — splitting 4 → 8 packs time=2026-05-04T22:14:20.837+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 from=<zero> to=94ab79ab source: Enumerating objects: 45345, done. source: Counting objects: 100% (8615/8615), done. source: Compressing objects: 100% (877/877), done. time=2026-05-04T22:14:22.412+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 estimated_bytes=34008750 target_limit_bytes=524288000 time=2026-05-04T22:14:34.208+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 estimated_bytes=34008750 target_limit_bytes=524288000 sent_bytes=527958028 will_subdivide=true error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a2bde68414f3e-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" time=2026-05-04T22:14:34.208+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=8 new_remaining=16 sent_bytes=527958028 limit_bytes=524288000 factor=3 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a2bde68414f3e-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" target rejected pack (target limit 500 MB) — splitting 8 → 16 packs time=2026-05-04T22:14:34.209+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 from=<zero> to=9e4e6195 source: Enumerating objects: 40767, done. source: Counting objects: 100% (8835/8835), done. source: Compressing objects: 100% (734/734), done. time=2026-05-04T22:14:34.929+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 estimated_bytes=30575250 target_limit_bytes=524288000 github.com → 1.51 GB @ 20.0 MB/s │ 1.51 GB @ 20.0 MB/s → 8b04592ed74a5cc…cloudflare.net (pack 1/16)
The smart subdivision is doing what it's designed to do, but the design itself is leaving signal on the table. Look at this pattern:
| iteration | sent_bytes | factor |
|---|---|---|
| 1 pack | 526 MB | 3 |
| 4 packs | 528 MB | 3 |
| 8 packs | 528 MB | 3 |
| 16 packs | (in flight) | … |
sent_bytes is essentially constant at ~528 MB every round. That's because Cloudflare cuts the connection at its body cap, so what we observe is always ≈ limit, regardless of how big the pack actually is. With safety multiplier 2, that always yields ceil(528 × 2 / 500) = 3 — a useless number. We learn nothing across rounds.
The fundamental issue: sent_bytes ≈ limit only tells us "the pack is at least limit-sized". The real pack could be 2×, 5×, 20× the limit. Without knowing, halving (subdivideCheckpoints doubles count per call) is conservative; combined with factor=3 it produces 1 → 4 → 8 → 16 → 32 with one round per server rejection.
There's an extra wrinkle: your repo is blob-heavy in early commits. Sub-pack 1/4 has 48,115 objects covering 670 commits; sub-pack 1/8 still has 45,345 objects. So early commits are massively heavy and even halving the commit range barely halves the byte size.
Two complementary fixes I'd suggest:
-
Bump the safety multiplier when
sent_bytesis at the cap. If we observe a capped failure (sent ≈ limit), the real size is unknown but most likely much bigger. Use 4× instead of 2×, so factor becomes ~5 and we jump 1 → 8 in one round instead of 1 → 4. Saves a round per rejection. -
Calibrate
bytesPerObjectfrom observed data. After each failure, computesent_bytes / pack_object_countas a lower bound. Use that calibrated ratio (with safety) in the pre-flightcheckPackSizeAndSubdivideso a too-big sub-pack is caught and split before upload, not after another wasted ~528 MB transfer. This is what avoids the wasted source fetches.
(1) is a 5-line change. (2) is ~30 LOC. Want me to do both? I'd recommend yes — alone, (1) saves ~1 round; combined with (2) you'd likely converge in 1-2 rounds instead of 5+, and skip the wasted uploads entirely.
Both branches synced. Final state:
-
soph/smart-subdivision— single commitd239127"Use observed pack size to size post-rejection subdivisions" containing all three pieces of the smart-subdivision system:packReadCounterfor measuring real bytes sentobservedSubdivisionFactorwith adaptive 4× multiplier whensent_bytes ≈ limitcalibrateBytesPerObjectratchets the per-object byte estimate up after each rejection so subsequent pre-flight checks catch oversized sub-packs before the wasted upload
-
soph/progress-indicators— 14 commits (headd670885), rebased on top of the updated smart-subdivision. All conflicts resolved keeping smart-subdivision's stronger semantics (objectCount return, calibrated estimate) while folding in the progress branch's notice-and-log additions.
What you should see on a re-run with the same Cloudflare repo:
- First push: 64,696 objects, default
bytesPerObject=750→ estimated 48 MB, push, fail at 528 MB. - Calibration kicks in:
2 × 528 MB / 64696 ≈ 17 KiB/object. Logged asobserved_bytes_per_object=17115. - Aggressive factor (1):
sent ≈ limit→ 4× multiplier →factor=5→ subdivide jumps1 → 8in one round (instead of1 → 4). - Pre-flight now catches oversized sub-packs (2): each sub-pack peek uses calibrated 17 KiB/object. A sub-pack with 45,000 objects projects to ~770 MB → triggers pre-emptive subdivide before upload.
You should converge in 1–2 rejections instead of 4–5, and the wasted upload roundtrips drop accordingly.
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 524288000 --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git time=2026-05-04T22:46:40.426+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-04T22:46:40.426+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-04T22:46:40.426+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-04T22:46:40.768+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=2682 estimated_batches=1 time=2026-05-04T22:46:40.768+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=1 resume_hash=<zero> time=2026-05-04T22:46:40.768+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 from=<zero> to=072f0f7a source: Enumerating objects: 64696, done. source: Counting objects: 100% (5308/5308), done. source: Compressing objects: 100% (238/238), done. time=2026-05-04T22:46:41.255+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48522000 object_count=64696 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-04T22:46:55.750+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=48522000 target_limit_bytes=524288000 sent_bytes=526909452 object_count=64696 will_subdivide=true error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a5b33eb3d77fb-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" time=2026-05-04T22:46:55.750+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=750 observed_bytes_per_object=16288 sent_bytes=526909452 object_count=64696 time=2026-05-04T22:46:55.752+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=1 new_remaining=8 sent_bytes=526909452 limit_bytes=524288000 factor=5 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a5b33eb3d77fb-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" target rejected pack (target limit 500 MB) — splitting 1 → 8 packs time=2026-05-04T22:46:55.752+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 from=<zero> to=94ab79ab source: Enumerating objects: 45345, done. source: Counting objects: 100% (8615/8615), done. source: Compressing objects: 100% (877/877), done. time=2026-05-04T22:46:57.333+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=8 new_remaining=16 estimated_bytes=738579360 calibrated_bytes_per_object=16288 estimated pack ~704 MB exceeds target limit 500 MB — splitting 8 → 16 packs (~44.0 MB each) time=2026-05-04T22:46:57.333+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 from=<zero> to=9e4e6195 source: Enumerating objects: 40767, done. source: Counting objects: 100% (8835/8835), done. source: Compressing objects: 100% (734/734), done. time=2026-05-04T22:46:57.933+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=16 new_remaining=32 estimated_bytes=664012896 calibrated_bytes_per_object=16288 estimated pack ~633 MB exceeds target limit 500 MB — splitting 16 → 32 packs (~19.8 MB each) time=2026-05-04T22:46:57.933+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 from=<zero> to=8daffc0d source: Enumerating objects: 30936, done. source: Counting objects: 100% (5229/5229), done. source: Compressing objects: 100% (669/669), done. time=2026-05-04T22:46:58.526+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 estimated_bytes=503885568 object_count=30936 target_limit_bytes=524288000 calibrated_bytes_per_object=16288 time=2026-05-04T22:47:11.025+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 estimated_bytes=503885568 target_limit_bytes=524288000 sent_bytes=529530892 object_count=30936 will_subdivide=true error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a5b9fde8177fb-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" time=2026-05-04T22:47:11.025+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=16288 observed_bytes_per_object=34233 sent_bytes=529530892 object_count=30936 time=2026-05-04T22:47:11.025+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=32 new_remaining=64 sent_bytes=529530892 limit_bytes=524288000 factor=5 error="target receive-pack: http 413: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a5b9fde8177fb-TXL, Server=cloudflare, Content-Type=text/html; charset=UTF-8] <html>\r\n<head><title>413 Payload Too Large</title></head>\r\n<body>\r\n<center><h1>413 Payload Too Large</h1></center>\r\n<hr><center>cloudflare</center>\r\n</body>\r\n</html>" target rejected pack (target limit 500 MB) — splitting 32 → 64 packs time=2026-05-04T22:47:11.025+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=64 from=<zero> to=0dd6a75e source: Enumerating objects: 28324, done. source: Counting objects: 100% (5316/5316), done. source: Compressing objects: 100% (624/624), done. time=2026-05-04T22:47:12.226+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=64 new_remaining=128 estimated_bytes=969615492 calibrated_bytes_per_object=34233 estimated pack ~925 MB exceeds target limit 500 MB — splitting 64 → 128 packs (~7.22 MB each) time=2026-05-04T22:47:12.226+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=128 from=<zero> to=ac6b0be6 source: Enumerating objects: 20777, done. source: Counting objects: 100% (3485/3485), done. source: Compressing objects: 100% (655/655), done. time=2026-05-04T22:47:13.482+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=128 new_remaining=256 estimated_bytes=711259041 calibrated_bytes_per_object=34233 estimated pack ~678 MB exceeds target limit 500 MB — splitting 128 → 256 packs (~2.65 MB each) time=2026-05-04T22:47:13.482+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 from=<zero> to=0c1a210d source: Enumerating objects: 12876, done. source: Counting objects: 100% (2621/2621), done. source: Compressing objects: 100% (504/504), done. time=2026-05-04T22:47:20.882+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 estimated_bytes=440784108 object_count=12876 target_limit_bytes=524288000 calibrated_bytes_per_object=34233 source: Total 12876 (delta 2240), reused 2117 (delta 2117), pack-reused 10255 (from 1) time=2026-05-04T22:47:36.923+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 estimated_bytes=440784108 target_limit_bytes=524288000 sent_bytes=291630438 object_count=12876 will_subdivide=false error="target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a5c2b99d077fb-TXL, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error" github.com → 1.26 GB @ 22.1 MB/s ✓ │ 1.26 GB @ 20.8 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f6a5c2b99d077fb-TXL, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error exit status 1
Good news first: the smart subdivision is working as designed. Look at the trajectory:
| round | packs | trigger |
|---|---|---|
| 1 | 1 → 8 | 413, factor=5 (4× multiplier) + calibration 750 → 16,288 |
| 2 | 8 → 16 | pre-flight (738 MB est. > 500 MB) — no wasted upload |
| 3 | 16 → 32 | pre-flight (664 MB est. > 500 MB) — no wasted upload |
| 4 | 32 → 64 | 413 again, calibration 16,288 → 34,233 |
| 5 | 64 → 128 | pre-flight (970 MB est.) |
| 6 | 128 → 256 | pre-flight (711 MB est.) |
| 7 | HTTP 500 at 291 MB sent (well under the 524 MB limit) |
That's 4 source re-fetches with wasted uploads (1, 4 — the 413s) plus 4 pre-flight subdivisions with no wasted uploads. Versus the old "halve on every rejection" path which would have been ~8 wasted ~500 MB uploads. So fixes 1+2 saved ≥ 4 × 500 MB = 2 GB of wasted bandwidth and ~2 minutes of clock time in this run.
The 500 is a different failure mode entirely. Critical detail: sent_bytes=291630438, well under target_limit_bytes=524288000. That's not a body-size cutoff — Cloudflare let us send 291 MB and then said "Internal Server Error". Two telling things:
- The push made it to
Total 12876 (delta 2240), reused 2117 (delta 2117), pack-reused 10255 (from 1)— the source actually finished sending the pack metadata, so the negotiation succeeded and we were streaming pack body when the 500 hit. - The 500's response body is just
Internal Server Error(not the HTML 413 page from before). Different code path on Cloudflare's side.
Possible causes (no way to confirm without Cloudflare logs):
- Cloudflare per-request timeout (we'd been pushing for 16 s when it 500'd)
- Cloudflare git backend choking on this specific pack content
- Accumulated server-side state from many prior failed pushes (the temp ref
refs/gitsync/bootstrap/heads/entire/checkpoints/v1might be in a weird state)
What you can try right now:
- Lower
--target-max-pack-bytesto e.g. 200 MB. Smart subdivision picks proportionally smaller packs from the start, the 1/256-shaped 291 MB push doesn't get attempted, and you might dodge whatever the 500 threshold is. - Retry once — could be transient. The
Cf-Ray=9f6a5c2b99d077fb-TXLis in your error message; if it persists you can paste that to Cloudflare support and they can check what their backend hit. - Clean up the temp ref on the target manually before re-running, in case stale state is in play.
What we could add to the code:
- Bounded retry on 5xx with exponential backoff, separate from the body-size path. Maybe 3 attempts spaced 1s/4s/16s. ~40 LOC. Catches transient infra blips without masking real bugs.
- Treat HTTP 500 + Server=cloudflare as a body-size signal. Risky — would also retry/subdivide on real backend failures. Don't recommend.
Want me to add the bounded 5xx retry, or first try --target-max-pack-bytes 200000000 to see if it dodges the 500 entirely?
question on the splitting: We looked at what the remote told us about size, then guessed what a reduction would bring and in the case of the cli-checkpoints repo the size distribution is not even at all and we added some improvements how to catch this.
Now I'm wondering: we are also streaming the data through us, couldn't we investigate the data flowing through? And for example look at how many objects are really in the packfile "flowing by" could that help in some way?
Good intuition — yes, the streaming data has more signal than we currently use. Three levels of usefulness, increasing in cost:
Level 1: count objects sent so far (lightweight parse).
The pack format is [12-byte header][object*][20-byte SHA1]. Each object is a variable-length type+size header followed by a zlib stream. With a streaming parser that walks object boundaries, we can know "we've fully sent N of the M objects in the header" at any point during the upload. That gives us:
- An observed bytes-per-object for the portion already sent (running calibration mid-stream, not just after a 413)
- A read on the distribution: e.g., "first 30% of objects consumed 70% of bytes" tells us the front of the pack is the heavy part
Cost: ~150 LOC if we hand-roll the parser, or wire up go-git's packfile/parser in observer mode (~50 LOC of glue). Modest zlib CPU overhead per object.
Level 2: mid-stream abort. Once Level 1 says "we've sent X bytes for Y objects, projected total = X × M/Y > limit", we close the request body ourselves before the server cuts us off. Saves the rest of the wasted upload (potentially hundreds of MB) and gives us a clean "we know" signal instead of waiting for the 413. Couples to Level 1 — without per-object accounting we can't project.
Cost: small — needs careful HTTP body-cancellation handling. Maybe 50 LOC.
Level 3: smarter checkpoint placement, not even split. This is the real win for your repo. Right now evenCheckpoints divides the commit chain by count. With observed bytes-per-object data from a previous run (or a probe), we could place checkpoints by cumulative bytes instead — e.g., checkpoints at the 25%, 50%, 75% byte percentiles of the chain, which for cli-checkpoints would put 3 checkpoints in the heavy first quarter and 1 in the lighter back three-quarters. No more "split 256 ways because the front is heavy" pathology.
Cost: most invasive — needs either a pre-fetch probe (measure first, then split) or to retain Level 1 data across a failed→retry cycle and use it to re-plan. ~300–500 LOC.
A cheaper approximation of Level 3 that's easier to ship: on the second push attempt onward, if we observed during the first attempt that the front of the pack is K× heavier than the back, place checkpoints unevenly with that K factor. Our current calibration only learns a global bytes-per-object number; this would be a "front-loaded vs back-loaded" boolean plus a ratio.
My recommendation: start with Level 1 + 2 as one PR (the streaming parser unlocks both early-abort and better calibration). It's incrementally useful and the parser is reusable for Level 3 later. If you do that you'd see your cli-checkpoints run probably converge in 1–2 rejections instead of 4, with each rejection costing ~100 MB of upload instead of ~500 MB.
Level 3 is the structural fix but it's a project. Worth scoping separately if smart subdivision still feels too lossy after 1+2.
Want me to sketch Level 1 + 2 against soph/smart-subdivision so you can read the diff before committing?
for 2: how would we know that, this only works if we know the limitations of the other side, which isn't given, but basically it would use --target-max-pack-bytes as the signal?
Right — you've spotted the dependency. Level 2 is only useful when we have a budget to compare against, and the budget only comes from one of:
--target-max-pack-bytesset by the user. Explicit, known.- A previous 413 parsed via
targetBodyLimit(err)(the regex-extractable "body exceeded size limit N" form) or auto-set todefaultTargetMaxPackBytes(512 MiB) byautoTargetMaxPackBytesafter the first 413. - An observed
sent_bytesfrom a previous capped attempt — i.e. "Cloudflare cut us at 524 MB last time, so set our threshold to 524 × 0.95 ≈ 500 MB next time".
So: first attempt with no flags and no prior context, no Level 2 benefit — same blind hope as today. Once we've been 413'd once or the user set the flag, Level 2 lights up and saves the rest of every subsequent failed upload.
Worth noting (3) specifically: we already track sent_bytes per attempt, so after the first capped failure we know with certainty "the server is going to cut around N bytes" as a floor, even if targetBodyLimit(err) couldn't extract a number. Cloudflare's 413 body is just HTML — no machine-readable limit — but our sent_bytes=526909452 IS the empirical limit. We could store that and use it as a self-imposed early-abort threshold on the next push.
That makes the picture cleaner:
| signal | budget for Level 2 |
|---|---|
--target-max-pack-bytes 524288000 | 524 MB × safety_margin |
| Previous 413 with parseable limit | parsed limit × safety_margin |
| Previous 413 with no parseable limit (Cloudflare case) | observed sent_bytes × safety_margin |
| First push, no prior context | none — Level 2 does nothing |
So Level 2's value is "save bandwidth on attempts 2..N", not on attempt 1. Which is fine — attempt 1 you eat the full Cloudflare-cut amount once to establish the budget, after that every wasted upload shrinks by ~5%.
For your cli-checkpoints scenario: first 413 establishes ~524 MB ceiling. Every subsequent attempt aborts at ~498 MB (95%) instead of waiting for the server to cut at 524 MB. Saves 26 MB per attempt × 4 attempts = ~100 MB. Modest, not the headline number — Level 1 (better calibration) does most of the actual work.
So to answer your question directly: yes, --target-max-pack-bytes is one signal but the more interesting source is previously observed sent_bytes. We don't need the server to tell us; we observed empirically where it cuts. We just have to remember it across attempts.
Here's a draft:
Add --progress for live per-side throughput, plus verbose diagnostics
Builds on top of soph/smart-subdivision. Adds a live two-row progress region for sync/replicate/bootstrap/fetch, with stable end-of-run summary, and meaningfully improves what --verbose shows during batched bootstraps so failure modes are diagnosable.
--progress
- Per-side throughput (source = bytes downloaded, target = bytes uploaded), updated in place via
\r+ ANSI\x1b[2K. - Hostnames extracted from the URLs, apex-preserving truncation when long (
8b04592ed74a5cc…cloudflare.net). - Rolling-window rate (~2 s sliding window) while a side is active, so the displayed number tracks wire throughput instead of being diluted by setup time. Switches to active-window average +
✓once a side goes idle, for a stable post-transfer headline. - Bootstrap activity surfaces inline as
(pack 3/8),(pushing pack),(pushing tags). - Auto-suppressed when stderr isn't a TTY.
End-of-run line under --stats
Same SideBytes data, average over the active window — appropriate for the post-mortem summary.
Coordination with --verbose and sideband progress
Both --verbose slog lines and go-git's sideband (`source: Counting objects:
can you also check the description of https://github.com/entireio/git-sync/pull/38 if it's still up2date
Coordination with --verbose and sideband progress
Both --verbose slog lines and go-git's sideband (source: Counting objects: 89%\r) are routed through the progress reporter when the live ticker is active:
- Permanent lines (slog, subdivision notices) scroll above the live region.
- Sideband
\r-terminated progress occupies a transient row above the ticker and updates in place — not one row per percentage update. - Cursor movement (
\x1b[NA+\x1b[J) repaints the whole region atomically, so the three "writers" (sideband transient / progress / scrolling permanent) never collide. --progressticker also delayed until after auth resolution so agit credential fillprompt isn't clobbered.
Verbose diagnostics during bootstrap
For diagnosing 413-vs-other failure modes with reverse proxies (Cloudflare, etc.):
bootstrap batch push attemptinglog per attempt, carriesestimated_bytes,object_count,target_limit_bytes,calibrated_bytes_per_object.bootstrap batch push failedlog carries the same plussent_bytesand the full HTTP error (status + Cf-Ray + body).- Subdivision events emit a one-line user-facing notice (
target rejected pack (target limit 500 MB) — splitting 4 → 8 packs) routed through the progress reporter so they land cleanly above the ticker.
Risk / scope
- New
--progressflag is opt-in; default behavior unchanged. - Verbose log fields are additive — no existing field semantics changed.
- Modern terminals only — relies on
\x1b[2Kand\x1b[NAcursor escapes. Already gated by TTY check, so non-TTY destinations get clean stderr (no escape sequences). - Stacked on
soph/smart-subdivision— that should land first or be merged with this. The smart-subdivision commit at the base is what makes the verbose diagnostics'calibrated_bytes_per_objectfield meaningful.
Test coverage
internal/syncer/progress_test.go— formatters, sample ring, render/notify clearing, multi-line buffering, transient row updates.internal/syncer/throughput_test.go— end-to-end via the existing httptest fixtures: real bootstrap with--stats+--progresspopulates per-side counters, prints the throughput line, renders both sides on the live ticker.
Want me to trim it further, or this is the right shape?
Add --progress for live per-side throughput, plus verbose diagnostics
Builds on soph/smart-subdivision. Adds a live two-row progress region for sync/replicate/bootstrap/fetch, with stable end-of-run summary, and improves what --verbose shows during batched bootstraps so failure modes are diagnosable.
--progress
- Per-side throughput (source = bytes downloaded, target = bytes uploaded), updated in place via
\r+ ANSI\x1b[2K. - Hostnames extracted from URLs, apex-preserving truncation when long (
8b04592ed74a5cc…cloudflare.net). - Rolling-window rate (~2 s sliding window) while a side is active, so the displayed number tracks wire throughput instead of being diluted by setup time. Switches to active-window average +
✓once a side goes idle. - Bootstrap activity surfaces inline as
(pack 3/8),(pushing pack),(pushing tags). - Auto-suppressed when stderr isn't a TTY.
End-of-run line under --stats
Same SideBytes data, average over the active window — appropriate for the post-mortem summary.
Coordination with --verbose and sideband progress
Both --verbose slog lines and go-git's sideband (source: Counting objects: 89%\r) are routed through the progress reporter when the live ticker is active:
- Permanent lines (slog, subdivision notices) scroll above the live region.
- Sideband
\r-terminated progress occupies a transient row above the ticker and updates in place — not one row per percentage update. - Cursor movement (
\x1b[NA+\x1b[J) repaints the whole region atomically, so the three "writers" (sideband transient / progress / scrolling permanent) never collide. - Ticker start delayed until after auth resolution so a
git credential fillprompt isn't clobbered.
Verbose diagnostics during bootstrap
For diagnosing 413-vs-other failure modes with reverse proxies (Cloudflare, etc.):
bootstrap batch push attemptingper attempt —estimated_bytes,object_count,target_limit_bytes,calibrated_bytes_per_object.bootstrap batch push failedaddssent_bytesand the full HTTP error (status +Cf-Ray+ body).- Subdivision events emit a one-line user-facing notice (
target rejected pack (target limit 500 MB) — splitting 4 → 8 packs) routed through the progress reporter so they land cleanly above the ticker.
can you also check https://github.com/entireio/git-sync/pull/38 if it's still up2date in the description?
The PR description on GitHub is stale — it only describes the original commit (single-multiplier, basic byte counting). The branch has since been amended twice with substantial additions:
- The
subdivideToFactorregression fix (always make forward progress whenfactor ≤ remaining) - Adaptive 4× multiplier when
sent_bytes ≈ limit calibrateBytesPerObject+ threading calibrated value intocheckPackSizeAndSubdivide's pre-flightcheckPackSizeAndSubdividenow also returnsobjectCount
The current commit message on the branch already covers all of this. Suggested updated PR description (drop-in for the GitHub UI):
Use observed pack size to size post-rejection subdivisions
Bootstrap's batched-mode subdivide path used to always halve the remaining checkpoint count after an HTTP 413. For a repo whose real bytes-per-object far exceeds the static estimatedBytesPerObject = 750 heuristic, that forces a long dance — 1 → 2 → 4 → 8 → 16 → 32 sub-packs across five rejected pushes, each preceded by a fresh source fetch. For a ~960 MiB pack against a ~500 MiB body cap, that's five wasted ~500 MiB uploads.
Wrap the pack reader handed to PushPack in a packReadCounter so the loop learns how many bytes actually went up before the cutoff. Three cooperating pieces use that signal:
-
observedSubdivisionFactor— usesbytes_sent ÷ limit(with safety multiplier) to choose how many sub-packs to split into. Whensent_bytes ≈ limit(server cut us off mid-stream — the common reverse-proxy 413 case), the true pack size is unknown but likely much larger than the cap, so the multiplier escalates to 4× to converge in a single round. Otherwise 2× is enough. -
subdivideToFactor— halves at least once per call and keeps going whilecount < target. The unconditional first round matters whenfactor ≤ remaining: each surviving range may still over-shoot, so always making forward progress prevents a hard failure on repeated 413s withsent_bytes ≈ limit. -
checkPackSizeAndSubdividenow takes a calibratedbytesPerObjectinstead of using the static 750-byte default. After each 413,calibrateBytesPerObjectderives2 × sent_bytes ÷ pack_objectsas a pessimistic upper bound and ratchets the running estimate up. Subsequent sub-packs are pre-emptively split when the calibrated estimate exceeds the limit — saving an entire ~limit-sized wasted upload on blob-heavy repos where the default ratio is 10–20× too low.
Falls back to the legacy halving behaviour when no signal is available (sentBytes ≤ 0 or limit ≤ 0). Surfaces sent_bytes, limit_bytes, factor, and calibrated_bytes_per_object in the slog output so verbose runs show the math.
Want me to update the PR description directly via gh pr edit?
ok, everything is merged, now I would like to go back to the discussion earlier about level 1+2 improvement
Both PRs merged to main. Quick recap and plan to make sure we're aligned before I start coding.
Goal: shrink the wasted-upload cost of cli-checkpoints-style failures from ~500 MB per round (current state — wait for server's 413) to ~50 MB per round (early-abort once we project we'll overshoot).
Level 1 — streaming pack parse, "objects-sent so far" signal
Wrap the request body with a parser that tracks object boundaries as bytes flow through. The pack format makes this tractable:
Two implementation paths:
- Hand-rolled minimal parser (~150 LOC): for each object, parse type+size header (1–8 bytes), for ofs/ref-delta consume offset/hash, then drive a
flate.NewReaderuntil EOF to find the object's end. Continue. - Reuse go-git's
format/packfile.Scanner/Parserin observer mode if it supports streaming-with-callback.
I'd start with (1) since we don't need to actually decode object content — only advance through. (2) might pull in more than we need.
Output: per-side counter gains an objectsSent int64 alongside bytes, plus a runningBytesPerObject derivable from the pair.
Level 2 — early abort
Budget comes from one of:
--target-max-pack-bytes(user-supplied)- Parsed limit from a previous 413 (
targetBodyLimit(err)) - Previously observed
sent_bytesfrom this session (the Cloudflare case — server gave us no parseable limit but cut us at N bytes; remember that)
On every Read in the body, check: bytes_sent / (budget × 0.95) > objects_sent / total_objects ⇒ abort. Cancel the request via context/CloseWithError. Server's response (likely 413) is now irrelevant — we already know.
Cost saving: instead of every failed attempt eating the full ~limit-sized upload, we cut at ~95% of budget. Combined with smart subdivision's calibration, every attempt after the first costs ~5% of limit instead of ~100%.
Plan:
- New branch
soph/streaming-pack-parseoffmain. - Commit 1: hand-rolled streaming parser (
packStreamObserveror similar), plumbed into the counting pack reader. ExposesBytesSent+ObjectsSent. Tests against synthetic packs and one realgit http-backendintegration test. - Commit 2: mid-stream abort. Budget tracked across attempts (per-host, in the bootstrap loop). Threshold check on every Read. Tests for budget-met-via-flag, budget-met-via-prior-413, budget-met-via-prior-sent-bytes. Integration test that asserts we abort early on a 413-prone backend.
About 300–400 LOC total + tests, two commits. Ship as one PR.
Sound good? If yes I'll cut the branch and start with the parser.
go-git has exactly what we need: packfile.Scanner (in plumbing/format/packfile/scanner.go). Streaming, sequential, emits HeaderSection then one ObjectSection per object then FooterSection. Reads from any io.Reader.
Integration shape: tee the pack stream — HTTP transport reads from the tee, a goroutine drives Scanner on the other side. Replace packReadCounter (or wrap it) with a packStreamObserver that exposes totalObjects + objectsSent atomics on top of the existing bytes counter.
One concern: Scanner does full zlib decompression of every object (line 432, CopyBufferPool(mw, zr)) because it computes the per-object SHA. We don't need the hash, but we do need to drive zlib forward to find object boundaries (compressed size isn't recorded in the pack format — only uncompressed). So the decompression cost is unavoidable regardless of approach. On modern CPU this is ~200–500 MB/s of zlib throughput; for typical upload speeds it should not bottleneck. We can confirm with a benchmark on a real run.
Pipe back-pressure: io.TeeReader + unbuffered io.Pipe would block the HTTP upload if Scanner falls behind. Mitigation: use a buffered intermediate (small bytes.Buffer ring or bufio.Writer flushing in a goroutine). If the buffer fills we'd rather drop the observation than slow the upload — but Scanner is stateful and can't tolerate dropped bytes mid-object. Realistic plan: use io.TeeReader + io.Pipe and trust that zlib keeps up; add a runtime check that warns if observation lags.
Concrete shape for commit 1:
Plus a small runner goroutine that drives Scanner on the pipe reader and updates atomics. Replace packReadCounter callers with this.
For commit 2 (abort), Read checks shouldAbort() against budget; on yes, return a sentinel error that closes the request body and surfaces a clean "we aborted" signal up to the bootstrap loop, which calibrates from the partial sample and subdivides.
Sound good? Want me to start with commit 1 (parser plumbed, no abort logic yet), then commit 2 (budget + abort)?
Partly — but rolling our own doesn't save as much as you'd hope, because the zlib bit decoding itself is unavoidable to find object boundaries.
Here's the actual breakdown:
What's unavoidable: Each object's compressed length isn't recorded anywhere — pack format relies on zlib being self-terminating. To skip past an object you have to walk its deflate stream until the end-of-block marker. That walk requires bit-level Huffman decoding even if you discard the output. On dynamic-Huffman blocks (which are the common case) you also have to decode the code-length codes first. Maybe 60–70% of full inflation cost.
What Scanner adds on top of that bare minimum:
- Per-object SHA-1 hash of inflated content (line 411–412, line 442–443 in scanner.go). Computed for non-delta objects. ~20–30% extra CPU.
- CRC32 over compressed bytes (line 122 in
NewScanner— theMultiWritertocrc). Cheap but real. - Pack-hash over the whole stream (also in the MultiWriter). Cheap.
- Content buffering for delta objects without a seekable source (line 427–430). Memory, not CPU.
So a bespoke parser would save roughly the per-object hash + the CRC, maybe 25% of Scanner's total CPU. Not the order-of-magnitude win one might hope for — the dominant cost is the zlib walk and that stays.
For a custom parser that walks but doesn't inflate-into-buffers:
That's ~80–120 LOC for the format parsing, and you still pay for flate.NewReader's bit decoding. It's strictly less than Scanner does, but not free.
Honest recommendation: start with packfile.Scanner. Push the actual repo, measure CPU during the upload. Modern zlib does 200–500 MB/s per core; even at 10 MB/s upload there's ~20× CPU headroom. If profiling shows zlib decoding eating into upload throughput, then drop to a custom parser. The benefit is well-defined and the work is bounded, but doing it speculatively is unlikely to pay off.
So my answer: yes, technically you'd need a custom one to even try to skip inflation — but you can't actually skip it, only the metadata around it. I'd treat that as a Phase 3 optimization gated on a measured CPU bottleneck, not a Phase 1 requirement.
ok, let's do it and then we benchmark, I've hit 30MB/s so we should have a good testbed
Branch pushed. Two commits, ~570 LOC including tests:
-
242ad00Stream-parse the pack as it uploads to expose objects-sent counter — replacespackReadCounterwithpackStreamObserverthat tees the upload throughpackfile.Scanneron a goroutine. SurfacesBytes(),ObjectsSent(),TotalObjects(). No behavior change yet, just data — visible in thebootstrap batch push failedslog line asobjects_sentandtotal_objects_in_packfields. 4 unit tests using a synthetic blob/tree/commit pack built viapackfile.NewEncoder. -
19606adAbort uploads early once projected to exceed the target body limit — addsSetAborter()to the observer, plus ashouldAbortPushprojection helper and aselfImposedBudgetlocal inexecuteBatched. Two-regime check: project frombytes × total ÷ sentonce any object is parsed; flatbytes > 95% × budgetfallback otherwise. Floor at 8 MiB. Self-imposed budget ratchets down across attempts using either the parsed 413 limit or the empiricalsentBytescutoff (the only signal Cloudflare gives us). 7 test cases covering each regime + observer abort contract.
For your cli-checkpoints re-run with --target-max-pack-bytes 524288000:
| attempt | what happens (with these changes) |
|---|---|
| 1 | Server cuts at 526 MB → calibrate bytesPerObject 750→16k, selfImposedBudget ratchets down to 526 MB |
| 2+ | Observer aborts at ~10% of upload because projection from bytes × total / sent projects ~700–900 MB. Each failed attempt now costs ~50–80 MB instead of 526 MB. |
For benchmarking — given you can hit 30 MB/s — let me know what numbers you see (especially: time-to-converge, total bytes wasted across rounds, and whether zlib CPU shows up in top while the upload runs). That'll tell us whether Phase 3 (custom format walker to skip Scanner's per-object SHA-1) is worth it.
how do I clear this again: error: sync: sync: list target refs: http 403: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/info/refs?service=git-receive-pack [Cf-Ray=9f7292df78fdc8d5-FRA, Server=cloudflare, Content-Type=text/plain;charset=UTF-8] Invalid or expired token exit status 1
That 403 is Cloudflare saying the token they have for you expired. The token's cached somewhere outside git-sync; need to evict the stale entry so the next run picks up fresh credentials.
Most likely it's in macOS Keychain (the default credential helper on macOS). Two ways to clear:
After erasing, the next git-sync sync … will fall back to either prompting (Username for ...) or whatever helper you have configured next, and the new token will be cached fresh.
Quick check of which helper is actually active:
If you see osxkeychain, the snippet above clears it. If you see something else (store, cache, manager), the erase is git credential-<helper> erase with the same url= stdin.
Also worth checking — git-sync has its own token store under internal/auth/entiredb.go keyed by host. If you ever logged in via the Entire flow, there's a stored token there too. Quickest way to confirm whether that's the source: re-run with --target-token=$NEW_TOKEN (or GITSYNC_TARGET_TOKEN=…) which takes precedence over both helper and DB lookup. If that works, the cached one is the only problem; clean it via Keychain Access. If you want to clear the Entire-side store specifically, the keyring service name is whatever credentialService(host) produces — visible in Keychain Access too as entire://…-prefixed entries.
❯ echo "url=https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git"
| git credential-osxkeychain erase
failed to erase: -1
Found it. The Entire token is stored in macOS Keychain under a service name entire:<host> — not keyed by URL the way git credential-osxkeychain looks things up. That's why git credential-osxkeychain erase returned -1 (it searches by the URL/protocol/host triple, not the service prefix).
Two ways to clear:
Via security CLI (fastest):
If multiple usernames have entries under that service, run it again until you get Could not find item to delete. — that's the cue you've cleared them all. Add -a "<username>" to target one specifically.
Via Keychain Access.app:
- Open Keychain Access
- Search for
entire:8b04592e…cloudflare.net(or justentire:and find the relevant entry) - Delete
Two service entries may exist per host:
entire:<host>— access tokenentire:<host>:refresh— refresh token (line 161 inentiredb.go)
Clear both if you want a fully fresh login:
After that the next sync will need fresh credentials — either via --target-token=$NEW_TOKEN or whatever flow originally seeded these entries.
why would it be an entire credential? this is what I try to run:
go run ./cmd/git-sync sync --target-max-pack-bytes 524288000 --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git
You're right — sorry, I jumped to a wrong conclusion. With no --target-token flag and no GITSYNC_TARGET_TOKEN, git-sync's flow is: explicit auth (none) → Entire DB lookup (also none if you never went through that flow) → git credential fill subprocess. So this is git's credential helper holding a stale entry, not Entire's.
The failed: -1 on your earlier erase is almost certainly because the stdin wasn't terminated correctly — git's credential protocol needs a blank line at the end and echo only adds one newline, not two. Try one of these:
Easiest — go through the configured chain (any helper, by host only):
(Note the blank line before EOF.)
Verify what's actually cached (and which helper has it) before erasing:
That'll print whatever username/password git would feed you next time, and tells you the helper actually has something. If it prints stuff, the reject above clears it. If it prompts you interactively, the cache is already empty and the stale token is somewhere else.
Confirm the active helper:
If it's osxkeychain, your earlier command should have worked with the trailing blank line:
If git credential fill shows nothing cached but the 403 still says "Invalid or expired token", the token is being supplied somewhere else — most likely:
- An entry inside the URL itself in your shell history (e.g.
https://TOKEN@host/...) — but your command showshttps://x@...with no password, so it's not that GITSYNC_TARGET_TOKENenv var set in your shell. Check withenv | grep -i gitsync
Which helper does git config --show-origin --get-all credential.helper print?
git-sync on soph/streaming-pack-parse via 🐹 v1.26.2 ❯ env | grep -i gitsync
git-sync on soph/streaming-pack-parse via 🐹 v1.26.2 ❯ git config --show-origin --get-all credential.helper file:/opt/homebrew/etc/gitconfig osxkeychain
OK so it's definitely going through osxkeychain, but your earlier erase didn't match. Most likely causes:
- Username mismatch — git-sync includes
username=xin the credential request (parsed from your URL'shttps://x@…). The earlierecho "url=…"form expanded to a query withoutusername, and osxkeychain may require matching it. - Internet vs generic password kind —
git credential-osxkeychainstores as Internet Passwords; thesecurity delete-generic-passwordfamily doesn't see those.
First, see what's actually cached:
If that prints back a password=… line, you've found the source. Then erase via git's standard interface:
(reject tells the configured helpers to forget. Note the blank line before EOF — required by git's credential protocol.)
If reject reports nothing useful, go directly via security:
If find-internet-password returns "could not be found", drop -a "x" to search by host only. The host might be stored without the https://x@ username prefix.
After it's clean, git credential fill with the same input should drop you to an interactive prompt instead of silently returning a password — that's the signal the cache is empty.
ok, the protocol in the printf command was it, but now running:
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 524288000 --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-05T22:52:12.333+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-05T22:52:12.333+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-05T22:52:12.333+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-05T22:52:12.724+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=2640 estimated_batches=1 time=2026-05-05T22:52:12.724+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=1 resume_hash=<zero> time=2026-05-05T22:52:12.724+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 from=<zero> to=44b4b3eb source: Enumerating objects: 65463, done. source: Counting objects: 100% (5649/5649), done. source: Compressing objects: 100% (421/421), done. time=2026-05-05T22:52:13.983+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=49097250 object_count=65463 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:15.405+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=49097250 target_limit_bytes=524288000 sent_bytes=8388620 object_count=65463 objects_sent=574 total_objects_in_pack=65463 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:15.405+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=1 new_remaining=2 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 1 → 2 packs time=2026-05-05T22:52:15.405+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2 from=<zero> to=9a18c4d7 source: Enumerating objects: 53304, done. source: Counting objects: 100% (9448/9448), done. source: Compressing objects: 100% (940/940), done. time=2026-05-05T22:52:16.640+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2 estimated_bytes=39978000 object_count=53304 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:18.156+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2 estimated_bytes=39978000 target_limit_bytes=524288000 sent_bytes=8388620 object_count=53304 objects_sent=290 total_objects_in_pack=53304 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:18.156+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=2 new_remaining=4 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 2 → 4 packs time=2026-05-05T22:52:18.156+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=4 from=<zero> to=8caf57f1 source: Enumerating objects: 48044, done. source: Counting objects: 100% (9303/9303), done. source: Compressing objects: 100% (920/920), done. time=2026-05-05T22:52:20.245+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=4 estimated_bytes=36033000 object_count=48044 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:22.120+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=4 estimated_bytes=36033000 target_limit_bytes=524288000 sent_bytes=8388620 object_count=48044 objects_sent=785 total_objects_in_pack=48044 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:22.120+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=4 new_remaining=8 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 4 → 8 packs time=2026-05-05T22:52:22.120+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 from=<zero> to=226c6f1f source: Enumerating objects: 45321, done. source: Counting objects: 100% (8608/8608), done. source: Compressing objects: 100% (881/881), done. time=2026-05-05T22:52:23.593+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 estimated_bytes=33990750 object_count=45321 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:25.115+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 estimated_bytes=33990750 target_limit_bytes=524288000 sent_bytes=8388620 object_count=45321 objects_sent=416 total_objects_in_pack=45321 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:25.115+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=8 new_remaining=16 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 8 → 16 packs time=2026-05-05T22:52:25.115+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 from=<zero> to=815f2cd2 source: Enumerating objects: 39579, done. source: Counting objects: 100% (8439/8439), done. source: Compressing objects: 100% (752/752), done. time=2026-05-05T22:52:25.693+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 estimated_bytes=29684250 object_count=39579 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:27.154+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 estimated_bytes=29684250 target_limit_bytes=524288000 sent_bytes=8388620 object_count=39579 objects_sent=291 total_objects_in_pack=39579 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:27.155+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=16 new_remaining=32 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 16 → 32 packs time=2026-05-05T22:52:27.155+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 from=<zero> to=915074a7 source: Enumerating objects: 30929, done. source: Counting objects: 100% (5227/5227), done. source: Compressing objects: 100% (668/668), done. time=2026-05-05T22:52:27.683+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 estimated_bytes=23196750 object_count=30929 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:29.075+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 estimated_bytes=23196750 target_limit_bytes=524288000 sent_bytes=8388620 object_count=30929 objects_sent=202 total_objects_in_pack=30929 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:29.075+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=32 new_remaining=64 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 32 → 64 packs time=2026-05-05T22:52:29.075+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=64 from=<zero> to=0dd6a75e source: Enumerating objects: 28324, done. source: Counting objects: 100% (5316/5316), done. source: Compressing objects: 100% (624/624), done. time=2026-05-05T22:52:30.205+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=64 estimated_bytes=21243000 object_count=28324 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:31.594+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=64 estimated_bytes=21243000 target_limit_bytes=524288000 sent_bytes=8388620 object_count=28324 objects_sent=417 total_objects_in_pack=28324 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:31.594+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=64 new_remaining=128 sent_bytes=8388620 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 64 → 128 packs time=2026-05-05T22:52:31.594+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=128 from=<zero> to=ac6b0be6 source: Enumerating objects: 20777, done. source: Counting objects: 100% (3485/3485), done. source: Compressing objects: 100% (655/655), done. time=2026-05-05T22:52:32.892+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=128 estimated_bytes=15582750 object_count=20777 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-05T22:52:49.196+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=128 estimated_bytes=15582750 target_limit_bytes=524288000 sent_bytes=92798988 object_count=20777 objects_sent=3871 total_objects_in_pack=20777 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:52:49.196+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=750 observed_bytes_per_object=8932 sent_bytes=92798988 object_count=20777 time=2026-05-05T22:52:49.196+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=128 new_remaining=256 sent_bytes=92798988 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 128 → 256 packs time=2026-05-05T22:52:49.196+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 from=<zero> to=0c1a210d source: Enumerating objects: 12876, done. source: Counting objects: 100% (2621/2621), done. source: Compressing objects: 100% (504/504), done. time=2026-05-05T22:52:57.198+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 estimated_bytes=115008432 object_count=12876 target_limit_bytes=524288000 calibrated_bytes_per_object=8932 time=2026-05-05T22:53:17.519+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 estimated_bytes=115008432 target_limit_bytes=524288000 sent_bytes=113246220 object_count=12876 objects_sent=2917 total_objects_in_pack=12876 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-05T22:53:17.519+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=8932 observed_bytes_per_object=17590 sent_bytes=113246220 object_count=12876 time=2026-05-05T22:53:17.519+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=256 new_remaining=512 sent_bytes=113246220 limit_bytes=524288000 factor=2 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 256 → 512 packs time=2026-05-05T22:53:17.519+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=512 from=<zero> to=142d3bcb source: Enumerating objects: 9483, done. source: Counting objects: 100% (1561/1561), done. source: Compressing objects: 100% (360/360), done. time=2026-05-05T22:53:26.710+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=512 estimated_bytes=166805970 object_count=9483 target_limit_bytes=524288000 calibrated_bytes_per_object=17590 github.com → 368 MB @ 3.61 MB/s ✓ │ 366 MB @ 3.59 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net (pack 1/512)^Csignal: interrupt
Pushed. Re-run the same command — what should change for cli-checkpoints:
| round | what was happening before this fix | what happens now |
|---|---|---|
| 1 | aborted at 8 MiB, factor=2 → 1→2 | aborted at 8 MiB, sizing uses projected ~957 MB → factor=8 → 1→8. Calibration jumps 750→~29K bytes/obj |
| 2+ | doubling each round | Pre-flight now estimates real sizes (29K × object_count) so most sub-packs that are still over the limit get caught in pre-flight, no upload at all |
| final | converged at 512 packs after 9 rounds | converged at the same eventual count, but in fewer rounds and with most "rounds" being source-fetch only (no wasted upload bandwidth) |
What I'd watch for in the trace this run:
- Round 1's
subdividing after target size rejectionline should now showsizing_bytes=~957000000andfactor=8(vs the oldfactor=2). - Round 2's pre-flight subdivision (the
subdividing before push (pack header estimate)line) should fire becausecalibrated_bytes_per_object=29000ish × object_countwill exceed 500 MB. - Total wall-clock should drop substantially since the wasted-upload phase is mostly replaced with cheap source-fetch + pre-flight.
If you still see many doubling rounds, that's a sign we need to also make subdivideToFactor jump more aggressively (e.g., insert N-1 midpoints per range in one call instead of just halving). But let's see the actual numbers first — give it a try.
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 524288000 --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-06T11:06:57.562+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-06T11:06:57.563+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-06T11:06:57.563+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-06T11:06:58.208+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=2682 estimated_batches=1 time=2026-05-06T11:06:58.208+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=1 resume_hash=<zero> time=2026-05-06T11:06:58.208+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 from=<zero> to=907fccff source: Enumerating objects: 65818, done. source: Counting objects: 100% (6004/6004), done. source: Compressing objects: 100% (656/656), done. time=2026-05-06T11:07:01.423+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=49363500 object_count=65818 target_limit_bytes=524288000 calibrated_bytes_per_object=750 time=2026-05-06T11:07:02.972+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1 estimated_bytes=49363500 target_limit_bytes=524288000 sent_bytes=8388620 object_count=65818 objects_sent=574 total_objects_in_pack=65818 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T11:07:02.972+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=750 observed_bytes_per_object=29228 sent_bytes=8388620 calibration_denom=574 object_count=65818 objects_sent=574 time=2026-05-06T11:07:02.972+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=1 new_remaining=8 sent_bytes=8388620 sizing_bytes=961885350 limit_bytes=524288000 factor=8 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 1 → 8 packs time=2026-05-06T11:07:02.973+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8 from=<zero> to=94ab79ab source: Enumerating objects: 45345, done. source: Counting objects: 100% (8615/8615), done. source: Compressing objects: 100% (877/877), done. time=2026-05-06T11:07:05.073+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=8 new_remaining=16 estimated_bytes=1325343660 calibrated_bytes_per_object=29228 estimated pack ~1.23 GB exceeds target limit 500 MB — splitting 8 → 16 packs (~79.0 MB each) time=2026-05-06T11:07:05.073+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=16 from=<zero> to=9e4e6195 source: Enumerating objects: 40767, done. source: Counting objects: 100% (8835/8835), done. source: Compressing objects: 100% (734/734), done. time=2026-05-06T11:07:05.718+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=16 new_remaining=32 estimated_bytes=1191537876 calibrated_bytes_per_object=29228 estimated pack ~1.11 GB exceeds target limit 500 MB — splitting 16 → 32 packs (~35.5 MB each) time=2026-05-06T11:07:05.718+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=32 from=<zero> to=8daffc0d source: Enumerating objects: 30936, done. source: Counting objects: 100% (5229/5229), done. source: Compressing objects: 100% (669/669), done. time=2026-05-06T11:07:06.223+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=32 new_remaining=64 estimated_bytes=904197408 calibrated_bytes_per_object=29228 estimated pack ~862 MB exceeds target limit 500 MB — splitting 32 → 64 packs (~13.5 MB each) time=2026-05-06T11:07:06.223+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=64 from=<zero> to=0dd6a75e source: Enumerating objects: 28324, done. source: Counting objects: 100% (5316/5316), done. source: Compressing objects: 100% (624/624), done. time=2026-05-06T11:07:07.353+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=64 new_remaining=128 estimated_bytes=827853872 calibrated_bytes_per_object=29228 estimated pack ~790 MB exceeds target limit 500 MB — splitting 64 → 128 packs (~6.17 MB each) time=2026-05-06T11:07:07.353+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=128 from=<zero> to=ac6b0be6 source: Enumerating objects: 20777, done. source: Counting objects: 100% (3485/3485), done. source: Compressing objects: 100% (655/655), done. time=2026-05-06T11:07:08.716+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=128 new_remaining=256 estimated_bytes=607270156 calibrated_bytes_per_object=29228 estimated pack ~579 MB exceeds target limit 500 MB — splitting 128 → 256 packs (~2.26 MB each) time=2026-05-06T11:07:08.716+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 from=<zero> to=0c1a210d source: Enumerating objects: 12876, done. source: Counting objects: 100% (2621/2621), done. source: Compressing objects: 100% (504/504), done. time=2026-05-06T11:07:16.497+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 estimated_bytes=376339728 object_count=12876 target_limit_bytes=524288000 calibrated_bytes_per_object=29228 time=2026-05-06T11:07:37.789+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=256 estimated_bytes=376339728 target_limit_bytes=524288000 sent_bytes=113246220 object_count=12876 objects_sent=2917 total_objects_in_pack=12876 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T11:07:37.789+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=29228 observed_bytes_per_object=77645 sent_bytes=113246220 calibration_denom=2917 object_count=12876 objects_sent=2917 time=2026-05-06T11:07:37.789+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=256 new_remaining=512 sent_bytes=113246220 sizing_bytes=499882868 limit_bytes=524288000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 500 MB) — splitting 256 → 512 packs time=2026-05-06T11:07:37.790+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=512 from=<zero> to=142d3bcb source: Enumerating objects: 9483, done. source: Counting objects: 100% (1561/1561), done. source: Compressing objects: 100% (360/360), done. time=2026-05-06T11:07:46.582+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=512 new_remaining=1024 estimated_bytes=736307535 calibrated_bytes_per_object=77645 estimated pack ~702 MB exceeds target limit 500 MB — splitting 512 → 1024 packs (~702 KB each) time=2026-05-06T11:07:46.583+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1024 from=<zero> to=20a8a759 source: Enumerating objects: 5281, done. source: Counting objects: 100% (1235/1235), done. source: Compressing objects: 100% (417/417), done. time=2026-05-06T11:07:57.699+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1024 estimated_bytes=410043245 object_count=5281 target_limit_bytes=524288000 calibrated_bytes_per_object=77645 source: Total 5281 (delta 894), reused 818 (delta 818), pack-reused 4046 (from 1) time=2026-05-06T11:08:31.171+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1024 estimated_bytes=410043245 target_limit_bytes=524288000 sent_bytes=132722203 object_count=5281 objects_sent=5281 total_objects_in_pack=5281 aborted_early=false will_subdivide=false error="target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f76d66dbf38dbf8-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error" github.com → 244 MB @ 2.61 MB/s ✓ │ 243 MB @ 2.38 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f76d66dbf38dbf8-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error exit status 1
Big progress visible — and a new wall to talk about.
What worked:
| before fix | this run | |
|---|---|---|
| Round 1 jump | 1 → 2 | 1 → 8 (factor=8 from sizing_bytes=961M) |
| Calibration after abort | no change (8M÷65k = 256 < 750) | 750 → 29 KiB/obj (using objectsSent=574) |
| Rounds 2–6 | full upload + abort each round | pre-flight catches all 5 — no wasted upload, ~1 second each |
| Total wall clock to converge to 256 packs | ~30+ s | ~10 s |
The streaming observer + projection-based factor + objectsSent calibration combo did exactly what we wanted. After the first round, calibration jumps to 29 K/obj and the next 5 subdivisions all happen in pre-flight (no upload). At round 256 we actually push, learn the front is even heavier than 29 K (it's 77 K), recalibrate, and pre-flight catches 512 → 1024 again.
What hit a wall: at 1024 packs / 5,281 objects / 132 MB pushed:
objects_sent == total_objects_in_pack means the entire pack uploaded successfully (132 MB, well under our 500 MB cap). Cloudflare accepted the body, then returned 500 from their backend processing. Same failure mode you and I hit earlier — not a body-size issue, something Cloudflare's git server is choking on for this specific commit range (from=<zero> to=20a8a759).
We can't fix Cloudflare from here, but two paths forward in our code:
-
Bounded retry on 5xx (~30 LOC). Maybe 3 attempts with 1s/4s/16s backoff. Catches transient infra blips. Doesn't help if the 500 is reproducible on this exact pack content (likely the case here).
-
Force smaller packs from the start. Pass
--target-max-pack-bytes 100000000(100 MB) — the first pre-flight estimate (65k × 750 = 49 MB) would still pass, but after the first abort calibration → 29 K, projection → 957 MB, and factor against the 100 MB limit = ceil(957 × 4 / 100) = 39, so we'd jump 1 → 64 in one round. Sub-packs would be ~10–20 MB on average. Maybe small enough that Cloudflare's 500 zone is below the noise. -
Investigate which object causes the 500. Cf-Ray=
9f76d66dbf38dbf8-FRA— if you have any way to pull Cloudflare logs for that ID, the response body before the truncation might say more thanInternal Server Error. The commit range<zero>→20a8a759covers the first few commits ofentire/checkpoints/v1; the 500 is reproducible on that range. If yougit ls-tree -r 20a8a759and find any unusual file (huge blob, weird name), that's a hypothesis for what Cloudflare can't ingest.
My honest take: option 2 (--target-max-pack-bytes 50000000 even, 50 MB) is the cheapest test. If a 50 MB sub-pack of the same commit range still 500s, the issue is specific content, not size. If it succeeds, the issue is size threshold and we just want even smaller packs.
What do you want to try?
source: Enumerating objects: 5279, done. source: Counting objects: 100% (1234/1234), done. source: Compressing objects: 100% (416/416), done. time=2026-05-06T11:24:04.890+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2681 estimated_bytes=133178612 object_count=5279 target_limit_bytes=50000000 calibrated_bytes_per_object=25228 time=2026-05-06T11:24:06.308+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2681 estimated_bytes=133178612 target_limit_bytes=50000000 sent_bytes=8388620 object_count=5279 objects_sent=597 total_objects_in_pack=5279 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T11:24:06.308+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=25228 observed_bytes_per_object=28102 sent_bytes=8388620 calibration_denom=597 object_count=5279 objects_sent=597 github.com → 17.8 MB @ 287 KB/s ✓ │ 16.0 MB @ 258 KB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack": round trip: pack upload aborted early: projected to exceed target body limit exit status 1
Two observations from this trace:
1. The 50 MB limit is below the floor. Look at batch_total=2681 — that's the chain length (2,682 commits, 2,681 checkpoints), meaning every remaining range is already a single commit. Can't subdivide further. Yet the first 1-commit sub-pack still projects to 70 MB (5,279 objects × ~14 KB observed). One commit alone can't fit under 50 MB. That's almost certainly the initial commit, which carries the entire repo state at import time — its tree + every blob reachable from it.
So 50 MB is below the natural floor for this repo. The minimum viable --target-max-pack-bytes is around 70-80 MB just for the initial commit, and pre-flight is doing the right thing rejecting it — but the loop has no further move.
A small UX gap I should fix while we're here: when subdivision can't proceed, the error message is the generic "pack upload aborted early" instead of the actually informative "chain exhausted, single commit too heavy for limit X". Cheap to add.
2. The earlier 1024-pack run got the whole pack up at 132 MB and Cloudflare returned 500 anyway. Re-reading that line: objects_sent=5281, total_objects_in_pack=5281, aborted_early=false — full upload received, then 500 from the backend. Different failure mode from a body-size cutoff.
So you're caught between two walls:
- below ~80 MB: can't fit the initial commit
- above some threshold near 132 MB-ish: Cloudflare's backend 500s (content-dependent, not size-dependent based on the evidence)
The window between them, if any, is what would let this repo through.
Concrete things to try, ordered by effort:
-
--target-max-pack-bytes 100000000(100 MB). Sweet spot guess. Initial commit (~70 MB) fits, and most chunks stay below the 132 MB threshold that triggered the 500. -
Same with
120000000. If 100 MB still has heavy sub-packs that bunch into 100+ MB regions and we end up doing too much subdivision. -
If both still 500: it's content, not size. The Cf-Ray (
9f76d66dbf38dbf8-FRAfrom the earlier run) is the only artifact you can hand to Cloudflare support to learn what their backend rejected. -
Bounded retry on 5xx in our code (~30 LOC). Helps transient blips; doesn't help if the same pack content reliably 500s.
I'd also like to land a small follow-up commit on this branch:
- Distinguish "chain exhausted" from generic abort error. When
subdivideToFactorreturns the input unchanged ANDsizeIssueis true, surface a notice like "cannot subdivide further: single-commit pack projects to ~70 MB > target limit 50 MB; raise --target-max-pack-bytes" and exit with that message instead of the generic round-trip error. Helps anyone who hits this floor.
Want me to add that error-clarification commit, then you try 100 MB? Or skip and just try 100 MB first?
time=2026-05-06T12:48:06.654+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2684 estimated_bytes=332957088 object_count=5279 target_limit_bytes=100000000 calibrated_bytes_per_object=63072 time=2026-05-06T12:48:08.854+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=2684 estimated_bytes=332957088 target_limit_bytes=100000000 sent_bytes=11534348 object_count=5279 objects_sent=625 total_objects_in_pack=5279 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" github.com → 20.8 MB @ 316 KB/s ✓ │ 19.0 MB @ 289 KB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack": round trip: pack upload aborted early: projected to exceed target body limit exit status 1
I start to feel this is fishy... like why would the initial commit so big, can we just check that maybe? repo is @../cli-checkpoints
Mystery solved — and your fishy instinct was right. The "initial commit" isn't big at all:
The size is in the second commit, which is a merge:
That second-parent (a9cfa80b) carries 5,279 objects on its own. The merge "Merge remote session logs" pulls them all into reachability. So when bootstrap fetches from=<zero> to=chain[1], the source sends those 5,279 objects.
This is what the trace object_count=5279 is — not the initial commit, but the first big merge that imports the session-log corpus. Compressed wire size ~132 MB.
The structural limit our bootstrap hits: we only place checkpoints on the first-parent chain. So the smallest possible sub-pack is "everything chain[K] adds over chain[K-1]" — and for chain[1] that's the entire session-log import in one pack. We can't subdivide inside a single first-parent commit's contribution.
That's why your runs converged to 2,684 packs (one per first-parent commit, exhausting the chain) but the first pack still couldn't be made smaller than ~133 MB — it carries that side-branch in its entirety.
So the actual floor for --target-max-pack-bytes on this repo is ~140 MB, and your Cloudflare 500 wall sits very close to that (132 MB push got 500, 113 MB push aborted before 500). There isn't a workable window with the current checkpointing strategy.
Two real fixes:
-
Checkpoint on second-parent ancestry too — when the first-parent chain produces a too-heavy step, walk into the second parent and place sub-checkpoints there. This is genuinely the structural fix for merge-heavy repos like cli-checkpoints. ~200-400 LOC change in
planCheckpointsFromChain/subdivideCheckpoints. Not trivial — has to keep the temp-ref machinery sensible — but well-defined. -
Skip the merge boundary entirely — push commits in full topo order instead of first-parent-only. Each commit becomes a potential checkpoint, including side-branch commits. More commits → more granular splitting. The downside is git-sync's whole bootstrap design is currently first-parent based; this is a deeper change.
For right now: the only thing that'd let your existing run succeed is if Cloudflare can actually accept a 140-150 MB pack reliably. Earlier you saw 132 MB get a 500 on a different commit range — but it's worth one more try at --target-max-pack-bytes 150000000 to confirm whether 140-ish MB is the wall or whether the previous 500 was specific to one pack's content.
Want me to take a stab at (1)?
2 makes sense if I do a full copy of everything anyhow? Or is the risk just more changes in the code base but otherwise fine?
Yes, for bootstrap specifically (empty target, one-shot copy), topo order is functionally equivalent — the final state is identical to first-parent, intermediate state isn't observable to anyone (we push to a temp ref and only create the real branch ref at the end), and total bytes transferred are the same. The risks are mostly volume-related, not correctness:
Real concerns:
-
3× more source fetches: cli-checkpoints has 2,681 first-parent commits but 8,331 topo commits. Each checkpoint = one source fetch. For your repo that's ~5,650 extra fetches — each cheap (small delta, fast HTTP round trip), but the connection setup overhead adds up. Probably 2–3× total wall-clock time for the source side, even if individual packs are now small enough to push without subdivision dance.
-
Source-side enumeration cost: each
git-upload-packenumerates reachability for the wants. 3× more fetches = 3× more enumeration work on the source. For huge repos (linux-kernel scale), this could be a real load issue. Less concern for cli-checkpoints sized things. -
Resume logic: temp ref currently advances along first-parent. With topo, it'd advance through side branches. If the bootstrap is interrupted mid-side-branch and re-run, we need the topo ordering to be deterministic across runs (so we can pick up where we left off). Git's topo order should be deterministic for a fixed graph, but the existing resume logic has assumptions about first-parent linearity that'd need an audit.
-
Code volume + tests: ~300-500 LOC across
planCheckpointsFromChain, the inner-loop commit picker, resume detection. New test scenarios for merge-heavy repos. Not trivial.
No real correctness risks I can see for bootstrap. The "we push side-branch commits via the temp ref before the user-facing ref exists" is invisible — temp refs are namespaced under refs/gitsync/bootstrap/... and nothing observes them; the real branch ref only gets created at the end pointing to the actual chain tip.
Recommendation — middle path: keep first-parent as the default and add topo as an opt-in or auto-fallback when first-parent produces a single sub-pack that exceeds the target limit (the case you just hit). Two lines of work:
- Phase A: implement topo-mode planning behind a flag (
--bootstrap-strategy=topo). Lets you unblock cli-checkpoints today, validates the design, doesn't risk regressions for repos where first-parent works fine. - Phase B: detect at planning time when a first-parent step is structurally too big (i.e., its second-parent ancestry contributes more objects than
target-max-pack-bytes / calibrated-bytes-per-objectcould fit) and auto-fall-back to topo for that segment. The other segments stay first-parent.
Phase A alone unblocks you. Phase B turns it from a manual escape hatch into automatic behavior.
I'd suggest Phase A first as a one-PR fix — much smaller (~150 LOC: replace the chain source in planCheckpointsFromChain when the flag is set, plus a flag on Config and a CLI option). Phase B is a follow-up.
Want me to do Phase A?
Branch pushed: https://github.com/entireio/git-sync/pull/new/soph/topo-bootstrap
What you should see when you re-run with --bootstrap-strategy=topo:
chain_len=8331instead of2682— the full topo chain.- First sub-pack:
from=<zero> to=792d7e0d(the true root with the empty tree) — only 2 objects. Then each sub-pack covers a small delta. - No more 5,279-object first sub-pack: the side-branch commits become individual checkpoints, so the heavy "Merge remote session logs" content is split across ~505 sub-packs naturally.
- More source fetches (~3× more checkpoints), but each fetch is small. Total transfer time should be similar; per-pack size stays under the limit.
The non-topo path is untouched — passing nothing or --bootstrap-strategy=first-parent keeps the historical behavior. Tests in internal/planner/planner_test.go cover the topological invariant (parents before children), the stop-set behavior, and determinism (same input → same chain order, required for resume).
Give it a try and let me know what you see. If 8,331 commits across the wire is too slow for source-side enumeration, the next step would be Phase B (auto-fall-back to topo only when first-parent produces a too-heavy step).
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 10000000 --bootstrap-strategy=topo --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-06T15:05:50.748+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-06T15:05:50.748+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-06T15:05:50.749+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-06T15:05:51.087+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=8475 estimated_batches=56 time=2026-05-06T15:05:51.087+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=56 resume_hash=941e69ae time=2026-05-06T15:05:51.087+02:00 level=INFO msg="bootstrap batch resuming from stale temp ref" branch=refs/heads/entire/checkpoints/v1 resume_hash=941e69ae remaining_commits=8459 new_batches=56 time=2026-05-06T15:05:51.087+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=56 from=941e69ae to=9ca3c51e source: Enumerating objects: 641, done. source: Counting objects: 100% (175/175), done. source: Compressing objects: 100% (87/87), done. time=2026-05-06T15:05:51.475+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=56 estimated_bytes=480750 object_count=641 target_limit_bytes=10000000 calibrated_bytes_per_object=750 source: Total 641 (delta 141), reused 88 (delta 88), pack-reused 466 (from 1) time=2026-05-06T15:05:57.246+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=56 time=2026-05-06T15:05:57.246+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=56 from=9ca3c51e to=5f1dc391 source: Enumerating objects: 100, done. source: Counting objects: 100% (24/24), done. source: Compressing objects: 100% (8/8), done. time=2026-05-06T15:05:57.651+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=56 estimated_bytes=75000 object_count=100 target_limit_bytes=10000000 calibrated_bytes_per_object=750 source: Total 100 (delta 17), reused 16 (delta 16), pack-reused 76 (from 1) time=2026-05-06T15:05:58.449+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=56 time=2026-05-06T15:05:58.449+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=56 from=5f1dc391 to=05fa8394 source: Enumerating objects: 3244, done. source: Counting objects: 100% (736/736), done. source: Compressing objects: 100% (309/309), done. time=2026-05-06T15:06:14.475+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=56 estimated_bytes=2433000 object_count=3244 target_limit_bytes=10000000 calibrated_bytes_per_object=750 source: Total 3244 (delta 534), reused 427 (delta 427), pack-reused 2508 (from 1) time=2026-05-06T15:06:37.045+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=56 estimated_bytes=2433000 target_limit_bytes=10000000 sent_bytes=83834245 object_count=3244 will_subdivide=false error="target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f78337888d4dbf8-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error" github.com → 90.1 MB @ 1.96 MB/s ✓ │ 88.5 MB @ 1.67 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f78337888d4dbf8-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error exit status 1
Big result from this trace — let me unpack:
Topo strategy works. Look at the logs:
chain_len=8475(vs2682for first-parent) — full topo chain with all side-branch commits.estimated_batches=56— the planner respects your tight 10 MB limit and divides 8,475 commits into 56 manageable checkpoints (~150 commits each).- Resume worked:
resuming from stale temp ref... resume_hash=941e69ae. A prior run had successfully pushed up to941e69ae; this run's topo walk found that exact commit's position in the rebuilt chain and picked up from there. The deterministic hash-tie-broken ordering paid off. - Batches 1 and 2 succeeded — 641 objects then 100 objects, both well under 10 MB.
The 500 at batch 3 is the same Cloudflare wall as before. Two telling things:
- Real upload reached 84 MB before the 500. With
target_limit_bytes=10000000(10 MB) we should have aborted at ~9.5 MB. But this branch (soph/topo-bootstrap) was forked offmain— it doesn't have the streaming-pack-parse aborter from the other branch. So we have no early-cut and just pushed until Cloudflare 500'd. will_subdivide=falsebecause the existing post-rejection logic only fires on body-size errors (413), not 500s.
In other words: topo got us much further (we landed 2 batches successfully where before nothing landed), but batch 3 hit the same content-or-size 500 that's been blocking you all along.
Two real next steps:
-
Combine the branches — rebase
soph/topo-bootstrapon top ofsoph/streaming-pack-parseso you have both the topo planning and the streaming aborter together. With the aborter, batch 3's 84 MB push would self-cut at ~9.5 MB, calibrate (real bytes/obj is ~26 KB, way over the static 750), then subdivide-within-topo to land smaller sub-packs. That should get past the 84 MB wall by never reaching it. -
Investigate the 500 itself. Cf-Ray
9f78337888d4dbf8-FRAis repeatable — if you have any way to ping Cloudflare support with that ID, it's the only path to learning why their backend 500s on this content. Without that we're working blind.
Want me to do (1) — rebase topo-bootstrap on top of streaming-pack-parse so you can test the combined behavior in one branch?
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 10000000 --bootstrap-strategy=topo --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-06T15:15:58.172+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-06T15:15:58.172+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-06T15:15:58.172+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-06T15:15:58.541+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=8475 estimated_batches=56 time=2026-05-06T15:15:58.541+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=56 resume_hash=5f1dc391 time=2026-05-06T15:15:58.541+02:00 level=INFO msg="bootstrap batch resuming from stale temp ref" branch=refs/heads/entire/checkpoints/v1 resume_hash=5f1dc391 remaining_commits=8157 new_batches=54 time=2026-05-06T15:15:58.541+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=54 from=5f1dc391 to=05fa8394 source: Enumerating objects: 3244, done. source: Counting objects: 100% (736/736), done. source: Compressing objects: 100% (309/309), done. time=2026-05-06T15:16:13.508+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=54 estimated_bytes=2433000 object_count=3244 target_limit_bytes=10000000 calibrated_bytes_per_object=750 time=2026-05-06T15:16:14.982+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=54 estimated_bytes=2433000 target_limit_bytes=10000000 sent_bytes=8388620 object_count=3244 objects_sent=407 total_objects_in_pack=3244 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T15:16:14.982+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=750 observed_bytes_per_object=41221 sent_bytes=8388620 calibration_denom=407 object_count=3244 objects_sent=407 time=2026-05-06T15:16:14.982+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=54 new_remaining=108 sent_bytes=8388620 sizing_bytes=66861629 limit_bytes=10000000 factor=27 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 9.54 MB) — splitting 54 → 108 packs time=2026-05-06T15:16:14.983+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=108 from=5f1dc391 to=60b18081 source: Enumerating objects: 2428, done. source: Counting objects: 100% (716/716), done. source: Compressing objects: 100% (263/263), done. time=2026-05-06T15:16:24.743+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=108 new_remaining=216 estimated_bytes=100084588 calibrated_bytes_per_object=41221 estimated pack ~95.4 MB exceeds target limit 9.54 MB — splitting 108 → 216 packs (~452 KB each) time=2026-05-06T15:16:24.743+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=216 from=5f1dc391 to=0d760ab3 source: Enumerating objects: 2051, done. source: Counting objects: 100% (606/606), done. source: Compressing objects: 100% (222/222), done. time=2026-05-06T15:16:34.542+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=216 new_remaining=432 estimated_bytes=84544271 calibrated_bytes_per_object=41221 estimated pack ~80.6 MB exceeds target limit 9.54 MB — splitting 216 → 432 packs (~191 KB each) time=2026-05-06T15:16:34.542+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=432 from=5f1dc391 to=b489c3db source: Enumerating objects: 1856, done. source: Counting objects: 100% (538/538), done. source: Compressing objects: 100% (191/191), done. time=2026-05-06T15:16:43.336+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=432 new_remaining=864 estimated_bytes=76506176 calibrated_bytes_per_object=41221 estimated pack ~73.0 MB exceeds target limit 9.54 MB — splitting 432 → 864 packs (~86.5 KB each) time=2026-05-06T15:16:43.336+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=864 from=5f1dc391 to=87140486 source: Enumerating objects: 71, done. source: Counting objects: 100% (33/33), done. source: Compressing objects: 100% (16/16), done. time=2026-05-06T15:16:43.619+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=864 estimated_bytes=2926691 object_count=71 target_limit_bytes=10000000 calibrated_bytes_per_object=41221 source: Total 71 (delta 24), reused 17 (delta 17), pack-reused 38 (from 1) time=2026-05-06T15:16:47.588+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=864 time=2026-05-06T15:16:47.588+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=864 from=87140486 to=b489c3db source: Enumerating objects: 1856, done. source: Counting objects: 100% (538/538), done. source: Compressing objects: 100% (191/191), done. time=2026-05-06T15:16:56.380+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=863 new_remaining=1726 estimated_bytes=76506176 calibrated_bytes_per_object=41221 estimated pack ~73.0 MB exceeds target limit 9.54 MB — splitting 863 → 1726 packs (~43.3 KB each) time=2026-05-06T15:16:56.380+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=1727 from=87140486 to=824dae38 source: Enumerating objects: 32, done. source: Counting objects: 100% (18/18), done. source: Compressing objects: 100% (12/12), done. time=2026-05-06T15:16:56.590+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=1727 estimated_bytes=1319072 object_count=32 target_limit_bytes=10000000 calibrated_bytes_per_object=41221 source: Total 32 (delta 11), reused 6 (delta 6), pack-reused 14 (from 1) time=2026-05-06T15:16:57.262+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=1727 time=2026-05-06T15:16:57.262+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=1727 from=824dae38 to=b489c3db source: Enumerating objects: 1856, done. source: Counting objects: 100% (538/538), done. source: Compressing objects: 100% (191/191), done. time=2026-05-06T15:17:06.027+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=1725 new_remaining=3450 estimated_bytes=76506176 calibrated_bytes_per_object=41221 estimated pack ~73.0 MB exceeds target limit 9.54 MB — splitting 1725 → 3450 packs (~21.7 KB each) time=2026-05-06T15:17:06.027+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=3452 from=824dae38 to=b2709d31 source: Enumerating objects: 18, done. source: Counting objects: 100% (4/4), done. source: Compressing objects: 100% (3/3), done. time=2026-05-06T15:17:06.246+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=3452 estimated_bytes=741978 object_count=18 target_limit_bytes=10000000 calibrated_bytes_per_object=41221 source: Total 18 (delta 2), reused 1 (delta 1), pack-reused 14 (from 1) time=2026-05-06T15:17:06.425+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=3452 time=2026-05-06T15:17:06.425+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=3452 from=b2709d31 to=b489c3db source: Enumerating objects: 1856, done. source: Counting objects: 100% (538/538), done. source: Compressing objects: 100% (191/191), done. time=2026-05-06T15:17:14.913+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=3449 new_remaining=6898 estimated_bytes=76506176 calibrated_bytes_per_object=41221 estimated pack ~73.0 MB exceeds target limit 9.54 MB — splitting 3449 → 6898 packs (~10.8 KB each) time=2026-05-06T15:17:14.913+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=6901 from=b2709d31 to=746c37b0 source: Enumerating objects: 7, done. source: Counting objects: 100% (4/4), done. source: Compressing objects: 100% (4/4), done. time=2026-05-06T15:17:15.363+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=6901 estimated_bytes=288547 object_count=7 target_limit_bytes=10000000 calibrated_bytes_per_object=41221 source: Total 7 (delta 0), reused 0 (delta 0), pack-reused 3 (from 1) time=2026-05-06T15:17:15.474+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=6901 time=2026-05-06T15:17:15.474+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=6901 from=746c37b0 to=b489c3db source: Enumerating objects: 1856, done. source: Counting objects: 100% (538/538), done. source: Compressing objects: 100% (191/191), done. time=2026-05-06T15:17:24.060+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=6897 new_remaining=8141 estimated_bytes=76506176 calibrated_bytes_per_object=41221 estimated pack ~73.0 MB exceeds target limit 9.54 MB — splitting 6897 → 8141 packs (~9.18 KB each) time=2026-05-06T15:17:24.060+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=8145 from=746c37b0 to=fe84051a source: Enumerating objects: 1847, done. source: Counting objects: 100% (535/535), done. source: Compressing objects: 100% (198/198), done. time=2026-05-06T15:17:32.517+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=8145 estimated_bytes=76135187 object_count=1847 target_limit_bytes=10000000 calibrated_bytes_per_object=41221 time=2026-05-06T15:17:34.047+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=8145 estimated_bytes=76135187 target_limit_bytes=10000000 sent_bytes=8388620 object_count=1847 objects_sent=248 total_objects_in_pack=1847 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T15:17:34.047+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=41221 observed_bytes_per_object=67650 sent_bytes=8388620 calibration_denom=248 object_count=1847 objects_sent=248 github.com → 19.8 MB @ 194 KB/s ✓ │ 18.0 MB @ 176 KB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack": round trip: pack upload aborted early: projected to exceed target body limit exit status 1
Big progress — looks like you've combined the branches and the smart abort + topo are now working together. Watch the trajectory:
| step | what happened |
|---|---|
Resume from 5f1dc391 (last successful push from prior run) ✓ | |
| Batch 1/54 | Aborted at 8 MB / 407 obj → calibrated 750→41,221 → factor=27 → 54 → 108 |
| 108 → 216 → 432 → 864 | All caught pre-flight with new calibration, no wasted upload |
| Batch 1/864 | 71 objects → push complete |
| 864 → 1726 → 3450 → 6898 → 8141 | More pre-flight catches |
| Batches 1, 2, 3, 4 | All pushed successfully (71 / 32 / 18 / 7 objects each) |
| Batch 5/8145 | 1,847 objects, real ~76 MB, chain exhausted — abort, can't subdivide further |
The streaming abort + topo combination is doing exactly what we wanted. Total wasted upload across this whole run: ~20 MB. Compared to GBs in earlier runs.
The new floor: a single merge commit (fe84051a) brings in 1,847 new objects (~76 MB at ~41 KB/obj calibrated). With --target-max-pack-bytes 10000000, that one commit can't possibly fit — and the chain has no finer-grained slices than "one commit at a time".
Two practical paths:
-
Raise the limit to fit the heaviest single-commit delta. Try
--target-max-pack-bytes 100000000(100 MB). 76 MB clears, but earlier we saw Cloudflare 500 around 84 MB. Close but might work because the content hitting 500 may have been specific to the larger first-parent packs we tried — at 100 MB with topo's narrower per-commit packs we might never trigger it. Worth trying. -
Accept the floor: the smallest workable limit for this repo is around the size of the heaviest single-commit content delta. For cli-checkpoints that's ~76 MB based on what we just observed. Set the limit to something like 90-120 MB and live with it.
Two small UX improvements I'd add to the code while we're here:
-
Clearer "chain exhausted" error. Right now batch 5 fails with
pack upload aborted early: projected to exceed target body limit— same wording whether subdivision could continue or not. When chain is exhausted, surface something like "cannot subdivide further: smallest possible pack at this checkpoint is ~76 MB, exceeds the 9.5 MB target limit. Increase --target-max-pack-bytes." Cheap and informative. -
Log the offending checkpoint hash so you can
git log -1 fe84051ato see what the merge contains and decide if it's content you can change at source.
Want me to land both UX improvements? They're maybe 30 LOC, no behavior change.
I'm trying to parse this: wasn't the idea with the topo strategy that we could walk merge commits and split them? Or did I get that wrong?
You got it right in intent; I overpromised in the mechanism. Let me be honest about what topo actually does vs. what's still missing.
What topo does fix: it puts every reachable commit (including side-branch commits) on the chain in topological order, so checkpoints can land between any two adjacent commits — not just first-parent steps. That's a real win and you saw it: 8,475 chain steps instead of 2,682, batches 1–4 succeeded, you walked through the side branch.
What topo doesn't fix — and what trips batch 5: when we push want=chain[K] have=chain[K-1], we send the source one have. The source sends every object reachable from chain[K] that's not reachable from that single have. For a merge commit chain[K] whose two parents are on different ancestries, declaring only chain[K-1] as a have means the source has to send back the other parent branch's missing ancestry — even though we already pushed it earlier in the topo walk.
In your batch 5:
fe84051a is a merge. 746c37b0 is the previous topo commit — probably one of fe84051a's parent branches' tips. The other parent branch's ancestry hasn't been declared as a have on this fetch (it was pushed in earlier batches but the temp ref doesn't remember). So source has to send all 1,847 objects of "fe84051a's first-parent ancestry that isn't reachable from 746c37b0".
The target's object store already has those objects from earlier pushes. We're paying to send them again because we only carry one have hash.
The structural fix (call it Phase A.5 or Phase B):
Track all successfully pushed checkpoint hashes and pass them as haves on each fetch. Git protocol supports many haves per request — typical clients send up to 256. The source enumerates backward from wants and stops at any have it reaches, so a merge commit whose both parents are in the haves set delivers a minimal delta (just the merge commit + its tree).
That's ~50 LOC: the bootstrap loop already tracks current (most recent push) and completedRefs (completed branch tips). Adding a sliding window of recent checkpoint pushes — say last N or all of them, capped — and threading them through packReaderForCheckpoint's haves construction would close this gap.
With that change, your batch 5 would:
- Receive haves = {746c37b0, …, all earlier topo checkpoints}
- Source enumerates
fe84051a→ walks parents → hits a have on the first-parent side at the previously-pushed commit, stops - Pack contains just
fe84051a+ its (small) merge tree + nothing else - Fits trivially under your 10 MB limit
So the real story: topo + smart abort got us to a single-commit-delta floor, but for merges we're still paying for unrelated-ancestry retransmission because we only declare one have. Tracking many haves is what actually completes the picture.
Want me to add multi-have tracking? It's a clean follow-up — same branch or new commit on top of what's there.
This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.
Summary:
-
Primary Request and Intent:
- The user started by asking about adding progress display with data throughput to git-sync (a Go CLI tool that mirrors git repos over HTTP).
- This evolved into a long iteration on UX (live ticker, hostnames, separators, sliding-window rates, sideband coordination, verbose log layering).
- Then into a structural problem: target servers (specifically Cloudflare's git endpoint at
8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git) reject large pack pushes with HTTP 413 / 500. User wanted to makegit-sync syncofhttps://github.com/entireio/cli-checkpoints.git(branchentire/checkpoints/v1) succeed. - User explicitly requested: smart subdivision based on observed pack size; streaming pack parser to count objects-sent; mid-stream early abort using a budget; topo-order bootstrap strategy as Phase A of merge-aware planning; meaningful commits; PR-style branches stacked on each other; a clear PR description.
- Most recent intent: user is asking whether multi-have tracking (the structural fix to make merges actually small under topo) belongs in the topo strategy PR or is a separate change.
-
Key Technical Concepts:
- Git smart-HTTP protocol v2:
info/refs,git-upload-pack,git-receive-pack - Pack format:
[12-byte PACK header][object*][20-byte SHA1]. Per-object: variable-length type+size header, optional OFS/REF delta reference, zlib-compressed body. Compressed object size is NOT recorded; object boundaries can only be found by walking the zlib stream. - go-git v6 (
v6.0.0-alpha.2):packfile.Scanner(sequential PackData iterator: HeaderSection, ObjectSection, FooterSection),packfile.NewEncoder,pktline,transport,plumbing. - go-git v6
Hashtype: 32-byte ID, hasCompare(other.Bytes()) intmethod (no direct indexing — early lint error). - First-parent chain vs topological order: first-parent walks only
commit.ParentHashes[0], topo includes every reachable commit with parents-before-children invariant. - Pack size estimation:
objectCount × bytesPerObjectheuristic, default 750 bytes/obj is wildly off for blob-heavy repos. - Mid-stream abort via
io.TeeReader+io.Pipe+ goroutine-drivenScanner. zlib decoding cost (~200-500 MB/s/core) is unavoidable to find object boundaries; only per-object SHA-1 is skippable savings. - Calibration:
2 × sentBytes / objectsObservedas pessimistic upper bound on bytes/object. - Subdivision factor from projection:
bytes × totalObjects / objectsSentprojects full-pack size;observedSubdivisionFactor(projected, limit)with adaptive multiplier (2× when sent < 90% of limit, 4× when at-cap). - macOS Keychain credential storage: git uses Internet Passwords;
git credential rejectwith blank-line-terminated stdin to evict. - Cloudflare git endpoint behaviors: 413 with HTML body (no parseable size limit), 500 sometimes content-dependent. Body cap observed at ~524 MB.
- Resume via temp ref:
refs/gitsync/bootstrap/heads/<branch>advances along chain; deterministic chain ordering required so resume position is findable on the next run. - Commit graph: cli-checkpoints has 2,682 first-parent commits but 8,331 topo commits (5,650 are second-parent ancestors of "Checkpoint: <hash>" merges).
- Git smart-HTTP protocol v2:
-
Files and Code Sections:
-
internal/strategy/bootstrap/bootstrap.go(touched on multiple branches)- On
soph/streaming-pack-parse: replacedpackReadCounterwithpackStreamObserver; addedselfImposedBudgetlocal; addedshouldAbortPushhelper; refined error path to handleabortedEarlysymmetrically toisTargetBodyLimitError; addedsizing_bytes(use projection when aborted_early) for factor calculation; calibration now usesobjectsSentas denominator when smaller than full pack count. - On
soph/topo-bootstrap: addedStrategy stringfield toParamswith comment about first-parent vs topo. InplanCheckpointsFromChain, dispatch based on strategy:
- On
-
internal/strategy/bootstrap/pack_observer.go(new on streaming-pack-parse)packStreamObserverwraps the request body, exposes atomicBytes(),ObjectsSent(),TotalObjects(), plusAborted()flag andSetAborter(func(bytes,sent,total int64) bool). Tees source toio.Pipe; goroutine runspackfile.NewScanner(pr)and updates atomics on HeaderSection/ObjectSection.ErrPackUploadAbortedsentinel returned fromReadonce the aborter triggers; observer keeps refusing further bytes after that.
-
internal/planner/checkpoint.go(extended on topo-bootstrap)- Added
TopoChainStoppingAt(store, tip, stopAt): BFS reachability collection, then Kahn's algorithm with hash-tie-brokenreadyqueue for deterministic emission. HelperappendSortedHashkeeps the queue sorted in O(n) per insertion. hashLess(a, b plumbing.Hash) bool { return a.Compare(b.Bytes()) < 0 }(had to fix froma[i] < b[i]because Hash is a struct, not array).
- Added
-
internal/syncer/syncer.go- Added
BootstrapStrategy stringtoConfig. - Added
Strategy: s.cfg.BootstrapStrategyto thebstrap.Executecall.
- Added
-
unstable/client.go- Added
BootstrapStrategy string(withomitempty) toAdvancedOptions. Plumbed throughbuildSyncConfigandbuildBootstrapConfig.
- Added
-
cmd/git-sync/bootstrap.goandcmd/git-sync/syncplan.go- Added
--bootstrap-strategyflag with description:checkpoint chain ordering: "first-parent" (default) or "topo". Use "topo" for merge-heavy repos where individual first-parent steps drag in unboundedly large side branches.
- Added
-
internal/planner/planner_test.go(new tests)TestTopoChainStoppingAtIncludesSideBranches— verifies parents-before-children with a small merge graph.TestTopoChainStoppingAtSkipsStopSet.TestTopoChainStoppingAtDeterministic— important for resume.
-
-
Errors and fixes:
Blob.Writer undefinedin test fixture: switched toobj.Writer()on the EncodedObject directly.- Conflict markers from rebase: resolved via Python scripts merging HEAD's smart-subdivision semantics with progress branch's UX additions, preserving smart-subdivision's
pushErrvariable name and 3-return-valuecheckPackSizeAndSubdividesignature. subdivideToFactorregression flagged by reviewer: original code returned input unchanged whenlen(remaining) >= targetCount, turning recoverable 413 withfactor=2andremaining=2into hard failure. Fixed to always callsubdivideCheckpointsonce unconditionally before checking the loop guard.- Cannot index
plumbing.Hashlint error: fixedhashLessto useHash.Compare(Bytes())method. - Static
factor=2every round on capped servers: fixed by computingsizingBytesfrom projection (bytesSent × totalObjects / objectsSent) whenabortedEarly, not rawsentBytes. - Calibration not improving (
8M / 65k = 256 < 750): fixed by dividing byobjectsSent(574) instead of fullpackObjectCount(65k) when the parser observed only the front of the pack. echo "url=..." | git credential-osxkeychain erasereturned-1: missing trailing blank line; protocol/host/username request viagit credential rejectwith HEREDOC and blank line worked.- User correction on Entire credentials: I incorrectly suggested clearing
entire:<host>keychain entries. User pointed out they had no Entire credential — it was a regulargit credential fillcache. I apologized and gave the correct path. - User pushback on topo claim: user noticed batch 5 still failed with merge bringing 1847 objects, and asked "wasn't the idea with the topo strategy that we could walk merge commits and split them?" I admitted I had overpromised: topo gives more granular checkpoints but doesn't fix the underlying issue that fetches use a single
have(the most recent temp ref position), so a merge commit's "delta" still includes the whole ancestry of the parent branch we didn't declare as a have, even though we already pushed it.
-
Problem Solving:
- Solved: live progress display with hostnames + arrows + separator, sliding-window rate, sideband coordination, idle freeze, done marker, in-place transient row, ANSI clear escapes, TTY gating, two-row live region.
- Solved: smart subdivision with calibration, projection-based factor, adaptive multiplier (2× / 4× at-cap), early abort via
packStreamObserver+shouldAbortPush, ratchet-downselfImposedBudget. - Solved: topo chain walk with deterministic ordering,
--bootstrap-strategyplumbing. - Outstanding: cli-checkpoints still hits a wall at the heaviest single-commit delta (~76 MB for a merge bringing in side-branch ancestry). Topo gets us to per-commit granularity, but a single merge can't be split. Cloudflare 500 happens above ~84 MB regardless, so the workable window is narrow.
- Identified root cause: each fetch declares only the most recent temp-ref commit as a have, so the source has to re-send the entire other-parent ancestry of a merge even though it was already pushed in earlier topo iterations.
- Proposed fix (
Phase A.5/B): track a sliding window or full set of successfully-pushed checkpoint hashes and pass them as multiple haves on each fetch. Source-side enumeration stops at any have, so a merge commit's delta becomes minimal.
-
All user messages:
- Initial: "Is there an easy way to show progress including data throughput? or is this a huge effort?"
- "Yeah I want both, ideally also separated for source and target..."
- "yes" (to design proposal)
- "can you do meaningful commits?"
- "go run cmd/git-sync/main.go: undefined: newRootCmd" (followed by trace)
- "❯ go run ./cmd/git-sync sync --progress https://github.com/entireio/cli.git ..." (Cloudflare auth/progress collision)
- "I'm more concerned about the collision, we should fix that somehow"
- "can we do: github.com -> 168 MB @ 4.35 MB/s · 168 MB ... → host names" + "something better than '·'"
- "let's do the separator you suggested and the hostname thing"
- Trace showing rate degrading: "1. once the transfer was done... rate started dropping. 2. ... bigger repo... can we surface which packfile we are currently sending?"
- Trace showing splitting: "this went from without to 1/2 and now 1/4 maybe we can add a log line above"
- "can we also output the what the pack size now is?"
- Cloudflare trace + question on garbled output
- "you would need to add more to verbose?"
- "[Request interrupted] sorry, continue and add those, but also make sure --verbose works with the progress things"
- "but could we not make it so it's updating in a single line?"
- Trace showing slow rate: "I have a network stats tool... it instantly says 44MB/s up/down each, but the git-sync output slowly moves up from 13 to 27 but never gets past that"
- "how much work is 2?"
- "let's do 2"
- Trace with subdivisions: "ok, works thanks, next thing: it's still splitting"
- "any idea why this is happening: ... Username for ... entire/checkpoints/v1: no source refs matched"
- "can you do meaningful commits?" (for progress branch)
- "this is from a run before... 1. once transfer done rate started dropping 2. surface which packfile"
- User asked about git push --mirror + GIT_TRACE_CURL diagnostics
- "that is too much tracing"
- Cloudflare trace with diagnostic verbose output
- "I start to feel this is fishy... like why would the initial commit so big, can we just check that maybe? repo is @../cli-checkpoints"
- "2 makes sense if I do a full copy of everything anyhow? Or is the risk just more changes in the code base but otherwise fine?"
- "yes, let's do Phase A"
- Trace with first topo run, batch 3 failing with 500
- Trace with combined topo + streaming-pack-parse, batch 5 failing because chain exhausted
- Most recent: "wasn't the idea with the topo strategy that we could walk merge commits and split them? Or did I get that wrong?"
- Final: "would it belong to the topo strategy?"
- Many iteration requests, PR description requests, commit splitting, force-pushes, rebase requests interspersed.
-
Pending Tasks:
- Implement multi-have tracking in bootstrap loop so merge commits' deltas only include genuinely-new content. Track set of pushed checkpoint hashes; pass them all (capped) as haves to
packReaderForCheckpoint. ~50 LOC. The user's most recent question is whether this should land onsoph/topo-bootstrapor as a separate change. - Optional UX improvement (offered, awaiting user decision): clearer error message when subdivision can't proceed because chain is exhausted. Surface the offending checkpoint hash so user can
git log -1 <hash>to inspect.
- Implement multi-have tracking in bootstrap loop so merge commits' deltas only include genuinely-new content. Track set of pushed checkpoint hashes; pass them all (capped) as haves to
-
Current Work:
- The user just ran the combined topo + streaming-pack-parse code (somehow merged on their end) against cli-checkpoints with
--target-max-pack-bytes 10000000 --bootstrap-strategy=topo. Batches 1-4 succeeded with calibration ratcheting (750 → 41,221 → 67,650 bytes/obj). Batch 5 failed withaborted_early=true, will_subdivide=truebut couldn't actually subdivide because chain was exhausted (old_remaining=8141, new_remaining=8141effectively). - I explained that the trace shows topo + streaming abort working together but the real missing piece is multi-have: when pushing
from=746c37b0 to=fe84051a(a merge), source has to send 1,847 objects because we only declared746c37b0as a have — the merge's other parent's ancestry (already pushed in earlier topo iterations) gets retransmitted. - I proposed multi-have tracking as Phase A.5 / B with this design: maintain a sliding window of pushed checkpoint hashes and thread them through
packReaderForCheckpoint's haves. - User's response: "would it belong to the topo strategy?" — a scoping question about whether to land multi-have on
soph/topo-bootstrapor as a separate change.
- The user just ran the combined topo + streaming-pack-parse code (somehow merged on their end) against cli-checkpoints with
-
Optional Next Step: Answer the user's scoping question in plain text. The user asked: "would it belong to the topo strategy?" referring to multi-have tracking. The answer should weigh:
- Coupling: topo exposes the issue more visibly than first-parent (because topo creates more merge boundaries) but multi-have helps both strategies.
- Reviewability: topo PR with multi-have is a larger change but presents a coherent merge-handling story; separated PRs are smaller but the topo PR is incomplete-feeling without multi-have for repos like cli-checkpoints.
- The user's repo specifically: topo without multi-have is incomplete — they can't actually finish the sync. So pragmatically they belong together for the user's use case.
No next code action without the user's confirmation on scoping. From the most recent exchange, the literal user message is just: "would it belong to the topo strategy?" — which calls for an opinion-based answer, not new code, before I touch anything.
If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/soph/.claude/projects/-Users-soph-Work-entire-devenv-git-sync/3ee1ca7a-a436-44c1-906a-a912c6d33f96.jsonl Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.
Multi-have belongs with the topo PR for your repo, but the cleaner framing is: it's a separate, sequential change that should land before you'd consider topo "done."
Here's the reasoning:
Why they're coupled in practice Topo without multi-have is what you just hit: granular checkpoints, but a single merge still drags its other-parent's full ancestry. For cli-checkpoints specifically, topo alone can't finish the sync. So if the topo PR's value prop is "merge-heavy repos succeed where first-parent gives up," it's incomplete without multi-have.
Why they're separable in code
- Topo strategy = chain ordering. ~150 LOC, self-contained in
planner+ a strategy switch. - Multi-have = fetch-side haves. ~50 LOC threading a slice through
packReaderForCheckpoint. Helps first-parent too — any time a checkpoint commit's first parent isn't the immediately-prior checkpoint, multi-have shrinks the delta.
What I'd actually do
Land them as a stack: soph/topo-bootstrap → soph/multi-have on top. Topo PR description honestly says "exposes more merge boundaries; depends on multi-have to finish merge-heavy syncs." Multi-have PR is small and reviewable on its own merits. The combined effect is what makes cli-checkpoints work.
If you'd rather just bundle them on soph/topo-bootstrap, that's also defensible — the PR description becomes "merge-aware bootstrap (Phase A: topo + multi-have)" and reviewers see one coherent story. The downside is a bigger diff.
My vote: stack them. Want me to start the multi-have branch on top of soph/topo-bootstrap?
I just have already: soph/streaming-pack-parse -> soph/topo-bootstrap so now do one more on top? but yeah fine with me
Branch soph/multi-have is now stacked on soph/topo-bootstrap with one commit (d719fc1). Build, vet, lint, and existing tests all pass. Want me to push it and open a draft PR against soph/topo-bootstrap, or hold off until you've tested locally against cli-checkpoints?
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 19000000 --bootstrap-strategy=topo --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-06T15:50:08.410+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-06T15:50:08.410+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-06T15:50:08.410+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-06T15:50:08.730+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=8478 estimated_batches=30 time=2026-05-06T15:50:08.730+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=30 resume_hash=746c37b0 time=2026-05-06T15:50:08.730+02:00 level=INFO msg="bootstrap batch resuming from stale temp ref" branch=refs/heads/entire/checkpoints/v1 resume_hash=746c37b0 remaining_commits=8144 new_batches=29 time=2026-05-06T15:50:08.730+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=29 from=746c37b0 to=000769e0 source: Enumerating objects: 4517, done. source: Counting objects: 100% (1046/1046), done. source: Compressing objects: 100% (442/442), done. time=2026-05-06T15:50:33.260+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=29 estimated_bytes=3387750 object_count=4517 target_limit_bytes=19000000 calibrated_bytes_per_object=750 time=2026-05-06T15:50:34.928+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=29 estimated_bytes=3387750 target_limit_bytes=19000000 sent_bytes=8388620 object_count=4517 objects_sent=521 total_objects_in_pack=4517 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T15:50:34.928+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=750 observed_bytes_per_object=32201 sent_bytes=8388620 calibration_denom=521 object_count=4517 objects_sent=521 time=2026-05-06T15:50:34.928+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=29 new_remaining=58 sent_bytes=8388620 sizing_bytes=72728208 limit_bytes=19000000 factor=16 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 18.1 MB) — splitting 29 → 58 packs time=2026-05-06T15:50:34.928+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=58 from=746c37b0 to=19d3253b source: Enumerating objects: 3299, done. source: Counting objects: 100% (753/753), done. source: Compressing objects: 100% (309/309), done. time=2026-05-06T15:50:50.412+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=58 new_remaining=116 estimated_bytes=106231099 calibrated_bytes_per_object=32201 estimated pack ~101 MB exceeds target limit 18.1 MB — splitting 58 → 116 packs (~894 KB each) time=2026-05-06T15:50:50.412+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=116 from=746c37b0 to=1a600530 source: Enumerating objects: 2543, done. source: Counting objects: 100% (750/750), done. source: Compressing objects: 100% (271/271), done. time=2026-05-06T15:51:00.224+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=116 new_remaining=232 estimated_bytes=81887143 calibrated_bytes_per_object=32201 estimated pack ~78.1 MB exceeds target limit 18.1 MB — splitting 116 → 232 packs (~345 KB each) time=2026-05-06T15:51:00.224+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=232 from=746c37b0 to=989051a7 source: Enumerating objects: 2185, done. source: Counting objects: 100% (656/656), done. source: Compressing objects: 100% (239/239), done. time=2026-05-06T15:51:09.858+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=232 new_remaining=464 estimated_bytes=70359185 calibrated_bytes_per_object=32201 estimated pack ~67.1 MB exceeds target limit 18.1 MB — splitting 232 → 464 packs (~148 KB each) time=2026-05-06T15:51:09.858+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=464 from=746c37b0 to=cb4143b8 source: Enumerating objects: 2009, done. source: Counting objects: 100% (594/594), done. source: Compressing objects: 100% (217/217), done. time=2026-05-06T15:51:19.629+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=464 new_remaining=928 estimated_bytes=64691809 calibrated_bytes_per_object=32201 estimated pack ~61.7 MB exceeds target limit 18.1 MB — splitting 464 → 928 packs (~68.1 KB each) time=2026-05-06T15:51:19.629+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=928 from=746c37b0 to=c10a2ed0 source: Enumerating objects: 1922, done. source: Counting objects: 100% (562/562), done. source: Compressing objects: 100% (203/203), done. time=2026-05-06T15:51:29.291+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=928 new_remaining=1856 estimated_bytes=61890322 calibrated_bytes_per_object=32201 estimated pack ~59.0 MB exceeds target limit 18.1 MB — splitting 928 → 1856 packs (~32.6 KB each) time=2026-05-06T15:51:29.291+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=1856 from=746c37b0 to=4323ced9 source: Enumerating objects: 1878, done. source: Counting objects: 100% (548/548), done. source: Compressing objects: 100% (198/198), done. time=2026-05-06T15:51:38.326+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=1856 new_remaining=3712 estimated_bytes=60473478 calibrated_bytes_per_object=32201 estimated pack ~57.7 MB exceeds target limit 18.1 MB — splitting 1856 → 3712 packs (~15.9 KB each) time=2026-05-06T15:51:38.326+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=3712 from=746c37b0 to=b489c3db source: Enumerating objects: 1856, done. source: Counting objects: 100% (538/538), done. source: Compressing objects: 100% (191/191), done. time=2026-05-06T15:51:47.769+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=3712 new_remaining=7424 estimated_bytes=59765056 calibrated_bytes_per_object=32201 estimated pack ~57.0 MB exceeds target limit 18.1 MB — splitting 3712 → 7424 packs (~7.86 KB each) time=2026-05-06T15:51:47.769+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=7424 from=746c37b0 to=fe84051a source: Enumerating objects: 1847, done. source: Counting objects: 100% (535/535), done. source: Compressing objects: 100% (198/198), done. time=2026-05-06T15:51:56.453+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=7424 new_remaining=8144 estimated_bytes=59475247 calibrated_bytes_per_object=32201 estimated pack ~56.7 MB exceeds target limit 18.1 MB — splitting 7424 → 8144 packs (~7.13 KB each) time=2026-05-06T15:51:56.453+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8144 from=746c37b0 to=fe84051a source: Enumerating objects: 1847, done. source: Counting objects: 100% (535/535), done. source: Compressing objects: 100% (198/198), done. time=2026-05-06T15:52:05.424+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8144 estimated_bytes=59475247 object_count=1847 target_limit_bytes=19000000 calibrated_bytes_per_object=32201 time=2026-05-06T15:52:07.007+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=8144 estimated_bytes=59475247 target_limit_bytes=19000000 sent_bytes=8388620 object_count=1847 objects_sent=248 total_objects_in_pack=1847 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T15:52:07.007+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=32201 observed_bytes_per_object=67650 sent_bytes=8388620 calibration_denom=248 object_count=1847 objects_sent=248 github.com → 17.8 MB @ 129 KB/s ✓ │ 16.0 MB @ 115 KB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack": round trip: pack upload aborted early: projected to exceed target body limit exit status 1
Try the sync again now — on resume, the source should see all 334 commits at-or-before 746c37b0 in the chain as haves, and the merge commit's "delta" should drop dramatically. Run the same command and let me see the trace.
time=2026-05-06T16:12:28.041+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=551 batch_total=7967 estimated_bytes=4516946 object_count=7 target_limit_bytes=19000000 calibrated_bytes_per_object=645278 source: Total 7 (delta 0), reused 0 (delta 0), pack-reused 3 (from 1) time=2026-05-06T16:12:28.958+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=551 batch_total=7967 time=2026-05-06T16:12:28.958+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=552 batch_total=7967 from=23581c57 to=3c71ddfb source: Enumerating objects: 2, done. source: Counting objects: 100% (1/1), done. source: Total 2 (delta 0), reused 0 (delta 0), pack-reused 1 (from 1) time=2026-05-06T16:12:29.151+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=552 batch_total=7967 estimated_bytes=1290556 object_count=2 target_limit_bytes=19000000 calibrated_bytes_per_object=645278 time=2026-05-06T16:12:29.258+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=552 batch_total=7967 time=2026-05-06T16:12:29.258+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=553 batch_total=7967 from=3c71ddfb to=f9a586ea source: Enumerating objects: 6, done. source: Counting objects: 100% (3/3), done. source: Compressing objects: 100% (3/3), done. time=2026-05-06T16:12:29.519+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=553 batch_total=7967 estimated_bytes=3871668 object_count=6 target_limit_bytes=19000000 calibrated_bytes_per_object=645278 source: Total 6 (delta 0), reused 0 (delta 0), pack-reused 3 (from 1) time=2026-05-06T16:12:29.718+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=553 batch_total=7967 time=2026-05-06T16:12:29.718+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=554 batch_total=7967 from=f9a586ea to=a01090b9 source: Enumerating objects: 6, done. source: Counting objects: 100% (4/4), done. source: Compressing objects: 100% (4/4), done. time=2026-05-06T16:12:30.019+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=554 batch_total=7967 estimated_bytes=3871668 object_count=6 target_limit_bytes=19000000 calibrated_bytes_per_object=645278 source: Total 6 (delta 0), reused 0 (delta 0), pack-reused 2 (from 1) time=2026-05-06T16:12:30.236+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=554 batch_total=7967 time=2026-05-06T16:12:30.236+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=555 batch_total=7967 from=a01090b9 to=46d3bb0d source: Enumerating objects: 6, done. source: Counting objects: 100% (3/3), done. source: Compressing objects: 100% (3/3), done. time=2026-05-06T16:12:30.429+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=555 batch_total=7967 estimated_bytes=3871668 object_count=6 target_limit_bytes=19000000 calibrated_bytes_per_object=645278 source: Total 6 (delta 0), reused 0 (delta 0), pack-reused 3 (from 1) github.com → 273 MB @ 482 KB/s │ 253 MB @ 380 KB/s → 8b04592ed74a5cc…cloudflare.net (pack 555/7967)^Csignal: interrupt
Multi-have is doing its job — most packs are now ~6 objects each. The slowness you're seeing is a different problem: once subdivision in iteration 1 split down to 7967 batches (back when haves were too narrow), we're committed to all 7967 round-trips even though each is tiny now.
Two follow-ups worth considering, but separate from this PR:
- Smarter initial sizing — use calibrated bytes-per-object up front to pick a span that targets ~80% of the limit, rather than halving 29 → 58 → … → 7967.
- Recombination — when consecutive small packs come in well under limit, merge remaining checkpoints back together.
Want me to:
- (a) push the multi-have branch and open the PR as-is, then tackle subdivision tuning separately, or
- (b) hold the PR and address subdivision aggressiveness first so the end-to-end story is "merge-heavy repos sync at reasonable throughput"?
My vote is (a) — multi-have is a coherent, reviewable change that's strictly correct on its own. Subdivision tuning is its own design discussion.
Actually no — we already have all the data needed. Pre-flight subdivision currently calls subdivideCheckpoints which just halves. The smart-subdivision PR taught the reactive path (after failure) to compute factor = ceil(observed / limit) and call subdivideToFactor, but the pre-flight path didn't get the same upgrade.
So the change for (1) is small: in checkPackSizeAndSubdivide, swap subdivideCheckpoints for subdivideToFactor with factor = ceil(estimated / limit). That would have collapsed the 58 → 116 → 232 → 464 → 928 → 1856 → 3712 → 7424 cascade into one or two rounds, since each pre-flight already knows the projection (~106 MB) and the limit (~19 MB), giving factor=6 directly.
Want me to land that as a small follow-up commit on soph/multi-have (since you'll observe the benefit most clearly with multi-have on), or as its own branch on top?
hmm, I wonder in general: We basically have an issue with pretty large commits, I think that is the case here, because even with the multi have and split it goes down to 7967 splits, then once that large thing is done, it's stuck on tiny pack files.
I put the log of the last run (copy&paste it from the terminal output) into tmp/test.log I wonder if this is working correctly but also if we can bring it back up again easily
Yes, it's working correctly — those packs are tiny because their actual deltas are tiny (with multi-have, most of each merge's ancestry is already in the haves). And yes, we can recover easily — what's missing is a recombination step. Subdivision currently ratchets one-way: once 928 → 1814 → 3628 → … happens for one heavy commit, the smaller commits behind it inherit that fine granularity forever.
Looking at the log, the wall is one heavy region around batch 22 of 928 (calibration jumps from 175 KB/obj to 645 KB/obj — a single commit ~30 MB worth of stuff). Everything past it could comfortably fit dozens of commits per pack, but we keep pushing 6-object packs.
Simplest heuristic that self-tunes:
That goes right after current = checkpoint. Self-correcting: if the merged span overshoots, the abort-early + subdivision path re-splits it.
Where do you want this — same soph/multi-have branch (since it makes the multi-have benefits actually visible end-to-end), or a separate soph/recombine on top?
Done. soph/multi-have now has two commits:
0a67638— multi-have tracking01a290b— recombination after small pushes
Try the same sync command again. After the heavy region is past, you should see bootstrap batch recombining after small push lines and the batch_total counter dropping back down as we coalesce. Wall-clock for the remainder of the chain should improve dramatically.
❯ go run ./cmd/git-sync sync --target-max-pack-bytes 50000000 --bootstrap-strategy=topo --verbose --branch entire/checkpoints/v1 --progress https://github.com/entireio/cli-checkpoints.git https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git Username for 'https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': x Password for 'https://x@8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net': time=2026-05-06T17:40:24.335+02:00 level=INFO msg="bootstrap batch planning checkpoints" branch_ref_count=1 time=2026-05-06T17:40:24.335+02:00 level=INFO msg="bootstrap batch trunk selected" source_head_target=refs/heads/entire/checkpoints/v1 trunk_target_ref=refs/heads/entire/checkpoints/v1 time=2026-05-06T17:40:24.335+02:00 level=INFO msg="bootstrap batch fetching commit graph" branch=refs/heads/entire/checkpoints/v1 have_count=0 stop_at_count=0 time=2026-05-06T17:40:24.755+02:00 level=INFO msg="bootstrap batch planned checkpoints" branch=refs/heads/entire/checkpoints/v1 chain_len=8481 estimated_batches=12 time=2026-05-06T17:40:24.755+02:00 level=INFO msg="bootstrap batch branch plan" branch=refs/heads/entire/checkpoints/v1 temp_ref=refs/gitsync/bootstrap/heads/entire/checkpoints/v1 planned_batches=12 resume_hash=8c336026 time=2026-05-06T17:40:24.755+02:00 level=INFO msg="bootstrap batch resuming from stale temp ref" branch=refs/heads/entire/checkpoints/v1 resume_hash=8c336026 remaining_commits=6476 new_batches=9 time=2026-05-06T17:40:24.755+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=9 from=8c336026 to=a9918719 source: Enumerating objects: 4832, done. source: Counting objects: 100% (1538/1538), done. source: Compressing objects: 100% (383/383), done. time=2026-05-06T17:40:27.949+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=9 estimated_bytes=3624000 object_count=4832 target_limit_bytes=50000000 calibrated_bytes_per_object=750 time=2026-05-06T17:40:29.395+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=9 estimated_bytes=3624000 target_limit_bytes=50000000 sent_bytes=8388620 object_count=4832 objects_sent=836 total_objects_in_pack=4832 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:40:29.395+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=750 observed_bytes_per_object=20068 sent_bytes=8388620 calibration_denom=836 object_count=4832 objects_sent=836 time=2026-05-06T17:40:29.396+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=9 new_remaining=18 sent_bytes=8388620 sizing_bytes=48485420 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 9 → 18 packs time=2026-05-06T17:40:29.396+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=18 from=8c336026 to=16d4413d source: Enumerating objects: 2443, done. source: Counting objects: 100% (624/624), done. source: Compressing objects: 100% (203/203), done. time=2026-05-06T17:40:30.588+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=18 estimated_bytes=49026124 object_count=2443 target_limit_bytes=50000000 calibrated_bytes_per_object=20068 time=2026-05-06T17:40:32.307+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=18 estimated_bytes=49026124 target_limit_bytes=50000000 sent_bytes=9961484 object_count=2443 objects_sent=492 total_objects_in_pack=2443 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:40:32.307+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=20068 observed_bytes_per_object=40493 sent_bytes=9961484 calibration_denom=492 object_count=2443 objects_sent=492 time=2026-05-06T17:40:32.308+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=18 new_remaining=36 sent_bytes=9961484 sizing_bytes=49463222 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 18 → 36 packs time=2026-05-06T17:40:32.308+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=36 from=8c336026 to=c97ad437 source: Enumerating objects: 1204, done. source: Counting objects: 100% (447/447), done. source: Compressing objects: 100% (132/132), done. time=2026-05-06T17:40:33.399+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=36 estimated_bytes=48753572 object_count=1204 target_limit_bytes=50000000 calibrated_bytes_per_object=40493 time=2026-05-06T17:40:34.895+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=36 estimated_bytes=48753572 target_limit_bytes=50000000 sent_bytes=8388620 object_count=1204 objects_sent=206 total_objects_in_pack=1204 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:40:34.895+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=40493 observed_bytes_per_object=81442 sent_bytes=8388620 calibration_denom=206 object_count=1204 objects_sent=206 time=2026-05-06T17:40:34.895+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=36 new_remaining=72 sent_bytes=8388620 sizing_bytes=49028633 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 36 → 72 packs time=2026-05-06T17:40:34.895+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=72 from=8c336026 to=8633438b source: Enumerating objects: 588, done. source: Counting objects: 100% (234/234), done. source: Compressing objects: 100% (93/93), done. time=2026-05-06T17:40:35.701+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=72 estimated_bytes=47887896 object_count=588 target_limit_bytes=50000000 calibrated_bytes_per_object=81442 time=2026-05-06T17:40:37.095+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=72 estimated_bytes=47887896 target_limit_bytes=50000000 sent_bytes=8388620 object_count=588 objects_sent=94 total_objects_in_pack=588 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:40:37.095+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=81442 observed_bytes_per_object=178481 sent_bytes=8388620 calibration_denom=94 object_count=588 objects_sent=94 time=2026-05-06T17:40:37.096+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=72 new_remaining=144 sent_bytes=8388620 sizing_bytes=52473495 limit_bytes=50000000 factor=5 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 72 → 144 packs time=2026-05-06T17:40:37.096+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=144 from=8c336026 to=e6474dc7 source: Enumerating objects: 326, done. source: Counting objects: 100% (132/132), done. source: Compressing objects: 100% (51/51), done. time=2026-05-06T17:40:37.330+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=144 new_remaining=288 estimated_bytes=58184806 calibrated_bytes_per_object=178481 estimated pack ~55.5 MB exceeds target limit 47.7 MB — splitting 144 → 288 packs (~197 KB each) time=2026-05-06T17:40:37.330+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=288 from=8c336026 to=2ef3c73e source: Enumerating objects: 158, done. source: Counting objects: 100% (70/70), done. source: Compressing objects: 100% (41/41), done. time=2026-05-06T17:40:37.613+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=288 estimated_bytes=28199998 object_count=158 target_limit_bytes=50000000 calibrated_bytes_per_object=178481 source: Total 158 (delta 52), reused 29 (delta 29), pack-reused 88 (from 1) time=2026-05-06T17:40:49.976+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=288 time=2026-05-06T17:40:49.976+02:00 level=INFO msg="bootstrap batch recombining after small push" branch=refs/heads/entire/checkpoints/v1 sent_bytes=3029143 target_limit_bytes=50000000 dropped_checkpoint=e6474dc7 remaining_checkpoints=287 time=2026-05-06T17:40:49.976+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=287 from=2ef3c73e to=46bfba9f source: Enumerating objects: 318, done. source: Counting objects: 100% (123/123), done. source: Compressing objects: 100% (48/48), done. time=2026-05-06T17:40:50.477+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=286 new_remaining=572 estimated_bytes=56756958 calibrated_bytes_per_object=178481 estimated pack ~54.1 MB exceeds target limit 47.7 MB — splitting 286 → 572 packs (~96.9 KB each) time=2026-05-06T17:40:50.477+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=573 from=2ef3c73e to=e6474dc7 source: Enumerating objects: 179, done. source: Counting objects: 100% (69/69), done. source: Compressing objects: 100% (19/19), done. time=2026-05-06T17:40:50.708+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=573 estimated_bytes=31948099 object_count=179 target_limit_bytes=50000000 calibrated_bytes_per_object=178481 time=2026-05-06T17:40:52.175+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=573 estimated_bytes=31948099 target_limit_bytes=50000000 sent_bytes=8912908 object_count=179 objects_sent=32 total_objects_in_pack=179 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:40:52.175+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=178481 observed_bytes_per_object=557056 sent_bytes=8912908 calibration_denom=32 object_count=179 objects_sent=32 time=2026-05-06T17:40:52.175+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=572 new_remaining=1144 sent_bytes=8912908 sizing_bytes=49856579 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 572 → 1144 packs time=2026-05-06T17:40:52.175+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=1145 from=2ef3c73e to=241083bb source: Enumerating objects: 101, done. source: Counting objects: 100% (70/70), done. source: Compressing objects: 100% (22/22), done. time=2026-05-06T17:40:52.483+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=1144 new_remaining=2288 estimated_bytes=56262656 calibrated_bytes_per_object=557056 estimated pack ~53.7 MB exceeds target limit 47.7 MB — splitting 1144 → 2288 packs (~24.0 KB each) time=2026-05-06T17:40:52.483+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=2289 from=2ef3c73e to=3b88037a source: Enumerating objects: 53, done. source: Counting objects: 100% (39/39), done. source: Compressing objects: 100% (22/22), done. time=2026-05-06T17:40:52.853+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=2289 estimated_bytes=29523968 object_count=53 target_limit_bytes=50000000 calibrated_bytes_per_object=557056 source: Total 53 (delta 29), reused 17 (delta 17), pack-reused 14 (from 1) time=2026-05-06T17:40:54.454+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=2 batch_total=2289 time=2026-05-06T17:40:54.454+02:00 level=INFO msg="bootstrap batch recombining after small push" branch=refs/heads/entire/checkpoints/v1 sent_bytes=1516369 target_limit_bytes=50000000 dropped_checkpoint=241083bb remaining_checkpoints=2288 time=2026-05-06T17:40:54.454+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=2288 from=3b88037a to=6dfd9b6d source: Enumerating objects: 74, done. source: Counting objects: 100% (29/29), done. source: Compressing objects: 100% (12/12), done. time=2026-05-06T17:40:54.648+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=2288 estimated_bytes=41222144 object_count=74 target_limit_bytes=50000000 calibrated_bytes_per_object=557056 source: Total 74 (delta 22), reused 17 (delta 17), pack-reused 45 (from 1) time=2026-05-06T17:40:56.773+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=3 batch_total=2288 time=2026-05-06T17:40:56.773+02:00 level=INFO msg="bootstrap batch recombining after small push" branch=refs/heads/entire/checkpoints/v1 sent_bytes=1546015 target_limit_bytes=50000000 dropped_checkpoint=e6474dc7 remaining_checkpoints=2287 time=2026-05-06T17:40:56.773+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=2287 from=6dfd9b6d to=2ebdcfd9 source: Enumerating objects: 85, done. source: Counting objects: 100% (34/34), done. source: Compressing objects: 100% (20/20), done. time=2026-05-06T17:40:57.194+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=2287 estimated_bytes=47349760 object_count=85 target_limit_bytes=50000000 calibrated_bytes_per_object=557056 time=2026-05-06T17:40:59.190+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=2287 estimated_bytes=47349760 target_limit_bytes=50000000 sent_bytes=12058636 object_count=85 objects_sent=21 total_objects_in_pack=85 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:40:59.190+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=557056 observed_bytes_per_object=1148441 sent_bytes=12058636 calibration_denom=21 object_count=85 objects_sent=21 time=2026-05-06T17:40:59.190+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=2284 new_remaining=4568 sent_bytes=12058636 sizing_bytes=48808764 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 2284 → 4568 packs time=2026-05-06T17:40:59.190+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=4571 from=6dfd9b6d to=0f369f6d source: Enumerating objects: 41, done. source: Counting objects: 100% (20/20), done. source: Compressing objects: 100% (12/12), done. time=2026-05-06T17:40:59.591+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=4571 estimated_bytes=47086081 object_count=41 target_limit_bytes=50000000 calibrated_bytes_per_object=1148441 time=2026-05-06T17:41:01.532+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=4571 estimated_bytes=47086081 target_limit_bytes=50000000 sent_bytes=10485772 object_count=41 objects_sent=9 total_objects_in_pack=41 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:41:01.532+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=1148441 observed_bytes_per_object=2330171 sent_bytes=10485772 calibration_denom=9 object_count=41 objects_sent=9 time=2026-05-06T17:41:01.532+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=4568 new_remaining=6426 sent_bytes=10485772 sizing_bytes=47768516 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 4568 → 6426 packs time=2026-05-06T17:41:01.532+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=6429 from=6dfd9b6d to=6073093c source: Enumerating objects: 19, done. source: Counting objects: 100% (10/10), done. source: Compressing objects: 100% (7/7), done. time=2026-05-06T17:41:01.858+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=6429 estimated_bytes=44273249 object_count=19 target_limit_bytes=50000000 calibrated_bytes_per_object=2330171 source: Total 19 (delta 5), reused 3 (delta 3), pack-reused 9 (from 1) time=2026-05-06T17:41:02.044+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=4 batch_total=6429 time=2026-05-06T17:41:02.044+02:00 level=INFO msg="bootstrap batch recombining after small push" branch=refs/heads/entire/checkpoints/v1 sent_bytes=73277 target_limit_bytes=50000000 dropped_checkpoint=0f369f6d remaining_checkpoints=6428 time=2026-05-06T17:41:02.044+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=6428 from=6073093c to=0b4d9d99 source: Enumerating objects: 42, done. source: Counting objects: 100% (14/14), done. source: Compressing objects: 100% (7/7), done. time=2026-05-06T17:41:02.412+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=6424 new_remaining=6431 estimated_bytes=97867182 calibrated_bytes_per_object=2330171 estimated pack ~93.3 MB exceeds target limit 47.7 MB — splitting 6424 → 6431 packs (~14.9 KB each) time=2026-05-06T17:41:02.412+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=6435 from=6073093c to=0f369f6d source: Enumerating objects: 22, done. source: Counting objects: 100% (10/10), done. source: Compressing objects: 100% (6/6), done. time=2026-05-06T17:41:02.708+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=6431 new_remaining=6434 estimated_bytes=51263762 calibrated_bytes_per_object=2330171 estimated pack ~48.9 MB exceeds target limit 47.7 MB — splitting 6431 → 6434 packs (~7.78 KB each) time=2026-05-06T17:41:02.708+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=6438 from=6073093c to=a693fa49 source: Enumerating objects: 4, done. source: Counting objects: 100% (3/3), done. source: Compressing objects: 100% (3/3), done. time=2026-05-06T17:41:02.927+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=6438 estimated_bytes=9320684 object_count=4 target_limit_bytes=50000000 calibrated_bytes_per_object=2330171 source: Total 4 (delta 0), reused 0 (delta 0), pack-reused 1 (from 1) time=2026-05-06T17:41:03.026+02:00 level=INFO msg="bootstrap batch checkpoint complete" branch=refs/heads/entire/checkpoints/v1 batch=5 batch_total=6438 time=2026-05-06T17:41:03.026+02:00 level=INFO msg="bootstrap batch recombining after small push" branch=refs/heads/entire/checkpoints/v1 sent_bytes=6200 target_limit_bytes=50000000 dropped_checkpoint=0f369f6d remaining_checkpoints=6437 time=2026-05-06T17:41:03.026+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6437 from=a693fa49 to=e6474dc7 source: Enumerating objects: 29, done. source: Counting objects: 100% (8/8), done. source: Compressing objects: 100% (4/4), done. time=2026-05-06T17:41:03.427+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=6432 new_remaining=6434 estimated_bytes=67574959 calibrated_bytes_per_object=2330171 estimated pack ~64.4 MB exceeds target limit 47.7 MB — splitting 6432 → 6434 packs (~10.3 KB each) time=2026-05-06T17:41:03.427+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6439 from=a693fa49 to=42a4c2b2 source: Enumerating objects: 11, done. source: Counting objects: 100% (5/5), done. source: Compressing objects: 100% (4/4), done. time=2026-05-06T17:41:03.671+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6439 estimated_bytes=25631881 object_count=11 target_limit_bytes=50000000 calibrated_bytes_per_object=2330171 time=2026-05-06T17:41:05.880+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6439 estimated_bytes=25631881 target_limit_bytes=50000000 sent_bytes=13107212 object_count=11 objects_sent=3 total_objects_in_pack=11 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:41:05.880+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=2330171 observed_bytes_per_object=8738141 sent_bytes=13107212 calibration_denom=3 object_count=11 objects_sent=3 time=2026-05-06T17:41:05.881+02:00 level=INFO msg="bootstrap batch subdividing after target size rejection" branch=refs/heads/entire/checkpoints/v1 old_remaining=6434 new_remaining=6435 sent_bytes=13107212 sizing_bytes=48059777 limit_bytes=50000000 factor=4 aborted_early=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" projected to exceed target limit (target limit 47.7 MB) — splitting 6434 → 6435 packs time=2026-05-06T17:41:05.881+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6440 from=a693fa49 to=42a4c2b2 source: Enumerating objects: 11, done. source: Counting objects: 100% (5/5), done. source: Compressing objects: 100% (4/4), done. time=2026-05-06T17:41:06.240+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6440 estimated_bytes=96119551 object_count=11 target_limit_bytes=50000000 calibrated_bytes_per_object=8738141 time=2026-05-06T17:41:08.441+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=6 batch_total=6440 estimated_bytes=96119551 target_limit_bytes=50000000 sent_bytes=13107212 object_count=11 objects_sent=3 total_objects_in_pack=11 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" github.com → 98.5 MB @ 1.28 MB/s ✓ │ 94.4 MB @ 1.23 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack": round trip: pack upload aborted early: projected to exceed target body limit exit status 1
Done. Two commits on soph/multi-have. Now retry with a larger pack budget — the actual blocker is one chain commit whose content is ~50 MB:
You should see:
- The recovery after the first heavy region is much faster — recombination drops up to 8 checkpoints per small push instead of 1.
- The
a693fa49 → 42a4c2b2commit (~50 MB compressed) fits under the new limit. - Once past it, throughput should ramp back up quickly via the same recombination.
can we figure out what this is?
time=2026-05-06T17:49:30.911+02:00 level=INFO msg="bootstrap batch subdividing before push (pack header estimate)" branch=refs/heads/entire/checkpoints/v1 old_remaining=4608 new_remaining=6435 estimated_bytes=59319480 calibrated_bytes_per_object=5392680 estimated pack ~56.6 MB exceeds target limit 47.7 MB — splitting 4608 → 6435 packs (~9.00 KB each) time=2026-05-06T17:49:30.911+02:00 level=INFO msg="bootstrap batch push checkpoint" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=6435 from=a693fa49 to=42a4c2b2 source: Enumerating objects: 11, done. source: Counting objects: 100% (5/5), done. source: Compressing objects: 100% (4/4), done. time=2026-05-06T17:49:31.137+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=6435 estimated_bytes=59319480 object_count=11 target_limit_bytes=50000000 calibrated_bytes_per_object=5392680 time=2026-05-06T17:49:33.312+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=6435 estimated_bytes=59319480 target_limit_bytes=50000000 sent_bytes=13107212 object_count=11 objects_sent=3 total_objects_in_pack=11 aborted_early=true will_subdivide=true error="target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack\": round trip: pack upload aborted early: projected to exceed target body limit" time=2026-05-06T17:49:33.312+02:00 level=INFO msg="bootstrap batch calibrated bytes-per-object" branch=refs/heads/entire/checkpoints/v1 previous_bytes_per_object=5392680 observed_bytes_per_object=8738141 sent_bytes=13107212 calibration_denom=3 object_count=11 objects_sent=3 github.com → 99.0 MB @ 2.47 MB/s ✓ │ 96.0 MB @ 2.40 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: post RPC: Post "https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack": round trip: pack upload aborted early: projected to exceed target body limit exit status 1
Found it. The wall is 5d/5816d3a76f/0/full.jsonl — a 36.5 MB Claude Code session log committed as a single checkpoint. The pack contains that one blob plus a handful of trees/metadata files (11 objects total). Even with reasonable zlib compression on the JSONL, the resulting pack is too large to fit through 50 MB once you include the pack header and tree/commit objects.
This isn't really a git-sync bug — it's a checkpoint repo with one giant transcript blob in a single commit. Three options:
- Bump the limit further:
--target-max-pack-bytes 100000000(100 MB) clears it. Cloudflare's actual cap was ~524 MB based on earlier observations, so plenty of headroom. - Treat it as a known repo property and document
--target-max-pack-bytesselection guidance. - Surface this case clearly: when subdivision can't make further progress (the chain is 1 commit per checkpoint and we're still over budget), emit an actionable error pointing at the offending commit + biggest blob in it. That's a small UX improvement worth doing as a separate change.
Want me to do (3) on a follow-up branch, or skip and just push the multi-have stack as-is?
ok, with 100MB I'm back to a 500. My guess is now that Cloudflares Backend just can't handle 36MB (or what ever the exact border is)
Plausible. Two regimes: with 50 MB our own abort fires (projected overshoot), with 100 MB we let it go and the server returns 500. That means Cloudflare's backend has a real limit somewhere between those — likely close to the 36 MB blob's compressed size + protocol overhead. The earlier ~524 MB cutoff was probably a streaming body cap before the backend got involved; this 500 looks like the backend service itself choking on large object bodies.
That's outside what git-sync can fix — both branches we just landed (multi-have, recombination) still pay off elsewhere even if this specific repo can't fully sync to this specific endpoint.
If you want to confirm the hypothesis: try --target-max-pack-bytes 60000000 and 80000000. Whichever transitions from 500 → success would pin the server's actual limit. But that's curiosity work, not blocking.
Want me to push the multi-have branch and open the PR now?
time=2026-05-06T20:06:36.790+02:00 level=INFO msg="bootstrap batch push attempting" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=3072 estimated_bytes=41722308 object_count=18 target_limit_bytes=75000000 calibrated_bytes_per_object=2317906 source: Total 18 (delta 3), reused 3 (delta 3), pack-reused 11 (from 1) time=2026-05-06T20:06:53.693+02:00 level=INFO msg="bootstrap batch push failed" branch=refs/heads/entire/checkpoints/v1 batch=1 batch_total=3072 estimated_bytes=41722308 target_limit_bytes=75000000 sent_bytes=22316814 object_count=18 objects_sent=18 total_objects_in_pack=18 aborted_early=false will_subdivide=false error="target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f79eb783d26dbf8-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error" github.com → 122 MB @ 2.24 MB/s ✓ │ 119 MB @ 1.76 MB/s ✓ → 8b04592ed74a5cc…cloudflare.net error: sync: sync: bootstrap execute: push bootstrap batch for refs/heads/entire/checkpoints/v1: target receive-pack: http 500: https://8b04592ed74a5cce30d355b07276caf3.artifacts.cloudflare.net/git/soph/test-repo-bench1.git/git-receive-pack [Cf-Ray=9f79eb783d26dbf8-FRA, Server=cloudflare, Content-Type=text/plain; charset=UTF-8] Internal Server Error exit status 1
so 75000000 doesn't help, but also: why could we now create 41MB?
Two separate things in that line:
estimated_bytes (41 MB) is a projection: calibrated bytes/obj × object_count = 2,317,906 × 18. It's a pessimistic upper bound from earlier observations. sent_bytes (22 MB) is what actually went over the wire — the pack came out smaller than the projection said.
Why no early abort? With limit 75 MB the abort threshold is 75 × 95% = 71.25 MB. The projection while uploading is bytesSent × totalObjects/objectsSent. By the time objectsSent reaches totalObjects, projection equals bytesSent = 22 MB, well under threshold. So we let the full 22 MB through.
The interesting result: Cloudflare accepted the full 22 MB body and then 500'd from the backend. So the edge isn't the limit here — something downstream in the receive-pack pipeline chokes. The wall is below 22 MB. Trying --target-max-pack-bytes 20000000 will just abort us earlier (projection 22 MB > 19 MB threshold) and we'll subdivide forever on a single-commit pack we can't shrink.
This server endpoint has a real backend-side limit somewhere under 22 MB for receive-pack, and that's not something git-sync can route around — the data has to land in one request to be a valid push of that commit. Fundamentally a server-side issue.
Push the multi-have stack as-is? The two changes are independently useful regardless of this particular endpoint's bug.
I think we should be at three branches now: soph/streaming-pack-parse, soph/streaming-pack-parse and soph/multi-have
Yes — that's the local stack. Status vs origin:
soph/streaming-pack-parse(3 commits): up to date with originsoph/topo-bootstrap(4 commits, rebased onto streaming-pack-parse): origin still has the old layout (1 commit on main directly), so this needs--force-with-leasesoph/multi-have(6 commits): not on origin yet
No open PRs on any of them. To get there I need to:
git push origin soph/streaming-pack-parse(no-op, already there)git push --force-with-lease origin soph/topo-bootstrap(rewrites the remote branch history — it had topo-on-main, now it's topo-on-streaming-pack-parse)git push -u origin soph/multi-have- Open three stacked PRs:
main ← soph/streaming-pack-parsesoph/streaming-pack-parse ← soph/topo-bootstrapsoph/topo-bootstrap ← soph/multi-have
OK to proceed with all four?
soph/streaming-pack-parse — three-commit pre-req for the topo and multi-have work above it:
-
Stream-parse the pack — wraps the upload reader with a
packfile.Scannerrunning on anio.Pipe, exposing liveObjectsSent()/TotalObjects()atomics. -
Abort early on projected overshoot — uses those counters to cut a doomed upload at ~5% sent instead of waiting for the server to reject the full pack.
selfImposedBudgetratchets down from observed cutoffs. -
Use projection, not
sentBytes, for factor & calibration — fixes two bugs that fell out once the observer was wired up: subdivision factor was always 2 (becausesentBytesafter early abort was just the 8 MiB floor), and per-object calibration was diluted by dividing by the full pack count instead of objects actually observed.
Net effect for blob-heavy repos: failed retries cost ~5% of pack size instead of 100%, and subdivision converges in 1–2 rounds instead of 6+.
soph/topo-bootstrap — one commit on top of streaming-pack-parse:
Add topo bootstrap strategy — for merge-heavy repos where each first-parent step pulls in a large side-branch ancestry as one indivisible sub-pack. New planner.TopoChainStoppingAt does a deterministic topological walk (BFS reachability + Kahn's algorithm with hash-tie-broken queue for stable resume) that includes every reachable commit, parents before children. Plumbed through bootstrap.Params.Strategy ("first-parent" default, "topo" opt-in) → syncer.Config → unstable.AdvancedOptions → --bootstrap-strategy flag on sync/replicate/bootstrap.
Under topo, sub-pack boundaries can land inside side branches instead of being limited to first-parent granularity. Cost: more source-side enumeration proportional to the extra commit count — worth it when first-parent's smallest indivisible step exceeds the target's body limit.
Bootstrap loop itself is unchanged: want chain[i] have current produces the right pack whether chain[i] is on the backbone or a side branch.
soph/multi-have — two commits on top of topo-bootstrap:
-
Declare all pushed checkpoints as haves — under topo ordering, intermediate checkpoints sit on side branches that aren't ancestors of
current. Previously each fetch advertised only the latest temp-ref position, so a merge commit's "delta" re-sent the side-branch ancestry we'd already pushed. Track every successful checkpoint in a per-batch slice and thread the full set into fetch haves; on resume, seed from the chain prefix at-or-beforecurrentso commits pushed in prior runs are also declared. -
Recombine checkpoints when consecutive packs underuse the limit — subdivision is a one-way ratchet: the fine granularity needed for one heavy commit sticks for the rest of the chain. After each successful push, drop enough upcoming checkpoints that the next pack should land near half the limit (count = log2(target/2 / sent), capped). If we overshoot, abort-early + subdivision re-splits.
Together these turn topo from "more granular but still re-sends" into a workable strategy for merge-heavy repos: merge deltas drop to genuinely-new content only, and the chain doesn't get stuck pushing thousands of tiny packs after a heavy region.
- Medium: the new selfImposedBudget ratchet can become far smaller than the target’s real body limit when the server returns an explicit 413 before consuming much of the request body. In internal/strategy/bootstrap/bootstrap.go:446 you correctly parse a server-advertised limit into limit, but in internal/strategy/bootstrap/bootstrap.go:485 you still ratchet selfImposedBudget down to raw sentBytes on every non-self-aborted failure. If a proxy/server rejects early, sentBytes may be only a few MiB even though the actual cap is, say, 100 MiB. Subsequent retries will then self-abort around that much smaller budget and over-subdivide aggressively. I’d gate the sentBytes ratchet on “no parseable server limit”, or at least avoid preferring sentBytes when targetBodyLimit(pushErr) returned a trustworthy explicit limit.
I read the PR description for PR #40, “Streaming packfile parsing”. The stated goal is to stream-parse outgoing packs, abort oversized uploads early based on projected size, and use observed object counts to improve post-failure subdivision/calibration so blob-heavy repos converge in 1–2 retries instead of many more.
Stack rebased cleanly:
soph/streaming-pack-parse: +cf8bdad(the fix)soph/topo-bootstrap: rebased (new SHA5ef588e)soph/multi-have: rebased (new SHAsdca0dc6,c285e2d)
All build/test/lint green on each branch. To get this to the reviewer I need to git push --force-with-lease all three (PR #40 picks up the new commit; PR #41 and #42 update their bases). OK to push?
All three pushed. PR #40 now has the parsed-limit fix; PRs #41 and #42 rebased onto the updated base. Reviewer's medium issue should be resolved.
Findings
-
Medium: the early-abort path still degrades back to binary subdivision when the first completed object arrives late. In bootstrap.go calibration only switches to
objectsSentwhenobjectsSent > 0, and in bootstrap.go projectedsizingBytesis only used whenobjectsSent > 0 && totalObjects > 0. For a pack whose first object is a very large blob, the observer can self-abort before any object finishes scanning, leavingobjectsSent == 0; the retry then sizes from rawsentBytesat the abort floor and computesfactor == 2again. That recreates the slow 1→2→4→… convergence this PR is trying to remove, and there’s no test covering theabortedEarly && totalObjects > 0 && objectsSent == 0case in bootstrap_test.go. -
Medium:
minBytesBeforeAbortmakes learned budgets below 8 MiB impossible to enforce client-side.shouldAbortPushreturns false untilbytesSent >= 8 MiBregardless of the actual budget in bootstrap.go and bootstrap.go. ButnextSelfImposedBudgetcan ratchetselfImposedBudgetdown below that, e.g. to a 5 MiB proxy cutoff, in bootstrap.go and bootstrap.go. In that state the self-abort path can never trigger before the server does, so retries keep paying for server-side rejection instead of the advertised early cutoff. The current tests cover learning a 5 MiB budget but not honoring it on the next push.
The current PR description says the branch adds a streaming packfile.Scanner, aborts doomed uploads early from projected overshoot, and uses observed object counts to improve subdivision/calibration so blob-heavy retries converge in 1–2 rounds.
I also ran go test ./internal/strategy/bootstrap/...; it passes.
Findings
-
Medium: the new
topostrategy does not actually advertise all previously-pushed commits from the same batch ashaves, so later topo checkpoints can re-fetch side-branch history that was already uploaded earlier in the run. In bootstrap.go each checkpoint fetch still callspackReaderForCheckpoint(..., current, ...), and packReaderForCheckpoint only addscurrentplus completed branch tips to thehavesmap. That assumption is valid for first-parent order, where every earlier checkpoint is an ancestor ofcurrent, but it is not valid for topo order: previously-pushed side-branch commits are not necessarily ancestors of the latestcurrent. The result is that merge checkpoints can resend large side branches that topo planning was supposed to isolate, undermining the feature’s main benefit and potentially reintroducing oversized packs. I also don’t see a topo execution test covering theFetchPackhaves shape; the new tests only verify the topo walk itself. -
Low: the PR description says the new option is “validated for supported values”, but I don’t see validation anywhere in the request/config plumbing.
BootstrapStrategyis just copied through unstable/client.go, syncer.go, and the CLI flags, and the only rejection is deep inside bootstrap planning in bootstrap.go. That means an invalid value is not rejected at parse/config time and may be silently irrelevant on non-bootstrap code paths, which is weaker behavior than the PR describes.
I read the PR description for PR #41, “Add topo bootstrap strategy for merge-heavy repos.” It introduces an opt-in topo checkpoint ordering, plumbs it through CLI/API/config, and claims this lets batched bootstrap place boundaries inside side branches instead of only on the first-parent backbone.
I also ran go test ./internal/planner ./internal/strategy/bootstrap ./internal/syncer ./cmd/git-sync ./unstable; the targeted suites passed.
Reorganized:
- PR #40 (streaming-pack-parse): unchanged
- PR #41 (topo-bootstrap): topo + multi-have + validation — now self-sufficient
- PR #42 (multi-have): just recombination — smaller, narrower scope
PR #42 needs its title/description updated to reflect that it now only contains recombination. Push everything?
Title: Recombine checkpoints to recover pack granularity after heavy regions
Body:
After bootstrap subdivides aggressively to fit one heavy commit through the target's body limit, the fine granularity sticks for the rest of the chain — even when the commits afterward are tiny deltas that would comfortably fit dozens per pack. cli-checkpoints reproduces this: one ~30 MB session-log commit forces 928 → 7967 splits, then thousands of 6-object packs grind through one round-trip each.
This PR adds a self-tuning recombination step. After every successful push, drop enough upcoming checkpoints that the next pack should land near half the target limit. Each dropped checkpoint roughly doubles the next pack's span, so the count is log2(target/2 / sent), capped at 8 per success to keep recovery cost bounded if a heavy commit shows up immediately after. If we overshoot, the existing abort-early + subdivision path re-splits.
Self-tuning, no new state, no new flags. Only fires under batched bootstrap (--target-max-pack-bytes set).
Stacked on: PR #41 (topo + multi-have). Recombination is most visible when topo + multi-have are doing their job — under topo, lots of small merge deltas come in well under the limit and trigger the doublings.
Test plan
- New unit test covers the drop-count math across full-pack, half-pack, tiny-pack, and capped-by-remaining-checkpoints cases.
- Re-run the cli-checkpoints sync; observe
bootstrap batch recombining after small pushlines and thebatch_totalcounter dropping back down between heavy regions.
can you also look at the comment on https://github.com/entireio/git-sync/pull/41
The Bugbot finding (high severity):
Under
"topo"strategy, consecutive checkpoints can live on unrelated branches. The bootstrap loop updates the temp ref viaOld: current, New: checkpoint. When consecutive checkpoints aren't in an ancestor-descendant relationship, this is a non-fast-forward ref update. Servers withreceive.denyNonFastforwardswill reject it.
The concern is technically valid. Looking at the code path:
TopoChainStoppingAtemits parents-before-children but doesn't guarantee a linear ancestry —[..., A2, B2, ...]where A2 and B2 are on parallel branches is allowed.evenCheckpointspicks every Nth entry, sobatch.Checkpoints[k]andbatch.Checkpoints[k+1]can sit on those parallel branches.- The push at bootstrap.go:373 sets
TargetHash: current→ the wire command becomesOld: current, New: checkpoint. Ifcurrentisn't incheckpoint's ancestry, that's non-ff.
In practice: GitHub/Cloudflare don't enforce denyNonFastforwards by default for refs outside refs/heads/ (and our temp ref lives under refs/gitsync/bootstrap/heads/). The user's runs against both have succeeded across many such updates, so empirically this hasn't fired. But "works on the servers we tested" isn't quite "correct against any git server."
Three fix options ordered by intrusion:
-
Document the requirement — note in the topo strategy doc-comment that the target must accept non-ff updates on the gitsync namespace. Lowest cost; doesn't actually fix anything.
-
Force-style temp ref updates — when about to push to the temp ref, check (locally, via the loaded
graphStore) whethercurrentis an ancestor ofcheckpoint. If not, emit[delete temp_ref, create temp_ref at checkpoint]as a single push transaction instead of an update. Costs one extra pkt-line per non-ff push and an ancestry walk; transparent to callers; works on any server that accepts deletes. -
Fresh temp ref per checkpoint —
refs/gitsync/bootstrap/<branch>/ckpt-N. No updates ever. Cleanup at end of batch. More refs on the wire, more bookkeeping for resume.
My recommendation is #2. It's a focused, server-agnostic fix that adds maybe 30 lines and a small ancestry-check helper. The graph store is already loaded in the planning phase — we can pass an "is a reachable from b?" function down or precompute the ancestor set per checkpoint.
Want me to implement #2 on soph/topo-bootstrap? Or do you want #1 (just doc the constraint) given it hasn't actually broken in any of our test runs?
is this something we just need or for example the git cli also has so we could look for a best practice?
Worth checking. Closest precedents:
git push --force — wire-level, --force does nothing: it just disables a client-side ancestry check before sending the same Old: current, New: new command. The server's denyNonFastforwards setting is the sole arbiter. There's no "force this update" capability in the protocol. So git CLI's answer to "what about non-ff" is "rely on server config."
Gerrit (refs/changes/XX/YYYY/Z) — every code-review push goes to a new ref. No updates, ever. That's option #3 in my earlier list. Gerrit picked it precisely because they couldn't rely on servers allowing non-ff.
git-fetch with quarantine — server-side mechanism for atomicity, doesn't apply to our push direction.
No other widespread git tool has this problem because most workflows either update branches (and accept the ff/force constraint) or create new refs (Gerrit-style). git-sync's "repeatedly update a temp ref" is an unusual pattern.
So the actual best practice from the git ecosystem is one of:
- Accept the constraint and document it (what
git push --forceeffectively does — relies ondenyNonFastforwards = falsebeing the default, which it overwhelmingly is) - Don't update; create new refs (Gerrit pattern — option #3)
Detect-and-fallback (try update, retry as delete+create on non-ff rejection) is not a pattern git CLI uses, but it's not unreasonable as a defensive measure. It just isn't drawn from precedent.
Given this is for a gitsync-internal namespace and denyNonFastforwards is virtually never enabled by default on any major host, my updated recommendation is #1: document the constraint in the strategy comment and topo-strategy section of the README/docs. That's the closest match to how the rest of the git ecosystem handles this. If we ever hit a target that does enforce it, we can revisit with #2 or #3.
Want me to land just the doc note, or the full ancestry-check + fallback?
The ref that gets updated repeatedly during a batched topo bootstrap is:
So if you're syncing branch entire/checkpoints/v1, the temp ref is refs/gitsync/bootstrap/heads/entire/checkpoints/v1. Each successful checkpoint push moves this ref to the new tip; under topo, those successive tips can be on parallel branches.
That namespace is entirely internal to git-sync — no end-user ever names a ref starting with refs/gitsync/. It's not under refs/heads/ (so branch-specific server policies don't apply), not under refs/tags/ (so tag-specific don't apply), and not a well-known namespace like refs/notes/ or refs/changes/.
The actual user-visible refs (refs/heads/<branch>, refs/tags/<tag>) only get a single fast-forward update at the end of bootstrap, where current = ZeroHash (creation) or current = previous tip (a normal forward update because we're advancing from where the target was). So those aren't affected.
So the question really is: "Is the user's target going to enforce denyNonFastforwards on the refs/gitsync/ namespace?" In practice, virtually no server does — but a paranoid corporate setup could. Given this is purely internal scaffolding, I'd say documenting the constraint is the right call.
Ready to push. PR #41 now has 4 commits (topo + multi-have + validation + non-ff doc note); PR #42 has just the recombination. OK to push?
I think for topo we should add some doc explaining the difference, can you look at this:
Imagine a repo with a merge that pulls in a long side branch:
M is a merge commit. Its first parent is A. Its second parent is S4, which drags in S1..S4.
Say the target's pack-body limit is 2 commits' worth of objects.
First-parent chain (current default)
The planner walks only the backbone: root → A → M → tip.
Each arrow is one checkpoint = one indivisible pack:
checkpoint 1: root → A pack contains: {A} ✅ 1 commit, fits checkpoint 2: A → M pack contains: {S1,S2,S3,S4, M} ❌ 5 commits, too big checkpoint 3: M → tip pack contains: {tip} ✅ 1 commit, fits
The A → M step is one indivisible unit because the planner only knows about backbone commits. It cannot place a checkpoint at S2 — S2 isn't in the chain. The bootstrap fails: the pack exceeds the limit and can't be subdivided.
backbone: root ──── A ═══════════════════════ M ──── tip │ ◄── one big pack ──► │ │ │ side branch: └─── S1 ─ S2 ─ S3 ─ S4 ────┘
Topo chain (new opt-in)
The planner walks every reachable commit, parents-before-children. Hash-tie-broken so the order is stable:
chain: root → A → S1 → S2 → S3 → S4 → M → tip
Now S1..S4 are first-class checkpoints. The planner can place sub-pack boundaries inside the side branch:
checkpoint 1: root → A {A} ✅ checkpoint 2: A → S2 {S1, S2} ✅ fits checkpoint 3: S2 → S4 {S3, S4} ✅ fits checkpoint 4: S4 → M {M} ✅ tiny — only the merge commit itself checkpoint 5: M → tip {tip} ✅
The big indivisible step is gone.
Stack:
- PR #41: 5 commits — topo + multi-have + validation + non-ff doc note + usage docs with worked example
- PR #42: 1 commit — recombination
OK to push everything?
Both pushed (topo-bootstrap was actually a fast-forward — 88b109f..725f972 — since I only added commits on top this round).