From e9ca9556af5ed3fa47bde93ce20acf36b980ee55 Mon Sep 17 00:00:00 2001 From: Igor Platonov Date: Wed, 13 Mar 2024 08:48:17 +0100 Subject: [PATCH] feat: handle `run` command in skip/only settings (#634) * feat: handle `run` command in skip/only settings * chore: add a testscript * fix: call skip commands via shell * fix: try to use command for windows * ci: run integration tests on Windows * ci: checking windows section in testscript * ci: test another syntax testscript * ci: make it work with windows --------- Co-authored-by: Valentin Kiselev --- .github/workflows/test.yml | 4 +- docs/configuration.md | 15 +++ internal/config/command.go | 3 +- internal/config/exec.go | 35 +++++++ internal/config/hook.go | 3 +- internal/config/script.go | 3 +- internal/config/skip.go | 50 ---------- internal/config/skip_checker.go | 95 +++++++++++++++++++ .../{skip_test.go => skip_checker_test.go} | 28 +++++- testdata/add.txt | 2 + testdata/dump.txt | 2 + testdata/hide_unstaged.txt | 2 + testdata/remote.txt | 2 + testdata/remotes.txt | 2 + testdata/run_interrupt.txt | 2 + testdata/sh_syntax_in_files.txt | 3 + testdata/skip_run.txt | 42 ++++++++ testdata/skip_run_windows.txt | 42 ++++++++ 18 files changed, 279 insertions(+), 56 deletions(-) create mode 100644 internal/config/exec.go delete mode 100644 internal/config/skip.go create mode 100644 internal/config/skip_checker.go rename internal/config/{skip_test.go => skip_checker_test.go} (79%) create mode 100644 testdata/skip_run.txt create mode 100644 testdata/skip_run_windows.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e78f6669..50c65239 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: strategy: matrix: go-version: [1.21.x] - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GOCOVERDIR: ${{ github.workspace }}/_icoverdir_ @@ -91,5 +91,5 @@ jobs: steps: - uses: coverallsapp/github-action@v2 with: - carryforward: "integration-1.21.x ubuntu-latest,integration-1.21.x macos-latest,1.21.x ubuntu-latest,1.21.x macos-latest,1.21.x windows-latest" + carryforward: "integration-1.21.x ubuntu-latest,integration-1.21.x macos-latest,integration-1.21.x windows-latest,1.21.x ubuntu-latest,1.21.x macos-latest,1.21.x windows-latest" parallel-finished: true diff --git a/docs/configuration.md b/docs/configuration.md index 0d1f3db9..4e9f2f02 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -922,6 +922,21 @@ pre-commit: run: yarn test ``` +Skipping hook by running a command: + +```yml +# lefthook.yml + +pre-commit: + skip: + - run: test "${NO_HOOK}" -eq 1 + commands: + lint: + run: yarn lint + text: + run: yarn test +``` + **Notes** Always skipping is useful when you have a `lefthook-local.yml` config and you don't want to run some commands locally. So you just overwrite the `skip` option for them to be `true`. diff --git a/internal/config/command.go b/internal/config/command.go index 79a70f2e..1d47ac9a 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -40,7 +40,8 @@ func (c Command) Validate() error { } func (c Command) DoSkip(gitState git.State) bool { - return doSkip(gitState, c.Skip, c.Only) + skipChecker := NewSkipChecker(NewOsExec()) + return skipChecker.Check(gitState, c.Skip, c.Only) } type commandRunReplace struct { diff --git a/internal/config/exec.go b/internal/config/exec.go new file mode 100644 index 00000000..87971979 --- /dev/null +++ b/internal/config/exec.go @@ -0,0 +1,35 @@ +package config + +import ( + "os/exec" + "runtime" +) + +type Exec interface { + Cmd(commandLine string) bool +} + +type osExec struct{} + +// NewOsExec returns an object that executes given commands in the OS. +func NewOsExec() Exec { + return &osExec{} +} + +// Cmd runs plain string command. It checks only exit code and returns bool value. +func (o *osExec) Cmd(commandLine string) bool { + if commandLine == "" { + return false + } + + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("powershell", "-Command", commandLine) + } else { + cmd = exec.Command("sh", "-c", commandLine) + } + + err := cmd.Run() + + return err == nil +} diff --git a/internal/config/hook.go b/internal/config/hook.go index 692cc5cb..3a958c72 100644 --- a/internal/config/hook.go +++ b/internal/config/hook.go @@ -43,7 +43,8 @@ func (h *Hook) Validate() error { } func (h *Hook) DoSkip(gitState git.State) bool { - return doSkip(gitState, h.Skip, h.Only) + skipChecker := NewSkipChecker(NewOsExec()) + return skipChecker.Check(gitState, h.Skip, h.Only) } func unmarshalHooks(base, extra *viper.Viper) (*Hook, error) { diff --git a/internal/config/script.go b/internal/config/script.go index a2070dbe..085b65f7 100644 --- a/internal/config/script.go +++ b/internal/config/script.go @@ -24,7 +24,8 @@ type Script struct { } func (s Script) DoSkip(gitState git.State) bool { - return doSkip(gitState, s.Skip, s.Only) + skipChecker := NewSkipChecker(NewOsExec()) + return skipChecker.Check(gitState, s.Skip, s.Only) } type scriptRunnerReplace struct { diff --git a/internal/config/skip.go b/internal/config/skip.go deleted file mode 100644 index 9b52f3bd..00000000 --- a/internal/config/skip.go +++ /dev/null @@ -1,50 +0,0 @@ -package config - -import ( - "github.com/gobwas/glob" - - "github.com/evilmartians/lefthook/internal/git" -) - -func doSkip(gitState git.State, skip, only interface{}) bool { - if skip != nil { - if matches(gitState, skip) { - return true - } - } - - if only != nil { - return !matches(gitState, only) - } - - return false -} - -func matches(gitState git.State, value interface{}) bool { - switch typedValue := value.(type) { - case bool: - return typedValue - case string: - return typedValue == gitState.Step - case []interface{}: - for _, state := range typedValue { - switch typedState := state.(type) { - case string: - if typedState == gitState.Step { - return true - } - case map[string]interface{}: - ref := typedState["ref"].(string) - if ref == gitState.Branch { - return true - } - - g := glob.MustCompile(ref) - if g.Match(gitState.Branch) { - return true - } - } - } - } - return false -} diff --git a/internal/config/skip_checker.go b/internal/config/skip_checker.go new file mode 100644 index 00000000..581610f7 --- /dev/null +++ b/internal/config/skip_checker.go @@ -0,0 +1,95 @@ +package config + +import ( + "github.com/gobwas/glob" + + "github.com/evilmartians/lefthook/internal/git" + "github.com/evilmartians/lefthook/internal/log" +) + +type SkipChecker struct { + Executor Exec +} + +func NewSkipChecker(executor Exec) *SkipChecker { + if executor == nil { + executor = NewOsExec() + } + + return &SkipChecker{Executor: executor} +} + +func (sc *SkipChecker) Check(gitState git.State, skip interface{}, only interface{}) bool { + if skip != nil { + if sc.matches(gitState, skip) { + return true + } + } + + if only != nil { + return !sc.matches(gitState, only) + } + + return false +} + +func (sc *SkipChecker) matches(gitState git.State, value interface{}) bool { + switch typedValue := value.(type) { + case bool: + return typedValue + case string: + return typedValue == gitState.Step + case []interface{}: + return sc.matchesSlices(gitState, typedValue) + } + return false +} + +func (sc *SkipChecker) matchesSlices(gitState git.State, slice []interface{}) bool { + for _, state := range slice { + switch typedState := state.(type) { + case string: + if typedState == gitState.Step { + return true + } + case map[string]interface{}: + if sc.matchesRef(gitState, typedState) { + return true + } + + if sc.matchesCommands(typedState) { + return true + } + } + } + + return false +} + +func (sc *SkipChecker) matchesRef(gitState git.State, typedState map[string]interface{}) bool { + ref, ok := typedState["ref"].(string) + if !ok { + return false + } + + if ref == gitState.Branch { + return true + } + + g := glob.MustCompile(ref) + + return g.Match(gitState.Branch) +} + +func (sc *SkipChecker) matchesCommands(typedState map[string]interface{}) bool { + commandLine, ok := typedState["run"].(string) + if !ok { + return false + } + + result := sc.Executor.Cmd(commandLine) + + log.Debugf("[lefthook] skip/only cmd: %s, result: %t", commandLine, result) + + return result +} diff --git a/internal/config/skip_test.go b/internal/config/skip_checker_test.go similarity index 79% rename from internal/config/skip_test.go rename to internal/config/skip_checker_test.go index d4d3c6f3..4956859f 100644 --- a/internal/config/skip_test.go +++ b/internal/config/skip_checker_test.go @@ -6,7 +6,15 @@ import ( "github.com/evilmartians/lefthook/internal/git" ) +type mockExecutor struct{} + +func (mc mockExecutor) Cmd(cmd string) bool { + return cmd == "success" +} + func TestDoSkip(t *testing.T) { + skipChecker := NewSkipChecker(mockExecutor{}) + for _, tt := range [...]struct { name string state git.State @@ -111,9 +119,27 @@ func TestDoSkip(t *testing.T) { only: "rebase", skipped: true, }, + { + name: "when skip with run command", + state: git.State{}, + skip: []interface{}{map[string]interface{}{"run": "success"}}, + skipped: true, + }, + { + name: "when skip with multi-run command", + state: git.State{Branch: "feat"}, + skip: []interface{}{map[string]interface{}{"run": "success", "ref": "feat"}}, + skipped: true, + }, + { + name: "when only with run command", + state: git.State{}, + only: []interface{}{map[string]interface{}{"run": "fail"}}, + skipped: true, + }, } { t.Run(tt.name, func(t *testing.T) { - if doSkip(tt.state, tt.skip, tt.only) != tt.skipped { + if skipChecker.Check(tt.state, tt.skip, tt.only) != tt.skipped { t.Errorf("Expected: %v, Was %v", tt.skipped, !tt.skipped) } }) diff --git a/testdata/add.txt b/testdata/add.txt index 4aa86233..da9e0e95 100644 --- a/testdata/add.txt +++ b/testdata/add.txt @@ -1,3 +1,5 @@ +[windows] skip + exec git init exec lefthook add pre-commit ! stderr . diff --git a/testdata/dump.txt b/testdata/dump.txt index 3857f0d4..5a5559e3 100644 --- a/testdata/dump.txt +++ b/testdata/dump.txt @@ -1,3 +1,5 @@ +[windows] skip + exec git init exec lefthook dump cmp stdout lefthook-dumped.yml diff --git a/testdata/hide_unstaged.txt b/testdata/hide_unstaged.txt index 4147604e..5aa67510 100644 --- a/testdata/hide_unstaged.txt +++ b/testdata/hide_unstaged.txt @@ -1,3 +1,5 @@ +[windows] skip + exec git init exec lefthook install exec git config user.email "you@example.com" diff --git a/testdata/remote.txt b/testdata/remote.txt index 72435af1..df0c246b 100644 --- a/testdata/remote.txt +++ b/testdata/remote.txt @@ -1,3 +1,5 @@ +[windows] skip + exec git init exec lefthook install diff --git a/testdata/remotes.txt b/testdata/remotes.txt index 7858b6e3..beffc20f 100644 --- a/testdata/remotes.txt +++ b/testdata/remotes.txt @@ -1,3 +1,5 @@ +[windows] skip + exec git init exec lefthook install diff --git a/testdata/run_interrupt.txt b/testdata/run_interrupt.txt index 9103d014..2716c53d 100644 --- a/testdata/run_interrupt.txt +++ b/testdata/run_interrupt.txt @@ -1,3 +1,5 @@ +[windows] skip + chmod 0700 hook.sh chmod 0700 commit-with-interrupt.sh exec git init diff --git a/testdata/sh_syntax_in_files.txt b/testdata/sh_syntax_in_files.txt index a45c9a25..b65a7cb7 100644 --- a/testdata/sh_syntax_in_files.txt +++ b/testdata/sh_syntax_in_files.txt @@ -1,9 +1,12 @@ +[windows] skip + 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 -- diff --git a/testdata/skip_run.txt b/testdata/skip_run.txt new file mode 100644 index 00000000..7e2af672 --- /dev/null +++ b/testdata/skip_run.txt @@ -0,0 +1,42 @@ +[windows] skip + +exec git init +exec git add -A +exec lefthook run skip +! stdout 'Ha-ha!' +exec lefthook run no-skip +stdout 'Ha-ha!' + +exec lefthook run skip-var +! stdout 'Ha-ha!' + +env VAR=1 +exec lefthook run skip-var +stdout 'Ha-ha!' + +-- lefthook.yml -- +skip_output: + - skips + - meta + - summary + - execution_info +skip: + skip: + - run: test 1 -eq 1 + commands: + echo: + run: echo 'Ha-ha!' + +no-skip: + skip: + - run: "[ 1 -eq 0 ]" + commands: + echo: + run: echo 'Ha-ha!' + +skip-var: + skip: + - run: test -z $VAR + commands: + echo: + run: echo 'Ha-ha!' diff --git a/testdata/skip_run_windows.txt b/testdata/skip_run_windows.txt new file mode 100644 index 00000000..f5d6fd82 --- /dev/null +++ b/testdata/skip_run_windows.txt @@ -0,0 +1,42 @@ +[!windows] skip + +exec git init +exec git add -A +exec lefthook run skip +! stdout 'Ha-ha!' +exec lefthook run no-skip +stdout 'Ha-ha!' + +exec lefthook run skip-var +! stdout 'Ha-ha!' + +env VAR=1 +exec lefthook run skip-var +stdout 'Ha-ha!' + +-- lefthook.yml -- +skip_output: + - skips + - meta + - summary + - execution_info +skip: + skip: + - run: if (1 -eq 1) { exit 0 } else { exit 1 } + commands: + echo: + run: echo 'Ha-ha!' + +no-skip: + skip: + - run: if (1 -eq 0) { exit 0 } else { exit 1 } + commands: + echo: + run: echo 'Ha-ha!' + +skip-var: + skip: + - run: if ([string]::IsNullOrEmpty($env:VAR)) { exit 0 } else { exit 1 } + commands: + echo: + run: echo 'Ha-ha!'