From 43ca10dbacfbe339357b325ab22d0974ff2ba89f Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Fri, 6 Oct 2023 13:30:13 +0300 Subject: [PATCH] feat: add force flag to run command (#561) --- cmd/run.go | 5 +++ internal/lefthook/run.go | 2 + internal/lefthook/run/prepare_command.go | 54 +++++++++++++++--------- internal/lefthook/run/runner.go | 1 + internal/lefthook/run/runner_test.go | 33 +++++++++++++++ 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 94370465..4f2999b3 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -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", diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index a5031d3c..3d9266ea 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -25,6 +25,7 @@ const ( type RunArgs struct { NoTTY bool AllFiles bool + Force bool Files []string RunOnlyCommands []string } @@ -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, }, ) diff --git a/internal/lefthook/run/prepare_command.go b/internal/lefthook/run/prepare_command.go index a51f4433..644b8bb9 100644 --- a/internal/lefthook/run/prepare_command.go +++ b/internal/lefthook/run/prepare_command.go @@ -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") } @@ -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 @@ -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 { diff --git a/internal/lefthook/run/runner.go b/internal/lefthook/run/runner.go index c1b59398..37e42c1f 100644 --- a/internal/lefthook/run/runner.go +++ b/internal/lefthook/run/runner.go @@ -44,6 +44,7 @@ type Options struct { SkipSettings log.SkipSettings DisableTTY bool AllFiles bool + Force bool Files []string RunOnlyCommands []string } diff --git a/internal/lefthook/run/runner_test.go b/internal/lefthook/run/runner_test.go index c33dc5a1..211b0c94 100644 --- a/internal/lefthook/run/runner_test.go +++ b/internal/lefthook/run/runner_test.go @@ -112,6 +112,7 @@ func TestRunAll(t *testing.T) { hook *config.Hook success, fail []Result gitCommands []string + force bool }{ { name: "empty hook", @@ -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", @@ -737,6 +769,7 @@ func TestRunAll(t *testing.T) { HookName: tt.hookName, GitArgs: tt.args, ResultChan: resultChan, + Force: tt.force, }, executor: executor, }