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:

  1. 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 returns false and never pre-batches.
  2. Auto-batch-on-rejection (autoTargetMaxPackBytes, bootstrap.go:1380) only fires for body-limit errors — isTargetBodyLimitError (bootstrap.go:1430) matches 413 / "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:

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:

  1. bootstrap: treat receive-pack timeouts (408/504) as batchable — the core fix. New isTargetPushDeadlineError (matches http 408/http 504) and isBatchableTargetPushError (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.

  2. 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 bare http 408. Original error stays wrapped for errors.Is.

  3. bootstrap: trim redundant batchable-error test cases — the /simplify cleanup.

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.

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