Merge pull request #35 from entireio/soph/move-to-cobra · Entire
Merge pull request #35 from entireio/soph/move-to-cobra
752b6e3→main·
Soph·2mo ago·10 files·+608 added/-589 removed
Move git-sync CLI to cobra for per-subcommand help
Changes
10
cmd/git-sync
Abootstrap.go+86
Afetch.go+80
Aflags.go+117
Mmain.go+30/-589
Aprobe.go+74
Aroot.go+66
Asyncplan.go+124
Aversion.go+19
Mgo.mod+3
Mgo.sum+9
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
package main
import (
"errors"
"fmt"
"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 errors.New("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+86
``
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
package main
import (
"errors"
"fmt"
"strings"
"entire.io/entire/git-sync"
"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 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 errors.New("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+80
package main
import (
"errors"
"fmt"
"os"
"strings"
"entire.io/entire/git-sync"
"entire.io/entire/git-sync/internal/validation"
"entire.io/entire/git-sync/unstable"
"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
}
}
Acmd/git-sync/flags.go+117
``
1 unmodified line
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
package main
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")
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, "verbose", 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)
}
req.Scope.Mappings = append(req.Scope.Mappings, gitsync.RefMapping{
Source: mapping.Source,
Target: mapping.Target,
})
}
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
//nolint:wrapcheck // cobra surfaces errors that are already user-facing (RunE-prefixed or cobra arg validation); main prints them with an "error:" prefix
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 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", nil, "source ref name to advertise as have; short names map to branches")
fs.Var(&haveHashesRaw, "have", nil, "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
`, os.Args[0], unstable.DefaultMaterializedMaxObjects)
if message == "" {
return errors.New(strings.TrimSpace(usage))
}
return fmt.Errorf("%s\n\n%s", message, usage)
}
func showUsage(cmd *cobra.Command, err error) {
fmt.Fprint(cmd.OutOrStderr(), cmd.UsageString())
fmt.Fprintf(cmd.OutOrStderr(), "\nError: %v\n", err)
}