Hint at --force-blind on receive-pack lease failures · Entire
Hint at --force-blind on receive-pack lease failures
322747c→main·
Soph·2mo ago·2 files·+59 added/-1 removed
When a non-BestEffort push gets a CommandStatusErr whose status looks like a lease miss (stale info / fetch first / non-fast-forward / does not match expected old value), wrap it with "target ref X moved or differs from session start; rerun, or use --force-blind to overwrite."
The wrapper preserves the underlying CommandStatusErr via fmt.Errorf %w, so callers and tests that inspect the typed error keep working; the hint is purely additive on the error message.
Closes the reason-text portion of #47.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Sessions
6432aa0b89a7View transcript
[?
can you rebase soph/progress-indicators onto soph/smart-subdivisionClaude Code·1 step](/content/gh/entireio/git-sync/session/3ee1ca7a-a436-44c1-906a-a912c6d33f96#timeline-6432aa0b89a7/index.html)
Changes
2
internal/gitproto
Mpush.go+21/-1
Mpush_test.go+38
6 unmodified lines
7
8
9
10
11
12
13
91 unmodified lines
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
38 unmodified lines
168
169
170
151
171
172
173
174
6 unmodified lines
"fmt"
"io"
"os"
"strings"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/format/packfile"
91 unmodified lines
return req, hasDelete, hasUpdates, nil
}
// annotateLeaseFailure wraps a CommandStatusErr whose status looks like a
// lease failure (target ref moved during the sync, or the captured expected-old
// no longer matches) with a hint pointing users at the retry/override path.
// Other receive-pack errors pass through unchanged.
func annotateLeaseFailure(err error) error {
var cs *packp.CommandStatusErr
if !errors.As(err, &cs) {
return err
}
status := strings.ToLower(cs.Status)
if !strings.Contains(status, "stale info") &&
!strings.Contains(status, "fetch first") &&
!strings.Contains(status, "non-fast-forward") &&
!strings.Contains(status, "does not match") {
return err
}
return fmt.Errorf("%w (target ref %s moved or differs from session start; rerun, or use --force-blind to overwrite)", err, cs.ReferenceName)
}
// sendReceivePack encodes and POSTs a receive-pack request, then decodes the report.
func sendReceivePack(
ctx context.Context,
38 unmodified lines
if onRejection == nil {
if err := report.Error(); err != nil {
return fmt.Errorf("report-status: %w", err)
return fmt.Errorf("report-status: %w", annotateLeaseFailure(err))
}
return nil
}
Minternal/gitproto/push.go+21/-1
2 unmodified lines
3
4
5
6
7
8
9
370 unmodified lines
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
2 unmodified lines
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
370 unmodified lines
r.closed = true
return nil
}
func TestAnnotateLeaseFailureWrapsStaleInfo(t *testing.T) {
cases := []struct {
name string
status string
wrap bool
}{
{name: "stale info", status: "stale info", wrap: true},
{name: "stale info with detail", status: "stale info, exp 1234, got abcd", wrap: true},
{name: "fetch first", status: "fetch first", wrap: true},
{name: "non-fast-forward", status: "non-fast-forward", wrap: true},
{name: "does not match expected old", status: "remote ref does not match expected old value", wrap: true},
{name: "unrelated reason passes through", status: "deny updating a hidden ref", wrap: false},
{name: "case-insensitive match", status: "Stale Info", wrap: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
in := &packp.CommandStatusErr{ReferenceName: "refs/heads/main", Status: c.status}
out := annotateLeaseFailure(in)
wrapped := strings.Contains(out.Error(), "moved or differs from session start")
if wrapped != c.wrap {
t.Fatalf("wrap=%v want=%v (err=%q)", wrapped, c.wrap, out)
}
var inner *packp.CommandStatusErr
if !errors.As(out, &inner) || inner.Status != c.status {
t.Fatalf("annotateLeaseFailure must preserve the underlying CommandStatusErr; got %#v", out)
}
})
}
}
func TestAnnotateLeaseFailurePassesNonCommandStatusErrors(t *testing.T) {
err := errors.New("network blew up")
if got := annotateLeaseFailure(err); got != err {
t.Fatalf("unrelated error should pass through unchanged, got %v", got)
}
}