# Add optional target capability probe

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

Soph·3mo ago·5 files·+97 added/-6 removed

## Sessions

7aa99a6074b0View transcript

[?\
Can you take a look at the go code (wasm) in /Users/soph/Work/entire/devenv/entire-io-worktree1 based a bit on that I wonder if something like this can be build:Codex·GPT-5.4·1 step](/content/gh/entireio/git-sync/session/019d6d29-8cf7-7fe3-adc9-8c3e4d9d5603#timeline-7aa99a6074b0/index.html)

## Changes

5

- MREADME.md+10/-1
- cmd/git-sync

- Mmain.go+9/-2

- internal/syncer

- Mintegration_test.go+31
  - Mprotocol_v2.go+20
  - Msyncer.go+27/-3

````
9 unmodified lines
10
11
12
13
13
14
15
16
89 unmodified lines

106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

9 unmodified lines

The command surface is:

- `git-sync probe`: inspect a source remote
- `git-sync probe`: inspect a source remote, and optionally a target remote
- `git-sync fetch`: exercise source-side fetch negotiation without pushing
- `git-sync plan`: compute source-to-target ref actions without pushing
- `git-sync sync`: execute the planned changes against the target
89 unmodified lines

<source-url>
```

Probe both source and target remotes to inspect source fetch capabilities and target `receive-pack` capabilities:

```bash
go run ./cmd/git-sync probe \
  --stats \
  <source-url> \
  <target-url>
```

Fetch from a source remote into memory without pushing anywhere:

```bash
````

MREADME.md+10/-1

```
117 unmodified lines

118
119
120
121
122
123
124
125
126
127
128
129
130
6 unmodified lines

137
138
139
136
140
141
142
143
144
145
146
111 unmodified lines

258
259
260
254
261
262
263
264

117 unmodified lines

cfg := syncer.Config{}
	fs.StringVar(&cfg.Source.URL, "source-url", "", "source repository URL")
	fs.StringVar(&cfg.Target.URL, "target-url", "", "optional target repository URL")
	fs.StringVar(&cfg.Source.Token, "source-token", envOr("GITSYNC_SOURCE_TOKEN", ""), "source token/password")
	fs.StringVar(&cfg.Target.Token, "target-token", envOr("GITSYNC_TARGET_TOKEN", ""), "target token/password")
	fs.StringVar(&cfg.Source.Username, "source-username", envOr("GITSYNC_SOURCE_USERNAME", "git"), "source basic auth username")
	fs.StringVar(&cfg.Target.Username, "target-username", envOr("GITSYNC_TARGET_USERNAME", "git"), "target basic auth username")
	fs.StringVar(&cfg.Source.BearerToken, "source-bearer-token", envOr("GITSYNC_SOURCE_BEARER_TOKEN", ""), "source bearer token")
	fs.StringVar(&cfg.Target.BearerToken, "target-bearer-token", envOr("GITSYNC_TARGET_BEARER_TOKEN", ""), "target bearer token")
	fs.BoolVar(&cfg.IncludeTags, "tags", false, "include tag ref prefixes in probe")
	fs.StringVar(&cfg.ProtocolMode, "protocol", envOr("GITSYNC_PROTOCOL", "auto"), "protocol mode: auto, v1, or v2")
	fs.BoolVar(&cfg.ShowStats, "stats", false, "print transfer statistics")
6 unmodified lines

if cfg.Source.URL == "" && len(positional) > 0 {
		cfg.Source.URL = positional[0]
	}
	if len(positional) > 1 {
	if cfg.Target.URL == "" && len(positional) > 1 {
		cfg.Target.URL = positional[1]
	}
	if len(positional) > 2 {
		return usageError("too many positional arguments")
	}
	if cfg.Source.URL == "" {
111 unmodified lines

}

func usageError(message string) error {
	usage := "usage:\n  git-sync sync [flags] <source-url> <target-url>\n  git-sync plan [flags] <source-url> <target-url>\n  git-sync probe [flags] <source-url>\n  git-sync fetch [flags] <source-url>\n\nsync/plan flags:\n  --branch main,dev\n  --map main:stable\n  --tags\n  --force\n  --prune\n  --stats\n  --protocol auto|v1|v2\n  --source-token ...\n  --target-token ...\n  --source-username git\n  --target-username git\n  --source-bearer-token ...\n  --target-bearer-token ...\n  -v\n\nprobe flags:\n  --tags\n  --stats\n  --protocol auto|v1|v2\n  --source-token ...\n  --source-username git\n  --source-bearer-token ...\n\nfetch flags:\n  --branch main,dev\n  --tags\n  --stats\n  --protocol auto|v1|v2\n  --have-ref main\n  --have <hash>\n  --source-token ...\n  --source-username git\n  --source-bearer-token ...\n"
	usage := "usage:\n  git-sync sync [flags] <source-url> <target-url>\n  git-sync plan [flags] <source-url> <target-url>\n  git-sync probe [flags] <source-url> [target-url]\n  git-sync fetch [flags] <source-url>\n\nsync/plan flags:\n  --branch main,dev\n  --map main:stable\n  --tags\n  --force\n  --prune\n  --stats\n  --protocol auto|v1|v2\n  --source-token ...\n  --target-token ...\n  --source-username git\n  --target-username git\n  --source-bearer-token ...\n  --target-bearer-token ...\n  -v\n\nprobe flags:\n  --tags\n  --stats\n  --protocol auto|v1|v2\n  --source-token ...\n  --source-username git\n  --source-bearer-token ...\n  --target-token ...\n  --target-username git\n  --target-bearer-token ...\n\nfetch flags:\n  --branch main,dev\n  --tags\n  --stats\n  --protocol auto|v1|v2\n  --have-ref main\n  --have <hash>\n  --source-token ...\n  --source-username git\n  --source-bearer-token ...\n"
	if message == "" {
		return errors.New(strings.TrimSpace(usage))
	}
}

Mcmd/git-sync/main.go+9/-2

```

263 unmodified lines

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

263 unmodified lines

}

