refactor: rework batch command execution (#109)

This commit is contained in:
Robert Kaussow 2024-05-05 22:14:55 +02:00 committed by GitHub
parent 92a7c252d2
commit 179014e52e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1262 additions and 662 deletions

View File

@ -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:

View File

@ -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
}

View File

@ -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,
},
}
}

View File

@ -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()
}

View File

@ -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 -}}

View File

@ -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.

View File

@ -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,
}
}

73
git/clone_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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,
}
}

105
git/commit_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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,
}
}

124
git/config_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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,
}
}

23
git/init_test.go Normal file
View File

@ -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)
}

View File

@ -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,
}
}

139
git/remote_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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
}

84
git/status_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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
}

56
git/util.go Normal file
View File

@ -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
}

96
git/util_test.go Normal file
View File

@ -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))
})
}
}

View File

@ -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()
}

8
go.mod
View File

@ -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
)

9
go.sum
View File

@ -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=

42
internal/doc/main.go Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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,
},
}
}

43
plugin/rsync.go Normal file
View File

@ -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,
}
}

46
plugin/rsync_test.go Normal file
View File

@ -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)
})
}
}

15
plugin/util.go Normal file
View File

@ -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
}

View File

@ -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
}