diff --git a/internal/git/exec.go b/internal/git/exec.go index 78f655cf..91ae864e 100644 --- a/internal/git/exec.go +++ b/internal/git/exec.go @@ -3,7 +3,6 @@ package git import ( "os" "os/exec" - "runtime" "strings" "github.com/evilmartians/lefthook/internal/log" @@ -11,10 +10,8 @@ import ( type Exec interface { SetRootPath(root string) - Cmd(cmd string) (string, error) - CmdArgs(args ...string) (string, error) - CmdLines(cmd string) ([]string, error) - RawCmd(cmd string) (string, error) + Cmd(cmd []string) (string, error) + CmdLines(cmd []string) ([]string, error) } type OsExec struct { @@ -32,24 +29,8 @@ func (o *OsExec) SetRootPath(root string) { } // Cmd runs plain string command. Trims spaces around output. -func (o *OsExec) Cmd(cmd string) (string, error) { - args := strings.Split(cmd, " ") - return o.CmdArgs(args...) -} - -// CmdLines runs plain string command, returns its output split by newline. -func (o *OsExec) CmdLines(cmd string) ([]string, error) { - out, err := o.RawCmd(cmd) - if err != nil { - return nil, err - } - - return strings.Split(out, "\n"), nil -} - -// CmdArgs runs a command provided with separated words. Trims spaces around output. -func (o *OsExec) CmdArgs(args ...string) (string, error) { - out, err := o.rawExecArgs(args...) +func (o *OsExec) Cmd(cmd []string) (string, error) { + out, err := o.rawExecArgs(cmd) if err != nil { return "", err } @@ -57,21 +38,19 @@ func (o *OsExec) CmdArgs(args ...string) (string, error) { return strings.TrimSpace(out), nil } -// RawCmd runs a plain string command returning unprocessed output as string. -func (o *OsExec) RawCmd(cmd string) (string, error) { - var args []string - if runtime.GOOS == "windows" { - args = strings.Split(cmd, " ") - } else { - args = []string{"sh", "-c", cmd} +// CmdLines runs plain string command, returns its output split by newline. +func (o *OsExec) CmdLines(cmd []string) ([]string, error) { + out, err := o.rawExecArgs(cmd) + if err != nil { + return nil, err } - return o.rawExecArgs(args...) + return strings.Split(strings.TrimSpace(out), "\n"), nil } // rawExecArgs executes git command with LEFTHOOK=0 in order // to prevent calling subsequent lefthook hooks. -func (o *OsExec) rawExecArgs(args ...string) (string, error) { +func (o *OsExec) rawExecArgs(args []string) (string, error) { log.Debug("[lefthook] cmd: ", args) cmd := exec.Command(args[0], args[1:]...) diff --git a/internal/git/remote.go b/internal/git/remote.go index 0bd29fe4..be2bd01f 100644 --- a/internal/git/remote.go +++ b/internal/git/remote.go @@ -60,22 +60,22 @@ func (r *Repository) updateRemote(path, ref string) error { log.Debugf("Updating remote config repository: %s", path) if len(ref) != 0 { - _, err := r.Git.CmdArgs( + _, err := r.Git.Cmd([]string{ "git", "-C", path, "fetch", "--quiet", "--depth", "1", "origin", ref, - ) + }) if err != nil { return err } - _, err = r.Git.CmdArgs( + _, err = r.Git.Cmd([]string{ "git", "-C", path, "checkout", "FETCH_HEAD", - ) + }) if err != nil { return err } } else { - _, err := r.Git.CmdArgs("git", "-C", path, "pull", "--quiet") + _, err := r.Git.Cmd([]string{"git", "-C", path, "pull", "--quiet"}) if err != nil { return err } @@ -93,7 +93,7 @@ func (r *Repository) cloneRemote(path, url, ref string) error { } cmdClone = append(cmdClone, url) - _, err := r.Git.CmdArgs(cmdClone...) + _, err := r.Git.Cmd(cmdClone) if err != nil { return err } diff --git a/internal/git/repository.go b/internal/git/repository.go index 693ab6e4..c1dec4b9 100644 --- a/internal/git/repository.go +++ b/internal/git/repository.go @@ -1,7 +1,6 @@ package git import ( - "fmt" "os" "path/filepath" "regexp" @@ -11,25 +10,29 @@ import ( ) const ( - cmdRootPath = "git rev-parse --show-toplevel" - cmdHooksPath = "git rev-parse --git-path hooks" - cmdInfoPath = "git rev-parse --git-path info" - cmdGitPath = "git rev-parse --git-dir" - cmdStagedFiles = "git diff --name-only --cached --diff-filter=ACMR" - cmdAllFiles = "git ls-files --cached" - cmdPushFilesBase = "git diff --name-only HEAD @{push}" - cmdPushFilesHead = "git diff --name-only HEAD %s" - cmdStatusShort = "git status --short" - cmdCreateStash = "git stash create" - cmdListStash = "git stash list" - stashMessage = "lefthook auto backup" unstagedPatchName = "lefthook-unstaged.patch" infoDirMode = 0o775 minStatusLen = 3 ) -var headBranchRegexp = regexp.MustCompile(`HEAD -> (?P.*)$`) +var ( + headBranchRegexp = regexp.MustCompile(`HEAD -> (?P.*)$`) + cmdPushFilesBase = []string{"git", "diff", "--name-only", "HEAD", "@{push}"} + cmdPushFilesHead = []string{"git", "diff", "--name-only", "HEAD"} + cmdStagedFiles = []string{"git", "diff", "--name-only", "--cached", "--diff-filter=ACMR"} + cmdStatusShort = []string{"git", "status", "--short"} + cmdListStash = []string{"git", "stash", "list"} + cmdRootPath = []string{"git", "rev-parse", "--show-toplevel"} + cmdHooksPath = []string{"git", "rev-parse", "--git-path", "hooks"} + cmdInfoPath = []string{"git", "rev-parse", "--git-path", "info"} + cmdGitPath = []string{"git", "rev-parse", "--git-dir"} + cmdAllFiles = []string{"git", "ls-files", "--cached"} + cmdCreateStash = []string{"git", "stash", "create"} + cmdStageFiles = []string{"git", "add"} + cmdRemotes = []string{"git", "branch", " --remotes"} + cmdHideUnstaged = []string{"git", "checkout", "--force", "--"} +) // Repository represents a git repository. type Repository struct { @@ -112,7 +115,7 @@ func (r *Repository) PushFiles() ([]string, error) { } if len(r.headBranch) == 0 { - branches, err := r.Git.CmdLines("git branch --remotes") + branches, err := r.Git.CmdLines(cmdRemotes) if err != nil { return nil, err } @@ -126,7 +129,7 @@ func (r *Repository) PushFiles() ([]string, error) { break } } - return r.FilesByCommand(fmt.Sprintf(cmdPushFilesHead, r.headBranch)) + return r.FilesByCommand(append(cmdPushFilesHead, r.headBranch)) } // PartiallyStagedFiles returns the list of files that have both staged and @@ -163,7 +166,7 @@ func (r *Repository) PartiallyStagedFiles() ([]string, error) { } func (r *Repository) SaveUnstaged(files []string) error { - _, err := r.Git.CmdArgs( + _, err := r.Git.Cmd( append([]string{ "git", "diff", @@ -178,21 +181,14 @@ func (r *Repository) SaveUnstaged(files []string) error { "--output", r.unstagedPatchPath, "--", - }, files...)..., + }, files...), ) return err } func (r *Repository) HideUnstaged(files []string) error { - _, err := r.Git.CmdArgs( - append([]string{ - "git", - "checkout", - "--force", - "--", - }, files...)..., - ) + _, err := r.Git.Cmd(append(cmdHideUnstaged, files...)) return err } @@ -202,7 +198,7 @@ func (r *Repository) RestoreUnstaged() error { return nil } - _, err := r.Git.CmdArgs( + _, err := r.Git.Cmd([]string{ "git", "apply", "-v", @@ -210,7 +206,7 @@ func (r *Repository) RestoreUnstaged() error { "--recount", "--unidiff-zero", r.unstagedPatchPath, - ) + }) if err == nil { err = r.Fs.Remove(r.unstagedPatchPath) @@ -225,7 +221,7 @@ func (r *Repository) StashUnstaged() error { return err } - _, err = r.Git.CmdArgs( + _, err = r.Git.Cmd([]string{ "git", "stash", "store", @@ -233,7 +229,7 @@ func (r *Repository) StashUnstaged() error { "--message", stashMessage, stashHash, - ) + }) if err != nil { return err } @@ -258,13 +254,13 @@ func (r *Repository) DropUnstagedStash() error { stashID := stashRegexp.SubexpIndex("stash") if len(matches[stashID]) > 0 { - _, err := r.Git.CmdArgs( + _, err := r.Git.Cmd([]string{ "git", "stash", "drop", "--quiet", matches[stashID], - ) + }) if err != nil { return err } @@ -279,15 +275,15 @@ func (r *Repository) AddFiles(files []string) error { return nil } - _, err := r.Git.CmdArgs( - append([]string{"git", "add"}, files...)..., + _, err := r.Git.Cmd( + append(cmdStageFiles, files...), ) return err } // FilesByCommand accepts git command and returns its result as a list of filepaths. -func (r *Repository) FilesByCommand(command string) ([]string, error) { +func (r *Repository) FilesByCommand(command []string) ([]string, error) { lines, err := r.Git.CmdLines(command) if err != nil { return nil, err diff --git a/internal/git/repository_test.go b/internal/git/repository_test.go index bab080ba..ea2be416 100644 --- a/internal/git/repository_test.go +++ b/internal/git/repository_test.go @@ -13,20 +13,16 @@ type GitMock struct { func (g GitMock) SetRootPath(_root string) {} -func (g GitMock) Cmd(cmd string) (string, error) { - res, err := g.RawCmd(cmd) - if err != nil { - return "", err +func (g GitMock) Cmd(cmd []string) (string, error) { + res, ok := g.cases[(strings.Join(cmd, " "))] + if !ok { + return "", errors.New("doesn't exist") } return strings.TrimSpace(res), nil } -func (g GitMock) CmdArgs(args ...string) (string, error) { - return g.Cmd(strings.Join(args, " ")) -} - -func (g GitMock) CmdLines(cmd string) ([]string, error) { +func (g GitMock) CmdLines(cmd []string) ([]string, error) { res, err := g.Cmd(cmd) if err != nil { return nil, err @@ -35,15 +31,6 @@ func (g GitMock) CmdLines(cmd string) ([]string, error) { return strings.Split(res, "\n"), nil } -func (g GitMock) RawCmd(cmd string) (string, error) { - res, ok := g.cases[cmd] - if !ok { - return "", errors.New("doesn't exist") - } - - return res, nil -} - func TestPartiallyStagedFiles(t *testing.T) { for i, tt := range [...]struct { name, gitOut string diff --git a/internal/lefthook/run/prepare_command.go b/internal/lefthook/run/prepare_command.go index 644b8bb9..c984a624 100644 --- a/internal/lefthook/run/prepare_command.go +++ b/internal/lefthook/run/prepare_command.go @@ -88,7 +88,13 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) { config.PushFiles: r.Repo.PushFiles, config.SubAllFiles: r.Repo.AllFiles, config.SubFiles: func() ([]string, error) { - return r.Repo.FilesByCommand(filesCmd) + var cmd []string + if runtime.GOOS == "windows" { + cmd = strings.Split(filesCmd, " ") + } else { + cmd = []string{"sh", "-c", filesCmd} + } + return r.Repo.FilesByCommand(cmd) }, } diff --git a/internal/lefthook/run/runner_test.go b/internal/lefthook/run/runner_test.go index 9206aae0..243800c4 100644 --- a/internal/lefthook/run/runner_test.go +++ b/internal/lefthook/run/runner_test.go @@ -41,23 +41,16 @@ type GitMock struct { func (g *GitMock) SetRootPath(_root string) {} -func (g *GitMock) Cmd(cmd string) (string, error) { +func (g *GitMock) Cmd(cmd []string) (string, error) { g.mux.Lock() - g.commands = append(g.commands, cmd) - g.mux.Unlock() - - return "", nil -} - -func (g *GitMock) CmdArgs(args ...string) (string, error) { - g.mux.Lock() - g.commands = append(g.commands, strings.Join(args, " ")) + g.commands = append(g.commands, strings.Join(cmd, " ")) g.mux.Unlock() return "", nil } -func (g *GitMock) CmdLines(cmd string) ([]string, error) { +func (g *GitMock) CmdLines(args []string) ([]string, error) { + cmd := strings.Join(args, " ") g.mux.Lock() g.commands = append(g.commands, cmd) g.mux.Unlock() @@ -74,14 +67,6 @@ func (g *GitMock) CmdLines(cmd string) ([]string, error) { return nil, nil } -func (g *GitMock) RawCmd(cmd string) (string, error) { - g.mux.Lock() - g.commands = append(g.commands, cmd) - g.mux.Unlock() - - return "", nil -} - func (g *GitMock) reset() { g.mux.Lock() g.commands = []string{} diff --git a/internal/lefthook/run_test.go b/internal/lefthook/run_test.go index b984e1f5..ece670e7 100644 --- a/internal/lefthook/run_test.go +++ b/internal/lefthook/run_test.go @@ -14,22 +14,14 @@ type GitMock struct{} func (g GitMock) SetRootPath(_root string) {} -func (g GitMock) Cmd(_cmd string) (string, error) { +func (g GitMock) Cmd(_cmd []string) (string, error) { return "", nil } -func (g GitMock) CmdArgs(_args ...string) (string, error) { - return "", nil -} - -func (g GitMock) CmdLines(_cmd string) ([]string, error) { +func (g GitMock) CmdLines(_cmd []string) ([]string, error) { return nil, nil } -func (g GitMock) RawCmd(_cmd string) (string, error) { - return "", nil -} - func TestRun(t *testing.T) { root, err := filepath.Abs("src") if err != nil { diff --git a/internal/log/skip_settings.go b/internal/log/skip_settings.go index 6fbe6f27..47aa1fab 100644 --- a/internal/log/skip_settings.go +++ b/internal/log/skip_settings.go @@ -1,6 +1,8 @@ package log -import "strings" +import ( + "strings" +) const ( skipMeta = 1 << iota @@ -21,12 +23,14 @@ func (s *SkipSettings) ApplySettings(tags string, skipOutput interface{}) { switch typedSkipOutput := skipOutput.(type) { case bool: s.SkipAll(typedSkipOutput) - case []string: - if tags != "" { - typedSkipOutput = append(typedSkipOutput, strings.Split(tags, ",")...) - } + case []interface{}: for _, skipOption := range typedSkipOutput { - s.applySetting(skipOption) + s.applySetting(skipOption.(string)) + } + if tags != "" { + for _, skipOption := range strings.Split(tags, ",") { + s.applySetting(skipOption) + } } } } diff --git a/internal/log/skip_settings_test.go b/internal/log/skip_settings_test.go index 9eb12b53..15706964 100644 --- a/internal/log/skip_settings_test.go +++ b/internal/log/skip_settings_test.go @@ -11,7 +11,7 @@ func TestSkipSetting(t *testing.T) { results map[string]bool }{ { - settings: []string{}, + settings: []interface{}{}, results: map[string]bool{}, }, { @@ -19,14 +19,14 @@ func TestSkipSetting(t *testing.T) { results: map[string]bool{}, }, { - settings: []string{"failure", "execution"}, + settings: []interface{}{"failure", "execution"}, results: map[string]bool{ "failure": true, "execution": true, }, }, { - settings: []string{ + settings: []interface{}{ "meta", "summary", "success", diff --git a/testdata/sh_syntax_in_files.txt b/testdata/sh_syntax_in_files.txt new file mode 100644 index 00000000..a45c9a25 --- /dev/null +++ b/testdata/sh_syntax_in_files.txt @@ -0,0 +1,32 @@ +exec git init +exec lefthook install +exec git config user.email "you@example.com" +exec git config user.name "Your Name" + +exec lefthook run echo_files +stdout '1.txt 10.txt' + +-- lefthook.yml -- +skip_output: + - meta # Skips lefthook version printing + - summary # Skips summary block (successful and failed steps) printing + - empty_summary # Skips summary heading when there are no steps to run + - success # Skips successful steps printing + - failure # Skips failed steps printing + - execution_info # Skips printing `EXECUTE > ...` logging + - skips + +echo_files: + commands: + echo: + files: ls | grep 1 + run: echo {files} + +-- 1.txt -- +1.txt + +-- 10.txt -- +10.txt + +-- 20.txt -- +20.txt