Scope lease-failure escalation to --force-with-lease · Entire

Scope lease-failure escalation to --force-with-lease

5349be7→main·
Soph·2mo ago·2 files·+30 added/-2 removed

The lease-failure marker set in gitproto is intentionally broad to
catch phrasing across servers (stale info / fetch first /
non-fast-forward / does not match). That's right under
--force-with-lease, where the user has explicitly promised "rerun if
target moved." But the same server messages can mean ordinary policy
rejection on a --force-blind or non-force best-effort run, which
BestEffort is allowed to downgrade to warnings.

Gate leaseFailureError on cfg.ForceWithLease so the escalation only
fires when the lease contract is actually in effect. Other rejection
paths follow the BestEffort warn-and-continue contract as documented.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Sessions

153a4deb52baView transcript

Changes

2

429 unmodified lines

// when BestEffort would otherwise downgrade them. Without this, a sync with
// both --force-with-lease and --all-refs (which implies BestEffort) would
// silently treat a concurrent target update as a warning, defeating the lease.
//
// Scoped to --force-with-lease only. The lease-failure marker set in gitproto
// is intentionally broad to cover phrasing across servers (stale info / fetch
// first / non-fast-forward / does not match), but under --force-blind or
// non-force runs those same messages can mean ordinary policy rejection rather
// than a stale lease, which BestEffort is allowed to downgrade. The contract
// is only made when the user explicitly opts in.
func (s *syncSession) leaseFailureError() error {
    if !s.cfg.ForceWithLease {
        return nil
    }
    if len(s.rejections) == 0 {
        return nil
    }
}

Minternal/syncer/syncer.go+10

37 unmodified lines

func TestLeaseFailureErrorEscalatesPastBestEffort(t *testing.T) {
    main := plumbing.NewBranchReferenceName("main")
    pull := plumbing.ReferenceName("refs/pull/1/head")
    leased := Config{ForceWithLease: true}

// Non-lease rejection alone: BestEffort handles it as a warning, no fatal.
    s := &syncSession{rejections: map[plumbing.ReferenceName]string{
        s := &syncSession{cfg: leased, rejections: map[plumbing.ReferenceName]string{
            pull: "deny updating a hidden ref",
        }}
    if err := s.leaseFailureError(); err != nil {
        t.Fatalf("expected nil for non-lease rejection, got %v", err)
    }

// Lease rejection: must escalate, naming the affected ref and the override flag.
    // Lease rejection under --force-with-lease: must escalate, naming the
    // affected ref and the override flag.
    s.rejections[main] = "stale info, exp 1234, got abcd"
    err := s.leaseFailureError()
    if err == nil {
        7 unmodified lines
    }
}

func TestLeaseFailureErrorOnlyAppliesUnderForceWithLease(t *testing.T) {
    main := plumbing.NewBranchReferenceName("main")
    // The broad markers (non-fast-forward / fetch first) can mean ordinary
    // server policy rejection rather than a lease miss. Those rejections must
    // stay warnable under BestEffort when --force-with-lease isn't set.
    rejections := map[plumbing.ReferenceName]string{
        main: "non-fast-forward",
    }
    for _, cfg := range []Config{{}, {ForceBlind: true}} {
        s := &syncSession{cfg: cfg, rejections: rejections}
        if err := s.leaseFailureError(); err != nil {
            t.Errorf("cfg=%+v: expected nil (no lease contract), got %v", cfg, err)
        }
    }
}

func TestApplyRejectionsEmptyMapIsNoOp(t *testing.T) {
    plans := []BranchPlan{{TargetRef: plumbing.NewBranchReferenceName("main"), Action: ActionUpdate}}
    s := &syncSession{}
}`