diff --git a/git/cherry_pick.go b/git/cherry_pick.go index a0d0be3..3e592ad 100644 --- a/git/cherry_pick.go +++ b/git/cherry_pick.go @@ -24,15 +24,18 @@ type CherryPick struct { func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { logger := log.LoggerFromCtx(ctx) - err := tui.WithSpinner(ctx, "checking repository is ready ...", func(ctx context.Context, logger log.Logger) error { - logger.Infof("checking repository is dirty") + + logger.Infof("🍒 %s", color.Bold("starting cherry-picker\n")) + + err := tui.WithStep(ctx, "checking is repository ready", func(ctx context.Context, logger log.Logger) error { + logger.Infof("checking is repository dirty") if dirty, err := IsDirty(ctx); err != nil { return fmt.Errorf("error checking if the repository is dirty: %w", err) } else if dirty { return fmt.Errorf("the repository is dirty. please commit your changes before continuing") } - logger.Infof("checking repository is in a rebase or am") + logger.Infof("checking is repository in a rebase or am") if rebaseOrAm, err := IsInRebaseOrAm(ctx); err != nil { return fmt.Errorf("error checking if the repository is in a rebase or am: %w", err) } else if rebaseOrAm { @@ -44,20 +47,18 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { if err != nil { return err } - logger.Successf("repository is available for cherry-pick") var pr *gitobj.PullRequest - title := fmt.Sprintf("fetching the pull request %s ...", color.Cyan(fmt.Sprintf("#%d", cherryPick.PRNumber))) - err = tui.WithSpinner(ctx, title, func(ctx context.Context, logger log.Logger) error { - logger.Infof("getting the pull request %s", color.Cyan(fmt.Sprintf("#%d", cherryPick.PRNumber))) + err = tui.WithStep(ctx, "validating the pull request", func(ctx context.Context, logger log.Logger) error { + logger.WithField("pr", cherryPick.PRNumber).Infof("fetching the pull request") if pr, err = GetPullRequest(ctx, cherryPick.PRNumber); err != nil { return fmt.Errorf("error getting the pull request: %w", err) } - logger.Infof("%v (%v) by %v\n %v ← %v", color.Cyan(pr.Title), pr.PRNumberString(), color.Cyan(pr.Author.Login), color.Cyan(pr.BaseRefName), color.Cyan(pr.HeadRefName)) + logger.Successf("%s %s %s", pr.PRNumberString(), pr.Url, color.Grey(pr.Author.Login)) if pr.State != gitobj.PullRequestStateMerged { - return fmt.Errorf("PR %s is not merged (current state: %s). please ensure the PR is merged before continuing", pr.PRNumberString(), pr.StateString()) + return fmt.Errorf("PR is not merged (current state: %s). please ensure the PR is merged before continuing", pr.StateString()) } return nil @@ -65,18 +66,19 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { if err != nil { return err } - logger.Successf("fetched the pull request") var mergeStrategy MergeStrategy - err = tui.WithSpinner(ctx, "determining merge strategy ...", func(ctx context.Context, logger log.Logger) error { + err = tui.WithStep(ctx, "determining merge strategy", func(ctx context.Context, logger log.Logger) error { if cherryPick.MergeStrategy == MergeStrategyAuto { - logger.Infof("determining merge strategy automatically") + logger.Infof("no merge strategy given, determining merge strategy") if mergeStrategy, err = PRMergedWith(ctx, cherryPick.PRNumber); err != nil { return fmt.Errorf("error determining merge strategy: %w", err) } + + logger.Successf("determined merge strategy as %s", color.Cyan(mergeStrategy)) } else { - logger.Infof("using merge strategy %s with given flag", color.Cyan(cherryPick.MergeStrategy)) + logger.Infof("use merge strategy %s with given flag", color.Cyan(cherryPick.MergeStrategy)) } return nil @@ -84,26 +86,26 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { if err != nil { return err } - logger.Successf("determined merge strategy as %s", color.Cyan(mergeStrategy)) var cherryPickBranchName = fmt.Sprintf("cherry-pick-pr-%d-onto-%s-%d", cherryPick.PRNumber, strings.ReplaceAll(cherryPick.OnTo, "/", "-"), time.Now().Unix()) - err = tui.WithSpinner(ctx, "checking out branch ...", func(ctx context.Context, logger log.Logger) error { - logger.Infof("branch name: %v", color.Cyan(cherryPickBranchName)) - logger.Infof("starting point: %v", color.Cyan(fmt.Sprintf("origin/%s", cherryPick.OnTo))) - - logger.Infof("fetching the branch %v", color.Cyan(pr.BaseRefName)) + err = tui.WithStep(ctx, "checking out branch", func(ctx context.Context, logger log.Logger) error { + logger.WithField("branch", pr.BaseRefName).Infof("fetching the branch") if err = Fetch(ctx, "origin", pr.BaseRefName); err != nil { return fmt.Errorf("error fetching the branch '%s': %w", cherryPick.OnTo, err) } - logger.Infof("fetching the branch %v", color.Cyan(cherryPick.OnTo)) - if err = Fetch(ctx, "origin", cherryPick.OnTo); err != nil { - return fmt.Errorf("error fetching the branch '%s': %w", cherryPick.OnTo, err) + if cherryPick.OnTo != pr.BaseRefName { + logger.WithField("branch", cherryPick.OnTo).Infof("fetching the branch") + if err = Fetch(ctx, "origin", cherryPick.OnTo); err != nil { + return fmt.Errorf("error fetching the branch '%s': %w", cherryPick.OnTo, err) + } } - logger.Infof("checking out a new branch %v based on %v", color.Cyan(cherryPickBranchName), color.Cyan(cherryPick.OnTo)) + logger.WithField("branch", cherryPickBranchName). + WithField("base", cherryPick.OnTo). + Infof("checking out to new branch") if err = CheckoutNewBranch(ctx, cherryPickBranchName, fmt.Sprintf("origin/%s", cherryPick.OnTo)); err != nil { - return err + return fmt.Errorf("error checking out to new branch '%s': %w", cherryPickBranchName, err) } return nil @@ -111,12 +113,11 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { if err != nil { return err } - logger.Successf("checked out to %s based on %s", color.Cyan(cherryPickBranchName), color.Cyan(cherryPick.OnTo)) switch mergeStrategy { case MergeStrategyRebase: - err = tui.WithSpinner(ctx, "rebasing ...", func(ctx context.Context, logger log.Logger) error { - logger.Infof("fetching diff") + err = tui.WithStep(ctx, "rebasing PR", func(ctx context.Context, logger log.Logger) error { + logger.WithField("pr", cherryPick.PRNumber).Infof("fetching diff") var prDiff bytes.Buffer if err = NewCommand("gh", "pr", "diff", strconv.Itoa(cherryPick.PRNumber), "--patch").Run(ctx, WithStdout(&prDiff)); err != nil { return fmt.Errorf("error getting PR diff: %w", err) @@ -124,11 +125,13 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { logger.Infof("applying diff") if err = NewCommand("git", "am", "-3").Run(ctx, WithStdin(&prDiff)); err != nil { + helpMsg := fmt.Sprintf("run %s after resolve the conflicts\nrun %s if you want to abort the rebase", color.Green("`git am --continue`"), color.Yellow("`git am --abort`")) + var gitError *GitError - if errors.As(err, &gitError) && gitError.ExitCode == 1 && strings.Contains(gitError.Stderr, "error: could not apply") { - return fmt.Errorf("error applying PR diff\nplease resolve the conflicts and run %s. if you want to abort the rebase, run %s", color.Green("`git am --continue`"), color.Yellow("`git am --abort`")) + if errors.As(err, &gitError) && gitError.ExitCode == 1 && strings.Contains(gitError.Stderr, "error: Failed to merge in the changes") { + return fmt.Errorf("error applying PR diff\n%s", helpMsg) } - return fmt.Errorf("error cherry-picking PR merge commit: %w", err) + return fmt.Errorf("error applying PR diff\n%s\n\n%w", helpMsg, err) } return nil @@ -139,14 +142,16 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { logger.Successf("rebased branch %s onto %s", color.Cyan(cherryPickBranchName), color.Cyan(cherryPick.OnTo)) case MergeStrategySquash: - err = tui.WithSpinner(ctx, "cherry-picking ...", func(ctx context.Context, logger log.Logger) error { - logger.Infof("cherry-picking PR merge commit %v", color.Cyan(pr.MergeCommit.Sha)) + err = tui.WithStep(ctx, "cherry-picking PR merge commit", func(ctx context.Context, logger log.Logger) error { + logger.WithField("merge_commit", pr.MergeCommit.Sha[:7]).Infof("cherry-picking") if err = NewCommand("git", "cherry-pick", "--keep-redundant-commits", pr.MergeCommit.Sha).Run(ctx); err != nil { + helpMsg := fmt.Sprintf("run %v after resolve the conflicts\nrun %v if you want to abort the cherry-pick", color.Green("`git cherry-pick --continue`"), color.Yellow("`git cherry-pick --abort`")) + var gitError *GitError if errors.As(err, &gitError) && gitError.ExitCode == 1 && strings.Contains(gitError.Stderr, "error: could not apply") { - return fmt.Errorf("error cherry-picking PR merge commit\nplease resolve the conflicts and run %v. if you want to abort the cherry-pick, run %v\n\n%v", color.Green("`git cherry-pick --continue`"), color.Yellow("`git cherry-pick --abort`"), err) + return fmt.Errorf("error cherry-picking PR merge commit\n%s", helpMsg) } - return fmt.Errorf("error cherry-picking PR merge commit: %w", err) + return fmt.Errorf("error cherry-picking PR merge commit\n%s\n\n%w", helpMsg, err) } return nil @@ -158,18 +163,24 @@ func (cherryPick *CherryPick) RunWithContext(ctx context.Context) error { } if cherryPick.Push { - err = tui.WithSpinner(ctx, "pushing ...", func(ctx context.Context, logger log.Logger) error { - logger.Infof("pushing branch %v", color.Cyan(cherryPickBranchName)) + err = tui.WithStep(ctx, "pushing branch", func(ctx context.Context, logger log.Logger) error { + logger.WithField("branch", cherryPickBranchName).Infof("pushing") if err = Push(ctx, "origin", cherryPickBranchName); err != nil { return fmt.Errorf("error pushing branch %s: %w", cherryPickBranchName, err) } + nameWithOwner, _ := GetNameWithOwner(ctx) + + logger.Successf("pushed branch %s\ncreate a pull request by visiting:\n ", + color.Cyan(cherryPickBranchName), + fmt.Sprintf("https://github.com/%s/pull/new/%s", nameWithOwner, cherryPickBranchName), + ) + return nil }) if err != nil { return err } - logger.Successf("pushed branch %s", color.Cyan(cherryPickBranchName)) } return nil diff --git a/gitobj/pull_request.go b/gitobj/pull_request.go index d249168..394b08d 100644 --- a/gitobj/pull_request.go +++ b/gitobj/pull_request.go @@ -47,5 +47,18 @@ func (pr PullRequest) StateString() string { } func (pr PullRequest) PRNumberString() string { - return color.Cyan(fmt.Sprintf("#%d", pr.Number)) + str := fmt.Sprintf("#%d", pr.Number) + switch pr.State { + case PullRequestStateOpen: + return color.Green(str) + case PullRequestStateClosed: + return color.Red(str) + case PullRequestStateMerged: + return color.Purple(str) + default: + if pr.IsDraft { + return color.Grey(str) + } + return "UNKNOWN" + } } diff --git a/go.mod b/go.mod index aecd339..122fc5c 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,15 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.4.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index 8c4d516..8347d58 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,29 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= +github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= diff --git a/internal/color/color.go b/internal/color/color.go index ca28381..7b0b2ea 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -1,43 +1,50 @@ package color import ( - "github.com/fatih/color" + "fmt" + + "github.com/charmbracelet/lipgloss" ) var ( - blue = color.New(color.FgBlue).SprintFunc() - cyan = color.New(color.FgCyan).SprintFunc() - green = color.New(color.FgGreen).SprintFunc() - red = color.New(color.FgRed).SprintFunc() - yellow = color.New(color.FgYellow).SprintFunc() - purple = color.New(color.FgMagenta).SprintFunc() - grey = color.New(color.FgHiWhite).SprintFunc() + bold = lipgloss.NewStyle().Bold(true) + red = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(1)) + green = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(2)) + yellow = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(3)) + blue = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(4)) + purple = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(5)) + cyan = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(6)) + grey = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(7)) ) +func Bold(a ...interface{}) string { + return bold.Render(fmt.Sprint(a...)) +} + func Blue(a ...interface{}) string { - return blue(a...) + return blue.Render(fmt.Sprint(a...)) } func Cyan(a ...interface{}) string { - return cyan(a...) + return cyan.Render(fmt.Sprint(a...)) } func Green(a ...interface{}) string { - return green(a...) + return green.Render(fmt.Sprint(a...)) } func Red(a ...interface{}) string { - return red(a...) + return red.Render(fmt.Sprint(a...)) } func Yellow(a ...interface{}) string { - return yellow(a...) + return yellow.Render(fmt.Sprint(a...)) } func Purple(a ...interface{}) string { - return purple(a...) + return purple.Render(fmt.Sprint(a...)) } func Grey(a ...interface{}) string { - return grey(a...) + return grey.Render(fmt.Sprint(a...)) } diff --git a/internal/log/entry.go b/internal/log/entry.go new file mode 100644 index 0000000..1b2ddf5 --- /dev/null +++ b/internal/log/entry.go @@ -0,0 +1,126 @@ +package log + +import ( + "fmt" + "os" + "slices" + "strings" + + "github.com/charmbracelet/lipgloss" + + "github.com/134130/gh-cherry-pick/internal/color" +) + +var _ Logger = (*entry)(nil) + +func newEntry(l *logger) *entry { + return &entry{ + logger: l, + indent: l.Indent, + } +} + +type entry struct { + logger *logger + indent int + fields []field +} + +type field struct { + key string + value interface{} +} + +func (e *entry) WithField(s string, i interface{}) Logger { + var f []field + copy(f, e.fields) + + slices.DeleteFunc(f, func(item field) bool { + return item.key == s + }) + + f = append(f, field{ + key: s, + value: i, + }) + + return &entry{ + logger: e.logger, + fields: f, + } +} + +func (e *entry) WithError(err error) Logger { + if err == nil { + return e + } + return e.WithField("error", err.Error()) +} + +func (e *entry) Info(s string) { + e.print(infoIcon, s) +} + +func (e *entry) Warn(s string) { + e.print(warnIcon, s) +} + +func (e *entry) Success(s string) { + e.print(successIcon, s) +} + +func (e *entry) Fail(s string) { + e.print(failIcon, s) +} + +func (e *entry) Infof(s string, i ...interface{}) { + e.Info(fmt.Sprintf(s, i...)) +} + +func (e *entry) Warnf(s string, i ...interface{}) { + e.Warn(fmt.Sprintf(s, i...)) +} + +func (e *entry) Successf(s string, i ...interface{}) { + e.Success(fmt.Sprintf(s, i...)) +} + +func (e *entry) Failf(s string, i ...interface{}) { + e.Fail(fmt.Sprintf(s, i...)) +} + +func (e *entry) IncreaseIndent() { + e.logger.IncreaseIndent() +} + +func (e *entry) DecreaseIndent() { + e.logger.DecreaseIndent() +} + +func (e *entry) ResetIndent() { + e.logger.ResetIndent() +} + +func (e *entry) print(icon, msg string) { + bullet := lipgloss.NewStyle().PaddingLeft(1 + e.logger.Indent).Render(icon) + content := lipgloss.NewStyle().PaddingLeft(1).Render(msg) + + output := lipgloss.JoinHorizontal(lipgloss.Top, bullet, content) + if len(e.fields) == 0 { + _, _ = fmt.Fprintln(os.Stdout, output) + return + } + + fields := make([]string, 0, len(e.fields)) + for _, f := range e.fields { + fields = append(fields, fmt.Sprintf("%s=%v", color.Purple(f.key), f.value)) + } + + _, _ = fmt.Fprintln(os.Stderr, lipgloss.JoinHorizontal( + lipgloss.Top, + output, + lipgloss.NewStyle().PaddingLeft(max(maxIndent-lipgloss.Width(output), 0)).Render(strings.Join(fields, " "))), + ) +} + +var maxIndent = 70 diff --git a/internal/log/interface.go b/internal/log/interface.go new file mode 100644 index 0000000..d6915d7 --- /dev/null +++ b/internal/log/interface.go @@ -0,0 +1,19 @@ +package log + +type Logger interface { + WithField(string, interface{}) Logger + WithError(error) Logger + + Info(string) + Warn(string) + Success(string) + Fail(string) + Infof(string, ...interface{}) + Warnf(string, ...interface{}) + Successf(string, ...interface{}) + Failf(string, ...interface{}) + + IncreaseIndent() + DecreaseIndent() + ResetIndent() +} diff --git a/internal/log/logger.go b/internal/log/logger.go index 8dbdb55..f813e22 100644 --- a/internal/log/logger.go +++ b/internal/log/logger.go @@ -3,22 +3,19 @@ package log import ( "context" "fmt" + "os" - "github.com/fatih/color" + "github.com/charmbracelet/lipgloss" - internalColor "github.com/134130/gh-cherry-pick/internal/color" + "github.com/134130/gh-cherry-pick/internal/color" ) -type Logger interface { - Infof(string, ...interface{}) - Warnf(string, ...interface{}) - Successf(string, ...interface{}) - Failf(string, ...interface{}) - - IncreaseIndent() - DecreaseIndent() - ResetIndent() -} +var ( + infoIcon = color.Blue("•") + warnIcon = color.Yellow("!️") + successIcon = color.Green("✔") + failIcon = color.Red("✘") +) func NewLogger() Logger { return &logger{} @@ -44,20 +41,44 @@ type logger struct { Indent int } +func (l *logger) WithField(s string, i interface{}) Logger { + return newEntry(l).WithField(s, i) +} + +func (l *logger) WithError(err error) Logger { + return newEntry(l).WithError(err) +} + +func (l *logger) Info(s string) { + l.print(infoIcon, s) +} + +func (l *logger) Warn(s string) { + l.print(warnIcon, s) +} + +func (l *logger) Success(s string) { + l.print(successIcon, s) +} + +func (l *logger) Fail(s string) { + l.print(failIcon, s) +} + func (l *logger) Infof(s string, i ...interface{}) { - _, _ = fmt.Fprintf(color.Output, "%*s %s %s\n", l.Indent, "", internalColor.Blue("•"), fmt.Sprintf(s, i...)) + l.print(infoIcon, fmt.Sprintf(s, i...)) } func (l *logger) Warnf(s string, i ...interface{}) { - _, _ = fmt.Fprintf(color.Output, "%*s %s %s\n", l.Indent, "", internalColor.Yellow("!️"), fmt.Sprintf(s, i...)) + l.print(warnIcon, fmt.Sprintf(s, i...)) } func (l *logger) Successf(s string, i ...interface{}) { - _, _ = fmt.Fprintf(color.Output, "%*s %s %s\n", l.Indent, "", internalColor.Green("✔"), fmt.Sprintf(s, i...)) + l.print(successIcon, fmt.Sprintf(s, i...)) } func (l *logger) Failf(s string, i ...interface{}) { - _, _ = fmt.Fprintf(color.Output, "%*s %s %s\n", l.Indent, "", internalColor.Red("✘"), fmt.Sprintf(s, i...)) + l.print(failIcon, fmt.Sprintf(s, i...)) } func (l *logger) IncreaseIndent() { @@ -71,3 +92,10 @@ func (l *logger) DecreaseIndent() { func (l *logger) ResetIndent() { l.Indent = 0 } + +func (l *logger) print(icon, msg string) { + bullet := lipgloss.NewStyle().PaddingLeft(1 + l.Indent).Render(icon) + content := lipgloss.NewStyle().PaddingLeft(1).Render(msg) + + _, _ = fmt.Fprintln(os.Stdout, lipgloss.JoinHorizontal(lipgloss.Top, bullet, content)) +} diff --git a/internal/tui/spinner.go b/internal/tui/spinner.go index a7ef41f..1861d36 100644 --- a/internal/tui/spinner.go +++ b/internal/tui/spinner.go @@ -2,19 +2,37 @@ package tui import ( "context" + "fmt" + "os" "time" "github.com/briandowns/spinner" + internalColor "github.com/134130/gh-cherry-pick/internal/color" "github.com/134130/gh-cherry-pick/internal/log" ) +func WithStep(ctx context.Context, title string, f func(ctx context.Context, logger log.Logger) error) (err error) { + logger := log.LoggerFromCtx(ctx) + logger.Infof(internalColor.Bold(title)) + + logger.IncreaseIndent() + defer logger.DecreaseIndent() + + err = f(ctx, logger) + + _, _ = fmt.Fprintln(os.Stdout) + + return +} + func WithSpinner(ctx context.Context, title string, f func(ctx context.Context, logger log.Logger) error) (err error) { logger := log.LoggerFromCtx(ctx) logger.IncreaseIndent() sp := spinner.New(spinner.CharSets[14], 40*time.Millisecond, spinner.WithColor("cyan")) sp.Suffix = " " + title + sp.FinalMSG = fmt.Sprintf("%s %s\n", internalColor.Green("✔"), title) sp.Start() defer func() { sp.Stop()