# Auto-use bootstrap on empty targets

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

Soph·3mo ago·5 files·+133 added/-32 removed

## Sessions

8620d1269809View transcript

## Changes

5

- MREADME.md+2

- cmd/git-sync
  
  - Mmain\_test.go+3

- docs

- Mbootstrap.md+5

- internal/syncer

- Mintegration\_test.go+34
  
  - Msyncer.go+89/-32

```
  88 unmodified lines
  
  89
  90
  91
  92
  93
  94
  95
  96
  
  88 unmodified lines
  
  That is the intended way to compare the bootstrap relay path against the normal sync path on the same fixture or test repo.

When `sync` sees that all managed target refs are absent and the run is compatible with bootstrap semantics, it automatically uses the bootstrap relay path instead of the normal decode-and-repack sync path.
  
  Sync specific branches:

```bash
  ```
  ```
  117 unmodified lines
  
  118
  119
  120
  121
  122
  123
  124
  125
  126
  
  117 unmodified lines
  
	if result["dry_run"] != true {
		t.Fatalf("expected dry_run=true, got %#v", result["dry_run"])
	}
	if result["bootstrap_suggested"] != true {
		t.Fatalf("expected bootstrap_suggested=true, got %#v", result["bootstrap_suggested"])
	}
	plans, ok := result["plans"].([]any)
	if !ok || len(plans) == 0 {
		t.Fatalf("expected plan entries, got %#v", result["plans"])
	}

```

Mcmd/git-sync/main\_test.go+3

```
  150 unmodified lines
  
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  
  150 unmodified lines

- investigate whether target capability combinations require alternate pack handling
- measure source-to-target pack relay memory and CPU against current `sync`

Progress:

- `sync` now auto-selects the bootstrap relay path when all managed target refs are absent and the run matches bootstrap semantics
- dry-run `plan` surfaces a bootstrap suggestion for the same target shape

Phase 4:

- consider a more advanced incremental relay mode for non-empty targets

```
  Mdocs/bootstrap.md+5
  
  ```
  55 unmodified lines
  
  56
  57
  58
  59
  60
  61
  62
  63
  64
  11 unmodified lines
  
  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

55 unmodified lines
  
	if result.Pushed != 1 || result.Blocked != 0 {
		t.Fatalf("unexpected result: %+v", result)
	}
	if !result.Relay {
		t.Fatalf("expected sync to auto-switch to relay bootstrap on empty target")
	}

assertHeadsMatch(t, sourceRepo, targetRepo, testBranch)

11 unmodified lines

}
}

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

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 := Run(context.Background(), Config{
		Source:       Endpoint{URL: sourceServer.RepoURL()},
		Target:       Endpoint{URL: targetServer.RepoURL()},
		DryRun:       true,
		ProtocolMode: protocolModeAuto,
	})
	if err != nil {
		t.Fatalf("plan failed: %v", err)
	}
	if !result.DryRun || !result.BootstrapSuggested {
		t.Fatalf("expected bootstrap suggestion, got %+v", result)
	}
	if result.Relay {
		t.Fatalf("dry-run plan should not execute relay")
	}
}

func TestBootstrap_IntegrationInitialSyncToEmptyTarget(t *testing.T) {
	sourceRepo, sourceFS := newSourceRepo(t)
	makeCommits(t, sourceRepo, sourceFS, 4)
  
  ```
  Minternal/syncer/integration\_test.go+34
  
  ```
  99 unmodified lines
  
  100
  101
  102
  103
  104
  105
  106
  107
  128 unmodified lines
  
  236
  237
  238
  237
  238
  239
  240
  241
  242
  243
  20 unmodified lines
  
  264
  265
  266
  267
  268
  269
  270
  271
  272
  132 unmodified lines
  
  405
  406
  407
  408
  409
  410
  411
  412
  413
  414
  415
  416
  417
  418
  419
  420
  421
  422
  423
  424
  425
  426
  427
  428
  9 unmodified lines
  
  438
  439
  440
  441
  442
  443
  444
  96 unmodified lines
  
  541
  542
  543
  520
  521
  522
  523
  524
  525
  526
  527
  528
  529
  530
  531
  532
  533
  534
  535
  536
  537
  538
  539
  540
  541
  542
  543
  544
  545
  546
  547
  548
  544
  545
  550
  546
  547
  548
  549
  172 unmodified lines
  
  722
  723
  724
  725
  726
  727
  728
  729
  730
  731
  732
  733
  734
  735
  736
  737
  738
  739
  740
  741
  742
  743
  744
  745
  746
  747
  748
  749
  750
  751
  752
  753
  754
  755
  756
  757
  758
  759
  760
  761
  762
  763
  764
  765
  766
  767
  768
  769
  770
  771
  772
  773
  774
  775
  776
  777
  778
  779
  780
  781
  782
  783
  784
  785
  786
  787
  788
  
  99 unmodified lines
  
  	Blocked  int          `json:"blocked"`
	Deleted  int          `json:"deleted"`
	DryRun   bool         `json:"dry_run"`
	Relay    bool         `json:"relay"`
	BootstrapSuggested bool `json:"bootstrap_suggested"`
	Stats    Stats        `json:"stats"`
	Measurement Measurement `json:"measurement"`
	Protocol string       `json:"protocol"`

128 unmodified lines
  
	}

summary := fmt.Sprintf(
		"summary: pushed=%d deleted=%d skipped=%d blocked=%d protocol=%s",
		r.Pushed, r.Deleted, r.Skipped, r.Blocked, r.Protocol,
		"summary: pushed=%d deleted=%d skipped=%d blocked=%d protocol=%s relay=%t",
		r.Pushed, r.Deleted, r.Skipped, r.Blocked, r.Protocol, r.Relay,
	)
	if r.DryRun {
		summary += " dry-run=true"
	}
	
	return lines
}

func canBootstrapRelay(
	cfg Config,
	desired map[plumbing.ReferenceName]desiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) bool {
	if cfg.Force || cfg.Prune {
		return false
	}
	if len(desired) == 0 {
		return false
	}
	for targetRef := range desired {
		if !targetRefs[targetRef].IsZero() {
			return false
		}
	}
	return true
}

func bootstrapWithInputs(
	ctx context.Context,
	cfg Config,
	stats *statsCollector,
	sourceConn *transportConn,
	targetConn *transportConn,
	sourceService *sourceRefService,
	targetAdv *packp.AdvRefs,
	desiredRefs map[plumbing.ReferenceName]desiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
) (Result, error) {
	plans, err := buildBootstrapPlans(desiredRefs, targetRefs)
	if err != nil {
		return Result{}, err
	}

result := Result{
		Plans:       plans,
		Relay:       true,
		Stats:       stats.snapshot(),
		Protocol:    sourceService.protocol,
	}

packReader, err := sourceService.FetchPack(ctx, sourceConn, desiredRefs, nil)
	if err != nil {
		if errors.Is(err, git.NoErrAlreadyUpToDate) {
			return result, nil
		}
		return result, fmt.Errorf("fetch source pack: %w", err)
	}
	defer packReader.Close()
	packReader = limitPackReadCloser(packReader, cfg.MaxPackBytes)

if err := pushPackToTarget(ctx, targetConn, targetAdv, plans, packReader, cfg.Verbose); err != nil {
		return result, fmt.Errorf("push target refs: %w", err)
	}

result.Pushed = len(plans)
	result.Stats = stats.snapshot()
	return result, nil
}

func selectBranches(source map[string]plumbing.Hash, requested []string) map[string]plumbing.Hash {
	if len(requested) == 0 {
		return source
	}
