Move git-sync CLI to cobra for per-subcommand help · Entire

Home

Log in

Move git-sync CLI to cobra for per-subcommand help

f5cbb41→main·

Soph·2mo ago·10 files·+604 added/-589 removed

Bare git-sync and --help now print a clean subcommand listing on stdout instead of dumping the full usage block as an error. Each subcommand gets its own --help. Unknown command/flag errors fall back to the usage of the deepest matched subcommand. Flag names, env vars, positional args, JSON output, and inner error strings are unchanged.

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

Sessions

d76bd640e293View transcript

?\ can you review the changes in this branch and read the pr description if everything looks rightCodex·GPT-5.4·1 step

Changes

10

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

package main

import (
    "fmt"

gitsync "entire.io/entire/git-sync"
    "entire.io/entire/git-sync/internal/validation"
    "entire.io/entire/git-sync/unstable"
    "github.com/spf13/cobra"
)

func newBootstrapCmd() *cobra.Command {
    var (
        mappings    []string
        jsonOutput  bool
        sourceAuth  gitsync.EndpointAuth
        targetAuth  gitsync.EndpointAuth
        branches    string
        protocolVal = newProtocolFlag()
        req         = unstable.BootstrapRequest{}
    )

cmd := &cobra.Command{
        Use:           "bootstrap [flags] <source-url> <target-url>",
        Short:         "Seed an empty target by streaming the source pack",
        Args:          cobra.MaximumNArgs(2),
        SilenceErrors: true,
        SilenceUsage:  true,
        RunE: func(cmd *cobra.Command, args []string) error {
            req.Protocol = gitsync.ProtocolMode(protocolVal)

if req.Source.URL == "" && len(args) > 0 {
                req.Source.URL = args[0]
            }
            if req.Target.URL == "" && len(args) > 1 {
                req.Target.URL = args[1]
            }

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

if req.Source.URL == "" || req.Target.URL == "" {
                return fmt.Errorf("bootstrap requires source and target repository URLs")
            }

result, err := unstable.New(unstable.Options{
                Auth: gitsync.StaticAuthProvider{Source: sourceAuth, Target: targetAuth},
            }).Bootstrap(cmd.Context(), req)
            if err != nil {
                return fmt.Errorf("bootstrap: %w", err)
            }
            printOutput(jsonOutput, result)
            return nil
        },
    }

addSourceEndpoint(cmd, &req.Source)
    addTargetEndpoint(cmd, &req.Target)
    addSourceAuth(cmd, &sourceAuth)
    addTargetAuth(cmd, &targetAuth)

cmd.Flags().StringVar(&branches, "branch", "", "comma-separated branch list; default is all source branches")
    cmd.Flags().StringArrayVar(&mappings, "map", nil, "ref mapping in src:dst form; short names map branches, full refs map exact refs")
    cmd.Flags().BoolVar(&req.IncludeTags, "tags", false, "mirror tags")
    cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output")
    cmd.Flags().Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap if the streamed source pack exceeds this many bytes")
    cmd.Flags().Int64Var(&req.Options.TargetMaxPackBytes, "target-max-pack-bytes", 0, "target receive-pack body size limit; batches are planned and auto-subdivided to fit")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVarP(&req.Options.Verbose, "verbose", "v", false, "verbose logging")

return cmd
}

Acmd/git-sync/bootstrap.go+85

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

package main

import (
    "fmt"
    "strings"

gitsync "entire.io/entire/git-sync"
    "entire.io/entire/git-sync/unstable"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/spf13/cobra"
)

func newFetchCmd() *cobra.Command {
    var (
        haveRefs      []string
        haveHashesRaw []string
        jsonOutput    bool
        sourceAuth    gitsync.EndpointAuth
        branches      string
        protocolVal   = newProtocolFlag()
        req           = unstable.FetchRequest{}
    )

cmd := &cobra.Command{
        Use:           "fetch [flags] <source-url>",
        Short:         "Negotiate a fetch against the source and report packed objects",
        Args:          cobra.MaximumNArgs(1),
        SilenceErrors: true,
        SilenceUsage:  true,
        RunE: func(cmd *cobra.Command, args []string) error {
            req.Protocol = gitsync.ProtocolMode(protocolVal)

if req.Source.URL == "" && len(args) > 0 {
                req.Source.URL = args[0]
            }
            if req.Source.URL == "" {
                return fmt.Errorf("fetch requires a source repository URL")
            }
            if branches != "" {
                req.Scope.Branches = splitCSV(branches)
            }

haveHashes := make([]plumbing.Hash, 0, len(haveHashesRaw))
            for _, raw := range haveHashesRaw {
                hash := plumbing.NewHash(strings.TrimSpace(raw))
                if hash.IsZero() {
                    return fmt.Errorf("invalid --have %q", raw)
                }
                haveHashes = append(haveHashes, hash)
            }

req.HaveRefs = append(req.HaveRefs, haveRefs...)
            req.HaveHashes = append(req.HaveHashes, haveHashes...)

result, err := unstable.New(unstable.Options{
                Auth: gitsync.StaticAuthProvider{Source: sourceAuth},
            }).Fetch(cmd.Context(), req)
            if err != nil {
                return fmt.Errorf("fetch: %w", err)
            }
            printOutput(jsonOutput, result)
            return nil
        },
    }

addSourceEndpoint(cmd, &req.Source)
    addSourceAuth(cmd, &sourceAuth)

cmd.Flags().StringVar(&branches, "branch", "", "comma-separated branch list; default is all source branches")
    cmd.Flags().BoolVar(&req.IncludeTags, "tags", false, "include tags in the fetch request")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output")
    cmd.Flags().StringArrayVar(&haveRefs, "have-ref", nil, "source ref name to advertise as have; short names map to branches")
    cmd.Flags().StringArrayVar(&haveHashesRaw, "have", nil, "explicit object hash to advertise as have")

return cmd
}

