From dcbf0d4db2ba809eb5a569c15d1239ea964d07a7 Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Fri, 17 Jan 2025 12:07:53 +0300 Subject: [PATCH] feat: add custom plain templates (#930) --- docs/mdbook/SUMMARY.md | 11 ++--- docs/mdbook/configuration/README.md | 11 ++--- docs/mdbook/configuration/templates.md | 43 +++++++++++++++++++ internal/config/config.go | 2 + internal/lefthook/run.go | 1 + .../lefthook/runner/jobs/build_command.go | 26 ++++++----- .../runner/jobs/build_command_test.go | 14 +++--- internal/lefthook/runner/jobs/jobs.go | 1 + internal/lefthook/runner/run_jobs.go | 1 + internal/lefthook/runner/runner.go | 2 + schema.json | 9 +++- testdata/templates.txt | 14 ++++++ 12 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 docs/mdbook/configuration/templates.md create mode 100644 testdata/templates.txt diff --git a/docs/mdbook/SUMMARY.md b/docs/mdbook/SUMMARY.md index 4bdfc44c..3ce7ac35 100644 --- a/docs/mdbook/SUMMARY.md +++ b/docs/mdbook/SUMMARY.md @@ -27,22 +27,23 @@ - [Configuration](./configuration/README.md) - [`assert_lefthook_installed`](./configuration/assert_lefthook_installed.md) - [`colors`](./configuration/colors.md) - - [`no_tty`](./configuration/no_tty.md) - [`extends`](./configuration/extends.md) - [`lefthook`](./configuration/lefthook.md) - [`min_version`](./configuration/min_version.md) + - [`no_tty`](./configuration/no_tty.md) - [`output`](./configuration/output.md) - - [`skip_output`](./configuration/skip_output.md) - - [`source_dir`](./configuration/source_dir.md) - - [`source_dir_local`](./configuration/source_dir_local.md) - [`rc`](./configuration/rc.md) - - [`skip_lfs`](./configuration/skip_lfs.md) - [`remotes`](./configuration/remotes.md) - [`git_url`](./configuration/git_url.md) - [`ref`](./configuration/ref.md) - [`refetch`](./configuration/refetch.md) - [`refetch_frequency`](./configuration/refetch_frequency.md) - [`configs`](./configuration/configs.md) + - [`skip_output`](./configuration/skip_output.md) + - [`source_dir`](./configuration/source_dir.md) + - [`source_dir_local`](./configuration/source_dir_local.md) + - [`skip_lfs`](./configuration/skip_lfs.md) + - [`templates`](./configuration/templates.md) - [{Git hook name}](./configuration/Hook.md) - [`files`](./configuration/files-global.md) - [`parallel`](./configuration/parallel.md) diff --git a/docs/mdbook/configuration/README.md b/docs/mdbook/configuration/README.md index 689ae589..84673571 100644 --- a/docs/mdbook/configuration/README.md +++ b/docs/mdbook/configuration/README.md @@ -19,22 +19,23 @@ Lefthook also merges an extra config with the name `lefthook-local`. All support - [`assert_lefthook_installed`](./assert_lefthook_installed.md) - [`colors`](./colors.md) -- [`no_tty`](./no_tty.md) - [`extends`](./extends.md) - [`lefthook`](./lefthook.md) - [`min_version`](./min_version.md) +- [`no_tty`](./no_tty.md) - [`output`](./output.md) -- [`skip_output`](./skip_output.md) -- [`source_dir`](./source_dir.md) -- [`source_dir_local`](./source_dir_local.md) - [`rc`](./rc.md) -- [`skip_lfs`](./skip_lfs.md) - [`remotes`](./remotes.md) - [`git_url`](./git_url.md) - [`ref`](./ref.md) - [`refetch`](./refetch.md) - [`refetch_frequency`](./refetch_frequency.md) - [`configs`](./configs.md) +- [`skip_output`](./skip_output.md) +- [`source_dir`](./source_dir.md) +- [`source_dir_local`](./source_dir_local.md) +- [`skip_lfs`](./skip_lfs.md) +- [`templates`](./templates.md) - [{Git hook name}](./Hook.md) (e.g. `pre-commit`) - [`files` (global)](./files-global.md) - [`parallel`](./parallel.md) diff --git a/docs/mdbook/configuration/templates.md b/docs/mdbook/configuration/templates.md new file mode 100644 index 00000000..d25f033c --- /dev/null +++ b/docs/mdbook/configuration/templates.md @@ -0,0 +1,43 @@ +## `templates` + +Provide custom replacement for templates in `run` values. + +With `templates` you can specify what can be overridden via `lefthook-local.yml` without a need to overwrite every jobs in your configuration. + +## Example + +### Override with lefthook-local.yml + +```yml +# lefthook.yml + +templates: + dip: # empty + +pre-commit: + jobs: + # Will run: `bundle exec rubocop file1 file2 file3 ...` + - run: {dip} bundle exec rubocop {staged_files} +``` + +```yml +# lefthook.yml + +templates: + dip: dip # Will run: `dip bundle exec rubocop file1 file2 file3 ...` +``` + +### Reduce redundancy + +```yml +# lefthook.yml + +templates: + wrapper: docker-compose run --rm -v $(pwd):/app service + +pre-commit: + jobs: + - run: {wrapper} yarn format + - run: {wrapper} yarn lint + - run: {wrapper} yarn test +``` diff --git a/internal/config/config.go b/internal/config/config.go index db82c235..4593ea30 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,8 @@ type Config struct { Remotes []*Remote `json:"remotes,omitempty" jsonschema:"description=Provide multiple remote configs to use lefthook configurations shared across projects. Lefthook will automatically download and merge configurations into main config." mapstructure:"remotes,omitempty"` + Templates map[string]string `json:"templates,omitempty" jsonschema:"description=Custom templates for replacements in run commands." mapstructure:"templates,omitempty"` + // Deprecated: use Remotes Remote *Remote `json:"remote,omitempty" jsonschema:"description=Deprecated: use remotes" mapstructure:"-"` diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 575f53a4..2c85decd 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -168,6 +168,7 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error { LogSettings: logSettings, DisableTTY: cfg.NoTTY || args.NoTTY, SkipLFS: cfg.SkipLFS || args.SkipLFS, + Templates: cfg.Templates, Files: args.Files, Force: args.Force, RunOnlyCommands: args.RunOnlyCommands, diff --git a/internal/lefthook/runner/jobs/build_command.go b/internal/lefthook/runner/jobs/build_command.go index ab4ff49b..31144b67 100644 --- a/internal/lefthook/runner/jobs/build_command.go +++ b/internal/lefthook/runner/jobs/build_command.go @@ -16,8 +16,8 @@ import ( var surroundingQuotesRegexp = regexp.MustCompile(`^'(.*)'$`) -// template is stats for template replacements in a command string. -type template struct { +// fileTemplate contains for template replacements in a command string. +type filesTemplate struct { files []string cnt int } @@ -67,7 +67,7 @@ func buildCommand(params *Params) (*Job, error) { config.SubFiles: cmdFiles, } - templates := make(map[string]*template) + filesTemplates := make(map[string]*filesTemplate) filterParams := filters.Params{ Glob: params.Glob, @@ -81,8 +81,8 @@ func buildCommand(params *Params) (*Job, error) { continue } - templ := &template{cnt: cnt} - templates[filesType] = templ + templ := &filesTemplate{cnt: cnt} + filesTemplates[filesType] = templ files, err := fn() if err != nil { @@ -100,7 +100,7 @@ func buildCommand(params *Params) (*Job, error) { // Checking substitutions and skipping execution if it is empty. // // Special case for `files` option: return if the result of files command is empty. - if !params.Force && len(filesCmd) > 0 && templates[config.SubFiles] == nil { + if !params.Force && len(filesCmd) > 0 && filesTemplates[config.SubFiles] == nil { files, err := filesFns[config.SubFiles]() if err != nil { return nil, fmt.Errorf("error calling replace command for %s: %w", config.SubFiles, err) @@ -116,15 +116,19 @@ func buildCommand(params *Params) (*Job, error) { runString := params.Run runString = replacePositionalArguments(runString, params.GitArgs) + for keyword, replacement := range params.Templates { + runString = strings.ReplaceAll(runString, "{"+keyword+"}", replacement) + } + maxlen := system.MaxCmdLen() - result := replaceInChunks(runString, templates, maxlen) + result := replaceInChunks(runString, filesTemplates, maxlen) if params.Force || len(result.Files) != 0 { return result, nil } if config.HookUsesStagedFiles(params.HookName) { - ok, err := canSkipJob(params, filterParams, templates[config.SubStagedFiles], params.Repo.StagedFilesWithDeleted) + ok, err := canSkipJob(params, filterParams, filesTemplates[config.SubStagedFiles], params.Repo.StagedFilesWithDeleted) if err != nil { return nil, err } @@ -134,7 +138,7 @@ func buildCommand(params *Params) (*Job, error) { } if config.HookUsesPushFiles(params.HookName) { - ok, err := canSkipJob(params, filterParams, templates[config.SubPushFiles], params.Repo.PushFiles) + ok, err := canSkipJob(params, filterParams, filesTemplates[config.SubPushFiles], params.Repo.PushFiles) if err != nil { return nil, err } @@ -146,7 +150,7 @@ func buildCommand(params *Params) (*Job, error) { return result, nil } -func canSkipJob(params *Params, filterParams filters.Params, template *template, filesFn func() ([]string, error)) (bool, error) { +func canSkipJob(params *Params, filterParams filters.Params, template *filesTemplate, filesFn func() ([]string, error)) (bool, error) { if template != nil { return len(template.files) == 0, nil } @@ -184,7 +188,7 @@ func escapeFiles(files []string) []string { return filesEsc } -func replaceInChunks(str string, templates map[string]*template, maxlen int) *Job { +func replaceInChunks(str string, templates map[string]*filesTemplate, maxlen int) *Job { if len(templates) == 0 { return &Job{ Execs: []string{str}, diff --git a/internal/lefthook/runner/jobs/build_command_test.go b/internal/lefthook/runner/jobs/build_command_test.go index 7e654fd2..6ff71bb6 100644 --- a/internal/lefthook/runner/jobs/build_command_test.go +++ b/internal/lefthook/runner/jobs/build_command_test.go @@ -68,13 +68,13 @@ func Test_getNChars(t *testing.T) { func Test_replaceInChunks(t *testing.T) { for i, tt := range [...]struct { str string - templates map[string]*template + templates map[string]*filesTemplate maxlen int job *Job }{ { str: "echo {staged_files}", - templates: map[string]*template{ + templates: map[string]*filesTemplate{ "{staged_files}": { files: []string{"file1", "file2", "file3"}, cnt: 1, @@ -88,7 +88,7 @@ func Test_replaceInChunks(t *testing.T) { }, { str: "echo {staged_files}", - templates: map[string]*template{ + templates: map[string]*filesTemplate{ "{staged_files}": { files: []string{"file1", "file2", "file3"}, cnt: 1, @@ -106,7 +106,7 @@ func Test_replaceInChunks(t *testing.T) { }, { str: "echo {files} && git add {files}", - templates: map[string]*template{ + templates: map[string]*filesTemplate{ "{files}": { files: []string{"file1", "file2", "file3"}, cnt: 2, @@ -123,7 +123,7 @@ func Test_replaceInChunks(t *testing.T) { }, { str: "echo {files} && git add {files}", - templates: map[string]*template{ + templates: map[string]*filesTemplate{ "{files}": { files: []string{"file1", "file2", "file3"}, cnt: 2, @@ -139,7 +139,7 @@ func Test_replaceInChunks(t *testing.T) { }, { str: "echo {push_files} && git add {files}", - templates: map[string]*template{ + templates: map[string]*filesTemplate{ "{push_files}": { files: []string{"push-file"}, cnt: 1, @@ -160,7 +160,7 @@ func Test_replaceInChunks(t *testing.T) { }, { str: "echo {push_files} && git add {files}", - templates: map[string]*template{ + templates: map[string]*filesTemplate{ "{push_files}": { files: []string{"push1", "push2", "push3"}, cnt: 1, diff --git a/internal/lefthook/runner/jobs/jobs.go b/internal/lefthook/runner/jobs/jobs.go index 44363443..a53198eb 100644 --- a/internal/lefthook/runner/jobs/jobs.go +++ b/internal/lefthook/runner/jobs/jobs.go @@ -23,6 +23,7 @@ type Params struct { Files string FileTypes []string Tags []string + Templates map[string]string Exclude interface{} Only interface{} Skip interface{} diff --git a/internal/lefthook/runner/run_jobs.go b/internal/lefthook/runner/run_jobs.go index 0a31f799..c59720a0 100644 --- a/internal/lefthook/runner/run_jobs.go +++ b/internal/lefthook/runner/run_jobs.go @@ -147,6 +147,7 @@ func (r *Runner) runSingleJob(ctx context.Context, domain *domain, id string, jo Exclude: exclude, Only: job.Only, Skip: job.Skip, + Templates: r.Templates, }) if err != nil { r.logSkip(name, err.Error()) diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index d545fade..ef7f2a3a 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -43,6 +43,7 @@ type Options struct { RunOnlyCommands []string RunOnlyJobs []string SourceDirs []string + Templates map[string]string } // Runner responds for actual execution and handling the results. @@ -467,6 +468,7 @@ func (r *Runner) runCommand(ctx context.Context, name string, command *config.Co Exclude: command.Exclude, Only: command.Only, Skip: command.Skip, + Templates: r.Templates, }) if err != nil { r.logSkip(name, err.Error()) diff --git a/schema.json b/schema.json index 6fb3f7d5..db95a9c0 100644 --- a/schema.json +++ b/schema.json @@ -392,7 +392,7 @@ "type": "object" } }, - "$comment": "Last updated on 2025.01.14.", + "$comment": "Last updated on 2025.01.16.", "properties": { "min_version": { "type": "string", @@ -473,6 +473,13 @@ "type": "array", "description": "Provide multiple remote configs to use lefthook configurations shared across projects. Lefthook will automatically download and merge configurations into main config." }, + "templates": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Custom templates for replacements in run commands." + }, "remote": { "$ref": "#/$defs/Remote", "description": "Deprecated: use remotes" diff --git a/testdata/templates.txt b/testdata/templates.txt new file mode 100644 index 00000000..bd4d98ee --- /dev/null +++ b/testdata/templates.txt @@ -0,0 +1,14 @@ +exec git init +exec lefthook run test +stdout '^\s*hello\s*$' + +-- lefthook.yml -- +templates: + message: hello + +output: + - execution_out + +test: + jobs: + - run: echo {message}