Skip to content

Commit

Permalink
feat: add force flag to run command (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrexox authored Oct 6, 2023
1 parent c8e4b13 commit 43ca10d
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 20 deletions.
5 changes: 5 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ func newRunCmd(opts *lefthook.Options) *cobra.Command {
},
}

runCmd.Flags().BoolVarP(
&runArgs.Force, "force", "f", false,
"force execution of commands that can be skipped",
)

runCmd.Flags().BoolVarP(
&runArgs.NoTTY, "no-tty", "n", false,
"run hook non-interactively, disable spinner",
Expand Down
2 changes: 2 additions & 0 deletions internal/lefthook/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
type RunArgs struct {
NoTTY bool
AllFiles bool
Force bool
Files []string
RunOnlyCommands []string
}
Expand Down Expand Up @@ -124,6 +125,7 @@ Run 'lefthook install' manually.`,
DisableTTY: cfg.NoTTY || args.NoTTY,
AllFiles: args.AllFiles,
Files: args.Files,
Force: args.Force,
RunOnlyCommands: args.RunOnlyCommands,
},
)
Expand Down
54 changes: 34 additions & 20 deletions internal/lefthook/run/prepare_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) {
}

files = filter.Apply(command, files)
if len(files) == 0 {
if !r.Force && len(files) == 0 {
return nil, nil, errors.New("no files for inspection")
}

Expand All @@ -119,7 +119,7 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) {
// Checking substitutions and skipping execution if it is empty.
//
// Special case for `files` option: return if the result of files command is empty.
if len(filesCmd) > 0 && templates[config.SubFiles] == nil {
if !r.Force && len(filesCmd) > 0 && templates[config.SubFiles] == nil {
files, err := filesFns[config.SubFiles]()
if err != nil {
return nil, fmt.Errorf("error calling replace command for %s: %w", config.SubFiles, err), nil
Expand All @@ -146,35 +146,49 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) {
}
result := replaceInChunks(runString, templates, maxlen)

if len(result.files) == 0 && config.HookUsesStagedFiles(r.HookName) {
if templates[config.SubStagedFiles] != nil && len(templates[config.SubStagedFiles].files) == 0 {
return nil, nil, errors.New("no matching staged files")
}
if r.Force || len(result.files) != 0 {
return result, nil, nil
}

files, err := r.Repo.StagedFiles()
if err == nil {
if len(filter.Apply(command, files)) == 0 {
return nil, nil, errors.New("no matching staged files")
}
if config.HookUsesStagedFiles(r.HookName) {
ok, err := canSkipCommand(command, templates[config.SubStagedFiles], r.Repo.StagedFiles)
if err != nil {
return nil, err, nil
}
if ok {
return nil, nil, errors.New("no matching staged files")
}
}

if len(result.files) == 0 && config.HookUsesPushFiles(r.HookName) {
if templates[config.PushFiles] != nil && len(templates[config.PushFiles].files) == 0 {
return nil, nil, errors.New("no matching push files")
if config.HookUsesPushFiles(r.HookName) {
ok, err := canSkipCommand(command, templates[config.PushFiles], r.Repo.PushFiles)
if err != nil {
return nil, err, nil
}

files, err := r.Repo.PushFiles()
if err == nil {
if len(filter.Apply(command, files)) == 0 {
return nil, nil, errors.New("no matching push files")
}
if ok {
return nil, nil, errors.New("no matching push files")
}
}

return result, nil, nil
}

func canSkipCommand(command *config.Command, template *template, filesFn func() ([]string, error)) (bool, error) {
if template != nil {
return len(template.files) == 0, nil
}

files, err := filesFn()
if err != nil {
return false, fmt.Errorf("error getting files: %w", err)
}
if len(filter.Apply(command, files)) == 0 {
return true, nil
}

return false, nil
}

func replacePositionalArguments(str string, args []string) string {
str = strings.ReplaceAll(str, "{0}", strings.Join(args, " "))
for i, arg := range args {
Expand Down
1 change: 1 addition & 0 deletions internal/lefthook/run/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Options struct {
SkipSettings log.SkipSettings
DisableTTY bool
AllFiles bool
Force bool
Files []string
RunOnlyCommands []string
}
Expand Down
33 changes: 33 additions & 0 deletions internal/lefthook/run/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func TestRunAll(t *testing.T) {
hook *config.Hook
success, fail []Result
gitCommands []string
force bool
}{
{
name: "empty hook",
Expand Down Expand Up @@ -673,6 +674,37 @@ func TestRunAll(t *testing.T) {
"git stash list",
},
},
{
name: "skippable pre-commit hook with force",
hookName: "pre-commit",
existingFiles: []string{
filepath.Join(root, "README.md"),
},
hook: &config.Hook{
Commands: map[string]*config.Command{
"ok": {
Run: "success",
StageFixed: true,
Glob: "*.md",
},
"fail": {
Run: "fail",
StageFixed: true,
Glob: "*.sh",
},
},
},
force: true,
success: []Result{{Name: "ok", Status: StatusOk}},
fail: []Result{{Name: "fail", Status: StatusErr}},
gitCommands: []string{
"git status --short",
"git diff --name-only --cached --diff-filter=ACMR",
"git add .*README.md",
"git apply -v --whitespace=nowarn --recount --unidiff-zero ",
"git stash list",
},
},
{
name: "pre-commit hook with stage_fixed under root",
hookName: "pre-commit",
Expand Down Expand Up @@ -737,6 +769,7 @@ func TestRunAll(t *testing.T) {
HookName: tt.hookName,
GitArgs: tt.args,
ResultChan: resultChan,
Force: tt.force,
},
executor: executor,
}
Expand Down

0 comments on commit 43ca10d

Please sign in to comment.