Acmd/git-sync/fetch.go+79

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

package main

import (
    "fmt"
    "os"
    "strings"

gitsync "entire.io/entire/git-sync"
    "entire.io/entire/git-sync/internal/validation"
    "github.com/spf13/cobra"
)

func addSourceEndpoint(cmd *cobra.Command, ep *gitsync.Endpoint) {
    cmd.Flags().StringVar(&ep.URL, "source-url", "", "source repository URL")
    cmd.Flags().BoolVar(&ep.FollowInfoRefsRedirect, "source-follow-info-refs-redirect",
        envBool("GITSYNC_SOURCE_FOLLOW_INFO_REFS_REDIRECT"),
        "send follow-up source RPCs to the final /info/refs redirect host")
}

func addTargetEndpoint(cmd *cobra.Command, ep *gitsync.Endpoint) {
    cmd.Flags().StringVar(&ep.URL, "target-url", "", "target repository URL")
    cmd.Flags().BoolVar(&ep.FollowInfoRefsRedirect, "target-follow-info-refs-redirect",
        envBool("GITSYNC_TARGET_FOLLOW_INFO_REFS_REDIRECT"),
        "send follow-up target RPCs to the final /info/refs redirect host")
}

func addSourceAuth(cmd *cobra.Command, auth *gitsync.EndpointAuth) {
    cmd.Flags().StringVar(&auth.Token, "source-token", envOr("GITSYNC_SOURCE_TOKEN", ""), "source token/password")
    cmd.Flags().StringVar(&auth.Username, "source-username", envOr("GITSYNC_SOURCE_USERNAME", "git"), "source basic auth username")
    cmd.Flags().StringVar(&auth.BearerToken, "source-bearer-token", envOr("GITSYNC_SOURCE_BEARER_TOKEN", ""), "source bearer token")
    cmd.Flags().BoolVar(&auth.SkipTLSVerify, "source-insecure-skip-tls-verify",
        envBool("GITSYNC_SOURCE_INSECURE_SKIP_TLS_VERIFY"),
        "skip TLS certificate verification for the source")
}

func addTargetAuth(cmd *cobra.Command, auth *gitsync.EndpointAuth) {
    cmd.Flags().StringVar(&auth.Token, "target-token", envOr("GITSYNC_TARGET_TOKEN", ""), "target token/password")
    cmd.Flags().StringVar(&auth.Username, "target-username", envOr("GITSYNC_TARGET_USERNAME", "git"), "target basic auth username")
    cmd.Flags().StringVar(&auth.BearerToken, "target-bearer-token", envOr("GITSYNC_TARGET_BEARER_TOKEN", ""), "target bearer token")
    cmd.Flags().BoolVar(&auth.SkipTLSVerify, "target-insecure-skip-tls-verify",
        envBool("GITSYNC_TARGET_INSECURE_SKIP_TLS_VERIFY"),
        "skip TLS certificate verification for the target")
}

func addProtocolFlag(cmd *cobra.Command, mode *protocolModeFlag) {
    cmd.Flags().Var(mode, "protocol", "protocol mode: auto, v1, or v2")
}

func newProtocolFlag() protocolModeFlag {
    return protocolModeFlag(protocolMode(envOr("GITSYNC_PROTOCOL", validation.ProtocolAuto)))
}

func envOr(key, fallback string) string {
    value := os.Getenv(key)
    if value == "" {
        return fallback
    }
    return value
}

func envBool(key string) bool {
    value := strings.TrimSpace(strings.ToLower(os.Getenv(key)))
    return value == "1" || value == "true" || value == "yes" || value == "on"
}

func splitCSV(value string) []string {
    parts := strings.Split(value, ",")
    out := make([]string, 0, len(parts))
    for _, part := range parts {
        part = strings.TrimSpace(part)
        if part != "" {
            out = append(out, part)
        }
    }
    return out
}

type protocolMode gitsync.ProtocolMode
type operationMode gitsync.OperationMode

type protocolModeFlag protocolMode
type operationModeFlag operationMode

func (p *protocolModeFlag) String() string { return string(*p) }
func (p *protocolModeFlag) Type() string   { return "string" }

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

func (m *operationModeFlag) String() string { return string(*m) }
func (m *operationModeFlag) Type() string   { return "string" }

func (m *operationModeFlag) Set(value string) error {
    switch gitsync.OperationMode(value) {
    case gitsync.ModeSync, gitsync.ModeReplicate:
        *m = operationModeFlag(operationMode(value))
        return nil
    default:
        return fmt.Errorf("unsupported mode %q", value)
    }
}

// defaultOperationMode returns the starting value for the --mode flag.
// Subcommands that pin a mode (sync, replicate) pass it in; plan passes ""
// and gets sync as the default, letting --mode override it.
func defaultOperationMode(defaultMode gitsync.OperationMode) operationMode {
    if defaultMode != "" {
        return operationMode(defaultMode)
    }
    return operationMode(gitsync.ModeSync)
}

Acmd/git-sync/flags.go+117