func TestProbe_IntegrationTargetCapabilities(t *testing.T) {
	sourceRepo, sourceFS := newSourceRepo(t)
	makeCommits(t, sourceRepo, sourceFS, 1)

targetRepo, err := git.Init(memory.NewStorage(), nil)
	if err != nil {
		t.Fatalf("init target repo: %v", err)
	}

sourceServer := newSmartHTTPRepoServerV2(t, sourceRepo)
	targetServer := newSmartHTTPRepoServer(t, targetRepo)
	defer sourceServer.Close()
	defer targetServer.Close()

result, err := Probe(context.Background(), Config{
		Source:       Endpoint{URL: sourceServer.RepoURL()},
		Target:       Endpoint{URL: targetServer.RepoURL()},
		ProtocolMode: protocolModeAuto,
		ShowStats:    true,
	})
	if err != nil {
		t.Fatalf("probe with target failed: %v", err)
	}
	if result.TargetURL != targetServer.RepoURL() {
		t.Fatalf("unexpected target url %q", result.TargetURL)
	}
	if len(result.TargetCaps) == 0 {
		t.Fatalf("expected target capabilities")
	}
}

func TestFetch_IntegrationProtocolV2Source(t *testing.T) {
	sourceRepo, sourceFS := newSourceRepo(t)
	makeCommits(t, sourceRepo, sourceFS, 4)
```

Minternal/syncer/integration_test.go+31

```

297 unmodified lines

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

297 unmodified lines

}

func advCapabilities(adv *packp.AdvRefs) []string {
	if adv == nil || adv.Capabilities == nil {
		return nil
	}
	all := adv.Capabilities.All()
	items := make([]string, 0, len(all))
	for _, cap := range all {
		values := adv.Capabilities.Get(cap)
		if len(values) == 0 {
			items = append(items, string(cap))
			continue
		}
		for _, value := range values {
			items = append(items, string(cap)+"="+value)
		}
	}
	sort.Strings(items)
	return items
}

func listSourceRefsV1(ctx context.Context, conn *transportConn) (*packp.AdvRefs, []*plumbing.Reference, error) {
	adv, err := advertisedRefsV1(ctx, conn, transport.UploadPackServiceName)
	if err != nil {
		
```

Minternal/syncer/protocol_v2.go+20

```

98 unmodified lines

99
100
101
102
103
104
105
106
107
108
109
110
80 unmodified lines

191
192
193
192
194
195
196
197
198
199
200
201
202
203
197 unmodified lines

401
402
403
396
404
405
406
407
1 unmodified line

409
410
411
404
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431

98 unmodified lines

type ProbeResult struct {
	SourceURL     string
	TargetURL     string
	RequestedMode string
	Protocol      string
	RefPrefixes   []string
	Capabilities  []string
	TargetCaps    []string
	Refs          []RefInfo
	Stats         Stats
}
80 unmodified lines

lines = append(lines, "ref-prefixes: "+strings.Join(r.RefPrefixes, ", "))
	}
	if len(r.Capabilities) > 0 {
		lines = append(lines, "capabilities: "+strings.Join(r.Capabilities, ", "))
		lines = append(lines, "source-capabilities: "+strings.Join(r.Capabilities, ", "))
	}
	if r.TargetURL != "" {
		lines = append(lines, "target: "+r.TargetURL)
	}
	if len(r.TargetCaps) > 0 {
		lines = append(lines, "target-capabilities: "+strings.Join(r.TargetCaps, ", "))
	}

lines = append(lines, fmt.Sprintf("refs: %d", len(r.Refs)))

197 unmodified lines

return refInfos[i].Name < refInfos[j].Name
}

return ProbeResult{
	result := ProbeResult{
		SourceURL:     cfg.Source.URL,
		RequestedMode: cfg.ProtocolMode,
		Protocol:      service.protocol,
		Capabilities:  sourceCapabilities(service),
		Refs:          refInfos,
		Stats:         stats.snapshot(),
	}, nil
	}

if cfg.Target.URL != "" {
		targetConn, err := newTransportConn(cfg.Target, "target", stats)
		if err != nil {
			return ProbeResult{}, fmt.Errorf("create target transport: %w", err)
		}
		targetAdv, err := advertisedRefsV1(ctx, targetConn, transport.ReceivePackServiceName)
		if err != nil {
			return ProbeResult{}, fmt.Errorf("list target refs: %w", err)
		}
		result.TargetURL = cfg.Target.URL
		result.TargetCaps = advCapabilities(targetAdv)
		result.Stats = stats.snapshot()
	}

return result, nil
}

func Fetch(ctx context.Context, cfg Config, haveRefs []string, haveHashes []plumbing.Hash) (FetchResult, error) {

```

Minternal/syncer/syncer.go+27/-3
