0
0
mirror of https://github.com/thegeeklab/wp-git-clone.git synced 2024-06-02 18:29:42 +02:00

Compare commits

...

4 Commits

26 changed files with 843 additions and 634 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 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest
GENERATE ?=
XGO_VERSION := go-1.22.x XGO_VERSION := go-1.22.x
XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64
@ -65,11 +64,7 @@ lint: golangci-lint
.PHONY: generate .PHONY: generate
generate: generate:
$(GO) generate $(GENERATE) $(GO) generate $(PACKAGES)
.PHONY: generate-docs
generate-docs:
$(GO) generate ./cmd/$(EXECUTABLE)/flags.go
.PHONY: test .PHONY: test
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-clone/plugin"
"github.com/thegeeklab/wp-plugin-go/docs"
wp "github.com/thegeeklab/wp-plugin-go/plugin"
wp_template "github.com/thegeeklab/wp-plugin-go/template"
"github.com/urfave/cli/v2"
)
//go:embed templates/docs-data.yaml.tmpl
var yamlTemplate embed.FS
func main() {
settings := &plugin.Settings{}
app := &cli.App{
Flags: settingsFlags(settings, wp.FlagsPluginCategory),
}
out, err := toYAML(app)
if err != nil {
panic(err)
}
fi, err := os.Create("../../docs/data/data-raw.yaml")
if err != nil {
panic(err)
}
defer fi.Close()
if _, err := fi.WriteString(out); err != nil {
panic(err)
}
}
func toYAML(app *cli.App) (string, error) {
var w bytes.Buffer
yamlTmpl, err := template.New("docs").Funcs(wp_template.LoadFuncMap()).ParseFS(yamlTemplate, "templates/docs-data.yaml.tmpl")
if err != nil {
fmt.Println(yamlTmpl)
return "", err
}
if err := yamlTmpl.ExecuteTemplate(&w, "docs-data.yaml.tmpl", docs.GetTemplateData(app)); err != nil {
return "", err
}
return w.String(), nil
}

View File

@ -1,172 +0,0 @@
package main
import (
"github.com/thegeeklab/wp-git-clone/plugin"
"github.com/thegeeklab/wp-plugin-go/types"
"github.com/urfave/cli/v2"
)
// settingsFlags has the cli.Flags for the plugin.Settings.
//
//go:generate go run docs.go flags.go
func settingsFlags(settings *plugin.Settings, category string) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "git remote HTTP clone url",
EnvVars: []string{"PLUGIN_REMOTE", "CI_REPO_CLONE_URL"},
Destination: &settings.Repo.RemoteURL,
DefaultText: "$CI_REPO_CLONE_URL",
Category: category,
},
&cli.StringFlag{
Name: "remote-ssh",
Usage: "git remote SSH clone url",
EnvVars: []string{"PLUGIN_REMOTE_SSH", "CI_REPO_CLONE_SSH_URL"},
Destination: &settings.Repo.RemoteSSH,
DefaultText: "$CI_REPO_CLONE_SSH_URL",
Category: category,
},
&cli.StringFlag{
Name: "workdir",
Usage: "path to clone git repository",
EnvVars: []string{"PLUGIN_WORKDIR", "CI_WORKSPACE"},
Destination: &settings.WorkDir,
DefaultText: "$CI_WORKSPACE",
Category: category,
},
&cli.StringFlag{
Name: "sha",
Usage: "git commit sha",
EnvVars: []string{"PLUGIN_COMMIT_SHA", "CI_COMMIT_SHA"},
Destination: &settings.Repo.CommitSha,
DefaultText: "$CI_COMMIT_SHA",
Category: category,
},
&cli.StringFlag{
Name: "ref",
Usage: "git commit ref",
EnvVars: []string{"PLUGIN_COMMIT_REF", "CI_COMMIT_REF"},
Value: "refs/heads/main",
Destination: &settings.Repo.CommitRef,
Category: category,
},
&cli.StringFlag{
Name: "netrc.machine",
Usage: "netrc machine",
EnvVars: []string{"CI_NETRC_MACHINE"},
Destination: &settings.Netrc.Machine,
Category: category,
},
&cli.StringFlag{
Name: "netrc.username",
Usage: "netrc username",
EnvVars: []string{"CI_NETRC_USERNAME"},
Destination: &settings.Netrc.Password,
Category: category,
},
&cli.StringFlag{
Name: "netrc.password",
Usage: "netrc password",
EnvVars: []string{"CI_NETRC_PASSWORD"},
Destination: &settings.Netrc.Password,
Category: category,
},
&cli.IntFlag{
Name: "depth",
Usage: "clone depth",
EnvVars: []string{"PLUGIN_DEPTH"},
Destination: &settings.Depth,
Category: category,
},
&cli.BoolFlag{
Name: "recursive",
Usage: "clone submodules",
EnvVars: []string{"PLUGIN_RECURSIVE"},
Value: true,
Destination: &settings.Recursive,
Category: category,
},
&cli.BoolFlag{
Name: "tags",
Usage: "fetch git tags during clone",
EnvVars: []string{"PLUGIN_TAGS"},
Value: true,
Destination: &settings.Tags,
Category: category,
},
&cli.BoolFlag{
Name: "insecure-skip-ssl-verify",
Usage: "skip SSL verification of the remote machine",
EnvVars: []string{"PLUGIN_INSECURE_SKIP_SSL_VERIFY"},
Destination: &settings.Repo.InsecureSkipSSLVerify,
Category: category,
},
&cli.BoolFlag{
Name: "submodule-update-remote",
Usage: "update remote submodules",
EnvVars: []string{"PLUGIN_SUBMODULES_UPDATE_REMOTE", "PLUGIN_SUBMODULE_UPDATE_REMOTE"},
Destination: &settings.Repo.SubmoduleRemote,
Category: category,
},
&cli.GenericFlag{
Name: "submodule-override",
Usage: "JSON map of submodule overrides",
EnvVars: []string{"PLUGIN_SUBMODULE_OVERRIDE"},
Value: &types.MapFlag{},
Category: category,
},
&cli.BoolFlag{
Name: "submodule-partial",
Usage: "update submodules via partial clone (`depth=1`)",
EnvVars: []string{"PLUGIN_SUBMODULES_PARTIAL", "PLUGIN_SUBMODULE_PARTIAL"},
Value: true,
Destination: &settings.Repo.SubmodulePartial,
Category: category,
},
&cli.BoolFlag{
Name: "lfs",
Usage: "whether to retrieve LFS content if available",
EnvVars: []string{"PLUGIN_LFS"},
Value: true,
Destination: &settings.Lfs,
Category: category,
},
&cli.StringFlag{
Name: "branch",
Usage: "change branch name",
EnvVars: []string{"PLUGIN_BRANCH", "CI_COMMIT_BRANCH", "CI_REPO_DEFAULT_BRANCH"},
Destination: &settings.Repo.Branch,
Category: category,
},
&cli.BoolFlag{
Name: "partial",
Usage: "enable/disable partial clone",
EnvVars: []string{"PLUGIN_PARTIAL"},
Destination: &settings.Partial,
Category: category,
},
&cli.StringFlag{
Name: "safe-directory",
Usage: "define/replace safe directories",
EnvVars: []string{"PLUGIN_SAFE_DIRECTORY", "CI_WORKSPACE"},
Destination: &settings.Repo.SafeDirectory,
DefaultText: "$CI_WORKSPACE",
Category: category,
},
&cli.BoolFlag{
Name: "use-ssh",
Usage: "using SSH for git clone",
EnvVars: []string{"PLUGIN_USE_SSH"},
Destination: &settings.UseSSH,
Category: category,
},
&cli.StringFlag{
Name: "ssh-key",
Usage: "private key for SSH clone",
EnvVars: []string{"PLUGIN_SSH_KEY"},
Destination: &settings.SSHKey,
Category: category,
},
}
}