1 unmodified line

2
3
4
5
5
7
6
7
8
9
12
13
14
15
16
10
11
12
13
20
14
15
16
17
18
19
22
20
21
22
23
24
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
25
26
27
28
29
48
49
50
51
30
53
31
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
32
33
34
35
36
37
38
39
40
41
124
125
126
127
42
43
44
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
45
46
47
161
162
163
48
49
50
51
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
52
53
54
55

1 unmodified line

import (
    "context"
    "encoding/json"
    "errors"
    "flag"
    "fmt"
    "os"
    "strings"

"entire.io/entire/git-sync"
    "entire.io/entire/git-sync/cmd/git-sync/internal/versioninfo"
    "entire.io/entire/git-sync/internal/validation"
    "entire.io/entire/git-sync/unstable"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/spf13/cobra"
)

func main() {
    if err := run(context.Background(), os.Args[1:]); err != nil {
    err := run(context.Background(), os.Args[1:])
    if err == nil {
        return
    }
    if !errors.Is(err, errSilent) {
        fmt.Fprintf(os.Stderr, "error: %v\n", err)
        os.Exit(1)
    }
    os.Exit(1)
}

func run(ctx context.Context, args []string) error {
    if len(args) == 0 {
        return usageError("")
    }

switch args[0] {
    case "sync":
        return runSyncLike(ctx, "sync", args[1:], false, gitsync.ModeSync)
    case "replicate":
        return runSyncLike(ctx, "replicate", args[1:], false, gitsync.ModeReplicate)
    case "plan":
        return runSyncLike(ctx, "plan", args[1:], true, "")
    case "bootstrap":
        return runBootstrap(ctx, args[1:])
    case "probe":
        return runProbe(ctx, args[1:])
    case "fetch":
        return runFetch(ctx, args[1:])
    case "version", "--version":
        fmt.Printf("git-sync %s (commit %s, built %s)\n",
            versioninfo.Version, versioninfo.Commit, versioninfo.Date)
    rootCmd := newRootCmd()
    rootCmd.SetArgs(args)
    err := rootCmd.ExecuteContext(ctx)
    if err == nil {
        return nil
    case "help", "-h", "--help":
        return usageError("")
    default:
        return usageError(fmt.Sprintf("unknown command %q", args[0]))
    }
}

func runSyncLike(ctx context.Context, name string, args []string, dryRun bool, defaultMode gitsync.OperationMode) error {
    fs := flag.NewFlagSet(name, flag.ContinueOnError)
    fs.SetOutput(os.Stderr)

var mappings multiStringFlag
    var jsonOutput bool
    var sourceAuth gitsync.EndpointAuth
    var targetAuth gitsync.EndpointAuth
    req := unstable.SyncRequest{DryRun: dryRun}

fs.StringVar(&req.Source.URL, "source-url", "", "source repository URL")
    fs.StringVar(&req.Target.URL, "target-url", "", "target repository URL")
    fs.BoolVar(&req.Source.FollowInfoRefsRedirect, "source-follow-info-refs-redirect", envBool("GITSYNC_SOURCE_FOLLOW_INFO_REFS_REDIRECT"), "send follow-up source RPCs to the final /info/refs redirect host")
    fs.BoolVar(&req.Target.FollowInfoRefsRedirect, "target-follow-info-refs-redirect", envBool("GITSYNC_TARGET_FOLLOW_INFO_REFS_REDIRECT"), "send follow-up target RPCs to the final /info/refs redirect host")

fs.StringVar(&sourceAuth.Token, "source-token", envOr("GITSYNC_SOURCE_TOKEN", ""), "source token/password")
    fs.StringVar(&targetAuth.Token, "target-token", envOr("GITSYNC_TARGET_TOKEN", ""), "target token/password")
    fs.StringVar(&sourceAuth.Username, "source-username", envOr("GITSYNC_SOURCE_USERNAME", "git"), "source basic auth username")
    fs.StringVar(&targetAuth.Username, "target-username", envOr("GITSYNC_TARGET_USERNAME", "git"), "target basic auth username")
    fs.BoolVar(&sourceAuth.SkipTLSVerify, "source-insecure-skip-tls-verify", envBool("GITSYNC_SOURCE_INSECURE_SKIP_TLS_VERIFY"), "skip TLS certificate verification for the source")
    fs.BoolVar(&targetAuth.SkipTLSVerify, "target-insecure-skip-tls-verify", envBool("GITSYNC_TARGET_INSECURE_SKIP_TLS_VERIFY"), "skip TLS certificate verification for the target")

fs.StringVar(&sourceAuth.BearerToken, "source-bearer-token", envOr("GITSYNC_SOURCE_BEARER_TOKEN", ""), "source bearer token")
    fs.StringVar(&targetAuth.BearerToken, "target-bearer-token", envOr("GITSYNC_TARGET_BEARER_TOKEN", ""), "target bearer token")

branches := fs.String("branch", "", "comma-separated branch list; default is all source branches")
    fs.Var(&mappings, "map", "ref mapping in src:dst form; short names map branches, full refs map exact refs")
    modeValue := operationModeFlag(defaultOperationMode(defaultMode))
    if name == "plan" {
        fs.Var(&modeValue, "mode", "operation mode: sync or replicate")
    }
    fs.BoolVar(&req.Policy.IncludeTags, "tags", false, "mirror tags")
    fs.BoolVar(&req.Policy.Force, "force", false, "allow non-fast-forward branch updates and retarget tags")
    fs.BoolVar(&req.Policy.Prune, "prune", false, "delete managed target refs that no longer exist on source")
    fs.BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    fs.BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    fs.BoolVar(&jsonOutput, "json", false, "print JSON output")
    fs.IntVar(&req.Options.MaterializedMaxObjects, "materialized-max-objects", unstable.DefaultMaterializedMaxObjects, "abort non-relay materialized syncs above this many objects")
    fs.Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap-relay push if the streamed source pack exceeds this many bytes")
    fs.Int64Var(&req.Options.TargetMaxPackBytes, "target-max-pack-bytes", 0, "target receive-pack body size limit; batches are planned and auto-subdivided to fit")
    protocolValue := protocolModeFlag(protocolMode(envOr("GITSYNC_PROTOCOL", validation.ProtocolAuto)))
    fs.Var(&protocolValue, "protocol", "protocol mode: auto, v1, or v2")
    fs.BoolVar(&req.Options.Verbose, "v", false, "verbose logging")

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

positional := fs.Args()
    if req.Source.URL == "" && len(positional) > 0 {
        req.Source.URL = positional[0]
    }
    if req.Target.URL == "" && len(positional) > 1 {
        req.Target.URL = positional[1]
    }
    if len(positional) > 2 {
        return usageError("too many positional arguments")
    }

if *branches != "" {
        req.Scope.Branches = splitCSV(*branches)
    }
    for _, raw := range mappings {
        mapping, err := validation.ParseMapping(raw)
        if err != nil {
            return fmt.Errorf("parse mapping %q: %w", raw, err)
    // On unknown commands or unknown flags, fall back to printing usage so
    // the user sees what's available instead of a one-line cryptic error.
    // Use the deepest subcommand that matched the args so flag errors show
    // the relevant subcommand's usage, not the root.
    msg := err.Error()
    if strings.Contains(msg, "unknown command") || strings.Contains(msg, "unknown flag") || strings.Contains(msg, "unknown shorthand flag") {
        target := rootCmd
        if found, _, ferr := rootCmd.Find(args); ferr == nil && found != nil {
            target = found
        }
        req.Scope.Mappings = append(req.Scope.Mappings, gitsync.RefMapping{
            Source: mapping.Source,
            Target: mapping.Target,
        })
        showUsage(target, err)
        return errSilent
    }

if req.Source.URL == "" || req.Target.URL == "" {
        return usageError(name + " requires source and target repository URLs")
    }

client := unstable.New(unstable.Options{
        Auth: gitsync.StaticAuthProvider{Source: sourceAuth, Target: targetAuth},
    })
    var (
        result unstable.Result
        err    error
    )
    if dryRun {
        result, err = client.Plan(ctx, req)
    } else {
        if req.Policy.Mode == gitsync.ModeReplicate {
            result, err = client.Replicate(ctx, req)
        } else {
            result, err = client.Sync(ctx, req)
        }
    }
    if err != nil {
        return fmt.Errorf("sync: %w", err)
    }
    printOutput(jsonOutput, result)

if !dryRun && result.Blocked > 0 {
        return errors.New("one or more branches were skipped because the target was not fast-forwardable")
    }
    return nil
    return err
}

func runBootstrap(ctx context.Context, args []string) error {
    fs := flag.NewFlagSet("bootstrap", flag.ContinueOnError)
    fs.SetOutput(os.Stderr)
// errSilent signals that main should exit non-zero without re-printing the
// error (it has already been printed alongside the usage block).
var errSilent = errors.New("")

var mappings multiStringFlag
    var jsonOutput bool
    var sourceAuth gitsync.EndpointAuth
    var targetAuth gitsync.EndpointAuth
    req := unstable.BootstrapRequest{}

branches := fs.String("branch", "", "comma-separated branch list; default is all source branches")
    fs.Var(&mappings, "map", "ref mapping in src:dst form; short names map branches, full refs map exact refs")
    fs.BoolVar(&req.IncludeTags, "tags", false, "mirror tags")
    fs.BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    fs.BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    fs.BoolVar(&jsonOutput, "json", false, "print JSON output")
    fs.Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap if the streamed source pack exceeds this many bytes")
    fs.Int64Var(&req.Options.TargetMaxPackBytes, "target-max-pack-bytes", 0, "target receive-pack body size limit; batches are planned and auto-subdivided to fit")
    bootstrapProtocol := protocolModeFlag(protocolMode(envOr("GITSYNC_PROTOCOL", validation.ProtocolAuto)))
    fs.Var(&bootstrapProtocol, "protocol", "protocol mode: auto, v1, or v2")
    fs.BoolVar(&req.Options.Verbose, "v", false, "verbose logging")

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

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

if req.Source.URL == "" || req.Target.URL == "" {
        return usageError("bootstrap requires source and target repository URLs")
    }

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

func runProbe(ctx context.Context, args []string) error {
    fs := flag.NewFlagSet("probe", flag.ContinueOnError)
    fs.SetOutput(os.Stderr)

var jsonOutput bool
    var sourceAuth gitsync.EndpointAuth
    var targetAuth gitsync.EndpointAuth
    var targetFollowInfoRefsRedirect bool
    req := unstable.ProbeRequest{}
    fs.StringVar(&req.Source.URL, "source-url", "", "source repository URL")
    targetURL := fs.String("target-url", "", "optional target repository URL")
    fs.BoolVar(&req.Source.FollowInfoRefsRedirect, "source-follow-info-refs-redirect", envBool("GITSYNC_SOURCE_FOLLOW_INFO_REFS_REDIRECT"), "send follow-up source RPCs to the final /info/refs redirect host")
    fs.BoolVar(&targetFollowInfoRefsRedirect, "target-follow-info-refs-redirect", envBool("GITSYNC_TARGET_FOLLOW_INFO_REFS_REDIRECT"), "send follow-up target RPCs to the final /info/refs redirect host")
    fs.StringVar(&sourceAuth.Token, "source-token", envOr("GITSYNC_SOURCE_TOKEN", ""), "source token/password")
    fs.StringVar(&targetAuth.Token, "target-token", envOr("GITSYNC_TARGET_TOKEN", ""), "target token/password")
    fs.StringVar(&sourceAuth.Username, "source-username", envOr("GITSYNC_SOURCE_USERNAME", "git"), "source basic auth username")
    fs.StringVar(&targetAuth.Username, "target-username", envOr("GITSYNC_TARGET_USERNAME", "git"), "target basic auth username")
    fs.StringVar(&sourceAuth.BearerToken, "source-bearer-token", envOr("GITSYNC_SOURCE_BEARER_TOKEN", ""), "source bearer token")
    fs.StringVar(&targetAuth.BearerToken, "target-bearer-token", envOr("GITSYNC_TARGET_BEARER_TOKEN", ""), "target bearer token")
    fs.BoolVar(&sourceAuth.SkipTLSVerify, "source-insecure-skip-tls-verify", envBool("GITSYNC_SOURCE_INSECURE_SKIP_TLS_VERIFY"), "skip TLS certificate verification for the source")
    fs.BoolVar(&targetAuth.SkipTLSVerify, "target-insecure-skip-tls-verify", envBool("GITSYNC_TARGET_INSECURE_SKIP_TLS_VERIFY"), "skip TLS certificate verification for the target")
    fs.BoolVar(&req.IncludeTags, "tags", false, "include tag ref prefixes in probe")
    probeProtocol := protocolModeFlag(protocolMode(envOr("GITSYNC_PROTOCOL", validation.ProtocolAuto)))
    fs.Var(&probeProtocol, "protocol", "protocol mode: auto, v1, or v2")
    fs.BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    fs.BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    fs.BoolVar(&jsonOutput, "json", false, "print JSON output")

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

positional := fs.Args()
    if req.Source.URL == "" && len(positional) > 0 {
        req.Source.URL = positional[0]
    }
    if *targetURL == "" && len(positional) > 1 {
        *targetURL = positional[1]
    }
    if len(positional) > 2 {
        return usageError("too many positional arguments")
    }
    if req.Source.URL == "" {
        return usageError("probe requires a source repository URL")
    }
    if *targetURL != "" {
        req.Target = &gitsync.Endpoint{
            URL:                    *targetURL,
            FollowInfoRefsRedirect: targetFollowInfoRefsRedirect,
        }
    }

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

func runFetch(ctx context.Context, args []string) error {
    fs := flag.NewFlagSet("fetch", flag.ContinueOnError)
    fs.SetOutput(os.Stderr)

var haveRefs multiStringFlag
    var haveHashesRaw multiStringFlag
    var jsonOutput bool
    var sourceAuth gitsync.EndpointAuth
    req := unstable.FetchRequest{}

fs.StringVar(&req.Source.URL, "source-url", "", "source repository URL")
    fs.BoolVar(&req.Source.FollowInfoRefsRedirect, "source-follow-info-refs-redirect", envBool("GITSYNC_SOURCE_FOLLOW_INFO_REFS_REDIRECT"), "send follow-up source RPCs to the final /info/refs redirect host")
    fs.StringVar(&sourceAuth.Token, "source-token", envOr("GITSYNC_SOURCE_TOKEN", ""), "source token/password")
    fs.StringVar(&sourceAuth.Username, "source-username", envOr("GITSYNC_SOURCE_USERNAME", "git"), "source basic auth username")
    fs.StringVar(&sourceAuth.BearerToken, "source-bearer-token", envOr("GITSYNC_SOURCE_BEARER_TOKEN", ""), "source bearer token")
    fs.BoolVar(&sourceAuth.SkipTLSVerify, "source-insecure-skip-tls-verify", envBool("GITSYNC_SOURCE_INSECURE_SKIP_TLS_VERIFY"), "skip TLS certificate verification for the source")
    branches := fs.String("branch", "", "comma-separated branch list; default is all source branches")
    fs.BoolVar(&req.IncludeTags, "tags", false, "include tags in the fetch request")
    fetchProtocol := protocolModeFlag(protocolMode(envOr("GITSYNC_PROTOCOL", validation.ProtocolAuto)))
    fs.Var(&fetchProtocol, "protocol", "protocol mode: auto, v1, or v2")
    fs.BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    fs.BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    fs.BoolVar(&jsonOutput, "json", false, "print JSON output")
    fs.Var(&haveRefs, "have-ref", "source ref name to advertise as have; short names map to branches")
    fs.Var(&haveHashesRaw, "have", "explicit object hash to advertise as have")

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

positional := fs.Args()
    if req.Source.URL == "" && len(positional) > 0 {
        req.Source.URL = positional[0]
    }
    if len(positional) > 1 {
        return usageError("too many positional arguments")
    }
    if req.Source.URL == "" {
        return usageError("fetch requires a source repository URL")
    }
    if *branches != "" {
        req.Scope.Branches = splitCSV(*branches)
    }

req.HaveRefs = append(req.HaveRefs, haveRefs...)
    req.HaveHashes = append(req.HaveHashes, haveHashes...)
    result, err := unstable.New(unstable.Options{
        Auth: gitsync.StaticAuthProvider{Source: sourceAuth},
    }).Fetch(ctx, req)
    if err != nil {
        return fmt.Errorf("fetch: %w", err)
    }
    printOutput(jsonOutput, result)
    return nil
}

func printOutput(jsonOutput bool, value interface{ Lines() []string }) {
    if jsonOutput {
        data, err := marshalOutput(value)
        if err != nil {
            fmt.Fprintf(os.Stderr, "error: encode JSON output: %v\n", err)
            os.Exit(1)
        }
        fmt.Println(string(data))
        return
    }

for _, line := range value.Lines() {
        fmt.Println(line)
    }
}

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

type multiStringFlag []string

func (m *multiStringFlag) String() string {
    return strings.Join(*m, ",")
}

func (m *multiStringFlag) Set(value string) error {
    *m = append(*m, value)
    return nil
}

func envOr(key, fallback string) string {
    value := os.Getenv(key)
    if value == "" {
        return fallback
    }
    return value
}

func usageError(message string) error {
    usage := fmt.Sprintf(`usage:
  %[1]s sync [flags] <source-url> <target-url>
  %[1]s replicate [flags] <source-url> <target-url>
  %[1]s plan [flags] <source-url> <target-url>
  %[1]s bootstrap [flags] <source-url> <target-url>
  %[1]s probe [flags] <source-url> [target-url]
  %[1]s fetch [flags] <source-url>
  %[1]s version

sync flags:
  --branch main,dev
  --map main:stable
  --tags
  --force
  --prune
  --stats
  --measure-memory
  --json
  --materialized-max-objects %[2]d
  --max-pack-bytes <bytes>
  --target-max-pack-bytes <bytes>
  --protocol auto|v1|v2
  --source-token ...
  --target-token ...
  --source-username git
  --target-username git
  --source-bearer-token ...
  --target-bearer-token ...
  --source-insecure-skip-tls-verify
  --target-insecure-skip-tls-verify
  --source-follow-info-refs-redirect
  --target-follow-info-refs-redirect
  -v

replicate flags:
  --branch main,dev
  --map main:stable
  --tags
  --prune
  --stats
  --measure-memory
  --json
  --max-pack-bytes <bytes>
  --target-max-pack-bytes <bytes>
  --protocol auto|v1|v2
  --source-token ...
  --target-token ...
  --source-username git
  --target-username git
  --source-bearer-token ...
  --target-bearer-token ...
  --source-insecure-skip-tls-verify
  --target-insecure-skip-tls-verify
  --source-follow-info-refs-redirect
  --target-follow-info-refs-redirect
  -v

plan flags:
  --mode sync|replicate
  --branch main,dev
  --map main:stable
  --tags
  --force
  --prune
  --stats
  --measure-memory
  --json
  --max-pack-bytes <bytes>
  --target-max-pack-bytes <bytes>
  --protocol auto|v1|v2
  --source-token ...
  --target-token ...
  --source-username git
  --target-username git
  --source-bearer-token ...
  --target-bearer-token ...
  --source-insecure-skip-tls-verify
  --target-insecure-skip-tls-verify
  --source-follow-info-refs-redirect
  --target-follow-info-refs-redirect
  -v

bootstrap flags:
  --branch main,dev
  --map main:stable
  --tags
  --max-pack-bytes 104857600
  --target-max-pack-bytes 1073741824
  --stats
  --measure-memory
  --json
  --protocol auto|v1|v2
  --source-token ...
  --target-token ...
  --source-username git
  --target-username git
  --source-bearer-token ...
  --target-bearer-token ...
  --source-insecure-skip-tls-verify
  --target-insecure-skip-tls-verify
  --source-follow-info-refs-redirect
  --target-follow-info-refs-redirect
  -v

probe flags:
  --tags
  --stats
  --measure-memory
  --json
  --protocol auto|v1|v2
  --source-token ...
  --source-username git
  --source-bearer-token ...
  --target-token ...
  --target-username git
  --target-bearer-token ...
  --source-insecure-skip-tls-verify
  --target-insecure-skip-tls-verify
  --source-follow-info-refs-redirect
  --target-follow-info-refs-redirect

fetch flags:
  --branch main,dev
  --tags
  --stats
  --measure-memory
  --json
  --protocol auto|v1|v2
  --have-ref main
  --have <hash>
  --source-token ...
  --source-username git
  --source-bearer-token ...
  --source-insecure-skip-tls-verify
  --source-follow-info-refs-redirect
`, os.Args[0], unstable.DefaultMaterializedMaxObjects)
    if message == "" {
        return errors.New(strings.TrimSpace(usage))
    }
    return fmt.Errorf("%s\n\n%s", message, usage)
}

type protocolMode gitsync.ProtocolMode
type operationMode gitsync.OperationMode

type protocolModeFlag protocolMode
type operationModeFlag operationMode

func (p *protocolModeFlag) String() string {
    return string(*p)
}

func (m *operationModeFlag) String() string {
    return string(*m)
}

// defaultOperationMode returns the starting value for the --mode flag.
// Subcommands that pin a mode (sync, replicate) pass it in; plan passes ""
// and gets sync as the default, letting --mode override it.
func defaultOperationMode(defaultMode gitsync.OperationMode) operationMode {
    if defaultMode != "" {
        return operationMode(defaultMode)
    }
    return operationMode(gitsync.ModeSync)
func showUsage(cmd *cobra.Command, err error) {
    fmt.Fprint(cmd.OutOrStderr(), cmd.UsageString())
    fmt.Fprintf(cmd.OutOrStderr(), "\nError: %v\n", err)
}

Mcmd/git-sync/main.go+29/-589

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

package main

import (
    "fmt"

gitsync "entire.io/entire/git-sync"
    "entire.io/entire/git-sync/unstable"
    "github.com/spf13/cobra"
)

func newProbeCmd() *cobra.Command {
    var (
        jsonOutput                   bool
        sourceAuth                   gitsync.EndpointAuth
        targetAuth                   gitsync.EndpointAuth
        targetURL                    string
        targetFollowInfoRefsRedirect bool
        protocolVal                  = newProtocolFlag()
        req                          = unstable.ProbeRequest{}
    )

cmd := &cobra.Command{
        Use:           "probe [flags] <source-url> [target-url]",
        Short:         "Inspect refs advertised by source (and optionally target)",
        Args:          cobra.MaximumNArgs(2),
        SilenceErrors: true,
        SilenceUsage:  true,
        RunE: func(cmd *cobra.Command, args []string) error {
            req.Protocol = gitsync.ProtocolMode(protocolVal)

if req.Source.URL == "" && len(args) > 0 {
                req.Source.URL = args[0]
            }
            if targetURL == "" && len(args) > 1 {
                targetURL = args[1]
            }
            if req.Source.URL == "" {
                return fmt.Errorf("probe requires a source repository URL")
            }
            if targetURL != "" {
                req.Target = &gitsync.Endpoint{
                    URL:                    targetURL,
                    FollowInfoRefsRedirect: targetFollowInfoRefsRedirect,
                }
            }

result, err := unstable.New(unstable.Options{
                Auth: gitsync.StaticAuthProvider{Source: sourceAuth, Target: targetAuth},
            }).Probe(cmd.Context(), req)
            if err != nil {
                return fmt.Errorf("probe: %w", err)
            }
            printOutput(jsonOutput, result)
            return nil
        },
    }

addSourceEndpoint(cmd, &req.Source)
    cmd.Flags().StringVar(&targetURL, "target-url", "", "optional target repository URL")
    cmd.Flags().BoolVar(&targetFollowInfoRefsRedirect, "target-follow-info-refs-redirect",
        envBool("GITSYNC_TARGET_FOLLOW_INFO_REFS_REDIRECT"),
        "send follow-up target RPCs to the final /info/refs redirect host")
    addSourceAuth(cmd, &sourceAuth)
    addTargetAuth(cmd, &targetAuth)

cmd.Flags().BoolVar(&req.IncludeTags, "tags", false, "include tag ref prefixes in probe")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output")

return cmd
}

Acmd/git-sync/probe.go+73

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

package main

import (
    "encoding/json"
    "fmt"
    "os"

"entire.io/entire/git-sync/cmd/git-sync/internal/versioninfo"
    "github.com/spf13/cobra"
)

func newRootCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "git-sync",
        Short: "Sync, replicate, or probe git repositories over the smart HTTP protocol",
        Long: `git-sync moves refs and objects between two git HTTP endpoints without
needing a working tree. It can mirror a source into a target (sync), do a
fast-forward-only mirror (replicate), preview the work to be done (plan),
seed an empty target (bootstrap), or inspect either side (probe, fetch).`,
        Version:       versioninfo.Version,
        SilenceErrors: true,
        SilenceUsage:  true,
        CompletionOptions: cobra.CompletionOptions{
            HiddenDefaultCmd: true,
        },
        RunE: func(cmd *cobra.Command, _ []string) error {
            return cmd.Help()
        },
    }
    cmd.SetVersionTemplate(fmt.Sprintf("git-sync %s (commit %s, built %s)\n",
        versioninfo.Version, versioninfo.Commit, versioninfo.Date))

cmd.AddCommand(newSyncCmd())
    cmd.AddCommand(newReplicateCmd())
    cmd.AddCommand(newPlanCmd())
    cmd.AddCommand(newBootstrapCmd())
    cmd.AddCommand(newProbeCmd())
    cmd.AddCommand(newFetchCmd())
    cmd.AddCommand(newVersionCmd())

return cmd
}

for _, line := range value.Lines() {
        fmt.Println(line)
    }
}

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

Acmd/git-sync/root.go+66

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

package main

import (
    "errors"
    "fmt"

gitsync "entire.io/entire/git-sync"
    "entire.io/entire/git-sync/internal/validation"
    "entire.io/entire/git-sync/unstable"
    "github.com/spf13/cobra"
)

func newSyncCmd() *cobra.Command {
    return newSyncLikeCmd("sync", "Mirror refs and objects from source to target", false, gitsync.ModeSync)
}

func newReplicateCmd() *cobra.Command {
    return newSyncLikeCmd("replicate", "Fast-forward-only mirror from source to target", false, gitsync.ModeReplicate)
}

func newPlanCmd() *cobra.Command {
    return newSyncLikeCmd("plan", "Show what a sync or replicate would do without pushing", true, "")
}

func newSyncLikeCmd(name, short string, dryRun bool, defaultMode gitsync.OperationMode) *cobra.Command {
    var (
        mappings    []string
        jsonOutput  bool
        sourceAuth  gitsync.EndpointAuth
        targetAuth  gitsync.EndpointAuth
        branches    string
        modeValue   = operationModeFlag(defaultOperationMode(defaultMode))
        protocolVal = newProtocolFlag()
        req         = unstable.SyncRequest{DryRun: dryRun}
    )

cmd := &cobra.Command{
        Use:           name + " [flags] <source-url> <target-url>",
        Short:         short,
        Args:          cobra.MaximumNArgs(2),
        SilenceErrors: true,
        SilenceUsage:  true,
        RunE: func(cmd *cobra.Command, args []string) error {
            req.Policy.Mode = gitsync.OperationMode(modeValue)
            req.Policy.Protocol = gitsync.ProtocolMode(protocolVal)

if req.Source.URL == "" && len(args) > 0 {
                req.Source.URL = args[0]
            }
            if req.Target.URL == "" && len(args) > 1 {
                req.Target.URL = args[1]
            }

if req.Source.URL == "" || req.Target.URL == "" {
                return fmt.Errorf("%s requires source and target repository URLs", name)
            }

client := unstable.New(unstable.Options{
                Auth: gitsync.StaticAuthProvider{Source: sourceAuth, Target: targetAuth},
            })

var (
                result unstable.Result
                err    error
            )
            ctx := cmd.Context()
            switch {
            case dryRun:
                result, err = client.Plan(ctx, req)
            case req.Policy.Mode == gitsync.ModeReplicate:
                result, err = client.Replicate(ctx, req)
            default:
                result, err = client.Sync(ctx, req)
            }
            if err != nil {
                return fmt.Errorf("%s: %w", name, err)
            }
            printOutput(jsonOutput, result)

if !dryRun && result.Blocked > 0 {
                return errors.New("one or more branches were skipped because the target was not fast-forwardable")
            }
            return nil
        },
    }

addSourceEndpoint(cmd, &req.Source)
    addTargetEndpoint(cmd, &req.Target)
    addSourceAuth(cmd, &sourceAuth)
    addTargetAuth(cmd, &targetAuth)

cmd.Flags().StringVar(&branches, "branch", "", "comma-separated branch list; default is all source branches")
    cmd.Flags().StringArrayVar(&mappings, "map", nil, "ref mapping in src:dst form; short names map branches, full refs map exact refs")
    if name == "plan" {
        cmd.Flags().Var(&modeValue, "mode", "operation mode: sync or replicate")
    }
    cmd.Flags().BoolVar(&req.Policy.IncludeTags, "tags", false, "mirror tags")
    cmd.Flags().BoolVar(&req.Policy.Force, "force", false, "allow non-fast-forward branch updates and retarget tags")
    cmd.Flags().BoolVar(&req.Policy.Prune, "prune", false, "delete managed target refs that no longer exist on source")
    cmd.Flags().BoolVar(&req.Options.CollectStats, "stats", false, "print transfer statistics")
    cmd.Flags().BoolVar(&req.Options.MeasureMemory, "measure-memory", false, "sample elapsed time and Go heap usage")
    cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output")
    cmd.Flags().IntVar(&req.Options.MaterializedMaxObjects, "materialized-max-objects", unstable.DefaultMaterializedMaxObjects, "abort non-relay materialized syncs above this many objects")
    cmd.Flags().Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap-relay push if the streamed source pack exceeds this many bytes")
    cmd.Flags().Int64Var(&req.Options.TargetMaxPackBytes, "target-max-pack-bytes", 0, "target receive-pack body size limit; batches are planned and auto-subdivided to fit")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVarP(&req.Options.Verbose, "verbose", "v", false, "verbose logging")

return cmd
}

Acmd/git-sync/syncplan.go+124

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

package main

import (
    "fmt"

"entire.io/entire/git-sync/cmd/git-sync/internal/versioninfo"
    "github.com/spf13/cobra"
)

func newVersionCmd() *cobra.Command {
    return &cobra.Command{
        Use:   "version",
        Short: "Show build information",
        Run: func(cmd *cobra.Command, _ []string) {
            fmt.Fprintf(cmd.OutOrStdout(), "git-sync %s (commit %s, built %s)\n",
                versioninfo.Version, versioninfo.Commit, versioninfo.Date)
        },
    }
}

Acmd/git-sync/version.go+19

4 unmodified lines

5
6
7
8
9
10
11
8 unmodified lines

20
21
22
23
24
25
26
27
28
29
30
31
32

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.2
    github.com/spf13/cobra v1.10.2
    github.com/stretchr/testify v1.11.1
    github.com/zalando/go-keyring v0.2.8
)
8 unmodified lines

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
    github.com/inconshreveable/mousetrap v1.1.0 // indirect
    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
    github.com/spf13/pflag v1.0.9 // indirect
    golang.org/x/crypto v0.50.0 // indirect
    golang.org/x/net v0.53.0 // indirect
    golang.org/x/sync v0.20.0 // indirect

Mgo.mod+3

7 unmodified lines

8
9
10
11
12
13
14
15 unmodified lines

30
31
32
33
34
35
36
37
7 unmodified lines

45
46
47
48
49
50
51
52
53
54
55
56
57
2 unmodified lines

60
61
62
63
64
65
66

7 unmodified lines

github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
15 unmodified lines

github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
7 unmodified lines

github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
2 unmodified lines

github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=

Mgo.sum+9