diff --git a/Makefile b/Makefile index b5a74fd..77626cd 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,12 +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-action/docs.go b/cmd/wp-git-action/docs.go deleted file mode 100644 index 41423f8..0000000 --- a/cmd/wp-git-action/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-action/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-action/flags.go b/cmd/wp-git-action/flags.go deleted file mode 100644 index a435e1f..0000000 --- a/cmd/wp-git-action/flags.go +++ /dev/null @@ -1,163 +0,0 @@ -package main - -import ( - "github.com/thegeeklab/wp-git-action/plugin" - "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.StringSliceFlag{ - Name: "action", - Usage: "git action to execute", - EnvVars: []string{"PLUGIN_ACTION"}, - Destination: &settings.Action, - Required: true, - Category: category, - }, - &cli.StringFlag{ - Name: "author-name", - Usage: "git author name", - EnvVars: []string{"PLUGIN_AUTHOR_NAME", "CI_COMMIT_AUTHOR"}, - Destination: &settings.Repo.Author.Name, - Required: true, - Category: category, - }, - &cli.StringFlag{ - Name: "author-email", - Usage: "git author email", - EnvVars: []string{"PLUGIN_AUTHOR_EMAIL", "CI_COMMIT_AUTHOR_EMAIL"}, - Destination: &settings.Repo.Author.Email, - Required: true, - Category: category, - }, - - &cli.StringFlag{ - Name: "netrc.machine", - Usage: "netrc remote machine name", - EnvVars: []string{"PLUGIN_NETRC_MACHINE", "CI_NETRC_MACHINE"}, - Destination: &settings.Netrc.Machine, - Value: "github.com", - Category: category, - }, - &cli.StringFlag{ - Name: "netrc.username", - Usage: "netrc login user on the remote machine", - EnvVars: []string{"PLUGIN_NETRC_USERNAME", "CI_NETRC_USERNAME"}, - Destination: &settings.Netrc.Login, - Value: "token", - Category: category, - }, - &cli.StringFlag{ - Name: "netrc.password", - Usage: "netrc login password on the remote machine", - EnvVars: []string{"PLUGIN_NETRC_PASSWORD", "CI_NETRC_PASSWORD"}, - Destination: &settings.Netrc.Password, - Category: category, - }, - &cli.StringFlag{ - Name: "ssh-key", - Usage: "ssh private key for the remote repository", - EnvVars: []string{"PLUGIN_SSH_KEY"}, - Destination: &settings.SSHKey, - Category: category, - }, - - &cli.StringFlag{ - Name: "remote-url", - Usage: "url of the remote repository", - EnvVars: []string{"PLUGIN_REMOTE_URL", "CI_REPO_CLONE_URL"}, - Destination: &settings.Repo.RemoteURL, - Category: category, - }, - &cli.StringFlag{ - Name: "branch", - Usage: "name of the git source branch", - EnvVars: []string{"PLUGIN_BRANCH"}, - Destination: &settings.Repo.Branch, - Value: "main", - Category: category, - }, - &cli.StringFlag{ - Name: "path", - Usage: "path to clone git repository", - EnvVars: []string{"PLUGIN_PATH"}, - Destination: &settings.Repo.WorkDir, - Category: category, - }, - &cli.StringFlag{ - Name: "commit-message", - Usage: "commit message", - EnvVars: []string{"PLUGIN_MESSAGE"}, - Destination: &settings.Repo.CommitMsg, - Value: "[skip ci] commit dirty state", - Category: category, - }, - &cli.BoolFlag{ - Name: "force-push", - Usage: "enable force push to remote repository", - EnvVars: []string{"PLUGIN_FORCE"}, - Destination: &settings.Repo.ForcePush, - Value: false, - Category: category, - }, - &cli.BoolFlag{ - Name: "followtags", - Usage: "follow tags for pushes to remote repository", - EnvVars: []string{"PLUGIN_FOLLOWTAGS"}, - Destination: &settings.Repo.PushFollowTags, - Value: false, - 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, - Value: false, - Category: category, - }, - &cli.BoolFlag{ - Name: "empty-commit", - Usage: "allow empty commits", - EnvVars: []string{"PLUGIN_EMPTY_COMMIT"}, - Destination: &settings.Repo.EmptyCommit, - Value: false, - Category: category, - }, - &cli.BoolFlag{ - Name: "no-verify", - Usage: "bypass the pre-commit and commit-msg hooks", - EnvVars: []string{"PLUGIN_NO_VERIFY"}, - Destination: &settings.Repo.NoVerify, - Value: false, - Category: category, - }, - &cli.StringFlag{ - Name: "pages.directory", - Usage: "source directory to be synchronized with the pages banch", - EnvVars: []string{"PLUGIN_PAGES_DIRECTORY"}, - Destination: &settings.Pages.Directory, - Value: "docs/", - Category: category, - }, - &cli.StringSliceFlag{ - Name: "pages.exclude", - Usage: "files or directories to exclude from the pages rsync command", - EnvVars: []string{"PLUGIN_PAGES_EXCLUDE"}, - Destination: &settings.Pages.Exclude, - Category: category, - }, - &cli.BoolFlag{ - Name: "pages.delete", - Usage: "add delete flag to pages rsync command", - EnvVars: []string{"PLUGIN_PAGES_DELETE"}, - Destination: &settings.Pages.Delete, - Value: true, - Category: category, - }, - } -} diff --git a/cmd/wp-git-action/main.go b/cmd/wp-git-action/main.go index 25bf344..6d3c3f4 100644 --- a/cmd/wp-git-action/main.go +++ b/cmd/wp-git-action/main.go @@ -1,10 +1,7 @@ package main import ( - "fmt" - "github.com/thegeeklab/wp-git-action/plugin" - wp "github.com/thegeeklab/wp-plugin-go/plugin" ) //nolint:gochecknoglobals @@ -14,14 +11,5 @@ var ( ) func main() { - settings := &plugin.Settings{} - options := wp.Options{ - Name: "wp-git-action", - Description: "Perform git actions.", - 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-action/templates/docs-data.yaml.tmpl b/cmd/wp-git-action/templates/docs-data.yaml.tmpl deleted file mode 100644 index e453a95..0000000 --- a/cmd/wp-git-action/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: {{ . }} - {{- end }} - required: {{ default false $v.Required }} -{{ end -}} -{{ end -}} diff --git a/docs/data/data.yaml b/docs/data/data.yaml index c31ee1e..633390b 100644 --- a/docs/data/data.yaml +++ b/docs/data/data.yaml @@ -60,15 +60,22 @@ properties: defaultValue: false 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 defaultValue: false required: false + - name: log_level + description: | + Plugin log level. + type: string + defaultValue: "info" + required: false + - name: message description: | Commit message. diff --git a/git/clone.go b/git/clone.go index 260e3bc..0136bb3 100644 --- a/git/clone.go +++ b/git/clone.go @@ -2,43 +2,39 @@ package git import ( "fmt" - "os" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // FetchSource fetches the source from remote. -func FetchSource(repo Repository) *execabs.Cmd { +func FetchSource(repo Repository) *types.Cmd { args := []string{ "fetch", "origin", fmt.Sprintf("+%s:", repo.Branch), } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } // CheckoutHead handles branch checkout. -func CheckoutHead(repo Repository) *execabs.Cmd { +func CheckoutHead(repo Repository) *types.Cmd { args := []string{ "checkout", "-qf", repo.Branch, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } diff --git a/git/clone_test.go b/git/clone_test.go new file mode 100644 index 0000000..fc7fa90 --- /dev/null +++ b/git/clone_test.go @@ -0,0 +1,73 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFetchSource(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "fetch from origin with branch", + repo: Repository{ + WorkDir: "/path/to/repo", + Branch: "main", + }, + want: []string{gitBin, "fetch", "origin", "+main:"}, + }, + { + name: "fetch from origin with different branch", + repo: Repository{ + WorkDir: "/path/to/repo", + Branch: "develop", + }, + want: []string{gitBin, "fetch", "origin", "+develop:"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := FetchSource(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestCheckoutHead(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "checkout head with branch", + repo: Repository{ + WorkDir: "/path/to/repo", + Branch: "main", + }, + want: []string{gitBin, "checkout", "-qf", "main"}, + }, + { + name: "checkout head with different branch", + repo: Repository{ + WorkDir: "/path/to/repo", + Branch: "develop", + }, + want: []string{gitBin, "checkout", "-qf", "develop"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := CheckoutHead(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} diff --git a/git/commit.go b/git/commit.go index 6938336..82dc2f9 100644 --- a/git/commit.go +++ b/git/commit.go @@ -1,13 +1,12 @@ package git import ( - "os" - + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // ForceAdd forces the addition of all dirty files. -func ForceAdd(repo Repository) *execabs.Cmd { +func ForceAdd(repo Repository) *types.Cmd { cmd := execabs.Command( gitBin, "add", @@ -15,19 +14,19 @@ func ForceAdd(repo Repository) *execabs.Cmd { "--force", ) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } // Add updates the index to match the working tree. -func Add(repo Repository) *execabs.Cmd { +func Add(repo Repository) *types.Cmd { cmd := execabs.Command( gitBin, "add", ) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr if repo.Add != "" { cmd.Args = append(cmd.Args, repo.Add) @@ -35,11 +34,13 @@ func Add(repo Repository) *execabs.Cmd { cmd.Args = append(cmd.Args, "--all") } - return cmd + return &types.Cmd{ + Cmd: cmd, + } } // TestCleanTree returns non-zero if diff between index and local repository. -func TestCleanTree(repo Repository) *execabs.Cmd { +func IsCleanTree(repo Repository) *types.Cmd { cmd := execabs.Command( gitBin, "diff-index", @@ -48,13 +49,14 @@ func TestCleanTree(repo Repository) *execabs.Cmd { "--ignore-submodules", ) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } // EmptyCommit simply create an empty commit. -func EmptyCommit(repo Repository) *execabs.Cmd { +func EmptyCommit(repo Repository) *types.Cmd { args := []string{ "commit", "--allow-empty", @@ -62,38 +64,33 @@ func EmptyCommit(repo Repository) *execabs.Cmd { repo.CommitMsg, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr if repo.NoVerify { cmd.Args = append(cmd.Args, "--no-verify") } - return cmd + return &types.Cmd{ + Cmd: cmd, + } } -// ForceCommit commits every change while skipping CI. -func ForceCommit(repo Repository) *execabs.Cmd { +func Commit(repo Repository) *types.Cmd { args := []string{ "commit", "-m", repo.CommitMsg, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr if repo.NoVerify { cmd.Args = append(cmd.Args, "--no-verify") } - return cmd + return &types.Cmd{ + Cmd: cmd, + } } diff --git a/git/commit_test.go b/git/commit_test.go new file mode 100644 index 0000000..b898b82 --- /dev/null +++ b/git/commit_test.go @@ -0,0 +1,105 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAdd(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "add all files", + repo: Repository{ + WorkDir: "/path/to/repo", + Add: "", + }, + want: []string{gitBin, "add", "--all"}, + }, + { + name: "add specific file", + repo: Repository{ + WorkDir: "/path/to/repo", + Add: "file.go", + }, + want: []string{gitBin, "add", "file.go"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := Add(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestIsCleanTree(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "clean working tree", + repo: Repository{ + WorkDir: "/path/to/repo", + }, + want: []string{gitBin, "diff-index", "--quiet", "HEAD", "--ignore-submodules"}, + }, + { + name: "unclean working tree", + repo: Repository{ + WorkDir: "/path/to/unclean/repo", + }, + want: []string{gitBin, "diff-index", "--quiet", "HEAD", "--ignore-submodules"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := IsCleanTree(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestEmptyCommit(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "empty commit with default options", + repo: Repository{ + WorkDir: "/path/to/repo", + CommitMsg: "Empty commit", + }, + want: []string{gitBin, "commit", "--allow-empty", "-m", "Empty commit"}, + }, + { + name: "empty commit with no-verify option", + repo: Repository{ + WorkDir: "/path/to/repo", + CommitMsg: "Empty commit", + NoVerify: true, + }, + want: []string{gitBin, "commit", "--allow-empty", "-m", "Empty commit", "--no-verify"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := EmptyCommit(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} diff --git a/git/config.go b/git/config.go index 44ae9c9..1cc97e1 100644 --- a/git/config.go +++ b/git/config.go @@ -1,14 +1,15 @@ package git import ( - "os" "strconv" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) -// repoUserEmail sets the global git author email. -func ConfigAutocorrect(repo Repository) *execabs.Cmd { +// ConfigAutocorrect sets the local git autocorrect configuration for the given repository. +// The autocorrect setting determines how git handles minor typos in commands. +func ConfigAutocorrect(repo Repository) *types.Cmd { args := []string{ "config", "--local", @@ -16,18 +17,16 @@ func ConfigAutocorrect(repo Repository) *execabs.Cmd { repo.Autocorrect, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } -// repoUserEmail sets the global git author email. -func ConfigUserEmail(repo Repository) *execabs.Cmd { +// ConfigUserEmail sets the global git author email. +func ConfigUserEmail(repo Repository) *types.Cmd { args := []string{ "config", "--local", @@ -35,18 +34,16 @@ func ConfigUserEmail(repo Repository) *execabs.Cmd { repo.Author.Email, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } -// repoUserName sets the global git author name. -func ConfigUserName(repo Repository) *execabs.Cmd { +// ConfigUserName configures the user.name git config setting for the given repository. +func ConfigUserName(repo Repository) *types.Cmd { args := []string{ "config", "--local", @@ -54,31 +51,27 @@ func ConfigUserName(repo Repository) *execabs.Cmd { repo.Author.Name, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } -// ConfigSSLVerify disables globally the git ssl verification. -func ConfigSSLVerify(repo Repository) *execabs.Cmd { +// ConfigSSLVerify configures the http.sslVerify git config setting for the given repository. +func ConfigSSLVerify(repo Repository, skipVerify bool) *types.Cmd { args := []string{ "config", "--local", "http.sslVerify", - strconv.FormatBool(!repo.InsecureSkipSSLVerify), + strconv.FormatBool(!skipVerify), } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } diff --git a/git/config_test.go b/git/config_test.go new file mode 100644 index 0000000..e0d9651 --- /dev/null +++ b/git/config_test.go @@ -0,0 +1,124 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigAutocorrect(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "enable autocorrect", + repo: Repository{ + WorkDir: "/path/to/repo", + Autocorrect: "1", + }, + want: []string{gitBin, "config", "--local", "help.autocorrect", "1"}, + }, + { + name: "disable autocorrect", + repo: Repository{ + WorkDir: "/path/to/repo", + Autocorrect: "0", + }, + want: []string{gitBin, "config", "--local", "help.autocorrect", "0"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := ConfigAutocorrect(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestConfigUserEmail(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "set user email", + repo: Repository{ + WorkDir: "/path/to/repo", + Author: Author{ + Email: "user@example.com", + }, + }, + want: []string{gitBin, "config", "--local", "user.email", "user@example.com"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := ConfigUserEmail(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestConfigUserName(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "set user name", + repo: Repository{ + WorkDir: "/path/to/repo", + Author: Author{ + Name: "John Doe", + }, + }, + want: []string{gitBin, "config", "--local", "user.name", "John Doe"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := ConfigUserName(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestConfigSSLVerify(t *testing.T) { + tests := []struct { + name string + repo Repository + skipVerify bool + want []string + }{ + { + name: "enable SSL verification", + repo: Repository{WorkDir: "/path/to/repo"}, + skipVerify: false, + want: []string{gitBin, "config", "--local", "http.sslVerify", "true"}, + }, + { + name: "disable SSL verification", + repo: Repository{WorkDir: "/path/to/repo"}, + skipVerify: true, + want: []string{gitBin, "config", "--local", "http.sslVerify", "false"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := ConfigSSLVerify(tt.repo, tt.skipVerify) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} diff --git a/git/init.go b/git/init.go index 2c0c076..46dd28d 100644 --- a/git/init.go +++ b/git/init.go @@ -1,19 +1,19 @@ package git import ( - "os" - + "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 { +// Init creates a new Git repository in the specified directory. +func Init(repo Repository) *types.Cmd { cmd := execabs.Command( gitBin, "init", ) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } diff --git a/git/init_test.go b/git/init_test.go new file mode 100644 index 0000000..7c2457d --- /dev/null +++ b/git/init_test.go @@ -0,0 +1,23 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + repo := Repository{ + WorkDir: "/path/to/repo", + } + + cmd := Init(repo) + require.Equal(t, []string{gitBin, "init"}, cmd.Cmd.Args) + require.Equal(t, repo.WorkDir, cmd.Cmd.Dir) + + // Test with an empty work directory + repo.WorkDir = "" + cmd = Init(repo) + require.Equal(t, []string{gitBin, "init"}, cmd.Cmd.Args) + require.Empty(t, cmd.Cmd.Dir) +} diff --git a/git/remote.go b/git/remote.go index e0f9632..4ab71d4 100644 --- a/git/remote.go +++ b/git/remote.go @@ -2,31 +2,29 @@ package git import ( "fmt" - "os" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) // RemoteRemove drops the defined remote from a git repo. -func RemoteRemove(repo Repository) *execabs.Cmd { +func RemoteRemove(repo Repository) *types.Cmd { args := []string{ "remote", "rm", repo.RemoteName, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } // RemoteAdd adds an additional remote to a git repo. -func RemoteAdd(repo Repository) *execabs.Cmd { +func RemoteAdd(repo Repository) *types.Cmd { args := []string{ "remote", "add", @@ -34,43 +32,34 @@ func RemoteAdd(repo Repository) *execabs.Cmd { repo.RemoteURL, } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } // RemotePush pushs the changes from the local head to a remote branch. -func RemotePush(repo Repository) *execabs.Cmd { +func RemotePush(repo Repository) *types.Cmd { args := []string{ "push", repo.RemoteName, fmt.Sprintf("HEAD:%s", repo.Branch), } - cmd := execabs.Command( - gitBin, - args..., - ) + cmd := execabs.Command(gitBin, args...) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr if repo.ForcePush { - cmd.Args = append( - cmd.Args, - "--force", - ) + cmd.Args = append(cmd.Args, "--force") } if repo.PushFollowTags { - cmd.Args = append( - cmd.Args, - "--follow-tags") + cmd.Args = append(cmd.Args, "--follow-tags") } - return cmd + return &types.Cmd{ + Cmd: cmd, + } } diff --git a/git/remote_test.go b/git/remote_test.go new file mode 100644 index 0000000..5afc3b7 --- /dev/null +++ b/git/remote_test.go @@ -0,0 +1,139 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRemoteRemove(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "remove remote", + repo: Repository{ + WorkDir: "/path/to/repo", + RemoteName: "origin", + }, + want: []string{gitBin, "remote", "rm", "origin"}, + }, + { + name: "remove custom remote name", + repo: Repository{ + WorkDir: "/path/to/repo", + RemoteName: "upstream", + }, + want: []string{gitBin, "remote", "rm", "upstream"}, + }, + { + name: "remove remote with empty work dir", + repo: Repository{ + RemoteName: "origin", + }, + want: []string{gitBin, "remote", "rm", "origin"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := RemoteRemove(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestRemoteAdd(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "add remote with valid inputs", + repo: Repository{ + WorkDir: "/path/to/repo", + RemoteName: "origin", + RemoteURL: "https://example.com/repo.git", + }, + want: []string{gitBin, "remote", "add", "origin", "https://example.com/repo.git"}, + }, + { + name: "add remote with empty work dir", + repo: Repository{ + RemoteName: "origin", + RemoteURL: "https://example.com/repo.git", + }, + want: []string{gitBin, "remote", "add", "origin", "https://example.com/repo.git"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := RemoteAdd(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestRemotePush(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + forcePush bool + followTags bool + }{ + { + name: "push with default options", + repo: Repository{ + WorkDir: "/path/to/repo", + RemoteName: "origin", + Branch: "main", + }, + want: []string{gitBin, "push", "origin", "HEAD:main"}, + }, + { + name: "push with force option", + repo: Repository{ + WorkDir: "/path/to/repo", + RemoteName: "origin", + Branch: "main", + ForcePush: true, + }, + want: []string{gitBin, "push", "origin", "HEAD:main", "--force"}, + forcePush: true, + }, + { + name: "push with follow tags option", + repo: Repository{ + WorkDir: "/path/to/repo", + RemoteName: "origin", + Branch: "main", + PushFollowTags: true, + }, + want: []string{gitBin, "push", "origin", "HEAD:main", "--follow-tags"}, + followTags: true, + }, + { + name: "push with empty work dir", + repo: Repository{ + RemoteName: "origin", + Branch: "main", + }, + want: []string{gitBin, "push", "origin", "HEAD:main"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := RemotePush(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} diff --git a/git/status.go b/git/status.go index c4bc3e6..c3b82f0 100644 --- a/git/status.go +++ b/git/status.go @@ -1,41 +1,40 @@ package git import ( - "bytes" - "os" - "github.com/rs/zerolog/log" + "github.com/thegeeklab/wp-plugin-go/v2/types" "golang.org/x/sys/execabs" ) -func Status(repo Repository) *execabs.Cmd { +// Status returns a command that runs `git status --porcelain` for the given repository. +func Status(repo Repository) *types.Cmd { cmd := execabs.Command( gitBin, "status", "--porcelain", ) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - return cmd + return &types.Cmd{ + Cmd: cmd, + } } +// IsDirty checks if the given repository has any uncommitted changes. +// It runs `git status --porcelain` and returns true if the output is non-empty, +// indicating that there are uncommitted changes in the repository. +// If there is an error running the git command, it returns false. func IsDirty(repo Repository) bool { - res := bytes.NewBufferString("") - cmd := Status(repo) cmd.Dir = repo.WorkDir - cmd.Stderr = os.Stderr - cmd.Stdout = res - cmd.Stderr = res - err := runCommand(cmd) + out, err := cmd.CombinedOutput() if err != nil { return false } - if res.Len() > 0 { - log.Debug().Msg(res.String()) + if len(out) > 0 { + log.Debug().Msg(string(out)) return true } diff --git a/git/status_test.go b/git/status_test.go new file mode 100644 index 0000000..054bf6b --- /dev/null +++ b/git/status_test.go @@ -0,0 +1,84 @@ +package git + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStatus(t *testing.T) { + tests := []struct { + name string + repo Repository + want []string + }{ + { + name: "with work dir", + repo: Repository{ + WorkDir: "/path/to/repo", + }, + want: []string{gitBin, "status", "--porcelain"}, + }, + { + name: "without work dir", + repo: Repository{}, + want: []string{gitBin, "status", "--porcelain"}, + }, + { + name: "with custom stderr", + repo: Repository{ + WorkDir: "/path/to/repo", + }, + want: []string{gitBin, "status", "--porcelain"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := Status(tt.repo) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir) + }) + } +} + +func TestIsDirty(t *testing.T) { + tests := []struct { + name string + repo Repository + want bool + }{ + { + name: "dirty repo", + repo: Repository{ + WorkDir: t.TempDir(), + }, + want: true, + }, + { + name: "clean repo", + repo: Repository{ + WorkDir: t.TempDir(), + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Init(tt.repo).Run(); err != nil { + require.NoError(t, err) + } + + if tt.want { + _, err := os.Create(filepath.Join(tt.repo.WorkDir, "dummy")) + require.NoError(t, err) + } + + isDirty := IsDirty(tt.repo) + require.Equal(t, tt.want, isDirty) + }) + } +} diff --git a/git/type.go b/git/type.go index e8b9a0e..6d748fa 100644 --- a/git/type.go +++ b/git/type.go @@ -15,14 +15,13 @@ type Repository struct { Add string CommitMsg string - Autocorrect string - NoVerify bool - InsecureSkipSSLVerify bool - EmptyCommit bool - PushFollowTags bool - ForcePush bool - WorkDir string - InitExists bool + Autocorrect string + NoVerify bool + EmptyCommit bool + PushFollowTags bool + ForcePush bool + WorkDir string + IsEmpty bool Author Author } diff --git a/git/util.go b/git/util.go new file mode 100644 index 0000000..300d3e1 --- /dev/null +++ b/git/util.go @@ -0,0 +1,56 @@ +package git + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + netrcFile = `machine %s +login %s +password %s +` + configFile = `Host * +StrictHostKeyChecking no +UserKnownHostsFile=/dev/null +` +) + +const ( + strictFilePerm = 0o600 + strictDirPerm = 0o700 +) + +// WriteKey writes the SSH private key. +func WriteSSHKey(path, key string) error { + sshPath := filepath.Join(path, ".ssh") + confPath := filepath.Join(sshPath, "config") + keyPath := filepath.Join(sshPath, "id_rsa") + + if err := os.MkdirAll(sshPath, strictDirPerm); err != nil { + return fmt.Errorf("failed to create .ssh directory: %w", err) + } + + if err := os.WriteFile(confPath, []byte(configFile), strictFilePerm); err != nil { + return fmt.Errorf("failed to create .ssh/config file: %w", err) + } + + if err := os.WriteFile(keyPath, []byte(key), strictFilePerm); err != nil { + return fmt.Errorf("failed to create .ssh/id_rsa file: %w", err) + } + + return nil +} + +// 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 +} diff --git a/git/util_test.go b/git/util_test.go new file mode 100644 index 0000000..81b4580 --- /dev/null +++ b/git/util_test.go @@ -0,0 +1,96 @@ +package git + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWriteSSHKey(t *testing.T) { + tests := []struct { + name string + privateKey string + dir string + wantErr bool + }{ + { + name: "valid private key", + privateKey: "valid_private_key", + dir: t.TempDir(), + wantErr: false, + }, + { + name: "empty private key", + privateKey: "", + dir: t.TempDir(), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := WriteSSHKey(tt.dir, tt.privateKey) + if tt.wantErr { + require.Error(t, err) + + return + } + + require.NoError(t, err) + + privateKeyPath := filepath.Join(tt.dir, ".ssh", "id_rsa") + _, err = os.Stat(privateKeyPath) + require.NoError(t, err) + + configPath := filepath.Join(tt.dir, ".ssh", "config") + _, err = os.Stat(configPath) + require.NoError(t, err) + }) + } +} + +func TestWriteNetrc(t *testing.T) { + tests := []struct { + name string + path string + machine string + login string + password string + wantErr bool + }{ + { + name: "valid input", + path: t.TempDir(), + machine: "example.com", + login: "user", + password: "pass", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := WriteNetrc(tt.path, tt.machine, tt.login, tt.password) + if tt.wantErr { + require.Error(t, err) + + return + } + + require.NoError(t, err) + + netrcPath := filepath.Join(tt.path, ".netrc") + _, err = os.Stat(netrcPath) + require.NoError(t, err) + + content, err := os.ReadFile(netrcPath) + require.NoError(t, err) + + expected := fmt.Sprintf("machine %s\nlogin %s\npassword %s\n", tt.machine, tt.login, tt.password) + require.Equal(t, expected, string(content)) + }) + } +} diff --git a/git/utils.go b/git/utils.go deleted file mode 100644 index 2a846b3..0000000 --- a/git/utils.go +++ /dev/null @@ -1,103 +0,0 @@ -package git - -import ( - "fmt" - "os" - "os/user" - "path/filepath" - - "github.com/thegeeklab/wp-plugin-go/trace" - "golang.org/x/sys/execabs" -) - -const ( - netrcFile = ` -machine %s -login %s -password %s -` - configFile = ` -Host * -StrictHostKeyChecking no -UserKnownHostsFile=/dev/null -` -) - -const ( - strictFilePerm = 0o600 - strictDirPerm = 0o600 -) - -// WriteKey writes the SSH private key. -func WriteSSHKey(privateKey string) error { - home := "/root" - - if currentUser, err := user.Current(); err == nil { - home = currentUser.HomeDir - } - - sshpath := filepath.Join(home, ".ssh") - - if err := os.MkdirAll(sshpath, strictDirPerm); err != nil { - return err - } - - confpath := filepath.Join(sshpath, "config") - - if err := os.WriteFile( - confpath, - []byte(configFile), - strictFilePerm, - ); err != nil { - return err - } - - privpath := filepath.Join(sshpath, "id_rsa") - - return os.WriteFile( - privpath, - []byte(privateKey), - strictFilePerm, - ) -} - -// 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, - ) -} - -func runCommand(cmd *execabs.Cmd) error { - if cmd.Stdout == nil { - cmd.Stdout = os.Stdout - } - - if cmd.Stderr == nil { - cmd.Stderr = os.Stderr - } - - trace.Cmd(cmd) - - return cmd.Run() -} diff --git a/go.mod b/go.mod index f9fdb2a..7b2af17 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.22 require ( github.com/rs/zerolog v1.32.0 - github.com/thegeeklab/wp-plugin-go v1.7.1 + github.com/stretchr/testify v1.9.0 + github.com/thegeeklab/wp-plugin-go/v2 v2.2.0 github.com/urfave/cli/v2 v2.27.2 - golang.org/x/sys v0.19.0 + golang.org/x/sys v0.20.0 ) require ( @@ -14,6 +15,7 @@ require ( 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.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 github.com/imdario/mergo v0.3.11 // indirect @@ -22,10 +24,12 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect 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-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 ca477a7..c0ce16c 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ 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/thegeeklab/wp-plugin-go/v2 v2.2.0 h1:Z6UzL8N0v3J2uuk67DBnH19QNV1vXihaKC2OoH2TMAY= +github.com/thegeeklab/wp-plugin-go/v2 v2.2.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= @@ -76,8 +76,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= @@ -89,6 +89,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= diff --git a/internal/doc/main.go b/internal/doc/main.go new file mode 100644 index 0000000..6de7382 --- /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-action/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 1d40562..122a62c 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -8,6 +8,8 @@ import ( "path/filepath" "github.com/thegeeklab/wp-git-action/git" + "github.com/thegeeklab/wp-plugin-go/v2/file" + "github.com/thegeeklab/wp-plugin-go/v2/types" ) var ( @@ -20,6 +22,13 @@ var ( ErrGitCloneDestintionNotValid = errors.New("destination not valid") ) +const ( + ActionClone Action = "clone" + ActionCommit Action = "commit" + ActionPush Action = "push" + ActionPages Action = "pages" +) + //nolint:revive func (p *Plugin) run(ctx context.Context) error { if err := p.Validate(); err != nil { @@ -46,20 +55,21 @@ func (p *Plugin) Validate() error { } if err != nil { - return err + return fmt.Errorf("failed to get working directory: %w", err) } - for _, action := range p.Settings.Action.Value() { + for _, actionStr := range p.Settings.Action.Value() { + action := Action(actionStr) switch action { - case "clone": + case ActionClone: continue - case "commit": + case ActionCommit: continue - case "push": + case ActionPush: if p.Settings.SSHKey == "" && p.Settings.Netrc.Password == "" { return ErrAuthSourceNotSet } - case "pages": + case ActionPages: p.Settings.Pages.Directory = filepath.Join(p.Settings.Repo.WorkDir, p.Settings.Pages.Directory) p.Settings.Repo.WorkDir = filepath.Join(p.Settings.Repo.WorkDir, ".tmp") @@ -83,7 +93,7 @@ func (p *Plugin) Validate() error { return ErrPagesActionNotExclusive } default: - return fmt.Errorf("%w: %s", ErrActionUnknown, action) + return fmt.Errorf("%w: %s", ErrActionUnknown, actionStr) } } @@ -92,6 +102,8 @@ func (p *Plugin) Validate() error { // Execute provides the implementation of the plugin. func (p *Plugin) Execute() error { + homeDir := getUserHomeDir() + batchCmd := make([]*types.Cmd, 0) gitEnv := []string{ "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", @@ -103,147 +115,150 @@ func (p *Plugin) Execute() error { for _, env := range gitEnv { if err := os.Unsetenv(env); err != nil { - return err + return fmt.Errorf("failed to unset git env vars '%s': %w", env, err) } } if err := os.Setenv("GIT_TERMINAL_PROMPT", "0"); err != nil { - return err - } - - if err := p.handleInit(); err != nil { - return err - } - - if err := git.ConfigAutocorrect(p.Settings.Repo).Run(); err != nil { - return err - } - - if err := git.ConfigUserName(p.Settings.Repo).Run(); err != nil { - return err - } - - if err := git.ConfigUserEmail(p.Settings.Repo).Run(); err != nil { - return err - } - - if err := git.ConfigSSLVerify(p.Settings.Repo).Run(); err != nil { - return err + return fmt.Errorf("failed to git env var': %w", err) } + // Write SSH key and netrc file. if p.Settings.SSHKey != "" { - if err := git.WriteSSHKey(p.Settings.SSHKey); err != nil { + if err := git.WriteSSHKey(homeDir, p.Settings.SSHKey); err != nil { return err } } - if err := git.WriteNetrc(p.Settings.Netrc.Machine, p.Settings.Netrc.Login, p.Settings.Netrc.Password); err != nil { + netrc := p.Settings.Netrc + if err := git.WriteNetrc(homeDir, netrc.Machine, netrc.Login, netrc.Password); err != nil { return err } - for _, action := range p.Settings.Action.Value() { + // 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) + } + + isEmpty, err := file.IsDirEmpty(p.Settings.Repo.WorkDir) + if err != nil { + return fmt.Errorf("failed to check working directory: %w", err) + } + + p.Settings.Repo.IsEmpty = isEmpty + + 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 !isDir { + batchCmd = append(batchCmd, git.Init(p.Settings.Repo)) + } + + // Handle repo configuration. + batchCmd = append(batchCmd, git.ConfigAutocorrect(p.Settings.Repo)) + batchCmd = append(batchCmd, git.ConfigUserName(p.Settings.Repo)) + batchCmd = append(batchCmd, git.ConfigUserEmail(p.Settings.Repo)) + batchCmd = append(batchCmd, git.ConfigSSLVerify(p.Settings.Repo, p.Network.InsecureSkipVerify)) + + for _, actionStr := range p.Settings.Action.Value() { + action := Action(actionStr) switch action { - case "clone": - if err := p.handleClone(); err != nil { + case ActionClone: + cmds, err := p.handleClone() + if err != nil { return err } - case "commit": - if err := p.handleCommit(); err != nil { - return err - } - case "push": - if err := p.handlePush(); err != nil { - return err - } - case "pages": - if err := p.handlePages(); err != nil { + + batchCmd = append(batchCmd, cmds...) + case ActionCommit: + batchCmd = append(batchCmd, p.handleCommit()...) + case ActionPush: + batchCmd = append(batchCmd, p.handlePush()...) + case ActionPages: + cmds, err := p.handlePages() + if err != nil { return err } + + batchCmd = append(batchCmd, cmds...) + } + } + + for _, cmd := range batchCmd { + if err := cmd.Run(); err != nil { + return err } } return nil } -// handleInit initializes the repository. -func (p *Plugin) handleInit() error { - path := filepath.Join(p.Settings.Repo.WorkDir, ".git") +// handleClone clones the remote repository into the configured working directory. +// If the working directory is not empty, it returns an error. +func (p *Plugin) handleClone() ([]*types.Cmd, error) { + var cmds []*types.Cmd - if err := os.MkdirAll(p.Settings.Repo.WorkDir, os.ModePerm); err != nil { - return err - } - - if _, err := os.Stat(path); !os.IsNotExist(err) { - p.Settings.Repo.InitExists = true - - return nil - } - - return execute(git.Init(p.Settings.Repo)) -} - -// HandleClone clones remote. -func (p *Plugin) handleClone() error { - if p.Settings.Repo.InitExists { - return fmt.Errorf("%w: %s exists and not empty", ErrGitCloneDestintionNotValid, p.Settings.Repo.WorkDir) + if !p.Settings.Repo.IsEmpty { + return cmds, fmt.Errorf("%w: %s exists and not empty", ErrGitCloneDestintionNotValid, p.Settings.Repo.WorkDir) } if p.Settings.Repo.RemoteURL != "" { - if err := execute(git.RemoteAdd(p.Settings.Repo)); err != nil { - return err - } + cmds = append(cmds, git.RemoteAdd(p.Settings.Repo)) } - if err := execute(git.FetchSource(p.Settings.Repo)); err != nil { - return err - } + cmds = append(cmds, git.FetchSource(p.Settings.Repo)) + cmds = append(cmds, git.CheckoutHead(p.Settings.Repo)) - return execute(git.CheckoutHead(p.Settings.Repo)) + return cmds, nil } // HandleCommit commits changes locally. -func (p *Plugin) handleCommit() error { - if err := execute(git.Add(p.Settings.Repo)); err != nil { - return err - } +func (p *Plugin) handleCommit() []*types.Cmd { + var cmds []*types.Cmd - if err := execute(git.TestCleanTree(p.Settings.Repo)); err != nil { - if err := execute(git.ForceCommit(p.Settings.Repo)); err != nil { - return err - } + cmds = append(cmds, git.Add(p.Settings.Repo)) + + if err := git.IsCleanTree(p.Settings.Repo).Run(); err != nil { + cmds = append(cmds, git.Commit(p.Settings.Repo)) } if p.Settings.Repo.EmptyCommit { - if err := execute(git.EmptyCommit(p.Settings.Repo)); err != nil { - return err - } + cmds = append(cmds, git.EmptyCommit(p.Settings.Repo)) } - return nil + return cmds } // HandlePush pushs changes to remote. -func (p *Plugin) handlePush() error { - return execute(git.RemotePush(p.Settings.Repo)) +func (p *Plugin) handlePush() []*types.Cmd { + return []*types.Cmd{git.RemotePush(p.Settings.Repo)} } // HandlePages syncs, commits and pushes the changes from the pages directory to the pages branch. -func (p *Plugin) handlePages() error { +func (p *Plugin) handlePages() ([]*types.Cmd, error) { + var cmds []*types.Cmd + defer os.RemoveAll(p.Settings.Repo.WorkDir) - if err := p.handleClone(); err != nil { - return err + ccmd, err := p.handleClone() + if err != nil { + return cmds, err } - if err := execute( - rsyncDirectories(p.Settings.Pages, p.Settings.Repo), - ); err != nil { - return err - } + cmds = append(cmds, ccmd...) + cmds = append(cmds, + SyncDirectories( + p.Settings.Pages.Exclude.Value(), + p.Settings.Pages.Delete, + p.Settings.Pages.Directory, + p.Settings.Repo.WorkDir, + ), + ) - if err := p.handleCommit(); err != nil { - return err - } + cmds = append(cmds, p.handleCommit()...) + cmds = append(cmds, p.handlePush()...) - return p.handlePush() + return cmds, nil } diff --git a/plugin/plugin.go b/plugin/plugin.go index 2328b9c..7421915 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,11 +1,15 @@ package plugin import ( + "fmt" + "github.com/thegeeklab/wp-git-action/git" - wp "github.com/thegeeklab/wp-plugin-go/plugin" + wp "github.com/thegeeklab/wp-plugin-go/v2/plugin" "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 @@ -34,13 +38,181 @@ type Pages struct { Delete bool } -func New(options wp.Options, settings *Settings) *Plugin { - p := &Plugin{} +type Action string - options.Execute = p.run +func New(e wp.ExecuteFunc, build ...string) *Plugin { + p := &Plugin{ + Settings: &Settings{}, + } + + options := wp.Options{ + Name: "wp-git-action", + Description: "Perform git actions", + 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 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.StringSliceFlag{ + Name: "action", + Usage: "git action to execute", + EnvVars: []string{"PLUGIN_ACTION"}, + Destination: &settings.Action, + Required: true, + Category: category, + }, + &cli.StringFlag{ + Name: "author-name", + Usage: "git author name", + EnvVars: []string{"PLUGIN_AUTHOR_NAME", "CI_COMMIT_AUTHOR"}, + Destination: &settings.Repo.Author.Name, + Required: true, + Category: category, + }, + &cli.StringFlag{ + Name: "author-email", + Usage: "git author email", + EnvVars: []string{"PLUGIN_AUTHOR_EMAIL", "CI_COMMIT_AUTHOR_EMAIL"}, + Destination: &settings.Repo.Author.Email, + Required: true, + Category: category, + }, + + &cli.StringFlag{ + Name: "netrc.machine", + Usage: "netrc remote machine name", + EnvVars: []string{"PLUGIN_NETRC_MACHINE", "CI_NETRC_MACHINE"}, + Destination: &settings.Netrc.Machine, + Value: "github.com", + Category: category, + }, + &cli.StringFlag{ + Name: "netrc.username", + Usage: "netrc login user on the remote machine", + EnvVars: []string{"PLUGIN_NETRC_USERNAME", "CI_NETRC_USERNAME"}, + Destination: &settings.Netrc.Login, + Value: "token", + Category: category, + }, + &cli.StringFlag{ + Name: "netrc.password", + Usage: "netrc login password on the remote machine", + EnvVars: []string{"PLUGIN_NETRC_PASSWORD", "CI_NETRC_PASSWORD"}, + Destination: &settings.Netrc.Password, + Category: category, + }, + &cli.StringFlag{ + Name: "ssh-key", + Usage: "ssh private key for the remote repository", + EnvVars: []string{"PLUGIN_SSH_KEY"}, + Destination: &settings.SSHKey, + Category: category, + }, + + &cli.StringFlag{ + Name: "remote-url", + Usage: "url of the remote repository", + EnvVars: []string{"PLUGIN_REMOTE_URL", "CI_REPO_CLONE_URL"}, + Destination: &settings.Repo.RemoteURL, + Category: category, + }, + &cli.StringFlag{ + Name: "branch", + Usage: "name of the git source branch", + EnvVars: []string{"PLUGIN_BRANCH"}, + Destination: &settings.Repo.Branch, + Value: "main", + Category: category, + }, + &cli.StringFlag{ + Name: "path", + Usage: "path to clone git repository", + EnvVars: []string{"PLUGIN_PATH"}, + Destination: &settings.Repo.WorkDir, + Category: category, + }, + &cli.StringFlag{ + Name: "commit-message", + Usage: "commit message", + EnvVars: []string{"PLUGIN_MESSAGE"}, + Destination: &settings.Repo.CommitMsg, + Value: "[skip ci] commit dirty state", + Category: category, + }, + &cli.BoolFlag{ + Name: "force-push", + Usage: "enable force push to remote repository", + EnvVars: []string{"PLUGIN_FORCE"}, + Destination: &settings.Repo.ForcePush, + Value: false, + Category: category, + }, + &cli.BoolFlag{ + Name: "followtags", + Usage: "follow tags for pushes to remote repository", + EnvVars: []string{"PLUGIN_FOLLOWTAGS"}, + Destination: &settings.Repo.PushFollowTags, + Value: false, + Category: category, + }, + &cli.BoolFlag{ + Name: "empty-commit", + Usage: "allow empty commits", + EnvVars: []string{"PLUGIN_EMPTY_COMMIT"}, + Destination: &settings.Repo.EmptyCommit, + Value: false, + Category: category, + }, + &cli.BoolFlag{ + Name: "no-verify", + Usage: "bypass the pre-commit and commit-msg hooks", + EnvVars: []string{"PLUGIN_NO_VERIFY"}, + Destination: &settings.Repo.NoVerify, + Value: false, + Category: category, + }, + &cli.StringFlag{ + Name: "pages.directory", + Usage: "source directory to be synchronized with the pages banch", + EnvVars: []string{"PLUGIN_PAGES_DIRECTORY"}, + Destination: &settings.Pages.Directory, + Value: "docs/", + Category: category, + }, + &cli.StringSliceFlag{ + Name: "pages.exclude", + Usage: "files or directories to exclude from the pages rsync command", + EnvVars: []string{"PLUGIN_PAGES_EXCLUDE"}, + Destination: &settings.Pages.Exclude, + Category: category, + }, + &cli.BoolFlag{ + Name: "pages.delete", + Usage: "add delete flag to pages rsync command", + EnvVars: []string{"PLUGIN_PAGES_DELETE"}, + Destination: &settings.Pages.Delete, + Value: true, + Category: category, + }, + } +} diff --git a/plugin/rsync.go b/plugin/rsync.go new file mode 100644 index 0000000..52a5e1c --- /dev/null +++ b/plugin/rsync.go @@ -0,0 +1,43 @@ +package plugin + +import ( + "github.com/thegeeklab/wp-plugin-go/v2/types" + + "golang.org/x/sys/execabs" +) + +func SyncDirectories(exclude []string, del bool, src, dest string) *types.Cmd { + args := []string{ + "-r", + "--exclude", + ".git", + } + + for _, item := range exclude { + args = append( + args, + "--exclude", + item, + ) + } + + if del { + args = append( + args, + "--delete", + ) + } + + args = append( + args, + ".", + dest, + ) + + cmd := execabs.Command("rsync", args...) + cmd.Dir = src + + return &types.Cmd{ + Cmd: cmd, + } +} diff --git a/plugin/rsync_test.go b/plugin/rsync_test.go new file mode 100644 index 0000000..a6a2ebb --- /dev/null +++ b/plugin/rsync_test.go @@ -0,0 +1,46 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSyncDirectories(t *testing.T) { + tests := []struct { + name string + exclude []string + del bool + src string + dest string + want []string + }{ + { + name: "exclude .git and other patterns", + exclude: []string{"*.log", "temp/"}, + del: false, + src: "/path/to/src", + dest: "/path/to/dest", + want: []string{ + "rsync", "-r", "--exclude", ".git", "--exclude", "*.log", + "--exclude", "temp/", ".", "/path/to/dest", + }, + }, + { + name: "delete enabled", + exclude: []string{}, + del: true, + src: "/path/to/src", + dest: "/path/to/dest", + want: []string{"rsync", "-r", "--exclude", ".git", "--delete", ".", "/path/to/dest"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := SyncDirectories(tt.exclude, tt.del, tt.src, tt.dest) + require.Equal(t, tt.want, cmd.Cmd.Args) + require.Equal(t, tt.src, cmd.Cmd.Dir) + }) + } +} diff --git a/plugin/util.go b/plugin/util.go new file mode 100644 index 0000000..ed001f3 --- /dev/null +++ b/plugin/util.go @@ -0,0 +1,15 @@ +package plugin + +import ( + "os/user" +) + +func getUserHomeDir() string { + home := "/root" + + if currentUser, err := user.Current(); err == nil { + home = currentUser.HomeDir + } + + return home +} diff --git a/plugin/utils.go b/plugin/utils.go deleted file mode 100644 index a95c295..0000000 --- a/plugin/utils.go +++ /dev/null @@ -1,58 +0,0 @@ -package plugin - -import ( - "os" - "strings" - - "github.com/rs/zerolog/log" - "github.com/thegeeklab/wp-git-action/git" - "golang.org/x/sys/execabs" -) - -// helper function to simply wrap os execte command. -func execute(cmd *execabs.Cmd) error { - log.Debug().Msgf("+ %s", strings.Join(cmd.Args, " ")) - - cmd.Env = os.Environ() - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} - -func rsyncDirectories(pages Pages, repo git.Repository) *execabs.Cmd { - args := []string{ - "-r", - "--exclude", - ".git", - } - - for _, item := range pages.Exclude.Value() { - args = append( - args, - "--exclude", - item, - ) - } - - if pages.Delete { - args = append( - args, - "--delete", - ) - } - - args = append( - args, - ".", - repo.WorkDir, - ) - - cmd := execabs.Command( - "rsync", - args..., - ) - cmd.Dir = pages.Directory - - return cmd -}