[Home](/content/site-root.html)

Log in

# Merge pull request \#54 from entireio/soph/ssh-transport

`42b9cfe`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

Soph·2mo ago·25 files·+1,271 added/-162 removed

Add SSH transport support

## Changes

25

- MREADME.md+3/-1

- docs

- Mprotocol.md+2/-1

- Musage.md+24

- internal

- gitproto

- Aconn.go+18

- Mfetch.go+11/-11

- Mfetch\_test.go+14/-14

- Mpush.go+8/-8

- Mpush\_test.go+4/-4

- Mrefs.go+35/-5

- Mrefs\_test.go+140/-7

- Msmarthttp.go+58/-31

- Msmarthttp\_test.go+21/-21

- Assh.go+334

- Assh\_test.go+286

- strategy

- bootstrap

- Mbootstrap.go+13/-9

- Mbootstrap\_test.go+18/-18

- incremental

- Mincremental.go+2/-2

- Mincremental\_test.go+6/-6

- materialized

- Mmaterialized.go+2/-2

- replicate

- Mreplicate.go+2/-2

- Mreplicate\_test.go+3/-3

- syncer

- Mauth\_test.go+16/-7

- Assh\_integration\_test.go+106

- Msyncer.go+58/-7

- Msyncer\_test.go+87/-3

```
129 unmodified lines

130
131
132
133
133
134
135
136
137
138

129 unmodified lines

### Does it support SSH?

No. `git-sync` supports smart HTTP/HTTPS only.
Yes. `git-sync` supports SSH remotes through the local `ssh` binary, including
`ssh://`, SCP-style `git@host:path.git`, and `git+ssh://` URLs. See
[docs/usage.md](docs/usage.md) for details and current caveats.

### Does it run as a daemon or watch for changes?
```

MREADME.md+3/-1

```
19 unmodified lines

20
21
22
23
23
24
25
26
27

19 unmodified lines

- The pack format itself (object types, deltas, index format) — see [Git's pack-format docs](https://git-scm.com/docs/pack-format)
- Dumb HTTP — `git-sync` does not support it
- SSH transport — `git-sync` is HTTPS-only
- Full SSH transport details — `git-sync` supports SSH, but this document is
  focused on the Smart HTTP wire flow
- Bundle URI, partial clones, and other newer extensions

## Smart HTTP Overview
```

Mdocs/protocol.md+2/-1

````
120 unmodified lines

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

120 unmodified lines

<target-url>
```

### SSH remotes

`git-sync` also supports SSH remotes. Accepted forms include:

- `ssh://git@example.com/org/repo.git`
- `git@example.com:org/repo.git`
- `git+ssh://example.com/org/repo.git`

SSH transport shells out to the local `ssh` binary, so host aliases,
`IdentityFile`, agent-backed keys, and other `~/.ssh/config` behavior come
from your existing SSH setup rather than separate `git-sync` flags.

`git-sync` runs SSH with `BatchMode=yes`, which avoids interactive password or
host-key prompts during syncs. On first contact with a host, add it to
`known_hosts` ahead of time or configure `StrictHostKeyChecking=accept-new`
for that host in your SSH config.

Current limitation: `--progress` and `--show-stats` do not yet include
byte-counted SSH transfer metrics, so `--progress` and `--stats` omit
SSH-side throughput.

If `ssh` is not available on `PATH`, `git-sync` fails early with a clear
`locate ssh binary` error before contacting either remote.

## Sync Behavior

`sync` picks the bootstrap relay path automatically when the target is empty. For non-empty targets, safe fast-forward updates also use a relay path that streams the source pack directly into target `receive-pack` without local materialization. Anything not relay-eligible (force, prune, deletes, tag retargets) falls back to a materialized path bounded by `--materialized-max-objects`.
````

Mdocs/usage.md+24

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package gitproto

import (
	"context"
	"io"
	"net/url"
)

// Conn represents a connection to a Git remote transport such as Smart HTTP
// or SSH.
type Conn interface {
	RequestInfoRefs(ctx context.Context, service string, gitProtocol string) ([]byte, error)
	PostRPCStreamBody(ctx context.Context, service string, body io.Reader, v2 bool, phase string) (io.ReadCloser, error)
	Endpoint() *url.URL
	ProgressWriter() io.Writer
	SetProgressWriter(w io.Writer)
	Close() error
}
```

Ainternal/gitproto/conn.go+18

```
54 unmodified lines

55
56
57
58
58
59
60
61
19 unmodified lines

81
82
83
84
84
85
86
87
14 unmodified lines

102
103
104
105
105
106
107
108
47 unmodified lines

156
157
158
159
159
160
161
162
31 unmodified lines

194
195
196
197
197
198
199
200
201
202
202
203
204
205
34 unmodified lines

240
241
242
243
243
244
245
246
184 unmodified lines

431
432
433
434
434
435
436
437
18 unmodified lines

456
457
458
459
459
460
461
462
2 unmodified lines

465
466
467
468
468
469
470
471
19 unmodified lines

491
492
493
494
494
495
496
497

54 unmodified lines

func (s *RefService) FetchToStore(
	ctx context.Context,
	store storer.Storer,
	conn *Conn,
	conn Conn,
	desired map[plumbing.ReferenceName]DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) error {
19 unmodified lines

// Caller must close the returned ReadCloser.
func (s *RefService) FetchPack(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	desired map[plumbing.ReferenceName]DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) (io.ReadCloser, error) {
14 unmodified lines

func (s *RefService) FetchCommitGraph(
	ctx context.Context,
	store storer.Storer,
	conn *Conn,
	conn Conn,
	ref DesiredRef,
	haves []plumbing.Hash,
) error {
47 unmodified lines

func fetchToStoreV2(
	ctx context.Context,
	store storer.Storer,
	conn *Conn,
	conn Conn,
	caps *V2Capabilities,
	desired map[plumbing.ReferenceName]DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
31 unmodified lines

return err
	}
	defer ioutil.CheckClose(reader, &err)
	return storeV2FetchPack(store, reader, verbose, conn.ProgressOut)
	return storeV2FetchPack(store, reader, verbose, conn.ProgressWriter())
}

func fetchPackV2(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	caps *V2Capabilities,
	desired map[plumbing.ReferenceName]DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
34 unmodified lines

if err != nil {
		return nil, err
	}
	packStream, err := openV2PackStream(reader, verbose, conn.ProgressOut)
	packStream, err := openV2PackStream(reader, verbose, conn.ProgressWriter())
	if err != nil {
		_ = reader.Close()
		return nil, err
184 unmodified lines

func fetchToStoreV1(
	ctx context.Context,
	store storer.Storer,
	conn *Conn,
	conn Conn,
	adv *packp.AdvRefs,
	desired map[plumbing.ReferenceName]DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
18 unmodified lines

if drainErr := drainTrailingNAKs(buffered); drainErr != nil {
		return fmt.Errorf("drain server response: %w", drainErr)
	}
	sbReader := buildSidebandReader(caps, buffered, progressSink(verbose, "source: ", conn.ProgressOut))
	sbReader := buildSidebandReader(caps, buffered, progressSink(verbose, "source: ", conn.ProgressWriter()))
	if err := packfile.UpdateObjectStorage(store, sbReader); err != nil {
		return fmt.Errorf("update object storage: %w", err)
	}
2 unmodified lines

func fetchPackV1(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	adv *packp.AdvRefs,
	desired map[plumbing.ReferenceName]DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
19 unmodified lines

return nil, fmt.Errorf("drain server response: %w", drainErr)
	}
	return &wrappedRC{
		Reader: buildSidebandReader(caps, buffered, progressSink(verbose, "source: ", conn.ProgressOut)),
		Reader: buildSidebandReader(caps, buffered, progressSink(verbose, "source: ", conn.ProgressWriter())),
		Closer: reader,
	}, nil
}
```

Minternal/gitproto/fetch.go+11/-11

```
361 unmodified lines

362
363
364
365
365
366
367
368
42 unmodified lines

411
412
413
414
414
415
416
417
45 unmodified lines

463
464
465
466
466
467
468
469
44 unmodified lines

514
515
516
517
517
518
519
520
31 unmodified lines

552
553
554
555
555
556
557
558
50 unmodified lines

609
610
611
612
612
613
614
615
28 unmodified lines

644
645
646
647
647
648
649
650
35 unmodified lines

686
687
688
689
689
690
691
692
40 unmodified lines

733
734
735
736
736
737
738
739
36 unmodified lines

776
777
778
779
779
780
781
782
129 unmodified lines

912
913
914
915
915
916
917
918
80 unmodified lines

999
1000
1001
1002
1002
1003
1004
1005
39 unmodified lines

1045
1046
1047
1048
1048
1049
1050
1051
36 unmodified lines

1088
1089
1090
1091
1091
1092
1093
1094

361 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		started <- struct{}{}
		<-req.Context().Done()
		return nil, req.Context().Err()
42 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		started <- struct{}{}
		<-req.Context().Done()
		return nil, req.Context().Err()
45 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		started <- struct{}{}
		<-req.Context().Done()
		return nil, req.Context().Err()
44 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewBufferString(FormatPktLine("bogus\n") + "0000"))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
31 unmodified lines

}

ctx, cancel := context.WithCancel(context.Background())
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		body := &blockingPacketBody{
			ctx:         req.Context(),
			startedRead: startedRead,
50 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewBufferString("not-a-valid-server-response"))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
28 unmodified lines

data: []byte("0008NAK\nPACK"),
		err:  io.ErrUnexpectedEOF,
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
35 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewBufferString("0008NAK\nzzzz"))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
40 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewReader(payload))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
36 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewBufferString("0000"))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
129 unmodified lines

}

