can you take a look at https://github.com/entireio/git-sync/issues/70 · Entire
What's happening
The user is doing a one-shot bootstrap relay from a GitLab source to a fresh GitHub target. git-sync streams the pack straight from the source's upload-pack into the target's receive-pack POST (bootstrap.Execute → PushPack → sendReceivePack, which builds an io.MultiReader(header, packData) and POSTs it at internal/gitproto/push.go:301).
GitHub returns HTTP 408 (Request Timeout) on git-receive-pack. GitHub enforces a wall-clock deadline on the receive-pack POST. The repo is large (compiled kernels/modules = big blobs), and the relay is bottlenecked by a very slow source — the progress line shows gitlab.baylibre.com → 4.59 KB @ 402 B/s feeding 479 B @ 39 B/s → github.com. Because it's a direct relay, the slow GitLab read rate-limits the GitHub write, the single giant POST runs past GitHub's deadline, and GitHub kills it with 408. So the user's instinct in the issue is right: repo size (combined with slow source throughput) is the cause.
Why git-sync doesn't auto-recover here
There's already good machinery for "pack too big for the target" — batched bootstrap with checkpointing and auto-subdivision. But none of it triggers for this case:
- GitHub preflight (
githubBatchLimit,bootstrap.go:1304) only fires when the source is GitHub — it queries the GitHub API for the source repo's size. Here the source is GitLab, so it returnsfalseand never pre-batches. - Auto-batch-on-rejection (
autoTargetMaxPackBytes,bootstrap.go:1380) only fires for body-limit errors —isTargetBodyLimitError(bootstrap.go:1430) matches413/ "body exceeded size limit" / "payload too large". It does not match 408.
So the 408 falls through Execute's one-shot path (bootstrap.go:179-190) as a hard, unactionable failure. The error message also doesn't hint at --target-max-pack-bytes, which is the exact knob that would fix it (smaller packs each finish within GitHub's timeout window).
What I'd recommend
Immediate workaround for the user (can post as a comment): pass --target-max-pack-bytes to force batched bootstrap, e.g. --target-max-pack-bytes 104857600 (100 MiB). Batching also gives resumability across runs, which matters a lot for a slow link.
Code fix — treat a timeout-class rejection like a "this pack is too big for this target" signal so it benefits from the same batching:
- Classify
408(and arguably gateway timeouts502/503/504) as a "split it up" condition feedingautoTargetMaxPackBytes, so a one-shot 408 retries in batched mode instead of hard-failing. - When batching isn't possible (e.g. source lacks protocol-v2 filter support, so
SupportsBootstrapBatch()is false), surface an actionable error pointing at--target-max-pack-bytes.
I'd keep 408 conceptually separate from the 413 size errors (name it something like a "target push deadline / retryable" class) even though it routes into the same batching remedy, since the trigger is a timeout, not a size rejection.
Phase 0 — Gather the diff
Run git diff @{upstream}...HEAD (or git diff main...HEAD / git diff HEAD~1 if there's no upstream) to get the unified diff under review. If there are uncommitted changes, or the range diff is empty, also run git diff HEAD and include the working-tree changes in scope — the review often runs before the commit. If a PR number, branch name, or file path was passed as an argument, review that target instead. Treat this diff as the review scope.
Phase 1 — Review (4 cleanup agents in parallel)
Launch 4 independent review agents via the Agent tool, all in a single message so they run concurrently. Pass each agent the diff and one of the four angles below. Each returns its findings with file, line, a one-line summary, and the concrete cost (what is duplicated, wasted, or harder to maintain).
Reuse
Flag new code that re-implements something the codebase already has — Grep shared/utility modules and files adjacent to the change, and name the existing helper to call instead.
Simplification
Flag unnecessary complexity the diff adds: redundant or derivable state, copy-paste with slight variation, deep nesting, dead code left behind. Name the simpler form that does the same job.
Efficiency
Flag wasted work the diff introduces: redundant computation or repeated I/O, independent operations run sequentially, blocking work added to startup or hot paths. Also flag long-lived objects built from closures or captured environments — they keep the entire enclosing scope alive for the object's lifetime (a memory leak when that scope holds large values); prefer a class/struct that copies only the fields it needs. Name the cheaper alternative.
Altitude
Check that each change is implemented at the right depth, not as a fragile bandaid. Special cases layered on shared infrastructure are a sign the fix isn't deep enough — prefer generalizing the underlying mechanism over adding special cases.
Phase 2 — Apply the fixes
Wait for all four agents to complete, dedup findings that point at the same line or mechanism, and fix each remaining one directly. Skip any finding whose fix would change intended behavior, require changes well outside the reviewed diff, or that you judge to be a false positive — note the skip rather than arguing with it. Finish with a brief summary of what was fixed and what was skipped (or confirm the code was already clean).
What I shipped
Branch fix/target-push-timeout-batched-retry, three commits:
bootstrap: treat receive-pack timeouts (408/504) as batchable— the core fix. NewisTargetPushDeadlineError(matcheshttp 408/http 504) andisBatchableTargetPushError(body-limit OR deadline). Wired into both the one-shot auto-batch decision (autoTargetMaxPackBytes) and the mid-batch subdivision path, so a GitHub 408 now routes into the same batched-bootstrap fallback that 413 already used instead of hard-failing.bootstrap: clearer messaging when a push times out— the auto-batch notice now distinguishes "target push timed out" from "target rejected pack", and a one-shot batchable failure that can't fall back (source lacks the protocol-v2 fetch filter checkpointing needs) gets wrapped with guidance instead of a barehttp 408. Original error stays wrapped forerrors.Is.bootstrap: trim redundant batchable-error test cases— the/simplifycleanup.
Tests: 2 classifier table tests + TestAutoTargetMaxPackBytesTimeoutTriggersBatching + TestExecuteOneShotTimeoutWithoutBatchSupportIsActionable. Full go build/go vet/go test ./... green.
/simplify outcome
Four parallel review agents (reuse, simplification, efficiency, altitude). Net: the diff was already clean and consistent with the package's existing isTargetBodyLimitError substring-matching convention.
- Applied: trimmed
TestIsBatchableTargetPushErrorfrom 6 cases to 3 (the per-status edge cases are owned by the two dedicated predicate tests; the combinator test only needs to prove the OR wires both inputs). - Skipped, with reason: the double
strings.ToLower(err.Error())in the batchable check is cold-path-only (runs once per failed push) — not worth restructuring the predicates. The altitude finding (HTTP-status classification ideally lives ingitprotoas a typed error viaerrors.As, mirroring the recentRefRejectedError/ErrTargetRefMovedwork) is a legitimate architectural improvement but would require refactoring the pre-existing 413 path and threading the status code throughhttpError's two call sites — out of scope for this fix.
One thing worth flagging for a follow-up ticket: unify HTTP-status classification (413 + 408/504) into a typed gitproto error so neither the size nor the timeout path depends on the "http %d" string format. Want me to open that issue, or post the workaround (--target-max-pack-bytes) + root cause as a comment on #70?
PR created: https://github.com/entireio/git-sync/pull/75
- Base
main←fix/target-push-timeout-batched-retry, 3 commits. - Linked to issue #70 via "Fixes #70" — it'll auto-close on merge.
- Body covers the root cause (GitHub's receive-pack deadline + slow relay), the change, the tests, and the typed-error follow-up we discussed (explicitly scoped out, with the reasoning).