can you take a look at https://github.com/entireio/git-sync/issues/70

Claude Code·Opus 4.8[1m]·Soph·2mo ago·1hr 37min·5 Checkpoints·7 file changes·+200/-28·128.6K tokens
2mo ago·3m·12 calls

I've dug through the issue and the relevant code. Here's my read.

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.ExecutePushPacksendReceivePack, 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:

  • Classify 408 (and arguably gateway timeouts 502/503/504) as a "split it up" condition feeding autoTargetMaxPackBytes, 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.

Want me to:

  1. Implement the fix (classify 408/timeouts → batched retry + actionable error message), with tests, on a branch; or
  2. Post a diagnostic comment on issue #70 with the workaround and root cause; or
  3. both?

let's implement the fix in a new branch, do meaningful commits, run /simplify when done

2mo ago·6m·25 calls

Now run /simplify on the diff.

/simplify → 4 cleanup agents in parallel → apply the fixes

You are improving the quality of the changed code, not hunting for bugs. Review it for reuse, simplification, efficiency, and altitude issues, then fix what you find. Do not look for correctness bugs — that is what /code-review is for.

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).

2mo ago·2m·8 calls

Done. Here's the summary.

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.

  • Applied: trimmed TestIsBatchableTargetPushError from 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 in gitproto as a typed error via errors.As, mirroring the recent RefRejectedError/ErrTargetRefMoved work) is a legitimate architectural improvement but would require refactoring the pre-existing 413 path and threading the status code through httpError'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?

would the unify http status a bigger effort, or can we do it as part of this PR?

2mo ago·1m·2 calls

Bigger — and bigger than it first looks. I traced it:

What's genuinely easy: httpError (smarthttp.go:39) is the only producer of "http %d" errors, and %w-wrapping preserves the chain all the way up through PushPack and the TargetPusher interface to bootstrap. So a typed *HTTPStatusError{StatusCode, ...} set in that one constructor would be reachable via errors.As downstream. The 408/504 check becomes a clean err.StatusCode == 408.

What makes it a real refactor:

  1. The 413 path isn't status-based, so it doesn't simplify away. isTargetBodyLimitError deliberately matches body text"body exceeded size limit", "request body too large", "payload too large" — because proxies return that signal under varying (often non-413) status codes, and targetBodyLimit parses a number out of that body. So the typed error would have to carry both the status and the response body, and the body-limit detection would still inspect the body string. That's errors.As plus string inspection — more code on that path, not less.

  2. ~53 test assertions across two packages (bootstrap_test.go and syncer/integration_test.go) construct these as plain errors.New("body exceeded size limit …") / errors.New("http 413"). Switching detection to errors.As breaks all of them unless I either keep a string fallback (defeats the point) or rewrite each to build the typed error. That churn — touching the syncer integration tests — is the bulk of the work.

  3. It also mixes "make 408 work" with "re-architect HTTP error classification" in one PR, which muddies review and bisection, and likely means a new public API type (to match the RefRejectedError/ErrTargetRefMoved convention that's re-exported in errors.go).

My recommendation: keep this PR scoped to the timeout fix — it's complete and consistent with the package's own established isTargetBodyLimitError pattern. Do the unification as a follow-up that migrates 413 and 408/504 together into a typed gitproto error, with the test churn isolated to its own diff.

Want me to open that follow-up issue, or would you rather I fold it in here anyway?

can you create a PR

2mo ago·1m·2 calls

PR created: https://github.com/entireio/git-sync/pull/75

  • Base mainfix/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).

Want me to also post the workaround (--target-max-pack-bytes) as a comment on #70 so the reporter is unblocked before this merges, or open the follow-up issue for the HTTP-status unification?

can you look at the copilot comments?

2mo ago·6m·14 calls

Done. Copilot left 3 inline comments (nodo already approved). All three were the same class of issue — wording/naming that became misleading once this path started handling timeouts, not just size rejections — and I addressed each in commit 02883917:

  1. Log message (:189) — "after target rejection" → "after batchable push failure". The reason field already logs the specific cause.
  2. sizeIssue variable (:511) — renamed to subdivide, since it's now true for deadline timeouts too; keyed on the shared remedy instead of implying a size-only cause.
  3. Actionable error (:1493) — "rejected the pack" → "could not receive the pack — too large, or too slow to receive within its deadline".

