diff --git a/docs/configuration.md b/docs/configuration.md index 5c8bb04e..0d1f3db9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -163,8 +163,46 @@ If you want to specify a minimum version for lefthook binary (e.g. if you need s min_version: 1.1.3 ``` +### `output` + +You can manage verbosity using the `output` config. You can specify what to print in your output by setting these values, which you need to have + +Possible values are `meta,summary,success,failure,execution,execution_out,execution_info,skips`. +By default, all output values are enabled + +You can also disable all output with setting `output: false`. In this case only errors will be printed. + +This config quiets all outputs except for errors. + +`output` is enabled if there is no `skip_output` and `LEFTHOOK_QUIET`. + +**Example** + +```yml +# lefthook.yml + +output: + - meta # Print lefthook version + - summary # Print summary block (successful and failed steps) + - empty_summary # Print summary heading when there are no steps to run + - success # Print successful steps + - failure # Print failed steps printing + - execution # Print any execution logs (but prints if the execution failed) + - execution_out # Print execution output (but still prints failed commands output) + - execution_info # Print `EXECUTE > ...` logging + - skips # Print "skip" (i.e. no files matched) +``` + +You can also *extend* this list with an environment variable `LEFTHOOK_OUTPUT`: + +```bash +LEFTHOOK_OUTPUT="meta,success,summary" lefthook run pre-commit +``` + ### `skip_output` +> **Deprecated:** This feature is deprecated and might be removed in future versions. Please, use `[output]` instead for managing verbosity. + You can manage the verbosity using the `skip_output` config. You can set whether lefthook should print some parts of its output. Possible values are `meta,summary,success,failure,execution,execution_out,execution_info,skips`. diff --git a/internal/config/config.go b/internal/config/config.go index 19c72e5f..2af54794 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,7 @@ type Config struct { SourceDirLocal string `mapstructure:"source_dir_local"` Rc string `mapstructure:"rc,omitempty"` SkipOutput interface{} `mapstructure:"skip_output,omitempty"` + Output interface{} `mapstructure:"output,omitempty"` Extends []string `mapstructure:"extends,omitempty"` NoTTY bool `mapstructure:"no_tty,omitempty"` AssertLefthookInstalled bool `mapstructure:"assert_lefthook_installed,omitempty"` diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 6e0db14c..24e686f5 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -18,8 +18,9 @@ import ( ) const ( - envEnabled = "LEFTHOOK" // "0", "false" - envSkipOutput = "LEFTHOOK_QUIET" // "meta,success,failure,summary,skips,execution,execution_out,execution_info" + envEnabled = "LEFTHOOK" // "0", "false" + envSkipOutput = "LEFTHOOK_QUIET" // "meta,success,failure,summary,skips,execution,execution_out,execution_info" + envOutput = "LEFTHOOK_OUTPUT" // "meta,success,failure,summary,skips,execution,execution_out,execution_info" ) type RunArgs struct { @@ -75,12 +76,22 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error { log.SetLevel(log.WarnLevel) } - tags := os.Getenv(envSkipOutput) + outputLogTags := os.Getenv(envOutput) + outputSkipTags := os.Getenv(envSkipOutput) - var logSettings log.SkipSettings - (&logSettings).ApplySettings(tags, cfg.SkipOutput) + var logSettings log.Settings - if !logSettings.SkipMeta() { + if outputSkipTags == "" && cfg.SkipOutput == nil { + logSettings = log.NewSettings() + logSettings.ApplySettings(outputLogTags, cfg.Output) + } else { + log.Warn("skip_output is deprecated, please use output option") + + logSettings = log.NewSkipSettings() //nolint:staticcheck //SA1019: for temporary backward compatibility + logSettings.ApplySettings(outputSkipTags, cfg.SkipOutput) + } + + if logSettings.LogMeta() { log.Box( log.Cyan("🥊 lefthook ")+log.Gray(fmt.Sprintf("v%s", version.Version(false))), log.Gray("hook: ")+log.Bold(hookName), @@ -176,7 +187,7 @@ Run 'lefthook install' manually.`, return errors.New("Interrupted") } - if !logSettings.SkipSummary() { + if logSettings.LogSummary() { printSummary(time.Since(startTime), results, logSettings) } @@ -192,16 +203,16 @@ Run 'lefthook install' manually.`, func printSummary( duration time.Duration, results []run.Result, - logSettings log.SkipSettings, + logSettings log.Settings, ) { summaryPrint := log.Separate - if logSettings.SkipExecution() || (logSettings.SkipExecutionInfo() && logSettings.SkipExecutionOutput()) { + if !logSettings.LogExecution() || !(logSettings.LogExecutionInfo() && logSettings.LogExecutionOutput()) { summaryPrint = func(s string) { log.Info(s) } } if len(results) == 0 { - if !logSettings.SkipEmptySummary() { + if logSettings.LogEmptySummary() { summaryPrint( fmt.Sprintf( "%s %s %s", @@ -218,7 +229,7 @@ func printSummary( log.Cyan("summary: ") + log.Gray(fmt.Sprintf("(done in %.2f seconds)", duration.Seconds())), ) - if !logSettings.SkipSuccess() { + if logSettings.LogSuccess() { for _, result := range results { if result.Status != run.StatusOk { continue @@ -228,7 +239,7 @@ func printSummary( } } - if !logSettings.SkipFailure() { + if logSettings.LogFailure() { for _, result := range results { if result.Status != run.StatusErr { continue diff --git a/internal/lefthook/run/runner.go b/internal/lefthook/run/runner.go index 17fada29..8bfbdcb0 100644 --- a/internal/lefthook/run/runner.go +++ b/internal/lefthook/run/runner.go @@ -43,7 +43,7 @@ type Options struct { HookName string GitArgs []string ResultChan chan Result - SkipSettings log.SkipSettings + SkipSettings log.Settings DisableTTY bool Force bool Files []string @@ -426,14 +426,14 @@ func (r *Runner) run(ctx context.Context, opts exec.Options, follow bool) bool { log.SetName(opts.Name) defer log.UnsetName(opts.Name) - if (follow || opts.Interactive) && !r.SkipSettings.SkipExecution() { + if (follow || opts.Interactive) && r.SkipSettings.LogExecution() { r.logExecute(opts.Name, nil, nil) var out io.Writer - if r.SkipSettings.SkipExecutionOutput() { - out = io.Discard - } else { + if r.SkipSettings.LogExecutionOutput() { out = os.Stdout + } else { + out = io.Discard } err := r.executor.Execute(ctx, opts, out) @@ -478,7 +478,7 @@ func intersect(a, b []string) bool { } func (r *Runner) logSkip(name, reason string) { - if r.SkipSettings.SkipSkips() { + if !r.SkipSettings.LogSkips() { return } @@ -493,14 +493,14 @@ func (r *Runner) logSkip(name, reason string) { } func (r *Runner) logExecute(name string, err error, out io.Reader) { - if err == nil && r.SkipSettings.SkipExecution() { + if err == nil && !r.SkipSettings.LogExecution() { return } var execLog string var color lipgloss.TerminalColor switch { - case r.SkipSettings.SkipExecutionInfo(): + case !r.SkipSettings.LogExecutionInfo(): execLog = "" case err != nil: execLog = log.Red(fmt.Sprintf("%s ❯ ", name)) @@ -518,7 +518,7 @@ func (r *Runner) logExecute(name string, err error, out io.Reader) { log.Info() } - if err == nil && r.SkipSettings.SkipExecutionOutput() { + if err == nil && !r.SkipSettings.LogExecutionOutput() { return } diff --git a/internal/lefthook/run/runner_test.go b/internal/lefthook/run/runner_test.go index 243800c4..dc1e7841 100644 --- a/internal/lefthook/run/runner_test.go +++ b/internal/lefthook/run/runner_test.go @@ -16,6 +16,7 @@ import ( "github.com/evilmartians/lefthook/internal/config" "github.com/evilmartians/lefthook/internal/git" "github.com/evilmartians/lefthook/internal/lefthook/run/exec" + "github.com/evilmartians/lefthook/internal/log" ) type TestExecutor struct{} @@ -749,12 +750,13 @@ func TestRunAll(t *testing.T) { executor := TestExecutor{} runner := &Runner{ Options: Options{ - Repo: repo, - Hook: tt.hook, - HookName: tt.hookName, - GitArgs: tt.args, - ResultChan: resultChan, - Force: tt.force, + Repo: repo, + Hook: tt.hook, + HookName: tt.hookName, + SkipSettings: log.NewSettings(), + GitArgs: tt.args, + ResultChan: resultChan, + Force: tt.force, }, executor: executor, } diff --git a/internal/log/settings.go b/internal/log/settings.go new file mode 100644 index 00000000..f9c643c3 --- /dev/null +++ b/internal/log/settings.go @@ -0,0 +1,140 @@ +package log + +import ( + "strings" +) + +const ( + meta = 1 << iota + success + failure + summary + skips + execution + executionOutput + executionInfo + emptySummary + enableAll = ^0 // Set all bits as 1 +) + +type Settings interface { + ApplySettings(tags string, skipOutput interface{}) + LogSuccess() bool + LogFailure() bool + LogSummary() bool + LogMeta() bool + LogExecution() bool + LogExecutionOutput() bool + LogExecutionInfo() bool + LogSkips() bool + LogEmptySummary() bool +} + +type OutputSettings int16 + +func NewSettings() Settings { + var s OutputSettings + return &s +} + +func (s *OutputSettings) ApplySettings(tags string, output interface{}) { + if tags == "" && (output == nil || output == "") { + s.enableAll(true) + return + } + + if val, ok := output.(bool); ok { + s.enableAll(val) + return + } + + if options, ok := output.([]interface{}); ok { + if len(options) == 0 { + s.enableAll(true) + return + } + for _, option := range options { + if optStr, ok := option.(string); ok { + s.applySetting(optStr) + } + } + } + + if tags != "" { + for _, tag := range strings.Split(tags, ",") { + s.applySetting(tag) + } + } +} + +func (s *OutputSettings) applySetting(setting string) { + switch setting { + case "meta": + *s |= meta + case "success": + *s |= success + case "failure": + *s |= failure + case "summary": + *s |= summary + case "skips": + *s |= skips + case "execution": + *s |= execution + case "execution_out": + *s |= executionOutput + case "execution_info": + *s |= executionInfo + case "empty_summary": + *s |= emptySummary + } +} + +func (s *OutputSettings) enableAll(val bool) { + if val { + *s = enableAll // Enable all params + } else { + *s |= failure // Disable all params + } +} + +// Checks the state of params. +func (s OutputSettings) isEnable(option int16) bool { + return int16(s)&option != 0 +} + +func (s OutputSettings) LogSuccess() bool { + return s.isEnable(success) +} + +func (s OutputSettings) LogFailure() bool { + return s.isEnable(failure) +} + +func (s OutputSettings) LogSummary() bool { + return s.isEnable(summary) +} + +func (s OutputSettings) LogMeta() bool { + return s.isEnable(meta) +} + +func (s OutputSettings) LogExecution() bool { + return s.isEnable(execution) +} + +func (s OutputSettings) LogExecutionOutput() bool { + return s.isEnable(executionOutput) +} + +func (s OutputSettings) LogExecutionInfo() bool { + return s.isEnable(executionInfo) +} + +func (s OutputSettings) LogSkips() bool { + return s.isEnable(skips) +} + +func (s OutputSettings) LogEmptySummary() bool { + return s.isEnable(emptySummary) +} diff --git a/internal/log/settings_test.go b/internal/log/settings_test.go new file mode 100644 index 00000000..8521e29e --- /dev/null +++ b/internal/log/settings_test.go @@ -0,0 +1,138 @@ +package log + +import ( + "fmt" + "testing" +) + +func TestSetting(t *testing.T) { + for i, tt := range [...]struct { + tags string + settings interface{} + results map[string]bool + }{ + { + tags: "", + settings: []interface{}{}, + results: map[string]bool{ + "meta": true, + "summary": true, + "success": true, + "failure": true, + "skips": true, + "execution": true, + "execution_out": true, + "execution_info": true, + "empty_summary": true, + }, + }, + { + tags: "", + settings: false, + results: map[string]bool{ + "failure": true, + }, + }, + { + tags: "", + settings: []interface{}{"failure", "execution"}, + results: map[string]bool{ + "failure": true, + "execution": true, + }, + }, + { + tags: "", + settings: []interface{}{ + "meta", + "summary", + "success", + "failure", + "skips", + "execution", + "execution_out", + "execution_info", + "empty_summary", + }, + results: map[string]bool{ + "meta": true, + "summary": true, + "success": true, + "failure": true, + "skips": true, + "execution": true, + "execution_out": true, + "execution_info": true, + "empty_summary": true, + }, + }, + { + tags: "", + settings: true, + results: map[string]bool{ + "meta": true, + "summary": true, + "success": true, + "failure": true, + "skips": true, + "execution": true, + "execution_out": true, + "execution_info": true, + "empty_summary": true, + }, + }, + { + tags: "meta,summary,success,skips,empty_summary", + settings: nil, + results: map[string]bool{ + "meta": true, + "summary": true, + "success": true, + "skips": true, + "empty_summary": true, + }, + }, + } { //nolint:dupl // In next versions the `skip_settings_test` will be removed + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var settings OutputSettings + + (&settings).ApplySettings(tt.tags, tt.settings) + + if settings.LogMeta() != tt.results["meta"] { + t.Errorf("expected LogMeta to be %v", tt.results["meta"]) + } + + if settings.LogSuccess() != tt.results["success"] { + t.Errorf("expected LogSuccess to be %v", tt.results["success"]) + } + + if settings.LogFailure() != tt.results["failure"] { + t.Errorf("expected LogFailure to be %v", tt.results["failure"]) + } + + if settings.LogSummary() != tt.results["summary"] { + t.Errorf("expected LogSummary to be %v", tt.results["summary"]) + } + + if settings.LogExecution() != tt.results["execution"] { + t.Errorf("expected LogExecution to be %v", tt.results["execution"]) + } + + if settings.LogExecutionOutput() != tt.results["execution_out"] { + t.Errorf("expected LogExecutionOutput to be %v", tt.results["execution_out"]) + } + + if settings.LogExecutionInfo() != tt.results["execution_info"] { + t.Errorf("expected LogExecutionInfo to be %v", tt.results["execution_info"]) + } + + if settings.LogEmptySummary() != tt.results["empty_summary"] { + t.Errorf("expected LogEmptySummary to be %v", tt.results["empty_summary"]) + } + + if settings.LogSkips() != tt.results["skips"] { + t.Errorf("expected LogSkips to be %v", tt.results["skip"]) + } + }) + } +} diff --git a/internal/log/skip_settings.go b/internal/log/skip_settings.go index 2e9cb551..cc8c825e 100644 --- a/internal/log/skip_settings.go +++ b/internal/log/skip_settings.go @@ -17,12 +17,19 @@ const ( skipAll = (1 << iota) - 1 ) +// Deprecated: Use Settings instead. type SkipSettings int16 +// Deprecated: Use NewSettings instead. +func NewSkipSettings() Settings { + var s SkipSettings + return &s +} + func (s *SkipSettings) ApplySettings(tags string, skipOutput interface{}) { switch typedSkipOutput := skipOutput.(type) { case bool: - s.SkipAll(typedSkipOutput) + s.skipAll(typedSkipOutput) case []interface{}: for _, skipOption := range typedSkipOutput { s.applySetting(skipOption.(string)) @@ -59,7 +66,7 @@ func (s *SkipSettings) applySetting(setting string) { } } -func (s *SkipSettings) SkipAll(val bool) { +func (s *SkipSettings) skipAll(val bool) { if val { *s = skipAll &^ skipFailure } else { @@ -67,40 +74,40 @@ func (s *SkipSettings) SkipAll(val bool) { } } -func (s SkipSettings) SkipSuccess() bool { - return s.doSkip(skipSuccess) +func (s SkipSettings) LogSuccess() bool { + return !s.doSkip(skipSuccess) } -func (s SkipSettings) SkipFailure() bool { - return s.doSkip(skipFailure) +func (s SkipSettings) LogFailure() bool { + return !s.doSkip(skipFailure) } -func (s SkipSettings) SkipSummary() bool { - return s.doSkip(skipSummary) +func (s SkipSettings) LogSummary() bool { + return !s.doSkip(skipSummary) } -func (s SkipSettings) SkipMeta() bool { - return s.doSkip(skipMeta) +func (s SkipSettings) LogMeta() bool { + return !s.doSkip(skipMeta) } -func (s SkipSettings) SkipExecution() bool { - return s.doSkip(skipExecution) +func (s SkipSettings) LogExecution() bool { + return !s.doSkip(skipExecution) } -func (s SkipSettings) SkipExecutionOutput() bool { - return s.doSkip(skipExecutionOutput) +func (s SkipSettings) LogExecutionOutput() bool { + return !s.doSkip(skipExecutionOutput) } -func (s SkipSettings) SkipExecutionInfo() bool { - return s.doSkip(skipExecutionInfo) +func (s SkipSettings) LogExecutionInfo() bool { + return !s.doSkip(skipExecutionInfo) } -func (s SkipSettings) SkipSkips() bool { - return s.doSkip(skipSkips) +func (s SkipSettings) LogSkips() bool { + return !s.doSkip(skipSkips) } -func (s SkipSettings) SkipEmptySummary() bool { - return s.doSkip(skipEmptySummary) +func (s SkipSettings) LogEmptySummary() bool { + return !s.doSkip(skipEmptySummary) } func (s SkipSettings) doSkip(option int16) bool { diff --git a/internal/log/skip_settings_test.go b/internal/log/skip_settings_test.go index f2f85e9f..c546b764 100644 --- a/internal/log/skip_settings_test.go +++ b/internal/log/skip_settings_test.go @@ -14,19 +14,46 @@ func TestSkipSetting(t *testing.T) { { tags: "", settings: []interface{}{}, - results: map[string]bool{}, + results: map[string]bool{ + "meta": true, + "summary": true, + "success": true, + "failure": true, + "skips": true, + "execution": true, + "execution_out": true, + "execution_info": true, + "empty_summary": true, + }, }, { tags: "", settings: false, - results: map[string]bool{}, + results: map[string]bool{ + "meta": true, + "summary": true, + "success": true, + "failure": true, + "skips": true, + "execution": true, + "execution_out": true, + "execution_info": true, + "empty_summary": true, + }, }, { tags: "", settings: []interface{}{"failure", "execution"}, results: map[string]bool{ - "failure": true, - "execution": true, + "meta": true, + "summary": true, + "success": true, + "failure": false, + "skips": true, + "execution": false, + "execution_out": true, + "execution_info": true, + "empty_summary": true, }, }, { @@ -42,88 +69,65 @@ func TestSkipSetting(t *testing.T) { "execution_info", "empty_summary", }, - results: map[string]bool{ - "meta": true, - "summary": true, - "success": true, - "failure": true, - "skips": true, - "execution": true, - "execution_out": true, - "execution_info": true, - "empty_summary": true, - }, + results: map[string]bool{}, }, { tags: "", settings: true, results: map[string]bool{ - "meta": true, - "summary": true, - "success": true, - "failure": false, - "skips": true, - "execution": true, - "execution_out": true, - "execution_info": true, - "empty_summary": true, + "failure": true, }, }, { tags: "meta,summary,success,skips,empty_summary", settings: nil, results: map[string]bool{ - "meta": true, - "summary": true, - "success": true, - "failure": false, - "skips": true, - "execution": false, - "execution_out": false, - "execution_info": false, - "empty_summary": true, + "failure": true, + "execution": true, + "execution_out": true, + "execution_info": true, }, }, - } { + } { //nolint:dupl // In next versions the `skip_settings_test` will be removed t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { var settings SkipSettings (&settings).ApplySettings(tt.tags, tt.settings) - if settings.SkipMeta() != tt.results["meta"] { - t.Errorf("expected SkipMeta to be %v", tt.results["meta"]) + if settings.LogMeta() != tt.results["meta"] { + t.Errorf("expected LogMeta to be %v", tt.results["meta"]) } - if settings.SkipSuccess() != tt.results["success"] { - t.Errorf("expected SkipSuccess to be %v", tt.results["success"]) + if settings.LogSuccess() != tt.results["success"] { + t.Errorf("expected LogSuccess to be %v", tt.results["success"]) } - if settings.SkipFailure() != tt.results["failure"] { - t.Errorf("expected SkipFailure to be %v", tt.results["failure"]) + if settings.LogFailure() != tt.results["failure"] { + t.Errorf("expected LogFailure to be %v", tt.results["failure"]) } - if settings.SkipSummary() != tt.results["summary"] { - t.Errorf("expected SkipSummary to be %v", tt.results["summary"]) + if settings.LogSummary() != tt.results["summary"] { + t.Errorf("expected LogSummary to be %v", tt.results["summary"]) } - if settings.SkipExecution() != tt.results["execution"] { - t.Errorf("expected SkipExecution to be %v", tt.results["execution"]) + if settings.LogExecution() != tt.results["execution"] { + t.Errorf("expected LogExecution to be %v", tt.results["execution"]) } - if settings.SkipExecutionOutput() != tt.results["execution_out"] { - t.Errorf("expected SkipExecutionOutput to be %v", tt.results["execution_out"]) + if settings.LogExecutionOutput() != tt.results["execution_out"] { + t.Errorf("expected LogExecutionOutput to be %v", tt.results["execution_out"]) } - if settings.SkipExecutionInfo() != tt.results["execution_info"] { - t.Errorf("expected SkipExecutionInfo to be %v", tt.results["execution_info"]) + if settings.LogExecutionInfo() != tt.results["execution_info"] { + t.Errorf("expected LogExecutionInfo to be %v", tt.results["execution_info"]) } - if settings.SkipEmptySummary() != tt.results["empty_summary"] { - t.Errorf("expected SkipEmptySummary to be %v", tt.results["empty_summary"]) + if settings.LogEmptySummary() != tt.results["empty_summary"] { + t.Errorf("expected LogEmptySummary to be %v", tt.results["empty_summary"]) } - if settings.SkipSkips() != tt.results["skips"] { - t.Errorf("expected SkipSkips to be %v", tt.results["skip"]) + if settings.LogSkips() != tt.results["skips"] { + t.Errorf("expected LogSkips to be %v", tt.results["skip"]) } }) }