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 priorities to scripts #684

Merged
merged 3 commits into from
Apr 1, 2024
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
13 changes: 11 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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)
- [Examples](#examples)
- [More info](#more-info)

Expand Down Expand Up @@ -1280,9 +1281,9 @@ Whether to use interactive mode. This applies the certain behavior:
>
> 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.
> Value `0` is considered an `+Infinity`, so commands or scripts with `priority: 0` or without this setting will be run at the very end.

Set command priority from 1 to +Infinity. This option can be used to configure the order of the sequential commands.
Set priority from 1 to +Infinity. This option can be used to configure the order of the sequential steps.

**Example**

Expand All @@ -1301,6 +1302,14 @@ post-checkout:
db-seed:
priority: 3
run: rails db:seed

scripts:
"check-spelling.sh":
runner: bash
priority: 1
"check-grammar.rb":
runner: ruby
priority: 2
```

## Script
Expand Down
8 changes: 6 additions & 2 deletions internal/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ type Command struct {
StageFixed bool `json:"stage_fixed,omitempty" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`
}

type commandRunReplace struct {
Run string `mapstructure:"run"`
}

func (c Command) Validate() error {
if !isRunnerFilesCompatible(c.Run) {
return errFilesIncompatible
Expand All @@ -44,8 +48,8 @@ func (c Command) DoSkip(gitState git.State) bool {
return skipChecker.Check(gitState, c.Skip, c.Only)
}

type commandRunReplace struct {
Run string `mapstructure:"run"`
func (c Command) ExecutionPriority() int {
return c.Priority
}

func mergeCommands(base, extra *viper.Viper) (map[string]*Command, error) {
Expand Down
17 changes: 11 additions & 6 deletions internal/config/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,29 @@ import (
type Script struct {
Runner string `json:"runner" mapstructure:"runner" toml:"runner" yaml:"runner"`

Skip interface{} `json:"skip,omitempty" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"`
Only interface{} `json:"only,omitempty" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"`
Tags []string `json:"tags,omitempty" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"`
Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"`
Skip interface{} `json:"skip,omitempty" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"`
Only interface{} `json:"only,omitempty" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"`
Tags []string `json:"tags,omitempty" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"`
Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"`
Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"`

FailText string `json:"fail_text,omitempty" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"`
Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"`
UseStdin bool `json:"use_stdin,omitempty" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:",omitempty"`
StageFixed bool `json:"stage_fixed,omitempty" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"`
}

type scriptRunnerReplace struct {
Runner string `mapstructure:"runner"`
}

func (s Script) DoSkip(gitState git.State) bool {
skipChecker := NewSkipChecker(NewOsExec())
return skipChecker.Check(gitState, s.Skip, s.Only)
}

type scriptRunnerReplace struct {
Runner string `mapstructure:"runner"`
func (s Script) ExecutionPriority() int {
return s.Priority
}

func mergeScripts(base, extra *viper.Viper) (map[string]*Script, error) {
Expand Down
52 changes: 34 additions & 18 deletions internal/lefthook/run/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ func NewRunner(opts Options) *Runner {
}
}

type executable interface {
*config.Command | *config.Script
ExecutionPriority() int
}

// RunAll runs scripts and commands.
// LFS hook is executed at first if needed.
func (r *Runner) RunAll(ctx context.Context, sourceDirs []string) {
Expand Down Expand Up @@ -235,10 +240,20 @@ func (r *Runner) runScripts(ctx context.Context, dir string) {
return
}

scripts := make([]string, 0, len(files))
filesMap := make(map[string]os.FileInfo)
for _, file := range files {
filesMap[file.Name()] = file
scripts = append(scripts, file.Name())
}
sortByPriority(scripts, r.Hook.Scripts)

interactiveScripts := make([]os.FileInfo, 0)
var wg sync.WaitGroup

for _, file := range files {
for _, name := range scripts {
file := filesMap[name]

if ctx.Err() != nil {
return
}
Expand Down Expand Up @@ -331,7 +346,7 @@ func (r *Runner) runCommands(ctx context.Context) {
}
}

sortCommands(commands, r.Hook.Commands)
sortByPriority(commands, r.Hook.Commands)

interactiveCommands := make([]string, 0)
var wg sync.WaitGroup
Expand Down Expand Up @@ -531,44 +546,45 @@ func (r *Runner) logExecute(name string, err error, out io.Reader) {
}
}

// 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.
// sortByPriority sorts the tags by preceding numbers if they occur and special priority if it is set.
// If the names starts with letter the command name will be sorted alphabetically.
// If there's a `priority` field defined for a command or script it will be used instead of alphanumeric sorting.
//
// []string{"1_command", "10command", "3 command", "command5"} // -> 1_command, 3 command, 10command, command5
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]]
func sortByPriority[E executable](tags []string, executables map[string]E) {
sort.SliceStable(tags, func(i, j int) bool {
exeI, okI := executables[tags[i]]
exeJ, okJ := executables[tags[j]]

if iOk && commandI.Priority != 0 || jOk && commandJ.Priority != 0 {
if !iOk || commandI.Priority == 0 {
if okI && exeI.ExecutionPriority() != 0 || okJ && exeJ.ExecutionPriority() != 0 {
if !okI || exeI.ExecutionPriority() == 0 {
return false
}
if !jOk || commandJ.Priority == 0 {
if !okJ || exeJ.ExecutionPriority() == 0 {
return true
}

return commandI.Priority < commandJ.Priority
return exeI.ExecutionPriority() < exeJ.ExecutionPriority()
}

numEnds := -1
for idx, ch := range strs[i] {
for idx, ch := range tags[i] {
if unicode.IsDigit(ch) {
numEnds = idx
} else {
break
}
}
if numEnds == -1 {
return strs[i] < strs[j]
return tags[i] < tags[j]
}
numI, err := strconv.Atoi(strs[i][:numEnds+1])
numI, err := strconv.Atoi(tags[i][:numEnds+1])
if err != nil {
return strs[i] < strs[j]
return tags[i] < tags[j]
}

numEnds = -1
for idx, ch := range strs[j] {
for idx, ch := range tags[j] {
if unicode.IsDigit(ch) {
numEnds = idx
} else {
Expand All @@ -578,7 +594,7 @@ func sortCommands(strs []string, commands map[string]*config.Command) {
if numEnds == -1 {
return true
}
numJ, err := strconv.Atoi(strs[j][:numEnds+1])
numJ, err := strconv.Atoi(tags[j][:numEnds+1])
if err != nil {
return true
}
Expand Down
41 changes: 39 additions & 2 deletions internal/lefthook/run/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,8 @@ func TestReplaceQuoted(t *testing.T) {
}
}

func TestSortCommands(t *testing.T) {
//nolint:dupl
func TestSortByPriorityCommands(t *testing.T) {
for i, tt := range [...]struct {
name string
names []string
Expand All @@ -931,7 +932,43 @@ func TestSortCommands(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("%d: %s", i+1, tt.name), func(t *testing.T) {
sortCommands(tt.names, tt.commands)
sortByPriority(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])
}
}
})
}
}

//nolint:dupl
func TestSortByPriorityScripts(t *testing.T) {
for i, tt := range [...]struct {
name string
names []string
scripts map[string]*config.Script
result []string
}{
{
name: "alphanumeric sort",
names: []string{"10_a.sh", "1_a.sh", "2_a.sh", "5_b.sh"},
scripts: map[string]*config.Script{},
result: []string{"1_a.sh", "2_a.sh", "5_b.sh", "10_a.sh"},
},
{
name: "partial priority",
names: []string{"10.rb", "file.sh", "script.go", "5_a.sh"},
scripts: map[string]*config.Script{
"5_a.sh": {Priority: 10},
"script.go": {Priority: 1},
"10.rb": {},
},
result: []string{"script.go", "5_a.sh", "10.rb", "file.sh"},
},
} {
t.Run(fmt.Sprintf("%d: %s", i+1, tt.name), func(t *testing.T) {
sortByPriority(tt.names, tt.scripts)
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])
Expand Down
Loading