Skip to content

gitproto: match CommandStatusErr by value so target-ref moves classify#73

Open
nodo wants to merge 1 commit into
mainfrom
fix/ref-rejected-value-target
Open

gitproto: match CommandStatusErr by value so target-ref moves classify#73
nodo wants to merge 1 commit into
mainfrom
fix/ref-rejected-value-target

Conversation

@nodo

@nodo nodo commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

ErrTargetRefMoved (PR #71, unreleased) never attaches on the real push path. asRefRejectedError and annotateLeaseFailure match go-git's CommandStatusErr with a pointer target:

var cs *packp.CommandStatusErr
if !errors.As(err, &cs) { return err }

but go-git returns CommandStatusErr by value from ReportStatus.Error() (value receiver Error(), constructed by value in report_status.go). errors.As against a **CommandStatusErr target never matches a value-typed CommandStatusErr in the chain, so:

  • asRefRejectedError falls through → no *RefRejectedError, moved never set → errors.Is(err, ErrTargetRefMoved) is always false on a live receive-pack rejection.
  • annotateLeaseFailure falls through → the lease-failure hint never fires.

Net effect: every concurrent target-ref rejection (remote ref has changed) passes through unclassified. The downstream mirror-pipeline worker that relies on errors.Is(err, ErrTargetRefMoved) to classify benign races sees nothing — the typed-rejection feature is inert in production.

The existing tests masked this by feeding &packp.CommandStatusErr{} (a pointer) as input, which the pointer target does match — but that is not how go-git hands the error over.

Fix

  • Both functions now use a value errors.As target (var cs packp.CommandStatusErr), matching go-git's actual return type. go-git's own tests confirm a value target is correct (s.ErrorAs(rs.Error(), &CommandStatusErr{})).
  • The table tests now build their input via go-git's real (&packp.CommandStatus{...}).Error() (value), and the underlying-error assertions use value targets.
  • New TestAsRefRejectedError_RealReportStatusPath drives a real ReportStatus.Error() end to end through the production wrap — it fails on the pointer target and passes on the value target, so a pointer-vs-value relapse fails CI.

Verification

go test ./..., go vet, gofmt all clean. Confirmed the new test fails on the pre-fix pointer target and passes after.

Bug was in the unreleased typed-rejection feature (#71); never shipped in a tagged release.

🤖 Generated with Claude Code


Note

Medium Risk
Changes error-classification on the push path (retry vs fatal behavior for embedders using ErrTargetRefMoved), but the fix restores intended unreleased behavior rather than altering semantics.

Overview
Fixes a bug where typed push rejections never activated on real receive-pack failures. annotateLeaseFailure and asRefRejectedError used a pointer errors.As target for go-git's packp.CommandStatusErr, but go-git returns that error by value from ReportStatus.Error() — so live ng statuses passed through unwrapped, errors.As to *RefRejectedError failed, and errors.Is(err, ErrTargetRefMoved) stayed false for concurrent CAS/lease rejections like remote ref has changed.

Both helpers now use a value errors.As target so lease hints and RefRejectedError / ErrTargetRefMoved classification work on the production path. Tests build inputs via CommandStatus.Error() (not hand-made pointers), and a new regression test drives a real ReportStatus.Error() through the wrap chain so a pointer-target relapse fails CI. CHANGELOG documents the fix and notes callers should use a value errors.As for the underlying CommandStatusErr.

Reviewed by Cursor Bugbot for commit 7d7406e. Configure here.

…bustly

asRefRejectedError and annotateLeaseFailure used errors.As against a
*packp.CommandStatusErr (pointer) target, but go-git returns CommandStatusErr
BY VALUE from ReportStatus.Error() (value receiver, constructed by value in
report_status.go). A pointer target never matches a value in the error chain,
so on the real receive-pack path the errors.As fell through, no *RefRejectedError
was built, moved was never set, and errors.Is(err, ErrTargetRefMoved) was always
false. Every live concurrent target-ref rejection ("remote ref has changed")
passed through unclassified — the typed-rejection feature was inert in
production. The same bug silently disabled the lease-failure hint.

Extract the error through a shared commandStatusErr helper that accepts BOTH the
value form (today's go-git) and a *CommandStatusErr, since errors.As is exact
about value-vs-pointer and the form is not part of go-git's (alpha) API contract
— so a future switch can't silently regress classification to "every rejection
unclassified".

The existing unit tests masked the original bug by feeding a pointer
&packp.CommandStatusErr{} as input, which the pointer target did match — not how
go-git hands the error over. Make the table tests build input via go-git's real
(&CommandStatus{}).Error() (value), add TestAsRefRejectedError_RealReportStatusPath
(drives a real ReportStatus.Error() end to end; fails loud on a deeper type
change) and TestAsRefRejectedError_ToleratesPointerCommandStatusErr (pins the
pointer form).

Bug was in the unreleased typed-rejection feature (#71); never shipped tagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f226bb2085bf
@nodo nodo force-pushed the fix/ref-rejected-value-target branch from 7d7406e to 06be847 Compare June 13, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants