diff --git a/CHANGELOG.md b/CHANGELOG.md index 65055303..1f1c3573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## master (unreleased) +- feat: add priorities to commands ([#589](https://github.com/evilmartians/lefthook/pull/589)) by @mrexox + ## 1.5.4 (2023-11-27) - chore: add typos fixer by @mrexox diff --git a/docs/configuration.md b/docs/configuration.md index e2273d73..0eb5a7ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -46,6 +46,7 @@ Lefthook [supports](#config-file) YAML, JSON, and TOML configuration. In this do - [`stage_fixed`](#stage_fixed) - [`interactive`](#interactive) - [`use_stdin`](#use_stdin) + - [`priority`](#priority) - [Script](#script) - [`runner`](#runner) - [`skip`](#skip) @@ -339,7 +340,9 @@ remote: ref: v1.0.0 ``` -> :warning: Please, note that if you initially had `ref` option, ran `lefthook install`, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups. +> **Note** +> +> :warning: If you initially had `ref` option, ran `lefthook install`, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups. ### `config` @@ -427,9 +430,9 @@ pre-push: run: yarn test ``` -**Notes** - -If used with [`parallel`](#parallel) the output can be a mess, so please avoid setting both options to `true`. +> **Note** +> +> If used with [`parallel`](#parallel) the output can be a mess, so please avoid setting both options to `true`. ### `exclude_tags` @@ -599,7 +602,9 @@ pre-push: Simply run `bundle exec rubocop` on all files with `.rb` extension excluding `application.rb` and `routes.rb` files. -**Note:** `--force-exclusion` will apply `Exclude` configuration setting of Rubocop. +> **Note** +> +> `--force-exclusion` will apply `Exclude` configuration setting of Rubocop. ```yml # lefthook.yml @@ -785,9 +790,9 @@ pre-commit: You can force a command, script, or the whole hook to execute only in certain conditions. This option acts like the opposite of [`skip`](#skip). It accepts the same values but skips execution only if the condition is not satisfied. -**Note** - -`skip` option takes precedence over `only` option, so if you have conflicting conditions the execution will be skipped. +> **Note** +> +> `skip` option takes precedence over `only` option, so if you have conflicting conditions the execution will be skipped. **Example** @@ -1084,14 +1089,46 @@ pre-commit: **Default: `false`** +> **Note** +> +> If you want to pass stdin to your command or script but don't need to get the input from CLI, use [`use_stdin`](#use_stdin) option instead. + + Whether to use interactive mode. This applies the certain behavior: - All `interactive` commands/scripts are executed after non-interactive. - When executing, lefthook tries to open /dev/tty (Linux/Unix only) and use it as stdin. - When [`no_tty`](#no_tty) option is set, `interactive` is ignored. -**Note** +### `priority` + +**Default: `0`** + +> **Note** +> +> This option makes sense only when `parallel: false` or `piped: true` is set. +> +> Value `0` is considered an `+Infinity`, so commands with `priority: 0` or without this setting will be run at the very end. -If you want to pass stdin to your command or script but don't need to get the input from CLI, use [`use_stdin`](#use_stdin) option instead. +Set command priority from 1 to +Infinity. This option can be used to configure the order of the sequential commands. + +**Example** + +```yml +# lefthook.yml + +post-checkout: + piped: true + commands: + db-create: + priority: 1 + run: rails db:create + db-migrate: + priority: 2 + run: rails db:migrate + db-seed: + priority: 3 + run: rails db:seed +``` ## Script @@ -1141,11 +1178,11 @@ When you try to commit `git commit -m "bad commit text"` script `template_checke ### `use_stdin` -Pass the stdin from the OS to the command/script. +> **Note** +> +> With many commands or scripts having `use_stdin: true`, only one will receive the data. The others will have nothing. If you need to pass the data from stdin to every command or script, please, submit a [feature request](https://github.com/evilmartians/lefthook/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.md). -**Note** - -With many commands or scripts having `use_stdin: true`, only one will receive the data. The others will have nothing. If you need to pass the data from stdin to every command or script, please, submit a [feature request](https://github.com/evilmartians/lefthook/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.md). +Pass the stdin from the OS to the command/script. **Example** diff --git a/internal/config/command.go b/internal/config/command.go index a0412305..79a70f2e 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -21,8 +21,9 @@ type Command struct { Files string `mapstructure:"files" yaml:",omitempty" json:"files,omitempty" toml:"files,omitempty"` Env map[string]string `mapstructure:"env" yaml:",omitempty" json:"env,omitempty" toml:"env,omitempty"` - Root string `mapstructure:"root" yaml:",omitempty" json:"root,omitempty" toml:"root,omitempty"` - Exclude string `mapstructure:"exclude" yaml:",omitempty" json:"exclude,omitempty" toml:"exclude,omitempty"` + Root string `mapstructure:"root" yaml:",omitempty" json:"root,omitempty" toml:"root,omitempty"` + Exclude string `mapstructure:"exclude" yaml:",omitempty" json:"exclude,omitempty" toml:"exclude,omitempty"` + Priority int `mapstructure:"priority" yaml:",omitempty" json:"priority,omitempty" toml:"priority,omitempty"` FailText string `mapstructure:"fail_text" yaml:"fail_text,omitempty" json:"fail_text,omitempty" toml:"fail_text,omitempty"` Interactive bool `mapstructure:"interactive" yaml:",omitempty" json:"interactive,omitempty" toml:"interactive,omitempty"` diff --git a/internal/lefthook/run/runner.go b/internal/lefthook/run/runner.go index 6a8024ee..3ccd3c52 100644 --- a/internal/lefthook/run/runner.go +++ b/internal/lefthook/run/runner.go @@ -255,7 +255,7 @@ func (r *Runner) runScripts(ctx context.Context, dir string) { continue } - if script.Interactive { + if script.Interactive && !r.Hook.Piped { interactiveScripts = append(interactiveScripts, file) continue } @@ -332,7 +332,7 @@ func (r *Runner) runCommands(ctx context.Context) { } } - sortAlnum(commands) + sortCommands(commands, r.Hook.Commands) interactiveCommands := make([]string, 0) var wg sync.WaitGroup @@ -343,7 +343,7 @@ func (r *Runner) runCommands(ctx context.Context) { continue } - if r.Hook.Commands[name].Interactive { + if r.Hook.Commands[name].Interactive && !r.Hook.Piped { interactiveCommands = append(interactiveCommands, name) continue } @@ -532,12 +532,26 @@ func (r *Runner) logExecute(name string, err error, out io.Reader) { } } -// sortAlnum sorts the command names by preceding numbers if they occur. +// sortCommands sorts the command names by preceding numbers if they occur and special priority if it is set. // If the command names starts with letter the command name will be sorted alphabetically. // // []string{"1_command", "10command", "3 command", "command5"} // -> 1_command, 3 command, 10command, command5 -func sortAlnum(strs []string) { +func sortCommands(strs []string, commands map[string]*config.Command) { sort.SliceStable(strs, func(i, j int) bool { + commandI, iOk := commands[strs[i]] + commandJ, jOk := commands[strs[j]] + + if iOk && commandI.Priority != 0 || jOk && commandJ.Priority != 0 { + if !iOk || commandI.Priority == 0 { + return false + } + if !jOk || commandJ.Priority == 0 { + return true + } + + return commandI.Priority < commandJ.Priority + } + numEnds := -1 for idx, ch := range strs[i] { if unicode.IsDigit(ch) { diff --git a/internal/lefthook/run/runner_test.go b/internal/lefthook/run/runner_test.go index 211b0c94..9206aae0 100644 --- a/internal/lefthook/run/runner_test.go +++ b/internal/lefthook/run/runner_test.go @@ -918,3 +918,38 @@ func TestReplaceQuoted(t *testing.T) { }) } } + +func TestSortCommands(t *testing.T) { + for i, tt := range [...]struct { + name string + names []string + commands map[string]*config.Command + result []string + }{ + { + name: "alphanumeric sort", + names: []string{"10_a", "1_a", "2_a", "5_a"}, + commands: map[string]*config.Command{}, + result: []string{"1_a", "2_a", "5_a", "10_a"}, + }, + { + name: "partial priority", + names: []string{"10_a", "1_a", "2_a", "5_a"}, + commands: map[string]*config.Command{ + "5_a": {Priority: 10}, + "2_a": {Priority: 1}, + "10_a": {}, + }, + result: []string{"2_a", "5_a", "1_a", "10_a"}, + }, + } { + t.Run(fmt.Sprintf("%d: %s", i+1, tt.name), func(t *testing.T) { + sortCommands(tt.names, tt.commands) + for i, name := range tt.result { + if tt.names[i] != name { + t.Errorf("Not matching on index %d: %s != %s", i, name, tt.names[i]) + } + } + }) + } +}