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 force flag to run command #561

Merged
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
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