gitproto: retry credential auth against the actual challenge URL
Commit

The 401-retry path replayed against c.EndpointURL even when the 401 came from a cross-host redirect (e.g. github.com → replica.example). Go's http.Client strips Authorization on cross-host redirects (per shouldCopyHeaderOnRedirect), so the retry hit the challenger without auth, got 401 again, and we Reject'd the user's valid credentials — locking them out on the next sync, since Lookup would then return nothing.
Production flow that triggers this:
- GET origin/info/refs (anonymous) → 307 → challenger returns 401.
- res.Request.URL.Host == challenger; challengeURL keyed correctly.
- Lookup(challenger) returns the stored creds.
- Retry builds URL from c.EndpointURL = origin, attaches auth.
- http.Client follows 307 → strips Authorization → challenger 401.
- We hit the reject branch → Reject(challenger, valid-creds). Lost.
Fix:
-
tryHelperRetry captures res.Request.URL (the post-redirect URL the 401 actually came from) and passes it to the retry callback as an override target. The retry hits the challenger directly, no redirect to strip the header.
-
On a successful cross-host retry we also rewrite c.EndpointURL's scheme/host to the challenger via adoptChallengeHost. Otherwise follow-up ops on the same conn would redirect again, lose auth, and have their freshly-Approved creds Reject'd inside resolvePendingHelperCreds when the next 401 came through.
-
EnsureAuthForService had a related milder issue: Lookup ran against c.EndpointURL before the probe, but pendingHelperCreds.url was keyed on the post-probe challenge host. Cross-host: Lookup queried origin (probably no entry), then Approve/Reject targeted the challenger. Restructured to probe first, Lookup against challengeURL, then adopt the challenge host — same shape as the tryHelperRetry fix. Drops the pre-probe Lookup optimization (one fewer Lookup, one extra probe POST when the helper has no creds); necessary trade-off since a user may have creds for the challenger but not for the origin.
doInfoRefsRequest / doPostRPCRequest now accept an optional target *url.URL to support the override; nil keeps the previous "build from
c.EndpointURL" behaviour for non-retry callers.
Regression tests:
- TestRequestInfoRefs_OnUnauthorizedAfterCrossHostRedirectRetries- AgainstChallenger — drives the production scenario end-to-end and asserts (a) the retry RoundTrip lands on the challenger with a Basic auth header, (b) Approve fires keyed on the challenger, (c) no Reject, and (d) c.EndpointURL has adopted the challenger so follow-ups don't redirect.
- TestEnsureAuthForService_CrossHostProbeLooksUpAndAdoptsChallenger — same idea on the EnsureAuthForService path.
cmd/git-sync test that expected exactly one git-receive-pack POST now expects two (auth-probe + real push) since the probe is unconditional; TestMain comment updated to reflect that.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com Entire-Checkpoint: dcdc9414badf