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

feat: add skip option merge-commit #850

Merged
merged 4 commits into from
Oct 18, 2024
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: 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
Loading