Sync to targets with overlapping reachability · Entire
Sync to targets with overlapping reachability
9c95280→main·
Soph·2mo ago·5 files·+359 added/-20 removed
Three related fixes uncovered while syncing a repo into a target that already had main:
Materialized walker (planner.ObjectsToPush) treats objects pruned from the source pack as implicitly have'd by the target, instead of failing with "object not found". The fetch passes target refs as haves and the source server prunes everything reachable from them; the walker was erroring on those pruned objects even though receive-pack would accept the push (objects are reachable from existing target refs). Applies to both top-level wants and transitive references.
Incremental relay accepts branch creates and tolerates "no-thin" targets. Branch creates with non-empty target now use streaming relay with target refs as haves, instead of falling through to materialized. The no-thin restriction was over-conservative by the same reasoning already documented for replicate relay: gitproto.FetchPack never requests thin-pack, so the relayed pack is always self-contained.
httpError surfaces diagnostic response headers (Cf-Ray, Server, X-Request-Id, Content-Type, ...) when present. A 500 with a body of just "Internal Server Error" is unactionable; the headers give the user something concrete to share with the upstream service operator.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Sessions
0ceb3c36dd52View transcript
[?
git-sync sync --tags https://github.com/entireio/cli.git $ARTIFACTS_REMOTEClaude Code·Opus 4.7[1m]·16 steps
Changes
5
internal
gitproto
Msmarthttp.go+25/-1
planner
Mplanner.go+14
Mplanner_test.go+187/-10
Mrelay.go+25/-9
syncer
Mintegration_test.go+108
15 unmodified lines
const maxHTTPErrorBody = 64 * 1024
// diagnosticHeaders carry trace/correlation IDs that operators of upstream
// services use to look up the failing request server-side. Surfaced in
// httpError so a 500 with an opaque body (e.g. "Internal Server Error") still
// gives the user something actionable to share when reporting the failure.
var diagnosticHeaders = []string{
"Cf-Ray",
"X-Request-Id",
"Request-Id",
"X-Trace-Id",
"X-Amz-Request-Id",
"X-Github-Request-Id",
"Server",
"Content-Type",
};
// httpError checks an HTTP response status and returns an error for non-2xx responses.
func httpError(res *http.Response) error {
if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices {
// ... (truncated for brevity)
}
}
Minternal/gitproto/smarthttp.go+25/-1
406 unmodified lines
}
// ObjectsToPush computes the set of objects that need to be sent to the target.
// A fetch with target refs as haves prunes the source pack server-side, so
// objects reachable from a have are intentionally absent from the local
// store. Both top-level wants and transitive references encountered during
// the walk may be missing for this reason — they're treated as implicitly
// have'd by the target and excluded from the push pack. The target's
// receive-pack accepts ref updates referencing such objects because it
// already has them under one of its existing refs.
func ObjectsToPush(store storer.EncodedObjectStorer, wants []plumbing.Hash, targetRefs map[plumbing.ReferenceName]plumbing.Hash) ([]plumbing.Hash, error) {
haveSet := make(map[plumbing.Hash]struct{})
for _, h := range targetRefs {
// ... (further truncation for brevity)
}
}
Minternal/planner/planner.go+14
1037 unmodified lines
}
// CanIncrementalRelay checks whether all plans are eligible for the incremental
// relay fast-path (fast-forward branch updates + new tag creates).
// relay fast-path. Eligible plan shapes:
// - Branch fast-forward update (TargetHash is parent of SourceHash).
// - Branch create (target has no such ref yet). The relay still passes
// all target refs as haves, so the source pack covers only objects
// target doesn't already have via some existing ref. The receive-pack
// accepts the create command because any pruned objects are reachable
// from those existing refs.
// - New tag create.
// Incremental tolerates targets that advertise "no-thin" for the same reason
// as replicate (see SupportsReplicateRelay): gitproto.FetchPack never requests
// the "thin-pack" capability, so the relayed pack is always self-contained.
// If gitproto.FetchPack ever begins requesting thin-pack, this function must
// gain a matching fallback when target.NoThin is set.
func CanIncrementalRelay(force, prune, dryRun bool, plans []BranchPlan, target RelayTargetPolicy) (bool, string) {
if force || prune || dryRun {
return false, "incremental-disabled-by-force-prune-or-dry-run"
}
// ... (further truncation for brevity)
}
Minternal/planner/planner_test.go+187/-10
52 unmodified lines
// A "Create" plan with a non-zero TargetHash is incoherent — surface it
// rather than silently relay against the wrong have.
plans := []BranchPlan{{
Branch: "feature",
SourceRef: "refs/heads/feature",
TargetRef: "refs/heads/feature",
SourceHash: plumbing.NewHash("1111111111111111111111111111111111111111"),
TargetHash: plumbing.NewHash("2222222222222222222222222222222222222222"),
Kind: RefKindBranch,
Action: ActionCreate,
}}
// ... (further truncation for brevity)
}
Minternal/syncer/integration_test.go+108
157 unmodified lines
}