diff --git a/internal/config/command.go b/internal/config/command.go index 8d618424..c8131f20 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -14,10 +14,11 @@ var errFilesIncompatible = errors.New("One of your runners contains incompatible type Command struct { Run string `mapstructure:"run"` - Skip interface{} `mapstructure:"skip"` - Tags []string `mapstructure:"tags"` - Glob string `mapstructure:"glob"` - Files string `mapstructure:"files"` + Skip interface{} `mapstructure:"skip"` + Tags []string `mapstructure:"tags"` + Glob string `mapstructure:"glob"` + Files string `mapstructure:"files"` + Env map[string]string `mapstructure:"env"` Root string `mapstructure:"root"` Exclude string `mapstructure:"exclude"` diff --git a/internal/config/script.go b/internal/config/script.go index 45186c7c..f7d174e7 100644 --- a/internal/config/script.go +++ b/internal/config/script.go @@ -12,8 +12,9 @@ import ( type Script struct { Runner string `mapstructure:"runner"` - Skip interface{} `mapstructure:"skip"` - Tags []string `mapstructure:"tags"` + Skip interface{} `mapstructure:"skip"` + Tags []string `mapstructure:"tags"` + Env map[string]string `mapstructure:"env"` FailText string `mapstructure:"fail_text"` Interactive bool `mapstructure:"interactive"` diff --git a/internal/lefthook/runner/execute_unix.go b/internal/lefthook/runner/execute_unix.go index c4dd1752..2e138253 100644 --- a/internal/lefthook/runner/execute_unix.go +++ b/internal/lefthook/runner/execute_unix.go @@ -5,6 +5,7 @@ package runner import ( "bytes" + "fmt" "io" "os" "os/exec" @@ -19,9 +20,9 @@ import ( type CommandExecutor struct{} -func (e CommandExecutor) Execute(root string, args []string, interactive bool) (*bytes.Buffer, error) { +func (e CommandExecutor) Execute(opts ExecuteOptions) (*bytes.Buffer, error) { stdin := os.Stdin - if interactive && !isatty.IsTerminal(os.Stdin.Fd()) { + if opts.interactive && !isatty.IsTerminal(os.Stdin.Fd()) { tty, err := os.Open("/dev/tty") if err == nil { defer tty.Close() @@ -30,10 +31,17 @@ func (e CommandExecutor) Execute(root string, args []string, interactive bool) ( log.Errorf("Couldn't enable TTY input: %s\n", err) } } - command := exec.Command("sh", "-c", strings.Join(args, " ")) - rootDir, _ := filepath.Abs(root) + 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)) + } + + command.Env = append(os.Environ(), envList...) + p, err := pty.Start(command) if err != nil { return nil, err diff --git a/internal/lefthook/runner/execute_windows.go b/internal/lefthook/runner/execute_windows.go index 48a37988..2ceee79b 100644 --- a/internal/lefthook/runner/execute_windows.go +++ b/internal/lefthook/runner/execute_windows.go @@ -2,6 +2,7 @@ package runner import ( "bytes" + "fmt" "os" "os/exec" "path/filepath" @@ -11,13 +12,20 @@ import ( type CommandExecutor struct{} -func (e CommandExecutor) Execute(root string, args []string, _ bool) (*bytes.Buffer, error) { +func (e CommandExecutor) Execute(opts ExecuteOptions) (*bytes.Buffer, error) { command := exec.Command(args[0]) command.SysProcAttr = &syscall.SysProcAttr{ - CmdLine: strings.Join(args, " "), + CmdLine: strings.Join(opts.args, " "), } - rootDir, _ := filepath.Abs(root) + envList := make([]string, len(opts.env)) + for name, value := range opts.env { + 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 diff --git a/internal/lefthook/runner/executor.go b/internal/lefthook/runner/executor.go index 0cb10d2a..863659e4 100644 --- a/internal/lefthook/runner/executor.go +++ b/internal/lefthook/runner/executor.go @@ -4,9 +4,17 @@ import ( "bytes" ) +// ExecutorOptions contains the options that control the execution. +type ExecuteOptions struct { + name, root, failText string + args []string + interactive bool + env map[string]string +} + // Executor provides an interface for command execution. // It is used here for testing purpose mostly. type Executor interface { - Execute(root string, args []string, interactive bool) (*bytes.Buffer, error) + Execute(opts ExecuteOptions) (*bytes.Buffer, error) RawExecute(command string, args ...string) error } diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index d8fbac02..910d4125 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -27,13 +27,6 @@ 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 @@ -212,12 +205,13 @@ func (r *Runner) runScript(script *config.Script, unquotedPath string, file os.F args = append(args, quotedScriptPath) args = append(args, r.args[:]...) - r.run(RunOptions{ + r.run(ExecuteOptions{ name: file.Name(), root: r.repo.RootPath, args: args, failText: script.FailText, interactive: script.Interactive, + env: script.Env, }) } @@ -282,12 +276,13 @@ func (r *Runner) runCommand(name string, command *config.Command) { return } - r.run(RunOptions{ + r.run(ExecuteOptions{ name: name, root: filepath.Join(r.repo.RootPath, command.Root), args: args, failText: command.FailText, interactive: command.Interactive, + env: command.Env, }) } @@ -396,11 +391,11 @@ func replaceQuoted(source, substitution string, files []string) string { return source } -func (r *Runner) run(opts RunOptions) { +func (r *Runner) run(opts ExecuteOptions) { log.SetName(opts.name) defer log.UnsetName(opts.name) - out, err := r.exec.Execute(opts.root, opts.args, opts.interactive) + out, err := r.exec.Execute(opts) var execName string if err != nil {