diff --git a/internal/config/available_hooks.go b/internal/config/available_hooks.go index cdd46157..551d4d05 100644 --- a/internal/config/available_hooks.go +++ b/internal/config/available_hooks.go @@ -8,35 +8,35 @@ const GhostHookName = "prepare-commit-msg" // AvailableHooks - list of hooks taken from https://git-scm.com/docs/githooks. var AvailableHooks = [...]string{ - "pre-applypatch", - "applypatch-msg", - "post-applypatch", + "pre-commit", + "pre-push", "commit-msg", + "applypatch-msg", "fsmonitor-watchman", "p4-changelist", "p4-post-changelist", "p4-pre-submit", "p4-prepare-changelist", - "pre-commit", + "post-applypatch", + "post-checkout", "post-commit", - "pre-receive", - "proc-receive", - "post-receive", + "post-index-change", "post-merge", - "pre-rebase", - "rebase", - "update", - "post-update", + "post-receive", "post-rewrite", - "post-checkout", - "post-index-change", + "post-update", + "pre-applypatch", "pre-auto-gc", "pre-merge-commit", - "pre-push", + "pre-rebase", + "pre-receive", "prepare-commit-msg", + "proc-receive", "push-to-checkout", + "rebase", "reference-transaction", "sendemail-validate", + "update", } func HookUsesStagedFiles(hook string) bool { diff --git a/internal/git/exec.go b/internal/git/exec.go index 45fd06de..5f60d636 100644 --- a/internal/git/exec.go +++ b/internal/git/exec.go @@ -10,13 +10,16 @@ import ( ) type Exec interface { + SetRootPath(root string) Cmd(cmd string) (string, error) CmdArgs(args ...string) (string, error) CmdLines(cmd string) ([]string, error) RawCmd(cmd string) (string, error) } -type OsExec struct{} +type OsExec struct { + root string +} // NewOsExec returns an object that executes given commands // in the OS. @@ -24,6 +27,10 @@ func NewOsExec() *OsExec { return &OsExec{} } +func (o *OsExec) SetRootPath(root string) { + o.root = root +} + // Cmd runs plain string command. Trims spaces around output. func (o *OsExec) Cmd(cmd string) (string, error) { args := strings.Split(cmd, " ") @@ -68,9 +75,11 @@ func (o *OsExec) rawExecArgs(args ...string) (string, error) { log.Debug("[lefthook] cmd: ", args) cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = o.root cmd.Env = append(os.Environ(), "LEFTHOOK=0") out, err := cmd.CombinedOutput() + log.Debug("[lefthook] dir: ", o.root) log.Debug("[lefthook] err: ", err) log.Debug("[lefthook] out: ", string(out)) if err != nil { diff --git a/internal/git/repository.go b/internal/git/repository.go index b01027bf..0c6b0dd2 100644 --- a/internal/git/repository.go +++ b/internal/git/repository.go @@ -78,6 +78,8 @@ func NewRepository(fs afero.Fs, git Exec) (*Repository, error) { gitPath = filepath.Join(rootPath, gitPath) } + git.SetRootPath(rootPath) + return &Repository{ Fs: fs, Git: git, @@ -317,6 +319,9 @@ func (r *Repository) extractFiles(lines []string) ([]string, error) { } func (r *Repository) isFile(path string) (bool, error) { + if !strings.HasPrefix(path, r.RootPath) { + path = filepath.Join(r.RootPath, path) + } stat, err := r.Fs.Stat(path) if err != nil { if os.IsNotExist(err) { diff --git a/internal/git/repository_test.go b/internal/git/repository_test.go index 0eb9199e..bab080ba 100644 --- a/internal/git/repository_test.go +++ b/internal/git/repository_test.go @@ -11,6 +11,8 @@ type GitMock struct { cases map[string]string } +func (g GitMock) SetRootPath(_root string) {} + func (g GitMock) Cmd(cmd string) (string, error) { res, err := g.RawCmd(cmd) if err != nil { diff --git a/internal/lefthook/install.go b/internal/lefthook/install.go index 540e1b9c..3e77a78e 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -116,7 +116,14 @@ func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, force bool) error { return nil } - log.Info(log.Cyan("SYNCING")) + log.Infof(log.Cyan("sync hooks")) + + var success bool + defer func() { + if !success { + log.Info(log.Cyan(": ❌")) + } + }() checksum, err := l.configChecksum() if err != nil { @@ -148,8 +155,11 @@ func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, force bool) error { return err } + success = true if len(hookNames) > 0 { - log.Info(log.Cyan("SERVED HOOKS:"), log.Bold(strings.Join(hookNames, ", "))) + log.Info(log.Cyan(": ✔️"), log.Gray("("+strings.Join(hookNames, ", ")+")")) + } else { + log.Info(log.Cyan(": ✔️ ")) } return nil diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index d51a01f1..a5031d3c 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -82,7 +82,10 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error { } if !logSettings.SkipMeta() { - log.Info(log.Cyan("Lefthook v" + version.Version(false))) + log.Box( + log.Cyan("🥊 lefthook ")+log.Gray(fmt.Sprintf("v%s", version.Version(false))), + log.Gray("hook: ")+log.Bold(hookName), + ) } // This line controls updating the git hook if config has changed @@ -93,10 +96,6 @@ Run 'lefthook install' manually.`, ) } - if !logSettings.SkipMeta() { - log.Info(log.Cyan("RUNNING HOOK:"), log.Bold(hookName)) - } - // Find the hook hook, ok := cfg.Hooks[hookName] if !ok { @@ -182,14 +181,21 @@ func printSummary( ) { if len(results) == 0 { if !logSettings.SkipEmptySummary() { - log.Info(log.Cyan("\nSUMMARY: (SKIP EMPTY)")) + log.Separate( + fmt.Sprintf( + "%s %s %s", + log.Cyan("summary:"), + log.Gray("(skip)"), + log.Yellow("empty"), + ), + ) } return } - log.Info(log.Cyan( - fmt.Sprintf("\nSUMMARY: (done in %.2f seconds)", duration.Seconds()), - )) + log.Separate( + log.Cyan("summary: ") + log.Gray(fmt.Sprintf("(done in %.2f seconds)", duration.Seconds())), + ) if !logSettings.SkipSuccess() { for _, result := range results { diff --git a/internal/lefthook/run/runner.go b/internal/lefthook/run/runner.go index 15b9befd..c1b59398 100644 --- a/internal/lefthook/run/runner.go +++ b/internal/lefthook/run/runner.go @@ -15,6 +15,7 @@ import ( "sync" "sync/atomic" + "github.com/charmbracelet/lipgloss" "github.com/spf13/afero" "github.com/evilmartians/lefthook/internal/config" @@ -29,6 +30,7 @@ type status int8 const ( executableFileMode os.FileMode = 0o751 executableMask os.FileMode = 0o111 + execLogPadding = 2 ) var surroundingQuotesRegexp = regexp.MustCompile(`^'(.*)'$`) @@ -139,10 +141,10 @@ func (r *Runner) runLFSHook(ctx context.Context) error { output := strings.Trim(out.String(), "\n") if output != "" { - log.Debug("[git-lfs] output: ", output) + log.Debug("[git-lfs] out: ", output) } if err != nil { - log.Debug("[git-lfs] error: ", err) + log.Debug("[git-lfs] err: ", err) } if err == nil && output != "" { @@ -478,14 +480,14 @@ func (r *Runner) logSkip(name, reason string) { return } - log.Info( - fmt.Sprintf( - "%s: %s %s", - log.Bold(name), - log.Gray("(skip)"), - log.Yellow(reason), - ), - ) + log.Styled(). + WithLeftBorder(lipgloss.NormalBorder(), log.ColorCyan). + WithPadding(execLogPadding). + Info( + log.Cyan(log.Bold(name)) + " " + + log.Gray("(skip)") + " " + + log.Yellow(reason), + ) } func (r *Runner) logExecute(name string, err error, out io.Reader) { @@ -494,17 +496,24 @@ func (r *Runner) logExecute(name string, err error, out io.Reader) { } var execLog string + var color lipgloss.TerminalColor switch { case r.SkipSettings.SkipExecutionInfo(): execLog = "" case err != nil: - execLog = fmt.Sprint(log.Red("\n EXECUTE > "), log.Bold(name)) + execLog = log.Red(fmt.Sprintf("%s ❯ ", name)) + color = log.ColorRed default: - execLog = fmt.Sprint(log.Cyan("\n EXECUTE > "), log.Bold(name)) + execLog = log.Cyan(fmt.Sprintf("%s ❯ ", name)) + color = log.ColorCyan } if execLog != "" { - log.Info(execLog) + log.Styled(). + WithLeftBorder(lipgloss.ThickBorder(), color). + WithPadding(execLogPadding). + Info(execLog) + log.Info() } if err == nil && r.SkipSettings.SkipExecutionOutput() { diff --git a/internal/lefthook/run/runner_test.go b/internal/lefthook/run/runner_test.go index 8c05e514..c33dc5a1 100644 --- a/internal/lefthook/run/runner_test.go +++ b/internal/lefthook/run/runner_test.go @@ -39,6 +39,8 @@ type GitMock struct { commands []string } +func (g *GitMock) SetRootPath(_root string) {} + func (g *GitMock) Cmd(cmd string) (string, error) { g.mux.Lock() g.commands = append(g.commands, cmd) diff --git a/internal/lefthook/run_test.go b/internal/lefthook/run_test.go index cd634561..b984e1f5 100644 --- a/internal/lefthook/run_test.go +++ b/internal/lefthook/run_test.go @@ -12,6 +12,8 @@ import ( type GitMock struct{} +func (g GitMock) SetRootPath(_root string) {} + func (g GitMock) Cmd(_cmd string) (string, error) { return "", nil } diff --git a/internal/log/log.go b/internal/log/log.go index f3e4d383..8da2ec44 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -14,32 +14,37 @@ import ( ) var ( - colorRed lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ + ColorRed lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ Dark: lipgloss.CompleteColor{TrueColor: "#ff6347", ANSI256: "196", ANSI: "9"}, Light: lipgloss.CompleteColor{TrueColor: "#d70000", ANSI256: "160", ANSI: "1"}, } - colorGreen lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ + ColorGreen lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ Dark: lipgloss.CompleteColor{TrueColor: "#76ff7a", ANSI256: "155", ANSI: "10"}, Light: lipgloss.CompleteColor{TrueColor: "#afd700", ANSI256: "148", ANSI: "2"}, } - colorYellow lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ + ColorYellow lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ Dark: lipgloss.CompleteColor{TrueColor: "#fada5e", ANSI256: "191", ANSI: "11"}, Light: lipgloss.CompleteColor{TrueColor: "#ffaf00", ANSI256: "214", ANSI: "3"}, } - colorCyan lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ + ColorCyan lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ Dark: lipgloss.CompleteColor{TrueColor: "#70C0BA", ANSI256: "37", ANSI: "14"}, Light: lipgloss.CompleteColor{TrueColor: "#00af87", ANSI256: "36", ANSI: "6"}, } - colorGray lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ + GolorGray lipgloss.TerminalColor = lipgloss.CompleteAdaptiveColor{ Dark: lipgloss.CompleteColor{TrueColor: "#808080", ANSI256: "244", ANSI: "7"}, Light: lipgloss.CompleteColor{TrueColor: "#4e4e4e", ANSI256: "239", ANSI: "8"}, } + colorBorder lipgloss.TerminalColor = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} + std = New() + + separatorWidth = 36 + separatorMargin = 2 ) type Level uint32 @@ -55,6 +60,10 @@ const ( spinnerText = " waiting" ) +type StyleLogger struct { + style lipgloss.Style +} + type Logger struct { level Level out io.Writer @@ -85,9 +94,36 @@ func StopSpinner() { std.spinner.Stop() } +func Styled() StyleLogger { + return StyleLogger{ + style: lipgloss.NewStyle(), + } +} + +func (s StyleLogger) WithLeftBorder(border lipgloss.Border, color lipgloss.TerminalColor) StyleLogger { + s.style = s.style.BorderStyle(border).BorderLeft(true).BorderForeground(color) + + return s +} + +func (s StyleLogger) WithPadding(m int) StyleLogger { + s.style = s.style.PaddingLeft(m) + + return s +} + +func (s StyleLogger) Info(str string) { + Info( + lipgloss.JoinVertical( + lipgloss.Left, + s.style.Render(str), + ), + ) +} + func Debug(args ...interface{}) { res := fmt.Sprint(args...) - std.Debug(color(colorGray).Render(res)) + std.Debug(color(GolorGray).Render(res)) } func Debugf(format string, args ...interface{}) { @@ -98,6 +134,16 @@ func Info(args ...interface{}) { std.Info(args...) } +func InfoPad(s string) { + Info( + lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderLeft(true). + BorderForeground(ColorCyan). + Render(s), + ) +} + func Infof(format string, args ...interface{}) { std.Infof(format, args...) } @@ -136,14 +182,23 @@ func SetColors(colors interface{}) { switch typedColors := colors.(type) { case bool: std.colors = typedColors + if !std.colors { + setColor(lipgloss.NoColor{}, &ColorRed) + setColor(lipgloss.NoColor{}, &ColorGreen) + setColor(lipgloss.NoColor{}, &ColorYellow) + setColor(lipgloss.NoColor{}, &ColorCyan) + setColor(lipgloss.NoColor{}, &GolorGray) + setColor(lipgloss.NoColor{}, &colorBorder) + } return case map[string]interface{}: std.colors = true - setColor(typedColors["red"], &colorRed) - setColor(typedColors["green"], &colorGreen) - setColor(typedColors["yellow"], &colorYellow) - setColor(typedColors["cyan"], &colorCyan) - setColor(typedColors["gray"], &colorGray) + 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 @@ -151,16 +206,15 @@ func SetColors(colors interface{}) { } func setColor(colorCode interface{}, adaptiveColor *lipgloss.TerminalColor) { - if colorCode == nil { - return - } - var code string switch typedCode := colorCode.(type) { case int: code = strconv.Itoa(typedCode) case string: code = typedCode + case lipgloss.NoColor: + *adaptiveColor = typedCode + return default: return } @@ -173,23 +227,23 @@ func setColor(colorCode interface{}, adaptiveColor *lipgloss.TerminalColor) { } func Cyan(s string) string { - return color(colorCyan).Render(s) + return color(ColorCyan).Render(s) } func Green(s string) string { - return color(colorGreen).Render(s) + return color(ColorGreen).Render(s) } func Red(s string) string { - return color(colorRed).Render(s) + return color(ColorRed).Render(s) } func Yellow(s string) string { - return color(colorYellow).Render(s) + return color(ColorYellow).Render(s) } func Gray(s string) string { - return color(colorGray).Render(s) + return color(GolorGray).Render(s) } func Bold(s string) string { @@ -200,11 +254,41 @@ func Bold(s string) string { return lipgloss.NewStyle().Bold(true).Render(s) } -func color(clr lipgloss.TerminalColor) lipgloss.Style { - if !std.colors { - return lipgloss.NewStyle() - } +func Box(left, right string) { + Info( + lipgloss.JoinHorizontal( + lipgloss.Top, + lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder(), true, false, true, true). + BorderForeground(colorBorder). + Padding(0, 1). + Render(left), + lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder(), true, true, true, false). + BorderForeground(colorBorder). + Padding(0, 1). + Render(right), + ), + ) +} + +func Separate(s string) { + Info( + lipgloss.JoinVertical( + lipgloss.Left, + lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderBottom(true). + BorderForeground(colorBorder). + Width(separatorWidth). + MarginLeft(separatorMargin). + Render(""), + s, + ), + ) +} +func color(clr lipgloss.TerminalColor) lipgloss.Style { return lipgloss.NewStyle().Foreground(clr) } @@ -243,7 +327,12 @@ func (l *Logger) Info(args ...interface{}) { } func (l *Logger) Debug(args ...interface{}) { - l.Log(DebugLevel, args...) + leftBorder := lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderLeft(true). + BorderForeground(colorBorder). + Render("") + l.Log(DebugLevel, append([]interface{}{leftBorder}, args...)...) } func (l *Logger) Error(args ...interface{}) {