diff --git a/cmd/run.go b/cmd/run.go index 4f2999b3..a2fca760 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -33,10 +33,15 @@ func newRunCmd(opts *lefthook.Options) *cobra.Command { runCmd.Flags().BoolVar( &runArgs.AllFiles, "all-files", false, - "run hooks on all files", + "run hooks on all files in the Git index", ) - runCmd.Flags().StringSliceVar(&runArgs.Files, "files", nil, "run on specified files. takes precedence over --all-files") + runCmd.Flags().BoolVar( + &runArgs.AllFilesIncludingUntracked, "all-including-untracked-files", false, + "run hooks on all files, including untracked ones", + ) + + runCmd.Flags().StringSliceVar(&runArgs.Files, "files", nil, "run on specified files. takes precedence over --all-files and --all-including-untracked-filesß") runCmd.Flags().StringSliceVar(&runArgs.RunOnlyCommands, "commands", nil, "run only specified commands") diff --git a/docs/usage.md b/docs/usage.md index fcfae0e1..6ed0425c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -130,7 +130,7 @@ You can also specify a flag to run only some commands: $ lefthook run pre-commit --commands lint ``` -and optionally run either on all files (any `{staged_files}` placeholder acts as `{all_files}`) or a list of files: +and optionally run either on all files in the Git index (any `{staged_files}` placeholder acts as `{all_files}`) or a list of files: ```bash $ lefthook run pre-commit --all-files @@ -139,6 +139,8 @@ $ lefthook run pre-commit --files file1.js,file2.js (if both are specified, `--all-files` is ignored) +In the same way, you can use `--all-including-untracked-files`. + ### `lefthook version` You can check version with `lefthook version` and you can also check the commit hash with `lefthook version --full` diff --git a/internal/config/files.go b/internal/config/files.go index ba9f1da5..3c9964de 100644 --- a/internal/config/files.go +++ b/internal/config/files.go @@ -3,10 +3,11 @@ package config import "strings" const ( - SubFiles string = "{files}" - SubAllFiles string = "{all_files}" - SubStagedFiles string = "{staged_files}" - PushFiles string = "{push_files}" + SubFiles string = "{files}" + SubAllFiles string = "{all_files}" + SubAllFilesIncludingUntracked string = "{all_including_untracked_files}" + SubStagedFiles string = "{staged_files}" + PushFiles string = "{push_files}" ) func isRunnerFilesCompatible(runner string) bool { diff --git a/internal/git/repository.go b/internal/git/repository.go index 693ab6e4..4955033f 100644 --- a/internal/git/repository.go +++ b/internal/git/repository.go @@ -11,17 +11,18 @@ import ( ) const ( - cmdRootPath = "git rev-parse --show-toplevel" - cmdHooksPath = "git rev-parse --git-path hooks" - cmdInfoPath = "git rev-parse --git-path info" - cmdGitPath = "git rev-parse --git-dir" - cmdStagedFiles = "git diff --name-only --cached --diff-filter=ACMR" - cmdAllFiles = "git ls-files --cached" - cmdPushFilesBase = "git diff --name-only HEAD @{push}" - cmdPushFilesHead = "git diff --name-only HEAD %s" - cmdStatusShort = "git status --short" - cmdCreateStash = "git stash create" - cmdListStash = "git stash list" + cmdRootPath = "git rev-parse --show-toplevel" + cmdHooksPath = "git rev-parse --git-path hooks" + cmdInfoPath = "git rev-parse --git-path info" + cmdGitPath = "git rev-parse --git-dir" + cmdStagedFiles = "git diff --name-only --cached --diff-filter=ACMR" + cmdAllFiles = "git ls-files --cached" + cmdAllFilesIncludingUntracked = "git ls-files --cached --others --exclude-standard" + cmdPushFilesBase = "git diff --name-only HEAD @{push}" + cmdPushFilesHead = "git diff --name-only HEAD %s" + cmdStatusShort = "git status --short" + cmdCreateStash = "git stash create" + cmdListStash = "git stash list" stashMessage = "lefthook auto backup" unstagedPatchName = "lefthook-unstaged.patch" @@ -103,6 +104,12 @@ func (r *Repository) AllFiles() ([]string, error) { return r.FilesByCommand(cmdAllFiles) } +// StagedFiles returns a list of all files in repository +// or an error if git command fails. +func (r *Repository) AllFilesIncludingUntracked() ([]string, error) { + return r.FilesByCommand(cmdAllFilesIncludingUntracked) +} + // PushFiles returns a list of files that are ready to be pushed // or an error if git command fails. func (r *Repository) PushFiles() ([]string, error) { diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 0fec2287..b45c9b71 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -23,11 +23,12 @@ const ( ) type RunArgs struct { - NoTTY bool - AllFiles bool - Force bool - Files []string - RunOnlyCommands []string + NoTTY bool + AllFiles bool + AllFilesIncludingUntracked bool + Force bool + Files []string + RunOnlyCommands []string } func Run(opts *Options, args RunArgs, hookName string, gitArgs []string) error { @@ -116,17 +117,18 @@ Run 'lefthook install' manually.`, runner := run.NewRunner( run.Options{ - Repo: l.repo, - Hook: hook, - HookName: hookName, - GitArgs: gitArgs, - ResultChan: resultChan, - SkipSettings: logSettings, - DisableTTY: cfg.NoTTY || args.NoTTY, - AllFiles: args.AllFiles, - Files: args.Files, - Force: args.Force, - RunOnlyCommands: args.RunOnlyCommands, + Repo: l.repo, + Hook: hook, + HookName: hookName, + GitArgs: gitArgs, + ResultChan: resultChan, + SkipSettings: logSettings, + DisableTTY: cfg.NoTTY || args.NoTTY, + Files: args.Files, + AllFiles: args.AllFiles, + AllFilesIncludingUntracked: args.AllFilesIncludingUntracked, + Force: args.Force, + RunOnlyCommands: args.RunOnlyCommands, }, ) diff --git a/internal/lefthook/run/prepare_command.go b/internal/lefthook/run/prepare_command.go index 644b8bb9..3fef77b0 100644 --- a/internal/lefthook/run/prepare_command.go +++ b/internal/lefthook/run/prepare_command.go @@ -64,29 +64,24 @@ func (r *Runner) prepareCommand(name string, command *config.Command) (*run, err return args, nil } -func (r *Runner) buildRun(command *config.Command) (*run, error, error) { - filesCmd := r.Hook.Files - if len(command.Files) > 0 { - filesCmd = command.Files - } - if len(filesCmd) > 0 { - filesCmd = replacePositionalArguments(filesCmd, r.GitArgs) - } - +func (r *Runner) replaceTemplates(command *config.Command, filesCmd string) (map[string]*template, error, error) { var stagedFiles func() ([]string, error) switch { case len(r.Files) > 0: stagedFiles = func() ([]string, error) { return r.Files, nil } case r.AllFiles: stagedFiles = r.Repo.AllFiles + case r.AllFilesIncludingUntracked: + stagedFiles = r.Repo.AllFilesIncludingUntracked default: stagedFiles = r.Repo.StagedFiles } filesFns := map[string]func() ([]string, error){ - config.SubStagedFiles: stagedFiles, - config.PushFiles: r.Repo.PushFiles, - config.SubAllFiles: r.Repo.AllFiles, + config.SubStagedFiles: stagedFiles, + config.PushFiles: r.Repo.PushFiles, + config.SubAllFiles: r.Repo.AllFiles, + config.SubAllFilesIncludingUntracked: r.Repo.AllFilesIncludingUntracked, config.SubFiles: func() ([]string, error) { return r.Repo.FilesByCommand(filesCmd) }, @@ -96,8 +91,10 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) { for filesType, fn := range filesFns { cnt := strings.Count(command.Run, filesType) - if cnt == 0 { - continue + if cnt == 0 && + (r.AllFilesIncludingUntracked && filesType == config.SubAllFilesIncludingUntracked) || + (r.AllFiles && filesType == config.SubAllFiles) { + cnt++ } templ := &template{cnt: cnt} @@ -132,6 +129,25 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) { } } + return templates, nil, nil +} + +func (r *Runner) buildRun(command *config.Command) (*run, error, error) { + filesCmd := r.Hook.Files + if len(command.Files) > 0 { + filesCmd = command.Files + } + if len(filesCmd) > 0 { + filesCmd = replacePositionalArguments(filesCmd, r.GitArgs) + } + + templates, err1, err2 := r.replaceTemplates(command, filesCmd) + if err1 != nil { + return nil, err1, nil + } else if err2 != nil { + return nil, nil, err2 + } + runString := command.Run runString = replacePositionalArguments(runString, r.GitArgs) @@ -146,30 +162,29 @@ func (r *Runner) buildRun(command *config.Command) (*run, error, error) { } result := replaceInChunks(runString, templates, maxlen) - if r.Force || len(result.files) != 0 { + switch { + case r.Force || len(result.files) > 0: return result, nil, nil - } - if config.HookUsesStagedFiles(r.HookName) { + case len(result.files) == 0: + return nil, nil, errors.New("no matching files") + + case config.HookUsesStagedFiles(r.HookName): ok, err := canSkipCommand(command, templates[config.SubStagedFiles], r.Repo.StagedFiles) if err != nil { return nil, err, nil - } - if ok { + } else if ok { return nil, nil, errors.New("no matching staged files") } - } - if config.HookUsesPushFiles(r.HookName) { + case config.HookUsesPushFiles(r.HookName): ok, err := canSkipCommand(command, templates[config.PushFiles], r.Repo.PushFiles) if err != nil { return nil, err, nil - } - if ok { + } else if ok { return nil, nil, errors.New("no matching push files") } } - return result, nil, nil } @@ -232,6 +247,12 @@ func replaceInChunks(str string, templates map[string]*template, maxlen int) *ru template.files = escapeFiles(template.files) } + if len(allFiles) == 0 { + return &run{ + commands: []string{str}, + } + } + maxlen -= len(str) if cnt > 0 { diff --git a/internal/lefthook/run/runner.go b/internal/lefthook/run/runner.go index 3ccd3c52..0b6bce1b 100644 --- a/internal/lefthook/run/runner.go +++ b/internal/lefthook/run/runner.go @@ -38,17 +38,18 @@ const ( var surroundingQuotesRegexp = regexp.MustCompile(`^'(.*)'$`) type Options struct { - Repo *git.Repository - Hook *config.Hook - HookName string - GitArgs []string - ResultChan chan Result - SkipSettings log.SkipSettings - DisableTTY bool - AllFiles bool - Force bool - Files []string - RunOnlyCommands []string + Repo *git.Repository + Hook *config.Hook + HookName string + GitArgs []string + ResultChan chan Result + SkipSettings log.SkipSettings + DisableTTY bool + AllFiles bool + AllFilesIncludingUntracked bool + Force bool + Files []string + RunOnlyCommands []string } // Runner responds for actual execution and handling the results.