diff --git a/internal/config/do.go b/internal/config/actions.go similarity index 88% rename from internal/config/do.go rename to internal/config/actions.go index dec1b2a4..ebeae8cb 100644 --- a/internal/config/do.go +++ b/internal/config/actions.go @@ -1,6 +1,6 @@ package config -type Do struct { +type Action struct { Name string `json:"name,omitempty" mapstructure:"name" toml:"name,omitempty" yaml:",omitempty"` Run string `json:"run,omitempty" mapstructure:"run" toml:"run,omitempty" yaml:",omitempty"` Script string `json:"script,omitempty" mapstructure:"script" toml:"script,omitempty" yaml:",omitempty"` @@ -33,18 +33,18 @@ type Group struct { Parallel bool `json:"parallel,omitempty" mapstructure:"parallel" toml:"parallel,omitempty" yaml:",omitempty"` Piped bool `json:"piped,omitempty" mapstructure:"piped" toml:"piped,omitempty" yaml:",omitempty"` Glob string `json:"glob,omitempty" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"` - Do []*Do `json:"do,omitempty" mapstructure:"do" toml:"do,omitempty" yaml:",omitempty"` + Actions []*Action `json:"actions,omitempty" mapstructure:"actions" toml:"actions,omitempty" yaml:",omitempty"` } -func (do *Do) PrintableName(id string) string { - if len(do.Name) != 0 { - return do.Name +func (action *Action) PrintableName(id string) string { + if len(action.Name) != 0 { + return action.Name } - if len(do.Run) != 0 { - return do.Run + if len(action.Run) != 0 { + return action.Run } - if len(do.Script) != 0 { - return do.Script + if len(action.Script) != 0 { + return action.Script } return "[" + id + "]" diff --git a/internal/config/hook.go b/internal/config/hook.go index 1aa58e6b..fac54e2a 100644 --- a/internal/config/hook.go +++ b/internal/config/hook.go @@ -18,7 +18,7 @@ type Hook struct { 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"` - Do []*Do `json:"do,omitempty" mapstructure:"do" toml:"do,omitempty" yaml:",omitempty"` + Actions []*Action `json:"actions,omitempty" mapstructure:"actions" toml:"actions,omitempty" yaml:",omitempty"` // Should be unmarshalled with `mapstructure:"commands"` // But replacing '{cmd}' is still an issue diff --git a/internal/lefthook/runner/do.go b/internal/lefthook/runner/do.go deleted file mode 100644 index 0fceb8bb..00000000 --- a/internal/lefthook/runner/do.go +++ /dev/null @@ -1,213 +0,0 @@ -package runner - -import ( - "context" - "errors" - "path/filepath" - "strconv" - "sync" - "sync/atomic" - - "github.com/evilmartians/lefthook/internal/config" - "github.com/evilmartians/lefthook/internal/lefthook/runner/action" - "github.com/evilmartians/lefthook/internal/lefthook/runner/exec" - "github.com/evilmartians/lefthook/internal/lefthook/runner/filters" - "github.com/evilmartians/lefthook/internal/log" -) - -var ( - errDoContainsBothRunAndScript = errors.New("both `run` and `script` are not permitted") - errEmptyDo = errors.New("no execution instructions") - errEmptyGroup = errors.New("empty groups are not permitted") -) - -type domain struct { - failed atomic.Bool - glob string -} - -func (r *Runner) do(ctx context.Context) []Result { - var wg sync.WaitGroup - - results := make([]Result, 0, len(r.Hook.Do)) - resultsChan := make(chan Result, len(r.Hook.Do)) - domain := &domain{} - for i, do := range r.Hook.Do { - id := strconv.Itoa(i) - - if domain.failed.Load() && r.Hook.Piped { - r.logSkip(do.PrintableName(id), "broken pipe") - continue - } - - if !r.Hook.Parallel { - results = append(results, r.runDo(ctx, domain, id, do)) - continue - } - - wg.Add(1) - go func(do *config.Do) { - defer wg.Done() - resultsChan <- r.runDo(ctx, domain, id, do) - }(do) - } - - wg.Wait() - close(resultsChan) - for result := range resultsChan { - results = append(results, result) - } - - return results -} - -func (r *Runner) runDo(ctx context.Context, domain *domain, id string, do *config.Do) Result { - // Check if do action is properly configured - if len(do.Run) > 0 && len(do.Script) > 0 { - return failed(do.PrintableName(id), errDoContainsBothRunAndScript.Error()) - } - if len(do.Run) == 0 && len(do.Script) == 0 && do.Group == nil { - return failed(do.PrintableName(id), errEmptyDo.Error()) - } - - if do.Interactive && !r.DisableTTY && !r.Hook.Follow { - log.StopSpinner() - defer log.StartSpinner() - } - - if len(do.Run) != 0 || len(do.Script) != 0 { - return r.doAction(ctx, domain, id, do) - } - - if do.Group != nil { - return r.doGroup(ctx, id, do.Group) - } - - return failed(do.PrintableName(id), "don't know how to run action") -} - -func (r *Runner) doAction(ctx context.Context, domain *domain, id string, do *config.Do) Result { - name := do.PrintableName(id) - - glob := do.Glob - if len(glob) == 0 { - glob = domain.glob - } - - runAction, err := action.New(name, &action.Params{ - Repo: r.Repo, - Hook: r.Hook, - HookName: r.HookName, - ForceFiles: r.Files, - Force: r.Force, - SourceDirs: r.SourceDirs, - GitArgs: r.GitArgs, - Run: do.Run, - Root: do.Root, - Runner: do.Runner, - Script: do.Script, - Glob: glob, - Files: do.Files, - FileTypes: do.FileTypes, - Tags: do.Tags, - Exclude: do.Exclude, - Only: do.Only, - Skip: do.Skip, - }) - if err != nil { - r.logSkip(name, err.Error()) - - var skipErr action.SkipError - if errors.As(err, &skipErr) { - return skipped(name) - } - - domain.failed.Store(true) - return failed(name, err.Error()) - } - - ok := r.run(ctx, exec.Options{ - Name: name, - Root: filepath.Join(r.Repo.RootPath, do.Root), - Commands: runAction.Execs, - Interactive: do.Interactive && !r.DisableTTY, - UseStdin: do.UseStdin, - Env: do.Env, - }, r.Hook.Follow) - - if !ok { - domain.failed.Store(true) - return failed(name, do.FailText) - } - - if config.HookUsesStagedFiles(r.HookName) && do.StageFixed { - files := runAction.Files - - if len(files) == 0 { - var err error - files, err = r.Repo.StagedFiles() - if err != nil { - log.Warn("Couldn't stage fixed files:", err) - return succeeded(name) - } - - files = filters.Apply(r.Repo.Fs, files, filters.Params{ - Glob: do.Glob, - Root: do.Root, - Exclude: do.Exclude, - FileTypes: do.FileTypes, - }) - } - - if len(do.Root) > 0 { - for i, file := range files { - files[i] = filepath.Join(do.Root, file) - } - } - - r.addStagedFiles(files) - } - - return succeeded(name) -} - -func (r *Runner) doGroup(ctx context.Context, groupId string, group *config.Group) Result { - name := group.PrintableName(groupId) - - if len(group.Do) == 0 { - return failed(name, errEmptyGroup.Error()) - } - - results := make([]Result, 0, len(r.Hook.Do)) - resultsChan := make(chan Result, len(r.Hook.Do)) - domain := &domain{glob: group.Glob} - var wg sync.WaitGroup - - for i, subdo := range group.Do { - id := strconv.Itoa(i) - - if domain.failed.Load() && group.Piped { - r.logSkip(subdo.PrintableName(id), "broken pipe") - continue - } - - if !group.Parallel { - results = append(results, r.runDo(ctx, domain, id, subdo)) - continue - } - - wg.Add(1) - go func(do *config.Do) { - defer wg.Done() - resultsChan <- r.runDo(ctx, domain, id, do) - }(subdo) - } - - wg.Wait() - close(resultsChan) - for result := range resultsChan { - results = append(results, result) - } - - return groupResult(name, results) -} diff --git a/internal/lefthook/runner/run_actions.go b/internal/lefthook/runner/run_actions.go new file mode 100644 index 00000000..c237744b --- /dev/null +++ b/internal/lefthook/runner/run_actions.go @@ -0,0 +1,213 @@ +package runner + +import ( + "context" + "errors" + "path/filepath" + "strconv" + "sync" + "sync/atomic" + + "github.com/evilmartians/lefthook/internal/config" + "github.com/evilmartians/lefthook/internal/lefthook/runner/action" + "github.com/evilmartians/lefthook/internal/lefthook/runner/exec" + "github.com/evilmartians/lefthook/internal/lefthook/runner/filters" + "github.com/evilmartians/lefthook/internal/log" +) + +var ( + errActionContainsBothRunAndScript = errors.New("both `run` and `script` are not permitted") + errEmptyAction = errors.New("no execution instructions") + errEmptyGroup = errors.New("empty groups are not permitted") +) + +type domain struct { + failed atomic.Bool + glob string +} + +func (r *Runner) runActions(ctx context.Context) []Result { + var wg sync.WaitGroup + + results := make([]Result, 0, len(r.Hook.Actions)) + resultsChan := make(chan Result, len(r.Hook.Actions)) + domain := &domain{} + for i, action := range r.Hook.Actions { + id := strconv.Itoa(i) + + if domain.failed.Load() && r.Hook.Piped { + r.logSkip(action.PrintableName(id), "broken pipe") + continue + } + + if !r.Hook.Parallel { + results = append(results, r.runAction(ctx, domain, id, action)) + continue + } + + wg.Add(1) + go func(action *config.Action) { + defer wg.Done() + resultsChan <- r.runAction(ctx, domain, id, action) + }(action) + } + + wg.Wait() + close(resultsChan) + for result := range resultsChan { + results = append(results, result) + } + + return results +} + +func (r *Runner) runAction(ctx context.Context, domain *domain, id string, action *config.Action) Result { + // Check if do action is properly configured + if len(action.Run) > 0 && len(action.Script) > 0 { + return failed(action.PrintableName(id), errActionContainsBothRunAndScript.Error()) + } + if len(action.Run) == 0 && len(action.Script) == 0 && action.Group == nil { + return failed(action.PrintableName(id), errEmptyAction.Error()) + } + + if action.Interactive && !r.DisableTTY && !r.Hook.Follow { + log.StopSpinner() + defer log.StartSpinner() + } + + if len(action.Run) != 0 || len(action.Script) != 0 { + return r.runSingleAction(ctx, domain, id, action) + } + + if action.Group != nil { + return r.runGroup(ctx, id, action.Group) + } + + return failed(action.PrintableName(id), "don't know how to run action") +} + +func (r *Runner) runSingleAction(ctx context.Context, domain *domain, id string, act *config.Action) Result { + name := act.PrintableName(id) + + glob := act.Glob + if len(glob) == 0 { + glob = domain.glob + } + + runAction, err := action.New(name, &action.Params{ + Repo: r.Repo, + Hook: r.Hook, + HookName: r.HookName, + ForceFiles: r.Files, + Force: r.Force, + SourceDirs: r.SourceDirs, + GitArgs: r.GitArgs, + Run: act.Run, + Root: act.Root, + Runner: act.Runner, + Script: act.Script, + Glob: glob, + Files: act.Files, + FileTypes: act.FileTypes, + Tags: act.Tags, + Exclude: act.Exclude, + Only: act.Only, + Skip: act.Skip, + }) + if err != nil { + r.logSkip(name, err.Error()) + + var skipErr action.SkipError + if errors.As(err, &skipErr) { + return skipped(name) + } + + domain.failed.Store(true) + return failed(name, err.Error()) + } + + ok := r.run(ctx, exec.Options{ + Name: name, + Root: filepath.Join(r.Repo.RootPath, act.Root), + Commands: runAction.Execs, + Interactive: act.Interactive && !r.DisableTTY, + UseStdin: act.UseStdin, + Env: act.Env, + }, r.Hook.Follow) + + if !ok { + domain.failed.Store(true) + return failed(name, act.FailText) + } + + if config.HookUsesStagedFiles(r.HookName) && act.StageFixed { + files := runAction.Files + + if len(files) == 0 { + var err error + files, err = r.Repo.StagedFiles() + if err != nil { + log.Warn("Couldn't stage fixed files:", err) + return succeeded(name) + } + + files = filters.Apply(r.Repo.Fs, files, filters.Params{ + Glob: act.Glob, + Root: act.Root, + Exclude: act.Exclude, + FileTypes: act.FileTypes, + }) + } + + if len(act.Root) > 0 { + for i, file := range files { + files[i] = filepath.Join(act.Root, file) + } + } + + r.addStagedFiles(files) + } + + return succeeded(name) +} + +func (r *Runner) runGroup(ctx context.Context, groupId string, group *config.Group) Result { + name := group.PrintableName(groupId) + + if len(group.Actions) == 0 { + return failed(name, errEmptyGroup.Error()) + } + + results := make([]Result, 0, len(group.Actions)) + resultsChan := make(chan Result, len(group.Actions)) + domain := &domain{glob: group.Glob} + var wg sync.WaitGroup + + for i, action := range group.Actions { + id := strconv.Itoa(i) + + if domain.failed.Load() && group.Piped { + r.logSkip(action.PrintableName(id), "broken pipe") + continue + } + + if !group.Parallel { + results = append(results, r.runAction(ctx, domain, id, action)) + continue + } + + wg.Add(1) + go func(action *config.Action) { + defer wg.Done() + resultsChan <- r.runAction(ctx, domain, id, action) + }(action) + } + + wg.Wait() + close(resultsChan) + for result := range resultsChan { + results = append(results, result) + } + + return groupResult(name, results) +} diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index a11864a4..63d4b584 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -90,7 +90,7 @@ func (r *Runner) RunAll(ctx context.Context) ([]Result, error) { defer log.StopSpinner() } - results = append(results, r.do(ctx)...) + results = append(results, r.runActions(ctx)...) scriptDirs := make([]string, 0, len(r.SourceDirs)) for _, sourceDir := range r.SourceDirs {