Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Skip based on branch name and allow global skip rules #376

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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