Build/vet/tests green, pushed, and I posted a summary comment on the PR mapping each fix to its comment. No code-behavior changes — purely the consistency cleanups Copilot flagged.

Base directory for this skill: /Users/soph/Work/entire/devenv/git-sync/.claude/skills/changelog

Changelog Generation

Generate a changelog entry for a new release by analyzing merged PRs since the last tag.

Parameters

The user provides:

  • Version number -- e.g., 0.5.3
  • Additional PRs -- optionally, PRs not yet merged that should be included

Step 1: Gather Data

  1. Find the previous release tag: git tag --sort=-version:refname | head -1
  2. List all merge commits since that tag: git log <tag>..HEAD --merges --format='%s' | grep -v "Merge branch"
  3. Get PR details with: gh pr list --state merged --base main --search "merged:><tag-date>" --json number,title,author --limit 50
  4. List contributors: git log <tag>..HEAD --format='%an' --no-merges | sort | uniq -c | sort -rn

Step 2: Identify External Contributors

To determine if a contributor is external, check for an @entire.io email:

If they have an @entire.io email anywhere in git history, they are internal. Only list external contributors in the Thanks section.

Also check the memory file at memory/project_team_members.md for known internal/external mappings.

Step 3: Write the Entry

Insert the new version section at the top of CHANGELOG.md, after the header and before the previous version.

Format

Follow the existing style in CHANGELOG.md exactly:

Style Rules

  • Sections: Added, Changed, Fixed, Housekeeping, Thanks. Omit empty sections.
  • Each bullet: starts with a dash, concise description, PR link(s) at the end
  • Group related PRs into a single bullet when they're part of the same feature/fix
  • Work-in-progress features: call out explicitly, e.g., "Feature X (work in progress): ..."
  • Known limitations: note inline, e.g., "Note: subagent tracking is not yet supported due to..."
  • Thanks section: only external contributors. Name what they contributed specifically.
  • Dependency bumps: group into a single Housekeeping bullet unless a bump fixes a notable bug
  • PR links: always use full URL format [#N](https://github.com/entireio/git-sync/pull/N)
  • No trailing period on bullet items
  • Date: use the current date in YYYY-MM-DD format

Categorization Guide

  • Added: new features, new commands, new agent integrations, new CI workflows
  • Changed: behavior changes, API changes, UX changes, migrations
  • Fixed: bug fixes, E2E fixes, agent-specific fixes
  • Housekeeping: dependency bumps, docs, refactors, CI improvements, test improvements
2mo ago·2m·7 calls

Done. I generated the 0.7.0 changelog entry (dated 2026-06-16).

What I did

  • No version was passed, so I inferred 0.7.0 — a minor bump, since #71 adds new public API surface (RefRejectedError / ErrTargetRefMoved).
  • The existing [Unreleased] section already described #71 and #73 but had no PR links — I promoted it to [0.7.0] and appended the links.
  • Added the two PRs missing from the section: #74 (empty pack on pack-less ref creates) and #75 (408/504 timeout → batched fallback), both under Fixed.

Entry contents

  • Added — #71: typed push-rejection errors on the public API
  • Fixed — #73 (value-vs-pointer errors.As classification), #74 (subsumed-branch pack-less create), #75 (receive-pack timeout batching)
  • No Thanks section — both contributors (Andrea Nodari, Stefan Haubold) are internal @entire.io.

One judgment call to confirm

I kept the existing #73 "Fixed" bullet, which notes "Bug in the typed-rejection feature above — never shipped in a tagged release." Since #71 (the feature) and #73 (its fix) now ship in the same release, that interim bug arguably doesn't need its own entry — the feature simply works on first release. I left it because it was team-authored and is informative, but I can fold it away (drop the #73 bullet, keep only the #71 Added entry) if you'd prefer a cleaner first-ship narrative. Want me to?