View File

@ -1,11 +1,7 @@
package main package main
import ( import (
"fmt"
"github.com/thegeeklab/wp-git-clone/plugin" "github.com/thegeeklab/wp-git-clone/plugin"
wp "github.com/thegeeklab/wp-plugin-go/plugin"
) )
//nolint:gochecknoglobals //nolint:gochecknoglobals
@ -15,14 +11,5 @@ var (
) )
func main() { func main() {
settings := &plugin.Settings{} plugin.New(nil, BuildVersion, BuildDate).Run()
options := wp.Options{
Name: "wp-git-clone",
Description: "Clone git repository",
Version: BuildVersion,
VersionMetadata: fmt.Sprintf("date=%s", BuildDate),
Flags: settingsFlags(settings, wp.FlagsPluginCategory),
}
plugin.New(options, settings).Run()
} }

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: {{ . | toString }}
{{- end }}
required: {{ default false $v.Required }}
{{ end -}}
{{ end -}}

View File

@ -28,9 +28,9 @@ properties:
defaultValue: 0 defaultValue: 0
required: false required: false
- name: insecure_skip_ssl_verify - name: insecure_skip_verify
description: | description: |
Skip SSL verification of the remote machine. Skip SSL verification.
Activating this option is insecure and should be avoided in most cases. Activating this option is insecure and should be avoided in most cases.
type: bool type: bool
@ -44,6 +44,13 @@ properties:
defaultValue: true defaultValue: true
required: false required: false
- name: log_level
description: |
Plugin log level.
type: string
defaultValue: "info"
required: false
- name: partial - name: partial
description: | description: |
Enable/disable partial clone. Enable/disable partial clone.
@ -112,13 +119,6 @@ properties:
defaultValue: true defaultValue: true
required: false required: false
- name: use_ssh
description: |
Using SSH for git clone.
type: bool
defaultValue: false
required: false
- name: workdir - name: workdir
description: | description: |
Path to clone git repository. Path to clone git repository.

View File

