diff --git a/docs/configuration.md b/docs/configuration.md index 6cc3e41c..25788bb8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -242,9 +242,9 @@ Whether run commands and scripts in concurrently. ### `piped` -**Default: `true`** +**Default: `false`** -Whether run commands and scripts sequentially. +Whether run commands and scripts sequentially. Will stop execution if one of the commands/scripts fail. **Example** @@ -252,7 +252,7 @@ Whether run commands and scripts sequentially. # lefthook.yml database: - piped: true # if you perfer explicit configuration + piped: true # Stop if one of the steps fail commands: 1_create: run: rake db:create diff --git a/internal/lefthook/runner/execute_unix.go b/internal/lefthook/runner/execute_unix.go index 2e138253..78e1f45f 100644 --- a/internal/lefthook/runner/execute_unix.go +++ b/internal/lefthook/runner/execute_unix.go @@ -31,29 +31,47 @@ func (e CommandExecutor) Execute(opts ExecuteOptions) (*bytes.Buffer, error) { log.Errorf("Couldn't enable TTY input: %s\n", err) } } + command := exec.Command("sh", "-c", strings.Join(opts.args, " ")) + rootDir, _ := filepath.Abs(opts.root) command.Dir = rootDir envList := make([]string, len(opts.env)) for name, value := range opts.env { - envList = append(envList, fmt.Sprintf("%s=%s", strings.ToUpper(name), value)) + envList = append( + envList, + fmt.Sprintf("%s=%s", strings.ToUpper(name), value), + ) } command.Env = append(os.Environ(), envList...) - p, err := pty.Start(command) - if err != nil { - return nil, err - } + var out *bytes.Buffer - defer func() { _ = p.Close() }() - defer func() { _ = command.Process.Kill() }() + if opts.interactive { + command.Stdout = os.Stdout + command.Stdin = stdin + command.Stderr = os.Stderr + err := command.Start() + if err != nil { + return nil, err + } + } else { + p, err := pty.Start(command) + if err != nil { + return nil, err + } + + defer func() { _ = p.Close() }() - go func() { _, _ = io.Copy(p, stdin) }() + go func() { _, _ = io.Copy(p, stdin) }() - out := bytes.NewBuffer(make([]byte, 0)) - _, _ = io.Copy(out, p) + out = bytes.NewBuffer(make([]byte, 0)) + _, _ = io.Copy(out, p) + } + + defer func() { _ = command.Process.Kill() }() return out, command.Wait() } diff --git a/internal/lefthook/runner/execute_windows.go b/internal/lefthook/runner/execute_windows.go index ba951b80..fce93814 100644 --- a/internal/lefthook/runner/execute_windows.go +++ b/internal/lefthook/runner/execute_windows.go @@ -18,19 +18,27 @@ func (e CommandExecutor) Execute(opts ExecuteOptions) (*bytes.Buffer, error) { CmdLine: strings.Join(opts.args, " "), } + rootDir, _ := filepath.Abs(opts.root) + command.Dir = rootDir + envList := make([]string, len(opts.env)) for name, value := range opts.env { - envList = append(envList, fmt.Sprintf("%s=%s", strings.ToUpper(name), value)) + envList = append( + envList, + fmt.Sprintf("%s=%s", strings.ToUpper(name), value), + ) } command.Env = append(os.Environ(), envList...) - rootDir, _ := filepath.Abs(opts.root) - command.Dir = rootDir - var out bytes.Buffer - command.Stdout = &out + if opts.interactive { + command.Stdout = os.Stdout + } else { + command.Stdout = &out + } + command.Stdin = os.Stdin command.Stderr = os.Stderr err := command.Start() @@ -38,7 +46,7 @@ func (e CommandExecutor) Execute(opts ExecuteOptions) (*bytes.Buffer, error) { return nil, err } - defer command.Process.Kill() + defer func() { _ = command.Process.Kill() }() return &out, command.Wait() } diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index e90b3971..237ac0ea 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -9,6 +9,7 @@ import ( "sort" "strings" "sync" + "sync/atomic" "github.com/spf13/afero" "gopkg.in/alessio/shellescape.v1" @@ -33,7 +34,7 @@ type Runner struct { repo *git.Repository hook *config.Hook args []string - failed bool + failed atomic.Bool resultChan chan Result exec Executor logSettings log.SkipSettings @@ -78,12 +79,13 @@ func (r *Runner) RunAll(hookName string, sourceDirs []string) { for _, dir := range scriptDirs { r.runScripts(dir) } + r.runCommands() } func (r *Runner) fail(name, text string) { r.resultChan <- resultFail(name, text) - r.failed = true + r.failed.Store(true) } func (r *Runner) success(name string) { @@ -141,7 +143,9 @@ func (r *Runner) runScripts(dir string) { return } + interactiveScripts := make([]os.FileInfo, 0) var wg sync.WaitGroup + for _, file := range files { script, ok := r.hook.Scripts[file.Name()] if !ok { @@ -149,11 +153,16 @@ func (r *Runner) runScripts(dir string) { continue } - if r.failed && r.hook.Piped { + if r.failed.Load() && r.hook.Piped { logSkip(file.Name(), "(SKIP BY BROKEN PIPE)") continue } + if script.Interactive { + interactiveScripts = append(interactiveScripts, file) + continue + } + unquotedScriptPath := filepath.Join(dir, file.Name()) if r.hook.Parallel { @@ -168,6 +177,18 @@ func (r *Runner) runScripts(dir string) { } wg.Wait() + + for _, file := range interactiveScripts { + script := r.hook.Scripts[file.Name()] + if r.failed.Load() { + logSkip(file.Name(), "(SKIP INTERACTIVE BY FAILED)") + continue + } + + unquotedScriptPath := filepath.Join(dir, file.Name()) + + r.runScript(script, unquotedScriptPath, file) + } } func (r *Runner) runScript(script *config.Script, unquotedPath string, file os.FileInfo) { @@ -205,6 +226,11 @@ func (r *Runner) runScript(script *config.Script, unquotedPath string, file os.F args = append(args, quotedScriptPath) args = append(args, r.args[:]...) + if script.Interactive { + log.StopSpinner() + defer log.StartSpinner() + } + r.run(ExecuteOptions{ name: file.Name(), root: r.repo.RootPath, @@ -223,13 +249,20 @@ func (r *Runner) runCommands() { sort.Strings(commands) + interactiveCommands := make([]string, 0) var wg sync.WaitGroup + for _, name := range commands { - if r.failed && r.hook.Piped { + if r.failed.Load() && r.hook.Piped { logSkip(name, "(SKIP BY BROKEN PIPE)") continue } + if r.hook.Commands[name].Interactive { + interactiveCommands = append(interactiveCommands, name) + continue + } + if r.hook.Parallel { wg.Add(1) go func(name string, command *config.Command) { @@ -242,6 +275,15 @@ func (r *Runner) runCommands() { } wg.Wait() + + for _, name := range interactiveCommands { + if r.failed.Load() { + logSkip(name, "(SKIP INTERACTIVE BY FAILED)") + continue + } + + r.runCommand(name, r.hook.Commands[name]) + } } func (r *Runner) runCommand(name string, command *config.Command) { @@ -276,6 +318,11 @@ func (r *Runner) runCommand(name string, command *config.Command) { return } + if command.Interactive { + log.StopSpinner() + defer log.StartSpinner() + } + r.run(ExecuteOptions{ name: name, root: filepath.Join(r.repo.RootPath, command.Root), diff --git a/internal/lefthook/runner/runner_test.go b/internal/lefthook/runner/runner_test.go index cb9799c7..2ca73ba2 100644 --- a/internal/lefthook/runner/runner_test.go +++ b/internal/lefthook/runner/runner_test.go @@ -209,6 +209,37 @@ func TestRunAll(t *testing.T) { success: []Result{{Name: "script.sh", Status: StatusOk}}, fail: []Result{{Name: "failing.js", Status: StatusErr, Text: "install node"}}, }, + { + name: "with interactive and parallel", + sourceDirs: []string{filepath.Join(root, config.DefaultSourceDir)}, + existingFiles: []string{ + filepath.Join(root, config.DefaultSourceDir, hookName, "script.sh"), + filepath.Join(root, config.DefaultSourceDir, hookName, "failing.js"), + }, + hook: &config.Hook{ + Parallel: true, + Commands: map[string]*config.Command{ + "ok": { + Run: "success", + Interactive: true, + }, + "fail": { + Run: "fail", + }, + }, + Scripts: map[string]*config.Script{ + "script.sh": { + Runner: "success", + Interactive: true, + }, + "failing.js": { + Runner: "fail", + }, + }, + }, + success: []Result{}, // script.sh and ok are skipped + fail: []Result{{Name: "failing.js", Status: StatusErr}, {Name: "fail", Status: StatusErr}}, + }, } { fs := afero.NewMemMapFs() repo.Fs = fs