From e8c4aad467d85af16a2bf85dfdda2654beaedeee Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Fri, 2 Dec 2022 22:21:35 +0100 Subject: [PATCH] refactor: rework plugin structure and add pages action (#7) --- .dictionary | 1 + _docs/content/_index.md | 29 +++++- _docs/data/data.yaml | 27 ++++- cmd/drone-git-action/config.go | 77 ++++++++------ docker/Dockerfile.amd64 | 2 +- docker/Dockerfile.arm | 2 +- docker/Dockerfile.arm64 | 2 +- git/clone.go | 35 +++++++ git/commit.go | 51 +++++---- git/config.go | 43 ++++++-- git/remote.go | 39 ++++--- git/status.go | 42 ++++++++ git/type.go | 27 +++++ git/utils.go | 32 ++++-- plugin/git.go | 111 -------------------- plugin/impl.go | 184 +++++++++++++++++++++++++++------ plugin/utils.go | 43 ++++++-- 17 files changed, 507 insertions(+), 240 deletions(-) create mode 100644 git/clone.go create mode 100644 git/status.go create mode 100644 git/type.go delete mode 100644 plugin/git.go diff --git a/.dictionary b/.dictionary index 6d9a77e..4c1a95f 100644 --- a/.dictionary +++ b/.dictionary @@ -3,3 +3,4 @@ github url gh drone-git-action +rsync diff --git a/_docs/content/_index.md b/_docs/content/_index.md index d8d2f45..21cdf06 100644 --- a/_docs/content/_index.md +++ b/_docs/content/_index.md @@ -24,9 +24,12 @@ kind: pipeline name: default steps: - - name: commit artifact + - name: commit changelog image: thegeeklab/drone-git-action settings: + action: + - commit + - push netrc_password: ghp_3LbMg9Kncpdkhjp3bh3dMnKNXLjVMTsXk4sM author_name: octobot author_email: octobot@example.com @@ -41,6 +44,30 @@ steps: +### Examples + +#### Publish GitHub pages + +The plugin can be used to publish GitHub pages to the pages branch. Remember that the `pages` action cannot be combined with other actions. + +```YAML +kind: pipeline +name: default + +steps: + - name: publish + image: thegeeklab/drone-git-action + settings: + action: + - pages + author_email: bot@thegeeklab.de + author_name: thegeeklab-bot + message: "update pages" + branch: gh-pages + pages_directory: docs/ + netrc_password: ghp_3LbMg9Kncpdkhjp3bh3dMnKNXLjVMTsXk4sM +``` + ## Build Build the binary with the following command: diff --git a/_docs/data/data.yaml b/_docs/data/data.yaml index 1abf553..1a938b2 100644 --- a/_docs/data/data.yaml +++ b/_docs/data/data.yaml @@ -1,7 +1,14 @@ --- properties: - - name: actions - description: "Git actions to to execute. Supported actions: `clone|commit|push`." + - name: action + description: | + Git actions to be executed. Supported actions: `clone | commit | push | pages`. Specified actions are executed in the specified order + + - **clone:** Clones the repository in `remote` and checks out the `branch` to `path`. + - **commit:** Adds a commit to the default drone repository or the repository in `remote`. + - **push:** Pushes all commits to the default drone repository or the repository set in `remote`. + - **pages:** The `pages` action is a special action that cannot be combined with other actions. It is intended for use for + GitHub pages. It synchronizes the contents of `pages_directory` with the target `branch` using `rsync` and pushes the changes automatically. required: true type: list @@ -43,7 +50,7 @@ properties: type: string - name: path - description: Path to git repository. + description: Path to clone the git repository. type: string - name: message @@ -75,3 +82,17 @@ properties: description: Bypass the pre-commit and commit-msg hooks. defaultvalue: false type: bool + + - name: pages_directory + description: Source directory to be synchronized with the pages `branch`. + defaultvalue: docs/ + type: string + + - name: pages_exclude + description: Files or directories to exclude from the rsync command. + type: list + + - name: pages_delete + description: When set to `true`, the `--delete` flag is added to the rsync command to remove files from the `branch` that do not exist in the `pages_directory` either. + defaultvalue: true + type: bool diff --git a/cmd/drone-git-action/config.go b/cmd/drone-git-action/config.go index 26c36b0..fbb7a7c 100644 --- a/cmd/drone-git-action/config.go +++ b/cmd/drone-git-action/config.go @@ -9,33 +9,32 @@ import ( func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { return []cli.Flag{ &cli.StringSliceFlag{ - Name: "actions", - Usage: "git actions to to execute", - EnvVars: []string{"PLUGIN_ACTIONS"}, - Destination: &settings.Actions, + Name: "action", + Usage: "git action to to execute", + EnvVars: []string{"PLUGIN_ACTION"}, + Destination: &settings.Action, Required: true, Category: category, }, - &cli.StringFlag{ - Name: "commit-author-name", + Name: "author-name", Usage: "git author name", EnvVars: []string{"PLUGIN_AUTHOR_NAME", "DRONE_COMMIT_AUTHOR"}, - Destination: &settings.Commit.Author.Name, + Destination: &settings.Repo.Author.Name, Required: true, Category: category, }, &cli.StringFlag{ - Name: "commit-author-email", + Name: "author-email", Usage: "git author email", EnvVars: []string{"PLUGIN_AUTHOR_EMAIL", "DRONE_COMMIT_AUTHOR_EMAIL"}, - Destination: &settings.Commit.Author.Email, + Destination: &settings.Repo.Author.Email, Required: true, Category: category, }, &cli.StringFlag{ - Name: "netrc-machine", + Name: "netrc.machine", Usage: "netrc remote machine name", EnvVars: []string{"PLUGIN_NETRC_MACHINE", "DRONE_NETRC_MACHINE"}, Destination: &settings.Netrc.Machine, @@ -43,7 +42,7 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Category: category, }, &cli.StringFlag{ - Name: "netrc-username", + Name: "netrc.username", Usage: "netrc login user on the remote machine", EnvVars: []string{"PLUGIN_NETRC_USERNAME", "DRONE_NETRC_USERNAME"}, Destination: &settings.Netrc.Login, @@ -51,7 +50,7 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Category: category, }, &cli.StringFlag{ - Name: "netrc-password", + Name: "netrc.password", Usage: "netrc login password on the remote machine", EnvVars: []string{"PLUGIN_NETRC_PASSWORD", "DRONE_NETRC_PASSWORD"}, Destination: &settings.Netrc.Password, @@ -68,41 +67,38 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { &cli.StringFlag{ Name: "remote", Usage: "url of the remote repository", - EnvVars: []string{"PLUGIN_REMOTE"}, - Destination: &settings.Remote, + EnvVars: []string{"PLUGIN_REMOTE", "DRONE_REMOTE_URL"}, + Destination: &settings.Repo.RemoteURL, Category: category, }, &cli.StringFlag{ Name: "branch", - Usage: "name of the git branch", + Usage: "name of the git source branch", EnvVars: []string{"PLUGIN_BRANCH"}, - Destination: &settings.Branch, + Destination: &settings.Repo.Branch, Value: "main", Category: category, }, - &cli.StringFlag{ Name: "path", - Usage: "path to git repository", + Usage: "path to clone git repository", EnvVars: []string{"PLUGIN_PATH"}, - Destination: &settings.Path, + Destination: &settings.Repo.WorkDir, Category: category, }, - &cli.StringFlag{ - Name: "message", + Name: "commit-message", Usage: "commit message", EnvVars: []string{"PLUGIN_MESSAGE"}, - Destination: &settings.Message, + Destination: &settings.Repo.CommitMsg, Value: "[skip ci] commit dirty state", Category: category, }, - &cli.BoolFlag{ - Name: "force", + Name: "force-push", Usage: "enable force push to remote repository", EnvVars: []string{"PLUGIN_FORCE"}, - Destination: &settings.Force, + Destination: &settings.Repo.ForcePush, Value: false, Category: category, }, @@ -110,7 +106,7 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Name: "followtags", Usage: "follow tags for pushes to remote repository", EnvVars: []string{"PLUGIN_FOLLOWTAGS"}, - Destination: &settings.FollowTags, + Destination: &settings.Repo.PushFollowTags, Value: false, Category: category, }, @@ -118,7 +114,7 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Name: "insecure-ssl-verify", Usage: "set SSL verification of the remote machine", EnvVars: []string{"PLUGIN_INSECURE_SSL_VERIFY"}, - Destination: &settings.InsecureSSLVerify, + Destination: &settings.Repo.InsecureSSLVerify, Value: false, Category: category, }, @@ -126,7 +122,7 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Name: "empty-commit", Usage: "allow empty commits", EnvVars: []string{"PLUGIN_EMPTY_COMMIT"}, - Destination: &settings.EmptyCommit, + Destination: &settings.Repo.EmptyCommit, Value: false, Category: category, }, @@ -134,9 +130,32 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Name: "no-verify", Usage: "bypass the pre-commit and commit-msg hooks", EnvVars: []string{"PLUGIN_NO_VERIFY"}, - Destination: &settings.NoVerify, + Destination: &settings.Repo.NoVerify, Value: false, Category: category, }, + &cli.StringFlag{ + Name: "pages.directory", + Usage: "source directory for pages sync", + EnvVars: []string{"PLUGIN_PAGES_DIRECTORY"}, + Destination: &settings.Pages.Directory, + Value: "docs/", + Category: category, + }, + &cli.StringSliceFlag{ + Name: "pages.exclude", + Usage: "exclude flag added to pages rsnyc command", + EnvVars: []string{"PLUGIN_PAGES_EXCLUDE"}, + Destination: &settings.Pages.Exclude, + Category: category, + }, + &cli.BoolFlag{ + Name: "pages.delete", + Usage: "delete flag added to pages rsync command", + EnvVars: []string{"PLUGIN_PAGES_DELETE"}, + Destination: &settings.Pages.Delete, + Value: true, + Category: category, + }, } } diff --git a/docker/Dockerfile.amd64 b/docker/Dockerfile.amd64 index 89ca7b5..4c26f20 100644 --- a/docker/Dockerfile.amd64 +++ b/docker/Dockerfile.amd64 @@ -7,7 +7,7 @@ LABEL org.opencontainers.image.url="https://github.com/thegeeklab/drone-git-acti LABEL org.opencontainers.image.source="https://github.com/thegeeklab/drone-git-action" LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/drone-git-action" -RUN apk --update add --no-cache git && \ +RUN apk --update add --no-cache git rsync && \ rm -rf /var/cache/apk/* && \ rm -rf /tmp/* diff --git a/docker/Dockerfile.arm b/docker/Dockerfile.arm index 315d7a7..63a9c9a 100644 --- a/docker/Dockerfile.arm +++ b/docker/Dockerfile.arm @@ -7,7 +7,7 @@ LABEL org.opencontainers.image.url="https://github.com/thegeeklab/drone-git-acti LABEL org.opencontainers.image.source="https://github.com/thegeeklab/drone-git-action" LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/drone-git-action" -RUN apk --update add --no-cache git && \ +RUN apk --update add --no-cache git rsync && \ rm -rf /var/cache/apk/* && \ rm -rf /tmp/* diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index 5e996d6..a632639 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -7,7 +7,7 @@ LABEL org.opencontainers.image.url="https://github.com/thegeeklab/drone-git-acti LABEL org.opencontainers.image.source="https://github.com/thegeeklab/drone-git-action" LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/drone-git-action" -RUN apk --update add --no-cache git && \ +RUN apk --update add --no-cache git rsync && \ rm -rf /var/cache/apk/* && \ rm -rf /tmp/* diff --git a/git/clone.go b/git/clone.go new file mode 100644 index 0000000..3621018 --- /dev/null +++ b/git/clone.go @@ -0,0 +1,35 @@ +package git + +import ( + "fmt" + "os" + "os/exec" +) + +// FetchSource fetches the source from remote. +func FetchSource(repo Repository) *exec.Cmd { + cmd := exec.Command( + "git", + "fetch", + "origin", + fmt.Sprintf("+%s:", repo.Branch), + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr + + return cmd +} + +// CheckoutHead handles branch checkout. +func CheckoutHead(repo Repository) *exec.Cmd { + cmd := exec.Command( + "git", + "checkout", + "-qf", + repo.Branch, + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr + + return cmd +} diff --git a/git/commit.go b/git/commit.go index 77113dc..d2fa230 100644 --- a/git/commit.go +++ b/git/commit.go @@ -1,74 +1,89 @@ package git import ( + "os" "os/exec" ) // ForceAdd forces the addition of all dirty files. -func ForceAdd() *exec.Cmd { +func ForceAdd(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "add", "--all", - "--force") + "--force", + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } // Add updates the index to match the working tree. -func Add() *exec.Cmd { +func Add(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "add", - "--all") + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr + + if repo.Add != "" { + cmd.Args = append(cmd.Args, repo.Add) + } else { + cmd.Args = append(cmd.Args, "--all") + } return cmd } // TestCleanTree returns non-zero if diff between index and local repository -func TestCleanTree() *exec.Cmd { +func TestCleanTree(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "diff-index", "--quiet", "HEAD", - "--ignore-submodules") + "--ignore-submodules", + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } // EmptyCommit simply create an empty commit -func EmptyCommit(msg string, noVerify bool) *exec.Cmd { +func EmptyCommit(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "commit", "--allow-empty", "-m", - msg, + repo.CommitMsg, ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr - if noVerify { - cmd.Args = append( - cmd.Args, - "--no-verify") + if repo.NoVerify { + cmd.Args = append(cmd.Args, "--no-verify") } return cmd } // ForceCommit commits every change while skipping CI. -func ForceCommit(msg string, noVerify bool) *exec.Cmd { +func ForceCommit(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "commit", "-m", - msg, + repo.CommitMsg, ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr - if noVerify { - cmd.Args = append( - cmd.Args, - "--no-verify") + if repo.NoVerify { + cmd.Args = append(cmd.Args, "--no-verify") } return cmd diff --git a/git/config.go b/git/config.go index 1b9d5c0..add7d9e 100644 --- a/git/config.go +++ b/git/config.go @@ -1,42 +1,67 @@ package git import ( + "os" "os/exec" "strconv" ) -// SetUserEmail sets the global git author email. -func SetUserEmail(email string) *exec.Cmd { +// repoUserEmail sets the global git author email. +func ConfigAutocorrect(repo Repository) *exec.Cmd { + cmd := exec.Command( + "git", + "config", + "--local", + "help.autocorrect", + repo.Autocorrect, + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr + + return cmd +} + +// repoUserEmail sets the global git author email. +func ConfigUserEmail(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "config", "--local", "user.email", - email) + repo.Author.Email, + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } -// SetUserName sets the global git author name. -func SetUserName(author string) *exec.Cmd { +// repoUserName sets the global git author name. +func ConfigUserName(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "config", "--local", "user.name", - author) + repo.Author.Name, + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } -// SetSSLSkipVerify disables globally the git ssl verification. -func SetSSLVerify(sslVerify bool) *exec.Cmd { +// repoSSLVerify disables globally the git ssl verification. +func ConfigSSLVerify(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "config", "--local", "http.sslVerify", - strconv.FormatBool(sslVerify)) + strconv.FormatBool(repo.SSLVerify), + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } diff --git a/git/remote.go b/git/remote.go index 24a08a0..331f8bc 100644 --- a/git/remote.go +++ b/git/remote.go @@ -1,52 +1,59 @@ package git import ( + "fmt" + "os" "os/exec" ) // RemoteRemove drops the defined remote from a git repo. -func RemoteRemove(name string) *exec.Cmd { +func RemoteRemove(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "remote", "rm", - name) + repo.RemoteName, + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } // RemoteAdd adds an additional remote to a git repo. -func RemoteAdd(name, url string) *exec.Cmd { +func RemoteAdd(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "remote", "add", - name, - url) + repo.RemoteName, + repo.RemoteURL, + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr return cmd } // RemotePush pushs the changes from the local head to a remote branch. -func RemotePush(remote, branch string, force, followtags bool) *exec.Cmd { - return RemotePushNamedBranch(remote, "HEAD", branch, force, followtags) -} - -// RemotePushNamedBranch puchs changes from a local to a remote branch. -func RemotePushNamedBranch(remote, localbranch, branch string, force, followtags bool) *exec.Cmd { +func RemotePush(repo Repository) *exec.Cmd { cmd := exec.Command( "git", "push", - remote, - localbranch+":"+branch) + repo.RemoteName, + fmt.Sprintf("HEAD:%s", repo.Branch), + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr - if force { + if repo.ForcePush { cmd.Args = append( cmd.Args, - "--force") + "--force", + ) } - if followtags { + if repo.PushFollowTags { cmd.Args = append( cmd.Args, "--follow-tags") diff --git a/git/status.go b/git/status.go new file mode 100644 index 0000000..225f05e --- /dev/null +++ b/git/status.go @@ -0,0 +1,42 @@ +package git + +import ( + "bytes" + "fmt" + "os" + "os/exec" +) + +func Status(repo Repository) *exec.Cmd { + cmd := exec.Command( + "git", + "status", + "--porcelain", + ) + cmd.Dir = repo.WorkDir + cmd.Stderr = os.Stderr + + return cmd +} + +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) + if err != nil { + return false + } + + if res.Len() > 0 { + fmt.Print(res.String()) + return true + } + + return false +} diff --git a/git/type.go b/git/type.go new file mode 100644 index 0000000..0114333 --- /dev/null +++ b/git/type.go @@ -0,0 +1,27 @@ +package git + +type Author struct { + Name string + Email string +} + +type Repository struct { + RemoteName string + RemoteURL string + Branch string + + Add string + CommitMsg string + + Autocorrect string + NoVerify bool + InsecureSSLVerify bool + EmptyCommit bool + PushFollowTags bool + ForcePush bool + SSLVerify bool + WorkDir string + InitExists bool + + Author Author +} diff --git a/git/utils.go b/git/utils.go index 95e3cea..7bf489e 100644 --- a/git/utils.go +++ b/git/utils.go @@ -3,8 +3,10 @@ package git import ( "fmt" "os" + "os/exec" "os/user" "path/filepath" + "strings" ) const netrcFile = ` @@ -27,17 +29,13 @@ func WriteSSHKey(privateKey string) error { home = currentUser.HomeDir } - sshpath := filepath.Join( - home, - ".ssh") + sshpath := filepath.Join(home, ".ssh") if err := os.MkdirAll(sshpath, 0o700); err != nil { return err } - confpath := filepath.Join( - sshpath, - "config") + confpath := filepath.Join(sshpath, "config") if err := os.WriteFile( confpath, @@ -47,10 +45,7 @@ func WriteSSHKey(privateKey string) error { return err } - privpath := filepath.Join( - sshpath, - "id_rsa", - ) + privpath := filepath.Join(sshpath, "id_rsa") if err := os.WriteFile( privpath, @@ -89,3 +84,20 @@ func WriteNetrc(machine, login, password string) error { 0o600, ) } + +func trace(cmd *exec.Cmd) { + fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) +} + +func runCommand(cmd *exec.Cmd) error { + if cmd.Stdout == nil { + cmd.Stdout = os.Stdout + } + + if cmd.Stderr == nil { + cmd.Stderr = os.Stderr + } + + trace(cmd) + return cmd.Run() +} diff --git a/plugin/git.go b/plugin/git.go deleted file mode 100644 index 5065b3a..0000000 --- a/plugin/git.go +++ /dev/null @@ -1,111 +0,0 @@ -package plugin - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/thegeeklab/drone-git-action/git" -) - -// InitRepo initializes the repository. -func (p Plugin) initRepo() error { - path := filepath.Join(p.settings.Path, ".git") - if err := os.MkdirAll(p.settings.Path, os.ModePerm); err != nil { - return err - } - - if err := os.Chdir(p.settings.Path); err != nil { - return err - } - - if isDirEmpty(path) { - return execute(exec.Command( - "git", - "init", - )) - } - - return nil -} - -// AddRemote adds a remote to repository. -func (p Plugin) addRemote() error { - if p.settings.Remote != "" { - if err := execute(git.RemoteAdd("origin", p.settings.Remote)); err != nil { - return err - } - } - - return nil -} - -// FetchSource fetches the source from remote. -func (p Plugin) fetchSource() error { - return execute(exec.Command( - "git", - "fetch", - "origin", - fmt.Sprintf("+%s:", p.settings.Branch), - )) -} - -// CheckoutHead handles branch checkout. -func (p Plugin) checkoutHead() error { - return execute(exec.Command( - "git", - "checkout", - "-qf", - p.settings.Branch, - )) -} - -// HandleClone clones remote. -func (p Plugin) handleClone() error { - if err := p.addRemote(); err != nil { - return err - } - - if err := p.fetchSource(); err != nil { - return err - } - - if err := p.checkoutHead(); err != nil { - return err - } - - return nil -} - -// HandleCommit commits changes locally. -func (p Plugin) handleCommit() error { - if err := execute(git.Add()); err != nil { - return err - } - - if err := execute(git.TestCleanTree()); err != nil { - if err := execute(git.ForceCommit(p.settings.Message, p.settings.NoVerify)); err != nil { - return err - } - } else { - if p.settings.EmptyCommit { - if err := execute(git.EmptyCommit(p.settings.Message, p.settings.NoVerify)); err != nil { - return err - } - } - } - - return nil -} - -// HandlePush pushs changes to remote. -func (p Plugin) handlePush() error { - return execute(git.RemotePushNamedBranch( - "origin", - p.settings.Branch, - p.settings.Branch, - p.settings.Force, - p.settings.FollowTags, - )) -} diff --git a/plugin/impl.go b/plugin/impl.go index 0fd6276..21cf214 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -3,6 +3,8 @@ package plugin import ( "fmt" "os" + "os/exec" + "path/filepath" "github.com/thegeeklab/drone-git-action/git" "github.com/urfave/cli/v2" @@ -14,37 +16,38 @@ type Netrc struct { Password string } -type Commit struct { - Author Author -} - -type Author struct { - Name string - Email string +type Pages struct { + Directory string + Exclude cli.StringSlice + Delete bool } // Settings for the Plugin. type Settings struct { - Actions cli.StringSlice - SSHKey string - Remote string - Branch string - Path string - Message string - Force bool - FollowTags bool - InsecureSSLVerify bool - EmptyCommit bool - NoVerify bool + Action cli.StringSlice + SSHKey string - Netrc Netrc - Commit Commit - Author Author + Netrc Netrc + Pages Pages + Repo git.Repository } // Validate handles the settings validation of the plugin. func (p *Plugin) Validate() error { - for _, action := range p.settings.Actions.Value() { + var err error + + p.settings.Repo.Autocorrect = "never" + p.settings.Repo.RemoteName = "origin" + p.settings.Repo.Add = "" + + if p.settings.Repo.WorkDir == "" { + p.settings.Repo.WorkDir, err = os.Getwd() + } + if err != nil { + return err + } + + for _, action := range p.settings.Action.Value() { switch action { case "clone": continue @@ -52,7 +55,30 @@ func (p *Plugin) Validate() error { continue case "push": if p.settings.SSHKey == "" && p.settings.Netrc.Password == "" { - return fmt.Errorf("either SSH key or netrc password are required") + return fmt.Errorf("either SSH key or netrc password is required") + } + case "pages": + 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") + + if _, err := os.Stat(p.settings.Pages.Directory); os.IsNotExist(err) { + return fmt.Errorf("pages directory '%s' must exist", p.settings.Pages.Directory) + } + + if info, _ := os.Stat(p.settings.Pages.Directory); !info.IsDir() { + return fmt.Errorf("pages directory '%s' is not a directory", p.settings.Pages.Directory) + } + + if p.settings.SSHKey == "" && p.settings.Netrc.Password == "" { + return fmt.Errorf("either SSH key or netrc password is required") + } + + if p.settings.Pages.Directory == "" { + return fmt.Errorf("pages source directory needs to be set") + } + + if len(p.settings.Action.Value()) > 1 { + return fmt.Errorf("pages action can not be combined with other actions") } default: return fmt.Errorf("unknown action %s", action) @@ -81,19 +107,20 @@ func (p *Plugin) Execute() error { return err } - if p.settings.Path != "" { - if err := p.initRepo(); err != nil { - return err - } + if err := p.initRepo(); err != nil { + return err } - if err := git.SetUserName(p.settings.Commit.Author.Name).Run(); err != nil { + if err := git.ConfigAutocorrect(p.settings.Repo).Run(); err != nil { return err } - if err := git.SetUserEmail(p.settings.Commit.Author.Email).Run(); err != nil { + if err := git.ConfigUserName(p.settings.Repo).Run(); err != nil { return err } - if err := git.SetSSLVerify(p.settings.InsecureSSLVerify).Run(); err != nil { + if err := git.ConfigUserEmail(p.settings.Repo).Run(); err != nil { + return err + } + if err := git.ConfigSSLVerify(p.settings.Repo).Run(); err != nil { return err } @@ -107,7 +134,7 @@ func (p *Plugin) Execute() error { return err } - for _, action := range p.settings.Actions.Value() { + for _, action := range p.settings.Action.Value() { switch action { case "clone": if err := p.handleClone(); err != nil { @@ -121,8 +148,103 @@ func (p *Plugin) Execute() error { if err := p.handlePush(); err != nil { return err } + case "pages": + if err := p.handlePages(); err != nil { + return err + } } } return nil } + +// InitRepo initializes the repository. +func (p *Plugin) initRepo() error { + path := filepath.Join(p.settings.Repo.WorkDir, ".git") + 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 + } + + cmd := exec.Command( + "git", + "init", + ) + cmd.Dir = p.settings.Repo.WorkDir + + return execute(cmd) +} + +// HandleClone clones remote. +func (p *Plugin) handleClone() error { + if p.settings.Repo.InitExists { + return fmt.Errorf("destination '%s' already exists and is not an empty directory", p.settings.Repo.WorkDir) + } + + if p.settings.Repo.RemoteURL != "" { + if err := execute(git.RemoteAdd(p.settings.Repo)); err != nil { + return err + } + } + + if err := execute(git.FetchSource(p.settings.Repo)); err != nil { + return err + } + + if err := execute(git.CheckoutHead(p.settings.Repo)); err != nil { + return err + } + + return nil +} + +// HandleCommit commits changes locally. +func (p *Plugin) handleCommit() error { + if err := execute(git.Add(p.settings.Repo)); err != nil { + return err + } + + if err := execute(git.TestCleanTree(p.settings.Repo)); err != nil { + if err := execute(git.ForceCommit(p.settings.Repo)); err != nil { + return err + } + } else { + if p.settings.Repo.EmptyCommit { + if err := execute(git.EmptyCommit(p.settings.Repo)); err != nil { + return err + } + } + } + + return nil +} + +// HandlePush pushs changes to remote. +func (p *Plugin) handlePush() error { + return execute(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 { + defer os.RemoveAll(p.settings.Repo.WorkDir) + + if err := p.handleClone(); err != nil { + return err + } + + if err := execute( + rsyncDirectories(p.settings.Pages, p.settings.Repo), + ); err != nil { + return err + } + + if err := p.handleCommit(); err != nil { + return err + } + + return p.handlePush() +} diff --git a/plugin/utils.go b/plugin/utils.go index de05972..1d931da 100644 --- a/plugin/utils.go +++ b/plugin/utils.go @@ -2,10 +2,11 @@ package plugin import ( "fmt" - "io" "os" "os/exec" "strings" + + "github.com/thegeeklab/drone-git-action/git" ) // helper function to simply wrap os execte command. @@ -19,15 +20,39 @@ func execute(cmd *exec.Cmd) error { return cmd.Run() } -// helper function returns true if directory dir is empty. -func isDirEmpty(dir string) bool { - f, err := os.Open(dir) - if err != nil { - return true +func rsyncDirectories(pages Pages, repo git.Repository) *exec.Cmd { + args := []string{ + "-r", + "--exclude", + ".git", } - defer f.Close() + for _, item := range pages.Exclude.Value() { + args = append( + args, + "--exclude", + item, + ) + } - _, err = f.Readdir(1) - return err == io.EOF + if pages.Delete { + args = append( + args, + "--delete", + ) + } + + args = append( + args, + ".", + repo.WorkDir, + ) + + cmd := exec.Command( + "rsync", + args..., + ) + cmd.Dir = pages.Directory + + return cmd }