From 3e08c0960c7313cddf50985b9f50e1630d6acba8 Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Fri, 30 Aug 2024 12:36:58 +0300 Subject: [PATCH 1/3] fix: add better colors control --- cmd/root.go | 11 ++- internal/lefthook/lefthook.go | 7 +- internal/lefthook/run.go | 15 +--- internal/lefthook/runner/exec/execute_unix.go | 6 ++ .../lefthook/runner/exec/execute_windows.go | 7 ++ internal/log/log.go | 88 ++++++++++++++++--- 6 files changed, 109 insertions(+), 25 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 2190990a..7fcb1953 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,6 +29,11 @@ func newRootCmd() *cobra.Command { rootCmd.PersistentFlags().BoolVarP( &options.Verbose, "verbose", "v", false, "verbose output", ) + + rootCmd.PersistentFlags().StringVar( + &options.Colors, "colors", "auto", "'auto', 'on', or 'off'", + ) + rootCmd.PersistentFlags().BoolVar( &options.NoColors, "no-colors", false, "disable colored output", ) @@ -42,7 +47,11 @@ func newRootCmd() *cobra.Command { &options.Aggressive, "aggressive", "a", false, "use --force flag instead", ) - err := rootCmd.Flags().MarkDeprecated("aggressive", "use command-specific --force option") + err := rootCmd.PersistentFlags().MarkDeprecated("no-colors", "use --colors") + if err != nil { + log.Warn("Unexpected error:", err) + } + err = rootCmd.Flags().MarkDeprecated("aggressive", "use command-specific --force option") if err != nil { log.Warn("Unexpected error:", err) } diff --git a/internal/lefthook/lefthook.go b/internal/lefthook/lefthook.go index b2c8e00c..c0f07e2d 100644 --- a/internal/lefthook/lefthook.go +++ b/internal/lefthook/lefthook.go @@ -26,6 +26,7 @@ var lefthookContentRegexp = regexp.MustCompile("LEFTHOOK") type Options struct { Fs afero.Fs Verbose, NoColors bool + Colors string // DEPRECATED. Will be removed in 1.3.0. Force, Aggressive bool @@ -49,7 +50,11 @@ func initialize(opts *Options) (*Lefthook, error) { log.SetLevel(log.DebugLevel) } - log.SetColors(!opts.NoColors) + // DEPRECATED: Will be removed with a --no-colors option + if opts.NoColors && opts.Colors == "auto" { + opts.Colors = "off" + } + log.SetColors(opts.Colors) repo, err := git.NewRepository(opts.Fs, git.NewExecutor(system.Cmd)) if err != nil { diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 1906574d..0c38c5c7 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -14,7 +14,6 @@ import ( "github.com/evilmartians/lefthook/internal/config" "github.com/evilmartians/lefthook/internal/lefthook/runner" "github.com/evilmartians/lefthook/internal/log" - "github.com/evilmartians/lefthook/internal/version" ) const ( @@ -91,10 +90,7 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error { // } if logSettings.LogMeta() { - log.Box( - log.Cyan("🥊 lefthook ")+log.Gray(fmt.Sprintf("v%s", version.Version(false))), - log.Gray("hook: ")+log.Bold(hookName), - ) + log.LogMeta(hookName) } if !args.NoAutoInstall { @@ -231,7 +227,7 @@ func printSummary( continue } - log.Infof("✔️ %s\n", log.Green(result.Name)) + log.Success(result.Name) } } @@ -241,12 +237,7 @@ func printSummary( continue } - failText := result.Text() - if len(failText) != 0 { - failText = fmt.Sprintf(": %s", failText) - } - - log.Infof("🥊 %s%s\n", log.Red(result.Name), log.Red(failText)) + log.Failure(result.Name, result.Text()) } } } diff --git a/internal/lefthook/runner/exec/execute_unix.go b/internal/lefthook/runner/exec/execute_unix.go index ccaa9a9b..45efaab0 100644 --- a/internal/lefthook/runner/exec/execute_unix.go +++ b/internal/lefthook/runner/exec/execute_unix.go @@ -47,6 +47,12 @@ func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader fmt.Sprintf("%s=%s", strings.ToUpper(name), os.ExpandEnv(value)), ) } + switch log.Colors() { + case log.ColorOn: + envs = append(envs, "CLICOLOR_FORCE=true") + case log.ColorOff: + envs = append(envs, "NO_COLOR=true") + } args := &executeArgs{ in: in, diff --git a/internal/lefthook/runner/exec/execute_windows.go b/internal/lefthook/runner/exec/execute_windows.go index 47d6bc1c..47d72757 100644 --- a/internal/lefthook/runner/exec/execute_windows.go +++ b/internal/lefthook/runner/exec/execute_windows.go @@ -11,6 +11,7 @@ import ( "syscall" "github.com/evilmartians/lefthook/internal/log" + "github.com/mattn/go-isatty" "github.com/mattn/go-tty" ) @@ -42,6 +43,12 @@ func (e CommandExecutor) Execute(ctx context.Context, opts Options, in io.Reader fmt.Sprintf("%s=%s", strings.ToUpper(name), os.ExpandEnv(value)), ) } + switch log.Colors() { + case log.ColorOn: + envs = append(envs, "CLICOLOR_FORCE=true") + case log.ColorOff: + envs = append(envs, "NO_COLOR=true") + } args := &executeArgs{ in: in, diff --git a/internal/log/log.go b/internal/log/log.go index 4461226d..b0843cc4 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -11,6 +11,8 @@ import ( "github.com/briandowns/spinner" "github.com/charmbracelet/lipgloss" + + "github.com/evilmartians/lefthook/internal/version" ) var ( @@ -38,6 +40,10 @@ const ( spinnerCharSet = 14 spinnerRefreshRate = 100 * time.Millisecond spinnerText = " waiting" + + ColorAuto = iota + ColorOn + ColorOff ) type StyleLogger struct { @@ -48,7 +54,7 @@ type Logger struct { level Level out io.Writer mu sync.Mutex - colors bool + colors int names []string spinner *spinner.Spinner } @@ -57,7 +63,7 @@ func New() *Logger { return &Logger{ level: InfoLevel, out: os.Stdout, - colors: true, + colors: ColorAuto, spinner: spinner.New( spinner.CharSets[spinnerCharSet], spinnerRefreshRate, @@ -66,6 +72,14 @@ func New() *Logger { } } +func Colors() int { + return std.colors +} + +func Colorized() bool { + return std.colors == ColorAuto || std.colors == ColorOn +} + func StartSpinner() { std.spinner.Start() } @@ -159,29 +173,49 @@ func SetLevel(level Level) { } func SetColors(colors interface{}) { + if colors == nil { + return + } + switch typedColors := colors.(type) { - case bool: - std.colors = typedColors - if !std.colors { + case string: + switch typedColors { + case "on": + std.colors = ColorOn + case "off": + std.colors = ColorOff setColor(lipgloss.NoColor{}, &ColorRed) setColor(lipgloss.NoColor{}, &ColorGreen) setColor(lipgloss.NoColor{}, &ColorYellow) setColor(lipgloss.NoColor{}, &ColorCyan) setColor(lipgloss.NoColor{}, &GolorGray) setColor(lipgloss.NoColor{}, &colorBorder) + default: + std.colors = ColorAuto } - return + case bool: + if typedColors { + std.colors = ColorOn + return + } + + std.colors = ColorOff + setColor(lipgloss.NoColor{}, &ColorRed) + setColor(lipgloss.NoColor{}, &ColorGreen) + setColor(lipgloss.NoColor{}, &ColorYellow) + setColor(lipgloss.NoColor{}, &ColorCyan) + setColor(lipgloss.NoColor{}, &GolorGray) + setColor(lipgloss.NoColor{}, &colorBorder) case map[string]interface{}: - std.colors = true + std.colors = ColorOn setColor(typedColors["red"], &ColorRed) setColor(typedColors["green"], &ColorGreen) setColor(typedColors["yellow"], &ColorYellow) setColor(typedColors["cyan"], &ColorCyan) setColor(typedColors["gray"], &GolorGray) setColor(typedColors["gray"], &colorBorder) - return default: - std.colors = true + std.colors = ColorAuto } } @@ -227,14 +261,46 @@ func Gray(s string) string { } func Bold(s string) string { - if !std.colors { + if !Colorized() { return lipgloss.NewStyle().Render(s) } return lipgloss.NewStyle().Bold(true).Render(s) } -func Box(left, right string) { +func LogMeta(hookName string) { + name := "🥊 lefthook " + if !Colorized() { + name = "lefthook " + } + + box( + Cyan(name)+Gray(fmt.Sprintf("v%s", version.Version(false))), + Gray("hook: ")+Bold(hookName), + ) +} + +func Success(name string) { + format := "✔️ %s\n" + if !Colorized() { + format = "✓ %s\n" + } + Infof(format, Green(name)) +} + +func Failure(name, failText string) { + if len(failText) != 0 { + failText = fmt.Sprintf(": %s", failText) + } + + format := "🥊 %s%s\n" + if !Colorized() { + format = "✗ %s%s\n" + } + Infof(format, Red(name), Red(failText)) +} + +func box(left, right string) { Info( lipgloss.JoinHorizontal( lipgloss.Top, From f7562635f114e14413fb4d909ff56d2800b77fb1 Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Fri, 30 Aug 2024 12:53:17 +0300 Subject: [PATCH 2/3] fix: support NO_COLOR and CLICOLOR_FORCE --- docs/configuration.md | 4 ++-- docs/usage.md | 10 ++++++++++ internal/lefthook/lefthook.go | 22 +++++++++++++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 11270cd2..8e5fda6f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -97,9 +97,9 @@ When set to `true`, fail (with exit status 1) if `lefthook` executable can't be ### `colors` -**Default: `true`** +**Default: `auto`** -Whether enable or disable colorful output of Lefthook. This option can be overwritten with `--no-colors` option. You can also provide your own color codes. +Whether enable or disable colorful output of Lefthook. This option can be overwritten with `--colors` option. You can also provide your own color codes. **Example** diff --git a/docs/usage.md b/docs/usage.md index 4524ceb5..f3f2a43f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -20,6 +20,8 @@ Then use git as usually, you don't need to reinstall lefthook when you change th - [`LEFTHOOK_QUIET`](#lefthook_quiet) - [`LEFTHOOK_VERBOSE`](#lefthook_verbose) - [`LEFTHOOK_BIN`](#lefthook_bin) + - [`NO_COLOR`](#no_color) + - [`CLICOLOR_FORCE`](#clicolor_force) - [Features and tips](#features-and-tips) - [Disable lefthook in CI](#disable-lefthook-in-ci) - [Local config](#local-config) @@ -215,6 +217,14 @@ Useful for cases when: - lefthook is installed multiple ways, and you want to be explicit about which one is used (example: installed through homebrew, but also is in Gemfile but you are using a ruby version manager like rbenv that prepends it to the path) - debugging and/or developing lefthook +### `NO_COLOR` + +Set `NO_COLOR=true` to disable colored output in lefthook and all subcommands that lefthook calls. + +### `CLICOLOR_FORCE` + +Set `CLICOLOR_FORCE=true` to force colored output in lefthook and all subcommands. + ## Features and tips ### Disable lefthook in CI diff --git a/internal/lefthook/lefthook.go b/internal/lefthook/lefthook.go index c0f07e2d..38e35275 100644 --- a/internal/lefthook/lefthook.go +++ b/internal/lefthook/lefthook.go @@ -17,6 +17,8 @@ import ( const ( EnvVerbose = "LEFTHOOK_VERBOSE" // keep all output + envNoColor = "NO_COLOR" + envForceColor = "CLICOLOR_FORCE" hookFileMode = 0o755 oldHookPostfix = ".old" ) @@ -42,7 +44,7 @@ type Lefthook struct { // New returns an instance of Lefthook. func initialize(opts *Options) (*Lefthook, error) { - if os.Getenv(EnvVerbose) == "1" || os.Getenv(EnvVerbose) == "true" { + if isEnvEnabled(EnvVerbose) { opts.Verbose = true } @@ -50,10 +52,19 @@ func initialize(opts *Options) (*Lefthook, error) { log.SetLevel(log.DebugLevel) } + if isEnvEnabled(envForceColor) { + opts.Colors = "on" + } + + if isEnvEnabled(envNoColor) { + opts.Colors = "off" + } + // DEPRECATED: Will be removed with a --no-colors option if opts.NoColors && opts.Colors == "auto" { opts.Colors = "off" } + log.SetColors(opts.Colors) repo, err := git.NewRepository(opts.Fs, git.NewExecutor(system.Cmd)) @@ -129,3 +140,12 @@ func (l *Lefthook) addHook(hook string, args templates.Args) error { l.Fs, hookPath, templates.Hook(hook, args), hookFileMode, ) } + +func isEnvEnabled(name string) bool { + value := os.Getenv(name) + if len(value) > 0 && value != "0" && value != "false" { + return true + } + + return false +} From 0d0050d7ea6fa500852b325a53012a23127847f9 Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Fri, 30 Aug 2024 13:02:25 +0300 Subject: [PATCH 3/3] fix: use ENV values only if --colors option not set --- internal/lefthook/lefthook.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/lefthook/lefthook.go b/internal/lefthook/lefthook.go index 38e35275..8840adca 100644 --- a/internal/lefthook/lefthook.go +++ b/internal/lefthook/lefthook.go @@ -52,17 +52,19 @@ func initialize(opts *Options) (*Lefthook, error) { log.SetLevel(log.DebugLevel) } - if isEnvEnabled(envForceColor) { - opts.Colors = "on" - } + if opts.Colors == "auto" { + if isEnvEnabled(envForceColor) { + opts.Colors = "on" + } - if isEnvEnabled(envNoColor) { - opts.Colors = "off" - } + if isEnvEnabled(envNoColor) { + opts.Colors = "off" + } - // DEPRECATED: Will be removed with a --no-colors option - if opts.NoColors && opts.Colors == "auto" { - opts.Colors = "off" + // DEPRECATED: Will be removed with a --no-colors option + if opts.NoColors { + opts.Colors = "off" + } } log.SetColors(opts.Colors)