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 stage_fixed option #445

Merged
merged 3 commits into from
Mar 15, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
- [`root`](#root)
- [`exclude`](#exclude)
- [`fail_text`](#fail_text)
- [`stage_fixed`](#stage_fixed)
- [`interactive`](#interactive)
- [Script](#script)
- [`runner`](#runner)
- [`skip`](#skip)
- [`tags`](#tags)
- [`env`](#env)
- [`fail_text`](#fail_text)
- [`stage_fixed`](#stage_fixed)
- [`interactive`](#interactive)
- [Examples](#examples)
- [More info](#more-info)
Expand Down Expand Up @@ -901,6 +903,26 @@ SUMMARY: (done in 0.01 seconds)
🥊 lint: Add node executable to $PATH env
```

### `stage_fixed`

**Default: `false`**

> Used **only for `pre-commit`** hook. Is ignored for other hooks.

When set to `true` lefthook will automatically call `git add` on files after running the command or script. For a command if [`files`](#files) option was specified, the specified command will be used to retrieve files for `git add`. For scripts and commands without [`files`](#files) option `{staged_files}` template will be used. All filters ([`glob`](#glob), [`exclude`](#exclude)) will be applied if specified.

**Example**

```yml
# lefthook.yml

pre-commit:
commands:
lint:
run: npm run lint --fix {staged_files}
stage_fixed: true
```

### `interactive`

**Default: `false`**
Expand Down
1 change: 1 addition & 0 deletions internal/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Command struct {

FailText string `mapstructure:"fail_text"`
Interactive bool `mapstructure:"interactive"`
StageFixed bool `mapstructure:"stage_fixed"`
}

func (c Command) Validate() error {
Expand Down
1 change: 1 addition & 0 deletions internal/config/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Script struct {

FailText string `mapstructure:"fail_text"`
Interactive bool `mapstructure:"interactive"`
StageFixed bool `mapstructure:"stage_fixed"`
}

func (s Script) DoSkip(gitState git.State) bool {
Expand Down
12 changes: 12 additions & 0 deletions internal/git/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,18 @@ func (r *Repository) DropUnstagedStash() error {
return nil
}

func (r *Repository) AddFiles(files []string) error {
if len(files) == 0 {
return nil
}

_, err := r.Git.CmdArgs(
append([]string{"git", "add"}, files...)...,
)

return err
}

// FilesByCommand accepts git command and returns its result as a list of filepaths.
func (r *Repository) FilesByCommand(command string) ([]string, error) {
lines, err := r.Git.CmdLines(command)
Expand Down
2 changes: 1 addition & 1 deletion internal/lefthook/runner/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
type ExecuteOptions struct {
name, root, failText string
args []string
interactive bool
env map[string]string
interactive bool
}

// Executor provides an interface for command execution.
Expand Down
19 changes: 14 additions & 5 deletions internal/lefthook/runner/prepare_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import (
"github.com/evilmartians/lefthook/internal/log"
)

func (r *Runner) prepareCommand(name string, command *config.Command) ([]string, error) {
type commandArgs struct {
all []string
files []string
}

func (r *Runner) prepareCommand(name string, command *config.Command) (*commandArgs, error) {
if command.Skip != nil && command.DoSkip(r.Repo.State()) {
return nil, errors.New("settings")
}
Expand All @@ -34,14 +39,14 @@ func (r *Runner) prepareCommand(name string, command *config.Command) ([]string,
log.Error(err)
return nil, errors.New("error")
}
if len(args) == 0 {
if args == nil || len(args.all) == 0 {
return nil, errors.New("no files for inspection")
}

return args, nil
}

func (r *Runner) buildCommandArgs(command *config.Command) ([]string, error) {
func (r *Runner) buildCommandArgs(command *config.Command) (*commandArgs, error) {
filesCommand := r.Hook.Files
if command.Files != "" {
filesCommand = command.Files
Expand All @@ -56,6 +61,7 @@ func (r *Runner) buildCommandArgs(command *config.Command) ([]string, error) {
},
}

filteredFiles := []string{}
runString := command.Run
for filesType, filesFn := range filesTypeToFn {
// Checking substitutions and skipping execution if it is empty.
Expand All @@ -76,7 +82,7 @@ func (r *Runner) buildCommandArgs(command *config.Command) ([]string, error) {
if len(filesPrepared) == 0 {
return nil, nil
}

filteredFiles = append(filteredFiles, filesPrepared...)
runString = replaceQuoted(runString, filesType, filesPrepared)
}
}
Expand All @@ -88,7 +94,10 @@ func (r *Runner) buildCommandArgs(command *config.Command) ([]string, error) {

log.Debug("[lefthook] executing: ", runString)

return strings.Split(runString, " "), nil
return &commandArgs{
files: filteredFiles,
all: strings.Split(runString, " "),
}, nil
}

func prepareFiles(command *config.Command, files []string) []string {
Expand Down
42 changes: 36 additions & 6 deletions internal/lefthook/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,26 @@ func (r *Runner) runScript(script *config.Script, path string, file os.FileInfo)
defer log.StartSpinner()
}

r.run(ExecuteOptions{
finished := r.run(ExecuteOptions{
name: file.Name(),
root: r.Repo.RootPath,
args: args,
failText: script.FailText,
interactive: script.Interactive && !r.DisableTTY,
env: script.Env,
}, r.Hook.Follow)

if finished && config.HookUsesStagedFiles(r.HookName) && script.StageFixed {
files, err := r.Repo.StagedFiles()
if err != nil {
log.Warn("Couldn't stage fixed files:", err)
return
}

if err := r.Repo.AddFiles(files); err != nil {
log.Warn("Couldn't stage fixed files:", err)
}
}
}

func (r *Runner) runCommands() {
Expand Down Expand Up @@ -346,17 +358,33 @@ func (r *Runner) runCommand(name string, command *config.Command) {
defer log.StartSpinner()
}

r.run(ExecuteOptions{
finished := r.run(ExecuteOptions{
name: name,
root: filepath.Join(r.Repo.RootPath, command.Root),
args: args,
args: args.all,
failText: command.FailText,
interactive: command.Interactive && !r.DisableTTY,
env: command.Env,
}, r.Hook.Follow)

if finished && config.HookUsesStagedFiles(r.HookName) && command.StageFixed {
files := args.files
if len(files) == 0 {
stagedFiles, err := r.Repo.StagedFiles()
if err != nil {
log.Warn("Couldn't stage fixed files:", err)
return
}
files = prepareFiles(command, stagedFiles)
}

if err := r.Repo.AddFiles(files); err != nil {
log.Warn("Couldn't stage fixed files:", err)
}
}
}

func (r *Runner) run(opts ExecuteOptions, follow bool) {
func (r *Runner) run(opts ExecuteOptions, follow bool) bool {
log.SetName(opts.name)
defer log.UnsetName(opts.name)

Expand All @@ -368,7 +396,7 @@ func (r *Runner) run(opts ExecuteOptions, follow bool) {
} else {
r.success(opts.name)
}
return
return err == nil
}

out := bytes.NewBuffer(make([]byte, 0))
Expand All @@ -384,14 +412,16 @@ func (r *Runner) run(opts ExecuteOptions, follow bool) {
}

if err == nil && r.SkipSettings.SkipExecution() {
return
return false
}

log.Infof("%s\n%s", execName, out)
if err != nil {
log.Infof("%s", err)
}
log.Infof("\n")

return err == nil
}

// Returns whether two arrays have at least one similar element.
Expand Down