# Centralize bootstrap capability checks

## Sessions

---

## Changes

5

- docs

- Mrewrite-issue-list.md+3/-1

- internal
  
  - gitproto
  
    - Mfetch.go+16/-8

- Mfetch_test.go+37

- strategy/bootstrap
  
    - Mbootstrap.go+42/-28

- Mbootstrap_test.go+44/-3

```
426 unmodified lines

---

426 unmodified lines

- Batched lightweight-tag creation without an extra pack is now covered directly.
- Basic context cancellation coverage now exists.
- Batched bootstrap resume mismatch and final-tip cutover paths now have direct integration coverage.
- Some harder batch-failure injection and partial cutover failure paths still remain.
- Batched bootstrap reruns now also cover the "target ref already created, temp ref cleanup still pending" recovery path.
- Some harder injected batch-failure paths still remain.

### 22. No benchmark coverage for the expensive paths

---

- All mapping validation happens before network activity. Status: done
- Capability negotiation is centralized and enforced consistently. Status: partial
  Source-side fetch capability checks now live behind `gitproto.RefService` methods, but some target-side relay gating still relies on orchestration wiring rather than a fully typed capability boundary.
- Relay strategies are separate packages with explicit inputs and outputs. Status: done
- Tag creation is correct whether or not a pack transfer is needed. Status: done
- Stats are concurrency-safe. Status: done
```

```go
func (s *RefService) ProtocolName() string {
    if s == nil {
        return ""
    }
    return s.Protocol
}

// FetchFeatures summarizes negotiated source fetch features used by strategies.
type FetchFeatures struct {
    Filter     bool
    IncludeTag bool
}

func (s *RefService) SupportsFetchFeature(feature string) bool {
    return s != nil && s.Protocol == "v2" && s.V2Caps.FetchSupports(feature)
}

func (s *RefService) FetchFeatures() FetchFeatures {
    if s == nil || s.Protocol != "v2" || s.V2Caps == nil {
        return FetchFeatures{}
    }
    return s.V2Caps.FetchSupports(feature)
}
```

```go
func TestFetchFeatures(t *testing.T) {
    v2Caps := &V2Capabilities{
        Caps: map[string]string{
            "fetch": "shallow filter include-tag",
        },
    }
    rs := &RefService{Protocol: "v2", V2Caps: v2Caps}
    features := rs.FetchFeatures()
    if !features.Filter || !features.IncludeTag {
        t.Fatalf("FetchFeatures() = %+v, want filter and include-tag enabled", features)
    }
}
```

```go
// Execute runs the bootstrap strategy (one-shot or batched).
func Execute(ctx context.Context, p Params, relayReason string) (Result, error) {
    plans, err := planner.BuildBootstrapPlans(p.DesiredRefs, p.TargetRefs)
    if err != nil {
        return Result{}, err
    }
    
    result := Result{
        Plans: plans, Relay: true, RelayMode: "bootstrap", RelayReason: relayReason,
    }

if p.TargetPusher == nil {
        return result, fmt.Errorf("bootstrap strategy requires TargetPusher")
    }
}
```

```go
func TestExecuteRequiresTargetPusherBeforeFetch(t *testing.T) {
    mainRef := plumbing.NewBranchReferenceName("main")
    mainHash := plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")

tests := []struct {
        name         string
        batchMaxPack int64
    }{
        {name: "one-shot bootstrap", batchMaxPack: 0},
        {name: "batched bootstrap", batchMaxPack: 1},
    }

for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            calledFetch := false
            _, err := Execute(context.Background(), Params{
                SourceService: fakeBootstrapSource{
                    fetchPack: func(context.Context, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
                        calledFetch = true
                        return io.NopCloser(bytes.NewReader(nil)), nil
                    },
                },
                DesiredRefs: map[plumbing.ReferenceName]planner.DesiredRef{
                    mainRef: {
                        SourceRef:  mainRef,
                        TargetRef:  mainRef,
                        SourceHash: mainHash,
                        Kind:       planner.RefKindBranch,
                    },
                },
                TargetRefs:   map[plumbing.ReferenceName]plumbing.Hash{},
                BatchMaxPack: tt.batchMaxPack,
            }, "missing pusher")
            if err == nil || err.Error() != "bootstrap strategy requires TargetPusher" {
                t.Fatalf("Execute() error = %v, want missing TargetPusher", err)
            }
            if calledFetch {
                t.Fatal("expected bootstrap to fail before fetching source pack")
            }
        })
    }
}
```
