Add CI workflows and fix all golangci-lint violations · Entire
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
Changes
60
.github/workflows
Aci.yml+17
Alicense-check.yml+12
Alint.yml+36
A.golangci.yaml+118
cmd
git-sync-bench
Mmain.go+15/-7
git-sync
Mmain.go+17/-13
Mmain_test.go+11/-8
Mgo.mod+4
Mgo.sum+3
internal
auth
Mauth.go+7/-5
Mauth_test.go+21/-21
Mentiredb.go+8/-7
Mtokenstore.go+25/-13
gitproto
Mbenchmark_test.go+7/-7
Mcapability_test.go+2/-1
Mconvert.go+6/-1
Mfetch.go+23/-10
Mfetch_test.go+12/-12
Mpktline.go+7/-7
Mpktline_test.go+4/-3
Mpush.go+19/-9
Mpush_test.go+20/-9
Mrefs.go+11/-8
Mrefs_test.go+4/-3
Msmarthttp.go+8/-8
Msmarthttp_test.go+15/-6
Mtarget_features_test.go+6/-5
planner
Mbenchmark_test.go+6/-6
Mcheckpoint.go+3/-3
Mplanner.go+23/-18
Mplanner_test.go+1/-1
Mrelay.go+2/-4
Mtypes.go+10/-5
strategy
bootstrap
Mbootstrap.go+36/-25
Mbootstrap_test.go+16/-10
incremental
Mincremental.go+11/-6
Mincremental_test.go+7/-4
materialized
Mmaterialized.go+11/-6
replicate
Mreplicate.go+12/-5
syncer
Mauth_test.go+5/-3
Mbenchmark_test.go+4/-4
Mentire_local_smoke_test.go+6/-5
Mgit_http_backend_test.go+19/-19
Mintegration_test.go+58/-49
Mlive_bootstrap_test.go+7/-7
Mstats.go+8/-4
Msyncer.go+70/-45
syncertest
Mrepo.go+3/-3
mise-tasks/lint
A_default+3
Ago+13
Agofmt+15
Agomod+12
Ashellcheck+4
Mmise.toml+4
pkg/gitsync
Mclient.go+20/-15
Mclient_test.go+2/-1
Mexample_test.go+4/-2
internalbridge
Mconfig.go+10/-2
Mtypes.go+1/-1
unstable
Mclient.go+47/-18
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