@ -3,34 +3,34 @@ package git
import ( import (
"fmt" "fmt"
"github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
// FetchSource fetches the source from remote. // FetchSource fetches the source from remote.
func FetchSource(ref string, depth int, filter string) *execabs.Cmd { func (r *Repository) FetchSource(ref string) *types.Cmd {
args := []string{ args := []string{
"fetch", "fetch",
} }
if depth != 0 { if r.Depth != 0 {
args = append(args, fmt.Sprintf("--depth=%d", depth)) args = append(args, fmt.Sprintf("--depth=%d", r.Depth))
} }
if filter != "" { if r.Filter != "" {
args = append(args, "--filter="+filter) args = append(args, "--filter", r.Filter)
} }
args = append(args, "origin") args = append(args, "origin")
args = append(args, fmt.Sprintf("+%s:", ref)) args = append(args, fmt.Sprintf("+%s:", ref))
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// FetchTags fetches the source from remote. // FetchTags fetches the source from remote.
func FetchTags() *execabs.Cmd { func (r *Repository) FetchTags() *types.Cmd {
args := []string{ args := []string{
"fetch", "fetch",
"--tags", "--tags",
@ -38,27 +38,25 @@ func FetchTags() *execabs.Cmd {
"origin", "origin",
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// FetchLFS fetches lfs. // FetchLFS fetches lfs.
func FetchLFS() *execabs.Cmd { func (r *Repository) FetchLFS() *types.Cmd {
args := []string{ args := []string{
"lfs", "lfs",
"fetch", "fetch",
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// CheckoutHead handles head checkout. // CheckoutHead handles head checkout.
func CheckoutHead() *execabs.Cmd { func (r *Repository) CheckoutHead() *types.Cmd {
args := []string{ args := []string{
"checkout", "checkout",
"--force", "--force",
@ -66,36 +64,33 @@ func CheckoutHead() *execabs.Cmd {
"FETCH_HEAD", "FETCH_HEAD",
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// CheckoutSha handles commit checkout. // CheckoutSha handles commit checkout.
func CheckoutSha(repo Repository) *execabs.Cmd { func (r *Repository) CheckoutSha() *types.Cmd {
args := []string{ args := []string{
"reset", "reset",
"--hard", "--hard",
"--quiet", "--quiet",
repo.CommitSha, r.CommitSha,
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// CheckoutLFS handles commit checkout. // CheckoutLFS handles commit checkout.
func CheckoutLFS() *execabs.Cmd { func (r *Repository) CheckoutLFS() *types.Cmd {
args := []string{ args := []string{
"lfs", "lfs",
"checkout", "checkout",
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }

View File

@ -2,62 +2,206 @@ package git
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
// TestFetch tests if the arguments to `git fetch` are constructed properly. func TestFetchSource(t *testing.T) {
func TestFetch(t *testing.T) {
testdata := []struct { testdata := []struct {
ref string name string
repo *Repository
tags bool tags bool
depth int depth int
exp []string want []string
}{ }{
{ {
"refs/heads/master", name: "fetch main without tags",
false, repo: &Repository{
0, CommitRef: "refs/heads/main",
[]string{ },
"/usr/bin/git", tags: false,
depth: 0,
want: []string{
gitBin,
"fetch", "fetch",
"origin", "origin",
"+refs/heads/master:", "+refs/heads/main:",
}, },
}, },
{ {
"refs/heads/master", name: "fetch main without tags with depth",
false, repo: &Repository{
50, CommitRef: "refs/heads/main",
[]string{ Depth: 50,
"/usr/bin/git", },
tags: false,
depth: 50,
want: []string{
gitBin,
"fetch", "fetch",
"--depth=50", "--depth=50",
"origin", "origin",
"+refs/heads/master:", "+refs/heads/main:",
}, },
}, },
{ {
"refs/heads/master", name: "fetch main with tags and depth",
true, repo: &Repository{
100, CommitRef: "refs/heads/main",
[]string{ Depth: 100,
"/usr/bin/git", },
tags: true,
depth: 100,
want: []string{
gitBin,
"fetch", "fetch",
"--depth=100", "--depth=100",
"origin", "origin",
"+refs/heads/master:", "+refs/heads/main:",
}, },
}, },
} }
for _, td := range testdata {
c := FetchSource(td.ref, td.depth, "")
if len(c.Args) != len(td.exp) {
t.Errorf("Expected: %s, got %s", td.exp, c.Args)
}
for i := range c.Args { for _, tt := range testdata {
if c.Args[i] != td.exp[i] { t.Run(tt.name, func(t *testing.T) {
t.Errorf("Expected: %s, got %s", td.exp, c.Args) cmd := tt.repo.FetchSource(tt.repo.CommitRef)
} assert.Equal(t, tt.want, cmd.Args)
} })
}
}
func TestFetchTags(t *testing.T) {
testdata := []struct {
name string
repo *Repository
tags bool
depth int
want []string
}{
{
name: "fetch tags",
repo: &Repository{},
tags: true,
want: []string{
gitBin,
"fetch",
"--tags",
"--quiet",
"origin",
},
},
}
for _, tt := range testdata {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.FetchTags()
assert.Equal(t, tt.want, cmd.Args)
})
}
}
func TestFetchLFS(t *testing.T) {
testdata := []struct {
name string
repo *Repository
want []string
}{
{
name: "fetch LFS",
repo: &Repository{},
want: []string{
gitBin,
"lfs",
"fetch",
},
},
}
for _, tt := range testdata {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.FetchLFS()
assert.Equal(t, tt.want, cmd.Args)
})
}
}
func TestCheckoutHead(t *testing.T) {
testdata := []struct {
name string
repo *Repository
want []string
}{
{
name: "checkout head",
repo: &Repository{},
want: []string{
gitBin,
"checkout",
"--force",
"--quiet",
"FETCH_HEAD",
},
},
}
for _, tt := range testdata {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.CheckoutHead()
assert.Equal(t, tt.want, cmd.Args)
})
}
}
func TestCheckoutSha(t *testing.T) {
testdata := []struct {
name string
repo *Repository
want []string
}{
{
name: "checkout sha",
repo: &Repository{
CommitSha: "abcd1234",
},
want: []string{
gitBin,
"reset",
"--hard",
"--quiet",
"abcd1234",
},
},
}
for _, tt := range testdata {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.CheckoutSha()
assert.Equal(t, tt.want, cmd.Args)
})
}
}
func TestCheckoutLFS(t *testing.T) {
testdata := []struct {
name string
repo *Repository
want []string
}{
{
name: "checkout LFS with no arguments",
repo: &Repository{},
want: []string{
gitBin,
"lfs",
"checkout",
},
},
}
for _, tt := range testdata {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.CheckoutLFS()
assert.Equal(t, tt.want, cmd.Args)
})
} }
} }

View File

@ -4,43 +4,42 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
// ConfigSSLVerify disables globally the git ssl verification. // ConfigSSLVerify disables globally the git ssl verification.
func ConfigSSLVerify(repo Repository) *execabs.Cmd { func (r *Repository) ConfigSSLVerify(skipVerify bool) *types.Cmd {
args := []string{ args := []string{
"config", "config",
"--global", "--global",
"http.sslVerify", "http.sslVerify",
strconv.FormatBool(!repo.InsecureSkipSSLVerify), strconv.FormatBool(!skipVerify),
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// ConfigSafeDirectory disables globally the git ssl verification. // ConfigSafeDirectory disables globally the git ssl verification.
func ConfigSafeDirectory(repo Repository) *execabs.Cmd { func (r *Repository) ConfigSafeDirectory() *types.Cmd {
args := []string{ args := []string{
"config", "config",
"--global", "--global",
"--replace-all", "--replace-all",
"safe.directory", "safe.directory",
repo.SafeDirectory, r.SafeDirectory,
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// ConfigRemapSubmodule returns a git command that, when executed configures git to // ConfigRemapSubmodule returns a git command that, when executed configures git to
// remap submodule urls. // remap submodule urls.
func ConfigRemapSubmodule(name, url string) *execabs.Cmd { func (r *Repository) ConfigRemapSubmodule(name, url string) *types.Cmd {
args := []string{ args := []string{
"config", "config",
"--global", "--global",
@ -48,14 +47,13 @@ func ConfigRemapSubmodule(name, url string) *execabs.Cmd {
url, url,
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }
// ConfigSSHCommand sets custom SSH key. // ConfigSSHCommand sets custom SSH key.
func ConfigSSHCommand(sshKey string) *execabs.Cmd { func (r *Repository) ConfigSSHCommand(sshKey string) *types.Cmd {
args := []string{ args := []string{
"config", "config",
"--global", "--global",
@ -63,8 +61,10 @@ func ConfigSSHCommand(sshKey string) *execabs.Cmd {
"ssh -i " + sshKey, "ssh -i " + sshKey,
} }
return execabs.Command( cmd := &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
) cmd.SetTrace(false)
return cmd
} }

121
git/config_test.go Normal file
View File

@ -0,0 +1,121 @@
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigSSLVerify(t *testing.T) {
tests := []struct {
name string
repo Repository
skipVerify bool
want []string
}{
{
name: "enable SSL verification",
repo: Repository{},
skipVerify: false,
want: []string{gitBin, "config", "--global", "http.sslVerify", "true"},
},
{
name: "disable SSL verification",
repo: Repository{},
skipVerify: true,
want: []string{gitBin, "config", "--global", "http.sslVerify", "false"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.ConfigSSLVerify(tt.skipVerify)
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}
func TestConfigSafeDirectory(t *testing.T) {
tests := []struct {
name string
repo Repository
safeDir string
want []string
}{
{
name: "set safe directory",
repo: Repository{
SafeDirectory: "/path/to/safe/dir",
},
want: []string{gitBin, "config", "--global", "--replace-all", "safe.directory", "/path/to/safe/dir"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.ConfigSafeDirectory()
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}
func TestConfigRemapSubmodule(t *testing.T) {
tests := []struct {
name string
repo Repository
subName string
subURL string
want []string
}{
{
name: "remap submodule URL",
repo: Repository{},
subName: "mysubmodule",
subURL: "https://example.com/mysubmodule.git",
want: []string{
gitBin, "config", "--global", "submodule.mysubmodule.url",
"https://example.com/mysubmodule.git",
},
},
{
name: "remap submodule URL with spaces",
repo: Repository{},
subName: "my submodule",
subURL: "https://example.com/my submodule.git",
want: []string{
gitBin, "config", "--global", "submodule.my submodule.url",
"https://example.com/my submodule.git",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.ConfigRemapSubmodule(tt.subName, tt.subURL)
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}
func TestConfigSSHCommand(t *testing.T) {
tests := []struct {
name string
repo Repository
sshKey string
want []string
}{
{
name: "set SSH command with key",
repo: Repository{},
sshKey: "/path/to/ssh/key",
want: []string{gitBin, "config", "--global", "core.sshCommand", "ssh -i /path/to/ssh/key"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.ConfigSSHCommand(tt.sshKey)
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}

View File

@ -1,7 +1,5 @@
package git package git
const gitBin = "/usr/bin/git"
type Repository struct { type Repository struct {
RemoteURL string RemoteURL string
RemoteSSH string RemoteSSH string
@ -12,7 +10,11 @@ type Repository struct {
SubmoduleRemote bool SubmoduleRemote bool
SubmodulePartial bool SubmodulePartial bool
InsecureSkipSSLVerify bool SafeDirectory string
SafeDirectory string WorkDir string
InitExists bool IsEmpty bool
Filter string
Depth int
} }
const gitBin = "/usr/bin/git"

View File

@ -1,19 +1,19 @@
package git package git
import ( import (
"github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
// RemoteRemove drops the defined remote from a git repo. // RemoteRemove drops the defined remote from a git repo.
func Init(repo Repository) *execabs.Cmd { func (r *Repository) Init() *types.Cmd {
args := []string{ args := []string{
"init", "init",
"-b", "-b",
repo.Branch, r.Branch,
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }

30
git/init_test.go Normal file
View File

@ -0,0 +1,30 @@
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
tests := []struct {
name string
repo Repository
expected []string
}{
{
name: "init repo",
repo: Repository{
Branch: "main",
},
expected: []string{gitBin, "init", "-b", "main"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.repo.Init()
assert.Equal(t, tt.expected, cmd.Cmd.Args)
})
}
}

View File

@ -1,20 +1,20 @@
package git package git
import ( import (
"github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
// RemoteAdd adds an additional remote to a git repo. // RemoteAdd adds an additional remote to a git repo.
func RemoteAdd(url string) *execabs.Cmd { func (r *Repository) RemoteAdd() *types.Cmd {
args := []string{ args := []string{
"remote", "remote",
"add", "add",
"origin", "origin",
url, r.RemoteURL,
} }
return execabs.Command( return &types.Cmd{
gitBin, Cmd: execabs.Command(gitBin, args...),
args..., }
)
} }

31
git/remote_test.go Normal file
View File

@ -0,0 +1,31 @@
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRemoteAdd(t *testing.T) {
tests := []struct {
name string
repo Repository
want []string
}{
{
name: "add remote with valid inputs",
repo: Repository{
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 := tt.repo.RemoteAdd()
assert.Equal(t, tt.want, cmd.Cmd.Args)
assert.Equal(t, tt.repo.WorkDir, cmd.Cmd.Dir)
})
}
}

View File

@ -1,11 +1,12 @@
package git package git
import ( import (
"github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
// SubmoduleUpdate recursively initializes and updates submodules. // SubmoduleUpdate recursively initializes and updates submodules.
func SubmoduleUpdate(repo Repository) *execabs.Cmd { func (r *Repository) SubmoduleUpdate() *types.Cmd {
args := []string{ args := []string{
"submodule", "submodule",
"update", "update",
@ -13,18 +14,15 @@ func SubmoduleUpdate(repo Repository) *execabs.Cmd {
"--recursive", "--recursive",
} }
if repo.SubmodulePartial { if r.SubmodulePartial {
args = append(args, "--depth=1", "--recommend-shallow") args = append(args, "--depth=1", "--recommend-shallow")
} }
cmd := execabs.Command( if r.SubmoduleRemote {
gitBin, args = append(args, "--remote")
args...,
)
if repo.SubmoduleRemote {
cmd.Args = append(cmd.Args, "--remote")
} }
return cmd return &types.Cmd{
Cmd: execabs.Command(gitBin, args...),
}
} }

View File

@ -2,19 +2,25 @@ package git
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
// TestUpdateSubmodules tests if the arguments to `git submodule update` // TestUpdateSubmodules tests if the arguments to `git submodule update`
// are constructed properly. // are constructed properly.
func TestUpdateSubmodules(t *testing.T) { func TestUpdateSubmodules(t *testing.T) {
tests := []struct { tests := []struct {
partial bool name string
exp []string repo *Repository
want []string
}{ }{
{ {
false, name: "full submodule update",
[]string{ repo: &Repository{
"/usr/bin/git", SubmodulePartial: false,
},
want: []string{
gitBin,
"submodule", "submodule",
"update", "update",
"--init", "--init",
@ -22,9 +28,12 @@ func TestUpdateSubmodules(t *testing.T) {
}, },
}, },
{ {
true, name: "partial submodule update",
[]string{ repo: &Repository{
"/usr/bin/git", SubmodulePartial: true,
},
want: []string{
gitBin,
"submodule", "submodule",
"update", "update",
"--init", "--init",
@ -33,35 +42,13 @@ func TestUpdateSubmodules(t *testing.T) {
"--recommend-shallow", "--recommend-shallow",
}, },
}, },
}
for _, tt := range tests {
repo := Repository{
SubmoduleRemote: false,
SubmodulePartial: tt.partial,
}
c := SubmoduleUpdate(repo)
if len(c.Args) != len(tt.exp) {
t.Errorf("Expected: %s, got %s", tt.exp, c.Args)
}
for i := range c.Args {
if c.Args[i] != tt.exp[i] {
t.Errorf("Expected: %s, got %s", tt.exp, c.Args)
}
}
}
}
// TestUpdateSubmodules tests if the arguments to `git submodule update`
// are constructed properly.
func TestUpdateSubmodulesRemote(t *testing.T) {
tests := []struct {
exp []string
}{
{ {
[]string{ name: "submodule update with remote",
"/usr/bin/git", repo: &Repository{
SubmoduleRemote: true,
},
want: []string{
gitBin,
"submodule", "submodule",
"update", "update",
"--init", "--init",
@ -70,31 +57,28 @@ func TestUpdateSubmodulesRemote(t *testing.T) {
}, },
}, },
{ {
[]string{ name: "submodule update with remote and partial",
"/usr/bin/git", repo: &Repository{
SubmoduleRemote: true,
SubmodulePartial: true,
},
want: []string{
gitBin,
"submodule", "submodule",
"update", "update",
"--init", "--init",
"--recursive", "--recursive",
"--depth=1",
"--recommend-shallow",
"--remote", "--remote",
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
repo := Repository{ t.Run(tt.name, func(t *testing.T) {
SubmoduleRemote: true, cmd := tt.repo.SubmoduleUpdate()
SubmodulePartial: false, assert.Equal(t, tt.want, cmd.Args)
} })
c := SubmoduleUpdate(repo)
if len(c.Args) != len(tt.exp) {
t.Errorf("Expected: %s, got %s", tt.exp, c.Args)
}
for i := range c.Args {
if c.Args[i] != tt.exp[i] {
t.Errorf("Expected: %s, got %s", tt.exp, c.Args)
}
}
} }
} }

View File

@ -1,47 +0,0 @@
package git
import (
"fmt"
"os"
"os/user"
"path/filepath"
)
const (
netrcFile = `
machine %s
login %s
password %s
`
)
const (
strictFilePerm = 0o600
)
// WriteNetrc writes the netrc file.
func WriteNetrc(machine, login, password string) error {
netrcContent := fmt.Sprintf(
netrcFile,
machine,
login,
password,
)
home := "/root"
if currentUser, err := user.Current(); err == nil {
home = currentUser.HomeDir
}
netpath := filepath.Join(
home,
".netrc",
)
return os.WriteFile(
netpath,
[]byte(netrcContent),
strictFilePerm,
)
}

10
go.mod
View File

@ -6,16 +6,16 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
github.com/rs/zerolog v1.32.0 github.com/rs/zerolog v1.32.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/thegeeklab/wp-plugin-go v1.7.1 github.com/thegeeklab/wp-plugin-go/v2 v2.3.0
github.com/urfave/cli/v2 v2.27.1 github.com/urfave/cli/v2 v2.27.2
golang.org/x/sys v0.19.0 golang.org/x/sys v0.20.0
) )
require ( require (
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.1.1 // indirect github.com/google/uuid v1.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect github.com/huandu/xstrings v1.3.3 // indirect
@ -29,7 +29,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.22.0 // indirect golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect golang.org/x/net v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

20
go.sum
View File

@ -8,8 +8,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -48,12 +48,12 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/v2 v2.3.0 h1:9LOdITzjxEEbgcH9yU0tDZL0dcDZzNYzbk2+hZ5QsBA=
github.com/thegeeklab/wp-plugin-go v1.7.1/go.mod h1:Ixi5plt9tpFGTu6yc/Inm5DcDpp3xPTeohfr86gf2EU= github.com/thegeeklab/wp-plugin-go/v2 v2.3.0/go.mod h1:I/3M/4OPvr4FFS+s0aaImpX1llA/lS2KC6Bnp+qzsCs=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@ -78,8 +78,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

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-clone/plugin"
"github.com/thegeeklab/wp-plugin-go/v2/docs"
"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 := 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,14 +8,12 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thegeeklab/wp-git-clone/git" "github.com/thegeeklab/wp-plugin-go/v2/file"
"github.com/thegeeklab/wp-plugin-go/trace" "github.com/thegeeklab/wp-plugin-go/v2/types"
"github.com/thegeeklab/wp-plugin-go/types" "github.com/thegeeklab/wp-plugin-go/v2/util"
"golang.org/x/sys/execabs"
) )
const ( const (
@ -48,22 +46,24 @@ func (p *Plugin) run(ctx context.Context) error {
// Validate handles the settings validation of the plugin. // Validate handles the settings validation of the plugin.
func (p *Plugin) Validate() error { func (p *Plugin) Validate() error {
var err error
// This default cannot be set in the cli flag, as the CI_* environment variables // This default cannot be set in the cli flag, as the CI_* environment variables
// can be set empty, resulting in an empty default value. // can be set empty, resulting in an empty default value.
if p.Settings.Repo.Branch == "" { if p.Settings.Repo.Branch == "" {
p.Settings.Repo.Branch = "main" p.Settings.Repo.Branch = "main"
} }
if p.Settings.WorkDir == "" { if p.Settings.Repo.WorkDir == "" {
var err error p.Settings.Repo.WorkDir, err = os.Getwd()
if p.Settings.WorkDir, err = os.Getwd(); err != nil { if err != nil {
return err return fmt.Errorf("failed to get working directory: %w", err)
} }
} }
if p.Settings.Partial { if p.Settings.Partial {
p.Settings.Depth = 1 p.Settings.Repo.Depth = 1
p.Settings.Filter = "tree:0" p.Settings.Repo.Filter = "tree:0"
} }
return nil return nil
@ -71,76 +71,93 @@ func (p *Plugin) Validate() error {
// Execute provides the implementation of the plugin. // Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error { func (p *Plugin) Execute() error {
cmds := make([]*execabs.Cmd, 0) var err error
// Handle init homeDir := util.GetUserHomeDir()
initPath := filepath.Join(p.Settings.WorkDir, ".git") batchCmd := make([]*types.Cmd, 0)
if err := os.MkdirAll(p.Settings.WorkDir, os.ModePerm); err != nil { fmt.Println(p.Settings.Repo.WorkDir)
return err
// Handle repo initialization.
if err := os.MkdirAll(p.Settings.Repo.WorkDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create working directory: %w", err)
} }
//nolint:nestif p.Settings.Repo.IsEmpty, err = file.IsDirEmpty(p.Settings.Repo.WorkDir)
if _, err := os.Stat(initPath); os.IsNotExist(err) { if err != nil {
cmds = append(cmds, git.ConfigSafeDirectory(p.Settings.Repo)) return fmt.Errorf("failed to check working directory: %w", err)
}
if err := p.execCmd(git.Init(p.Settings.Repo), new(bytes.Buffer)); err != nil { isDir, err := file.IsDir(filepath.Join(p.Settings.Repo.WorkDir, ".git"))
return err if err != nil {
} return fmt.Errorf("failed to check working directory: %w", err)
}
if p.Settings.UseSSH { if !isDir {
cmds = append(cmds, git.RemoteAdd(p.Settings.Repo.RemoteSSH)) batchCmd = append(batchCmd, p.Settings.Repo.Init())
if p.Settings.SSHKey != "" { batchCmd = append(batchCmd, p.Settings.Repo.RemoteAdd())
cmds = append(cmds, git.ConfigSSHCommand(p.Settings.SSHKey))
} if p.Settings.SSHKey != "" {
} else { batchCmd = append(batchCmd, p.Settings.Repo.ConfigSSHCommand(p.Settings.SSHKey))
cmds = append(cmds, git.RemoteAdd(p.Settings.Repo.RemoteURL))
} }
} }
if p.Settings.Repo.InsecureSkipSSLVerify { batchCmd = append(batchCmd, p.Settings.Repo.ConfigSSLVerify(p.Network.InsecureSkipVerify))
cmds = append(cmds, git.ConfigSSLVerify(p.Settings.Repo))
}
if err := git.WriteNetrc(p.Settings.Netrc.Machine, p.Settings.Netrc.Login, p.Settings.Netrc.Password); err != nil { netrc := p.Settings.Netrc
if err := WriteNetrc(homeDir, netrc.Machine, netrc.Login, netrc.Password); err != nil {
return err return err
} }
// Handle clone // Handle clone
if p.Settings.Repo.CommitSha == "" { if p.Settings.Repo.CommitSha == "" {
// fetch and checkout by ref // fetch and checkout by ref
log.Info().Msg("no commit information: using head checkout") log.Info().Msg("no commit information: using head checkout")
cmds = append(cmds, git.FetchSource(p.Settings.Repo.CommitRef, p.Settings.Depth, p.Settings.Filter)) batchCmd = append(batchCmd, p.Settings.Repo.FetchSource(p.Settings.Repo.CommitRef))
cmds = append(cmds, git.CheckoutHead()) batchCmd = append(batchCmd, p.Settings.Repo.CheckoutHead())
} else { } else {
cmds = append(cmds, git.FetchSource(p.Settings.Repo.CommitSha, p.Settings.Depth, p.Settings.Filter)) batchCmd = append(batchCmd, p.Settings.Repo.FetchSource(p.Settings.Repo.CommitSha))
cmds = append(cmds, git.CheckoutSha(p.Settings.Repo)) batchCmd = append(batchCmd, p.Settings.Repo.CheckoutSha())
} }
if p.Settings.Tags { if p.Settings.Tags {
cmds = append(cmds, git.FetchTags()) batchCmd = append(batchCmd, p.Settings.Repo.FetchTags())
} }
for name, submoduleURL := range p.Settings.Repo.Submodules { for name, submoduleURL := range p.Settings.Repo.Submodules {
cmds = append(cmds, git.ConfigRemapSubmodule(name, submoduleURL)) batchCmd = append(batchCmd, p.Settings.Repo.ConfigRemapSubmodule(name, submoduleURL))
} }
if p.Settings.Recursive { if p.Settings.Recursive {
cmds = append(cmds, git.SubmoduleUpdate(p.Settings.Repo)) batchCmd = append(batchCmd, p.Settings.Repo.SubmoduleUpdate())
} }
if p.Settings.Lfs { if p.Settings.Lfs {
cmds = append(cmds, git.FetchLFS()) batchCmd = append(batchCmd, p.Settings.Repo.FetchLFS())
cmds = append(cmds, git.CheckoutLFS()) batchCmd = append(batchCmd, p.Settings.Repo.CheckoutLFS())
} }
for _, cmd := range cmds { for _, cmd := range batchCmd {
log.Debug().Msgf("+ %s", strings.Join(cmd.Args, " "))
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err := p.execCmd(cmd, buf)
// Don' set GIT_TERMINAL_PROMPT=0 as it prevents git from loading .netrc
defaultEnvVars := []string{
"GIT_LFS_SKIP_SMUDGE=1", // prevents git-lfs from retrieving any LFS files
}
if p.Settings.Home != "" {
if _, err := os.Stat(p.Settings.Home); !os.IsNotExist(err) {
defaultEnvVars = append(defaultEnvVars, fmt.Sprintf("HOME=%s", p.Settings.Home))
}
}
cmd.Env = append(os.Environ(), defaultEnvVars...)
cmd.Stdout = io.MultiWriter(os.Stdout, buf)
cmd.Stderr = io.MultiWriter(os.Stderr, buf)
cmd.Dir = p.Settings.Repo.WorkDir
err := cmd.Run()
switch { switch {
case err != nil && shouldRetry(buf.String()): case err != nil && shouldRetry(buf.String()):
@ -163,25 +180,3 @@ func (p *Plugin) FlagsFromContext() error {
return nil return nil
} }
func (p *Plugin) execCmd(cmd *execabs.Cmd, buf *bytes.Buffer) error {
// Don' set GIT_TERMINAL_PROMPT=0 as it prevents git from loading .netrc
defaultEnvVars := []string{
"GIT_LFS_SKIP_SMUDGE=1", // prevents git-lfs from retrieving any LFS files
}
if p.Settings.Home != "" {
if _, err := os.Stat(p.Settings.Home); !os.IsNotExist(err) {
defaultEnvVars = append(defaultEnvVars, fmt.Sprintf("HOME=%s", p.Settings.Home))
}
}
cmd.Env = append(os.Environ(), defaultEnvVars...)
cmd.Stdout = io.MultiWriter(os.Stdout, buf)
cmd.Stderr = io.MultiWriter(os.Stderr, buf)
cmd.Dir = p.Settings.WorkDir
trace.Cmd(cmd)
return cmd.Run()
}

View File

@ -1,6 +1,7 @@
package plugin package plugin
import ( import (
"context"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -28,19 +29,18 @@ func TestClone(t *testing.T) {
dir := setup() dir := setup()
defer teardown(dir) defer teardown(dir)
plugin := Plugin{ plugin := New(func(_ context.Context) error { return nil })
Settings: &Settings{ plugin.Settings = &Settings{
Repo: git.Repository{ Repo: git.Repository{
RemoteURL: tt.clone, RemoteURL: tt.clone,
CommitRef: tt.ref, CommitRef: tt.ref,
CommitSha: tt.commit, CommitSha: tt.commit,
Branch: "main", Branch: "main",
},
Home: "/tmp",
WorkDir: filepath.Join(dir, tt.path), WorkDir: filepath.Join(dir, tt.path),
Recursive: tt.recursive,
Lfs: tt.lfs,
}, },
Home: "/tmp",
Recursive: tt.recursive,
Lfs: tt.lfs,
} }
if err := plugin.Execute(); err != nil { if err := plugin.Execute(); err != nil {
@ -48,14 +48,14 @@ func TestClone(t *testing.T) {
} }
if tt.data != "" { if tt.data != "" {
data := readFile(plugin.Settings.WorkDir, tt.file) data := readFile(plugin.Settings.Repo.WorkDir, tt.file)
if data != tt.data { if data != tt.data {
t.Errorf("Expected %s to contain [%s]. Got [%s].", tt.file, tt.data, data) t.Errorf("Expected %s to contain [%s]. Got [%s].", tt.file, tt.data, data)
} }
} }
if tt.dataSize != 0 { if tt.dataSize != 0 {
size := getFileSize(plugin.Settings.WorkDir, tt.file) size := getFileSize(plugin.Settings.Repo.WorkDir, tt.file)
if size != tt.dataSize { if size != tt.dataSize {
t.Errorf("Expected %s size to be [%d]. Got [%d].", tt.file, tt.dataSize, size) t.Errorf("Expected %s size to be [%d]. Got [%d].", tt.file, tt.dataSize, size)
} }
@ -71,19 +71,18 @@ func TestCloneNonEmpty(t *testing.T) {
defer teardown(dir) defer teardown(dir)
for _, tt := range getCommits() { for _, tt := range getCommits() {
plugin := Plugin{ plugin := New(func(_ context.Context) error { return nil })
Settings: &Settings{ plugin.Settings = &Settings{
Repo: git.Repository{ Repo: git.Repository{
RemoteURL: tt.clone, RemoteURL: tt.clone,
CommitRef: tt.ref, CommitRef: tt.ref,
CommitSha: tt.commit, CommitSha: tt.commit,
Branch: "main", Branch: "main",
},
Home: "/tmp",
WorkDir: filepath.Join(dir, tt.path), WorkDir: filepath.Join(dir, tt.path),
Recursive: tt.recursive,
Lfs: tt.lfs,
}, },
Home: "/tmp",
Recursive: tt.recursive,
Lfs: tt.lfs,
} }
if err := plugin.Execute(); err != nil { if err := plugin.Execute(); err != nil {
@ -91,7 +90,7 @@ func TestCloneNonEmpty(t *testing.T) {
} }
if tt.data != "" { if tt.data != "" {
data := readFile(plugin.Settings.WorkDir, tt.file) data := readFile(plugin.Settings.Repo.WorkDir, tt.file)
if data != tt.data { if data != tt.data {
t.Errorf("Expected %s to contain [%q]. Got [%q].", tt.file, tt.data, data) t.Errorf("Expected %s to contain [%q]. Got [%q].", tt.file, tt.data, data)
@ -100,7 +99,7 @@ func TestCloneNonEmpty(t *testing.T) {
} }
if tt.dataSize != 0 { if tt.dataSize != 0 {
size := getFileSize(plugin.Settings.WorkDir, tt.file) size := getFileSize(plugin.Settings.Repo.WorkDir, tt.file)
if size != tt.dataSize { if size != tt.dataSize {
t.Errorf("Expected %s size to be [%d]. Got [%d].", tt.file, tt.dataSize, size) t.Errorf("Expected %s size to be [%d]. Got [%d].", tt.file, tt.dataSize, size)
} }

View File

@ -1,10 +1,16 @@
package plugin package plugin
import ( import (
"fmt"
"github.com/thegeeklab/wp-git-clone/git" "github.com/thegeeklab/wp-git-clone/git"
wp "github.com/thegeeklab/wp-plugin-go/plugin" wp "github.com/thegeeklab/wp-plugin-go/v2/plugin"
"github.com/thegeeklab/wp-plugin-go/v2/types"
"github.com/urfave/cli/v2"
) )
//go:generate go run ../internal/doc/main.go -output=../docs/data/data-raw.yaml
// Plugin implements provide the plugin. // Plugin implements provide the plugin.
type Plugin struct { type Plugin struct {
*wp.Plugin *wp.Plugin
@ -19,30 +25,192 @@ type Netrc struct {
// Settings for the plugin. // Settings for the plugin.
type Settings struct { type Settings struct {
Depth int
Recursive bool Recursive bool
Tags bool Tags bool
Lfs bool Lfs bool
Partial bool Partial bool
Filter string
UseSSH bool
SSHKey string
Home string Home string
WorkDir string SSHKey string
Netrc Netrc Netrc Netrc
Repo git.Repository Repo git.Repository
} }
func New(options wp.Options, settings *Settings) *Plugin { func New(e wp.ExecuteFunc, build ...string) *Plugin {
p := &Plugin{} p := &Plugin{
Settings: &Settings{},
}
if options.Execute == nil { options := wp.Options{
options.Execute = p.run Name: "wp-git-clone",
Description: "Clone git repository",
Flags: Flags(p.Settings, wp.FlagsPluginCategory),
Execute: p.run,
HideWoodpeckerFlags: true,
}
if len(build) > 0 {
options.Version = build[0]
}
if len(build) > 1 {
options.VersionMetadata = fmt.Sprintf("date=%s", build[1])
}
if e != nil {
options.Execute = e
} }
p.Plugin = wp.New(options) p.Plugin = wp.New(options)
p.Settings = settings
return p return p
} }
// Flags returns a slice of CLI flags for the plugin.
func Flags(settings *Settings, category string) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "git remote HTTP clone url",
EnvVars: []string{"PLUGIN_REMOTE", "CI_REPO_CLONE_URL"},
Destination: &settings.Repo.RemoteURL,
DefaultText: "$CI_REPO_CLONE_URL",
Category: category,
},
&cli.StringFlag{
Name: "remote-ssh",
Usage: "git remote SSH clone url",
EnvVars: []string{"PLUGIN_REMOTE_SSH", "CI_REPO_CLONE_SSH_URL"},
Destination: &settings.Repo.RemoteSSH,
DefaultText: "$CI_REPO_CLONE_SSH_URL",
Category: category,
},
&cli.StringFlag{
Name: "workdir",
Usage: "path to clone git repository",
EnvVars: []string{"PLUGIN_WORKDIR", "CI_WORKSPACE"},
Destination: &settings.Repo.WorkDir,
DefaultText: "$CI_WORKSPACE",
Category: category,
},
&cli.StringFlag{
Name: "sha",
Usage: "git commit sha",
EnvVars: []string{"PLUGIN_COMMIT_SHA", "CI_COMMIT_SHA"},
Destination: &settings.Repo.CommitSha,
DefaultText: "$CI_COMMIT_SHA",
Category: category,
},
&cli.StringFlag{
Name: "ref",
Usage: "git commit ref",
EnvVars: []string{"PLUGIN_COMMIT_REF", "CI_COMMIT_REF"},
Value: "refs/heads/main",
Destination: &settings.Repo.CommitRef,
Category: category,
},
&cli.StringFlag{
Name: "netrc.machine",
Usage: "netrc machine",
EnvVars: []string{"CI_NETRC_MACHINE"},
Destination: &settings.Netrc.Machine,
Category: category,
},
&cli.StringFlag{
Name: "netrc.username",
Usage: "netrc username",
EnvVars: []string{"CI_NETRC_USERNAME"},
Destination: &settings.Netrc.Password,
Category: category,
},
&cli.StringFlag{
Name: "netrc.password",
Usage: "netrc password",
EnvVars: []string{"CI_NETRC_PASSWORD"},
Destination: &settings.Netrc.Password,
Category: category,
},
&cli.IntFlag{
Name: "depth",
Usage: "clone depth",
EnvVars: []string{"PLUGIN_DEPTH"},
Destination: &settings.Repo.Depth,
Category: category,
},
&cli.BoolFlag{
Name: "recursive",
Usage: "clone submodules",
EnvVars: []string{"PLUGIN_RECURSIVE"},
Value: true,
Destination: &settings.Recursive,
Category: category,
},
&cli.BoolFlag{
Name: "tags",
Usage: "fetch git tags during clone",
EnvVars: []string{"PLUGIN_TAGS"},
Value: true,
Destination: &settings.Tags,
Category: category,
},
&cli.BoolFlag{
Name: "submodule-update-remote",
Usage: "update remote submodules",
EnvVars: []string{"PLUGIN_SUBMODULES_UPDATE_REMOTE", "PLUGIN_SUBMODULE_UPDATE_REMOTE"},
Destination: &settings.Repo.SubmoduleRemote,
Category: category,
},
&cli.GenericFlag{
Name: "submodule-override",
Usage: "JSON map of submodule overrides",
EnvVars: []string{"PLUGIN_SUBMODULE_OVERRIDE"},
Value: &types.MapFlag{},
Category: category,
},
&cli.BoolFlag{
Name: "submodule-partial",
Usage: "update submodules via partial clone (`depth=1`)",
EnvVars: []string{"PLUGIN_SUBMODULES_PARTIAL", "PLUGIN_SUBMODULE_PARTIAL"},
Value: true,
Destination: &settings.Repo.SubmodulePartial,
Category: category,
},
&cli.BoolFlag{
Name: "lfs",
Usage: "whether to retrieve LFS content if available",
EnvVars: []string{"PLUGIN_LFS"},
Value: true,
Destination: &settings.Lfs,
Category: category,
},
&cli.StringFlag{
Name: "branch",
Usage: "change branch name",
EnvVars: []string{"PLUGIN_BRANCH", "CI_COMMIT_BRANCH", "CI_REPO_DEFAULT_BRANCH"},
Destination: &settings.Repo.Branch,
Category: category,
},
&cli.BoolFlag{
Name: "partial",
Usage: "enable/disable partial clone",
EnvVars: []string{"PLUGIN_PARTIAL"},
Destination: &settings.Partial,
Category: category,
},
&cli.StringFlag{
Name: "safe-directory",
Usage: "define/replace safe directories",
EnvVars: []string{"PLUGIN_SAFE_DIRECTORY", "CI_WORKSPACE"},
Destination: &settings.Repo.SafeDirectory,
DefaultText: "$CI_WORKSPACE",
Category: category,
},
&cli.StringFlag{
Name: "ssh-key",
Usage: "private key for SSH clone",
EnvVars: []string{"PLUGIN_SSH_KEY"},
Destination: &settings.SSHKey,
Category: category,
},
}
}

View File

@ -1,12 +1,10 @@
package main package plugin
import ( import (
"context" "context"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/thegeeklab/wp-git-clone/plugin"
wp "github.com/thegeeklab/wp-plugin-go/plugin"
) )
func Test_pluginOptions(t *testing.T) { func Test_pluginOptions(t *testing.T) {
@ -29,14 +27,7 @@ func Test_pluginOptions(t *testing.T) {
t.Setenv(key, value) t.Setenv(key, value)
} }
settings := &plugin.Settings{} got := New(func(_ context.Context) error { return nil })
options := wp.Options{
Name: "wp-git-clone",
Flags: settingsFlags(settings, wp.FlagsPluginCategory),
Execute: func(_ context.Context) error { return nil },
}
got := plugin.New(options, settings)
_ = got.App.Run([]string{"wp-git-clone"}) _ = got.App.Run([]string{"wp-git-clone"})
_ = got.Validate() _ = got.Validate()

View File

@ -1,16 +1,26 @@
package plugin package plugin
import ( import (
"fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/thegeeklab/wp-plugin-go/trace" "github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
const (
netrcFile = `machine %s
login %s
password %s
`
strictFilePerm = 0o600
)
// shouldRetry returns true if the command should be re-executed. Currently // shouldRetry returns true if the command should be re-executed. Currently
// this only returns true if the remote ref does not exist. // this only returns true if the remote ref does not exist.
func shouldRetry(s string) bool { func shouldRetry(s string) bool {
@ -25,19 +35,19 @@ func newBackoff(maxRetries uint64) backoff.BackOff {
return backoff.WithMaxRetries(b, maxRetries) return backoff.WithMaxRetries(b, maxRetries)
} }
func retryCmd(cmd *execabs.Cmd) error { func retryCmd(cmd *types.Cmd) error {
backoffOps := func() error { backoffOps := func() error {
// copy the original command // copy the original command
//nolint:gosec //nolint:gosec
retry := execabs.Command(cmd.Args[0], cmd.Args[1:]...) retry := &types.Cmd{
retry.Dir = cmd.Dir Cmd: execabs.Command(cmd.Cmd.Path, cmd.Cmd.Args...),
}
retry.Env = cmd.Env retry.Env = cmd.Env
retry.Stdout = os.Stdout retry.Stdout = cmd.Stdout
retry.Stderr = os.Stderr retry.Stderr = cmd.Stderr
retry.Dir = cmd.Dir
trace.Cmd(cmd) return retry.Run()
return cmd.Run()
} }
backoffLog := func(err error, delay time.Duration) { backoffLog := func(err error, delay time.Duration) {
log.Error().Msgf("failed to find remote ref: %v: retry in %s", err, delay.Truncate(time.Second)) log.Error().Msgf("failed to find remote ref: %v: retry in %s", err, delay.Truncate(time.Second))
@ -45,3 +55,15 @@ func retryCmd(cmd *execabs.Cmd) error {
return backoff.RetryNotify(backoffOps, newBackoff(daemonBackoffMaxRetries), backoffLog) return backoff.RetryNotify(backoffOps, newBackoff(daemonBackoffMaxRetries), backoffLog)
} }
// WriteNetrc writes the netrc file.
func WriteNetrc(path, machine, login, password string) error {
netrcPath := filepath.Join(path, ".netrc")
netrcContent := fmt.Sprintf(netrcFile, machine, login, password)
if err := os.WriteFile(netrcPath, []byte(netrcContent), strictFilePerm); err != nil {
return fmt.Errorf("failed to create .netrc file: %w", err)
}
return nil
}