From 7fd0de6cc47709e9cc623390a68753ffd23229aa Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Mon, 6 May 2024 20:30:18 +0200 Subject: [PATCH] refactor: switch to plugin Cmd and add tests (#44) --- Makefile | 7 +- cmd/wp-git-clone/docs.go | 58 ----- cmd/wp-git-clone/flags.go | 172 --------------- cmd/wp-git-clone/main.go | 15 +- .../templates/docs-data.yaml.tmpl | 18 -- docs/data/data.yaml | 18 +- git/clone.go | 65 +++--- git/clone_test.go | 208 +++++++++++++++--- git/config.go | 41 ++-- git/{type.go => git.go} | 12 +- git/init.go | 12 +- git/remote.go | 12 +- git/submodule.go | 18 +- git/submodule_test.go | 90 ++++---- git/utils.go | 47 ---- go.mod | 10 +- go.sum | 20 +- internal/doc/main.go | 42 ++++ plugin/impl.go | 133 ++++++----- plugin/impl_test.go | 51 +++-- plugin/plugin.go | 190 +++++++++++++++- .../config_test.go => plugin/plugin_test.go | 13 +- plugin/{utils.go => util.go} | 40 +++- 23 files changed, 658 insertions(+), 634 deletions(-) delete mode 100644 cmd/wp-git-clone/docs.go delete mode 100644 cmd/wp-git-clone/flags.go delete mode 100644 cmd/wp-git-clone/templates/docs-data.yaml.tmpl rename git/{type.go => git.go} (72%) delete mode 100644 git/utils.go create mode 100644 internal/doc/main.go rename cmd/wp-git-clone/config_test.go => plugin/plugin_test.go (61%) rename plugin/{utils.go => util.go} (57%) diff --git a/Makefile b/Makefile index adbd6d8..244a6ed 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,6 @@ GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(G XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest -GENERATE ?= XGO_VERSION := go-1.22.x XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 @@ -65,11 +64,7 @@ lint: golangci-lint .PHONY: generate generate: - $(GO) generate $(GENERATE) - -.PHONY: generate-docs -generate-docs: - $(GO) generate ./cmd/$(EXECUTABLE)/flags.go + $(GO) generate $(PACKAGES) .PHONY: test test: diff --git a/cmd/wp-git-clone/docs.go b/cmd/wp-git-clone/docs.go deleted file mode 100644 index 09f480e..0000000 --- a/cmd/wp-git-clone/docs.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build generate -// +build generate - -package main - -import ( - "bytes" - "embed" - "fmt" - "os" - "text/template" - - "github.com/thegeeklab/wp-git-clone/plugin" - "github.com/thegeeklab/wp-plugin-go/docs" - wp "github.com/thegeeklab/wp-plugin-go/plugin" - wp_template "github.com/thegeeklab/wp-plugin-go/template" - "github.com/urfave/cli/v2" -) - -//go:embed templates/docs-data.yaml.tmpl -var yamlTemplate embed.FS - -func main() { - settings := &plugin.Settings{} - app := &cli.App{ - Flags: settingsFlags(settings, wp.FlagsPluginCategory), - } - - out, err := toYAML(app) - if err != nil { - panic(err) - } - - fi, err := os.Create("../../docs/data/data-raw.yaml") - if err != nil { - panic(err) - } - defer fi.Close() - if _, err := fi.WriteString(out); err != nil { - panic(err) - } -} - -func toYAML(app *cli.App) (string, error) { - var w bytes.Buffer - - yamlTmpl, err := template.New("docs").Funcs(wp_template.LoadFuncMap()).ParseFS(yamlTemplate, "templates/docs-data.yaml.tmpl") - if err != nil { - fmt.Println(yamlTmpl) - return "", err - } - - if err := yamlTmpl.ExecuteTemplate(&w, "docs-data.yaml.tmpl", docs.GetTemplateData(app)); err != nil { - return "", err - } - - return w.String(), nil -} diff --git a/cmd/wp-git-clone/flags.go b/cmd/wp-git-clone/flags.go deleted file mode 100644 index 6ebcce7..0000000 --- a/cmd/wp-git-clone/flags.go +++ /dev/null @@ -1,172 +0,0 @@ -package main - -import ( - "github.com/thegeeklab/wp-git-clone/plugin" - "github.com/thegeeklab/wp-plugin-go/types" - "github.com/urfave/cli/v2" -) - -// settingsFlags has the cli.Flags for the plugin.Settings. -// -//go:generate go run docs.go flags.go -func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "remote", - Usage: "git remote HTTP clone url", - EnvVars: []string{"PLUGIN_REMOTE", "CI_REPO_CLONE_URL"}, - Destination: &settings.Repo.RemoteURL, - DefaultText: "$CI_REPO_CLONE_URL", - Category: category, - }, - &cli.StringFlag{ - Name: "remote-ssh", - Usage: "git remote SSH clone url", - EnvVars: []string{"PLUGIN_REMOTE_SSH", "CI_REPO_CLONE_SSH_URL"}, - Destination: &settings.Repo.RemoteSSH, - DefaultText: "$CI_REPO_CLONE_SSH_URL", - Category: category, - }, - &cli.StringFlag{ - Name: "workdir", - Usage: "path to clone git repository", - EnvVars: []string{"PLUGIN_WORKDIR", "CI_WORKSPACE"}, - Destination: &settings.WorkDir, - DefaultText: "$CI_WORKSPACE", - Category: category, - }, - &cli.StringFlag{ - Name: "sha", - Usage: "git commit sha", - EnvVars: []string{"PLUGIN_COMMIT_SHA", "CI_COMMIT_SHA"}, - Destination: &settings.Repo.CommitSha, - DefaultText: "$CI_COMMIT_SHA", - Category: category, - }, - &cli.StringFlag{ - Name: "ref", - Usage: "git commit ref", - EnvVars: []string{"PLUGIN_COMMIT_REF", "CI_COMMIT_REF"}, - Value: "refs/heads/main", - Destination: &settings.Repo.CommitRef, - Category: category, - }, - &cli.StringFlag{ - Name: "netrc.machine", - Usage: "netrc machine", - EnvVars: []string{"CI_NETRC_MACHINE"}, - Destination: &settings.Netrc.Machine, - Category: category, - }, - &cli.StringFlag{ - Name: "netrc.username", - Usage: "netrc username", - EnvVars: []string{"CI_NETRC_USERNAME"}, - Destination: &settings.Netrc.Password, - Category: category, - }, - &cli.StringFlag{ - Name: "netrc.password", - Usage: "netrc password", - EnvVars: []string{"CI_NETRC_PASSWORD"}, - Destination: &settings.Netrc.Password, - Category: category, - }, - &cli.IntFlag{ - Name: "depth", - Usage: "clone depth", - EnvVars: []string{"PLUGIN_DEPTH"}, - Destination: &settings.Depth, - Category: category, - }, - &cli.BoolFlag{ - Name: "recursive", - Usage: "clone submodules", - EnvVars: []string{"PLUGIN_RECURSIVE"}, - Value: true, - Destination: &settings.Recursive, - Category: category, - }, - &cli.BoolFlag{ - Name: "tags", - Usage: "fetch git tags during clone", - EnvVars: []string{"PLUGIN_TAGS"}, - Value: true, - Destination: &settings.Tags, - Category: category, - }, - &cli.BoolFlag{ - Name: "insecure-skip-ssl-verify", - Usage: "skip SSL verification of the remote machine", - EnvVars: []string{"PLUGIN_INSECURE_SKIP_SSL_VERIFY"}, - Destination: &settings.Repo.InsecureSkipSSLVerify, - Category: category, - }, - &cli.BoolFlag{ - Name: "submodule-update-remote", - Usage: "update remote submodules", - EnvVars: []string{"PLUGIN_SUBMODULES_UPDATE_REMOTE", "PLUGIN_SUBMODULE_UPDATE_REMOTE"}, - Destination: &settings.Repo.SubmoduleRemote, - Category: category, - }, - &cli.GenericFlag{ - Name: "submodule-override", - Usage: "JSON map of submodule overrides", - EnvVars: []string{"PLUGIN_SUBMODULE_OVERRIDE"}, - Value: &types.MapFlag{}, - Category: category, - }, - &cli.BoolFlag{ - Name: "submodule-partial", - Usage: "update submodules via partial clone (`depth=1`)", - EnvVars: []string{"PLUGIN_SUBMODULES_PARTIAL", "PLUGIN_SUBMODULE_PARTIAL"}, - Value: true, - Destination: &settings.Repo.SubmodulePartial, - Category: category, - }, - &cli.BoolFlag{ - Name: "lfs", - Usage: "whether to retrieve LFS content if available", - EnvVars: []string{"PLUGIN_LFS"}, - Value: true, - Destination: &settings.Lfs, - Category: category, - }, - &cli.StringFlag{ - Name: "branch", - Usage: "change branch name", - EnvVars: []string{"PLUGIN_BRANCH", "CI_COMMIT_BRANCH", "CI_REPO_DEFAULT_BRANCH"}, - Destination: &settings.Repo.Branch, - Category: category, - }, - &cli.BoolFlag{ - Name: "partial", - Usage: "enable/disable partial clone", - EnvVars: []string{"PLUGIN_PARTIAL"}, - Destination: &settings.Partial, - Category: category, - }, - &cli.StringFlag{ - Name: "safe-directory", - Usage: "define/replace safe directories", - EnvVars: []string{"PLUGIN_SAFE_DIRECTORY", "CI_WORKSPACE"}, - Destination: &settings.Repo.SafeDirectory, - DefaultText: "$CI_WORKSPACE", - Category: category, - }, - &cli.BoolFlag{ - Name: "use-ssh", - Usage: "using SSH for git clone", - EnvVars: []string{"PLUGIN_USE_SSH"}, - Destination: &settings.UseSSH, - Category: category, - }, - &cli.StringFlag{ - Name: "ssh-key", - Usage: "private key for SSH clone", - EnvVars: []string{"PLUGIN_SSH_KEY"}, - Destination: &settings.SSHKey, - Category: category, - }, - } -} diff --git a/cmd/wp-git-clone/main.go b/cmd/wp-git-clone/main.go index fe4c354..20a537f 100644 --- a/cmd/wp-git-clone/main.go +++ b/cmd/wp-git-clone/main.go @@ -1,11 +1,7 @@ package main import ( - "fmt" - "github.com/thegeeklab/wp-git-clone/plugin" - - wp "github.com/thegeeklab/wp-plugin-go/plugin" ) //nolint:gochecknoglobals @@ -15,14 +11,5 @@ var ( ) func main() { - settings := &plugin.Settings{} - options := wp.Options{ - Name: "wp-git-clone", - Description: "Clone git repository", - Version: BuildVersion, - VersionMetadata: fmt.Sprintf("date=%s", BuildDate), - Flags: settingsFlags(settings, wp.FlagsPluginCategory), - } - - plugin.New(options, settings).Run() + plugin.New(nil, BuildVersion, BuildDate).Run() } diff --git a/cmd/wp-git-clone/templates/docs-data.yaml.tmpl b/cmd/wp-git-clone/templates/docs-data.yaml.tmpl deleted file mode 100644 index a795bdb..0000000 --- a/cmd/wp-git-clone/templates/docs-data.yaml.tmpl +++ /dev/null @@ -1,18 +0,0 @@ ---- -{{- if .GlobalArgs }} -properties: -{{- range $v := .GlobalArgs }} - - name: {{ $v.Name }} - {{- with $v.Description }} - description: | - {{ . | ToSentence }} - {{- end }} - {{- with $v.Type }} - type: {{ . }} - {{- end }} - {{- with $v.Default }} - defaultValue: {{ . | toString }} - {{- end }} - required: {{ default false $v.Required }} -{{ end -}} -{{ end -}} diff --git a/docs/data/data.yaml b/docs/data/data.yaml index 1e07534..0ef6c61 100644 --- a/docs/data/data.yaml +++ b/docs/data/data.yaml @@ -28,9 +28,9 @@ properties: defaultValue: 0 required: false - - name: insecure_skip_ssl_verify + - name: insecure_skip_verify description: | - Skip SSL verification of the remote machine. + Skip SSL verification. Activating this option is insecure and should be avoided in most cases. type: bool @@ -44,6 +44,13 @@ properties: defaultValue: true required: false + - name: log_level + description: | + Plugin log level. + type: string + defaultValue: "info" + required: false + - name: partial description: | Enable/disable partial clone. @@ -112,13 +119,6 @@ properties: defaultValue: true required: false - - name: use_ssh - description: | - Using SSH for git clone. - type: bool - defaultValue: false - required: false - - name: workdir description: | Path to clone git repository. diff --git a/git/clone.go b/git/clone.go index 981fbe2..d80095d 100644 --- a/git/clone.go +++ b/git/clone.go @@ -3,34 +3,34 @@ package git import ( "fmt" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // FetchSource fetches the source from remote. -func FetchSource(ref string, depth int, filter string) *execabs.Cmd { +func (r *Repository) FetchSource(ref string) *types.Cmd { args := []string{ "fetch", } - if depth != 0 { - args = append(args, fmt.Sprintf("--depth=%d", depth)) + if r.Depth != 0 { + args = append(args, fmt.Sprintf("--depth=%d", r.Depth)) } - if filter != "" { - args = append(args, "--filter="+filter) + if r.Filter != "" { + args = append(args, "--filter", r.Filter) } args = append(args, "origin") args = append(args, fmt.Sprintf("+%s:", ref)) - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // FetchTags fetches the source from remote. -func FetchTags() *execabs.Cmd { +func (r *Repository) FetchTags() *types.Cmd { args := []string{ "fetch", "--tags", @@ -38,27 +38,25 @@ func FetchTags() *execabs.Cmd { "origin", } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // FetchLFS fetches lfs. -func FetchLFS() *execabs.Cmd { +func (r *Repository) FetchLFS() *types.Cmd { args := []string{ "lfs", "fetch", } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // CheckoutHead handles head checkout. -func CheckoutHead() *execabs.Cmd { +func (r *Repository) CheckoutHead() *types.Cmd { args := []string{ "checkout", "--force", @@ -66,36 +64,33 @@ func CheckoutHead() *execabs.Cmd { "FETCH_HEAD", } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // CheckoutSha handles commit checkout. -func CheckoutSha(repo Repository) *execabs.Cmd { +func (r *Repository) CheckoutSha() *types.Cmd { args := []string{ "reset", "--hard", "--quiet", - repo.CommitSha, + r.CommitSha, } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // CheckoutLFS handles commit checkout. -func CheckoutLFS() *execabs.Cmd { +func (r *Repository) CheckoutLFS() *types.Cmd { args := []string{ "lfs", "checkout", } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } diff --git a/git/clone_test.go b/git/clone_test.go index d46823f..46ac00b 100644 --- a/git/clone_test.go +++ b/git/clone_test.go @@ -2,62 +2,206 @@ package git import ( "testing" + + "github.com/stretchr/testify/require" ) -// TestFetch tests if the arguments to `git fetch` are constructed properly. -func TestFetch(t *testing.T) { +func TestFetchSource(t *testing.T) { testdata := []struct { - ref string + name string + repo *Repository tags bool depth int - exp []string + want []string }{ { - "refs/heads/master", - false, - 0, - []string{ - "/usr/bin/git", + name: "fetch main without tags", + repo: &Repository{ + CommitRef: "refs/heads/main", + }, + tags: false, + depth: 0, + want: []string{ + gitBin, "fetch", "origin", - "+refs/heads/master:", + "+refs/heads/main:", }, }, { - "refs/heads/master", - false, - 50, - []string{ - "/usr/bin/git", + name: "fetch main without tags with depth", + repo: &Repository{ + CommitRef: "refs/heads/main", + Depth: 50, + }, + tags: false, + depth: 50, + want: []string{ + gitBin, "fetch", "--depth=50", "origin", - "+refs/heads/master:", + "+refs/heads/main:", }, }, { - "refs/heads/master", - true, - 100, - []string{ - "/usr/bin/git", + name: "fetch main with tags and depth", + repo: &Repository{ + CommitRef: "refs/heads/main", + Depth: 100, + }, + tags: true, + depth: 100, + want: []string{ + gitBin, "fetch", "--depth=100", "origin", - "+refs/heads/master:", + "+refs/heads/main:", + }, + }, + } + + for _, tt := range testdata { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.FetchSource(tt.repo.CommitRef) + require.Equal(t, tt.want, cmd.Args) + }) + } +} + +func TestFetchTags(t *testing.T) { + testdata := []struct { + name string + repo *Repository + tags bool + depth int + want []string + }{ + { + name: "fetch tags", + repo: &Repository{}, + tags: true, + want: []string{ + gitBin, + "fetch", + "--tags", + "--quiet", + "origin", + }, + }, + } + + for _, tt := range testdata { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.FetchTags() + require.Equal(t, tt.want, cmd.Args) + }) + } +} + +func TestFetchLFS(t *testing.T) { + testdata := []struct { + name string + repo *Repository + want []string + }{ + { + name: "fetch LFS", + repo: &Repository{}, + want: []string{ + gitBin, + "lfs", + "fetch", + }, + }, + } + + for _, tt := range testdata { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.FetchLFS() + require.Equal(t, tt.want, cmd.Args) + }) + } +} + +func TestCheckoutHead(t *testing.T) { + testdata := []struct { + name string + repo *Repository + want []string + }{ + { + name: "checkout head", + repo: &Repository{}, + want: []string{ + gitBin, + "checkout", + "--force", + "--quiet", + "FETCH_HEAD", + }, + }, + } + + for _, tt := range testdata { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.CheckoutHead() + require.Equal(t, tt.want, cmd.Args) + }) + } +} + +func TestCheckoutSha(t *testing.T) { + testdata := []struct { + name string + repo *Repository + want []string + }{ + { + name: "checkout sha", + repo: &Repository{ + CommitSha: "abcd1234", + }, + want: []string{ + gitBin, + "reset", + "--hard", + "--quiet", + "abcd1234", + }, + }, + } + + for _, tt := range testdata { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.CheckoutSha() + require.Equal(t, tt.want, cmd.Args) + }) + } +} + +func TestCheckoutLFS(t *testing.T) { + testdata := []struct { + name string + repo *Repository + want []string + }{ + { + name: "checkout LFS with no arguments", + repo: &Repository{}, + want: []string{ + gitBin, + "lfs", + "checkout", }, }, } - for _, td := range testdata { - c := FetchSource(td.ref, td.depth, "") - if len(c.Args) != len(td.exp) { - t.Errorf("Expected: %s, got %s", td.exp, c.Args) - } - for i := range c.Args { - if c.Args[i] != td.exp[i] { - t.Errorf("Expected: %s, got %s", td.exp, c.Args) - } - } + for _, tt := range testdata { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.CheckoutLFS() + require.Equal(t, tt.want, cmd.Args) + }) } } diff --git a/git/config.go b/git/config.go index cd7e744..28ec184 100644 --- a/git/config.go +++ b/git/config.go @@ -4,43 +4,42 @@ import ( "fmt" "strconv" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // ConfigSSLVerify disables globally the git ssl verification. -func ConfigSSLVerify(repo Repository) *execabs.Cmd { +func (r *Repository) ConfigSSLVerify(skipVerify bool) *types.Cmd { args := []string{ "config", "--global", "http.sslVerify", - strconv.FormatBool(!repo.InsecureSkipSSLVerify), + strconv.FormatBool(!skipVerify), } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // ConfigSafeDirectory disables globally the git ssl verification. -func ConfigSafeDirectory(repo Repository) *execabs.Cmd { +func (r *Repository) ConfigSafeDirectory() *types.Cmd { args := []string{ "config", "--global", "--replace-all", "safe.directory", - repo.SafeDirectory, + r.SafeDirectory, } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // ConfigRemapSubmodule returns a git command that, when executed configures git to // remap submodule urls. -func ConfigRemapSubmodule(name, url string) *execabs.Cmd { +func (r *Repository) ConfigRemapSubmodule(name, url string) *types.Cmd { args := []string{ "config", "--global", @@ -48,14 +47,13 @@ func ConfigRemapSubmodule(name, url string) *execabs.Cmd { url, } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } // ConfigSSHCommand sets custom SSH key. -func ConfigSSHCommand(sshKey string) *execabs.Cmd { +func (r *Repository) ConfigSSHCommand(sshKey string) *types.Cmd { args := []string{ "config", "--global", @@ -63,8 +61,7 @@ func ConfigSSHCommand(sshKey string) *execabs.Cmd { "ssh -i " + sshKey, } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } diff --git a/git/type.go b/git/git.go similarity index 72% rename from git/type.go rename to git/git.go index 2b5a713..fe38ad0 100644 --- a/git/type.go +++ b/git/git.go @@ -1,7 +1,5 @@ package git -const gitBin = "/usr/bin/git" - type Repository struct { RemoteURL string RemoteSSH string @@ -12,7 +10,11 @@ type Repository struct { SubmoduleRemote bool SubmodulePartial bool - InsecureSkipSSLVerify bool - SafeDirectory string - InitExists bool + SafeDirectory string + WorkDir string + IsEmpty bool + Filter string + Depth int } + +const gitBin = "/usr/bin/git" diff --git a/git/init.go b/git/init.go index bd3b9c9..660bbc2 100644 --- a/git/init.go +++ b/git/init.go @@ -1,19 +1,19 @@ package git import ( + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // RemoteRemove drops the defined remote from a git repo. -func Init(repo Repository) *execabs.Cmd { +func (r *Repository) Init() *types.Cmd { args := []string{ "init", "-b", - repo.Branch, + r.Branch, } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } diff --git a/git/remote.go b/git/remote.go index 148eaf8..5b7d8c6 100644 --- a/git/remote.go +++ b/git/remote.go @@ -1,20 +1,20 @@ package git import ( + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // RemoteAdd adds an additional remote to a git repo. -func RemoteAdd(url string) *execabs.Cmd { +func (r *Repository) RemoteAdd() *types.Cmd { args := []string{ "remote", "add", "origin", - url, + r.RemoteURL, } - return execabs.Command( - gitBin, - args..., - ) + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } diff --git a/git/submodule.go b/git/submodule.go index 3795654..f8771b1 100644 --- a/git/submodule.go +++ b/git/submodule.go @@ -1,11 +1,12 @@ package git import ( + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // SubmoduleUpdate recursively initializes and updates submodules. -func SubmoduleUpdate(repo Repository) *execabs.Cmd { +func (r *Repository) SubmoduleUpdate() *types.Cmd { args := []string{ "submodule", "update", @@ -13,18 +14,15 @@ func SubmoduleUpdate(repo Repository) *execabs.Cmd { "--recursive", } - if repo.SubmodulePartial { + if r.SubmodulePartial { args = append(args, "--depth=1", "--recommend-shallow") } - cmd := execabs.Command( - gitBin, - args..., - ) - - if repo.SubmoduleRemote { - cmd.Args = append(cmd.Args, "--remote") + if r.SubmoduleRemote { + args = append(args, "--remote") } - return cmd + return &types.Cmd{ + Cmd: execabs.Command(gitBin, args...), + } } diff --git a/git/submodule_test.go b/git/submodule_test.go index 661d687..56ab769 100644 --- a/git/submodule_test.go +++ b/git/submodule_test.go @@ -2,19 +2,25 @@ package git import ( "testing" + + "github.com/stretchr/testify/require" ) // TestUpdateSubmodules tests if the arguments to `git submodule update` // are constructed properly. func TestUpdateSubmodules(t *testing.T) { tests := []struct { - partial bool - exp []string + name string + repo *Repository + want []string }{ { - false, - []string{ - "/usr/bin/git", + name: "full submodule update", + repo: &Repository{ + SubmodulePartial: false, + }, + want: []string{ + gitBin, "submodule", "update", "--init", @@ -22,9 +28,12 @@ func TestUpdateSubmodules(t *testing.T) { }, }, { - true, - []string{ - "/usr/bin/git", + name: "partial submodule update", + repo: &Repository{ + SubmodulePartial: true, + }, + want: []string{ + gitBin, "submodule", "update", "--init", @@ -33,35 +42,13 @@ func TestUpdateSubmodules(t *testing.T) { "--recommend-shallow", }, }, - } - for _, tt := range tests { - repo := Repository{ - SubmoduleRemote: false, - SubmodulePartial: tt.partial, - } - - c := SubmoduleUpdate(repo) - if len(c.Args) != len(tt.exp) { - t.Errorf("Expected: %s, got %s", tt.exp, c.Args) - } - - for i := range c.Args { - if c.Args[i] != tt.exp[i] { - t.Errorf("Expected: %s, got %s", tt.exp, c.Args) - } - } - } -} - -// TestUpdateSubmodules tests if the arguments to `git submodule update` -// are constructed properly. -func TestUpdateSubmodulesRemote(t *testing.T) { - tests := []struct { - exp []string - }{ { - []string{ - "/usr/bin/git", + name: "submodule update with remote", + repo: &Repository{ + SubmoduleRemote: true, + }, + want: []string{ + gitBin, "submodule", "update", "--init", @@ -70,31 +57,28 @@ func TestUpdateSubmodulesRemote(t *testing.T) { }, }, { - []string{ - "/usr/bin/git", + name: "submodule update with remote and partial", + repo: &Repository{ + SubmoduleRemote: true, + SubmodulePartial: true, + }, + want: []string{ + gitBin, "submodule", "update", "--init", "--recursive", + "--depth=1", + "--recommend-shallow", "--remote", }, }, } - for _, tt := range tests { - repo := Repository{ - SubmoduleRemote: true, - SubmodulePartial: false, - } - c := SubmoduleUpdate(repo) - if len(c.Args) != len(tt.exp) { - t.Errorf("Expected: %s, got %s", tt.exp, c.Args) - } - - for i := range c.Args { - if c.Args[i] != tt.exp[i] { - t.Errorf("Expected: %s, got %s", tt.exp, c.Args) - } - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.repo.SubmoduleUpdate() + require.Equal(t, tt.want, cmd.Args) + }) } } diff --git a/git/utils.go b/git/utils.go deleted file mode 100644 index 4b36b9a..0000000 --- a/git/utils.go +++ /dev/null @@ -1,47 +0,0 @@ -package git - -import ( - "fmt" - "os" - "os/user" - "path/filepath" -) - -const ( - netrcFile = ` -machine %s -login %s -password %s -` -) - -const ( - strictFilePerm = 0o600 -) - -// WriteNetrc writes the netrc file. -func WriteNetrc(machine, login, password string) error { - netrcContent := fmt.Sprintf( - netrcFile, - machine, - login, - password, - ) - - home := "/root" - - if currentUser, err := user.Current(); err == nil { - home = currentUser.HomeDir - } - - netpath := filepath.Join( - home, - ".netrc", - ) - - return os.WriteFile( - netpath, - []byte(netrcContent), - strictFilePerm, - ) -} diff --git a/go.mod b/go.mod index e0a624a..9aead93 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,16 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/rs/zerolog v1.32.0 github.com/stretchr/testify v1.9.0 - github.com/thegeeklab/wp-plugin-go v1.7.1 - github.com/urfave/cli/v2 v2.27.1 - golang.org/x/sys v0.19.0 + github.com/thegeeklab/wp-plugin-go/v2 v2.3.0 + github.com/urfave/cli/v2 v2.27.2 + golang.org/x/sys v0.20.0 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect @@ -29,7 +29,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 63ef955..01a97ea 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -48,12 +48,12 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/thegeeklab/wp-plugin-go v1.7.1 h1:zfR/rfNPuyVhXJu1fsLfp4+Mz2pTf6WwW/mIqw9750I= -github.com/thegeeklab/wp-plugin-go v1.7.1/go.mod h1:Ixi5plt9tpFGTu6yc/Inm5DcDpp3xPTeohfr86gf2EU= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/thegeeklab/wp-plugin-go/v2 v2.3.0 h1:9LOdITzjxEEbgcH9yU0tDZL0dcDZzNYzbk2+hZ5QsBA= +github.com/thegeeklab/wp-plugin-go/v2 v2.3.0/go.mod h1:I/3M/4OPvr4FFS+s0aaImpX1llA/lS2KC6Bnp+qzsCs= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -78,8 +78,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= diff --git a/internal/doc/main.go b/internal/doc/main.go new file mode 100644 index 0000000..f8f36df --- /dev/null +++ b/internal/doc/main.go @@ -0,0 +1,42 @@ +//go:build generate +// +build generate + +package main + +import ( + "context" + "flag" + "net/http" + "os" + "time" + + "github.com/thegeeklab/wp-git-clone/plugin" + "github.com/thegeeklab/wp-plugin-go/v2/docs" + wp_template "github.com/thegeeklab/wp-plugin-go/v2/template" +) + +func main() { + tmpl := "https://raw.githubusercontent.com/thegeeklab/woodpecker-plugins/main/templates/docs-data.yaml.tmpl" + client := http.Client{ + Timeout: 30 * time.Second, + } + + p := plugin.New(nil) + + out, err := wp_template.Render(context.Background(), client, tmpl, docs.GetTemplateData(p.App)) + if err != nil { + panic(err) + } + + outputFile := flag.String("output", "", "Output file path") + flag.Parse() + + if *outputFile == "" { + panic("no output file specified") + } + + err = os.WriteFile(*outputFile, []byte(out), 0o644) + if err != nil { + panic(err) + } +} diff --git a/plugin/impl.go b/plugin/impl.go index 38a21ed..4c387b8 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -8,14 +8,12 @@ import ( "io" "os" "path/filepath" - "strings" "time" "github.com/rs/zerolog/log" - "github.com/thegeeklab/wp-git-clone/git" - "github.com/thegeeklab/wp-plugin-go/trace" - "github.com/thegeeklab/wp-plugin-go/types" - "golang.org/x/sys/execabs" + "github.com/thegeeklab/wp-plugin-go/v2/file" + "github.com/thegeeklab/wp-plugin-go/v2/types" + "github.com/thegeeklab/wp-plugin-go/v2/util" ) const ( @@ -48,22 +46,24 @@ func (p *Plugin) run(ctx context.Context) error { // Validate handles the settings validation of the plugin. func (p *Plugin) Validate() error { + var err error + // This default cannot be set in the cli flag, as the CI_* environment variables // can be set empty, resulting in an empty default value. if p.Settings.Repo.Branch == "" { p.Settings.Repo.Branch = "main" } - if p.Settings.WorkDir == "" { - var err error - if p.Settings.WorkDir, err = os.Getwd(); err != nil { - return err + if p.Settings.Repo.WorkDir == "" { + p.Settings.Repo.WorkDir, err = os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) } } if p.Settings.Partial { - p.Settings.Depth = 1 - p.Settings.Filter = "tree:0" + p.Settings.Repo.Depth = 1 + p.Settings.Repo.Filter = "tree:0" } return nil @@ -71,76 +71,93 @@ func (p *Plugin) Validate() error { // Execute provides the implementation of the plugin. func (p *Plugin) Execute() error { - cmds := make([]*execabs.Cmd, 0) + var err error - // Handle init - initPath := filepath.Join(p.Settings.WorkDir, ".git") + homeDir := util.GetUserHomeDir() + batchCmd := make([]*types.Cmd, 0) - if err := os.MkdirAll(p.Settings.WorkDir, os.ModePerm); err != nil { - return err + fmt.Println(p.Settings.Repo.WorkDir) + + // Handle repo initialization. + if err := os.MkdirAll(p.Settings.Repo.WorkDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create working directory: %w", err) } - //nolint:nestif - if _, err := os.Stat(initPath); os.IsNotExist(err) { - cmds = append(cmds, git.ConfigSafeDirectory(p.Settings.Repo)) + p.Settings.Repo.IsEmpty, err = file.IsDirEmpty(p.Settings.Repo.WorkDir) + if err != nil { + return fmt.Errorf("failed to check working directory: %w", err) + } - if err := p.execCmd(git.Init(p.Settings.Repo), new(bytes.Buffer)); err != nil { - return err - } + isDir, err := file.IsDir(filepath.Join(p.Settings.Repo.WorkDir, ".git")) + if err != nil { + return fmt.Errorf("failed to check working directory: %w", err) + } - if p.Settings.UseSSH { - cmds = append(cmds, git.RemoteAdd(p.Settings.Repo.RemoteSSH)) - if p.Settings.SSHKey != "" { - cmds = append(cmds, git.ConfigSSHCommand(p.Settings.SSHKey)) - } - } else { - cmds = append(cmds, git.RemoteAdd(p.Settings.Repo.RemoteURL)) + if !isDir { + batchCmd = append(batchCmd, p.Settings.Repo.Init()) + batchCmd = append(batchCmd, p.Settings.Repo.RemoteAdd()) + + if p.Settings.SSHKey != "" { + batchCmd = append(batchCmd, p.Settings.Repo.ConfigSSHCommand(p.Settings.SSHKey)) } } - if p.Settings.Repo.InsecureSkipSSLVerify { - cmds = append(cmds, git.ConfigSSLVerify(p.Settings.Repo)) - } + batchCmd = append(batchCmd, p.Settings.Repo.ConfigSSLVerify(p.Network.InsecureSkipVerify)) - if err := git.WriteNetrc(p.Settings.Netrc.Machine, p.Settings.Netrc.Login, p.Settings.Netrc.Password); err != nil { + netrc := p.Settings.Netrc + if err := WriteNetrc(homeDir, netrc.Machine, netrc.Login, netrc.Password); err != nil { return err } // Handle clone - if p.Settings.Repo.CommitSha == "" { // fetch and checkout by ref log.Info().Msg("no commit information: using head checkout") - cmds = append(cmds, git.FetchSource(p.Settings.Repo.CommitRef, p.Settings.Depth, p.Settings.Filter)) - cmds = append(cmds, git.CheckoutHead()) + batchCmd = append(batchCmd, p.Settings.Repo.FetchSource(p.Settings.Repo.CommitRef)) + batchCmd = append(batchCmd, p.Settings.Repo.CheckoutHead()) } else { - cmds = append(cmds, git.FetchSource(p.Settings.Repo.CommitSha, p.Settings.Depth, p.Settings.Filter)) - cmds = append(cmds, git.CheckoutSha(p.Settings.Repo)) + batchCmd = append(batchCmd, p.Settings.Repo.FetchSource(p.Settings.Repo.CommitSha)) + batchCmd = append(batchCmd, p.Settings.Repo.CheckoutSha()) } if p.Settings.Tags { - cmds = append(cmds, git.FetchTags()) + batchCmd = append(batchCmd, p.Settings.Repo.FetchTags()) } for name, submoduleURL := range p.Settings.Repo.Submodules { - cmds = append(cmds, git.ConfigRemapSubmodule(name, submoduleURL)) + batchCmd = append(batchCmd, p.Settings.Repo.ConfigRemapSubmodule(name, submoduleURL)) } if p.Settings.Recursive { - cmds = append(cmds, git.SubmoduleUpdate(p.Settings.Repo)) + batchCmd = append(batchCmd, p.Settings.Repo.SubmoduleUpdate()) } if p.Settings.Lfs { - cmds = append(cmds, git.FetchLFS()) - cmds = append(cmds, git.CheckoutLFS()) + batchCmd = append(batchCmd, p.Settings.Repo.FetchLFS()) + batchCmd = append(batchCmd, p.Settings.Repo.CheckoutLFS()) } - for _, cmd := range cmds { - log.Debug().Msgf("+ %s", strings.Join(cmd.Args, " ")) - + for _, cmd := range batchCmd { buf := new(bytes.Buffer) - err := p.execCmd(cmd, buf) + + // Don' set GIT_TERMINAL_PROMPT=0 as it prevents git from loading .netrc + defaultEnvVars := []string{ + "GIT_LFS_SKIP_SMUDGE=1", // prevents git-lfs from retrieving any LFS files + } + + if p.Settings.Home != "" { + if _, err := os.Stat(p.Settings.Home); !os.IsNotExist(err) { + defaultEnvVars = append(defaultEnvVars, fmt.Sprintf("HOME=%s", p.Settings.Home)) + } + } + + cmd.Env = append(os.Environ(), defaultEnvVars...) + cmd.Stdout = io.MultiWriter(os.Stdout, buf) + cmd.Stderr = io.MultiWriter(os.Stderr, buf) + cmd.Dir = p.Settings.Repo.WorkDir + + err := cmd.Run() switch { case err != nil && shouldRetry(buf.String()): @@ -163,25 +180,3 @@ func (p *Plugin) FlagsFromContext() error { return nil } - -func (p *Plugin) execCmd(cmd *execabs.Cmd, buf *bytes.Buffer) error { - // Don' set GIT_TERMINAL_PROMPT=0 as it prevents git from loading .netrc - defaultEnvVars := []string{ - "GIT_LFS_SKIP_SMUDGE=1", // prevents git-lfs from retrieving any LFS files - } - - if p.Settings.Home != "" { - if _, err := os.Stat(p.Settings.Home); !os.IsNotExist(err) { - defaultEnvVars = append(defaultEnvVars, fmt.Sprintf("HOME=%s", p.Settings.Home)) - } - } - - cmd.Env = append(os.Environ(), defaultEnvVars...) - cmd.Stdout = io.MultiWriter(os.Stdout, buf) - cmd.Stderr = io.MultiWriter(os.Stderr, buf) - cmd.Dir = p.Settings.WorkDir - - trace.Cmd(cmd) - - return cmd.Run() -} diff --git a/plugin/impl_test.go b/plugin/impl_test.go index 0674f05..cd2731d 100644 --- a/plugin/impl_test.go +++ b/plugin/impl_test.go @@ -1,6 +1,7 @@ package plugin import ( + "context" "os" "path/filepath" "testing" @@ -28,19 +29,18 @@ func TestClone(t *testing.T) { dir := setup() defer teardown(dir) - plugin := Plugin{ - Settings: &Settings{ - Repo: git.Repository{ - RemoteURL: tt.clone, - CommitRef: tt.ref, - CommitSha: tt.commit, - Branch: "main", - }, - Home: "/tmp", + plugin := New(func(_ context.Context) error { return nil }) + plugin.Settings = &Settings{ + Repo: git.Repository{ + RemoteURL: tt.clone, + CommitRef: tt.ref, + CommitSha: tt.commit, + Branch: "main", WorkDir: filepath.Join(dir, tt.path), - Recursive: tt.recursive, - Lfs: tt.lfs, }, + Home: "/tmp", + Recursive: tt.recursive, + Lfs: tt.lfs, } if err := plugin.Execute(); err != nil { @@ -48,14 +48,14 @@ func TestClone(t *testing.T) { } if tt.data != "" { - data := readFile(plugin.Settings.WorkDir, tt.file) + data := readFile(plugin.Settings.Repo.WorkDir, tt.file) if data != tt.data { t.Errorf("Expected %s to contain [%s]. Got [%s].", tt.file, tt.data, data) } } if tt.dataSize != 0 { - size := getFileSize(plugin.Settings.WorkDir, tt.file) + size := getFileSize(plugin.Settings.Repo.WorkDir, tt.file) if size != tt.dataSize { t.Errorf("Expected %s size to be [%d]. Got [%d].", tt.file, tt.dataSize, size) } @@ -71,19 +71,18 @@ func TestCloneNonEmpty(t *testing.T) { defer teardown(dir) for _, tt := range getCommits() { - plugin := Plugin{ - Settings: &Settings{ - Repo: git.Repository{ - RemoteURL: tt.clone, - CommitRef: tt.ref, - CommitSha: tt.commit, - Branch: "main", - }, - Home: "/tmp", + plugin := New(func(_ context.Context) error { return nil }) + plugin.Settings = &Settings{ + Repo: git.Repository{ + RemoteURL: tt.clone, + CommitRef: tt.ref, + CommitSha: tt.commit, + Branch: "main", WorkDir: filepath.Join(dir, tt.path), - Recursive: tt.recursive, - Lfs: tt.lfs, }, + Home: "/tmp", + Recursive: tt.recursive, + Lfs: tt.lfs, } if err := plugin.Execute(); err != nil { @@ -91,7 +90,7 @@ func TestCloneNonEmpty(t *testing.T) { } if tt.data != "" { - data := readFile(plugin.Settings.WorkDir, tt.file) + data := readFile(plugin.Settings.Repo.WorkDir, tt.file) if data != tt.data { t.Errorf("Expected %s to contain [%q]. Got [%q].", tt.file, tt.data, data) @@ -100,7 +99,7 @@ func TestCloneNonEmpty(t *testing.T) { } if tt.dataSize != 0 { - size := getFileSize(plugin.Settings.WorkDir, tt.file) + size := getFileSize(plugin.Settings.Repo.WorkDir, tt.file) if size != tt.dataSize { t.Errorf("Expected %s size to be [%d]. Got [%d].", tt.file, tt.dataSize, size) } diff --git a/plugin/plugin.go b/plugin/plugin.go index 38c73a9..38017eb 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,10 +1,16 @@ package plugin import ( + "fmt" + "github.com/thegeeklab/wp-git-clone/git" - wp "github.com/thegeeklab/wp-plugin-go/plugin" + wp "github.com/thegeeklab/wp-plugin-go/v2/plugin" + "github.com/thegeeklab/wp-plugin-go/v2/types" + "github.com/urfave/cli/v2" ) +//go:generate go run ../internal/doc/main.go -output=../docs/data/data-raw.yaml + // Plugin implements provide the plugin. type Plugin struct { *wp.Plugin @@ -19,30 +25,192 @@ type Netrc struct { // Settings for the plugin. type Settings struct { - Depth int Recursive bool Tags bool Lfs bool Partial bool - Filter string - UseSSH bool - SSHKey string Home string - WorkDir string + SSHKey string Netrc Netrc Repo git.Repository } -func New(options wp.Options, settings *Settings) *Plugin { - p := &Plugin{} +func New(e wp.ExecuteFunc, build ...string) *Plugin { + p := &Plugin{ + Settings: &Settings{}, + } + + options := wp.Options{ + Name: "wp-git-clone", + Description: "Clone git repository", + Flags: Flags(p.Settings, wp.FlagsPluginCategory), + Execute: p.run, + HideWoodpeckerFlags: true, + } + + if len(build) > 0 { + options.Version = build[0] + } + + if len(build) > 1 { + options.VersionMetadata = fmt.Sprintf("date=%s", build[1]) + } - if options.Execute == nil { - options.Execute = p.run + if e != nil { + options.Execute = e } p.Plugin = wp.New(options) - p.Settings = settings return p } + +// Flags returns a slice of CLI flags for the plugin. +func Flags(settings *Settings, category string) []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "remote", + Usage: "git remote HTTP clone url", + EnvVars: []string{"PLUGIN_REMOTE", "CI_REPO_CLONE_URL"}, + Destination: &settings.Repo.RemoteURL, + DefaultText: "$CI_REPO_CLONE_URL", + Category: category, + }, + &cli.StringFlag{ + Name: "remote-ssh", + Usage: "git remote SSH clone url", + EnvVars: []string{"PLUGIN_REMOTE_SSH", "CI_REPO_CLONE_SSH_URL"}, + Destination: &settings.Repo.RemoteSSH, + DefaultText: "$CI_REPO_CLONE_SSH_URL", + Category: category, + }, + &cli.StringFlag{ + Name: "workdir", + Usage: "path to clone git repository", + EnvVars: []string{"PLUGIN_WORKDIR", "CI_WORKSPACE"}, + Destination: &settings.Repo.WorkDir, + DefaultText: "$CI_WORKSPACE", + Category: category, + }, + &cli.StringFlag{ + Name: "sha", + Usage: "git commit sha", + EnvVars: []string{"PLUGIN_COMMIT_SHA", "CI_COMMIT_SHA"}, + Destination: &settings.Repo.CommitSha, + DefaultText: "$CI_COMMIT_SHA", + Category: category, + }, + &cli.StringFlag{ + Name: "ref", + Usage: "git commit ref", + EnvVars: []string{"PLUGIN_COMMIT_REF", "CI_COMMIT_REF"}, + Value: "refs/heads/main", + Destination: &settings.Repo.CommitRef, + Category: category, + }, + &cli.StringFlag{ + Name: "netrc.machine", + Usage: "netrc machine", + EnvVars: []string{"CI_NETRC_MACHINE"}, + Destination: &settings.Netrc.Machine, + Category: category, + }, + &cli.StringFlag{ + Name: "netrc.username", + Usage: "netrc username", + EnvVars: []string{"CI_NETRC_USERNAME"}, + Destination: &settings.Netrc.Password, + Category: category, + }, + &cli.StringFlag{ + Name: "netrc.password", + Usage: "netrc password", + EnvVars: []string{"CI_NETRC_PASSWORD"}, + Destination: &settings.Netrc.Password, + Category: category, + }, + &cli.IntFlag{ + Name: "depth", + Usage: "clone depth", + EnvVars: []string{"PLUGIN_DEPTH"}, + Destination: &settings.Repo.Depth, + Category: category, + }, + &cli.BoolFlag{ + Name: "recursive", + Usage: "clone submodules", + EnvVars: []string{"PLUGIN_RECURSIVE"}, + Value: true, + Destination: &settings.Recursive, + Category: category, + }, + &cli.BoolFlag{ + Name: "tags", + Usage: "fetch git tags during clone", + EnvVars: []string{"PLUGIN_TAGS"}, + Value: true, + Destination: &settings.Tags, + Category: category, + }, + &cli.BoolFlag{ + Name: "submodule-update-remote", + Usage: "update remote submodules", + EnvVars: []string{"PLUGIN_SUBMODULES_UPDATE_REMOTE", "PLUGIN_SUBMODULE_UPDATE_REMOTE"}, + Destination: &settings.Repo.SubmoduleRemote, + Category: category, + }, + &cli.GenericFlag{ + Name: "submodule-override", + Usage: "JSON map of submodule overrides", + EnvVars: []string{"PLUGIN_SUBMODULE_OVERRIDE"}, + Value: &types.MapFlag{}, + Category: category, + }, + &cli.BoolFlag{ + Name: "submodule-partial", + Usage: "update submodules via partial clone (`depth=1`)", + EnvVars: []string{"PLUGIN_SUBMODULES_PARTIAL", "PLUGIN_SUBMODULE_PARTIAL"}, + Value: true, + Destination: &settings.Repo.SubmodulePartial, + Category: category, + }, + &cli.BoolFlag{ + Name: "lfs", + Usage: "whether to retrieve LFS content if available", + EnvVars: []string{"PLUGIN_LFS"}, + Value: true, + Destination: &settings.Lfs, + Category: category, + }, + &cli.StringFlag{ + Name: "branch", + Usage: "change branch name", + EnvVars: []string{"PLUGIN_BRANCH", "CI_COMMIT_BRANCH", "CI_REPO_DEFAULT_BRANCH"}, + Destination: &settings.Repo.Branch, + Category: category, + }, + &cli.BoolFlag{ + Name: "partial", + Usage: "enable/disable partial clone", + EnvVars: []string{"PLUGIN_PARTIAL"}, + Destination: &settings.Partial, + Category: category, + }, + &cli.StringFlag{ + Name: "safe-directory", + Usage: "define/replace safe directories", + EnvVars: []string{"PLUGIN_SAFE_DIRECTORY", "CI_WORKSPACE"}, + Destination: &settings.Repo.SafeDirectory, + DefaultText: "$CI_WORKSPACE", + Category: category, + }, + &cli.StringFlag{ + Name: "ssh-key", + Usage: "private key for SSH clone", + EnvVars: []string{"PLUGIN_SSH_KEY"}, + Destination: &settings.SSHKey, + Category: category, + }, + } +} diff --git a/cmd/wp-git-clone/config_test.go b/plugin/plugin_test.go similarity index 61% rename from cmd/wp-git-clone/config_test.go rename to plugin/plugin_test.go index 3e9c30b..ea6b531 100644 --- a/cmd/wp-git-clone/config_test.go +++ b/plugin/plugin_test.go @@ -1,12 +1,10 @@ -package main +package plugin import ( "context" "testing" "github.com/stretchr/testify/assert" - "github.com/thegeeklab/wp-git-clone/plugin" - wp "github.com/thegeeklab/wp-plugin-go/plugin" ) func Test_pluginOptions(t *testing.T) { @@ -29,14 +27,7 @@ func Test_pluginOptions(t *testing.T) { t.Setenv(key, value) } - settings := &plugin.Settings{} - options := wp.Options{ - Name: "wp-git-clone", - Flags: settingsFlags(settings, wp.FlagsPluginCategory), - Execute: func(_ context.Context) error { return nil }, - } - - got := plugin.New(options, settings) + got := New(func(_ context.Context) error { return nil }) _ = got.App.Run([]string{"wp-git-clone"}) _ = got.Validate() diff --git a/plugin/utils.go b/plugin/util.go similarity index 57% rename from plugin/utils.go rename to plugin/util.go index 37c1905..23d2ff1 100644 --- a/plugin/utils.go +++ b/plugin/util.go @@ -1,16 +1,26 @@ package plugin import ( + "fmt" "os" + "path/filepath" "strings" "time" "github.com/cenkalti/backoff/v4" "github.com/rs/zerolog/log" - "github.com/thegeeklab/wp-plugin-go/trace" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) +const ( + netrcFile = `machine %s +login %s +password %s +` + strictFilePerm = 0o600 +) + // shouldRetry returns true if the command should be re-executed. Currently // this only returns true if the remote ref does not exist. func shouldRetry(s string) bool { @@ -25,19 +35,19 @@ func newBackoff(maxRetries uint64) backoff.BackOff { return backoff.WithMaxRetries(b, maxRetries) } -func retryCmd(cmd *execabs.Cmd) error { +func retryCmd(cmd *types.Cmd) error { backoffOps := func() error { // copy the original command //nolint:gosec - retry := execabs.Command(cmd.Args[0], cmd.Args[1:]...) - retry.Dir = cmd.Dir + retry := &types.Cmd{ + Cmd: execabs.Command(cmd.Cmd.Path, cmd.Cmd.Args...), + } retry.Env = cmd.Env - retry.Stdout = os.Stdout - retry.Stderr = os.Stderr - - trace.Cmd(cmd) + retry.Stdout = cmd.Stdout + retry.Stderr = cmd.Stderr + retry.Dir = cmd.Dir - return cmd.Run() + return retry.Run() } backoffLog := func(err error, delay time.Duration) { log.Error().Msgf("failed to find remote ref: %v: retry in %s", err, delay.Truncate(time.Second)) @@ -45,3 +55,15 @@ func retryCmd(cmd *execabs.Cmd) error { return backoff.RetryNotify(backoffOps, newBackoff(daemonBackoffMaxRetries), backoffLog) } + +// WriteNetrc writes the netrc file. +func WriteNetrc(path, machine, login, password string) error { + netrcPath := filepath.Join(path, ".netrc") + netrcContent := fmt.Sprintf(netrcFile, machine, login, password) + + if err := os.WriteFile(netrcPath, []byte(netrcContent), strictFilePerm); err != nil { + return fmt.Errorf("failed to create .netrc file: %w", err) + } + + return nil +}