Add CI workflows and fix all golangci-lint violations · Entire

Home

Log in

Add CI workflows and fix all golangci-lint violations

d456614→main·

Soph·3mo ago·60 files·+891 added/-434 removed

Add GitHub Actions PR gates matching the CLI repo pattern: - ci.yml: tests with -race detection on PRs and push to main - lint.yml: golangci-lint (60+ linters) with inline PR annotations, gofmt, go mod tidy, and shellcheck checks - license-check.yml: reusable license compliance from entireio/shared

Add .golangci.yaml with full linter config adapted from CLI, and mise-tasks/lint/ scripts for local development parity.

Fix all 340 existing lint violations across 51 files: - wrapcheck: wrap external/interface errors with descriptive context - perfsprint: errors.New for static strings, string concat for Sprintf - errcheck: handle or explicitly acknowledge error returns - intrange: modernize for loops to range-over-int (Go 1.22+) - embeddedstructfieldcheck: separate embedded from regular struct fields - inamedparam: name interface method parameters - exhaustive: add missing switch cases - errorlint: use errors.Is() instead of == for wrapped error checks - revive: rename unused parameters to _ - goconst: extract repeated string literals into constants - gocritic: restructure if-else chains and duplicate branches - noctx: use CommandContext/NewRequestWithContext - staticcheck: replace nil contexts with t.Context() - usestdlibvars: use http.MethodGet instead of "GET"

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

Sessions

79aa4e9d44f3View transcript

?\ Review commit b718e82 ("Add batched planning backend sensitivity test") in /Users/soph/Work/entire/devenv/git-sync. Focus on whether the new git-http-backend test is a sound experiment surface for batCodex·1 step

Changes

60

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

name: Tests
on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - main
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - uses: jdx/mise-action@v4
      - name: Tests
        run: mise run test:ci

A.github/workflows/ci.yml+17

1
2
3
4
5
6
7
8
9
10
11
12

name: License Check

on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - main

jobs:
  check-licenses:
    uses: entireio/shared/.github/workflows/license-check-reusable.yml@main

A.github/workflows/license-check.yml+12

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

name: Lint
on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-go@v6
        with:
          go-version: 'stable'

- uses: jdx/mise-action@v4

- name: Run linters
        run: mise run lint

# Uses the same config as `mise run lint:go`, but with special sauce to
      # create inline feedback on GitHub's UI.  On local dev, the same issues
      # should be surfaced by mise-tasks/lint/go
      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v9
        with:
          version: 'v2.11.3'
          debug: 'clean'

A.github/workflows/lint.yml+36

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

version: "2"
issues:
  max-issues-per-linter: 0
  max-same-issues: 0
linters:
  default: standard
  enable:
    - asciicheck
    - bidichk
    - bodyclose
    - canonicalheader
    - copyloopvar
    - decorder
    - durationcheck
    - embeddedstructfieldcheck
    - errchkjson
    - errname
    - errorlint
    - exhaustive
    - exptostd
    - forcetypeassert
    - gocheckcompilerdirectives
    - gochecknoinits
    - gochecksumtype
    - goconst
    - gocritic
    - gosec
    - govet
    - grouper
    - iface
    - importas
    - inamedparam
    - ineffassign
    - intrange
    - ireturn
    - loggercheck
    - maintidx
    - makezero
    - mirror
    - misspell
    - musttag
    - nakedret
    - nilerr
    - nilnesserr
    - nilnil
    - noctx
    - nolintlint
    - nosprintfhostport
    - perfsprint
    - promlinter
    - protogetter
    - reassign
    - recvcheck
    - revive
    - sloglint
    - spancheck
    - staticcheck
    - tagalign
    - testableexamples
    - testifylint
    - tparallel
    - unconvert
    - unparam
    - unused
    - usestdlibvars
    - usetesting
    - wastedassign
    - whitespace
    - wrapcheck
  settings:
    gosec:
      excludes:
        - G104 # errors are handled at appropriate levels, checked by errcheck linter
        - G115 # integer overflow in flock fd conversion is safe on all platforms
        - G204 # subprocess with variables is expected for git credential helpers
        - G301 # directory permissions 0755 are fine for local data dirs
        - G304 # file paths from variables are expected in token store and config readers
        - G703 # path traversal via taint is expected when reading user config files
    dupl:
      threshold: 75
    errcheck:
      check-type-assertions: true
      check-blank: true
    govet:
      enable-all: true
      disable:
        - fieldalignment
        - shadow
    ireturn:
      allow:
        - anon
        - error
        - empty
        - stdlib
        - github.com/go-git/go-git/v6/plumbing/storer.ReferenceIter
        - github.com/go-git/go-git/v6/plumbing.EncodedObject
        - github.com/go-git/go-git/v6/storage.Storer
        - github.com/go-git/go-git/v6/plumbing/storer.EncodedObjectIter
        - github.com/go-git/go-billy/v6.Filesystem
        - github.com/go-git/go-git/v6/plumbing/transport.AuthMethod
    nolintlint:
      require-explanation: true
      require-specific: true
    sloglint:
      attr-only: true
    testifylint:
      enable-all: true
    unparam:
      check-exported: true
  exclusions:
    presets:
      - comments
      - std-error-handling
    rules:
      - path: _test\.go
        linters:
          - gosec
          - wrapcheck

A.golangci.yaml+118

116 unmodified lines

117
118
119
120
120
121
122
123
9 unmodified lines

133
134
135
136
136
137
138
139
38 unmodified lines

178
179
180
181
181
182
183
184
30 unmodified lines

215
216
217
218
218
219
220
221
9 unmodified lines

231
232
233
234
234
235
236
237
1 unmodified line

239
240
241
242
243
244
245
246
243
247
248
249
250
251
252
253
254
255
256
257
258
259
260
207 unmodified lines

468
469
470
463
471
472
473
474

116 unmodified lines

fs.BoolVar(&cfg.Options.Verbose, "v", false, "verbose logging")