seenRequest := false
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		seenRequest = true
		body, err := io.ReadAll(req.Body)
		if err != nil {
80 unmodified lines

data: wire.Bytes(),
		err:  io.ErrUnexpectedEOF,
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
39 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewBufferString(FormatPktLine("packfile\n") + "zzzz"))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
36 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	body := &trackingReadCloser{ReadCloser: io.NopCloser(bytes.NewBufferString(FormatPktLine("packfile\n") + "zzzz"))}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		return &http.Response{
			StatusCode: http.StatusOK,
			Request:    req,
```

Minternal/gitproto/fetch\_test.go+14/-14

```
33 unmodified lines

34
35
36
37
37
38
39
40
41
42
43
44
44
45
46
47
92 unmodified lines

140
141
142
143
143
144
145
146
19 unmodified lines

166
167
168
169
169
170
171
172
173
173
174
175
176
24 unmodified lines

201
202
203
204
204
205
206
207
34 unmodified lines

242
243
244
245
245
246
247
248
27 unmodified lines

276
277
278
279
279
280
281
282

33 unmodified lines

// construction without worrying about whether downstream strategies have
// already captured a value copy.
type Pusher struct {
	Conn        *Conn
	Conn        Conn
	Adv         *packp.AdvRefs
	Verbose     bool
	OnRejection func(refName plumbing.ReferenceName, status string)
}

// NewPusher builds a target-side push executor.
func NewPusher(conn *Conn, adv *packp.AdvRefs, verbose bool) *Pusher {
func NewPusher(conn Conn, adv *packp.AdvRefs, verbose bool) *Pusher {
	return &Pusher{Conn: conn, Adv: adv, Verbose: verbose}
}

92 unmodified lines

// sendReceivePack encodes and POSTs a receive-pack request, then decodes the report.
func sendReceivePack(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	req *packp.UpdateRequests,
	packData io.Reader,
	verbose bool,
19 unmodified lines

switch {
	case req.Capabilities.Supports(capability.Sideband64k):
		dem := sideband.NewDemuxer(sideband.Sideband64k, reader)
		dem.Progress = progressSink(verbose, "target: ", conn.ProgressOut)
		dem.Progress = progressSink(verbose, "target: ", conn.ProgressWriter())
		respReader = dem
	case req.Capabilities.Supports(capability.Sideband):
		dem := sideband.NewDemuxer(sideband.Sideband, reader)
		dem.Progress = progressSink(verbose, "target: ", conn.ProgressOut)
		dem.Progress = progressSink(verbose, "target: ", conn.ProgressWriter())
		respReader = dem
	}

24 unmodified lines

// PushObjects pushes locally-materialized objects to the target.
func PushObjects(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	adv *packp.AdvRefs,
	commands []PushCommand,
	store storer.Storer,
34 unmodified lines

// PushPack pushes a pack stream (relay) to the target.
func PushPack(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	adv *packp.AdvRefs,
	commands []PushCommand,
	pack io.ReadCloser,
27 unmodified lines

// PushCommands sends ref update commands without a pack (for ref-only changes).
func PushCommands(
	ctx context.Context,
	conn *Conn,
	conn Conn,
	adv *packp.AdvRefs,
	commands []PushCommand,
	verbose bool,
```

Minternal/gitproto/push.go+8/-8

```
126 unmodified lines

127
128
129
130
130
131
132
133
134
135
136
136
137
138
139
49 unmodified lines

189
190
191
192
192
193
194
195
126 unmodified lines

322
323
324
325
325
326
327
328

126 unmodified lines

}))
}

func connForServer(t *testing.T, srv *httptest.Server) *Conn {
func connForServer(t *testing.T, srv *httptest.Server) *HTTPConn {
	t.Helper()
	ep, err := transport.ParseURL(srv.URL + "/repo.git")
	if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	return NewConn(ep, "test", nil, srv.Client().Transport)
	return NewHTTPConn(ep, "test", nil, srv.Client().Transport)
}

func TestPushPackClosesPackOnSuccess(t *testing.T) {
49 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "target", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "target", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		started <- struct{}{}
		<-req.Context().Done()
		return nil, req.Context().Err()
126 unmodified lines

// Use a nil-transport conn -- we should never reach the network.
	ep, err := transport.ParseURL("https://example.com/repo.git")
	require.NoError(t, err)
	conn := &Conn{Endpoint: ep, HTTP: &http.Client{}}
	conn := &HTTPConn{EndpointURL: ep, HTTP: &http.Client{}}

err = PushPack(context.Background(), conn, adv, []PushCommand{
		{Name: "refs/heads/old", Delete: true},
```

Minternal/gitproto/push\_test.go+4/-4

```
15 unmodified lines

16
17
18
19
20
21
22
23
24
25
12 unmodified lines

38
39
40
37
41
42
43
44
3 unmodified lines

48
49
50
47
51
52
53
54
55
56
57
58
59
60
61
62
25 unmodified lines

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
81
111
112
113
114
43 unmodified lines

158
159
160
131
161
162
163
164
5 unmodified lines

170
171
172
143
173
174
175
176

15 unmodified lines

"github.com/go-git/go-git/v6/plumbing/transport"
)

// GitProtocolV2 is the value passed via the Git-Protocol header / GIT_PROTOCOL
// env var to negotiate protocol v2 with the remote.
const GitProtocolV2 = "version=2"

// RefService encapsulates the result of source ref discovery and the negotiated
// protocol, providing methods for subsequent fetch and pack operations.
type RefService struct {
12 unmodified lines

// ListSourceRefs discovers refs from the source using the configured protocol mode.
// Returns the list of refs and a RefService for subsequent operations.
func ListSourceRefs(ctx context.Context, conn *Conn, protocolMode string, refPrefixes []string) ([]*plumbing.Reference, *RefService, error) {
func ListSourceRefs(ctx context.Context, conn Conn, protocolMode string, refPrefixes []string) ([]*plumbing.Reference, *RefService, error) {
	switch protocolMode {
	case "v1":
		adv, refs, err := listSourceRefsV1(ctx, conn)
3 unmodified lines

return refs, &RefService{Protocol: "v1", V1Adv: adv, HeadTarget: headTargetFromAdv(adv)}, nil

case "auto", "v2":
		data, err := RequestInfoRefs(ctx, conn, transport.UploadPackService, "version=2")
		data, err := RequestInfoRefs(ctx, conn, transport.UploadPackService, GitProtocolV2)
		if err != nil {
			if protocolMode == "auto" && isSSHScheme(conn) {
				refs, svc, v1Err := listSourceRefsAutoV1(ctx, conn)
				if v1Err != nil {
					return nil, nil, errors.Join(err, v1Err)
				}
				return refs, svc, nil
			}
			return nil, nil, err
		}
		if caps, err := DecodeV2Capabilities(bytes.NewReader(data)); err == nil {
25 unmodified lines

}
}

func listSourceRefsAutoV1(ctx context.Context, conn Conn) ([]*plumbing.Reference, *RefService, error) {
	adv, refs, err := listSourceRefsV1(ctx, conn)
	if err != nil {
		return nil, nil, err
	}
	return refs, &RefService{Protocol: "v1", V1Adv: adv, HeadTarget: headTargetFromAdv(adv)}, nil
}

func isSSHScheme(conn Conn) bool {
	if conn == nil || conn.Endpoint() == nil {
		return false
	}
	switch conn.Endpoint().Scheme {
	case "ssh", "git+ssh":
		return true
	}
	return false
}

// AdvertisedRefsV1 fetches and decodes v1 advertised refs for the given service.
func AdvertisedRefsV1(ctx context.Context, conn *Conn, service string) (*packp.AdvRefs, error) {
func AdvertisedRefsV1(ctx context.Context, conn Conn, service string) (*packp.AdvRefs, error) {
	data, err := RequestInfoRefs(ctx, conn, service, "")
	if err != nil {
		return nil, err
43 unmodified lines

return items
}

func listSourceRefsV1(ctx context.Context, conn *Conn) (*packp.AdvRefs, []*plumbing.Reference, error) {
func listSourceRefsV1(ctx context.Context, conn Conn) (*packp.AdvRefs, []*plumbing.Reference, error) {
	adv, err := AdvertisedRefsV1(ctx, conn, transport.UploadPackService)
	if err != nil {
		return nil, nil, err
5 unmodified lines

return adv, refs, nil
}

func listSourceRefsV2(ctx context.Context, conn *Conn, caps *V2Capabilities, prefixes []string) ([]*plumbing.Reference, plumbing.ReferenceName, error) {
func listSourceRefsV2(ctx context.Context, conn Conn, caps *V2Capabilities, prefixes []string) ([]*plumbing.Reference, plumbing.ReferenceName, error) {
	// Always include "HEAD" so the server returns the symref-target attribute
	// for HEAD. Without this, callers that pass only "refs/heads/" or
	// "refs/tags/" prefixes filter HEAD out of the response and lose the
```

Minternal/gitproto/refs.go+35/-5

```
2 unmodified lines

3
4
5
6
7
8
9
10
9 unmodified lines

20
21
22
21
23
24
23
25
26
27
28
1 unmodified line

30
31
32
31
33
34
35
36
15 unmodified lines

52
53
54
53
55
56
57
58
49 unmodified lines

108
109
110
109
111
112
113
114
9 unmodified lines

124
125
126
125
126
127
128
129
130
131
70 unmodified lines

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335

2 unmodified lines

import (
	"context"
	"errors"
	"io"
	"net/url"
	"strings"
	"testing"

9 unmodified lines

hashB := plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")

refs := []*plumbing.Reference{
		plumbing.NewHashReference("refs/heads/main", hashA),
		plumbing.NewHashReference(refsHeadsMain, hashA),
		plumbing.NewHashReference("refs/heads/dev", hashB),
		plumbing.NewSymbolicReference("HEAD", "refs/heads/main"), // symbolic, should be skipped
		plumbing.NewSymbolicReference("HEAD", refsHeadsMain), // symbolic, should be skipped
	}

m := RefHashMap(refs)
1 unmodified line

if len(m) != 2 {
		t.Fatalf("expected 2 entries, got %d", len(m))
	}
	if got := m["refs/heads/main"]; got != hashA {
	if got := m[refsHeadsMain]; got != hashA {
		t.Errorf("refs/heads/main = %s, want %s", got, hashA)
	}
	if got := m["refs/heads/dev"]; got != hashB {
15 unmodified lines

adv := &packp.AdvRefs{}
	adv.Capabilities.Add(capability.SymRef, "HEAD:refs/heads/main")
	if got := headTargetFromAdv(adv); got.String() != "refs/heads/main" {
	if got := headTargetFromAdv(adv); got.String() != refsHeadsMain {
		t.Errorf("headTargetFromAdv = %q, want refs/heads/main", got)
	}

49 unmodified lines

hashA := plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
	hashB := plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
	adv.References = []*plumbing.Reference{
		plumbing.NewHashReference("refs/heads/main", hashA),
		plumbing.NewHashReference(refsHeadsMain, hashA),
		plumbing.NewHashReference("refs/heads/dev", hashB),
	}

9 unmodified lines

for _, ref := range refs {
		found[ref.Name()] = ref.Hash()
	}
	if found["refs/heads/main"] != hashA {
		t.Errorf("refs/heads/main = %s, want %s", found["refs/heads/main"], hashA)
	if found[refsHeadsMain] != hashA {
		t.Errorf("refs/heads/main = %s, want %s", found[refsHeadsMain], hashA)
	}
	if found["refs/heads/dev"] != hashB {
		t.Errorf("refs/heads/dev = %s, want %s", found["refs/heads/dev"], hashB)
70 unmodified lines

t.Fatal("expected error for unsupported protocol mode")
	}
}

func TestListSourceRefsAutoFallsBackToV1AfterSSHV2ProbeError(t *testing.T) {
	t.Parallel()

conn := &stubConn{
		reqInfoRefs: func(_ context.Context, _ string, gitProtocol string) ([]byte, error) {
			if gitProtocol == GitProtocolV2 {
				return nil, errors.New("ssh server rejected v2 probe")
			}

var body strings.Builder
			if _, err := pktline.Writef(&body, "# service=%s\n", transport.UploadPackService); err != nil {
				t.Fatalf("write smart service line: %v", err)
			}
			if err := pktline.WriteFlush(&body); err != nil {
				t.Fatalf("write smart flush: %v", err)
			}
			if _, err := pktline.Writef(&body, "%s HEAD\x00%s\n", strings.Repeat("a", 40), capability.SymRef+"=HEAD:refs/heads/main"); err != nil {
				t.Fatalf("write advertised head: %v", err)
			}
			if _, err := pktline.Writef(&body, "%s refs/heads/main\n", strings.Repeat("a", 40)); err != nil {
				t.Fatalf("write advertised ref: %v", err)
			}
			if err := pktline.WriteFlush(&body); err != nil {
				t.Fatalf("write trailing flush: %v", err)
			}
			return []byte(body.String()), nil
		},
	}

refs, svc, err := ListSourceRefs(t.Context(), conn, "auto", nil)
	if err != nil {
		t.Fatalf("ListSourceRefs(auto) error = %v", err)
	}
	if svc.Protocol != "v1" {
		t.Fatalf("protocol = %q, want v1", svc.Protocol)
	}
	if got := svc.HeadTarget.String(); got != refsHeadsMain {
		t.Fatalf("head target = %q, want refs/heads/main", got)
	}
	foundMain := false
	for _, ref := range refs {
		if ref.Name().String() == refsHeadsMain {
			foundMain = true
			break
		}
	}
	if !foundMain {
		t.Fatalf("refs = %#v, want refs/heads/main to be advertised", refs)
	}
}

func TestListSourceRefsAutoJoinsErrorsWhenV1FallbackAlsoFails(t *testing.T) {
	t.Parallel()

v2Err := errors.New("ssh v2 probe failed")
	v1Err := errors.New("ssh v1 fallback failed")
	conn := &stubConn{
		reqInfoRefs: func(_ context.Context, _ string, gitProtocol string) ([]byte, error) {
			if gitProtocol == GitProtocolV2 {
				return nil, v2Err
			}
			return nil, v1Err
		},
	}

_, _, err := ListSourceRefs(t.Context(), conn, "auto", nil)
	if err == nil {
		t.Fatal("expected error when both v2 probe and v1 fallback fail")
	}
	if !errors.Is(err, v2Err) {
		t.Errorf("err does not wrap v2 error: %v", err)
	}
	if !errors.Is(err, v1Err) {
		t.Errorf("err does not wrap v1 error: %v", err)
	}
}

func TestListSourceRefsAutoDoesNotFallBackForNonSSH(t *testing.T) {
	t.Parallel()

v2Err := errors.New("https v2 probe failed")
	v1Called := false
	conn := &stubConn{
		reqInfoRefs: func(_ context.Context, _ string, gitProtocol string) ([]byte, error) {
			if gitProtocol == GitProtocolV2 {
				return nil, v2Err
			}
			v1Called = true
			return nil, errors.New("unexpected v1 call")
		},
		endpoint: &url.URL{Scheme: "https", Host: "github.com"},
	}

_, _, err := ListSourceRefs(t.Context(), conn, "auto", nil)
	if err == nil {
		t.Fatal("expected error from v2 probe")
	}
	if !errors.Is(err, v2Err) {
		t.Errorf("err does not wrap v2 error: %v", err)
	}
	if v1Called {
		t.Error("v1 fallback should not be attempted for non-SSH schemes")
	}
}

type stubConn struct {
	reqInfoRefs func(ctx context.Context, service string, gitProtocol string) ([]byte, error)
	endpoint    *url.URL
}

func (s *stubConn) RequestInfoRefs(ctx context.Context, service string, gitProtocol string) ([]byte, error) {
	return s.reqInfoRefs(ctx, service, gitProtocol)
}

func (s *stubConn) PostRPCStreamBody(context.Context, string, io.Reader, bool, string) (io.ReadCloser, error) {
	return nil, errors.New("unexpected PostRPCStreamBody call")
}

func (s *stubConn) Endpoint() *url.URL {
	if s.endpoint != nil {
		return s.endpoint
	}
	return &url.URL{Scheme: "ssh", Host: "github.com"}
}

func (s *stubConn) ProgressWriter() io.Writer { return nil }

func (s *stubConn) SetProgressWriter(io.Writer) {}

func (s *stubConn) Close() error { return nil }
```

Minternal/gitproto/refs\_test.go+140/-7

```
69 unmodified lines

70
71
72
73
74
75
76
77
78
73
74
75
76
77
78
79
80
81
14 unmodified lines

96
97
98
99
100
99
100
101
102
102
103
104
105
105
106
107
108
108
109
110
111
112
113
114
115
116
117
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
19 unmodified lines

151
152
153
146
147
154
155
156
157
158
159
160
161
162
163
164
165
166
167
4 unmodified lines

172
173
174
158
175
176
160
177
178
179
180
12 unmodified lines

193
194
195
179
196
197
181
182
183
198
199
200
201
202
203
11 unmodified lines

215
216
217
201
218
219
220
221
13 unmodified lines

235
236
237
221
238
239
240
241
242
243
227
228
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
3 unmodified lines

262
263
264
238
265
266
240
267
268
242
269
270
271
272

69 unmodified lines

Authorizer(req *http.Request) error
}

// Conn represents a connection to a remote Git HTTP endpoint.
type Conn struct {
	Label    string
	Endpoint *url.URL
	HTTP     *http.Client
	Auth     AuthMethod
// HTTPConn represents a connection to a remote Git HTTP endpoint.
type HTTPConn struct {
	Label       string
	EndpointURL *url.URL
	HTTP        *http.Client
	Auth        AuthMethod

// FollowInfoRefsRedirect, when true, rewrites Endpoint.Scheme and
	// Endpoint.Host to the final URL returned by RequestInfoRefs after
14 unmodified lines

ProgressOut io.Writer
}

// NewConn creates a new connection to the given endpoint.
func NewConn(ep *url.URL, label string, auth AuthMethod, rt http.RoundTripper) *Conn {
// NewHTTPConn creates a new connection to the given endpoint.
func NewHTTPConn(ep *url.URL, label string, auth AuthMethod, rt http.RoundTripper) *HTTPConn {
	httpClient := &http.Client{Transport: rt}
	return NewConnWithHTTPClient(ep, label, auth, httpClient)
	return NewHTTPConnWithClient(ep, label, auth, httpClient)
}

// NewConnWithHTTPClient creates a new connection using the provided HTTP client.
// NewHTTPConnWithClient creates a new connection using the provided HTTP client.
// Passing nil falls back to a default client and is intended only for direct
// callers outside git-sync's normal instrumented session setup.
func NewConnWithHTTPClient(ep *url.URL, label string, auth AuthMethod, httpClient *http.Client) *Conn {
func NewHTTPConnWithClient(ep *url.URL, label string, auth AuthMethod, httpClient *http.Client) *HTTPConn {
	if httpClient == nil {
		httpClient = &http.Client{Transport: http.DefaultTransport}
	}
	normalizeEndpointPath(ep)
	return &Conn{
		Label:    label,
		Endpoint: ep,
		HTTP:     httpClient,
		Auth:     auth,
	return &HTTPConn{
		Label:       label,
		EndpointURL: ep,
		HTTP:        httpClient,
		Auth:        auth,
	}
}

func (c *HTTPConn) Endpoint() *url.URL { return c.EndpointURL }

func (c *HTTPConn) ProgressWriter() io.Writer { return c.ProgressOut }

func (c *HTTPConn) SetProgressWriter(w io.Writer) { c.ProgressOut = w }

func (c *HTTPConn) Close() error { return nil }

func normalizeEndpointPath(ep *url.URL) {
	if ep == nil {
		return
19 unmodified lines

}

// RequestInfoRefs fetches /info/refs for the given service.
func RequestInfoRefs(ctx context.Context, conn *Conn, service string, gitProtocol string) ([]byte, error) {
	reqURL := fmt.Sprintf("%s/info/refs?service=%s", conn.Endpoint.String(), service)
func RequestInfoRefs(ctx context.Context, conn Conn, service string, gitProtocol string) ([]byte, error) {
	data, err := conn.RequestInfoRefs(ctx, service, gitProtocol)
	if err != nil {
		return nil, fmt.Errorf("request info refs: %w", err)
	}
	return data, nil
}

// RequestInfoRefs fetches /info/refs for the given service.
func (c *HTTPConn) RequestInfoRefs(ctx context.Context, service string, gitProtocol string) ([]byte, error) {
	reqURL := fmt.Sprintf("%s/info/refs?service=%s", c.EndpointURL.String(), service)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("create info-refs request: %w", err)
4 unmodified lines

if gitProtocol != "" {
		req.Header.Set("Git-Protocol", gitProtocol)
	}
	ApplyAuth(req, conn.Auth)
	ApplyAuth(req, c.Auth)

res, err := conn.HTTP.Do(req)
	res, err := c.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request info-refs: %w", err)
	}
12 unmodified lines

if gotMediaType != wantContentType {
		return nil, fmt.Errorf("unexpected info/refs content-type %q, want %q", gotContentType, wantContentType)
	}
	if conn.FollowInfoRefsRedirect && res.Request != nil && res.Request.URL != nil {
	if c.FollowInfoRefsRedirect && res.Request != nil && res.Request.URL != nil {
		final := res.Request.URL
		if final.Host != conn.Endpoint.Host || final.Scheme != conn.Endpoint.Scheme {
			conn.Endpoint.Scheme = final.Scheme
			conn.Endpoint.Host = final.Host
		if final.Host != c.EndpointURL.Host || final.Scheme != c.EndpointURL.Scheme {
			c.EndpointURL.Scheme = final.Scheme
			c.EndpointURL.Host = final.Host
		}
	}
	// Bound the read to prevent unbounded memory allocation (issue #9).
11 unmodified lines

// PostRPC sends a buffered POST to the given service and returns the full response body.
// Responses are bounded to prevent unbounded memory allocation (issue #9).
func PostRPC(ctx context.Context, conn *Conn, service string, body []byte, v2 bool, phase string) ([]byte, error) {
func PostRPC(ctx context.Context, conn Conn, service string, body []byte, v2 bool, phase string) ([]byte, error) {
	reader, err := PostRPCStream(ctx, conn, service, body, v2, phase)
	if err != nil {
		return nil, err
13 unmodified lines

// PostRPCStream sends a POST to the given service and returns the response body
// as a streaming reader. Caller must close the returned ReadCloser.
func PostRPCStream(ctx context.Context, conn *Conn, service string, body []byte, v2 bool, phase string) (io.ReadCloser, error) {
func PostRPCStream(ctx context.Context, conn Conn, service string, body []byte, v2 bool, phase string) (io.ReadCloser, error) {
	return PostRPCStreamBody(ctx, conn, service, bytes.NewReader(body), v2, phase)
}

// PostRPCStreamBody sends a POST to the given service using a streaming request body.
// Caller must close the returned ReadCloser.
func PostRPCStreamBody(ctx context.Context, conn *Conn, service string, body io.Reader, v2 bool, phase string) (io.ReadCloser, error) {
	reqURL := fmt.Sprintf("%s/%s", conn.Endpoint.String(), service)
func PostRPCStreamBody(ctx context.Context, conn Conn, service string, body io.Reader, v2 bool, phase string) (io.ReadCloser, error) {
	reader, err := conn.PostRPCStreamBody(ctx, service, body, v2, phase)
	if err != nil {
		return nil, fmt.Errorf("post RPC stream body: %w", err)
	}
	return reader, nil
}

// PostRPCStreamBody sends a POST to the given service using a streaming request body.
// Caller must close the returned ReadCloser.
func (c *HTTPConn) PostRPCStreamBody(ctx context.Context, service string, body io.Reader, v2 bool, phase string) (io.ReadCloser, error) {
	reqURL := fmt.Sprintf("%s/%s", c.EndpointURL.String(), service)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, body)
	if err != nil {
		return nil, fmt.Errorf("create RPC request: %w", err)
3 unmodified lines

req.Header.Set("User-Agent", capability.DefaultAgent())
	req.Header.Set(StatsPhaseHeader, phase)
	if v2 {
		req.Header.Set("Git-Protocol", "version=2")
		req.Header.Set("Git-Protocol", GitProtocolV2)
	}
	ApplyAuth(req, conn.Auth)
	ApplyAuth(req, c.Auth)

res, err := conn.HTTP.Do(req)
	res, err := c.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("post RPC: %w", err)
	}
```

Minternal/gitproto/smarthttp.go+58/-31

```
14 unmodified lines

15
16
17
18
18
19
20
21
22
23
24
24
25
26
27
28
29
30
29
30
31
32
33
3 unmodified lines

37
38
39
40
40
41
42
43
44
45
46
46
47
48
49
8 unmodified lines

58
59
60
61
62
61
62
63
64
65
75 unmodified lines

141
142
143
144
144
145
146
147
3 unmodified lines

151
152
153
154
154
155
156
157
53 unmodified lines

211
212
213
214
214
215
216
217
32 unmodified lines

250
251
252
253
253
254
255
256
28 unmodified lines

285
286
287
288
288
289
290
291
15 unmodified lines

307
308
309
310
310
311
312
313
1 unmodified line

315
316
317
318
319
318
319
320
321
322
37 unmodified lines

360
361
362
363
363
364
365
366
20 unmodified lines

387
388
389
390
390
391
392
393
13 unmodified lines

407
408
409
410
410
411
412
413
414
415
416
417
418
417
418
419
420
421

14 unmodified lines

transporthttp "github.com/go-git/go-git/v6/plumbing/transport/http"
)

func TestNewConn(t *testing.T) {
func TestNewHTTPConn(t *testing.T) {
	ep, err := transport.ParseURL("https://github.com/user/repo.git")
	if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	auth := &transporthttp.BasicAuth{Username: "user", Password: "pass"}
	conn := NewConn(ep, "test-label", auth, http.DefaultTransport)
	conn := NewHTTPConn(ep, "test-label", auth, http.DefaultTransport)

if conn.Label != "test-label" {
		t.Errorf("Label = %q, want %q", conn.Label, "test-label")
	}
	if conn.Endpoint != ep {
		t.Error("Endpoint mismatch")
	if conn.EndpointURL != ep {
		t.Error("EndpointURL mismatch")
	}
	if conn.Auth != auth {
		t.Error("Auth mismatch")
3 unmodified lines

}
}

func TestNewConnStripsTrailingEndpointSlash(t *testing.T) {
func TestNewHTTPConnStripsTrailingEndpointSlash(t *testing.T) {
	ep, err := url.Parse("https://example.com/repo.git///")
	if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	var gotURLs []string
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		gotURLs = append(gotURLs, req.URL.String())
		res := &http.Response{
			StatusCode: http.StatusOK,
8 unmodified lines

return res, nil
	}))

if got, want := conn.Endpoint.Path, "/repo.git"; got != want {
		t.Fatalf("Endpoint.Path = %q, want %q", got, want)
	if got, want := conn.EndpointURL.Path, "/repo.git"; got != want {
		t.Fatalf("EndpointURL.Path = %q, want %q", got, want)
	}
	if _, err := RequestInfoRefs(t.Context(), conn, transport.UploadPackService, ""); err != nil {
		t.Fatalf("RequestInfoRefs: %v", err)
75 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		started <- struct{}{}
		<-req.Context().Done()
		return nil, req.Context().Err()
3 unmodified lines

done := make(chan error, 1)
	go func() {
		_, err := RequestInfoRefs(ctx, conn, "git-upload-pack", "version=2")
		_, err := RequestInfoRefs(ctx, conn, "git-upload-pack", GitProtocolV2)
		done <- err
	}()

53 unmodified lines

if err != nil {
				t.Fatalf("parse endpoint: %v", err)
			}
			conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
			conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
				res := &http.Response{
					StatusCode: http.StatusOK,
					Request:    req,
32 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
	conn := NewHTTPConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
		started <- struct{}{}
		<-req.Context().Done()
		return nil, req.Context().Err()
28 unmodified lines

}

// TestRequestInfoRefs_FollowInfoRefsRedirect verifies that when the flag is
// set, a 307 on /info/refs rewrites Conn.Endpoint.Host so subsequent PostRPC
// set, a 307 on /info/refs rewrites HTTPConn.EndpointURL.Host so subsequent PostRPC
// calls target the redirected node. Matches vanilla git's smart-HTTP
// behaviour and lets clients use a cluster entry domain for info/refs while
// packs land on the hosting replica.
15 unmodified lines

if err != nil {
		t.Fatalf("parse endpoint: %v", err)
	}
	conn := NewConn(ep, "test", nil, http.DefaultTransport)
	conn := NewHTTPConn(ep, "test", nil, http.DefaultTransport)
	conn.FollowInfoRefsRedirect = true

if _, err := RequestInfoRefs(t.Context(), conn, transport.UploadPackService, ""); err != nil {
1 unmodified line

}

nodeURL := strings.TrimPrefix(node.URL, "http://")
	if conn.Endpoint.Host != nodeURL {
		t.Errorf("Endpoint.Host = %q, want %q (endpoint should follow the 307)", conn.Endpoint.Host, nodeURL)
	if conn.EndpointURL.Host != nodeURL {
		t.Errorf("EndpointURL.Host = %q, want %q (endpoint should follow the 307)", conn.EndpointURL.Host, nodeURL)
	}
}

37 unmodified lines

if _, err := RequestInfoRefs(t.Context(), conn, transport.UploadPackService, ""); err != nil {
20 unmodified lines

}

// TestRequestInfoRefs_DoesNotFollowByDefault confirms the default behaviour
// is unchanged: Endpoint is stable even if the server 307s.
// is unchanged: EndpointURL is stable even if the server 307s.
func TestRequestInfoRefs_DoesNotFollowByDefault(t *testing.T) {
	node := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
13 unmodified lines

t.Fatalf("parse endpoint: %v", err)
	}
	entryHost := ep.Host
	conn := NewConn(ep, "test", nil, http.DefaultTransport)
	conn := NewHTTPConn(ep, "test", nil, http.DefaultTransport)
	// FollowInfoRefsRedirect intentionally not set.

if _, err := RequestInfoRefs(t.Context(), conn, transport.UploadPackService, ""); err != nil {
		t.Fatalf("RequestInfoRefs: %v", err)
	}

if conn.Endpoint.Host != entryHost {
		t.Errorf("Endpoint.Host = %q, want %q (endpoint should be unchanged by default)", conn.Endpoint.Host, entryHost)
	if conn.EndpointURL.Host != entryHost {
		t.Errorf("EndpointURL.Host = %q, want %q (endpoint should be unchanged by default)", conn.EndpointURL.Host, entryHost)
	}
}
```

Minternal/gitproto/smarthttp\_test.go+21/-21

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334

package gitproto

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os/exec"
	"strings"
	"sync"
)

// SSHLookPath is replaceable in tests.
var SSHLookPath = exec.LookPath

// SSHConn represents a Git transport over the local ssh binary.
type SSHConn struct {
	Label       string
	EndpointURL *url.URL
	sshPath     string
	progressOut io.Writer
}

// NewSSHConn creates a new SSH transport connection backed by the local ssh
// binary.
func NewSSHConn(ep *url.URL, label string) (*SSHConn, error) {
	sshPath, err := SSHLookPath("ssh")
	if err != nil {
		return nil, fmt.Errorf("locate ssh binary: %w", err)
	}
	normalizeEndpointPath(ep)
	return &SSHConn{
		Label:       label,
		EndpointURL: ep,
		sshPath:     sshPath,
	}, nil
}

func (c *SSHConn) Endpoint() *url.URL { return c.EndpointURL }

func (c *SSHConn) ProgressWriter() io.Writer { return c.progressOut }

func (c *SSHConn) SetProgressWriter(w io.Writer) { c.progressOut = w }

func (c *SSHConn) Close() error { return nil }

func (c *SSHConn) RequestInfoRefs(ctx context.Context, service string, gitProtocol string) ([]byte, error) {
	cmd, stderr, err := c.startRPC(ctx, service, gitProtocol)
	if err != nil {
		return nil, err
	}
	return requestInfoRefsWithCommand(ctx, service, cmd, stderr)
}

func requestInfoRefsWithCommand(ctx context.Context, service string, cmd *sshCommand, stderr *sshCommandError) ([]byte, error) {
	if err := cmd.Stdin.Close(); err != nil {
		return nil, fmt.Errorf("close ssh stdin for %s: %w", service, errors.Join(err, cleanupSSHCommand(cmd)))
	}
	data, readErr := io.ReadAll(cmd.Stdout)
	waitErr := cmd.wait()
	if ctx.Err() != nil {
		return nil, errors.Join(ctx.Err(), readErr, stderr.wrap(waitErr))
	}
	if readErr != nil {
		return nil, fmt.Errorf("%s info-refs: %w", service, readErr)
	}
	if len(data) > 0 {
		return data, nil
	}
	if waitErr != nil {
		return nil, fmt.Errorf("%s info-refs: %w", service, stderr.wrap(waitErr))
	}
	return data, nil
}

func (c *SSHConn) PostRPCStreamBody(ctx context.Context, service string, body io.Reader, v2 bool, phase string) (io.ReadCloser, error) {
	_ = phase // phase labels are HTTP-only today; SSH transport has no per-RPC stats tagging
	gitProtocol := ""
	if v2 {
		gitProtocol = GitProtocolV2
	}
	cmd, stderr, err := c.startRPC(ctx, service, gitProtocol)
	if err != nil {
		return nil, err
	}
	copyErr := make(chan error, 1)
	go func() {
		_, err := io.Copy(cmd.Stdin, body)
		closeErr := cmd.Stdin.Close()
		if err != nil {
			copyErr <- err
			return
		}
		copyErr <- closeErr
	}()
	stdout, err := discardSSHAdvertisement(cmd.Stdout)
	if err != nil {
		_ = cmd.Stdout.Close()
		waitErr := cmd.wait()
		return nil, fmt.Errorf("%s advertisement: %w", service, errors.Join(stderr.wrap(err), waitErr))
	}
	return &sshRPCStream{
		ctx:     ctx,
		stdout:  stdout,
		wait:    cmd.wait,
		copyErr: copyErr,
		stderr:  stderr,
	}, nil
}

func (c *SSHConn) startRPC(ctx context.Context, service string, gitProtocol string) (*sshCommand, *sshCommandError, error) {
	args, err := sshInvocationArgs(c.EndpointURL, service, gitProtocol)
	if err != nil {
		return nil, nil, err
	}
	cmd := exec.CommandContext(ctx, c.sshPath, args...)
	stderr := &sshCommandError{}
	cmd.Stderr = stderr
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, nil, fmt.Errorf("open ssh stdout for %s: %w", service, err)
	}
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, nil, fmt.Errorf("open ssh stdin for %s: %w", service, err)
	}
	if err := cmd.Start(); err != nil {
		return nil, nil, fmt.Errorf("start ssh for %s: %w", service, stderr.wrap(err))
	}
	return &sshCommand{Cmd: cmd, Stdin: stdin, Stdout: stdout}, stderr, nil
}

func sshInvocationArgs(ep *url.URL, service string, gitProtocol string) ([]string, error) {
	destination, err := sshDestination(ep)
	if err != nil {
		return nil, err
	}
	remoteCommand, err := sshRemoteCommand(ep, service, gitProtocol)
	if err != nil {
		return nil, err
	}
	args := []string{"-o", "BatchMode=yes"}
	if port := ep.Port(); port != "" {
		args = append(args, "-p", port)
	}
	args = append(args, destination, remoteCommand)
	return args, nil
}

func sshDestination(ep *url.URL) (string, error) {
	if ep == nil || ep.Hostname() == "" {
		return "", errors.New("missing SSH host")
	}
	host := ep.Hostname()
	if ep.User != nil && ep.User.Username() != "" {
		return ep.User.Username() + "@" + host, nil
	}
	return host, nil
}

func sshRemoteCommand(ep *url.URL, service string, gitProtocol string) (string, error) {
	if ep == nil || ep.Path == "" {
		return "", errors.New("missing SSH repository path")
	}
	path := shellQuotePath(ep.Path)
	if gitProtocol != "" {
		return gitProtocolEnv(gitProtocol) + " " + service + " " + path, nil
	}
	return service + " " + path, nil
}

func gitProtocolEnv(gitProtocol string) string {
	return "GIT_PROTOCOL=" + shellQuote(gitProtocol)
}

func shellQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'"
}

func shellQuotePath(path string) string {
	if !strings.HasPrefix(path, "~") {
		return shellQuote(path)
	}
	slash := strings.IndexByte(path, '/')
	if slash < 0 {
		return path
	}
	return path[:slash+1] + shellQuote(path[slash+1:])
}

type sshCommand struct {
	Cmd    *exec.Cmd
	Stdin  io.WriteCloser
	Stdout io.ReadCloser
	waitFn func() error
}

func (c *sshCommand) wait() error {
	if c.waitFn != nil {
		return c.waitFn()
	}
	if err := c.Cmd.Wait(); err != nil {
		return fmt.Errorf("wait for ssh command: %w", err)
	}
	return nil
}

func cleanupSSHCommand(cmd *sshCommand) error {
	if cmd == nil {
		return nil
	}
	var errs []error
	if cmd.Stdout != nil {
		if err := cmd.Stdout.Close(); err != nil {
			errs = append(errs, fmt.Errorf("close ssh stdout: %w", err))
		}
	}
	if err := cmd.wait(); err != nil {
		errs = append(errs, err)
	}
	return errors.Join(errs...)
}

func discardSSHAdvertisement(stdout io.ReadCloser) (io.ReadCloser, error) {
	buffered := bufio.NewReader(stdout)
	header, err := buffered.Peek(4)
	if err != nil {
		if errors.Is(err, io.EOF) {
			return &bufferedReadCloser{Reader: buffered, Closer: stdout}, nil
		}
		return nil, fmt.Errorf("peek SSH advertisement: %w", err)
	}
	if !looksLikePktlineHeader(header) {
		return &bufferedReadCloser{Reader: buffered, Closer: stdout}, nil
	}
	reader := NewPacketReader(buffered)
	for {
		kind, _, err := reader.ReadPacket()
		if err != nil {
			return nil, err
		}
		if kind == PacketFlush {
			break
		}
	}
	return &bufferedReadCloser{Reader: reader.BufReader(), Closer: stdout}, nil
}

func looksLikePktlineHeader(header []byte) bool {
	if len(header) != 4 {
		return false
	}
	var fixed [4]byte
	copy(fixed[:], header)
	switch string(fixed[:]) {
	case "0000", "0001", "0002":
		return true
	}
	_, err := parseHexLength(fixed)
	return err == nil
}

type sshRPCStream struct {
	ctx     context.Context
	stdout  io.ReadCloser
	wait    func() error
	copyErr <-chan error
	stderr  *sshCommandError

waitOnce sync.Once
	waitErr  error
}

type bufferedReadCloser struct {
	*bufio.Reader
	io.Closer
}

func (s *sshRPCStream) Read(p []byte) (int, error) {
	n, err := s.stdout.Read(p)
	return n, err //nolint:wrapcheck // io.Reader contract requires forwarding EOF and stream errors as-is
}

func (s *sshRPCStream) Close() error {
	closeErr := s.stdout.Close()
	s.waitOnce.Do(func() {
		copyErr := <-s.copyErr
		waitErr := s.wait()
		if copyErr != nil {
			s.waitErr = copyErr
			if waitErr != nil {
				s.waitErr = errors.Join(copyErr, s.stderr.wrap(waitErr))
			}
			return
		}
		if waitErr != nil {
			s.waitErr = s.stderr.wrap(waitErr)
		}
		if s.ctx != nil && s.ctx.Err() != nil {
			s.waitErr = errors.Join(s.ctx.Err(), s.waitErr)
		}
	})
	return errors.Join(closeErr, s.waitErr)
}

type sshCommandError struct {
	mu  sync.Mutex
	buf bytes.Buffer
}

func (e *sshCommandError) Write(p []byte) (int, error) {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.buf.Write(p) //nolint:wrapcheck // io.Writer implementation forwards buffer write errors verbatim
}

func (e *sshCommandError) String() string {
	e.mu.Lock()
	defer e.mu.Unlock()
	return strings.TrimSpace(e.buf.String())
}

func (e *sshCommandError) wrap(err error) error {
	if err == nil {
		return nil
	}
	if msg := e.String(); msg != "" {
		return fmt.Errorf("%w: %s", err, msg)
	}
	return err
}
```

Ainternal/gitproto/ssh.go+334

package gitproto

import (
	"context"
	"errors"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

"github.com/go-git/go-git/v6/plumbing/transport"
)

func TestNewSSHConnRequiresBinary(t *testing.T) {
	orig := SSHLookPath
	t.Cleanup(func() { SSHLookPath = orig })
	SSHLookPath = func(string) (string, error) {
		return "", errors.New("not found")
	}

ep, err := transport.ParseURL("ssh://example.com/repo.git")
	if err != nil {
		t.Fatalf("parse url: %v", err)
	}
	_, err = NewSSHConn(ep, "source")
	if err == nil || !strings.Contains(err.Error(), "locate ssh binary") {
		t.Fatalf("NewSSHConn error = %v, want locate ssh binary failure", err)
	}
}

func TestSSHConnRequestInfoRefsHonorsUserConfigAndProtocolV2(t *testing.T) {
	env := newSSHShimEnv(t)
	conn := newSSHTestConn(t, "ssh://example.com/repo.git", env.script)

body, err := conn.RequestInfoRefs(t.Context(), "git-upload-pack", GitProtocolV2)
	if err != nil {
		t.Fatalf("RequestInfoRefs: %v", err)
	}
	if string(body) != "response-1" {
		t.Fatalf("RequestInfoRefs body = %q, want %q", body, "response-1")
	}

logLine := env.logLines(t)[0]
	if got, want := logLine, "example.com\tGIT_PROTOCOL='version=2' git-upload-pack '/repo.git'"; got != want {
		t.Fatalf("ssh invocation = %q, want %q", got, want)
	}
}

func TestSSHConnRequestInfoRefsSupportsSCPStyleAndPort(t *testing.T) {
	env := newSSHShimEnv(t)

scpConn := newSSHTestConn(t, "git@example.com:repo.git", env.script)
	if _, err := scpConn.RequestInfoRefs(t.Context(), "git-upload-pack", ""); err != nil {
		t.Fatalf("scp RequestInfoRefs: %v", err)
	}

portConn := newSSHTestConn(t, "ssh://alice@example.com:2222/repo.git", env.script)
	if _, err := portConn.RequestInfoRefs(t.Context(), "git-upload-pack", ""); err != nil {
		t.Fatalf("port RequestInfoRefs: %v", err)
	}

lines := env.logLines(t)
	if got, want := lines[0], "git@example.com\tgit-upload-pack 'repo.git'"; got != want {
		t.Fatalf("scp invocation = %q, want %q", got, want)
	}
	if got, want := lines[1], "-p 2222 alice@example.com\tgit-upload-pack '/repo.git'"; got != want {
		t.Fatalf("port invocation = %q, want %q", got, want)
	}
}

func TestSSHConnRequestInfoRefsPreservesTildePaths(t *testing.T) {
	env := newSSHShimEnv(t)

conn := newSSHTestConn(t, "git@example.com:~/repo with spaces.git", env.script)
	if _, err := conn.RequestInfoRefs(t.Context(), "git-upload-pack", ""); err != nil {
		t.Fatalf("RequestInfoRefs: %v", err)
	}
	if got, want := env.logLines(t)[0], "git@example.com\tgit-upload-pack ~/'repo with spaces.git'"; got != want {
		t.Fatalf("ssh invocation = %q, want %q", got, want)
	}
}

func TestSSHConnPostRPCStreamBodyCanBeCalledRepeatedly(t *testing.T) {
	env := newSSHShimEnv(t)
	conn := newSSHTestConn(t, "ssh://example.com/repo.git", env.script)

reader1, err := conn.PostRPCStreamBody(t.Context(), "git-upload-pack", strings.NewReader("first-body"), false, "fetch one")
	if err != nil {
		t.Fatalf("PostRPCStreamBody first: %v", err)
	}
	data1, err := io.ReadAll(reader1)
	if err != nil {
		t.Fatalf("read first response: %v", err)
	}
	if err := reader1.Close(); err != nil {
		t.Fatalf("close first response: %v", err)
	}

reader2, err := conn.PostRPCStreamBody(t.Context(), "git-upload-pack", strings.NewReader("second-body"), false, "fetch two")
	if err != nil {
		t.Fatalf("PostRPCStreamBody second: %v", err)
	}
	data2, err := io.ReadAll(reader2)
	if err != nil {
		t.Fatalf("read second response: %v", err)
	}
	if err := reader2.Close(); err != nil {
		t.Fatalf("close second response: %v", err)
	}

if string(data1) != "response-1" || string(data2) != "response-2" {
		t.Fatalf("responses = %q / %q, want response-1 / response-2", data1, data2)
	}
	if got, want := env.body(t, 1), "first-body"; got != want {
		t.Fatalf("first request body = %q, want %q", got, want)
	}
	if got, want := env.body(t, 2), "second-body"; got != want {
		t.Fatalf("second request body = %q, want %q", got, want)
	}
	if got := len(env.logLines(t)); got != 2 {
		t.Fatalf("ssh invocation count = %d, want 2", got)
	}
}

func TestSSHConnRequestInfoRefsHonorsContext(t *testing.T) {
	dir := t.TempDir()
	script := filepath.Join(dir, "ssh-sleep.sh")
	if err := os.WriteFile(script, []byte("#!/bin/sh\nsleep 5\n"), 0o755); err != nil {
		t.Fatalf("write script: %v", err)
	}

conn := newSSHTestConn(t, "ssh://example.com/repo.git", script)
	ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
	defer cancel()

_, err := conn.RequestInfoRefs(ctx, "git-upload-pack", "")
	if err == nil {
		t.Fatal("RequestInfoRefs returned nil error on canceled context")
	}
	if !strings.Contains(err.Error(), "context deadline exceeded") {
		t.Fatalf("RequestInfoRefs error = %v, want context deadline exceeded", err)
	}
}

func TestSSHConnPostRPCStreamBodyHonorsContext(t *testing.T) {
	dir := t.TempDir()
	script := filepath.Join(dir, "ssh-read-sleep.sh")
	if err := os.WriteFile(script, []byte("#!/bin/sh\ncat >/dev/null\nsleep 5\n"), 0o755); err != nil {
		t.Fatalf("write script: %v", err)
	}

conn := newSSHTestConn(t, "ssh://example.com/repo.git", script)
	ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
	defer cancel()

reader, err := conn.PostRPCStreamBody(ctx, "git-upload-pack", strings.NewReader("body"), false, "fetch")
	if err != nil {
		t.Fatalf("PostRPCStreamBody: %v", err)
	}
	if _, err := io.ReadAll(reader); err != nil && !strings.Contains(err.Error(), "context deadline exceeded") {
		t.Fatalf("ReadAll error = %v", err)
	}
	if err := reader.Close(); err == nil || !strings.Contains(err.Error(), "context deadline exceeded") {
		t.Fatalf("reader.Close error = %v, want context deadline exceeded", err)
	}
}

func TestRequestInfoRefsCleansUpWhenStdinCloseFails(t *testing.T) {
	t.Parallel()

stdoutClosed := false
	waitCalled := false
	_, err := requestInfoRefsWithCommand(
		t.Context(),
		"git-upload-pack",
		&sshCommand{
			Stdin:  closeWriterFunc(func() error { return errors.New("close failed") }),
			Stdout: closeReaderFunc(func() error { stdoutClosed = true; return nil }),
			waitFn: func() error { waitCalled = true; return nil },
		},
		&sshCommandError{},
	)
	if err == nil || !strings.Contains(err.Error(), "close ssh stdin for git-upload-pack") {
		t.Fatalf("requestInfoRefsWithCommand error = %v", err)
	}
	if !stdoutClosed {
		t.Fatal("stdout was not closed on stdin-close failure")
	}
	if !waitCalled {
		t.Fatal("wait was not called on stdin-close failure")
	}
}

type sshShimEnv struct {
	script     string
	logFile    string
	bodyPrefix string
}

type closeWriterFunc func() error

func (f closeWriterFunc) Write(p []byte) (int, error) { return len(p), nil }
func (f closeWriterFunc) Close() error                { return f() }

type closeReaderFunc func() error

func (f closeReaderFunc) Read([]byte) (int, error) { return 0, io.EOF }
func (f closeReaderFunc) Close() error             { return f() }

func newSSHShimEnv(t *testing.T) sshShimEnv {
	t.Helper()
	dir := t.TempDir()
	logFile := filepath.Join(dir, "ssh.log")
	countFile := filepath.Join(dir, "count")
	bodyPrefix := filepath.Join(dir, "body-")
	script := filepath.Join(dir, "ssh-shim.sh")
	content := strings.Join([]string{
		"#!/bin/sh",
		"count=0",
		"if [ -f " + shellQuote(countFile) + " ]; then count=$(cat " + shellQuote(countFile) + "); fi",
		"count=$((count+1))",
		"printf '%s' \"$count\" >" + shellQuote(countFile),
		"dest=\"\"",
		"remote=\"\"",
		"if [ \"$1\" = \"-o\" ]; then",
		"  shift 2",
		"fi",
		"if [ \"$1\" = \"-p\" ]; then",
		"  port=\"$2\"",
		"  shift 2",
		"  dest=\"-p $port $1\"",
		"else",
		"  dest=\"$1\"",
		"fi",
		"remote=\"$2\"",
		"printf '%s\\t%s\\n' \"$dest\" \"$remote\" >>" + shellQuote(logFile),
		"cat >" + shellQuote(bodyPrefix) + "\"$count\"",
		"printf 'response-%s' \"$count\"",
	}, "\n")
	if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
		t.Fatalf("write ssh shim: %v", err)
	}
	return sshShimEnv{script: script, logFile: logFile, bodyPrefix: bodyPrefix}
}

func newSSHTestConn(t *testing.T, rawURL, script string) *SSHConn {
	t.Helper()
	orig := SSHLookPath
	t.Cleanup(func() { SSHLookPath = orig })
	SSHLookPath = func(string) (string, error) { return script, nil }

ep, err := transport.ParseURL(rawURL)
	if err != nil {
		t.Fatalf("parse url %q: %v", rawURL, err)
	}
	conn, err := NewSSHConn(ep, "source")
	if err != nil {
		t.Fatalf("NewSSHConn(%q): %v", rawURL, err)
	}
	return conn
}

func (e sshShimEnv) logLines(t *testing.T) []string {
	t.Helper()
	data, err := os.ReadFile(e.logFile)
	if err != nil {
		t.Fatalf("read log: %v", err)
	}
	trimmed := strings.TrimSpace(string(data))
	if trimmed == "" {
		return nil
	}
	return strings.Split(trimmed, "\n")
}

func (e sshShimEnv) body(t *testing.T, count int) string {
	t.Helper()
	data, err := os.ReadFile(e.bodyPrefix + strconv.Itoa(count))
	if err != nil {
		t.Fatalf("read body %d: %v", count, err)
	}
	return string(data)
}
```

Ainternal/gitproto/ssh\_test.go+286

```
39 unmodified lines

40
41
42
43
43
44
45
46
45
46
47
48
49
1276 unmodified lines

1326
1327
1328
1329
1329
1330
1331
1332
13 unmodified lines

1346
1347
1348
1349
1349
1350
1351
1352
1353
1354
1355
1356
7 unmodified lines

1364
1365
1366
1363
1367
1368
1369
1370
11 unmodified lines

1382
1383
1384
1381
1382
1385
1386
1387
1388
1385
1389
1390
1391
1392

39 unmodified lines

// Params holds the inputs for a bootstrap execution.
type Params struct {
	SourceConn    *gitproto.Conn
	SourceConn    gitproto.Conn
	SourceService interface {
		FetchPack(ctx context.Context, conn *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
		FetchCommitGraph(ctx context.Context, store storer.Storer, conn *gitproto.Conn, ref gitproto.DesiredRef, haves []plumbing.Hash) error
		FetchPack(ctx context.Context, conn gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
		FetchCommitGraph(ctx context.Context, store storer.Storer, conn gitproto.Conn, ref gitproto.DesiredRef, haves []plumbing.Hash) error
		SupportsBootstrapBatch() bool
	}
	TargetPusher interface {
1276 unmodified lines

// --- GitHub preflight ---

func githubBatchLimit(ctx context.Context, p Params) (int64, bool) {
	if p.TargetMaxPack > 0 || p.SourceConn == nil || p.SourceConn.Endpoint == nil {
	if p.TargetMaxPack > 0 || p.SourceConn == nil || p.SourceConn.Endpoint() == nil {
		return 0, false
	}
	if p.SourceService == nil || !p.SourceService.SupportsBootstrapBatch() {
13 unmodified lines

return limit, true
}

func lookupGitHubRepoSizeKB(ctx context.Context, conn *gitproto.Conn) (int64, bool) {
func lookupGitHubRepoSizeKB(ctx context.Context, conn gitproto.Conn) (int64, bool) {
	httpConn, ok := conn.(*gitproto.HTTPConn)
	if !ok {
		return 0, false
	}
	owner, repo, ok := GitHubOwnerRepo(conn)
	if !ok {
		return 0, false
7 unmodified lines

req.Header.Set("X-Github-Api-Version", "2022-11-28")
	req.Header.Set("User-Agent", capability.DefaultAgent())
	req.Header.Set(gitproto.StatsPhaseHeader, "github repo metadata")
	resp, err := conn.HTTP.Do(req)
	resp, err := httpConn.HTTP.Do(req)
	if err != nil {
		return 0, false
	}
11 unmodified lines

}

// GitHubOwnerRepo extracts the owner/repo from a GitHub endpoint.
func GitHubOwnerRepo(conn *gitproto.Conn) (string, string, bool) {
	if conn == nil || conn.Endpoint == nil {
func GitHubOwnerRepo(conn gitproto.Conn) (string, string, bool) {
	if conn == nil || conn.Endpoint() == nil {
		return "", "", false
	}
	ep := conn.Endpoint
	ep := conn.Endpoint()
	if ep.Scheme != "http" && ep.Scheme != "https" {
		return "", "", false
	}
```

Minternal/strategy/bootstrap/bootstrap.go+13/-9

```
1148 unmodified lines

1149
1150
1151
1152
1152
1153
1154
1155
1 unmodified line

1157
1158
1159
1160
1160
1161
1162
1163
48 unmodified lines

1212
1213
1214
1215
1216
1215
1216
1217
1218
1219
1220
1221
1221
1222
1223
1224
3 unmodified lines

1228
1229
1230
1231
1231
1232
1233
1234
63 unmodified lines

1298
1299
1300
1301
1301
1302
1303
1304
39 unmodified lines

1344
1345
1346
1347
1347
1348
1349
1350
27 unmodified lines

1378
1379
1380
1381
1381
1382
1383
1384
26 unmodified lines

1411
1412
1413
1414
1414
1415
1416
1417
1418
1418
1419
1420
1421
29 unmodified lines

1451
1452
1453
1454
1454
1455
1456
1457
1458
1458
1459
1460
1461
40 unmodified lines

1502
1503
1504
1505
1505
1506
1507
1508
37 unmodified lines

1546
1547
1548
1549
1550
1551
1549
1550
1551
1552
1553
1554
1554
1555
1556
1557

1148 unmodified lines

_, err := Execute(context.Background(), Params{
		SourceService: fakeBootstrapSource{
			fetchCommitGraph: func(_ context.Context, store storer.Storer, _ *gitproto.Conn, ref gitproto.DesiredRef, _ []plumbing.Hash) error {
			fetchCommitGraph: func(_ context.Context, store storer.Storer, _ gitproto.Conn, ref gitproto.DesiredRef, _ []plumbing.Hash) error {
				graphFetches++
				if ref.SourceRef != mainRef {
					t.Errorf("unexpected commit-graph fetch for %s; subsumed branch should have been skipped", ref.SourceRef)
1 unmodified line

writeLinearCommitChain(t, store, 3)
				return nil
			},
			fetchPack: func(_ context.Context, _ *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				packFetches++
				if _, ok := desired[featureRef]; ok {
					t.Errorf("unexpected pack fetch including feature ref: %+v", desired)
48 unmodified lines

}

type fakeBootstrapSource struct {
	fetchPack        func(context.Context, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
	fetchCommitGraph func(context.Context, storer.Storer, *gitproto.Conn, gitproto.DesiredRef, []plumbing.Hash) error
	fetchPack        func(context.Context, gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
	fetchCommitGraph func(context.Context, storer.Storer, gitproto.Conn, gitproto.DesiredRef, []plumbing.Hash) error
}

func (f fakeBootstrapSource) FetchPack(
	ctx context.Context,
	conn *gitproto.Conn,
	conn gitproto.Conn,
	desired map[plumbing.ReferenceName]gitproto.DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) (io.ReadCloser, error) {
3 unmodified lines

func (f fakeBootstrapSource) FetchCommitGraph(
	ctx context.Context,
	store storer.Storer,
	conn *gitproto.Conn,
	conn gitproto.Conn,
	ref gitproto.DesiredRef,
	haves []plumbing.Hash,
) error {
63 unmodified lines

result, err := Execute(context.Background(), Params{
		SourceService: fakeBootstrapSource{
			fetchPack: func(_ context.Context, _ *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				gotDesired = desired
				if targetRefs != nil {
					t.Fatalf("expected nil target refs during one-shot bootstrap fetch, got %v", targetRefs)
39 unmodified lines

_, 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) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				return pack, nil
			},
		},
27 unmodified lines

_, 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) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				return pack, nil
			},
		},
26 unmodified lines

_, err := Execute(context.Background(), Params{
		SourceService: fakeBootstrapSource{
			fetchCommitGraph: func(_ context.Context, store storer.Storer, _ *gitproto.Conn, _ gitproto.DesiredRef, _ []plumbing.Hash) error {
			fetchCommitGraph: func(_ context.Context, store storer.Storer, _ gitproto.Conn, _ gitproto.DesiredRef, _ []plumbing.Hash) error {
				writeLinearCommitChain(t, store, 1)
				return nil
			},
			fetchPack: func(_ context.Context, _ *gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				return pack, nil
			},
		},
29 unmodified lines

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) {
					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
					},
37 unmodified lines

}

_, err = Execute(context.Background(), Params{
		SourceConn: &gitproto.Conn{
			Endpoint: ep,
			HTTP:     server.Client(),
		SourceConn: &gitproto.HTTPConn{
			EndpointURL: ep,
			HTTP:        server.Client(),
		},
		SourceService: fakeBootstrapSource{
			fetchPack: func(context.Context, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(context.Context, gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				t.Fatal("unexpected fetch")
				return nil, nil //nolint:nilnil // test fake returns nil to signal no data
			},
```

Minternal/strategy/bootstrap/bootstrap\_test.go+18/-18

```
18 unmodified lines

19
20
21
22
22
23
24
24
25
26
27

18 unmodified lines

// Params holds the inputs for an incremental relay execution.
type Params struct {
	SourceConn    *gitproto.Conn
	SourceConn    gitproto.Conn
	SourceService interface {
		FetchPack(ctx context.Context, conn *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
		FetchPack(ctx context.Context, conn gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
	}
	TargetPusher interface {
		PushPack(ctx context.Context, cmds []gitproto.PushCommand, pack io.ReadCloser) error
```

Minternal/strategy/incremental/incremental.go+2/-2

```
112 unmodified lines

113
114
115
116
116
117
118
119
120
121
121
122
123
124
54 unmodified lines

179
180
181
182
182
183
184
185
56 unmodified lines

242
243
244
245
245
246
247
248
48 unmodified lines

297
298
299
300
300
301
302
303
40 unmodified lines

344
345
346
347
347
348
349
350

112 unmodified lines

}

type fakeSourceService struct {
	fetchPack func(context.Context, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
	fetchPack func(context.Context, gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
}

func (f fakeSourceService) FetchPack(
	ctx context.Context,
	conn *gitproto.Conn,
	conn gitproto.Conn,
	desired map[plumbing.ReferenceName]gitproto.DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) (io.ReadCloser, error) {
54 unmodified lines

params := Params{
		SourceService: fakeSourceService{
			fetchPack: func(_ context.Context, _ *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				gotDesired = desired
				gotHaves = targetRefs
				return io.NopCloser(bytes.NewReader([]byte("PACK"))), nil
56 unmodified lines

params := Params{
		SourceService: fakeSourceService{
			fetchPack: func(_ context.Context, _ *gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				gotHaves = targetRefs
				return io.NopCloser(bytes.NewReader([]byte("PACK"))), nil
			},
48 unmodified lines

_, err := Execute(context.Background(), Params{
		SourceService: fakeSourceService{
			fetchPack: func(_ context.Context, _ *gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				return pack, nil
			},
		},
40 unmodified lines

_, err := Execute(context.Background(), Params{
		SourceService: fakeSourceService{
			fetchPack: func(_ context.Context, _ *gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				return pack, nil
			},
		},
```

Minternal/strategy/incremental/incremental\_test.go+6/-6

```
19 unmodified lines

20
21
22
23
23
24
25
25
26
27
28

19 unmodified lines

// Params holds the inputs for a materialized push.
type Params struct {
	Store         storer.Storer
	SourceConn    *gitproto.Conn
	SourceConn    gitproto.Conn
	SourceService interface {
		FetchToStore(ctx context.Context, store storer.Storer, conn *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) error
		FetchToStore(ctx context.Context, store storer.Storer, conn gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) error
	}
	TargetPusher interface {
		PushObjects(ctx context.Context, cmds []gitproto.PushCommand, store storer.Storer, hashes []plumbing.Hash) error
```

Minternal/strategy/materialized/materialized.go+2/-2

```
16 unmodified lines

17
18
19
20
20
21
22
22
23
24
25

16 unmodified lines

// Params holds the inputs for a replication relay execution.
type Params struct {
	SourceConn    *gitproto.Conn
	SourceConn    gitproto.Conn
	SourceService interface {
		FetchPack(ctx context.Context, conn *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
		FetchPack(ctx context.Context, conn gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, haves map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
	}
	TargetPusher interface {
		PushPack(ctx context.Context, cmds []gitproto.PushCommand, pack io.ReadCloser) error
```

Minternal/strategy/replicate/replicate.go+2/-2

```
12 unmodified lines

13
14
15
16
16
17
18
19
20
21
21
22
23
24
26 unmodified lines

51
52
53
54
54
55
56
57

12 unmodified lines

)

func (f fakeSourceService) FetchPack(
	ctx context.Context,
	conn *gitproto.Conn,
	conn gitproto.Conn,
	desired map[plumbing.ReferenceName]gitproto.DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) (io.ReadCloser, error) {
26 unmodified lines

result, err := Execute(context.Background(), Params{
		SourceService: fakeSourceService{
			fetchPack: func(_ context.Context, _ *gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
			fetchPack: func(_ context.Context, _ gitproto.Conn, desired map[plumbing.ReferenceName]gitproto.DesiredRef, targetRefs map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
				gotDesired = desired
				gotHaves = targetRefs
				return io.NopCloser(bytes.NewReader([]byte("PACK"))), nil
```

Minternal/strategy/replicate/replicate\_test.go+3/-3

```
14 unmodified lines

15
16
17
18
19
20
21
26 unmodified lines

48
49
50
50
51
52
53
54
2 unmodified lines

57
58
59
59
60
61
61
62
63
64
65
66
67
68
69
4 unmodified lines

74
75
76
72
77
78
79
80
2 unmodified lines

83
84
85
81
86
87
88
89
90
91
92
84
93
94
86
95
96
97
98

14 unmodified lines

transporthttp "github.com/go-git/go-git/v6/plumbing/transport/http"

"entire.io/entire/git-sync/internal/auth"
	"entire.io/entire/git-sync/internal/gitproto"
)

func TestResolveAuthMethodPrefersExplicitToken(t *testing.T) {
26 unmodified lines

}
}

func TestNewConnSkipTLSVerify(t *testing.T) {
func TestNewHTTPConnSkipTLSVerify(t *testing.T) {
	stats := newStats(false)
	conn, err := newConn(Endpoint{
		URL:           "https://example.com/repo.git",
2 unmodified lines

if err != nil {
		t.Fatalf("new conn: %v", err)
	}
	rt, ok := conn.HTTP.Transport.(*countingRoundTripper)
	httpConn, ok := conn.(*gitproto.HTTPConn)
	if !ok {
		t.Fatalf("expected countingRoundTripper, got %T", conn.HTTP.Transport)
		t.Fatalf("expected *gitproto.HTTPConn, got %T", conn)
	}
	rt, ok := httpConn.HTTP.Transport.(*countingRoundTripper)
	if !ok {
		t.Fatalf("expected countingRoundTripper, got %T", httpConn.HTTP.Transport)
	}
	base, ok := rt.base.(*http.Transport)
	if !ok {
4 unmodified lines

}
}

func TestNewConnUsesProvidedHTTPClient(t *testing.T) {
func TestNewHTTPConnUsesProvidedHTTPClient(t *testing.T) {
	stats := newStats(false)
	baseTransport := http.DefaultTransport
	baseClient := &http.Client{Transport: baseTransport}
2 unmodified lines

if err != nil {
		t.Fatalf("new conn: %v", err)
	}
	if conn.HTTP == baseClient {
	httpConn, ok := conn.(*gitproto.HTTPConn)
	if !ok {
		t.Fatalf("expected *gitproto.HTTPConn, got %T", conn)
	}
	if httpConn.HTTP == baseClient {
		t.Fatalf("expected cloned HTTP client, got original pointer")
	}
	rt, ok := conn.HTTP.Transport.(*countingRoundTripper)
	rt, ok := httpConn.HTTP.Transport.(*countingRoundTripper)
	if !ok {
		t.Fatalf("expected countingRoundTripper, got %T", conn.HTTP.Transport)
		t.Fatalf("expected countingRoundTripper, got %T", httpConn.HTTP.Transport)
	}
	if rt.base != baseTransport {
		t.Fatalf("wrapped base transport = %T, want %T", rt.base, baseTransport)
```

Minternal/syncer/auth\_test.go+16/-7

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

package syncer

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

"entire.io/entire/git-sync/internal/gitproto"
	"github.com/go-git/go-git/v6/plumbing"
)

func TestRun_IntegrationSyncOverSSHShimV2(t *testing.T) {
	root := t.TempDir()
	sourceBare := filepath.Join(root, "source.git")
	targetBare := filepath.Join(root, "target.git")
	worktree := filepath.Join(root, "worktree")
	logFile := filepath.Join(root, "ssh.log")
	shim := filepath.Join(root, "ssh-shim.sh")

runGit(t, root, "init", "--bare", sourceBare)
	runGit(t, root, "init", "--bare", targetBare)
	runGit(t, root, "init", worktree)
	runGit(t, worktree, "config", "user.name", "test")
	runGit(t, worktree, "config", "user.email", "test@example.com")
	writeFile(t, filepath.Join(worktree, "tracked.txt"), "hello over ssh\n")
	runGit(t, worktree, "add", "tracked.txt")
	runGit(t, worktree, "commit", "-m", "initial")
	runGit(t, worktree, "remote", "add", "origin", sourceBare)
	runGit(t, worktree, "push", "origin", "HEAD:refs/heads/"+testBranch)

shimBody := strings.Join([]string{
		"#!/bin/sh",
		"if [ \"$1\" = \"-o\" ]; then",
		"  shift 2",
		"fi",
		"if [ \"$1\" = \"-p\" ]; then",
		"  shift 2",
		"fi",
		"dest=\"$1\"",
		"remote=\"$2\"",
		"printf '%s\\t%s\\n' \"$dest\" \"$remote\" >>" + shSingleQuote(logFile),
		"exec sh -c \"$remote\"",
	}, "\n")
	if err := os.WriteFile(shim, []byte(shimBody), 0o755); err != nil {
		t.Fatalf("write ssh shim: %v", err)
	}

orig := gitproto.SSHLookPath
	t.Cleanup(func() { gitproto.SSHLookPath = orig })
	gitproto.SSHLookPath = func(string) (string, error) {
		return shim, nil
	}

result, err := Run(context.Background(), Config{
		Source:       Endpoint{URL: "ssh://example.com" + sourceBare},
		Target:       Endpoint{URL: "ssh://example.com" + targetBare},
		ProtocolMode: protocolModeAuto,
	})
	if err != nil {
		t.Fatalf("Run over SSH shim failed: %v", err)
	}
	if result.Protocol != protocolModeV2 {
		t.Fatalf("result.Protocol = %q, want %q", result.Protocol, protocolModeV2)
	}

assertGitRefEqual(t, sourceBare, targetBare, plumbing.NewBranchReferenceName(testBranch))

data, err := os.ReadFile(logFile)
	if err != nil {
		t.Fatalf("read ssh log: %v", err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) < 5 {
		t.Fatalf("expected at least 5 ssh invocations, got %d\n%s", len(lines), string(data))
	}

uploadPackCalls := 0
	receivePackCalls := 0
	v2Calls := 0
	for _, line := range lines {
		switch {
		case strings.Contains(line, "git-upload-pack"):
			uploadPackCalls++
		case strings.Contains(line, "git-receive-pack"):
			receivePackCalls++
		}
		if strings.Contains(line, "GIT_PROTOCOL='version=2' git-upload-pack") {
			v2Calls++
		}
	}
	if uploadPackCalls < 3 {
		t.Fatalf("expected repeated source upload-pack RPCs, got %d\n%s", uploadPackCalls, string(data))
	}
	if receivePackCalls < 2 {
		t.Fatalf("expected target receive-pack discovery + push, got %d\n%s", receivePackCalls, string(data))
	}
	if v2Calls == 0 {
		t.Fatalf("expected at least one protocol v2 upload-pack call\n%s", string(data))
	}
}

func shSingleQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'"
}
```

Ainternal/syncer/ssh\_integration\_test.go+106

```
348 unmodified lines

349
350
351
352
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
6 unmodified lines

375
376
377
369
378
379
380
381
158 unmodified lines

540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
2 unmodified lines

567
568
569
542
570
571
572
573
10 unmodified lines

584
585
586
587
588
589
590
591
592
593
594
595
9 unmodified lines

605
606
607
574
608
609
610
611
36 unmodified lines

648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
4 unmodified lines

674
675
676
627
677
678
679
680
681
9 unmodified lines

691
692
693
643
694
695
696
697
698
22 unmodified lines

721
722
723
672
724
725
726

348 unmodified lines

// --- Session setup ---

func newConn(raw Endpoint, label string, stats *statsCollector, httpClient *http.Client) (*gitproto.Conn, error) {
func newConn(raw Endpoint, label string, stats *statsCollector, httpClient *http.Client) (gitproto.Conn, error) { //nolint:ireturn // transport selection intentionally returns the shared connection interface
	ep, err := transport.ParseURL(raw.URL)
	if err != nil {
		return nil, fmt.Errorf("parse endpoint: %w", err)
	}
	switch ep.Scheme {
	case "ssh", "git+ssh":
		stats.setSideDisplay(label, hostnameFromURL(raw.URL))
		conn, err := gitproto.NewSSHConn(ep, label)
		if err != nil {
			return nil, fmt.Errorf("new SSH connection: %w", err)
		}
		return conn, nil
	}
	authEp := auth.Endpoint{
		Username:      raw.Username,
		Token:         raw.Token,
6 unmodified lines

}
	stats.setSideDisplay(label, hostnameFromURL(raw.URL))
	client := instrumentHTTPClient(httpClient, raw.SkipTLSVerify, label, stats)
	conn := gitproto.NewConnWithHTTPClient(ep, label, authMethod, client)
	conn := gitproto.NewHTTPConnWithClient(ep, label, authMethod, client)
	conn.FollowInfoRefsRedirect = raw.FollowInfoRefsRedirect
	return conn, nil
}
158 unmodified lines

return false
}

func sshStatsWarning(cfg Config, sourceConn, targetConn gitproto.Conn) string {
	if !cfg.Progress && !cfg.ShowStats {
		return ""
	}
	hasSSH := false
	if _, ok := sourceConn.(*gitproto.SSHConn); ok {
		hasSSH = true
	}
	if !hasSSH && targetConn != nil {
		if _, ok := targetConn.(*gitproto.SSHConn); ok {
			hasSSH = true
		}
	}
	if !hasSSH {
		return ""
	}
	return "warning: SSH transport does not yet expose byte-counted throughput; --progress and --stats output will omit SSH transfer bytes"
}

// --- Session setup (issue #12) ---

// syncSession holds the shared state for a sync operation, reducing
2 unmodified lines

cfg             Config
	stats           *statsCollector
	logger          *slog.Logger
	sourceConn      *gitproto.Conn
	sourceConn      gitproto.Conn
	sourceService   *gitproto.RefService
	sourceRefMap    map[plumbing.ReferenceName]plumbing.Hash
	target          *targetSession
10 unmodified lines

if s.progress != nil {
		s.progress.terminate()
	}
	if s.sourceConn != nil {
		_ = s.sourceConn.Close()
	}
	if s.target != nil && s.target.conn != nil {
		_ = s.target.conn.Close()
	}
}

// notice surfaces a one-line human-readable event during a sync. When
9 unmodified lines

}

type targetSession struct {
	conn     *gitproto.Conn
	conn     gitproto.Conn
	adv      *packp.AdvRefs
	refMap   map[plumbing.ReferenceName]plumbing.Hash
	features gitproto.TargetFeatures
36 unmodified lines

stats:           newStats(cfg.ShowStats),
		measurementDone: startMeasurement(cfg.MeasureMemory),
	}
	var warnedSSHStats bool
	warnSSHStats := func(sourceConn, targetConn gitproto.Conn) {
		if warnedSSHStats {
			return
		}
		warning := sshStatsWarning(cfg, sourceConn, targetConn)
		if warning == "" {
			return
		}
		warnedSSHStats = true
		out := cfg.progressOut
		if out == nil {
			out = os.Stderr
		}
		fmt.Fprintln(out, warning)
	}
	if cfg.Verbose {
		s.logger = slog.New(slog.NewTextHandler(&sessionStderr{s: s}, &slog.HandlerOptions{
			Level: slog.LevelInfo,
4 unmodified lines

if err != nil {
		return nil, fmt.Errorf("create source transport: %w", err)
	}
	s.sourceConn.ProgressOut = &sessionStderr{s: s}
	s.sourceConn.SetProgressWriter(&sessionStderr{s: s})
	warnSSHStats(s.sourceConn, nil)

refPrefixes := planner.RefPrefixes(planConfig(cfg))
	sourceRefs, sourceService, err := gitproto.ListSourceRefs(ctx, s.sourceConn, cfg.ProtocolMode, refPrefixes)
9 unmodified lines

if err != nil {
			return nil, fmt.Errorf("create target transport: %w", err)
		}
		targetConn.ProgressOut = &sessionStderr{s: s}
		targetConn.SetProgressWriter(&sessionStderr{s: s})
		warnSSHStats(s.sourceConn, targetConn)
		targetAdv, err := gitproto.AdvertisedRefsV1(ctx, targetConn, transport.ReceivePackService)
		if err != nil {
			return nil, fmt.Errorf("list target refs: %w", err)
22 unmodified lines

}
		}
	}

// Start the live progress ticker only after auth resolution and the
	// initial ref-listing round trips have completed. The auth path may
	// shell out to `git credential fill`, which inherits our stderr and
```

Minternal/syncer/syncer.go+58/-7

```
1 unmodified line

2
3
4
5
6
7
8
9
10
11
12
13
14
15
199 unmodified lines

215
216
217
215
218
219
220
221
222
223
224
222
225
226
227
228
229
230
231
232
1 unmodified line

234
235
236
230
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317

1 unmodified line

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

"github.com/go-git/go-git/v6/plumbing"

"entire.io/entire/git-sync/internal/gitproto"
	bstrap "entire.io/entire/git-sync/internal/strategy/bootstrap"
)

199 unmodified lines

// TestNewConn_PropagatesFollowInfoRefsRedirect proves the plumbing from
// Endpoint → gitproto.Conn is in place. Without this the flag on
// Endpoint is dead config.
func TestNewConn_PropagatesFollowInfoRefsRedirect(t *testing.T) {
func TestNewHTTPConn_PropagatesFollowInfoRefsRedirect(t *testing.T) {
	stats := newStats(false)

off, err := newConn(Endpoint{URL: "https://node.example/repo.git"}, "target", stats, nil)
	if err != nil {
		t.Fatalf("new conn (off): %v", err)
	}
	if off.FollowInfoRefsRedirect {
	offHTTP, ok := off.(*gitproto.HTTPConn)
	if !ok {
		t.Fatalf("expected *gitproto.HTTPConn, got %T", off)
	}
	if offHTTP.FollowInfoRefsRedirect {
		t.Error("FollowInfoRefsRedirect should default to false")
	}

1 unmodified line

if err != nil {
		t.Fatalf("new conn (on): %v", err)
	}
	if !on.FollowInfoRefsRedirect {
	onHTTP, ok := on.(*gitproto.HTTPConn)
	if !ok {
		t.Fatalf("expected *gitproto.HTTPConn, got %T", on)
	}
	if !onHTTP.FollowInfoRefsRedirect {
		t.Error("FollowInfoRefsRedirect was not propagated from Endpoint to Conn")
	}
}

func TestNewConnBuildsSSHTransport(t *testing.T) {
	orig := gitproto.SSHLookPath
	t.Cleanup(func() { gitproto.SSHLookPath = orig })
	script := filepath.Join(t.TempDir(), "ssh-stub.sh")
	if err := os.WriteFile(script, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
		t.Fatalf("write ssh stub: %v", err)
	}
	gitproto.SSHLookPath = func(string) (string, error) {
		return script, nil
	}

stats := newStats(false)
	tests := []string{
		"ssh://example.com/repo.git",
		"git+ssh://example.com/repo.git",
		"git@example.com:repo.git",
	}
	for _, raw := range tests {
		t.Run(raw, func(t *testing.T) {
			conn, err := newConn(Endpoint{URL: raw}, "source", stats, nil)
			if err != nil {
				t.Fatalf("new conn: %v", err)
			}
			if _, ok := conn.(*gitproto.SSHConn); !ok {
				t.Fatalf("expected *gitproto.SSHConn, got %T", conn)
			}
		})
	}
}

func TestSSHStatsWarning(t *testing.T) {
	tests := []struct {
		name   string
		cfg    Config
		source gitproto.Conn
		target gitproto.Conn
		want   bool
	}{
		{
			name:   "no flags",
			cfg:    Config{},
			source: &gitproto.SSHConn{},
		},
		{
			name:   "http only",
			cfg:    Config{Progress: true},
			source: &gitproto.HTTPConn{},
			target: &gitproto.HTTPConn{},
		},
		{
			name:   "progress with ssh source",
			cfg:    Config{Progress: true},
			source: &gitproto.SSHConn{},
			want:   true,
		},
		{
			name:   "show stats with ssh target",
			cfg:    Config{ShowStats: true},
			source: &gitproto.HTTPConn{},
			target: &gitproto.SSHConn{},
			want:   true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := sshStatsWarning(tt.cfg, tt.source, tt.target)
			if (got != "") != tt.want {
				t.Fatalf("sshStatsWarning() = %q, want warning=%t", got, tt.want)
			}
		})
	}
}
```

Minternal/syncer/syncer\_test.go+87/-3
