diff --git a/README.md b/README.md index fe38163..2c3f8df 100644 --- a/README.md +++ b/README.md @@ -747,13 +747,14 @@ bottom, each stack rising out of it and the current branch marked. Under each br PR state and titles come from a local cache that's refreshed when it goes stale, and the commits come from local git, so it stays quick. -`git stack log short` draws the same tree with just the branch names, one line each. It only reads -local git, so it never waits on GitHub. To make that what `git stack` and `git stack log` show, +`git stack log short` draws the same tree with just the branch names, one line each, and +`(needs restack)` after any branch whose parent has moved on. It only reads local git, so it never +waits on GitHub. To make that what `git stack` and `git stack log` show, `git config --global stack.log.short true`; `git stack log long` still gets you the full tree. ``` ❯ git stack log short -○ billing-webhook-retries +○ billing-webhook-retries (needs restack) ● billing-webhook-schema │ ○ fix-login-timeout ├─┘ diff --git a/cmd/log.go b/cmd/log.go index 36792d7..4bdc7d1 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -74,8 +74,8 @@ func showLog(ctx context.Context, c *cli, m logMode) error { } o := app.ViewOptions{CurrentStackOnly: m.currentStack, PRs: app.PRsFresh, Commits: true} if short { - // Names only: no PR state, commits or restack checks to work out. - o = app.ViewOptions{CurrentStackOnly: m.currentStack, SkipRestackCheck: true} + // Names and restack markers only: no PR state or commits to work out. + o = app.ViewOptions{CurrentStackOnly: m.currentStack} } v, err := a.View(ctx, repo, o) if err != nil { diff --git a/cmd/log_short.go b/cmd/log_short.go index cc27d80..d158f52 100644 --- a/cmd/log_short.go +++ b/cmd/log_short.go @@ -10,8 +10,9 @@ func newLogShortCmd(c *cli, currentStack *bool) *cobra.Command { return &cobra.Command{ Use: "short", Short: "Show every stack as a tree of branch names", - Long: `Shows the same tree as git stack log with only the branch names; no PRs, state -or commits. It reads nothing but local git, so it never touches the network.`, + Long: `Shows the same tree as git stack log with only the branch names, and +"(needs restack)" after any branch whose parent has moved on; no PRs, state or +commits. It reads nothing but local git, so it never touches the network.`, Args: cobra.NoArgs, ValidArgsFunction: completeNothing, RunE: func(cmd *cobra.Command, _ []string) error { diff --git a/cmd/log_test.go b/cmd/log_test.go index 9495713..44eef8f 100644 --- a/cmd/log_test.go +++ b/cmd/log_test.go @@ -122,6 +122,17 @@ func TestLogShortAndStack(t *testing.T) { if err != nil || !strings.Contains(out, "other") || !strings.Contains(errOut, "loose is in no stack") { t.Errorf("log short -s on an untracked branch:\n%s\n%s (%v)", out, errOut, err) } + + // Moving trunk on leaves the bottom branches needing a restack. + gittest.Run(t, dir, "switch", "-q", "main") + gittest.Commit(t, dir, "main.txt", "more", "more") + out, errOut, err = runWith(t, f, "--cwd", dir, "log", "short") + if err != nil { + t.Fatalf("log short after trunk moved: %v\n%s", err, errOut) + } + if want := "○ c\n○ b\n○ a (needs restack)\n│ ○ other (needs restack)\n├─┘\n● main\n"; out != want { + t.Errorf("log short after trunk moved =\n%s\nwant\n%s", out, want) + } } // TestLogShortConfig checks that stack.log.short makes log and bare git stack diff --git a/pkg/ui/log.go b/pkg/ui/log.go index 138b92b..0fb8a02 100644 --- a/pkg/ui/log.go +++ b/pkg/ui/log.go @@ -26,7 +26,8 @@ type LogOptions struct { // would run past it is cut short with "…". 0 never cuts (not a // terminal). Width int - // Short shows only the branch names, with no PR, state or commits. + // Short shows only the branch names, with "(needs restack)" after those + // that need one, and no PR, state or commits. Short bool } @@ -57,10 +58,11 @@ type LogOptions struct { // ├─┘ // ■ main // -// Short leaves just the branch names, one line each: +// Short leaves just the branch names, one line each, marking those that +// need a restack: // // ● feat-b -// ○ feat-a +// ○ feat-a (needs restack) // │ ○ hotfix // ├─┘ // ■ main @@ -105,10 +107,15 @@ func RenderLog(v *app.View, o LogOptions) string { gutter := prefix + st.Prefix.Render("│") for i := len(s) - 1; i >= 0; i-- { r := s[i] - b.WriteString(prefix + logMarker(r, st) + " " + logName(r, st) + "\n") + b.WriteString(prefix + logMarker(r, st) + " " + logName(r, st)) if o.Short { + if r.NeedsRestack { + b.WriteString(" " + st.Restack.Render("(needs restack)")) + } + b.WriteString("\n") continue } + b.WriteString("\n") pr, title := logPR(r, o) b.WriteString(FitLine(gutter+" "+pr, title, o.Width) + "\n") if meta := logMeta(r, o); meta != "" { diff --git a/pkg/ui/log_test.go b/pkg/ui/log_test.go index caaea52..fd3f6bb 100644 --- a/pkg/ui/log_test.go +++ b/pkg/ui/log_test.go @@ -77,7 +77,7 @@ func TestRenderLogPlain(t *testing.T) { func TestRenderLogShort(t *testing.T) { out := RenderLog(&app.View{Rows: logRows()}, LogOptions{Now: now, Short: true}) want := strings.Join([]string{ - "○ feat/ui-tests", + "○ feat/ui-tests (needs restack)", "● feat/ui", "○ feat/api", "│ ○ fix/typo", diff --git a/pkg/ui/picker_test.go b/pkg/ui/picker_test.go index c547e72..1e3cfce 100644 --- a/pkg/ui/picker_test.go +++ b/pkg/ui/picker_test.go @@ -95,8 +95,10 @@ func TestPickerNavigationKeys(t *testing.T) { func TestPickerFilterKeepsTreeContext(t *testing.T) { tm := newTestPicker(t, PickerOptions{}) + // No waiting on "> hot": the renderer may draw just the last "t" after a + // cursor move, so the text need never appear whole. Messages are handled + // in order, so enter still sees the full filter. tm.Type("hot") - waitForText(t, tm, "> hot") tm.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) p := finalPicker(t, tm) if r := p.Result(); r.Branch != "hotfix" {