From 3880ba7a73da3a2edfbd3820b2d200593d79550a Mon Sep 17 00:00:00 2001 From: "A.A.Abroskin" Date: Tue, 2 Jul 2019 12:19:37 +0300 Subject: [PATCH] Add piped option --- cmd/run.go | 51 ++++++++++++- cmd/run_windows.go | 73 +++++++++++++++---- docs/full_guide.md | 14 ++++ .../.lefthook-local/pre-commit/hello.go | 2 +- go.sum | 2 + 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 41cfb5d7..004be72d 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "regexp" + "sort" "strings" "sync" "time" @@ -33,6 +34,7 @@ var ( failList []string mutex sync.Mutex envExcludeTags []string // store for LEFTHOOK_EXCLUDE=tag,tag + isPipeBroken bool ) const ( @@ -53,6 +55,7 @@ const ( subStagedFiles string = "{staged_files}" runnerWrapPattern string = "{cmd}" tagsConfigKey string = "tags" + pipedConfigKey string = "piped" excludeTagsConfigKey string = "exclude_tags" execMode os.FileMode = 0751 ) @@ -96,6 +99,11 @@ func RunCmdExecutor(args []string, fs afero.Fs) error { startTime := time.Now() log.Println(au.Cyan("RUNNING HOOKS GROUP:"), au.Bold(hooksGroup)) + if isPipedAndParallel(hooksGroup) { + log.Println(au.Brown("Config error! Conflicted options 'piped' and 'parallel'. Remove one of this option from hook group.")) + return errors.New("Piped and Parallel options in conflict.") + } + sourcePath := filepath.Join(getSourceDir(), hooksGroup) executables, err := afero.ReadDir(fs, sourcePath) if err == nil && len(executables) > 0 { @@ -125,7 +133,7 @@ func RunCmdExecutor(args []string, fs afero.Fs) error { commands := getCommands(hooksGroup) if len(commands) != 0 { - for commandName := range commands { + for _, commandName := range commands { wg.Add(1) if getParallel(hooksGroup) { go executeCommand(hooksGroup, commandName, &wg) @@ -148,6 +156,12 @@ func RunCmdExecutor(args []string, fs afero.Fs) error { func executeCommand(hooksGroup, commandName string, wg *sync.WaitGroup) { defer wg.Done() + if getPiped(hooksGroup) && isPipeBroken { + log.Println(au.Cyan("\n EXECUTE >"), au.Bold(commandName)) + log.Println(au.Brown("(SKIP BY BROKEN PIPE)")) + return + } + files, _ := context.AllFiles() runner := getRunner(hooksGroup, commandsConfigKey, commandName) @@ -176,6 +190,7 @@ func executeCommand(hooksGroup, commandName string, wg *sync.WaitGroup) { log.Println(au.Cyan("\n EXECUTE >"), au.Bold(commandName)) if err != nil { failList = append(failList, commandName) + setPipeBroken() log.Println(err) return } @@ -199,6 +214,7 @@ func executeCommand(hooksGroup, commandName string, wg *sync.WaitGroup) { okList = append(okList, commandName) } else { failList = append(failList, commandName) + setPipeBroken() } } @@ -206,6 +222,12 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W defer wg.Done() executableName := executable.Name() + if getPiped(hooksGroup) && isPipeBroken { + log.Println(au.Cyan("\n EXECUTE >"), au.Bold(executableName)) + log.Println(au.Brown("(SKIP BY BROKEN PIPE)")) + return + } + pathToExecutable := filepath.Join(source, executableName) if err := isExecutable(executable); err != nil { @@ -240,6 +262,7 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W failList = append(failList, executableName) log.Println(err) log.Println(au.Brown("TIP: Command start failed. Checkout `runner:` option for this script")) + setPipeBroken() return } if isSkipScript(hooksGroup, executableName) { @@ -257,6 +280,7 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W okList = append(okList, executableName) } else { failList = append(failList, executableName) + setPipeBroken() } } @@ -332,9 +356,17 @@ func isSkipEmptyCommmand(hooksGroup, executableName string) bool { return true } -func getCommands(hooksGroup string) map[string]interface{} { +func getCommands(hooksGroup string) []string { key := strings.Join([]string{hooksGroup, commandsConfigKey}, ".") - return viper.GetStringMap(key) + commands := viper.GetStringMap(key) + + keys := make([]string, 0, len(commands)) + for k := range commands { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys } func getCommandIncludeRegexp(hooksGroup, executableName string) string { @@ -398,6 +430,19 @@ func getParallel(hooksGroup string) bool { return viper.GetBool(key) } +func getPiped(hooksGroup string) bool { + key := strings.Join([]string{hooksGroup, pipedConfigKey}, ".") + return viper.GetBool(key) +} + +func isPipedAndParallel(hooksGroup string) bool { + return getParallel(hooksGroup) && getPiped(hooksGroup) +} + +func setPipeBroken() { + isPipeBroken = true +} + func FilterGlob(vs []string, matcher string) []string { if matcher == "" { return vs diff --git a/cmd/run_windows.go b/cmd/run_windows.go index 5e5dba62..9c78f303 100644 --- a/cmd/run_windows.go +++ b/cmd/run_windows.go @@ -1,13 +1,17 @@ +// On windows we can`t use pty, so we cant sync output in parallel mode + package cmd import ( "errors" "fmt" + // "io" // win specific "log" "os" "os/exec" "path/filepath" "regexp" + "sort" "strings" "sync" "time" @@ -16,6 +20,7 @@ import ( arrop "github.com/adam-hanna/arrayOperations" "github.com/gobwas/glob" + // "github.com/kr/pty" //win specific "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -29,6 +34,7 @@ var ( failList []string mutex sync.Mutex envExcludeTags []string // store for LEFTHOOK_EXCLUDE=tag,tag + isPipeBroken bool ) const ( @@ -49,6 +55,7 @@ const ( subStagedFiles string = "{staged_files}" runnerWrapPattern string = "{cmd}" tagsConfigKey string = "tags" + pipedConfigKey string = "piped" excludeTagsConfigKey string = "exclude_tags" execMode os.FileMode = 0751 ) @@ -92,6 +99,11 @@ func RunCmdExecutor(args []string, fs afero.Fs) error { startTime := time.Now() log.Println(au.Cyan("RUNNING HOOKS GROUP:"), au.Bold(hooksGroup)) + if isPipedAndParallel(hooksGroup) { + log.Println(au.Brown("Config error! Conflicted options 'piped' and 'parallel'. Remove one of this option from hook group.")) + return errors.New("Piped and Parallel options in conflict.") + } + sourcePath := filepath.Join(getSourceDir(), hooksGroup) executables, err := afero.ReadDir(fs, sourcePath) if err == nil && len(executables) > 0 { @@ -121,7 +133,7 @@ func RunCmdExecutor(args []string, fs afero.Fs) error { commands := getCommands(hooksGroup) if len(commands) != 0 { - for commandName := range commands { + for _, commandName := range commands { wg.Add(1) if getParallel(hooksGroup) { go executeCommand(hooksGroup, commandName, &wg) @@ -144,6 +156,12 @@ func RunCmdExecutor(args []string, fs afero.Fs) error { func executeCommand(hooksGroup, commandName string, wg *sync.WaitGroup) { defer wg.Done() + if getPiped(hooksGroup) && isPipeBroken { + log.Println(au.Cyan("\n EXECUTE >"), au.Bold(commandName)) + log.Println(au.Brown("(SKIP BY BROKEN PIPE)")) + return + } + files, _ := context.AllFiles() runner := getRunner(hooksGroup, commandsConfigKey, commandName) @@ -163,18 +181,18 @@ func executeCommand(hooksGroup, commandName string, wg *sync.WaitGroup) { runnerArg := strings.Split(runner, " ") command := exec.Command(runnerArg[0], runnerArg[1:]...) - command.Stdout = os.Stdout - command.Stderr = os.Stderr command.Stdin = os.Stdin + command.Stdout = os.Stdout // win specific + command.Stderr = os.Stderr // win specific - err := command.Start() - + err := command.Start() // ptyOut, err := pty.Start(command) // win specific mutex.Lock() defer mutex.Unlock() log.Println(au.Cyan("\n EXECUTE >"), au.Bold(commandName)) if err != nil { failList = append(failList, commandName) + setPipeBroken() log.Println(err) return } @@ -191,11 +209,12 @@ func executeCommand(hooksGroup, commandName string, wg *sync.WaitGroup) { log.Println(au.Brown("(SKIP. NO FILES FOR INSPECTING)")) return } - + // io.Copy(os.Stdout, ptyOut) // win specific if command.Wait() == nil { okList = append(okList, commandName) } else { failList = append(failList, commandName) + setPipeBroken() } } @@ -203,6 +222,12 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W defer wg.Done() executableName := executable.Name() + if getPiped(hooksGroup) && isPipeBroken { + log.Println(au.Cyan("\n EXECUTE >"), au.Bold(executableName)) + log.Println(au.Brown("(SKIP BY BROKEN PIPE)")) + return + } + pathToExecutable := filepath.Join(source, executableName) if err := isExecutable(executable); err != nil { @@ -218,12 +243,11 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W command = exec.Command(runnerArg[0], runnerArg[1:]...) } - command.Stdout = os.Stdout - command.Stderr = os.Stderr command.Stdin = os.Stdin + command.Stdout = os.Stdout // win specific + command.Stderr = os.Stderr // win specific - err := command.Start() - + err := command.Start() // ptyOut, err := pty.Start(command) // win specific mutex.Lock() defer mutex.Unlock() @@ -240,6 +264,7 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W failList = append(failList, executableName) log.Println(err) log.Println(au.Brown("TIP: Command start failed. Checkout `runner:` option for this script")) + setPipeBroken() return } if isSkipScript(hooksGroup, executableName) { @@ -250,11 +275,12 @@ func executeScript(hooksGroup, source string, executable os.FileInfo, wg *sync.W log.Println(au.Brown("(SKIP BY TAGS)")) return } - + // io.Copy(os.Stdout, ptyOut) // win specific if command.Wait() == nil { okList = append(okList, executableName) } else { failList = append(failList, executableName) + setPipeBroken() } } @@ -330,9 +356,17 @@ func isSkipEmptyCommmand(hooksGroup, executableName string) bool { return true } -func getCommands(hooksGroup string) map[string]interface{} { +func getCommands(hooksGroup string) []string { key := strings.Join([]string{hooksGroup, commandsConfigKey}, ".") - return viper.GetStringMap(key) + commands := viper.GetStringMap(key) + + keys := make([]string, 0, len(commands)) + for k := range commands { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys } func getCommandIncludeRegexp(hooksGroup, executableName string) string { @@ -396,6 +430,19 @@ func getParallel(hooksGroup string) bool { return viper.GetBool(key) } +func getPiped(hooksGroup string) bool { + key := strings.Join([]string{hooksGroup, pipedConfigKey}, ".") + return viper.GetBool(key) +} + +func isPipedAndParallel(hooksGroup string) bool { + return getParallel(hooksGroup) && getPiped(hooksGroup) +} + +func setPipeBroken() { + isPipeBroken = true +} + func FilterGlob(vs []string, matcher string) []string { if matcher == "" { return vs diff --git a/docs/full_guide.md b/docs/full_guide.md index ecc7eff8..15b5eb60 100644 --- a/docs/full_guide.md +++ b/docs/full_guide.md @@ -242,6 +242,20 @@ pre-push: - frontend ``` +## Piped option +If any command in the sequence fails, the other will not be executed. +```yml +database: + piped: true + commands: + 1_create: + run: rake db:create + 2_migrate: + run: rake db:migrate + 3_seed: + run: rake db:seed +``` + ## Referencing commands from lefthook.yml If you have the following config diff --git a/examples/complete/.lefthook-local/pre-commit/hello.go b/examples/complete/.lefthook-local/pre-commit/hello.go index 87595e3c..c0481191 100644 --- a/examples/complete/.lefthook-local/pre-commit/hello.go +++ b/examples/complete/.lefthook-local/pre-commit/hello.go @@ -3,5 +3,5 @@ package main import "fmt" func main() { - fmt.Println("hello world") + fmt.Println("hello world") } diff --git a/go.sum b/go.sum index f1dc6e6e..ee6e3998 100644 --- a/go.sum +++ b/go.sum @@ -36,9 +36,11 @@ github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=