diff --git a/internal/config/command.go b/internal/config/command.go index 8d4a73ed..8d618424 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -22,7 +22,8 @@ type Command struct { Root string `mapstructure:"root"` Exclude string `mapstructure:"exclude"` - FailText string `mapstructure:"fail_text"` + FailText string `mapstructure:"fail_text"` + Interactive bool `mapstructure:"interactive"` // DEPRECATED, will be deleted in 1.2.0 Runner string `mapstructure:"runner"` diff --git a/internal/config/script.go b/internal/config/script.go index ca774824..45186c7c 100644 --- a/internal/config/script.go +++ b/internal/config/script.go @@ -15,7 +15,8 @@ type Script struct { Skip interface{} `mapstructure:"skip"` Tags []string `mapstructure:"tags"` - FailText string `mapstructure:"fail_text"` + FailText string `mapstructure:"fail_text"` + Interactive bool `mapstructure:"interactive"` // DEPRECATED Run string `mapstructure:"run"` @@ -116,17 +117,21 @@ func unmarshalScripts(s map[string]interface{}) (map[string]*Script, error) { // // ```yaml // scripts: -// "example.sh": -// runner: bash +// +// "example.sh": +// runner: bash +// // ``` // // Unmarshals into this: // // ```yaml // scripts: -// example: -// sh: -// runner: bash +// +// example: +// sh: +// runner: bash +// // ``` // // This is not an expected behavior and cannot be controlled yet diff --git a/internal/lefthook/runner/execute_unix.go b/internal/lefthook/runner/execute_unix.go index 1f4d2370..c4dd1752 100644 --- a/internal/lefthook/runner/execute_unix.go +++ b/internal/lefthook/runner/execute_unix.go @@ -12,22 +12,40 @@ import ( "strings" "github.com/creack/pty" + "github.com/mattn/go-isatty" + + "github.com/evilmartians/lefthook/internal/log" ) type CommandExecutor struct{} -func (e CommandExecutor) Execute(root string, args []string) (*bytes.Buffer, error) { +func (e CommandExecutor) Execute(root string, args []string, interactive bool) (*bytes.Buffer, error) { + stdin := os.Stdin + if interactive && !isatty.IsTerminal(os.Stdin.Fd()) { + tty, err := os.Open("/dev/tty") + if err == nil { + defer tty.Close() + stdin = tty + } else { + log.Errorf("Couldn't enable TTY input: %s\n", err) + } + } command := exec.Command("sh", "-c", strings.Join(args, " ")) rootDir, _ := filepath.Abs(root) command.Dir = rootDir - ptyOut, err := pty.Start(command) + p, err := pty.Start(command) if err != nil { return nil, err } - defer func() { _ = ptyOut.Close() }() + + defer func() { _ = p.Close() }() + defer func() { _ = command.Process.Kill() }() + + go func() { _, _ = io.Copy(p, stdin) }() + out := bytes.NewBuffer(make([]byte, 0)) - _, _ = io.Copy(out, ptyOut) + _, _ = io.Copy(out, p) return out, command.Wait() } diff --git a/internal/lefthook/runner/execute_windows.go b/internal/lefthook/runner/execute_windows.go index f695d9bc..48a37988 100644 --- a/internal/lefthook/runner/execute_windows.go +++ b/internal/lefthook/runner/execute_windows.go @@ -11,7 +11,7 @@ import ( type CommandExecutor struct{} -func (e CommandExecutor) Execute(root string, args []string) (*bytes.Buffer, error) { +func (e CommandExecutor) Execute(root string, args []string, _ bool) (*bytes.Buffer, error) { command := exec.Command(args[0]) command.SysProcAttr = &syscall.SysProcAttr{ CmdLine: strings.Join(args, " "), @@ -29,6 +29,9 @@ func (e CommandExecutor) Execute(root string, args []string) (*bytes.Buffer, err if err != nil { return nil, err } + + defer command.Process.Kill() + return &out, command.Wait() } diff --git a/internal/lefthook/runner/executor.go b/internal/lefthook/runner/executor.go index 4e10ba67..0cb10d2a 100644 --- a/internal/lefthook/runner/executor.go +++ b/internal/lefthook/runner/executor.go @@ -7,6 +7,6 @@ import ( // Executor provides an interface for command execution. // It is used here for testing purpose mostly. type Executor interface { - Execute(root string, args []string) (*bytes.Buffer, error) + Execute(root string, args []string, interactive bool) (*bytes.Buffer, error) RawExecute(command string, args ...string) error } diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index ac2e6a2f..dac53898 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -27,6 +27,14 @@ const ( var surroundingQuotesRegexp = regexp.MustCompile(`^'(.*)'$`) +// RunOptions contains the options that control the execution. +type RunOptions struct { + name, root, failText string + args []string + interactive bool +} + +// Runner responds for actual execution and handling the results. type Runner struct { fs afero.Fs repo *git.Repository @@ -57,6 +65,8 @@ func NewRunner( } } +// RunAll runs scripts and commands. +// LFS hook is executed at first if needed. func (r *Runner) RunAll(hookName string, sourceDirs []string) { if err := r.runLFSHook(hookName); err != nil { log.Error(err) @@ -201,7 +211,13 @@ func (r *Runner) runScript(script *config.Script, path string, file os.FileInfo) args = append(args, path) args = append(args, r.args[:]...) - r.run(file.Name(), r.repo.RootPath, script.FailText, args) + r.run(RunOptions{ + name: file.Name(), + root: r.repo.RootPath, + args: args, + failText: script.FailText, + interactive: script.Interactive, + }) } func (r *Runner) runCommands() { @@ -260,8 +276,13 @@ func (r *Runner) runCommand(name string, command *config.Command) { return } - root := filepath.Join(r.repo.RootPath, command.Root) - r.run(name, root, command.FailText, args) + r.run(RunOptions{ + name: name, + root: filepath.Join(r.repo.RootPath, command.Root), + args: args, + failText: command.FailText, + interactive: command.Interactive, + }) } func (r *Runner) buildCommandArgs(command *config.Command) ([]string, error) { @@ -369,16 +390,16 @@ func replaceQuoted(source, substitution string, files []string) string { return source } -func (r *Runner) run(name, root, failText string, args []string) { - out, err := r.exec.Execute(root, args) +func (r *Runner) run(opts RunOptions) { + out, err := r.exec.Execute(opts.root, opts.args, opts.interactive) var execName string if err != nil { - r.fail(name, failText) - execName = fmt.Sprint(log.Red("\n EXECUTE >"), log.Bold(name)) + r.fail(opts.name, opts.failText) + execName = fmt.Sprint(log.Red("\n EXECUTE >"), log.Bold(opts.name)) } else { - r.success(name) - execName = fmt.Sprint(log.Cyan("\n EXECUTE >"), log.Bold(name)) + r.success(opts.name) + execName = fmt.Sprint(log.Cyan("\n EXECUTE >"), log.Bold(opts.name)) } if out != nil { diff --git a/internal/lefthook/runner/runner_test.go b/internal/lefthook/runner/runner_test.go index 3f9b1d74..6673664c 100644 --- a/internal/lefthook/runner/runner_test.go +++ b/internal/lefthook/runner/runner_test.go @@ -15,7 +15,7 @@ import ( type TestExecutor struct{} -func (e TestExecutor) Execute(root string, args []string) (out *bytes.Buffer, err error) { +func (e TestExecutor) Execute(root string, args []string, interactive bool) (out *bytes.Buffer, err error) { out = bytes.NewBuffer(make([]byte, 0)) if args[0] == "success" { diff --git a/internal/templates/hook.tmpl b/internal/templates/hook.tmpl index 4511c18d..cc351d43 100644 --- a/internal/templates/hook.tmpl +++ b/internal/templates/hook.tmpl @@ -4,10 +4,6 @@ if [ "$LEFTHOOK" = "0" ]; then exit 0 fi -if [ -t 1 ] ; then - exec < /dev/tty ; # <- enables interactive shell -fi - call_lefthook() { dir="$(git rev-parse --show-toplevel)"