if err := fs.Parse(args); err != nil {
        return err
        return fmt.Errorf("parse flags: %w", err)
    }
    cfg.Policy.Protocol = gitsync.ProtocolMode(benchProtocol)
    if len(fs.Args()) > 0 {
9 unmodified lines

for _, raw := range mappings {
        mapping, err := validation.ParseMapping(raw)
        if err != nil {
            return err
            return fmt.Errorf("parse mapping %q: %w", raw, err)
        }
        cfg.Scope.Mappings = append(cfg.Scope.Mappings, gitsync.RefMapping{
            Source: mapping.Source,
38 unmodified lines

Runs:        make([]runSummary, 0, repeat),
    }

for i := 0; i < repeat; i++ {
    for i := range repeat {
        runCfg := cfg
        targetPath := filepath.Join(workDir, fmt.Sprintf("%s-run-%03d.git", sc, i+1))
        if err := os.RemoveAll(targetPath); err != nil {
30 unmodified lines

report.Aggregate = summarizeRuns(report.Runs)

if jsonOutput {
        data, err := json.MarshalIndent(report, "", "  ")
        data, err := json.MarshalIndent(report, "", "  ") //nolint:musttag // bench debug output, nested types from other packages use default names
        if err != nil {
            return fmt.Errorf("marshal report: %w", err)
        }
9 unmodified lines

client := unstable.New(unstable.Options{})
    switch sc {
    case scenarioBootstrap:
        return client.Bootstrap(ctx, unstable.BootstrapRequest{
        result, err := client.Bootstrap(ctx, unstable.BootstrapRequest{
            Source:      gitsync.Endpoint{URL: cfg.SourceURL},
            Target:      gitsync.Endpoint{URL: targetURL},
            Scope:       cfg.Scope,
1 unmodified line

Protocol:    cfg.Policy.Protocol,
            Options:     cfg.Options,
        })
        if err != nil {
            return unstable.Result{}, fmt.Errorf("bootstrap: %w", err)
        }
        return result, nil
    case scenarioSync:
        return client.Sync(ctx, unstable.SyncRequest{
        result, err := client.Sync(ctx, unstable.SyncRequest{
            Source:  gitsync.Endpoint{URL: cfg.SourceURL},
            Target:  gitsync.Endpoint{URL: targetURL},
            Scope:   cfg.Scope,
            Policy:  cfg.Policy,
            Options: cfg.Options,
        })
        if err != nil {
            return unstable.Result{}, fmt.Errorf("sync: %w", err)
        }
        return result, nil
    default:
        return unstable.Result{}, fmt.Errorf("unsupported scenario %q", sc)
    }
207 unmodified lines

func (p *benchProtocolModeFlag) Set(value string) error {
    mode, err := validation.NormalizeProtocolMode(value)
    if err != nil {
        return err
        return fmt.Errorf("normalize protocol: %w", err)
    }
    *p = benchProtocolModeFlag(benchProtocolMode(gitsync.ProtocolMode(mode)))
    return nil

Mcmd/git-sync-bench/main.go+15/-7

8 unmodified lines

9
10
11
12
12
13
14
15
16
17
18
71 unmodified lines

90
91
92
93
93
94
95
96
15 unmodified lines

112
113
114
115
115
116
117
118
22 unmodified lines

141
142
143
144
144
145
146
147
39 unmodified lines

187
188
189
190
190
191
192
193
14 unmodified lines

208
209
210
211
211
212
213
214
9 unmodified lines

224
225
226
227
227
228
229
230
25 unmodified lines

256
257
258
259
259
260
261
262
18 unmodified lines

281
282
283
284
284
285
286
287
25 unmodified lines

313
314
315
316
316
317
318
319
26 unmodified lines

346
347
348
349
349
350
351
352
16 unmodified lines

369
370
371
372
372
373
374
375
376
377
378
379
53 unmodified lines

433
434
435
432
436
437
438
439

8 unmodified lines

"os"
    "strings"

"github.com/go-git/go-git/v6/plumbing"
    "github.com/entirehq/git-sync/internal/validation"
    "github.com/entirehq/git-sync/pkg/gitsync"
    "github.com/entirehq/git-sync/pkg/gitsync/unstable"
    "github.com/go-git/go-git/v6/plumbing"
)

func main() {
71 unmodified lines

fs.BoolVar(&req.Options.Verbose, "v", false, "verbose logging")

if err := fs.Parse(args); err != nil {
        return err
        return fmt.Errorf("parse flags: %w", err)
    }
    req.Policy.Mode = gitsync.OperationMode(modeValue)
    req.Policy.Protocol = gitsync.ProtocolMode(protocolValue)
15 unmodified lines

for _, raw := range mappings {
        mapping, err := validation.ParseMapping(raw)
        if err != nil {
            return err
            return fmt.Errorf("parse mapping %q: %w", raw, err)
        }
        req.Scope.Mappings = append(req.Scope.Mappings, gitsync.RefMapping{
            Source: mapping.Source,
22 unmodified lines

}
    }
    if err != nil {
        return err
        return fmt.Errorf("sync: %w", err)
    }
    printOutput(jsonOutput, result)

39 unmodified lines

fs.BoolVar(&req.Options.Verbose, "v", false, "verbose logging")

if err := fs.Parse(args); err != nil {
        return err
        return fmt.Errorf("parse flags: %w", err)
    }
    req.Protocol = gitsync.ProtocolMode(bootstrapProtocol)

14 unmodified lines

for _, raw := range mappings {
        mapping, err := validation.ParseMapping(raw)
        if err != nil {
            return err
            return fmt.Errorf("parse mapping %q: %w", raw, err)
        }
        req.Scope.Mappings = append(req.Scope.Mappings, gitsync.RefMapping{
            Source: mapping.Source,
9 unmodified lines

Auth: gitsync.StaticAuthProvider{Source: sourceAuth, Target: targetAuth},
    }).Bootstrap(ctx, req)
    if err != nil {
        return err
        return fmt.Errorf("bootstrap: %w", err)
    }
    printOutput(jsonOutput, result)
    return nil
25 unmodified lines

fs.BoolVar(&jsonOutput, "json", false, "print JSON output")

if err := fs.Parse(args); err != nil {
        return err
        return fmt.Errorf("parse flags: %w", err)
    }
    req.Protocol = gitsync.ProtocolMode(probeProtocol)

18 unmodified lines

Auth: gitsync.StaticAuthProvider{Source: sourceAuth, Target: targetAuth},
    }).Probe(ctx, req)
    if err != nil {
        return err
        return fmt.Errorf("probe: %w", err)
    }
    printOutput(jsonOutput, result)
    return nil
25 unmodified lines

fs.Var(&haveHashesRaw, "have", "explicit object hash to advertise as have")

if err := fs.Parse(args); err != nil {
        return err
        return fmt.Errorf("parse flags: %w", err)
    }
    req.Protocol = gitsync.ProtocolMode(fetchProtocol)

26 unmodified lines

Auth: gitsync.StaticAuthProvider{Source: sourceAuth},
    }).Fetch(ctx, req)
    if err != nil {
        return err
        return fmt.Errorf("fetch: %w", err)
    }
    printOutput(jsonOutput, result)
    return nil
16 unmodified lines

}

func marshalOutput(value interface{}) ([]byte, error) {
    return json.MarshalIndent(value, "", "  ")
    data, err := json.MarshalIndent(value, "", "  ")
    if err != nil {
        return nil, fmt.Errorf("marshal JSON: %w", err)
    }
    return data, nil
}

type multiStringFlag []string
53 unmodified lines

func (p *protocolModeFlag) Set(value string) error {
    mode, err := validation.NormalizeProtocolMode(value)
    if err != nil {
        return err
        return fmt.Errorf("normalize protocol: %w", err)
    }
    *p = protocolModeFlag(protocolMode(gitsync.ProtocolMode(mode)))
    return nil

Mcmd/git-sync/main.go+17/-13

13 unmodified lines

14
15
16
17
18
19
20
4 unmodified lines

25
26
27
27
28
29
30
31
32
33
34
139 unmodified lines

174
175
176
176
177
178
179
180
7 unmodified lines

188
189
190
190
191
192
193
194
14 unmodified lines

209
210
211
211
212
213
214
215
10 unmodified lines

226
227
228
228
229
230
231
232
24 unmodified lines

257
258
259
259
260
261
262
263
46 unmodified lines

310
311
312
312
313
314
315
316
176 unmodified lines

493
494
495
495
496
497
498
499
500
501

13 unmodified lines

"testing"
    "time"

"github.com/entirehq/git-sync/pkg/gitsync/unstable"
    billy "github.com/go-git/go-billy/v6"
    "github.com/go-git/go-billy/v6/memfs"
    git "github.com/go-git/go-git/v6"
4 unmodified lines

"github.com/go-git/go-git/v6/plumbing/transport"
    transporthttp "github.com/go-git/go-git/v6/plumbing/transport/http"
    "github.com/go-git/go-git/v6/storage/memory"
    "github.com/entirehq/git-sync/pkg/gitsync/unstable"
)

const testBranch = "master"
const modeReplicate = "replicate"

func TestMarshalOutput_JSONShape(t *testing.T) {
    data, err := marshalOutput(unstable.FetchResult{
139 unmodified lines

output, err := captureStdout(func() error {
        return run(context.Background(), []string{
            "plan",
            "--mode", "replicate",
            "--mode", modeReplicate,
            "--json",
            sourceServer.RepoURL(),
            targetServer.RepoURL(),
7 unmodified lines

if err := json.Unmarshal([]byte(output), &result); err != nil {
        t.Fatalf("decode plan json: %v\noutput=%s", err, output)
    }
    if result["operation_mode"] != "replicate" {
    if result["operation_mode"] != modeReplicate {
        t.Fatalf("expected operation_mode=replicate, got %#v", result["operation_mode"])
    }
}
14 unmodified lines

output, err := captureStdout(func() error {
        return run(context.Background(), []string{
            "replicate",
            modeReplicate,
            "--json",
            sourceServer.RepoURL(),
            targetServer.RepoURL(),
10 unmodified lines

if result["dry_run"] != false {
        t.Fatalf("expected dry_run=false, got %#v", result["dry_run"])
    }
    if result["operation_mode"] != "replicate" {
    if result["operation_mode"] != modeReplicate {
        t.Fatalf("expected operation_mode=replicate, got %#v", result["operation_mode"])
    }
    if result["pushed"] != float64(1) {
24 unmodified lines

func TestRun_Replicate_SubcommandRejectsForce(t *testing.T) {
    err := run(context.Background(), []string{
        "replicate",
        modeReplicate,
        "--force",
        "http://127.0.0.1:1/source.git",
        "http://127.0.0.1:1/target.git",
46 unmodified lines

t.Fatalf("open worktree: %v", err)
    }

for i := 0; i < count; i++ {
    for i := range count {
        content := strings.Repeat(fmt.Sprintf("line %d %d\n", i, time.Now().UnixNano()), 24)
        file, err := fs.Create("tracked.txt")
        if err != nil {
176 unmodified lines

w.Header().Set("Content-Type", "application/x-git-receive-pack-result")
    if buf.Len() > 0 {
        _, _ = w.Write(buf.Bytes())
        if _, err := w.Write(buf.Bytes()); err != nil {
            s.t.Fatalf("write receive-pack response: %v", err)
        }
    }
    if err != nil {
        return

Mcmd/git-sync/main_test.go+11/-8

4 unmodified lines

5
6
7
8
9
10
11
3 unmodified lines

15
16
17
18
19
20
21
1 unmodified line

23
24
25
26
27
28
29
30
31
32
33

4 unmodified lines

require (
    github.com/go-git/go-billy/v6 v6.0.0-20260410103409-85b6241850b5
    github.com/go-git/go-git/v6 v6.0.0-alpha.1
    github.com/stretchr/testify v1.11.1
    github.com/zalando/go-keyring v0.2.8
)

3 unmodified lines

github.com/cloudflare/circl v1.6.3 // indirect
    github.com/cyphar/filepath-securejoin v0.6.1 // indirect
    github.com/danieljoos/wincred v1.2.3 // indirect
    github.com/davecgh/go-spew v1.1.1 // indirect
    github.com/emirpasic/gods v1.18.1 // indirect
    github.com/go-git/gcfg/v2 v2.0.2 // indirect
    github.com/godbus/dbus/v5 v5.2.2 // indirect
1 unmodified line

github.com/kevinburke/ssh_config v1.6.0 // indirect
    github.com/klauspost/cpuid/v2 v2.3.0 // indirect
    github.com/pjbgf/sha1cd v0.5.0 // indirect
    github.com/pmezard/go-difflib v1.0.0 // indirect
    github.com/sergi/go-diff v1.4.0 // indirect
    golang.org/x/crypto v0.48.0 // indirect
    golang.org/x/net v0.51.0 // indirect
    golang.org/x/sync v0.19.0 // indirect
    golang.org/x/sys v0.43.0 // indirect
    gopkg.in/yaml.v3 v3.0.1 // indirect
)

Mgo.mod+4

34 unmodified lines

35
36
37
38
39
40
41
42
43
44
22 unmodified lines

67
68
69
70
71
72
73

34 unmodified lines

github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
22 unmodified lines

golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

Mgo.sum+3

9 unmodified lines

10
11
12
13
14
15
16
17
9 unmodified lines

27
28
29
28
30
31
32
31
33
34
35
36
3 unmodified lines

40
41
42
41
43
44
45
46
3 unmodified lines

50
51
52
51
53
54
55
56
26 unmodified lines

83
84
85
84
86
87
88
89

9 unmodified lines

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

const defaultGitUsername = "git"

// Endpoint holds the authentication-related fields for a remote.
type Endpoint struct {
    Username      string
9 unmodified lines

return auth, nil
    }
    if ep == nil {
        return nil, nil
        return nil, nil //nolint:nilnil // nil signals no auth method found at this stage
    }
    if ep.Scheme != "http" && ep.Scheme != "https" {
        return nil, nil
        return nil, nil //nolint:nilnil // nil signals no auth method found at this stage
    }
    if username, password, ok, err := LookupEntireDBCredential(raw, ep); err != nil {
        return nil, err // issue #7: surface refresh failure explicitly
3 unmodified lines

if username, password, ok := lookupGitCredential(ep); ok {
        return &transporthttp.BasicAuth{Username: username, Password: password}, nil
    }
    return nil, nil
    return nil, nil //nolint:nilnil // nil signals no auth method found at this stage
}

func explicitAuth(raw Endpoint) transport.AuthMethod {
3 unmodified lines

if raw.Token != "" {
        username := raw.Username
        if username == "" {
            username = "git"
            username = defaultGitUsername
        }
        return &transporthttp.BasicAuth{Username: username, Password: raw.Token}
    }
26 unmodified lines

if ep.User != nil && ep.User.Username() != "" {
            username = ep.User.Username()
        } else {
            username = "git"
            username = defaultGitUsername
        }
    }
    return username, password, true

Minternal/auth/auth.go+7/-5

21 unmodified lines

22
23
24
25
26
25
26
27
28
29
138 unmodified lines

168
169
170
171
172
173
174
175
171
172
173
174
175
176
177
178
56 unmodified lines

235
236
237
238
239
240
241
242
243
244
245
238
239
240
241
242
243
244
245
246
247
248
28 unmodified lines

277
278
279
280
281
280
281
282
283
284
9 unmodified lines

294
295
296
297
298
297
298
299
300
301
227 unmodified lines

529
530
531
532
532
533
534
535
30 unmodified lines

566
567
568
569
569
570
571
572

21 unmodified lines

name      string
        encoded   string
        wantToken string
        wantZero  bool   // if true, expect time.Time zero value
        wantUnix  int64  // checked only when wantZero is false
        wantZero  bool  // if true, expect time.Time zero value
        wantUnix  int64 // checked only when wantZero is false
    }{
        {
            name:      "token with pipe-separated unix timestamp",
138 unmodified lines

func TestParseCredentialOutput(t *testing.T) {
    tests := []struct {
        name       string
        output     string
        wantUser   string
        wantPass   string
        wantLen    int
        name     string
        output   string
        wantUser string
        wantPass string
        wantLen  int
    }{
        {
            name:     "standard username and password",
56 unmodified lines

sshEP := &transport.Endpoint{URL: url.URL{Scheme: "ssh", Host: "example.com", Path: "/repo.git"}}

tests := []struct {
        name      string
        raw       Endpoint
        ep        *transport.Endpoint
        mockCred  func(ctx context.Context, input string) ([]byte, error)
        wantType  string // "token", "basic", "nil"
        wantUser  string
        wantPass  string
        wantErr   bool
        name     string
        raw      Endpoint
        ep       *transport.Endpoint
        mockCred func(ctx context.Context, input string) ([]byte, error)
        wantType string // "token", "basic", "nil"
        wantUser string
        wantPass string
        wantErr  bool
    }{
        {
            name:     "bearer token set returns TokenAuth",
28 unmodified lines

name: "nothing set HTTP endpoint no credential helper returns nil",
            raw:  Endpoint{},
            ep:   ep,
            mockCred: func(ctx context.Context, input string) ([]byte, error) {
                return nil, fmt.Errorf("no helper")
            mockCred: func(_ context.Context, _ string) ([]byte, error) {
                return nil, errors.New("no helper")
            },
            wantType: "nil",
        },
9 unmodified lines

GitCredentialFillCommand = tt.mockCred
            } else {
                // Default mock: no credential helper.
                GitCredentialFillCommand = func(ctx context.Context, input string) ([]byte, error) {
                    return nil, fmt.Errorf("no helper")
                GitCredentialFillCommand = func(_ context.Context, _ string) ([]byte, error) {
                    return nil, errors.New("no helper")
                }
            }

227 unmodified lines

if !isNotFound(keyring.ErrNotFound) {
        t.Error("expected isNotFound(keyring.ErrNotFound) = true")
    }
    if isNotFound(fmt.Errorf("some other error")) {
    if isNotFound(errors.New("some other error")) {
        t.Error("expected isNotFound(other error) = false")
    }
    // Wrapped ErrNotFound should also be detected.
30 unmodified lines

// Set up a hosts.json so lookupEntireDBToken would find a user.
        configDir := t.TempDir()
        t.Setenv("ENTIRE_CONFIG_DIR", configDir)
        hostsJSON := fmt.Sprintf(`{"example.com":{"activeUser":"alice","users":["alice"]}}`)
        hostsJSON := `{"example.com":{"activeUser":"alice","users":["alice"]}}`
        if err := os.WriteFile(filepath.Join(configDir, "hosts.json"), []byte(hostsJSON), 0o644); err != nil {
            t.Fatal(err)
        }

Minternal/auth/auth_test.go+21/-21

52 unmodified lines

53
54
55
56
56
57
58
59
22 unmodified lines

82
83
84
85
85
86
87
88
85 unmodified lines

174
175
176
177
177
178
179
180
181
182
183
184
184
185
186
187
188
189
189
190
191
192
2 unmodified lines

195
196
197
198
198
199
200
201
7 unmodified lines

209
210
211
212
212
213
214
215
216

52 unmodified lines

}
    username := raw.Username
    if username == "" {
        username = "git"
        username = defaultGitUsername
    }
    return username, token, true, nil
}
22 unmodified lines

if configDir == "" {
        home, err := os.UserHomeDir()
        if err != nil {
            return "", nil // no config dir, not an error
            return "", nil //nolint:nilerr // missing config dir means no stored credentials, not an error
        }
        configDir = filepath.Join(home, ".config", "entire")
    }
85 unmodified lines

req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(baseURL, "/")+"/oauth/token", strings.NewReader(form.Encode()))
    if err != nil {
        return "", err
        return "", fmt.Errorf("create token refresh request: %w", err)
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

client := &http.Client{
        Timeout: 30 * time.Second,
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLS}, //nolint:gosec
            TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLS}, //nolint:gosec // InsecureSkipVerify is controlled by user flag
        },
    }
    resp, err := client.Do(req)
    if err != nil {
        return "", err
        return "", fmt.Errorf("execute token refresh request: %w", err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
2 unmodified lines

var tokenResp oauthTokenResponse
    if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
        return "", err
        return "", fmt.Errorf("decode token refresh response: %w", err)
    }
    if tokenResp.AccessToken == "" {
        return "", errors.New("empty access token in refresh response")
7 unmodified lines

return "", err
    }
    if tokenResp.RefreshToken != "" {
        _ = WriteStoredToken(credentialService(host)+":refresh", username, tokenResp.RefreshToken)
        //nolint:errcheck // best-effort refresh token storage; access token already saved successfully
        WriteStoredToken(credentialService(host)+":refresh", username, tokenResp.RefreshToken)
    }
    return tokenResp.AccessToken, nil
}

Minternal/auth/entiredb.go+8/-7

1 unmodified line

2
3
4
5
6
7
8
6 unmodified lines

15
16
17
17
18
19
20
21
22
23
24
25
1 unmodified line

27
28
29
25
30
31
32
33
34
35
36
28 unmodified lines

65
66
67
60
68
69
70
71
64
72
73
74
75
13 unmodified lines

89
90
91
84
92
93
94
95
7 unmodified lines

103
104
105
98
106
107
108
101
109
110
111
112
1 unmodified line

114
115
116
109
117
118
119
120
121
114
122
123
116
124
125
126
127
128
129
130
9 unmodified lines

140
141
142
132
143
144
145
146
136
147
148
149
139
150
151
152
153
154

1 unmodified line

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    "syscall"
6 unmodified lines

if os.Getenv("ENTIRE_TOKEN_STORE") == "file" {
        return readFileToken(fileTokenPath(), service, username)
    }
    return keyring.Get(service, username)
    token, err := keyring.Get(service, username)
    if err != nil {
        return "", fmt.Errorf("read keyring token: %w", err)
    }
    return token, nil
}

// WriteStoredToken writes a token to the configured store.
1 unmodified line

if os.Getenv("ENTIRE_TOKEN_STORE") == "file" {
        return writeFileToken(fileTokenPath(), service, username, password)
    }
    return keyring.Set(service, username, password)
    if err := keyring.Set(service, username, password); err != nil {
        return fmt.Errorf("write keyring token: %w", err)
    }
    return nil
}

func fileTokenPath() string {
28 unmodified lines

if os.IsNotExist(err) {
            return "", keyring.ErrNotFound
        }
        return "", err
        return "", fmt.Errorf("read token store file: %w", err)
    }
    var store map[string]map[string]string
    if err := json.Unmarshal(data, &store); err != nil {
        return "", err
        return "", fmt.Errorf("unmarshal token store: %w", err)
    }
    users := store[service]
    if users == nil {
13 unmodified lines

return os.ErrInvalid
    }
    if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
        return err
        return fmt.Errorf("create config directory: %w", err)
    }

// Acquire exclusive lock for the write.
7 unmodified lines

store := map[string]map[string]string{}
    if data, err := os.ReadFile(path); err == nil {
        if err := json.Unmarshal(data, &store); err != nil {
            return err
            return fmt.Errorf("unmarshal token store: %w", err)
        }
    } else if !os.IsNotExist(err) {
        return err
        return fmt.Errorf("read token store file: %w", err)
    }
    if store[service] == nil {
        store[service] = map[string]string{}
1 unmodified line

store[service][username] = password
    data, err := json.Marshal(store)
    if err != nil {
        return err
        return fmt.Errorf("marshal token store: %w", err)
    }
    // Atomic write: write to temp file then rename to prevent corruption on crash.
    tmp := path + ".tmp"
    if err := os.WriteFile(tmp, data, 0o600); err != nil {
        return err
        return fmt.Errorf("write token store temp file: %w", err)
    }
    return os.Rename(tmp, path)
    if err := os.Rename(tmp, path); err != nil {
        return fmt.Errorf("rename token store temp file: %w", err)
    }
    return nil
}

// flockShared acquires a shared (read) lock on path+".lock".
9 unmodified lines

func flockOpen(lockPath string, how int) (func(), error) {
    f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("open lock file: %w", err)
    }
    if err := syscall.Flock(int(f.Fd()), how); err != nil {
        f.Close()
        return nil, err
        return nil, fmt.Errorf("acquire file lock: %w", err)
    }
    return func() {
        _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
        //nolint:errcheck // unlock errors on close are not actionable
        syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
        f.Close()
    }, nil
}

Minternal/auth/tokenstore.go+25/-13

13 unmodified lines

14
15
16
17
17
18
19
20
21
22
23
24
24
25
26
26
27
28
29
17 unmodified lines

47
48
49
50
50
51
52
53
1 unmodified line

55
56
57
58
58
59
60
61
9 unmodified lines

71
72
73
74
74
75
76
77
78
79
79
80
81
82

13 unmodified lines

const packetCount = 1000
    payload := "data\n"
    pkt := FormatPktLine(payload)
    for i := 0; i < packetCount; i++ {
    for range packetCount {
        wire.WriteString(pkt)
    }
    wire.WriteString("0000") // flush to terminate
    data := wire.String()

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        reader := NewPacketReader(bytes.NewBufferString(data))
        for j := 0; j < packetCount; j++ {
        for range packetCount {
            kind, p, err := reader.ReadPacket()
            if err != nil {
                b.Fatal(err)
17 unmodified lines

// Build a capability advertisement with 10 capabilities.
    var wire strings.Builder
    wire.WriteString(FormatPktLine("version 2\n"))
    for i := 0; i < 10; i++ {
    for i := range 10 {
        line := fmt.Sprintf("capability-%d=value-%d\n", i, i)
        wire.WriteString(FormatPktLine(line))
    }
1 unmodified line

data := wire.String()

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        caps, err := DecodeV2Capabilities(bytes.NewBufferString(data))
        if err != nil {
            b.Fatal(err)
9 unmodified lines

capArgs := []string{"agent=git-sync/bench"}
    cmdArgs := make([]string, 0, 52)
    cmdArgs = append(cmdArgs, "ofs-delta", "no-progress")
    for i := 0; i < 50; i++ {
    for i := range 50 {
        cmdArgs = append(cmdArgs, fmt.Sprintf("want %040x", i+1))
    }

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        data, err := EncodeCommand("fetch", capArgs, cmdArgs)
        if err != nil {
            b.Fatal(err)

Minternal/gitproto/benchmark_test.go+7/-7

3 unmodified lines

4
5
6
7
8
9
10
136 unmodified lines

147
148
149
149
150
151
152
153

3 unmodified lines

"testing"

"github.com/go-git/go-git/v6/plumbing/protocol/packp/capability"
    "github.com/stretchr/testify/require"
)

func TestV2CapabilitiesFetchSupports(t *testing.T) {
136 unmodified lines

t.Run(tt.name, func(t *testing.T) {
            list := capability.NewList()
            for _, c := range tt.caps {
                _ = list.Set(c)
                require.NoError(t, list.Set(c))
            }
            got := PreferredSideband(list)
            if got != tt.want {

Minternal/gitproto/capability_test.go+2/-1

1
2
3
4
5
6
7
34 unmodified lines

42
43
44
45
46
47
48
4 unmodified lines

53
54
55
54
56
57
58
59
60

package gitproto

import (
    "errors"
    "fmt"
    "io"

34 unmodified lines

type packLimitRC struct {
    io.ReadCloser

max  int64
    read int64
}
4 unmodified lines

if r.read > r.max {
        return n, fmt.Errorf("source pack exceeded max-pack-bytes limit (%d)", r.max)
    }
    return n, err
    if err != nil && !errors.Is(err, io.EOF) {
        return n, fmt.Errorf("read: %w", err)
    }
    return n, err //nolint:wrapcheck // io.EOF must pass through for io.Reader contract
}

Minternal/gitproto/convert.go+6/-1

95 unmodified lines

96
97
98
99
99
100
101
102
102
103
104
105
146 unmodified lines

252
253
254
255
255
256
257
258
259
260
261
60 unmodified lines

322
323
324
322
325
326
327
328
329
325
330
331
332
333
334
335
329
336
337
338
339
340
332
341
342
343
344
345
346
41 unmodified lines

388
389
390
380
391
392
393
394
395
396
397
47 unmodified lines

445
446
447
434
448
449
450
451
64 unmodified lines

516
517
518
505

95 unmodified lines

ref DesiredRef,
) error {
    if s.Protocol != "v2" {
        return fmt.Errorf("commit graph fetch requires protocol v2")
        return errors.New("commit graph fetch requires protocol v2")
    }
    if !s.V2Caps.FetchSupports("filter") {
        return fmt.Errorf("source does not advertise fetch filter support")
        return errors.New("source does not advertise fetch filter support")
    }

cmdArgs := []string{
146 unmodified lines

case "packfile\n":
                demux := sideband.NewDemuxer(sideband.Sideband64k, reader.BufReader())
                demux.Progress = progressSink(verbose, "source: ")
                return packfile.UpdateObjectStorage(store, demux)
                if err := packfile.UpdateObjectStorage(store, demux); err != nil {
                    return fmt.Errorf("update object storage: %w", err)
                }
                return nil
            case "acknowledgments\n", "shallow-info\n":
                if err := SkipSection(reader); err != nil {
                    return err
60 unmodified lines

req := packp.NewUploadRequest()
    req.Wants = wants
    if !verbose && adv.Capabilities.Supports(capability.NoProgress) {
        _ = req.Capabilities.Set(capability.NoProgress)
        if err := req.Capabilities.Set(capability.NoProgress); err != nil {
            return nil, nil, fmt.Errorf("set capability: %w", err)
        }
    }
    if includeTags && adv.Capabilities.Supports(capability.IncludeTag) {
        _ = req.Capabilities.Set(capability.IncludeTag)
        if err := req.Capabilities.Set(capability.IncludeTag); err != nil {
            return nil, nil, fmt.Errorf("set capability: %w", err)
        }
    }
    // Prefer sideband64k over sideband (issue #4).
    if sb := PreferredSideband(adv.Capabilities); sb != "" {
        _ = req.Capabilities.Set(sb)
        if err := req.Capabilities.Set(sb); err != nil {
            return nil, nil, fmt.Errorf("set capability: %w", err)
        }
    }
    if adv.Capabilities.Supports(capability.OFSDelta) {
        _ = req.Capabilities.Set(capability.OFSDelta)
        if err := req.Capabilities.Set(capability.OFSDelta); err != nil {
            return nil, nil, fmt.Errorf("set capability: %w", err)
        }
    }
    // NOTE: we intentionally do not request capability.ThinPack. The relayed
    // pack must stay self-contained because callers (e.g. replicate) forward
41 unmodified lines

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

func fetchPackV1(
47 unmodified lines

return nil
        }
        if _, err := r.Discard(8); err != nil {
            return err
            return fmt.Errorf("discard trailing NAK: %w", err)
        }
    }
}
64 unmodified lines

io.Reader
    io.Closer
}

Minternal/gitproto/fetch.go+23/-10

16 unmodified lines

17
18
19
20
21
22
23
17 unmodified lines

41
42
43
43
44
45
46
47
58 unmodified lines

106
107
108
108
109
110
111
112
1 unmodified line

114
115
116
116
117
118
119
120
3 unmodified lines

124
125
126
126
127
128
129
130
16 unmodified lines

147
148
149
149
150
151
152
118 unmodified lines

271
272
273
274
274
275
276
277
1 unmodified line

279
280
281
282
282
283
284
285
1 unmodified line

287
288
289
290
290
291
292
293
6 unmodified lines

300
301
302
303
303
304
305
306
12 unmodified lines

319
320
321
322
322
323
324
325
319 unmodified lines

645
646
647
648
648
649
650
651
40 unmodified lines

692
693
694
695
695
696
697
698

16 unmodified lines

"github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband"
    "github.com/go-git/go-git/v6/plumbing/transport"
    "github.com/go-git/go-git/v6/storage/memory"
    "github.com/stretchr/testify/require"
)

func TestCapabilities(t *testing.T) {
17 unmodified lines

// v1 protocol
    adv := packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.OFSDelta)
    require.NoError(t, adv.Capabilities.Set(capability.OFSDelta))
    rs = &RefService{Protocol: "v1", V1Adv: adv}
    got = rs.Capabilities()
    if len(got) == 0 {
58 unmodified lines

// With Sideband64k -- should return a demuxer (different reader).
    caps = capability.NewList()
    _ = caps.Set(capability.Sideband64k)
    require.NoError(t, caps.Set(capability.Sideband64k))
    got = buildSidebandReader(caps, reader, nil)
    if got == reader {
        t.Error("expected wrapped reader when Sideband64k is set")
1 unmodified line

// With Sideband (not 64k) -- should return a demuxer.
    caps = capability.NewList()
    _ = caps.Set(capability.Sideband)
    require.NoError(t, caps.Set(capability.Sideband))
    got = buildSidebandReader(caps, reader, nil)
    if got == reader {
        t.Error("expected wrapped reader when Sideband is set")
3 unmodified lines

func TestBuildSidebandReaderWithProgress(t *testing.T) {
    reader := bytes.NewBufferString("test")
    caps := capability.NewList()
    _ = caps.Set(capability.Sideband64k)
    require.NoError(t, caps.Set(capability.Sideband64k))
    var progress sideband.Progress = io.Discard
    got := buildSidebandReader(caps, reader, progress)
    if got == reader {
16 unmodified lines

// wrappedRC should close the underlying closer.
    called := false
    rc := &wrappedRC{
        Reader: bytes.NewBufferString("data"),
        Closer: closerFunc(func() error {
            called = true
            return nil
118 unmodified lines

func TestFetchToStoreUnsupportedProtocol(t *testing.T) {
    rs := &RefService{Protocol: "v99"}
    err := rs.FetchToStore(nil, nil, nil, nil, nil)
    err := rs.FetchToStore(t.Context(), nil, nil, nil, nil)
    if err == nil {
        t.Fatal("expected error for unsupported protocol")
    }
1 unmodified line

func TestFetchPackUnsupportedProtocol(t *testing.T) {
    rs := &RefService{Protocol: "v99"}
    _, err := rs.FetchPack(nil, nil, nil, nil)
    _, err := rs.FetchPack(t.Context(), nil, nil, nil)
    if err == nil {
        t.Fatal("expected error for unsupported protocol")
    }
1 unmodified line

func TestFetchCommitGraphRequiresV2(t *testing.T) {
    rs := &RefService{Protocol: "v1"}
    err := rs.FetchCommitGraph(nil, nil, nil, DesiredRef{})
    err := rs.FetchCommitGraph(t.Context(), nil, nil, DesiredRef{})
    if err == nil {
        t.Fatal("expected error for non-v2 protocol")
    }
6 unmodified lines

},
    }
    rs := &RefService{Protocol: "v2", V2Caps: caps}
    err := rs.FetchCommitGraph(nil, nil, nil, DesiredRef{})
    err := rs.FetchCommitGraph(t.Context(), nil, nil, DesiredRef{})
    if err == nil {
        t.Fatal("expected error when filter not supported")
    }
12 unmodified lines

}))

adv := packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.Sideband64k)
    require.NoError(t, adv.Capabilities.Set(capability.Sideband64k))
    desired := map[plumbing.ReferenceName]DesiredRef{
        plumbing.NewBranchReferenceName("main"): {
            SourceRef:  plumbing.NewBranchReferenceName("main"),
319 unmodified lines

}))

adv := packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.Sideband64k)
    require.NoError(t, adv.Capabilities.Set(capability.Sideband64k))
    desired := map[plumbing.ReferenceName]DesiredRef{
        plumbing.NewBranchReferenceName("main"): {
            SourceRef:  plumbing.NewBranchReferenceName("main"),
40 unmodified lines

}))

adv := packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.Sideband64k)
    require.NoError(t, adv.Capabilities.Set(capability.Sideband64k))
    desired := map[plumbing.ReferenceName]DesiredRef{
        plumbing.NewBranchReferenceName("main"): {
            SourceRef:  plumbing.NewBranchReferenceName("main"),

Minternal/gitproto/fetch_test.go+12/-12

44 unmodified lines

45
46
47
48
48
49
50
51
20 unmodified lines

72
73
74
75
75
76
77
78
27 unmodified lines

106
107
108
109
109
110
111
112
113
113
114
115
116
117
118
118
119
120
121
122
122
123
124
125
126
127
127
128
129
130

44 unmodified lines

// only valid until the next call to ReadPacket.
func (pr *PacketReader) ReadPacket() (PacketType, []byte, error) {
    if _, err := io.ReadFull(pr.r, pr.header[:]); err != nil {
        return PacketData, nil, err
        return PacketData, nil, fmt.Errorf("read pktline header: %w", err)
    }

switch string(pr.header[:]) {
20 unmodified lines

pr.buf = pr.buf[:payloadLen]
    }
    if _, err := io.ReadFull(pr.r, pr.buf); err != nil {
        return PacketData, nil, err
        return PacketData, nil, fmt.Errorf("read pktline payload: %w", err)
    }
    return PacketData, pr.buf, nil
}
27 unmodified lines

func EncodeCommand(command string, capArgs, cmdArgs []string) ([]byte, error) {
    var buf bytes.Buffer
    if _, err := pktline.Writef(&buf, "command=%s\n", command); err != nil {
        return nil, err
        return nil, fmt.Errorf("write command: %w", err)
    }
    for _, arg := range capArgs {
        if _, err := pktline.Writef(&buf, "%s\n", arg); err != nil {
            return nil, err
            return nil, fmt.Errorf("write capability arg: %w", err)
        }
    }
    if len(cmdArgs) > 0 {
        if err := pktline.WriteDelim(&buf); err != nil {
            return nil, err
            return nil, fmt.Errorf("write delimiter: %w", err)
        }
        for _, arg := range cmdArgs {
            if _, err := pktline.Writef(&buf, "%s\n", arg); err != nil {
                return nil, err
                return nil, fmt.Errorf("write command arg: %w", err)
            }
        }
    }
    if err := pktline.WriteFlush(&buf); err != nil {
        return nil, err
        return nil, fmt.Errorf("write flush: %w", err)
    }
    return buf.Bytes(), nil
}

Minternal/gitproto/pktline.go+7/-7

2 unmodified lines

3
4
5
6
7
8
9
9 unmodified lines

19
20
21
21
22
23
24
25
1 unmodified line

27
28
29
29
30
31
32
33
204 unmodified lines

238
239
240
240
241
242
243
244

2 unmodified lines

import (
    "bufio"
    "bytes"
    "errors"
    "io"
    "testing"
)
9 unmodified lines

t.Fatalf("unexpected flush: kind=%v payload=%q", kind, payload)
    }

kind, payload, err = reader.ReadPacket()
    kind, _, err = reader.ReadPacket()
    if err != nil {
        t.Fatalf("read delim: %v", err)
    }
1 unmodified line

t.Fatalf("unexpected delim kind: %v", kind)
    }

kind, payload, err = reader.ReadPacket()
    kind, _, err = reader.ReadPacket()
    if err != nil {
        t.Fatalf("read response-end: %v", err)
    }
204 unmodified lines

if err == nil {
        t.Fatal("expected error from empty reader, got nil")
    }
    if err != io.EOF && err != io.ErrUnexpectedEOF {
    if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
        t.Fatalf("expected io.EOF or io.ErrUnexpectedEOF, got %v", err)
    }
}

Minternal/gitproto/pktline_test.go+4/-3

2 unmodified lines

3
4
5
6
7
8
9
50 unmodified lines

60
61
62
62
63
64
65
66
67
65
68
69
70
71
72
73
12 unmodified lines

86
87
88
84
89
90
86
91
92
93
94
95
96
42 unmodified lines

139
140
141
135
142
143
144
145
51 unmodified lines

197
198
199
193
200
201
202
203
8 unmodified lines

212
213
214
208
215
216
217
218
219
220
221
44 unmodified lines

266
267
268
259
269
270
271
272
8 unmodified lines

281
282
283
274
284
285
286
287

2 unmodified lines

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "io"
    "os"
50 unmodified lines

) (*packp.UpdateRequests, bool, bool, error) {
    req := packp.NewUpdateRequests()
    if sb := PreferredSideband(adv.Capabilities); sb != "" {
        _ = req.Capabilities.Set(sb)
        if err := req.Capabilities.Set(sb); err != nil {
            return nil, false, false, fmt.Errorf("set capability: %w", err)
        }
    }
    if adv.Capabilities.Supports(capability.ReportStatus) {
        _ = req.Capabilities.Set(capability.ReportStatus)
        if err := req.Capabilities.Set(capability.ReportStatus); err != nil {
            return nil, false, false, fmt.Errorf("set capability: %w", err)
        }
    }

hasDelete := false
12 unmodified lines

if hasDelete {
        if !adv.Capabilities.Supports(capability.DeleteRefs) {
            return nil, false, false, fmt.Errorf("target does not support delete-refs")
            return nil, false, false, errors.New("target does not support delete-refs")
        }
        _ = req.Capabilities.Set(capability.DeleteRefs)
        if err := req.Capabilities.Set(capability.DeleteRefs); err != nil {
            return nil, false, false, fmt.Errorf("set capability: %w", err)
        }
    }

_ = verbose // progress handling is server-side in HTTP mode
42 unmodified lines

return fmt.Errorf("decode report-status: %w", err)
        }
        if err := report.Error(); err != nil {
            return err
            return fmt.Errorf("report-status: %w", err)
        }
    }
    return nil
51 unmodified lines

for _, cmd := range commands {
        if cmd.Delete {
            _ = pack.Close()
            return fmt.Errorf("pack push only supports create and update actions")
            return errors.New("pack push only supports create and update actions")
        }
    }

8 unmodified lines

if err != nil {
        return err
    }
    return closeErr
    if closeErr != nil {
        return fmt.Errorf("close pack: %w", closeErr)
    }
    return nil
}

// PushCommands sends ref update commands without a pack (for ref-only changes).
44 unmodified lines

for len(b) > 0 {
        if p.atLineStart {
            if _, err := io.WriteString(p.w, p.prefix); err != nil {
                return consumed, err
                return consumed, fmt.Errorf("write prefix: %w", err)
            }
            p.atLineStart = false
        }
8 unmodified lines

n, err := p.w.Write(chunk)
        consumed += n
        if err != nil {
            return consumed, err
            return consumed, fmt.Errorf("write chunk: %w", err)
        }
        b = b[len(chunk):]
    }

Minternal/gitproto/push.go+19/-9

13 unmodified lines

14
15
16
17
18
19
20
84 unmodified lines

105
106
107
107
108
109
110
111
112
113
3 unmodified lines

117
118
119
117
120
121
122
123
124
125
33 unmodified lines

159
160
161
157
162
163
164
165
166
167
67 unmodified lines

235
236
237
231
238
239
240
241
242
243
37 unmodified lines

281
282
283
275
276
277
284
285
286
287
288
289
34 unmodified lines

324
325
326
318
327
328
329
330
321
331
332
333
334
6 unmodified lines

341
342
343
344
345
346
347

13 unmodified lines

"github.com/go-git/go-git/v6/plumbing/protocol/packp"
    "github.com/go-git/go-git/v6/plumbing/protocol/packp/capability"
    "github.com/go-git/go-git/v6/plumbing/transport"
    "github.com/stretchr/testify/require"
)

func TestPrefixedLineWriter(t *testing.T) {
84 unmodified lines

t.Helper()
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Consume the request body.
        _, _ = io.Copy(io.Discard, r.Body)
        if _, err := io.Copy(io.Discard, r.Body); err != nil {
            t.Logf("drain request body: %v", err)
        }
        _ = r.Body.Close()

w.Header().Set("Content-Type", "application/x-git-receive-pack-result")
3 unmodified lines

// Write a minimal report-status with an error.
            report := packp.NewReportStatus()
            report.UnpackStatus = reportErr
            _ = report.Encode(w)
            if err := report.Encode(w); err != nil {
                t.Logf("encode report: %v", err)
            }
        }
        // If no reportErr, write nothing -- PushPack will not try to
        // decode report-status when the capability is not negotiated.
33 unmodified lines

func TestPushPackClosesPackOnReceivePackError(t *testing.T) {
    // Server that returns HTTP 500 so the POST fails.
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        _, _ = io.Copy(io.Discard, r.Body)
        if _, err := io.Copy(io.Discard, r.Body); err != nil {
            t.Logf("drain request body: %v", err)
        }
        _ = r.Body.Close()
        http.Error(w, "receive-pack failed", http.StatusInternalServerError)
    }))
67 unmodified lines

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        started <- struct{}{}
        _, _ = io.Copy(io.Discard, r.Body)
        if _, err := io.Copy(io.Discard, r.Body); err != nil {
            t.Logf("drain request body: %v", err)
        }
        _ = r.Body.Close()
        w.WriteHeader(http.StatusOK)
    }))
37 unmodified lines

func TestBuildUpdateRequest(t *testing.T) {
    adv := packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.ReportStatus)
    _ = adv.Capabilities.Set(capability.DeleteRefs)
    _ = adv.Capabilities.Set(capability.Sideband64k)
    require.NoError(t, adv.Capabilities.Set(capability.ReportStatus))
    require.NoError(t, adv.Capabilities.Set(capability.DeleteRefs))
    require.NoError(t, adv.Capabilities.Set(capability.Sideband64k))

req, hasDelete, hasUpdates, err := buildUpdateRequest(adv, []PushCommand{
        {Name: "refs/heads/main", New: plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")},
34 unmodified lines

adv := packp.NewAdvRefs()
    adv.Capabilities = capability.NewList()
    // Use a nil-transport conn -- we should never reach the network.
    ep, _ := transport.NewEndpoint("https://example.com/repo.git")
    ep, err := transport.NewEndpoint("https://example.com/repo.git")
    require.NoError(t, err)
    conn := &Conn{Endpoint: ep, HTTP: &http.Client{}}

err := PushPack(context.Background(), conn, adv, []PushCommand{
    err = PushPack(context.Background(), conn, adv, []PushCommand{
        {Name: "refs/heads/old", Delete: true},
    }, pack, false)
    if err == nil {
6 unmodified lines

type trackingReadCloser struct {
    io.ReadCloser

closed bool
}

Minternal/gitproto/push_test.go+20/-9

44 unmodified lines

45
46
47
48
48
49
50
51
2 unmodified lines

54
55
56
57
57
58
59
60
24 unmodified lines

85
86
87
88
88
89
90
91
92
92
93
94
95
2 unmodified lines

98
99
100
101
101
102
103
104
105
106
107
82 unmodified lines

190
191
192
190
193
194
195
196
4 unmodified lines

201
202
203
201
204
205
206
207
1 unmodified line

209
210
211
209
212
213
214
215

44 unmodified lines

}
        if caps, err := DecodeV2Capabilities(bytes.NewReader(data)); err == nil {
            if !caps.Supports("ls-refs") || !caps.Supports("fetch") {
                return nil, nil, fmt.Errorf("source does not advertise required protocol v2 commands")
                return nil, nil, errors.New("source does not advertise required protocol v2 commands")
            }
            refs, err := listSourceRefsV2(ctx, conn, caps, refPrefixes)
            if err != nil {
2 unmodified lines

return refs, &RefService{Protocol: "v2", V2Caps: caps}, nil
        }
        if protocolMode == "v2" {
            return nil, nil, fmt.Errorf("source did not negotiate protocol v2")
            return nil, nil, errors.New("source did not negotiate protocol v2")
        }
        // Fall back to v1
        adv, err := decodeV1AdvRefs(data)
24 unmodified lines

func AdvRefsToSlice(ar *packp.AdvRefs) ([]*plumbing.Reference, error) {
    refs, err := ar.AllReferences()
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("all references: %w", err)
    }
    iter, err := refs.IterReferences()
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("iter references: %w", err)
    }
    defer iter.Close()

2 unmodified lines

out = append(out, ref)
        return nil
    })
    return out, err
    if err != nil {
        return nil, fmt.Errorf("iterate references: %w", err)
    }
    return out, nil
}

// AdvRefsCaps returns the sorted capability list from an AdvRefs.
82 unmodified lines

ar := packp.NewAdvRefs()
    if err := ar.Decode(rd); err != nil {
        if err == packp.ErrEmptyAdvRefs {
        if errors.Is(err, packp.ErrEmptyAdvRefs) {
            return nil, transport.ErrEmptyRemoteRepository
        }
        return nil, fmt.Errorf("%w; body-prefix=%q", err, bodyPreview(data))
4 unmodified lines

func consumeSmartInfoRefsHeader(rd *bufio.Reader) (bool, error) {
    _, prefix, err := pktline.PeekLine(rd)
    if err != nil {
        return false, err
        return false, fmt.Errorf("peek pktline: %w", err)
    }
    if !bytes.HasPrefix(prefix, []byte("# service=")) {
        return false, nil
1 unmodified line

var reply packp.SmartReply
    if err := reply.Decode(rd); err != nil {
        return true, err
        return true, fmt.Errorf("decode smart reply: %w", err)
    }
    if reply.Service == "" {
        return true, errors.New("missing smart HTTP service name")

Minternal/gitproto/refs.go+11/-8

10 unmodified lines

11
12
13
14
15
16
17
40 unmodified lines

58
59
60
60
61
62
61
62
63
64
65
66

10 unmodified lines

func TestRefHashMap(t *testing.T) {
40 unmodified lines

// AdvRefs with populated capabilities.
    adv = packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.OFSDelta)
    _ = adv.Capabilities.Add(capability.Agent, "git/test-agent")
    _ = adv.Capabilities.Set(capability.NoProgress)
    require.NoError(t, adv.Capabilities.Set(capability.OFSDelta))
    require.NoError(t, adv.Capabilities.Add(capability.Agent, "git/test-agent"))
    require.NoError(t, adv.Capabilities.Set(capability.NoProgress))

items := AdvRefsCaps(adv)
    if len(items) == 0 {

Minternal/gitproto/refs_test.go+4/-3

22 unmodified lines

23
24
25
26
27
26
27
28
29
30
60 unmodified lines

91
92
93
94
94
95
96
97
5 unmodified lines

103
104
105
106
106
107
108
109
4 unmodified lines

114
115
116
117
117
118
119
120
13 unmodified lines

134
135
136
137
137
138
139
140
14 unmodified lines

155
156
157
158
158
159
160
161
6 unmodified lines

168
169
170
171
171
172
173
174

22 unmodified lines

var reason string
    if res.Body != nil {
        limited := io.LimitReader(res.Body, maxHTTPErrorBody+1)
        data, _ := io.ReadAll(limited)
        if len(data) > 0 {
        data, err := io.ReadAll(limited)
        if err == nil && len(data) > 0 {
            if len(data) > maxHTTPErrorBody {
                data = append(data[:maxHTTPErrorBody], []byte("...")...)
            }
60 unmodified lines

url := fmt.Sprintf("%s/info/refs?service=%s", conn.Endpoint.String(), svc)
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("create info-refs request: %w", err)
    }
    req.Header.Set("Accept", "*/*")
    req.Header.Set("User-Agent", capability.DefaultAgent())
5 unmodified lines

res, err := conn.HTTP.Do(req)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("request info-refs: %w", err)
    }
    defer res.Body.Close()
    if err := httpError(res); err != nil {
4 unmodified lines

lr := io.LimitReader(res.Body, maxInfoRefsSize+1)
    data, err := io.ReadAll(lr)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("read info-refs response: %w", err)
    }
    if int64(len(data)) > maxInfoRefsSize {
        return nil, fmt.Errorf("info/refs response exceeds %d byte limit", maxInfoRefsSize)
13 unmodified lines

lr := io.LimitReader(reader, maxRPCResponse+1)
    data, err := io.ReadAll(lr)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("read RPC response: %w", err)
    }
    if int64(len(data)) > maxRPCResponse {
        return nil, fmt.Errorf("RPC response for %s exceeds %d byte limit", service, maxRPCResponse)
14 unmodified lines

url := fmt.Sprintf("%s/%s", conn.Endpoint.String(), svc)
    req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("create RPC request: %w", err)
    }
    req.Header.Set("Content-Type", fmt.Sprintf("application/x-%s-request", svc))
    req.Header.Set("Accept", fmt.Sprintf("application/x-%s-result", svc))
6 unmodified lines

res, err := conn.HTTP.Do(req)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("post RPC: %w", err)
    }
    if err := httpError(res); err != nil {
        _ = res.Body.Close()

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

58 unmodified lines

59
60
61
62
62
63
64
65
66
67
68
2 unmodified lines

71
72
73
71
74
75
76
77
78
79
80
2 unmodified lines

83
84
85
80
86
87
88
89
90
91
92
78 unmodified lines

171
172
173
165
174
175
167
176
177
178
179
30 unmodified lines

210
211
212
204
213
214
215
216

58 unmodified lines

func TestApplyAuth(t *testing.T) {
    // BasicAuth
    req, _ := http.NewRequest("GET", "https://example.com", nil)
    req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://example.com", nil)
    if err != nil {
        t.Fatalf("NewRequestWithContext: %v", err)
    }
    auth := &transporthttp.BasicAuth{Username: "user", Password: "pass"}
    ApplyAuth(req, auth)
    user, pass, ok := req.BasicAuth()
2 unmodified lines

}

// TokenAuth
    req, _ = http.NewRequest("GET", "https://example.com", nil)
    req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, "https://example.com", nil)
    if err != nil {
        t.Fatalf("NewRequestWithContext: %v", err)
    }
    tokenAuth := &transporthttp.TokenAuth{Token: "my-token"}
    ApplyAuth(req, tokenAuth)
    got := req.Header.Get("Authorization")
2 unmodified lines

}

// nil auth should not panic.
    req, _ = http.NewRequest("GET", "https://example.com", nil)
    req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, "https://example.com", nil)
    if err != nil {
        t.Fatalf("NewRequestWithContext: %v", err)
    }
    ApplyAuth(req, nil)
}

78 unmodified lines

}

func TestHTTPErrorBoundsBodyRead(t *testing.T) {
    req, err := http.NewRequest(http.MethodGet, "https://example.com/repo.git", nil)
    req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://example.com/repo.git", nil)
    if err != nil {
        t.Fatalf("NewRequest: %v", err)
        t.Fatalf("NewRequestWithContext: %v", err)
    }

body := &roundTripReader{remaining: maxHTTPErrorBody + 4096}
30 unmodified lines

if n > r.remaining {
        n = r.remaining
    }
    for i := 0; i < n; i++ {
    for i := range n {
        p[i] = 'x'
    }
    r.remaining -= n

Minternal/gitproto/smarthttp_test.go+15/-6

4 unmodified lines

5
6
7
8
9
10
11
12
12
13
14
15
16
13
14
15
16
17
18
19
20

4 unmodified lines

"github.com/go-git/go-git/v6/plumbing/protocol/packp"
    "github.com/go-git/go-git/v6/plumbing/protocol/packp/capability"
    "github.com/stretchr/testify/require"
)

func TestTargetFeaturesFromAdvRefs(t *testing.T) {
    adv := packp.NewAdvRefs()
    _ = adv.Capabilities.Set(capability.DeleteRefs)
    _ = adv.Capabilities.Set(capability.Capability("no-thin"))
    _ = adv.Capabilities.Set(capability.OFSDelta)
    _ = adv.Capabilities.Set(capability.ReportStatus)
    _ = adv.Capabilities.Set(capability.Sideband64k)
    require.NoError(t, adv.Capabilities.Set(capability.DeleteRefs))
    require.NoError(t, adv.Capabilities.Set(capability.Capability("no-thin")))
    require.NoError(t, adv.Capabilities.Set(capability.OFSDelta))
    require.NoError(t, adv.Capabilities.Set(capability.ReportStatus))
    require.NoError(t, adv.Capabilities.Set(capability.Sideband64k))

got := TargetFeaturesFromAdvRefs(adv)
    if !got.Known || !got.DeleteRefs || !got.NoThin || !got.OFSDelta || !got.ReportStatus || !got.Sideband64k {

Minternal/gitproto/target_features_test.go+6/-5

10 unmodified lines

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

40
41
42
43
43
44
45
46
29 unmodified lines

76
77
78
79
79
80
81
82
14 unmodified lines

97
98
99
100
100
101
102
103
18 unmodified lines

122
123
124
125
125
126
127
128

10 unmodified lines

func BenchmarkBuildDesiredRefs(b *testing.B) {
    sourceRefs := make(map[plumbing.ReferenceName]plumbing.Hash, 100)
    for i := 0; i < 100; i++ {
    for i := range 100 {
        name := plumbing.NewBranchReferenceName(fmt.Sprintf("branch-%03d", i))
        sourceRefs[name] = plumbing.NewHash(fmt.Sprintf("%040x", i+1))
    }
    cfg := PlanConfig{}

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        _, _, err := BuildDesiredRefs(sourceRefs, cfg)
        if err != nil {
            b.Fatal(err)
15 unmodified lines

targetRefs := make(map[plumbing.ReferenceName]plumbing.Hash, 100)
    managed := make(map[plumbing.ReferenceName]ManagedTarget, 100)

for i := 0; i < 100; i++ {
    for i := range 100 {
        ref := plumbing.NewBranchReferenceName(fmt.Sprintf("branch-%03d", i))
        short := ref.Short()

29 unmodified lines

cfg := PlanConfig{}

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        // Copy managed map each iteration because BuildPlans can mutate it
        // when Prune is set (not the case here, but copy for safety).
        mgdCopy := make(map[plumbing.ReferenceName]ManagedTarget, len(managed))
14 unmodified lines

prevSpan := 500

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        candidates := SampledCheckpointCandidates(lo, hi, prevSpan)
        if len(candidates) == 0 {
            b.Fatal("expected candidates")
18 unmodified lines

root := hashes[0]

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        ok, err := ReachesCommit(repo.Storer, tip, root)
        if err != nil {
            b.Fatal(err)

Minternal/planner/benchmark_test.go+6/-6

21 unmodified lines

22
23
24
25
25
26
27
28
3 unmodified lines

32
33
34
35
35
36
37
38
56 unmodified lines

95
96
97
98
98
99
100
101

21 unmodified lines

func FirstParentChain(store storer.EncodedObjectStorer, tip plumbing.Hash) ([]plumbing.Hash, error) {
    commit, err := object.GetCommit(store, tip)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("load tip commit %s: %w", tip, err)
    }
    chain := make([]plumbing.Hash, 0, 128)
    for {
3 unmodified lines

}
        commit, err = object.GetCommit(store, commit.ParentHashes[0])
        if err != nil {
            return nil, err
            return nil, fmt.Errorf("load parent commit %s: %w", commit.ParentHashes[0], err)
        }
    }
    // Reverse in-place to get root-to-tip order.
56 unmodified lines

const sampleCount = 4
    current := projected
    for i := 0; i < sampleCount-1; i++ {
    for range sampleCount - 1 {
        if current <= lo {
            add(lo)
            continue

Minternal/planner/checkpoint.go+3/-3

4 unmodified lines

5
6
7
8
9
10
11
11
12
13
14
39 unmodified lines

54
55
56
57
57
58
59
60
68 unmodified lines

129
130
131
132
132
133
134
135
7 unmodified lines

143
144
145
146
146
147
148
149
13 unmodified lines

163
164
165
166
167
168
169
170
35 unmodified lines

206
207
208
207
209
210
211
212
61 unmodified lines

274
275
276
275
277
278
279
280
281
282
283
282
284
285
286
287
286
288
289
290
291
2 unmodified lines

294
295
296
295
297
298
299
300
301
302
303
302
304
305
306
307
308
309
308
310
311
312
313
314
313
315
316
317
318
10 unmodified lines

329
330
331
330
332
333
334
335
336
337
336
338
339
340
341
342
343
344
343
345
346
347
348
345
349
350
351
352
45 unmodified lines

398
399
400
397
401
402
403
404
86 unmodified lines

491
492
493
490
494
495
496
497
498

4 unmodified lines

"fmt"
    "sort"

"github.com/entirehq/git-sync/internal/validation"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/go-git/go-git/v6/plumbing/object"
    "github.com/go-git/go-git/v6/plumbing/storer"
    "github.com/entirehq/git-sync/internal/validation"
)

// PlanConfig holds configuration for plan generation.
39 unmodified lines

// Validate all mappings up front (issue #2, #3)
        normalized, err := validation.ValidateMappings(cfg.Mappings)
        if err != nil {
            return nil, nil, err
            return nil, nil, fmt.Errorf("validate ref mappings: %w", err)
        }
        for _, nm := range normalized {
            kind := RefKindFromName(nm.TargetRef)
68 unmodified lines

TargetHash: targetHash,
                    Kind:       info.Kind,
                    Action:     ActionDelete,
                    Reason:     fmt.Sprintf("%s -> <deleted>", ShortHash(targetHash)),
                    Reason:     ShortHash(targetHash) + " -> <deleted>",
                })
            }
            continue
7 unmodified lines

SourceHash: want.SourceHash,
                Kind:       want.Kind,
                Action:     ActionCreate,
                Reason:     fmt.Sprintf("%s -> <new>", ShortHash(want.SourceHash)),
                Reason:     ShortHash(want.SourceHash) + " -> <new>",
            })
            continue
        }
13 unmodified lines

// BuildReplicationPlans generates overwrite-oriented plans for replication mode.
// Divergent refs are updated directly rather than blocked.
//
//nolint:unparam // error return kept for API consistency with BuildPlans
func BuildReplicationPlans(
    desired map[plumbing.ReferenceName]DesiredRef,
    targetRefs map[plumbing.ReferenceName]plumbing.Hash,
35 unmodified lines

TargetHash: targetHash,
                    Kind:       info.Kind,
                    Action:     ActionDelete,
                    Reason:     fmt.Sprintf("%s -> <deleted>", ShortHash(targetHash)),
                    Reason:     ShortHash(targetHash) + " -> <deleted>",
                })
            }
            continue
61 unmodified lines

if want.SourceHash == targetHash {
        plan.Action = ActionSkip
        plan.Reason = fmt.Sprintf("%s already current", ShortHash(want.SourceHash))
        plan.Reason = ShortHash(want.SourceHash) + " already current"
        return plan, nil
    }

if want.Kind == RefKindTag {
        if force {
            plan.Action = ActionUpdate
            plan.Reason = fmt.Sprintf("%s -> %s (force tag update)", ShortHash(targetHash), ShortHash(want.SourceHash))
            plan.Reason = ShortHash(targetHash) + " -> " + ShortHash(want.SourceHash) + " (force tag update)"
            return plan, nil
        }
        plan.Action = ActionBlock
        plan.Reason = fmt.Sprintf("%s differs from %s; use --force to retarget tag", ShortHash(targetHash), ShortHash(want.SourceHash))
        plan.Reason = ShortHash(targetHash) + " differs from " + ShortHash(want.SourceHash) + "; use --force to retarget tag"
        return plan, nil
    }

2 unmodified lines

if errors.Is(err, ErrAncestryDepthExceeded) {
            // Can't prove fast-forward within depth limit — block with explanation.
            plan.Action = ActionBlock
            plan.Reason = fmt.Sprintf("ancestry check for %s exceeded depth limit; use --force if this is a valid fast-forward", want.TargetRef)
            plan.Reason = "ancestry check for " + want.TargetRef.String() + " exceeded depth limit; use --force if this is a valid fast-forward"
            return plan, nil
        }
        return plan, fmt.Errorf("check fast-forward for %s: %w", want.TargetRef, err)
    }
    if isFF {
        plan.Action = ActionUpdate
        plan.Reason = fmt.Sprintf("%s -> %s", ShortHash(targetHash), ShortHash(want.SourceHash))
        plan.Reason = ShortHash(targetHash) + " -> " + ShortHash(want.SourceHash)
        return plan, nil
    }

if force {
        plan.Action = ActionUpdate
        plan.Reason = fmt.Sprintf("%s -> %s (force)", ShortHash(targetHash), ShortHash(want.SourceHash))
        plan.Reason = ShortHash(targetHash) + " -> " + ShortHash(want.SourceHash) + " (force)"
        return plan, nil
    }

plan.Action = ActionBlock
    plan.Reason = fmt.Sprintf("%s is not an ancestor of %s", ShortHash(targetHash), ShortHash(want.SourceHash))
    plan.Reason = ShortHash(targetHash) + " is not an ancestor of " + ShortHash(want.SourceHash)
    return plan, nil
}

10 unmodified lines

if want.SourceHash == targetHash {
        plan.Action = ActionSkip
        plan.Reason = fmt.Sprintf("%s already current", ShortHash(want.SourceHash))
        plan.Reason = ShortHash(want.SourceHash) + " already current"
        return plan
    }

if !existsOnTarget || targetHash.IsZero() {
        plan.Action = ActionCreate
        plan.Reason = fmt.Sprintf("%s -> <new>", ShortHash(want.SourceHash))
        plan.Reason = ShortHash(want.SourceHash) + " -> <new>"
        return plan
    }

plan.Action = ActionUpdate
    switch want.Kind {
    case RefKindTag:
        plan.Reason = fmt.Sprintf("%s -> %s (replicate tag overwrite)", ShortHash(targetHash), ShortHash(want.SourceHash))
        plan.Reason = ShortHash(targetHash) + " -> " + ShortHash(want.SourceHash) + " (replicate tag overwrite)"
    case RefKindBranch:
        plan.Reason = ShortHash(targetHash) + " -> " + ShortHash(want.SourceHash) + " (replicate overwrite)"
    default:
        plan.Reason = fmt.Sprintf("%s -> %s (replicate overwrite)", ShortHash(targetHash), ShortHash(want.SourceHash))
        plan.Reason = ShortHash(targetHash) + " -> " + ShortHash(want.SourceHash) + " (replicate overwrite)"
    }
    return plan
}
45 unmodified lines

if errors.Is(err, plumbing.ErrObjectNotFound) {
                    continue
                }
                return false, err
                return false, fmt.Errorf("load parent commit %s: %w", parentHash, err)
            }
            stack = append(stack, parent)
        }
86 unmodified lines

return err
        }
    case plumbing.BlobObject:
    default:
        // Blobs are leaf objects — nothing to recurse into.
    case plumbing.InvalidObject, plumbing.OFSDeltaObject, plumbing.REFDeltaObject, plumbing.AnyObject:
        return fmt.Errorf("unsupported object type %s for %s", obj.Type(), hash)
    }

Minternal/planner/planner.go+23/-18

5 unmodified lines

6
7
8
9
10
11
12
13
13
14
15
16

5 unmodified lines

"testing"
    "time"

"github.com/entirehq/git-sync/internal/validation"
    git "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/go-git/go-git/v6/plumbing/object"
    "github.com/go-git/go-git/v6/storage/memory"
    "github.com/entirehq/git-sync/internal/validation"
)

func TestSelectBranches(t *testing.T) {

Minternal/planner/planner_test.go+1/-1

117 unmodified lines

118
119
120
121
122
123
124
121
122
123
124
125
126

117 unmodified lines

func RelayFallbackReason(force, prune, dryRun bool, plans []BranchPlan, target RelayTargetPolicy) string {
    if ok, reason := CanIncrementalRelay(force, prune, dryRun, plans, target); ok {
        return reason
    } else if ok, reason := CanFullTagCreateRelay(plans); ok {
        return reason
    } else {
        return reason
    }
    _, reason := CanFullTagCreateRelay(plans)
    return reason
}

// CanReplicateRelay checks whether replication mode can execute as a relay-only

Minternal/planner/relay.go+2/-4

5 unmodified lines

6
7
8
9
9
10
11
12
13
56 unmodified lines

70
71
72
73
73
74
75
76
3 unmodified lines

80
81
82
83
84
85
86
87
88
89
61 unmodified lines

151
152
153
150
154
155
156
152
157
158
154
159
160
161
162

5 unmodified lines

"sort"
    "strings"

"github.com/go-git/go-git/v6/plumbing"
    "github.com/entirehq/git-sync/internal/validation"
    "github.com/go-git/go-git/v6/plumbing"
)

// RefKind distinguishes branch refs from tag refs.
56 unmodified lines

Action     Action  `json:"action"`
        Reason     string  `json:"reason"`
    }
    return json.Marshal(bp{
    data, err := json.Marshal(bp{
        Branch:     p.Branch,
        SourceRef:  p.SourceRef.String(),
        TargetRef:  p.TargetRef.String(),
3 unmodified lines

Action:     p.Action,
        Reason:     p.Reason,
    })
    if err != nil {
        return nil, fmt.Errorf("marshal branch plan: %w", err)
    }
    return data, nil
}

// ShortHash returns the first 8 characters of a hash, or "<zero>" for zero hashes.
61 unmodified lines

if len(mappings) > 0 {
        for _, m := range mappings {
            src := strings.TrimSpace(m.Source)
            if strings.HasPrefix(src, "refs/tags/") {
            switch {
            case strings.HasPrefix(src, "refs/tags/"):
                prefixSet["refs/tags/"] = struct{}{}
            } else if strings.HasPrefix(src, "refs/heads/") {
            case strings.HasPrefix(src, "refs/heads/"):
                prefixSet["refs/heads/"] = struct{}{}
            } else if !strings.HasPrefix(src, "refs/") {
            case !strings.HasPrefix(src, "refs/"):
                prefixSet["refs/heads/"] = struct{}{}
            }
        }

Minternal/planner/types.go+10/-5

28 unmodified lines

29
30
31
32
33
32
33
34
35
36
5 unmodified lines

42
43
44
45
46
45
46
47
48
49
50
51
50
51
52
53
54
55
53
54
55
56
57
58
57
58
59
60
61
11 unmodified lines

73
74
75
76
77
78
79
80
81
82
82
83
84
85
86
9 unmodified lines

96
97
98
98
99
100
101
102
58 unmodified lines

161
162
163
163
164
165
166
167
168
169
170
170
171
172
173
174
11 unmodified lines

186
187
188
188
189
190
191
192
263 unmodified lines

456
457
458
458
459
460
461
462
40 unmodified lines

503
504
505
505
506
507
508
509
510
511
511
512
513
514
515
5 unmodified lines

521
522
523
523
524
525
526
527
5 unmodified lines

533
534
535
535
536
537
538
539
540
541
542
543
544
545
47 unmodified lines

593
594
595
589
596
597
598
599
23 unmodified lines

623
624
625
619
626
627
628
629
32 unmodified lines

662
663
664
658
665
666
667
668
102 unmodified lines

771
772
773
774
775
776
777
2 unmodified lines

780
781
782
775
783
784
785
786
787
788
789

28 unmodified lines

)

const (
    defaultTargetMaxPackBytes = 512 * 1024 * 1024
    githubLargeRepoThresholdKB   = 1536 * 1024
    defaultTargetMaxPackBytes  = 512 * 1024 * 1024
    githubLargeRepoThresholdKB = 1536 * 1024
)

var bodyLimitPattern = regexp.MustCompile(`body exceeded size limit ([0-9]+)`)
5 unmodified lines

type Params struct {
    SourceConn    *gitproto.Conn
    SourceService interface {
        FetchPack(context.Context, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error)
        FetchCommitGraph(context.Context, storer.Storer, *gitproto.Conn, gitproto.DesiredRef) 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) error
        SupportsBootstrapBatch() bool
    }
    TargetPusher interface {
        PushPack(context.Context, []gitproto.PushCommand, io.ReadCloser) error
        PushCommands(context.Context, []gitproto.PushCommand) error
        PushPack(ctx context.Context, cmds []gitproto.PushCommand, pack io.ReadCloser) error
        PushCommands(ctx context.Context, cmds []gitproto.PushCommand) error
    }
    DesiredRefs  map[plumbing.ReferenceName]planner.DesiredRef
    TargetRefs   map[plumbing.ReferenceName]plumbing.Hash
    MaxPackBytes int64
    DesiredRefs   map[plumbing.ReferenceName]planner.DesiredRef
    TargetRefs    map[plumbing.ReferenceName]plumbing.Hash
    MaxPackBytes  int64
    TargetMaxPack int64
    Verbose      bool
    Logger       *slog.Logger
    Verbose       bool
    Logger        *slog.Logger
}

// Result holds the outcome of the bootstrap strategy.
11 unmodified lines

type plannedBatch struct {
    planner.BootstrapBatch

chain []plumbing.Hash // full first-parent chain (root→tip) for subdividing on push failure
}

// Execute runs the bootstrap strategy (one-shot or batched).
func Execute(ctx context.Context, p Params, relayReason string) (Result, error) {
    if p.TargetPusher == nil {
        return Result{Relay: true, RelayMode: "bootstrap", RelayReason: relayReason}, fmt.Errorf("bootstrap strategy requires TargetPusher")
        return Result{Relay: true, RelayMode: "bootstrap", RelayReason: relayReason}, errors.New("bootstrap strategy requires TargetPusher")
    }

// GitHub large-repo preflight
9 unmodified lines

}
    plans, err := planner.BuildBootstrapPlans(p.DesiredRefs, planTargetRefs)
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("build bootstrap plans: %w", err)
    }

result := Result{
58 unmodified lines

// --- Batched bootstrap ---

func executeBatched(
func executeBatched( //nolint:maintidx // complex batch logic is inherently branchy
    ctx context.Context,
    p Params,
    plans []planner.BranchPlan,
    result Result,
) (Result, error) {
    if !p.SourceService.SupportsBootstrapBatch() {
        return result, fmt.Errorf("bootstrap batching requires protocol v2 source fetch filter support")
        return result, errors.New("bootstrap batching requires protocol v2 source fetch filter support")
    }

planRefs := make([]planner.DesiredRef, 0, len(plans))
11 unmodified lines

continue
        }
        if !plan.SourceRef.IsBranch() || !plan.TargetRef.IsBranch() {
            return result, fmt.Errorf("bootstrap batching currently supports branch refs and create-only tags")
            return result, errors.New("bootstrap batching currently supports branch refs and create-only tags")
        }
        planRefs = append(planRefs, p.DesiredRefs[plan.TargetRef])
    }
263 unmodified lines

return nil, nil, fmt.Errorf("fetch bootstrap planning graph for %s: %w", ref.TargetRef, err)
    }
    chain, err := planner.FirstParentChain(graphStore, ref.SourceHash)
    graphStore = nil // allow GC to reclaim the commit graph store (~4.6 GB for linux)
    graphStore = nil //nolint:ineffassign,wastedassign // clear reference so GC can reclaim ~4.6 GB commit graph store
    runtime.GC()
    if err != nil {
        return nil, nil, fmt.Errorf("walk first-parent chain for %s: %w", ref.TargetRef, err)
40 unmodified lines

r io.ReadCloser,
    batchLimit int64,
    subdivide func() bool,
) (io.ReadCloser, error) {
) (io.ReadCloser, error) { //nolint:unparam // error return kept for future use
    var header [12]byte
    n, err := io.ReadFull(r, header[:])
    if err != nil {
        // Short pack or error — let the push handle it
        prefixed := io.MultiReader(bytes.NewReader(header[:n]), r)
        return &wrappedMultiRC{Reader: prefixed, Closer: r}, nil
        return &wrappedMultiRC{Reader: prefixed, Closer: r}, nil //nolint:nilerr // below threshold is not an error, we return the original reader
    }
    if string(header[:4]) != "PACK" {
        // Not a standard packfile — can't estimate, proceed
5 unmodified lines

if estimated > batchLimit && subdivide() {
        _ = r.Close()
        return nil, nil
        return nil, nil //nolint:nilnil // nil reader signals no subdivision needed
    }

prefixed := io.MultiReader(bytes.NewReader(header[:]), r)
5 unmodified lines

io.Closer
}

func (w *wrappedMultiRC) Read(p []byte) (int, error) { return w.Reader.Read(p) }
func (w *wrappedMultiRC) Read(p []byte) (int, error) {
    n, err := w.Reader.Read(p)
    if err != nil && !errors.Is(err, io.EOF) {
        return n, fmt.Errorf("read prepended pack: %w", err)
    }
    return n, err //nolint:wrapcheck // io.EOF must not be wrapped to preserve io.Reader contract
}

// chainPosition returns the index of hash in chain, or -1 if not found.
func chainPosition(chain []plumbing.Hash, hash plumbing.Hash) int {
47 unmodified lines

}
    checkpoints := make([]plumbing.Hash, 0, numBatches)
    batchSize := len(chain) / numBatches
    for i := 0; i < numBatches-1; i++ {
    for i := range numBatches - 1 {
        idx := (i+1)*batchSize - 1
        if idx >= len(chain)-1 {
            break
23 unmodified lines

}
    packReader, err := p.SourceService.FetchPack(ctx, p.SourceConn, desired, haves)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("fetch checkpoint pack: %w", err)
    }
    return gitproto.LimitPackReader(packReader, batchLimit), nil
}
32 unmodified lines

return 0, false
    }
    req.Header.Set("Accept", "application/vnd.github+json")
    req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
    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)
102 unmodified lines

type closeOnceReadCloser struct {
    io.ReadCloser

once sync.Once
}

2 unmodified lines

c.once.Do(func() {
        err = c.ReadCloser.Close()
    })
    return err
    if err != nil {
        return fmt.Errorf("close pack reader: %w", err)
    }
    return nil
}

func closeOnce(rc io.ReadCloser) io.ReadCloser {

Minternal/strategy/bootstrap/bootstrap.go+36/-25

266 unmodified lines

267
268
269
270
270
271
272
273
274
275
10 unmodified lines

286
287
288
287
289
290
291
292
293
294
295
135 unmodified lines

431
432
433
434
435
436
437
161 unmodified lines

599
600
601
596
602
603
604
605
11 unmodified lines

617
618
619
614
620
621
622
623
15 unmodified lines

639
640
641
636
642
643
644
645
12 unmodified lines

658
659
660
655
661
662
663
664
34 unmodified lines

699
700
701
696
702
703
704
705
8 unmodified lines

714
715
716
711
717
718
719
720
16 unmodified lines

737
738
739
734
740
741
742
743
23 unmodified lines

767
768
769
764
770
771
772
773

266 unmodified lines

t.Run("small pack proceeds without subdivide", func(t *testing.T) {
        header := makePackHeader(100) // 100 * 750 = 75000 bytes estimated
        body := append(header, []byte("packdata")...)
        body := make([]byte, 0, len(header)+len("packdata"))
        body = append(body, header...)
        body = append(body, []byte("packdata")...)
        r := io.NopCloser(bytes.NewReader(body))
        subdivided := false
        got, err := checkPackSizeAndSubdivide(r, 1_000_000, func() bool {
10 unmodified lines

t.Fatal("should not subdivide small pack")
        }
        // Verify the PACK header was prepended back
        out, _ := io.ReadAll(got)
        out, err2 := io.ReadAll(got)
        if err2 != nil {
            t.Fatalf("unexpected ReadAll error: %v", err2)
        }
        if string(out[:4]) != "PACK" {
            t.Fatalf("expected PACK header preserved, got %q", out[:4])
        }
135 unmodified lines

type trackingReadCloser struct {
    io.Reader

closed bool
}

161 unmodified lines

writeLinearCommitChain(t, store, 1)
                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, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
                return pack, nil
            },
        },
11 unmodified lines

Label:      "main",
            },
        },
        TargetRefs:   map[plumbing.ReferenceName]plumbing.Hash{},
        TargetRefs:    map[plumbing.ReferenceName]plumbing.Hash{},
        TargetMaxPack: 10,
    }, "empty target")
    if err == nil || !strings.Contains(err.Error(), "push bootstrap batch") {
15 unmodified lines

writeLinearCommitChain(t, store, 1)
                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, _ map[plumbing.ReferenceName]gitproto.DesiredRef, _ map[plumbing.ReferenceName]plumbing.Hash) (io.ReadCloser, error) {
                return pack, nil
            },
        },
12 unmodified lines

Label:      "main",
            },
        },
        TargetRefs:   map[plumbing.ReferenceName]plumbing.Hash{},
        TargetRefs:    map[plumbing.ReferenceName]plumbing.Hash{},
        TargetMaxPack: 10,
    }, "empty target")
    if !errors.Is(err, io.ErrUnexpectedEOF) {
34 unmodified lines

Kind:       planner.RefKindBranch,
                    },
                },
                TargetRefs:   map[plumbing.ReferenceName]plumbing.Hash{},
                TargetRefs:    map[plumbing.ReferenceName]plumbing.Hash{},
                TargetMaxPack: tt.batchMaxPack,
            }, "missing pusher")
            if err == nil || err.Error() != "bootstrap strategy requires TargetPusher" {
8 unmodified lines

func TestExecuteRequiresTargetPusherBeforeGitHubPreflight(t *testing.T) {
    requests := 0
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
        requests++
        t.Fatalf("unexpected preflight request: %s %s", r.Method, r.URL.String())
    }))
16 unmodified lines

SourceService: fakeBootstrapSource{
            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
                return nil, nil //nolint:nilnil // test fake returns nil to signal no data
            },
        },
        DesiredRefs: map[plumbing.ReferenceName]planner.DesiredRef{
23 unmodified lines

func writeLinearCommitChain(tb testing.TB, store storer.Storer, count int) []plumbing.Hash {
    tb.Helper()
    hashes := make([]plumbing.Hash, 0, count)
    for i := 0; i < count; i++ {
    for i := range count {
        obj := store.NewEncodedObject()
        var parents []plumbing.Hash
        if len(hashes) > 0 {

Minternal/strategy/bootstrap/bootstrap_test.go+16/-10

4 unmodified lines

5
6
7
8
9
10
11
9 unmodified lines

21
22
23
23
24
25
26
26
27
28
29
30
17 unmodified lines

48
49
50
50
51
52
53
53
54
55
56
57
13 unmodified lines

71
72
73
73
74
75
76
77
16 unmodified lines

94
95
96
97
98
99
100
2 unmodified lines

103
104
105
104
106
107
108
109
110
111
112

4 unmodified lines

import (
    "context"
    "errors"
    "fmt"
    "io"
    "sync"
9 unmodified lines

type Params struct {
    SourceConn    *gitproto.Conn
    SourceService interface {
        FetchPack(context.Context, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, 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(context.Context, []gitproto.PushCommand, io.ReadCloser) error
        PushPack(ctx context.Context, cmds []gitproto.PushCommand, pack io.ReadCloser) error
    }
    DesiredRefs  map[plumbing.ReferenceName]planner.DesiredRef
    TargetRefs   map[plumbing.ReferenceName]plumbing.Hash
17 unmodified lines

func Execute(ctx context.Context, p Params, cfg planner.PlanConfig) (Result, error) {
    canRelay := p.CanRelay
    if canRelay == nil {
        return Result{}, fmt.Errorf("incremental strategy requires CanRelay")
        return Result{}, errors.New("incremental strategy requires CanRelay")
    }
    if p.TargetPusher == nil {
        return Result{}, fmt.Errorf("incremental strategy requires TargetPusher")
        return Result{}, errors.New("incremental strategy requires TargetPusher")
    }
    cmds := convert.PlansToPushCommands(p.PushPlans)
    if ok, reason := canRelay(cfg.Force, cfg.Prune, false, p.PushPlans); ok {
13 unmodified lines

}

if p.CanTagRelay == nil {
        return Result{}, fmt.Errorf("incremental strategy requires CanTagRelay")
        return Result{}, errors.New("incremental strategy requires CanTagRelay")
    }
    if ok, reason := p.CanTagRelay(p.PushPlans); ok {
        desired := convert.DesiredRefsForPlans(p.DesiredRefs, p.PushPlans)
16 unmodified lines

type closeOnceReadCloser struct {
    io.ReadCloser

once sync.Once
}

2 unmodified lines

c.once.Do(func() {
        err = c.ReadCloser.Close()
    })
    return err
    if err != nil {
        return fmt.Errorf("close pack reader: %w", err)
    }
    return nil
}

func closeOnce(rc io.ReadCloser) io.ReadCloser {

Minternal/strategy/incremental/incremental.go+11/-6

134 unmodified lines

135
136
137
138
139
140
141
24 unmodified lines

166
167
168
169
170
171
172
173
39 unmodified lines

213
214
215
213
216
217
218
219
220
221
222
220
223
224
225
226
98 unmodified lines

325
326
327
325
328
329
330
331
40 unmodified lines

372
373
374
372
375
376
377
378

134 unmodified lines

type trackingReadCloser struct {
    io.Reader

closed bool
}

24 unmodified lines

return nil
}

const testReasonFastForward = "fast-forward"

func TestExecuteIncrementalRelayUsesTargetRefsAsHaves(t *testing.T) {
    mainRef := plumbing.NewBranchReferenceName("main")
    oldHash := plumbing.NewHash("1111111111111111111111111111111111111111")
39 unmodified lines

if force || prune || dryRun || len(plans) != 1 {
                t.Fatalf("unexpected relay inputs: force=%v prune=%v dryRun=%v plans=%d", force, prune, dryRun, len(plans))
            }
            return true, "fast-forward"
            return true, testReasonFastForward
        },
    }
    result, err := Execute(context.Background(), params, planner.PlanConfig{})
    if err != nil {
        t.Fatalf("Execute() error = %v", err)
    }
    if !result.Relay || result.RelayMode != "incremental" || result.RelayReason != "fast-forward" {
    if !result.Relay || result.RelayMode != "incremental" || result.RelayReason != testReasonFastForward {
        t.Fatalf("unexpected result: %+v", result)
    }
    if gotDesired[mainRef].SourceHash != newHash {
98 unmodified lines

Action:     planner.ActionUpdate,
        }},
        CanRelay: func(bool, bool, bool, []planner.BranchPlan) (bool, string) {
            return true, "fast-forward"
            return true, testReasonFastForward
        },
    }, planner.PlanConfig{})
    if err == nil || err.Error() != "push target refs: boom" {
40 unmodified lines

Action:     planner.ActionUpdate,
        }},
        CanRelay: func(bool, bool, bool, []planner.BranchPlan) (bool, string) {
            return true, "fast-forward"
            return true, testReasonFastForward
        },
    }, planner.PlanConfig{})
    if !errors.Is(err, io.ErrUnexpectedEOF) {

Minternal/strategy/incremental/incremental_test.go+7/-4

4 unmodified lines

5
6
7
8
9
10
11
10 unmodified lines

22
23
24
24
25
26
27
27
28
29
30
31
51 unmodified lines

83
84
85
85
86
86
87
88
89
90
5 unmodified lines

96
97
98
98
99
100
101
102
103
104
105
106
10 unmodified lines

117
118
119
115
120
121
122
123

4 unmodified lines

import (
    "context"
    "errors"
    "fmt"

git "github.com/go-git/go-git/v6"
10 unmodified lines

Store         storer.Storer
    SourceConn    *gitproto.Conn
    SourceService interface {
        FetchToStore(context.Context, storer.Storer, *gitproto.Conn, map[plumbing.ReferenceName]gitproto.DesiredRef, 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(context.Context, []gitproto.PushCommand, storer.Storer, []plumbing.Hash) error
        PushObjects(ctx context.Context, cmds []gitproto.PushCommand, store storer.Storer, hashes []plumbing.Hash) error
    }
    DesiredRefs map[plumbing.ReferenceName]planner.DesiredRef
    TargetRefs  map[plumbing.ReferenceName]plumbing.Hash
51 unmodified lines

return nil
    }
    err := e.params.SourceService.FetchToStore(e.ctx, e.params.Store, e.params.SourceConn, tagDesired, nil)
    if err != nil && err != git.NoErrAlreadyUpToDate {
        return err
    if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
        return fmt.Errorf("fetch tag objects to store: %w", err)
    }
    return nil
}
5 unmodified lines

objects = append(objects, plan.SourceHash)
        }
    }
    return planner.ObjectsToPush(e.params.Store, objects, e.params.TargetRefs)
    hashes, err := planner.ObjectsToPush(e.params.Store, objects, e.params.TargetRefs)
    if err != nil {
        return nil, fmt.Errorf("compute objects to push: %w", err)
    }
    return hashes, nil
}

func (e *executor) enforceObjectLimit(hashes []plumbing.Hash) error {
10 unmodified lines

func (e *executor) push(hashes []plumbing.Hash) error {
    cmds := convert.PlansToPushCommands(e.params.PushPlans)
    if e.params.TargetPusher == nil {
        return fmt.Errorf("materialized strategy requires TargetPusher")
        return errors.New("materialized strategy requires TargetPusher")
    }
    if err := e.params.TargetPusher.PushObjects(e.ctx, cmds, e.params.Store, hashes); err != nil {
        return fmt.Errorf("push target refs: %w", err)

Minternal/strategy/materialized/materialized.go+11/-6

2 unmodified lines

3
4
5
6
7
8
9
9 unmodified lines

19
20
21
21
22
23
24
24
25
25
26
27
28
29
12 unmodified lines

42
43
44
44
45
46
47
48
4 unmodified lines

53
54
55
56
57
58
59
60
25 unmodified lines

86
87
88
89
90
91
92
2 unmodified lines

95
96
97
94
98
99
100
101
102
103
104

2 unmodified lines

import (
    "context"
    "errors"
    "fmt"
    "io"
    "sync"
9 unmodified lines

// relay and deletes are sent afterwards as ref-only commands.
func Execute(ctx context.Context, p Params) (Result, error) {
    if p.TargetPusher == nil {
        return Result{}, fmt.Errorf("replicate strategy requires TargetPusher")
        return Result{}, errors.New("replicate strategy requires TargetPusher")
    }

updatePlans := make([]planner.BranchPlan, 0, len(p.PushPlans))
4 unmodified lines

updatePlans = append(updatePlans, plan)
        case planner.ActionDelete:
            deletePlans = append(deletePlans, plan)
        case planner.ActionSkip, planner.ActionBlock:
            // not applicable
        default:
            return Result{}, fmt.Errorf("replicate strategy does not support %s actions", plan.Action)
        }
25 unmodified lines

type closeOnceReadCloser struct {
    io.ReadCloser

once sync.Once
}

2 unmodified lines

c.once.Do(func() {
        err = c.ReadCloser.Close()
    })
    return err
    if err != nil {
        return fmt.Errorf("close pack reader: %w", err)
    }
    return nil
}

func closeOnce(rc io.ReadCloser) io.ReadCloser {

Minternal/strategy/replicate/replicate.go+12/-5

24 unmodified lines

25
26
27
28
28
29
30
31
79 unmodified lines

111
112
113
114
114
115
116
117
30 unmodified lines

148
149
150
151
151
152
153
154
155
156

24 unmodified lines

originalFill := auth.GitCredentialFillCommand
    t.Cleanup(func() { auth.GitCredentialFillCommand = originalFill })
    auth.GitCredentialFillCommand = func(ctx context.Context, input string) ([]byte, error) {
    auth.GitCredentialFillCommand = func(_ context.Context, input string) ([]byte, error) {
        t.Fatalf("unexpected git credential fill call with input %q", input)
        return nil, nil
    }
79 unmodified lines

originalFill := auth.GitCredentialFillCommand
    t.Cleanup(func() { auth.GitCredentialFillCommand = originalFill })
    auth.GitCredentialFillCommand = func(ctx context.Context, input string) ([]byte, error) {
    auth.GitCredentialFillCommand = func(_ context.Context, input string) ([]byte, error) {
        t.Fatalf("unexpected git credential fill call with input %q", input)
        return nil, nil
    }
30 unmodified lines

t.Fatalf("unexpected refresh token: %s", got)
        }
        w.Header().Set("Content-Type", "application/json")
        _, _ = w.Write([]byte(`{"access_token":"new-token","refresh_token":"new-refresh","expires_in":3600}`))
        if _, err := w.Write([]byte(`{"access_token":"new-token","refresh_token":"new-refresh","expires_in":3600}`)); err != nil {
            t.Errorf("write response: %v", err)
        }
    }))
    defer server.Close()

Minternal/syncer/auth_test.go+5/-3

1 unmodified line

2
3
4
5
6
7
8
9
9
10
11
12
7 unmodified lines

20
21
22
23
23
24
25
26
20 unmodified lines

47
48
49
50
50
51
52
53
31 unmodified lines

85
86
87
88
88
89
90
91

1 unmodified line

import (
    "context"
    "github.com/entirehq/git-sync/internal/syncertest"
    git "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/go-git/go-git/v6/plumbing/storer"
    "github.com/go-git/go-git/v6/storage/memory"
    "github.com/entirehq/git-sync/internal/syncertest"
    "io"
    "testing"
)
7 unmodified lines

defer sourceServer.Close()

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        b.StopTimer()
        targetRepo, err := git.Init(memory.NewStorage())
        if err != nil {
20 unmodified lines

b.ReportAllocs()

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        b.StopTimer()
        sourceRepo, sourceFS := syncertest.NewMemoryRepo(b)
        syncertest.MakeBenchmarkCommits(b, sourceRepo, sourceFS, 2)
31 unmodified lines

b.ReportAllocs()

b.ResetTimer()
    for i := 0; i < b.N; i++ {
    for range b.N {
        b.StopTimer()
        sourceRepo, sourceFS := syncertest.NewMemoryRepo(b)
        syncertest.MakeBenchmarkCommits(b, sourceRepo, sourceFS, 3)

Minternal/syncer/benchmark_test.go+4/-4

2 unmodified lines

3
4
5
6
7
8
9
106 unmodified lines

116
117
118
118
119
120
119
120
121
122
122
123
124
125
126
70 unmodified lines

197
198
199
199
200
201
202
203

2 unmodified lines

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/url"
    "os"
106 unmodified lines

Token:         token,
            SkipTLSVerify: skipTLSVerify,
        },
        Branches:          []string{branch},
        Verbose:           true,
        MaxPackBytes:      maxPackBytes,
        Branches:           []string{branch},
        Verbose:            true,
        MaxPackBytes:       maxPackBytes,
        TargetMaxPackBytes: batchMaxPackBytes,
        ProtocolMode:      protocolMode,
        ProtocolMode:       protocolMode,
    })
    if err != nil {
        t.Fatalf("sync public source into Entire failed: %v", err)
70 unmodified lines

if path, err := exec.LookPath("entiredb"); err == nil {
        return path, nil
    }
    return "", fmt.Errorf("could not find entiredb; set GITSYNC_E2E_ENTIREDB_BIN or put entiredb on PATH")
    return "", errors.New("could not find entiredb; set GITSYNC_E2E_ENTIREDB_BIN or put entiredb on PATH")
}

func entireCLIEnv(baseURL string, skipTLSVerify bool) []string {

Minternal/syncer/entire_local_smoke_test.go+6/-5

14 unmodified lines

15
16
17
18
18
19
20
21
22
23
24
62 unmodified lines

87
88
89
90
90
91
92
93
125 unmodified lines

219
220
221
222
222
223
224
225
65 unmodified lines

291
292
293
294
294
295
296
297
54 unmodified lines

352
353
354
355
355
356
357
358
85 unmodified lines

444
445
446
447
448
447
448
449
450
451
2 unmodified lines

454
455
456
457
457
458
459
460
47 unmodified lines

508
509
510
511
512
511
512
513
514
514
515
516
517
35 unmodified lines

553
554
555
556
556
557
558
559
74 unmodified lines

634
635
636
637
637
638
639
640
59 unmodified lines

700
701
702
703
704
705
703
704
705
706
707
708
2 unmodified lines

711
712
713
714
714
715
716
717
52 unmodified lines

770
771
772
773
773
774
775
776
28 unmodified lines

805
806
807
808
808
809
810
811

14 unmodified lines

"sync"
    "testing"

"github.com/go-git/go-git/v6/plumbing"
    "github.com/entirehq/git-sync/internal/gitproto"
    "github.com/entirehq/git-sync/internal/planner"
    bstrap "github.com/entirehq/git-sync/internal/strategy/bootstrap"
    "github.com/go-git/go-git/v6/plumbing"
)

const gitHTTPBackendEnv = "GITSYNC_E2E_GIT_HTTP_BACKEND"
62 unmodified lines

if result.Pushed != 1 || result.Blocked != 0 {
        t.Fatalf("unexpected incremental result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "incremental" {
    if !result.Relay || result.RelayMode != relayModeIncremental {
        t.Fatalf("expected incremental sync to use incremental relay, got %+v", result)
    }

125 unmodified lines

if result.Pushed != 2 || result.Blocked != 0 {
        t.Fatalf("unexpected multi-branch result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "incremental" {
    if !result.Relay || result.RelayMode != relayModeIncremental {
        t.Fatalf("expected multi-branch fast-forward sync to use incremental relay, got %+v", result)
    }

65 unmodified lines

if result.Pushed != 1 || result.Blocked != 0 {
        t.Fatalf("unexpected mapped incremental result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "incremental" {
    if !result.Relay || result.RelayMode != relayModeIncremental {
        t.Fatalf("expected mapped incremental sync to use incremental relay, got %+v", result)
    }

54 unmodified lines

if result.Pushed != 1 || result.Blocked != 0 {
        t.Fatalf("unexpected tag-create result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "incremental" {
    if !result.Relay || result.RelayMode != relayModeIncremental {
        t.Fatalf("expected tag-create sync to use incremental relay, got %+v", result)
    }

85 unmodified lines

targetURL := server.RepoURL("target.git")

result, err := Bootstrap(context.Background(), Config{
        Source:            Endpoint{URL: sourceURL},
        Target:            Endpoint{URL: targetURL},
        Source:             Endpoint{URL: sourceURL},
        Target:             Endpoint{URL: targetURL},
        TargetMaxPackBytes: 350_000,
    })
    if err != nil {
2 unmodified lines

if result.Pushed != 1 || result.Blocked != 0 {
        t.Fatalf("unexpected batched bootstrap result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" || !result.Batching {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch || !result.Batching {
        t.Fatalf("expected batched bootstrap relay result, got %+v", result)
    }
    if result.BatchCount < 2 {
47 unmodified lines

sourceURL := server.RepoURL("source.git")
    targetURL := server.RepoURL("target.git")
    cfg := Config{
        Source:            Endpoint{URL: sourceURL},
        Target:            Endpoint{URL: targetURL},
        Source:             Endpoint{URL: sourceURL},
        Target:             Endpoint{URL: targetURL},
        TargetMaxPackBytes: 350_000,
        ProtocolMode:      protocolModeAuto,
        ProtocolMode:       protocolModeAuto,
    }

stats := newStats(false)
35 unmodified lines

if result.Pushed != 1 || result.Blocked != 0 {
        t.Fatalf("unexpected batched resume result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" || !result.Batching {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch || !result.Batching {
        t.Fatalf("expected batched bootstrap resume relay result, got %+v", result)
    }
    if result.BatchCount >= len(checkpoints) {
74 unmodified lines

checkpoints, err := bstrap.PlanCheckpoints(context.Background(), bstrap.Params{
            SourceConn:    sourceConn,
            SourceService: sourceService,
            TargetMaxPack:  limit,
            TargetMaxPack: limit,
            Verbose:       cfg.Verbose,
        }, ref)
        if err != nil {
59 unmodified lines

targetURL := server.RepoURL("target.git")

result, err := Bootstrap(context.Background(), Config{
        Source:            Endpoint{URL: sourceURL},
        Target:            Endpoint{URL: targetURL},
        IncludeTags:       true,
        Source:             Endpoint{URL: sourceURL},
        Target:             Endpoint{URL: targetURL},
        IncludeTags:        true,
        TargetMaxPackBytes: 350_000,
    })
    if err != nil {
2 unmodified lines

if result.Pushed != 2 || result.Blocked != 0 {
        t.Fatalf("unexpected batched bootstrap with tags result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" || !result.Batching {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch || !result.Batching {
        t.Fatalf("expected batched bootstrap with tags relay result, got %+v", result)
    }

52 unmodified lines

func runGit(t *testing.T, dir string, args ...string) string {
    t.Helper()

cmd := exec.Command("git", args...)
    cmd := exec.CommandContext(t.Context(), "git", args...)
    cmd.Dir = dir
    cmd.Env = append(os.Environ(),
        "GIT_TERMINAL_PROMPT=0",
28 unmodified lines

func assertGitRefAbsent(t *testing.T, repoPath string, ref plumbing.ReferenceName) {
    t.Helper()
    cmd := exec.Command("git", "show-ref", "--verify", "--quiet", ref.String())
    cmd := exec.CommandContext(t.Context(), "git", "show-ref", "--verify", "--quiet", ref.String())
    cmd.Dir = repoPath
    cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
    if err := cmd.Run(); err == nil {

Minternal/syncer/git_http_backend_test.go+19/-19

14 unmodified lines

15
16
17
18
19
20
21
22
23
24
7 unmodified lines

32
33
34
31
32
33
34
35
36
37
37
38
39
40
41
42
43
44
45
23 unmodified lines

69
70
71
67
72
73
74
75
39 unmodified lines

115
116
117
113
118
119
120
121
286 unmodified lines

408
409
410
406
411
412
413
414
5 unmodified lines

420
421
422
418
423
424
425
426
119 unmodified lines

546
547
548
544
549
550
551
552
3 unmodified lines

556
557
558
554
559
560
561
562
150 unmodified lines

713
714
715
711
716
717
718
719
24 unmodified lines

744
745
746
742
743
744
747
748
749
750
751
752
753
754
750
755
756
757
758
38 unmodified lines

797
798
799
795
796
797
800
801
802
803
804
805
806
807
803
808
809
810
811
54 unmodified lines

866
867
868
864
865
866
869
870
871
872
873
874
15 unmodified lines

890
891
892
888
893
894
895
896
16 unmodified lines

913
914
915
911
912
913
916
917
918
919
920
921
44 unmodified lines

966
967
968
964
969
970
971
972
34 unmodified lines

1007
1008
1009
1005
1006
1007
1008
1010
1011
1012
1013
1014
1015
1016
1017
1018
1014
1019
1020
1021
1022
113 unmodified lines

1136
1137
1138
1134
1139
1140
1141
1142
138 unmodified lines

1281
1282
1283
1279
1284
1285
1286
1287
19 unmodified lines

1307
1308
1309
1305
1310
1311
1312
1313
230 unmodified lines

1544
1545
1546
1542
1547
1548
1549
1550
178 unmodified lines

1729
1730
1731
1727
1728
1729
1730
1732
1733
1734
1735
1736
1737
1738
39 unmodified lines

1778
1779
1780
1776
1781
1782
1783
1784
129 unmodified lines

1914
1915
1916
1912
1917
1918
1919
1920
147 unmodified lines

2068
2069
2070
2066
2071
2072
2073
2074
186 unmodified lines

2261
2262
2263
2259
2264
2265
2266
2267
2268
2269
42 unmodified lines

2312
2313
2314
2308
2315
2316
2317
2318
56 unmodified lines

2375
2376
2377
2371
2378
2379
2380
2381
337 unmodified lines

2719
2720
2721
2722
2723
2724
2725
2726
1 unmodified line

2728
2729
2730
2722
2723
2731
2732
2733
2734
2735

14 unmodified lines

"testing"
    "time"

"github.com/entirehq/git-sync/internal/auth"
    "github.com/entirehq/git-sync/internal/gitproto"
    "github.com/entirehq/git-sync/internal/planner"
    "github.com/entirehq/git-sync/internal/syncertest"
    billy "github.com/go-git/go-billy/v6"
    git "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing"
7 unmodified lines

"github.com/go-git/go-git/v6/plumbing/transport"
    transporthttp "github.com/go-git/go-git/v6/plumbing/transport/http"
    "github.com/go-git/go-git/v6/storage/memory"
    "github.com/entirehq/git-sync/internal/auth"
    "github.com/entirehq/git-sync/internal/gitproto"
    "github.com/entirehq/git-sync/internal/planner"
    "github.com/entirehq/git-sync/internal/syncertest"
)

const testBranch = "master"
const (
    testBranch                   = "master"
    reasonEmptyTargetManagedRefs = "empty-target-managed-refs"
    relayModeIncremental         = "incremental"
    relayModeBootstrapBatch      = "bootstrap-batch"
)

func TestRun_IntegrationInitialSyncToEmptyTarget(t *testing.T) {
    sourceRepo, sourceFS := newSourceRepo(t)
23 unmodified lines

if !result.Relay {
        t.Fatalf("expected sync to auto-switch to relay bootstrap on empty target")
    }
    if result.RelayReason != "empty-target-managed-refs" {
    if result.RelayReason != reasonEmptyTargetManagedRefs {
        t.Fatalf("expected bootstrap relay reason, got %+v", result)
    }

39 unmodified lines

if result.Pushed != 1 || result.Blocked != 0 {
        t.Fatalf("unexpected result: %+v", result)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" || !result.Batching {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch || !result.Batching {
        t.Fatalf("expected batched relay fallback result, got %+v", result)
    }
    if result.BatchCount < 2 {
286 unmodified lines

sourceServer := newSmartHTTPRepoServerV2(t, sourceRepo)
    targetServer := newSmartHTTPRepoServer(t, targetRepo)
    targetServer.receivePackRaw = func(w http.ResponseWriter, r *http.Request) bool {
    targetServer.receivePackRaw = func(_ http.ResponseWriter, r *http.Request) bool {
        defer r.Body.Close()
        buf := make([]byte, 32)
        n, err := r.Body.Read(buf)
5 unmodified lines

default:
        }
        <-release
        _, _ = r.Body.Read(buf)
        _, _ = r.Body.Read(buf) //nolint:errcheck // drain after cancellation; error is expected
        return true
    }
    defer sourceServer.Close()
119 unmodified lines

if !result.DryRun || !result.BootstrapSuggested {
        t.Fatalf("expected bootstrap suggestion, got %+v", result)
    }
    if result.RelayReason != "empty-target-managed-refs" {
    if result.RelayReason != reasonEmptyTargetManagedRefs {
        t.Fatalf("expected bootstrap suggestion reason, got %+v", result)
    }
    if result.Relay {
3 unmodified lines

func TestProbe_ContextCanceled(t *testing.T) {
    started := make(chan struct{}, 1)
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
        started <- struct{}{}
        <-r.Context().Done()
    }))
150 unmodified lines

assertHeadsMatch(t, sourceRepo, targetRepo, testBranch)

// Stale temp ref should have been cleaned up.
    if _, err := targetRepo.Reference(tempRef, true); err != plumbing.ErrReferenceNotFound {
    if _, err := targetRepo.Reference(tempRef, true); !errors.Is(err, plumbing.ErrReferenceNotFound) {
        t.Fatalf("expected stale temp ref to be deleted, got err=%v", err)
    }
}
24 unmodified lines

defer targetServer.Close()

result, err := Bootstrap(context.Background(), Config{
        Source:            Endpoint{URL: sourceServer.RepoURL()},
        Target:            Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:      protocolModeAuto,
        Source:             Endpoint{URL: sourceServer.RepoURL()},
        Target:             Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:       protocolModeAuto,
        TargetMaxPackBytes: 350_000,
    })
    if err != nil {
        t.Fatalf("batched bootstrap final-tip cutover failed: %v", err)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch {
        t.Fatalf("expected batched bootstrap result, got %+v", result)
    }
    targetHead, err := targetRepo.Reference(plumbing.NewBranchReferenceName(testBranch), true)
38 unmodified lines

defer targetServer.Close()

result, err := Bootstrap(context.Background(), Config{
        Source:            Endpoint{URL: sourceServer.RepoURL()},
        Target:            Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:      protocolModeAuto,
        Source:             Endpoint{URL: sourceServer.RepoURL()},
        Target:             Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:       protocolModeAuto,
        TargetMaxPackBytes: 350_000,
    })
    if err != nil {
        t.Fatalf("batched bootstrap cleanup rerun failed: %v", err)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch {
        t.Fatalf("expected batched bootstrap result, got %+v", result)
    }
    targetHead, err := targetRepo.Reference(targetRef, true)
54 unmodified lines

}

cfg := Config{
        Source:            Endpoint{URL: sourceServer.RepoURL()},
        Target:            Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:      protocolModeAuto,
        Source:             Endpoint{URL: sourceServer.RepoURL()},
        Target:             Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:       protocolModeAuto,
        TargetMaxPackBytes: 350_000,
    }

15 unmodified lines

if err != nil {
        t.Fatalf("bootstrap retry after delete failure failed: %v", err)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch {
        t.Fatalf("expected batched bootstrap result, got %+v", result)
    }
    if _, err := targetRepo.Reference(tempRef, true); err == nil {
16 unmodified lines

defer targetServer.Close()

44 unmodified lines

if err != nil {
        t.Fatalf("bootstrap retry after checkpoint pack failure failed: %v", err)
    }
    if !result.Relay || result.RelayMode != "bootstrap-batch" {
    if !result.Relay || result.RelayMode != relayModeBootstrapBatch {
        t.Fatalf("expected batched bootstrap result, got %+v", result)
    }
    targetHead, err := targetRepo.Reference(targetRef, true)
34 unmodified lines

defer targetServer.Close()

result, err := Bootstrap(context.Background(), Config{
        Source:            Endpoint{URL: sourceServer.RepoURL()},
        Target:            Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:      protocolModeAuto,
        IncludeTags:       true,
        Source:             Endpoint{URL: sourceServer.RepoURL()},
        Target:             Endpoint{URL: targetServer.RepoURL()},
        ProtocolMode:       protocolModeAuto,
        IncludeTags:        true,
        TargetMaxPackBytes: 350_000,
    })
    if err != nil {
        t.Fatalf("batched bootstrap with lightweight tag failed: %v", err)
    }
    if result.Pushed != 2 || !result.Batching || result.RelayMode != "bootstrap-batch" {
    if result.Pushed != 2 || !result.Batching || result.RelayMode != relayModeBootstrapBatch {
        t.Fatalf("unexpected result: %+v", result)
    }
    tagRef, err := targetRepo.Reference(plumbing.NewTagReferenceName("v1"), true)
113 unmodified lines

if !strings.Contains(err.Error(), "max-pack-bytes") {
        t.Fatalf("expected max-pack-bytes error, got %v", err)
    }
    if _, err := targetRepo.Reference(plumbing.NewBranchReferenceName(testBranch), true); err != plumbing.ErrReferenceNotFound {
    if _, err := targetRepo.Reference(plumbing.NewBranchReferenceName(testBranch), true); !errors.Is(err, plumbing.ErrReferenceNotFound) {
        t.Fatalf("expected target branch to remain absent, got %v", err)
    }
}
138 unmodified lines

if targetServer.Count(serviceReceivePack, metricPack) != 0 {
        t.Fatalf("expected no receive-pack POSTs during dry-run, got %d", targetServer.Count(serviceReceivePack, metricPack))
    }
    if _, err := targetRepo.Reference(plumbing.NewBranchReferenceName(testBranch), true); err != plumbing.ErrReferenceNotFound {
    if _, err := targetRepo.Reference(plumbing.NewBranchReferenceName(testBranch), true); !errors.Is(err, plumbing.ErrReferenceNotFound) {
        t.Fatalf("expected target branch to remain absent, got %v", err)
    }
}
19 unmodified lines

t.Cleanup(func() {
        auth.GitCredentialFillCommand = originalFill
    })
    auth.GitCredentialFillCommand = func(ctx context.Context, input string) ([]byte, error) {
    auth.GitCredentialFillCommand = func(_ context.Context, input string) ([]byte, error) {
        if !strings.Contains(input, "protocol=http\n") {
            t.Fatalf("expected protocol in credential input, got %q", input)
        }
230 unmodified lines

if _, err := targetRepo.Reference(plumbing.NewTagReferenceName("v1"), true); err != nil {
        t.Fatalf("expected v1 tag on target: %v", err)
    }
    if _, err := targetRepo.Reference(plumbing.NewTagReferenceName("stale"), true); err != plumbing.ErrReferenceNotFound {
    if _, err := targetRepo.Reference(plumbing.NewTagReferenceName("stale"), true); !errors.Is(err, plumbing.ErrReferenceNotFound) {
        t.Fatalf("expected stale tag to be pruned, got %v", err)
    }

178 unmodified lines

defer targetServer.Close()

result, err := Run(context.Background(), Config{
        Source:            Endpoint{URL: sourceServer.RepoURL()},
        Target:            Endpoint{URL: targetServer.RepoURL()},
        Mode:              modeReplicate,
        ProtocolMode:      protocolModeAuto,
        Source:             Endpoint{URL: sourceServer.RepoURL()},
        Target:             Endpoint{URL: targetServer.RepoURL()},
        Mode:               modeReplicate,
        ProtocolMode:       protocolModeAuto,
        TargetMaxPackBytes: 350_000, // force > 1 batch for the generated pack
    })
    if err != nil {
39 unmodified lines

if !result.Relay {
        t.Fatalf("expected bootstrap relay to run, got %+v", result)
    }
    if result.RelayReason != "empty-target-managed-refs" {
    if result.RelayReason != reasonEmptyTargetManagedRefs {
        t.Fatalf("expected empty-target bootstrap reason, got %+v", result)
    }
    if result.Pushed != 1 {
129 unmodified lines

t.Fatalf("expected exactly one pushed ref alongside the delete, got %+v", result)
    }

if _, err := targetRepo.Reference(orphanRef, true); err != plumbing.ErrReferenceNotFound {
    if _, err := targetRepo.Reference(orphanRef, true); !errors.Is(err, plumbing.ErrReferenceNotFound) {
        t.Fatalf("expected orphan branch to be pruned, got err=%v", err)
    }
    assertHeadsMatch(t, sourceRepo, targetRepo, testBranch)
147 unmodified lines

}
}

func assertHeadsMatch(t *testing.T, sourceRepo, targetRepo *git.Repository, branch string) {
func assertHeadsMatch(t *testing.T, sourceRepo, targetRepo *git.Repository, branch string) { //nolint:unparam // kept as param for test readability
    syncertest.AssertBranchHeadsMatch(t, sourceRepo, targetRepo, branch)
}

186 unmodified lines

caps.Delete(capability.Capability("no-thin"))
            }
            if s.receivePackNoThin {
                _ = caps.Set(capability.Capability("no-thin"))
                if err := caps.Set(capability.Capability("no-thin")); err != nil {
                    s.tb.Fatalf("set no-thin capability: %v", err)
                }
            }
        })
        if err != nil {
42 unmodified lines

return buf.Bytes(), nil
}

func (s *smartHTTPRepoServer) handleInfoRefsV2(w http.ResponseWriter, r *http.Request) {
func (s *smartHTTPRepoServer) handleInfoRefsV2(w http.ResponseWriter, _ *http.Request) {
    var buf bytes.Buffer
    lines := []string{
        "version 2\n",
56 unmodified lines

s.recordMetric(serviceUploadPack, metricPack, int64(len(body)), int64(buf.Len()), wantCount, haveCount)
}

func (s *smartHTTPRepoServer) handleUploadPackV2(w http.ResponseWriter, r *http.Request, body []byte) {
func (s *smartHTTPRepoServer) handleUploadPackV2(w http.ResponseWriter, _ *http.Request, body []byte) {
    req, err := decodeV2TestCommandRequest(body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
337 unmodified lines

if inArgs {
                req.Args = append(req.Args, line)
            }
        case gitproto.PacketResponseEnd:
            return req, nil
        default:
            return req, fmt.Errorf("unexpected packet type %v", kind)
        }
1 unmodified line

}

func TestMain(m *testing.M) {
    originalHTTP, _ := transport.Get("http")
    originalHTTPS, _ := transport.Get("https")
    originalHTTP, _ := transport.Get("http")   //nolint:errcheck // best-effort save; nil is acceptable
    originalHTTPS, _ := transport.Get("https") //nolint:errcheck // best-effort save; nil is acceptable

customHTTP := transporthttp.NewTransport(&transporthttp.TransportOptions{Client: &http.Client{}})
    transport.Register("http", customHTTP)

Minternal/syncer/integration_test.go+58/-49

38 unmodified lines

39
40
41
42
43
44
42
43
44
45
46
47
48
49
46
47
48
49
50
51
52

38 unmodified lines

defer server.Close()

result, err := Bootstrap(context.Background(), Config{
        Source:        Endpoint{URL: "https://github.com/torvalds/linux.git"},
        Target:        Endpoint{URL: server.RepoURL("target.git")},
        Branches:      []string{"master"},
        Source:             Endpoint{URL: "https://github.com/torvalds/linux.git"},
        Target:             Endpoint{URL: server.RepoURL("target.git")},
        Branches:           []string{"master"},
        TargetMaxPackBytes: batchMaxPackBytes,
        ProtocolMode:  protocolModeAuto,
        ShowStats:     true,
        MeasureMemory: true,
        Verbose:       true,
        ProtocolMode:       protocolModeAuto,
        ShowStats:          true,
        MeasureMemory:      true,
        Verbose:            true,
    })
    if err != nil {
        t.Fatalf("live linux bootstrap failed: %v\nbackend-stderr:\n%s", err, server.Stderr())

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

1
2
3
4
5
6
7
62 unmodified lines

70
71
72
72
73
74
75
4 unmodified lines

80
81
82
83
83
84
85
86
20 unmodified lines

107
108
109
110
111
112
113
1 unmodified line

115
116
117
117
118
119
120
121
2 unmodified lines

124
125
126
126
127
128
129
130
131

package syncer

import (
    "fmt"
    "io"
    "net/http"
    "strings"
62 unmodified lines

return out
}

// countingRoundTripper wraps an HTTP transport to record transfer stats.
type countingRoundTripper struct {
    base  http.RoundTripper
4 unmodified lines

func (rt *countingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    res, err := rt.base.RoundTrip(req)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("round trip: %w", err)
    }

serviceName := req.Header.Get(gitproto.StatsPhaseHeader)
20 unmodified lines

type countingReadCloser struct {
    io.ReadCloser

n       int64
    onClose func(int64)
}
1 unmodified line

func (c *countingReadCloser) Read(p []byte) (int, error) {
    n, err := c.ReadCloser.Read(p)
    c.n += int64(n)
    return n, err
    return n, err //nolint:wrapcheck // Read must preserve io.EOF for io.Reader contract
}

func (c *countingReadCloser) Close() error {
2 unmodified lines

c.onClose(c.n)
        c.onClose = nil
    }
    return err
    if err != nil {
        return fmt.Errorf("close: %w", err)
    }
    return nil
}

Minternal/syncer/stats.go+8/-4

101 unmodified lines

102
103
104
105
105
106
107
108
109
110
111
112
37 unmodified lines

150
151
152
149
153
154
155
156
14 unmodified lines

171
172
173
170
171
172
174
175
176
177
178
179
43 unmodified lines

223
224
225
222
226
227
228
229
230
231
232
233
234
235
236
237
238
231
232
233
239
240
241
242
243
244
2 unmodified lines

247
248
249
242
250
251
252
253
35 unmodified lines

289
290
291
284
292
293
294
295
3 unmodified lines

299
300
301
294
302
303
304
305
51 unmodified lines

357
358
359
352
360
361
362
363
4 unmodified lines

368
369
370
363
371
372
373
366
374
375
376
377
75 unmodified lines

453
454
455
448
456
457
458
451
459
460
461
462
1 unmodified line

464
465
466
459
467
468
469
470
12 unmodified lines

483
484
485
478
486
487
488
489
490
491
484
492
493
494
495
46 unmodified lines

542
543
544
545
546
547
548
549
4 unmodified lines

554
555
556
547
557
558
559
550
560
561
562
563
5 unmodified lines

569
570
571
562
572
573
574
575
11 unmodified lines

587
588
589
580
590
591
592
593
45 unmodified lines

639
640
641
642
643
644
645
646
30 unmodified lines

677
678
679
668
680
681
682
671
683
684
685
674
686
687
688
689
3 unmodified lines

693
694
695
684
696
697
698
687
699
700
701
702
5 unmodified lines

708
709
710
699
711
712
713
714
6 unmodified lines

721
722
723
712
724
725
726
727
7 unmodified lines

735
736
737
726
738
739
740
741
730
742
743
744
745
746
747
736
748
749
750
751
20 unmodified lines

772
773
774
763
775
776
777
778
9 unmodified lines

788
789
790
779
791
792
793
794
2 unmodified lines

797
798
799
800
801
802
803
804
805
806
2 unmodified lines

809
810
811
796
812
813
814
815
800
816
817
818
819
820
821
822
1 unmodified line

824
825
826
808
827
828
829
830
831
832
833
834
835
836
837
838
839
840
818
841
842
843
821
844
845
846
847
73 unmodified lines

921
922
923
901
924
925
926
927
905
928
929
930
908
909
931
932
933
934
935

101 unmodified lines

Name string `json:"name"`
        Hash string `json:"hash"`
    }
    return json.Marshal(ri{Name: r.Name, Hash: r.Hash.String()})
    b, err := json.Marshal(ri{Name: r.Name, Hash: r.Hash.String()})
    if err != nil {
        return nil, fmt.Errorf("marshal JSON: %w", err)
    }
    return b, nil
}

// Result holds the outcome of a sync or bootstrap operation.
37 unmodified lines

lines = append(lines, "hint: target refs are absent; bootstrap can seed them without local object storage")
    }
    if r.Batching && len(r.TempRefs) > 0 {
        lines = append(lines, fmt.Sprintf("batching: temp-refs=%s", strings.Join(r.TempRefs, ",")))
        lines = append(lines, "batching: temp-refs="+strings.Join(r.TempRefs, ","))
    }
    return lines
}
14 unmodified lines

func (r ProbeResult) Lines() []string {
    lines := []string{
        fmt.Sprintf("source: %s", r.SourceURL),
        fmt.Sprintf("requested-protocol: %s", r.RequestedMode),
        fmt.Sprintf("negotiated-protocol: %s", r.Protocol),
        "source: " + r.SourceURL,
        "requested-protocol: " + r.RequestedMode,
        "negotiated-protocol: " + r.Protocol,
    }
    if len(r.RefPrefixes) > 0 {
        lines = append(lines, "ref-prefixes: "+strings.Join(r.RefPrefixes, ", "))
43 unmodified lines

for _, h := range r.Haves {
        haves = append(haves, h.String())
    }
    return json.Marshal(fr{
    b, err := json.Marshal(fr{
        SourceURL: r.SourceURL, RequestedMode: r.RequestedMode,
        Protocol: r.Protocol, Wants: r.Wants, Haves: haves,
        FetchedObjects: r.FetchedObjects, Stats: r.Stats, Measurement: r.Measurement,
    })
    if err != nil {
        return nil, fmt.Errorf("marshal JSON: %w", err)
    }
    return b, nil
}

func (r FetchResult) Lines() []string {
    lines := []string{
        fmt.Sprintf("source: %s", r.SourceURL),
        fmt.Sprintf("requested-protocol: %s", r.RequestedMode),
        fmt.Sprintf("negotiated-protocol: %s", r.Protocol),
        "source: " + r.SourceURL,
        "requested-protocol: " + r.RequestedMode,
        "negotiated-protocol: " + r.Protocol,
        fmt.Sprintf("wants: %d", len(r.Wants)),
        fmt.Sprintf("haves: %d", len(r.Haves)),
        fmt.Sprintf("fetched-objects: %d", r.FetchedObjects),
2 unmodified lines

lines = append(lines, fmt.Sprintf("want: %s %s", w.Hash.String(), w.Name))
    }
    for _, h := range r.Haves {
        lines = append(lines, fmt.Sprintf("have: %s", h.String()))
        lines = append(lines, "have: "+h.String())
    }
    lines = append(lines, statsLines(r.Stats)...)
    lines = append(lines, measurementLine(r.Measurement)...)
35 unmodified lines

func newConn(raw Endpoint, label string, stats *statsCollector, httpClient *http.Client) (*gitproto.Conn, error) {
    ep, err := transport.NewEndpoint(raw.URL)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("parse endpoint: %w", err)
    }
    authEp := auth.Endpoint{
        Username:      raw.Username,
3 unmodified lines

}
    authMethod, err := auth.Resolve(authEp, ep)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("resolve auth: %w", err)
    }
    client := instrumentHTTPClient(httpClient, raw.SkipTLSVerify, label, stats)
    return gitproto.NewConnWithHTTPClient(ep, label, authMethod, client), nil
51 unmodified lines

func newSession(ctx context.Context, cfg Config, needTarget bool) (*syncSession, error) {
    mode, err := validation.NormalizeProtocolMode(cfg.ProtocolMode)
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("normalize protocol mode: %w", err)
    }
    cfg.ProtocolMode = mode
    switch cfg.Mode {
4 unmodified lines

return nil, fmt.Errorf("unsupported operation mode %q", cfg.Mode)
    }
    if _, err := validation.ValidateMappings(cfg.Mappings); err != nil {
        return nil, err
        return nil, fmt.Errorf("validate mappings: %w", err)
    }
    if cfg.Mode == modeReplicate && cfg.Force {
        return nil, fmt.Errorf("replicate does not support --force; use sync instead")
        return nil, errors.New("replicate does not support --force; use sync instead")
    }

s := &syncSession{
75 unmodified lines

desiredRefs, managedTargets, err := planner.BuildDesiredRefs(sourceRefMap, planConfig(s.cfg))
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("build desired refs: %w", err)
    }
    if len(desiredRefs) == 0 {
        return Result{}, fmt.Errorf("no source refs matched")
        return Result{}, errors.New("no source refs matched")
    }

// Check for bootstrap opportunity (before allocating in-memory repo)
1 unmodified line

if s.cfg.DryRun {
            plans, err := planner.BuildBootstrapPlans(desiredRefs, targetRefMap)
            if err != nil {
                return Result{}, err
                return Result{}, fmt.Errorf("build bootstrap plans: %w", err)
            }
            return Result{
                Plans: plans, DryRun: true, RelayReason: reason,
12 unmodified lines

gpDesired := convert.DesiredRefs(desiredRefs)
    if err := sourceService.FetchToStore(ctx, repo.Storer, s.sourceConn, gpDesired, targetRefMap); err != nil {
        if !errors.Is(err, git.NoErrAlreadyUpToDate) {
            return Result{}, err
            return Result{}, fmt.Errorf("fetch to store: %w", err)
        }
    }

plans, err := planner.BuildPlans(repo.Storer, desiredRefs, targetRefMap, managedTargets, planConfig(s.cfg))
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("build plans: %w", err)
    }

result := Result{
46 unmodified lines

result.Pushed++
        case ActionDelete:
            result.Deleted++
        case ActionSkip, ActionBlock:
            // not applicable in this context
        }
    }
    result.Stats = stats.snapshot()
4 unmodified lines

func (s *syncSession) runReplicate(ctx context.Context) (Result, error) {
    desiredRefs, managedTargets, err := planner.BuildDesiredRefs(s.sourceRefMap, planConfig(s.cfg))
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("build desired refs: %w", err)
    }
    if len(desiredRefs) == 0 {
        return Result{}, fmt.Errorf("no source refs matched")
        return Result{}, errors.New("no source refs matched")
    }

if ok, reason := planner.SupportsReplicateRelay(s.target.policy); !ok {
5 unmodified lines

if s.cfg.DryRun {
            plans, err := planner.BuildBootstrapPlans(desiredRefs, s.target.refMap)
            if err != nil {
                return Result{}, err
                return Result{}, fmt.Errorf("build bootstrap plans: %w", err)
            }
            return Result{
                Plans:              plans,
11 unmodified lines

plans, err := planner.BuildReplicationPlans(desiredRefs, s.target.refMap, managedTargets, planConfig(s.cfg))
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("build replication plans: %w", err)
    }

result := Result{
45 unmodified lines

result.Pushed++
        case ActionDelete:
            result.Deleted++
        case ActionSkip, ActionBlock:
            // not applicable in this context
        }
    }
    result.Stats = s.stats.snapshot()
30 unmodified lines

// Bootstrap seeds an empty target with relay behavior.
func Bootstrap(ctx context.Context, cfg Config) (Result, error) {
    if cfg.Force {
        return Result{}, fmt.Errorf("bootstrap does not support --force")
        return Result{}, errors.New("bootstrap does not support --force")
    }
    if cfg.Prune {
        return Result{}, fmt.Errorf("bootstrap does not support --prune")
        return Result{}, errors.New("bootstrap does not support --prune")
    }
    if cfg.DryRun {
        return Result{}, fmt.Errorf("bootstrap does not support dry-run; use plan or sync")
        return Result{}, errors.New("bootstrap does not support dry-run; use plan or sync")
    }

s, err := newSession(ctx, cfg, true)
3 unmodified lines

desiredRefs, _, err := planner.BuildDesiredRefs(s.sourceRefMap, planConfig(cfg))
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("build desired refs: %w", err)
    }
    if len(desiredRefs) == 0 {
        return Result{}, fmt.Errorf("no source refs matched")
        return Result{}, errors.New("no source refs matched")
    }

_, reason := planner.CanBootstrapRelay(cfg.Force, cfg.Prune, desiredRefs, s.target.refMap)
5 unmodified lines

// Probe inspects source and optionally target remotes.
func Probe(ctx context.Context, cfg Config) (ProbeResult, error) {
    if cfg.Source.URL == "" {
        return ProbeResult{}, fmt.Errorf("source repository URL is required")
        return ProbeResult{}, errors.New("source repository URL is required")
    }

s, err := newSession(ctx, cfg, cfg.Target.URL != "")
6 unmodified lines

// Fetch exercises source-side fetch negotiation.
func Fetch(ctx context.Context, cfg Config, haveRefs []string, haveHashes []plumbing.Hash) (FetchResult, error) {
    if cfg.Source.URL == "" {
        return FetchResult{}, fmt.Errorf("source repository URL is required")
        return FetchResult{}, errors.New("source repository URL is required")
    }

s, err := newSession(ctx, cfg, false)
7 unmodified lines

}
    desiredRefs, err := s.buildDesiredRefs()
    if err != nil {
        return FetchResult{}, err
        return FetchResult{}, fmt.Errorf("build desired refs: %w", err)
    }
    targetRefMap, err := s.buildHaveRefMap(haveRefs, haveHashes)
    if err != nil {
        return FetchResult{}, err
        return FetchResult{}, fmt.Errorf("build have ref map: %w", err)
    }

gpDesired := convert.DesiredRefs(desiredRefs)
    if err := s.sourceService.FetchToStore(ctx, repo.Storer, s.sourceConn, gpDesired, targetRefMap); err != nil {
        if !errors.Is(err, git.NoErrAlreadyUpToDate) {
            return FetchResult{}, err
            return FetchResult{}, fmt.Errorf("fetch to store: %w", err)
        }
    }

20 unmodified lines

Verbose: s.cfg.Verbose, Logger: s.logger,
    }, relayReason)
    if err != nil {
        return Result{}, err
        return Result{}, fmt.Errorf("bootstrap execute: %w", err)
    }
    return Result{
        Plans: bResult.Plans, Pushed: bResult.Pushed, OperationMode: s.cfg.Mode,
9 unmodified lines

desiredRefs map[plumbing.ReferenceName]planner.DesiredRef,
    pushPlans []planner.BranchPlan,
) (incremental.Result, error) {
    return incremental.Execute(ctx, incremental.Params{
    incResult, incErr := incremental.Execute(ctx, incremental.Params{
        SourceConn: s.sourceConn, SourceService: s.sourceService, TargetPusher: s.target.pusher,
        DesiredRefs: desiredRefs, TargetRefs: s.target.refMap,
        PushPlans: pushPlans, MaxPackBytes: s.cfg.MaxPackBytes,
2 unmodified lines

},
        CanTagRelay: planner.CanFullTagCreateRelay,
    }, planConfig(s.cfg))
    if incErr != nil {
        return incResult, fmt.Errorf("incremental execute: %w", incErr)
    }
    return incResult, nil
}

func (s *syncSession) executeMaterialized(
2 unmodified lines

desiredRefs map[plumbing.ReferenceName]planner.DesiredRef,
    pushPlans []planner.BranchPlan,
) error {
    return materialized.Execute(ctx, materialized.Params{
    if err := materialized.Execute(ctx, materialized.Params{
        Store: store, SourceConn: s.sourceConn, SourceService: s.sourceService, TargetPusher: s.target.pusher,
        DesiredRefs: desiredRefs, TargetRefs: s.target.refMap,
        PushPlans: pushPlans, MaxObjects: s.cfg.MaterializedMaxObjects,
    })
    }); err != nil {
        return fmt.Errorf("materialized execute: %w", err)
    }
    return nil
}

func (s *syncSession) executeReplicate(
1 unmodified line

desiredRefs map[plumbing.ReferenceName]planner.DesiredRef,
    pushPlans []planner.BranchPlan,
) (repstrat.Result, error) {
    return repstrat.Execute(ctx, repstrat.Params{
    repResult, repErr := repstrat.Execute(ctx, repstrat.Params{
        SourceConn: s.sourceConn, SourceService: s.sourceService, TargetPusher: s.target.pusher,
        DesiredRefs: desiredRefs, TargetRefs: s.target.refMap,
        PushPlans: pushPlans, MaxPackBytes: s.cfg.MaxPackBytes,
    })
    if repErr != nil {
        return repResult, fmt.Errorf("replicate execute: %w", repErr)
    }
    return repResult, nil
}

func (s *syncSession) buildDesiredRefs() (map[plumbing.ReferenceName]planner.DesiredRef, error) {
    desiredRefs, _, err := planner.BuildDesiredRefs(s.sourceRefMap, planConfig(s.cfg))
    if err != nil {
        return nil, err
        return nil, fmt.Errorf("build desired refs: %w", err)
    }
    if len(desiredRefs) == 0 {
        return nil, fmt.Errorf("no source refs matched")
        return nil, errors.New("no source refs matched")
    }
    return desiredRefs, nil
}
73 unmodified lines

func countObjects(store storer.EncodedObjectStorer) (int, error) {
    iter, err := store.IterEncodedObjects(plumbing.AnyObject)
    if err != nil {
        return 0, err
        return 0, fmt.Errorf("iterate encoded objects: %w", err)
    }
    defer iter.Close()
    count := 0
    err = iter.ForEach(func(_ plumbing.EncodedObject) error {
    if err := iter.ForEach(func(_ plumbing.EncodedObject) error {
        count++
        return nil
    })
    return count, err
    }); err != nil {
        return 0, fmt.Errorf("count encoded objects: %w", err)
    }
    return count, nil
}

Minternal/syncer/syncer.go+70/-45

35 unmodified lines

36
37
38
39
39
40
41
42
27 unmodified lines

70
71
72
73
73
74
75
76
27 unmodified lines

104
105
106
107
107
108
109
110

35 unmodified lines

tb.Fatalf("open worktree: %v", err)
    }

for i := 0; i < count; i++ {
    for i := range count {
        content := strings.Repeat(fmt.Sprintf("line %d %d\n", i, time.Now().UnixNano()), 24)
        file, err := fs.Create("tracked.txt")
        if err != nil {
27 unmodified lines

tb.Fatalf("open worktree: %v", err)
    }

for i := 0; i < count; i++ {
    for i := range count {
        content := fmt.Sprintf("bench line %d %d\n", i, time.Now().UnixNano())
        file, err := fs.Create("tracked.txt")
        if err != nil {
27 unmodified lines

tb.Fatalf("open worktree: %v", err)
    }

for i := 0; i < count; i++ {
    for i := range count {
        name := fmt.Sprintf("blob-%d.bin", i)
        file, err := fs.Create(name)
        if err != nil {

Minternal/syncertest/repo.go+3/-3

1
2
3

#!/bin/sh
#MISE description="Lint"
#MISE depends=["lint:go", "lint:gofmt", "lint:gomod", "lint:shellcheck"]

Amise-tasks/lint/_default+3

1
2
3
4
5
6
7
8
9
10
11
12
13

#!/bin/sh
#MISE description="Lint go"

golangci-lint version
golangci-lint config verify

# In CI, we rely on golangci/golangci-lint-action@v9 defined in .github/workflows/lint.yml
# so only run this locally:
if [ "$CI" = "true" ]; then
  echo "skipped because CI=true"
else
  golangci-lint run --timeout=30m --fix ./...
fi

Amise-tasks/lint/go+13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#!/bin/sh
#MISE description="Lint gofmt"

# List of non-formatted files:
files="$(gofmt -l -s .)"

# Is that list empty?  If not exit nonzero and say which files are unformatted:
if [ -n "$files" ]; then
  echo "These .go files need to be formatted:"
  echo ""
  echo "$files"
  echo ""
  echo "To fix: mise run fmt"
  exit 1
fi

Amise-tasks/lint/gofmt+15

1
2
3
4
5
6
7
8
9
10
11
12

#!/bin/sh
#MISE description="go mod tidy"

go mod tidy

# Check and fail if there are differences with committed files:
status="$(git status --porcelain=v1 go.mod go.sum 2>/dev/null)"
if [ "$status" != "" ]; then
  echo "go module files modified, please commit result of 'go mod tidy'!"
  echo "$status"
  exit 1
fi

Amise-tasks/lint/gomod+12

1
2
3
4

#!/bin/sh
#MISE description="Lint shell scripts with shellcheck"

find mise-tasks -type f -exec sh -c 'head -1 "$1" | grep -q "^#!/bin/sh\|^#!/bin/bash"' _ {} \; -print0 | xargs -0 shellcheck

Amise-tasks/lint/shellcheck+4

11 unmodified lines

12
13
14
15
16
17
18
19
20
21

11 unmodified lines

description = "Run tests"
run = "go test ./..."

[tasks."test:ci"]
description = "Run all tests with race detection"
run = "go test -race ./..."

[tasks."test:git-http-backend"]
description = "Run optional git-http-backend integration tests"
run = "GITSYNC_E2E_GIT_HTTP_BACKEND=1 go test ./internal/syncer -run 'TestRun_GitHTTPBackendSync|TestBootstrap_GitHTTPBackendSync' -v"

Mmise.toml+4

1 unmodified line

2
3
4
5
6
7
8
29 unmodified lines

38
39
40
40
41
42
43
44
9 unmodified lines

54
55
56
56
57
58
59
60
9 unmodified lines

70
71
72
72
73
74
75
76
57 unmodified lines

134
135
136
136
137
138
139
140
141
142
143
144
145
141
146
147
148
144
149
150
151
152
153
154
150
155
156
157
153
158
159
160
161
162
163
164
160
165
166
167
163
168
169
170
171
172
173
169
174
175
176
172
177
178
179
180
181
182
183
179
184
185
186
182
187
188
189
185
190
191
192
193

1 unmodified line

import (
    "context"
    "errors"
    "fmt"
    "net/http"

29 unmodified lines

}
    result, err := internalbridge.Probe(ctx, cfg)
    if err != nil {
        return ProbeResult{}, err
        return ProbeResult{}, fmt.Errorf("probe: %w", err)
    }
    return internalbridge.FromProbeResult(result), nil
}
9 unmodified lines

}
    result, err := internalbridge.Run(ctx, cfg)
    if err != nil {
        return PlanResult{}, err
        return PlanResult{}, fmt.Errorf("plan: %w", err)
    }
    return internalbridge.FromSyncResult(result), nil
}
9 unmodified lines

}
    result, err := internalbridge.Run(ctx, cfg)
    if err != nil {
        return SyncResult{}, err
        return SyncResult{}, fmt.Errorf("sync: %w", err)
    }
    return internalbridge.FromSyncResult(result), nil
}
57 unmodified lines

if c.auth == nil {
        return EndpointAuth{}, nil
    }
    return c.auth.AuthFor(ctx, endpoint, role)
    auth, err := c.auth.AuthFor(ctx, endpoint, role)
    if err != nil {
        return EndpointAuth{}, fmt.Errorf("resolve auth for %s: %w", role, err)
    }
    return auth, nil
}

func (r SyncRequest) Validate() error {
    if r.Source.URL == "" {
        return fmt.Errorf("source URL is required")
        return errors.New("source URL is required")
    }
    if r.Target.URL == "" {
        return fmt.Errorf("target URL is required")
        return errors.New("target URL is required")
    }
    if err := validateOperationMode(r.Policy.Mode); err != nil {
        return err
    }
    if _, err := validation.NormalizeProtocolMode(string(r.Policy.Protocol)); err != nil {
        return err
        return fmt.Errorf("normalize protocol: %w", err)
    }
    if _, err := validation.ValidateMappings(validationMappings(r.Scope.Mappings)); err != nil {
        return err
        return fmt.Errorf("validate mappings: %w", err)
    }
    return nil
}

func (r PlanRequest) Validate() error {
    if r.Source.URL == "" {
        return fmt.Errorf("source URL is required")
        return errors.New("source URL is required")
    }
    if r.Target.URL == "" {
        return fmt.Errorf("target URL is required")
        return errors.New("target URL is required")
    }
    if err := validateOperationMode(r.Policy.Mode); err != nil {
        return err
    }
    if _, err := validation.NormalizeProtocolMode(string(r.Policy.Protocol)); err != nil {
        return err
        return fmt.Errorf("normalize protocol: %w", err)
    }
    if _, err := validation.ValidateMappings(validationMappings(r.Scope.Mappings)); err != nil {
        return err
        return fmt.Errorf("validate mappings: %w", err)
    }
    return nil
}

func (r ProbeRequest) Validate() error {
    if r.Source.URL == "" {
        return fmt.Errorf("source URL is required")
        return errors.New("source URL is required")
    }
    if r.Target != nil && r.Target.URL == "" {
        return fmt.Errorf("target URL is required when target endpoint is provided")
        return errors.New("target URL is required when target endpoint is provided")
    }
    if _, err := validation.NormalizeProtocolMode(string(r.Protocol)); err != nil {
        return err
        return fmt.Errorf("normalize protocol: %w", err)
    }
    return nil
}

Mpkg/gitsync/client.go+20/-15

2 unmodified lines

3
4
5
6
7
8
9
11 unmodified lines

21
22
23
23
24
25
26
27

2 unmodified lines

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
11 unmodified lines

type errAuthProvider struct{}

func (errAuthProvider) AuthFor(_ context.Context, _ Endpoint, _ EndpointRole) (EndpointAuth, error) {
    return EndpointAuth{}, fmt.Errorf("boom")
    return EndpointAuth{}, errors.New("boom")
}

func TestValidateRequests(t *testing.T) {

Mpkg/gitsync/client_test.go+2/-1

15 unmodified lines

16
17
18
19
19
20
21
22
1 unmodified line

24
25
26
27
27
28
29
30
31
32

15 unmodified lines

},
    })

_, _ = client.Sync(context.Background(), gitsync.SyncRequest{
    if _, err := client.Sync(context.Background(), gitsync.SyncRequest{
        Source: gitsync.Endpoint{URL: "https://github.example/source/repo.git"},
        Target: gitsync.Endpoint{URL: "https://git.example/target/repo.git"},
        Scope:  gitsync.RefScope{Branches: []string{"main"}},
1 unmodified line

IncludeTags: true,
            Protocol:    gitsync.ProtocolAuto,
        },
    })
    }); err != nil {
        return // network error expected in example environment
    }

// Output:
}

Mpkg/gitsync/example_test.go+4/-2

83 unmodified lines

84
85
86
87
87
88
89
90
91
92
93
94
91
95
96
97
98
99
100
101
102

83 unmodified lines

}

func Probe(ctx context.Context, cfg Config) (syncer.ProbeResult, error) {
    return syncer.Probe(ctx, cfg.raw)
    result, err := syncer.Probe(ctx, cfg.raw)
    if err != nil {
        return syncer.ProbeResult{}, err //nolint:wrapcheck // pass-through layer, caller wraps with context
    }
    return result, nil
}

func Run(ctx context.Context, cfg Config) (syncer.Result, error) {
    return syncer.Run(ctx, cfg.raw)
    result, err := syncer.Run(ctx, cfg.raw)
    if err != nil {
        return syncer.Result{}, err //nolint:wrapcheck // pass-through layer, caller wraps with context
    }
    return result, nil
}

func ToSyncerEndpoint(endpoint Endpoint, auth EndpointAuth) syncer.Endpoint {

Mpkg/gitsync/internalbridge/config.go+10/-2

56 unmodified lines

57
58
59
60
60
61
62
63

56 unmodified lines

}

// AuthFor implements AuthProvider.
func (p StaticAuthProvider) AuthFor(_ context.Context, _ Endpoint, role EndpointRole) (EndpointAuth, error) {
func (p StaticAuthProvider) AuthFor(_ context.Context, _ Endpoint, role EndpointRole) (EndpointAuth, error) { //nolint:unparam // implements AuthProvider interface
    if role == TargetRole {
        return p.Target, nil
    }

Mpkg/gitsync/types.go+1/-1

1 unmodified line

2
3
4
5
6
7
8
79 unmodified lines

88
89
90
90
91
92
93
94
95
96
97
98
3 unmodified lines

102
103
104
100
105
106
107
108
109
110
111
112
1 unmodified line

114
115
116
108
117
118
119
120
121
122
123
124
2 unmodified lines

127
128
129
117
130
131
132
133
134
135
136
137
1 unmodified line

139
140
141
125
142
143
144
145
146
147
148
149
1 unmodified line

151
152
153
133
154
155
156
157
158
159
160
161
64 unmodified lines

226
227
228
204
205
206
207
208
209
210
211
212
229
230
231
232
233
234
235
236
237
238
214
215
239
240
241
242
243
18 unmodified lines

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

1 unmodified line

import (
    "context"
    "fmt"
    "net/http"

"github.com/go-git/go-git/v6/plumbing"
79 unmodified lines

if err != nil {
        return ProbeResult{}, err
    }
    return syncer.Probe(ctx, cfg)
    result, err := syncer.Probe(ctx, cfg)
    if err != nil {
        return ProbeResult{}, fmt.Errorf("probe: %w", err)
    }
    return result, nil
}

func (c *Client) Plan(ctx context.Context, req SyncRequest) (Result, error) {
3 unmodified lines

if err != nil {
        return Result{}, err
    }
    return syncer.Run(ctx, cfg)
    result, err := syncer.Run(ctx, cfg)
    if err != nil {
        return Result{}, fmt.Errorf("plan: %w", err)
    }
    return result, nil
}

func (c *Client) Sync(ctx context.Context, req SyncRequest) (Result, error) {
1 unmodified line

if err != nil {
        return Result{}, err
    }
    return syncer.Run(ctx, cfg)
    result, err := syncer.Run(ctx, cfg)
    if err != nil {
        return Result{}, fmt.Errorf("sync: %w", err)
    }
    return result, nil
}

func (c *Client) Replicate(ctx context.Context, req SyncRequest) (Result, error) {
2 unmodified lines

if err != nil {
        return Result{}, err
    }
    return syncer.Run(ctx, cfg)
    result, err := syncer.Run(ctx, cfg)
    if err != nil {
        return Result{}, fmt.Errorf("replicate: %w", err)
    }
    return result, nil
}

func (c *Client) Bootstrap(ctx context.Context, req BootstrapRequest) (Result, error) {
1 unmodified line

if err != nil {
        return Result{}, err
    }
    return syncer.Bootstrap(ctx, cfg)
    result, err := syncer.Bootstrap(ctx, cfg)
    if err != nil {
        return Result{}, fmt.Errorf("bootstrap: %w", err)
    }
    return result, nil
}

func (c *Client) Fetch(ctx context.Context, req FetchRequest) (FetchResult, error) {
1 unmodified line

if err != nil {
        return FetchResult{}, err
    }
    return syncer.Fetch(ctx, cfg, append([]string(nil), req.HaveRefs...), append([]plumbing.Hash(nil), req.HaveHashes...))
    result, err := syncer.Fetch(ctx, cfg, append([]string(nil), req.HaveRefs...), append([]plumbing.Hash(nil), req.HaveHashes...))
    if err != nil {
        return FetchResult{}, fmt.Errorf("fetch: %w", err)
    }
    return result, nil
}

func (c *Client) buildProbeConfig(ctx context.Context, req ProbeRequest) (syncer.Config, error) {
64 unmodified lines

return syncer.Config{}, err
    }
    return syncer.Config{
        Source:            source,
        Target:            target,
        HTTPClient:        c.httpClient,
        Branches:          append([]string(nil), req.Scope.Branches...),
        Mappings:          validationMappings(req.Scope.Mappings),
        IncludeTags:       req.IncludeTags,
        ShowStats:         req.Options.CollectStats,
        MeasureMemory:     req.Options.MeasureMemory,
        MaxPackBytes:      req.Options.MaxPackBytes,
        Source:             source,
        Target:             target,
        HTTPClient:         c.httpClient,
        Branches:           append([]string(nil), req.Scope.Branches...),
        Mappings:           validationMappings(req.Scope.Mappings),
        IncludeTags:        req.IncludeTags,
        ShowStats:          req.Options.CollectStats,
        MeasureMemory:      req.Options.MeasureMemory,
        MaxPackBytes:       req.Options.MaxPackBytes,
        TargetMaxPackBytes: req.Options.TargetMaxPackBytes,
        ProtocolMode:      protocolString(req.Protocol),
        Verbose:           req.Options.Verbose,
        ProtocolMode:       protocolString(req.Protocol),
        Verbose:            req.Options.Verbose,
    }, nil
}

18 unmodified lines

if c.auth == nil {
        return gitsync.EndpointAuth{}, nil
    }
    return c.auth.AuthFor(ctx, endpoint, role)
    auth, err := c.auth.AuthFor(ctx, endpoint, role)
    if err != nil {
        return gitsync.EndpointAuth{}, fmt.Errorf("resolve auth for %s: %w", role, err)
    }
    return auth, nil
}

func (c *Client) resolveEndpoint(ctx context.Context, endpoint gitsync.Endpoint, role gitsync.EndpointRole) (syncer.Endpoint, error) {

Mpkg/gitsync/unstable/client.go+47/-18