Skip to content

Commit

Permalink
feature: Skip based on branch name and allow global skip rules (evilm…
Browse files Browse the repository at this point in the history
  • Loading branch information
mrexox committed Nov 23, 2022
1 parent bb7482a commit 18c0df3
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## master (unreleased)

- feature: Skip based on branch name and allow global skip rules ([PR #376](https://github.com/evilmartians/lefthook/pull/376) by @mrexox)
- fix: Omit LFS output unless it is required ([PR #373](https://github.com/evilmartians/lefthook/pull/373) by @mrexox)

## 1.2.1 (2022-11-17)
Expand Down
20 changes: 18 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [`ref`](#ref)
- [`config`](#config)
- [Hook](#git-hook)
- [`skip`](#skip)
- [`files`](#files-global)
- [`parallel`](#parallel)
- [`piped`](#piped)
Expand Down Expand Up @@ -518,11 +519,11 @@ pre-commit

### `skip`

You can skip commands or scripts using `skip` option. You can only skip when merging or rebasing if you want.
You can skip all or specific commands and scripts using `skip` option. You can also skip when merging, rebasing, or being on a specific branch.

**Example**

Always skipping:
Always skipping a command:

```yml
# lefthook.yml
Expand Down Expand Up @@ -560,6 +561,21 @@ pre-commit:
run: yarn lint
```

Skipping the whole hook on `main` branch:

```yml
# lefthook.yml

pre-commit:
skip:
- ref: main
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`.
Expand Down
18 changes: 14 additions & 4 deletions internal/config/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"

"github.com/spf13/viper"

"github.com/evilmartians/lefthook/internal/git"
)

const CMD = "{cmd}"
Expand All @@ -23,10 +25,11 @@ type Hook struct {
// Unmarshaling it manually, so omit auto unmarshaling
Scripts map[string]*Script `mapstructure:"?"`

Files string `mapstructure:"files"`
Parallel bool `mapstructure:"parallel"`
Piped bool `mapstructure:"piped"`
ExcludeTags []string `mapstructure:"exclude_tags"`
Files string `mapstructure:"files"`
Parallel bool `mapstructure:"parallel"`
Piped bool `mapstructure:"piped"`
ExcludeTags []string `mapstructure:"exclude_tags"`
Skip interface{} `mapstructure:"skip"`
}

func (h *Hook) Validate() error {
Expand All @@ -37,6 +40,13 @@ func (h *Hook) Validate() error {
return nil
}

func (h *Hook) DoSkip(gitState git.State) bool {
if value := h.Skip; value != nil {
return isSkip(gitState, value)
}
return false
}

func unmarshalHooks(base, extra *viper.Viper) (*Hook, error) {
if base == nil && extra == nil {
return nil, nil
Expand Down
8 changes: 6 additions & 2 deletions internal/config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ remote:
config: examples/custom.yml
pre-commit:
skip:
- ref: main
commands:
global:
run: echo 'Global!'
Expand All @@ -239,7 +241,8 @@ pre-commit:
commands:
lint:
run: yarn lint
skip: true
skip:
- merge
scripts:
"test.sh":
runner: bash
Expand All @@ -256,10 +259,11 @@ pre-commit:
},
Hooks: map[string]*Hook{
"pre-commit": {
Skip: []interface{}{map[string]interface{}{"ref": "main"}},
Commands: map[string]*Command{
"lint": {
Run: "yarn lint",
Skip: true,
Skip: []interface{}{"merge"},
},
"global": {
Run: "echo 'Global!'",
Expand Down
17 changes: 12 additions & 5 deletions internal/config/skip.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ package config

import "github.com/evilmartians/lefthook/internal/git"

func isSkip(gitSkipState git.State, value interface{}) bool {
func isSkip(gitState git.State, value interface{}) bool {
switch typedValue := value.(type) {
case bool:
return typedValue
case string:
return git.State(typedValue) == gitSkipState
return typedValue == gitState.Step
case []interface{}:
for _, gitState := range typedValue {
if git.State(gitState.(string)) == gitSkipState {
return true
for _, state := range typedValue {
switch typedState := state.(type) {
case string:
if typedState == gitState.Step {
return true
}
case map[string]interface{}:
if typedState["ref"].(string) == gitState.Branch {
return true
}
}
}
}
Expand Down
56 changes: 49 additions & 7 deletions internal/git/state.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,68 @@
package git

import (
"bufio"
"os"
"path/filepath"
"regexp"
)

type State string
type State struct {
Branch, Step string
}

const (
NilState State = ""
MergeState State = "merge"
RebaseState State = "rebase"
NilStep string = ""
MergeStep string = "merge"
RebaseStep string = "rebase"
)

var refBranchRegexp = regexp.MustCompile(`^ref:\s*refs/heads/(.+)$`)

func (r *Repository) State() State {
branch := r.Branch()
if r.isMergeState() {
return MergeState
return State{
Branch: branch,
Step: MergeStep,
}
}
if r.isRebaseState() {
return RebaseState
return State{
Branch: branch,
Step: RebaseStep,
}
}
return State{
Branch: branch,
Step: NilStep,
}
}

func (r *Repository) Branch() string {
headFile := filepath.Join(r.GitPath, "HEAD")
if _, err := r.Fs.Stat(headFile); os.IsNotExist(err) {
return ""
}
return NilState

file, err := r.Fs.Open(headFile)
if err != nil {
return ""
}
defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)

for scanner.Scan() {
match := refBranchRegexp.FindStringSubmatch(scanner.Text())

if len(match) > 1 {
return match[1]
}
}

return ""
}

func (r *Repository) isMergeState() bool {
Expand Down
9 changes: 7 additions & 2 deletions internal/lefthook/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ func (r *Runner) RunAll(hookName string, sourceDirs []string) {
log.Error(err)
}

if r.hook.Skip != nil && r.hook.DoSkip(r.repo.State()) {
logSkip(hookName, "(SKIP BY HOOK SETTING)")
return
}

log.StartSpinner()
defer log.StopSpinner()

Expand Down Expand Up @@ -210,7 +215,7 @@ func (r *Runner) runScripts(dir string) {
}

func (r *Runner) runScript(script *config.Script, path string, file os.FileInfo) {
if script.DoSkip(r.repo.State()) {
if script.Skip != nil && script.DoSkip(r.repo.State()) {
logSkip(file.Name(), "(SKIP BY SETTINGS)")
return
}
Expand Down Expand Up @@ -304,7 +309,7 @@ func (r *Runner) runCommands() {
}

func (r *Runner) runCommand(name string, command *config.Command) {
if command.DoSkip(r.repo.State()) {
if command.Skip != nil && command.DoSkip(r.repo.State()) {
logSkip(name, "(SKIP BY SETTINGS)")
return
}
Expand Down
70 changes: 69 additions & 1 deletion internal/lefthook/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestRunAll(t *testing.T) {
}

for i, tt := range [...]struct {
name string
name, branch string
args []string
sourceDirs []string
existingFiles []string
Expand Down Expand Up @@ -154,6 +154,25 @@ func TestRunAll(t *testing.T) {
},
success: []Result{{Name: "lint", Status: StatusOk}},
},
{
name: "with global skip merge",
existingFiles: []string{
filepath.Join(gitPath, "MERGE_HEAD"),
},
hook: &config.Hook{
Skip: "merge",
Commands: map[string]*config.Command{
"test": {
Run: "success",
},
"lint": {
Run: "success",
},
},
Scripts: map[string]*config.Script{},
},
success: []Result{},
},
{
name: "with skip rebase and merge in an array",
existingFiles: []string{
Expand All @@ -174,6 +193,49 @@ func TestRunAll(t *testing.T) {
},
success: []Result{{Name: "lint", Status: StatusOk}},
},
{
name: "with global skip on ref",
branch: "main",
existingFiles: []string{
filepath.Join(gitPath, "HEAD"),
},
hook: &config.Hook{
Skip: []interface{}{"merge", map[string]interface{}{"ref": "main"}},
Commands: map[string]*config.Command{
"test": {
Run: "success",
},
"lint": {
Run: "success",
},
},
Scripts: map[string]*config.Script{},
},
success: []Result{},
},
{
name: "with global skip on another ref",
branch: "fix",
existingFiles: []string{
filepath.Join(gitPath, "HEAD"),
},
hook: &config.Hook{
Skip: []interface{}{"merge", map[string]interface{}{"ref": "main"}},
Commands: map[string]*config.Command{
"test": {
Run: "success",
},
"lint": {
Run: "success",
},
},
Scripts: map[string]*config.Script{},
},
success: []Result{
{Name: "test", Status: StatusOk},
{Name: "lint", Status: StatusOk},
},
},
{
name: "with fail test",
hook: &config.Hook{
Expand Down Expand Up @@ -263,6 +325,12 @@ func TestRunAll(t *testing.T) {
}
}

if len(tt.branch) > 0 {
if err := afero.WriteFile(fs, filepath.Join(gitPath, "HEAD"), []byte("ref: refs/heads/"+tt.branch), 0o644); err != nil {
t.Errorf("unexpected error: %s", err)
}
}

t.Run(fmt.Sprintf("%d: %s", i, tt.name), func(t *testing.T) {
runner.RunAll(hookName, tt.sourceDirs)
close(resultChan)
Expand Down

0 comments on commit 18c0df3

Please sign in to comment.