Skip to content

Commit

Permalink
feat: add skip option merge-commit (#850)
Browse files Browse the repository at this point in the history
* feat: add skip merge commit option

* refactor: tests

* docs: add skip option merge-commit

* docs: give a list of possible skip options
  • Loading branch information
mrexox authored Oct 18, 2024
1 parent ba633c0 commit d5b7e77
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 219 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ linters:
- errname
- errorlint
- exhaustive
- exportloopref
- forbidigo
- gci
- gochecknoinits
Expand Down
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,13 @@ pre-commit:

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. Globs are available for branches.

Possible skip values:
- `rebase` - when in rebase git state
- `merge` - when in merge git state
- `merge-commit` - when current HEAD commit is the merge commit
- `ref: main` - when on a `main` branch
- `run: test ${SKIP_ME} -eq 1` - when `test ${SKIP_ME} -eq 1` is successful (return code is 0)

**Example**

Always skipping a command:
Expand Down Expand Up @@ -839,6 +846,18 @@ pre-commit:
run: yarn lint
```

Skipping when your are on a merge commit:

```yml
# lefthook.yml
pre-push:
commands:
lint:
skip: merge-commit
run: yarn lint
```

Skipping the whole hook on `main` branch:

```yml
Expand Down
4 changes: 2 additions & 2 deletions internal/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func (c Command) Validate() error {
return nil
}

func (c Command) DoSkip(gitState git.State) bool {
func (c Command) DoSkip(state func() git.State) bool {
skipChecker := NewSkipChecker(system.Cmd)
return skipChecker.check(gitState, c.Skip, c.Only)
return skipChecker.check(state, c.Skip, c.Only)
}

func (c Command) ExecutionPriority() int {
Expand Down
4 changes: 2 additions & 2 deletions internal/config/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ func (h *Hook) Validate() error {
return nil
}

func (h *Hook) DoSkip(gitState git.State) bool {
func (h *Hook) DoSkip(state func() git.State) bool {
skipChecker := NewSkipChecker(system.Cmd)
return skipChecker.check(gitState, h.Skip, h.Only)
return skipChecker.check(state, h.Skip, h.Only)
}

func unmarshalHooks(base, extra *viper.Viper) (*Hook, error) {
Expand Down
4 changes: 2 additions & 2 deletions internal/config/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ type scriptRunnerReplace struct {
Runner string `mapstructure:"runner"`
}

func (s Script) DoSkip(gitState git.State) bool {
func (s Script) DoSkip(state func() git.State) bool {
skipChecker := NewSkipChecker(system.Cmd)
return skipChecker.check(gitState, s.Skip, s.Only)
return skipChecker.check(state, s.Skip, s.Only)
}

func (s Script) ExecutionPriority() int {
Expand Down
27 changes: 16 additions & 11 deletions internal/config/skip_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,41 @@ func NewSkipChecker(cmd system.Command) *skipChecker {
}

// check returns the result of applying a skip/only setting which can be a branch, git state, shell command, etc.
func (sc *skipChecker) check(gitState git.State, skip interface{}, only interface{}) bool {
func (sc *skipChecker) check(state func() git.State, skip interface{}, only interface{}) bool {
if skip == nil && only == nil {
return false
}

if skip != nil {
if sc.matches(gitState, skip) {
if sc.matches(state, skip) {
return true
}
}

if only != nil {
return !sc.matches(gitState, only)
return !sc.matches(state, only)
}

return false
}

func (sc *skipChecker) matches(gitState git.State, value interface{}) bool {
func (sc *skipChecker) matches(state func() git.State, value interface{}) bool {
switch typedValue := value.(type) {
case bool:
return typedValue
case string:
return typedValue == gitState.Step
return typedValue == state().State
case []interface{}:
return sc.matchesSlices(gitState, typedValue)
return sc.matchesSlices(state, typedValue)
}
return false
}

func (sc *skipChecker) matchesSlices(gitState git.State, slice []interface{}) bool {
func (sc *skipChecker) matchesSlices(gitState func() git.State, slice []interface{}) bool {
for _, state := range slice {
switch typedState := state.(type) {
case string:
if typedState == gitState.Step {
if typedState == gitState().State {
return true
}
case map[string]interface{}:
Expand All @@ -64,19 +68,20 @@ func (sc *skipChecker) matchesSlices(gitState git.State, slice []interface{}) bo
return false
}

func (sc *skipChecker) matchesRef(gitState git.State, typedState map[string]interface{}) bool {
func (sc *skipChecker) matchesRef(state func() git.State, typedState map[string]interface{}) bool {
ref, ok := typedState["ref"].(string)
if !ok {
return false
}

if ref == gitState.Branch {
branch := state().Branch
if ref == branch {
return true
}

g := glob.MustCompile(ref)

return g.Match(gitState.Branch)
return g.Match(branch)
}

func (sc *skipChecker) matchesCommands(typedState map[string]interface{}) bool {
Expand Down
46 changes: 26 additions & 20 deletions internal/config/skip_checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,123 +23,129 @@ func TestDoSkip(t *testing.T) {

for _, tt := range [...]struct {
name string
state git.State
state func() git.State
skip, only interface{}
skipped bool
}{
{
name: "when true",
state: git.State{},
state: func() git.State { return git.State{} },
skip: true,
skipped: true,
},
{
name: "when false",
state: git.State{},
state: func() git.State { return git.State{} },
skip: false,
skipped: false,
},
{
name: "when merge",
state: git.State{Step: "merge"},
state: func() git.State { return git.State{State: "merge"} },
skip: "merge",
skipped: true,
},
{
name: "when merge-commit",
state: func() git.State { return git.State{State: "merge-commit"} },
skip: "merge-commit",
skipped: true,
},
{
name: "when rebase (but want merge)",
state: git.State{Step: "rebase"},
state: func() git.State { return git.State{State: "rebase"} },
skip: "merge",
skipped: false,
},
{
name: "when rebase",
state: git.State{Step: "rebase"},
state: func() git.State { return git.State{State: "rebase"} },
skip: []interface{}{"rebase"},
skipped: true,
},
{
name: "when rebase (but want merge)",
state: git.State{Step: "rebase"},
state: func() git.State { return git.State{State: "rebase"} },
skip: []interface{}{"merge"},
skipped: false,
},
{
name: "when branch",
state: git.State{Branch: "feat/skipme"},
state: func() git.State { return git.State{Branch: "feat/skipme"} },
skip: []interface{}{map[string]interface{}{"ref": "feat/skipme"}},
skipped: true,
},
{
name: "when branch doesn't match",
state: git.State{Branch: "feat/important"},
state: func() git.State { return git.State{Branch: "feat/important"} },
skip: []interface{}{map[string]interface{}{"ref": "feat/skipme"}},
skipped: false,
},
{
name: "when branch glob",
state: git.State{Branch: "feat/important"},
state: func() git.State { return git.State{Branch: "feat/important"} },
skip: []interface{}{map[string]interface{}{"ref": "feat/*"}},
skipped: true,
},
{
name: "when branch glob doesn't match",
state: git.State{Branch: "feat"},
state: func() git.State { return git.State{Branch: "feat"} },
skip: []interface{}{map[string]interface{}{"ref": "feat/*"}},
skipped: false,
},
{
name: "when only specified",
state: git.State{Branch: "feat"},
state: func() git.State { return git.State{Branch: "feat"} },
only: []interface{}{map[string]interface{}{"ref": "feat"}},
skipped: false,
},
{
name: "when only branch doesn't match",
state: git.State{Branch: "dev"},
state: func() git.State { return git.State{Branch: "dev"} },
only: []interface{}{map[string]interface{}{"ref": "feat"}},
skipped: true,
},
{
name: "when only branch with glob",
state: git.State{Branch: "feat/important"},
state: func() git.State { return git.State{Branch: "feat/important"} },
only: []interface{}{map[string]interface{}{"ref": "feat/*"}},
skipped: false,
},
{
name: "when only merge",
state: git.State{Step: "merge"},
state: func() git.State { return git.State{State: "merge"} },
only: []interface{}{"merge"},
skipped: false,
},
{
name: "when only and skip",
state: git.State{Step: "rebase"},
state: func() git.State { return git.State{State: "rebase"} },
skip: []interface{}{map[string]interface{}{"ref": "feat/*"}},
only: "rebase",
skipped: false,
},
{
name: "when only and skip applies skip",
state: git.State{Step: "rebase"},
state: func() git.State { return git.State{State: "rebase"} },
skip: []interface{}{"rebase"},
only: "rebase",
skipped: true,
},
{
name: "when skip with run command",
state: git.State{},
state: func() git.State { return git.State{} },
skip: []interface{}{map[string]interface{}{"run": "success"}},
skipped: true,
},
{
name: "when skip with multi-run command",
state: git.State{Branch: "feat"},
state: func() git.State { return git.State{Branch: "feat"} },
skip: []interface{}{map[string]interface{}{"run": "success", "ref": "feat"}},
skipped: true,
},
{
name: "when only with run command",
state: git.State{},
state: func() git.State { return git.State{} },
only: []interface{}{map[string]interface{}{"run": "fail"}},
skipped: true,
},
Expand Down
61 changes: 50 additions & 11 deletions internal/git/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,68 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
)

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

const (
NilStep string = ""
MergeStep string = "merge"
RebaseStep string = "rebase"
Nil string = ""
Merge string = "merge"
MergeCommit string = "merge-commit"
Rebase string = "rebase"
)

var refBranchRegexp = regexp.MustCompile(`^ref:\s*refs/heads/(.+)$`)
var (
refBranchRegexp = regexp.MustCompile(`^ref:\s*refs/heads/(.+)$`)
cmdParentCommits = []string{"git", "show", "--no-patch", `--format="%P"`}
)

var (
state State
stateInitialized bool
)

func ResetState() {
stateInitialized = false
}

func (r *Repository) State() State {
if stateInitialized {
return state
}

stateInitialized = true
branch := r.Branch()
if r.inMergeState() {
return State{
state = State{
Branch: branch,
Step: MergeStep,
State: Merge,
}
return state
}
if r.inRebaseState() {
return State{
state = State{
Branch: branch,
Step: RebaseStep,
State: Rebase,
}
return state
}
return State{
if r.inMergeCommitState() {
state = State{
Branch: branch,
State: MergeCommit,
}
return state
}

state = State{
Branch: branch,
Step: NilStep,
State: Nil,
}
return state
}

func (r *Repository) Branch() string {
Expand Down Expand Up @@ -81,3 +111,12 @@ func (r *Repository) inRebaseState() bool {

return true
}

func (r *Repository) inMergeCommitState() bool {
parents, err := r.Git.Cmd(cmdParentCommits)
if err != nil {
return false
}

return strings.Contains(parents, " ")
}
Loading

0 comments on commit d5b7e77

Please sign in to comment.