Split hidden issue counts by level

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:21:00 +02:00
co-authored by Claude Sonnet 5.5
parent aec4ec6e3b
commit 17151a578d
8 changed files with 57 additions and 28 deletions
+5 -3
View File
@@ -53,9 +53,11 @@ everyone else's checkout.
**Context.** Only the first level is shown by default, so nested problems **Context.** Only the first level is shown by default, so nested problems
would be invisible. would be invisible.
**Decision.** Collapsed parents show `·k` (issues in all descendants), colored **Decision.** Collapsed parents show `·k` (issues in all descendants), one
by the worst one. Problems in a child do **not** mark the parent itself as count per level: red for danger, then yellow for warn (amended: a single
`changed`. count in the worst color made 1 error + 1 warning look like 2 errors; the
status column grew from 14 to 16 for the second count). Problems in a child
do **not** mark the parent itself as `changed`.
## D8 — Collapsed by default, branch-first navigation ## D8 — Collapsed by default, branch-first navigation
+2 -1
View File
@@ -34,7 +34,8 @@ commit hashes. It doesn't answer the questions users actually have:
- **F5** A collapsed submodule shows the number of its **direct** children, - **F5** A collapsed submodule shows the number of its **direct** children,
e.g. `[2]`. e.g. `[2]`.
- **F6** A collapsed submodule shows the number of issues **hidden** in all - **F6** A collapsed submodule shows the number of issues **hidden** in all
its descendants (`·2`), colored by the most severe hidden issue. its descendants (`·1 ·2`), one count per level: errors in red, then
warnings in yellow.
- **F7** Expansion state of descendants is remembered when a parent is - **F7** Expansion state of descendants is remembered when a parent is
collapsed and restored when it is expanded again (except after collapsed and restored when it is expanded again (except after
"collapse all", which resets it). "collapse all", which resets it).
+3 -2
View File
@@ -96,8 +96,9 @@ Rules:
### Hidden issues ### Hidden issues
`Submodule.HiddenIssues()` counts the issues of **all** loaded descendants `Submodule.HiddenIssues()` counts the issues of **all** loaded descendants
and returns the most severe level. A collapsed submodule shows this as `·k` per level (`Hidden{Warn, Danger}`). A collapsed submodule shows them as `·k`
in that level's color, so a danger can never hide inside a collapsed subtree. per level, red first, then yellow, so a danger can never hide inside a
collapsed subtree and a warning is never mistaken for one.
A descendant whose load failed (`error`) counts as one danger. A descendant whose load failed (`error`) counts as one danger.
## Resolving a pin to a ref ## Resolving a pin to a ref
+1 -1
View File
@@ -47,7 +47,7 @@ indent ▸ name [n] status ref delta
| indent | 2 per depth | | | indent | 2 per depth | |
| toggle | 3 | `▸` collapsed, `▾` expanded, blank for leaves | | toggle | 3 | `▸` collapsed, `▾` expanded, blank for leaves |
| name | fits the widest visible name | name, `[n]` = direct children (collapsed only) | | name | fits the widest visible name | name, `[n]` = direct children (collapsed only) |
| status | 14 | issue word, `+n`, `·k` hidden issues; muted `in sync` without issues; spinner while loading | | status | 16 | issue word, `+n`, `·k` hidden issues per level (red, then yellow); muted `in sync` without issues; spinner while loading |
| ref | widest ref, max 22 | resolved ref; muted short SHA if unresolved | | ref | widest ref, max 22 | resolved ref; muted short SHA if unresolved |
| delta | 6 | `↑n`, `↓n`, `↑n↓m` | | delta | 6 | `↑n`, `↓n`, `↑n↓m` |
+19 -9
View File
@@ -60,21 +60,31 @@ func (s *Status) Issues() []Issue {
return out return out
} }
// HiddenIssues counts the issues of all loaded descendants and returns the // Hidden counts issues below a submodule per level.
// most severe level among them. A descendant whose load failed counts as one type Hidden struct{ Warn, Danger int }
// danger, since its state is unknown.
func (m *Submodule) HiddenIssues() (count int, worst Level) { // Total is the number of issues of all levels.
func (h Hidden) Total() int { return h.Warn + h.Danger }
// HiddenIssues counts the issues of all loaded descendants per level. A
// descendant whose load failed counts as one danger, since its state is
// unknown.
func (m *Submodule) HiddenIssues() Hidden {
var h Hidden
Walk(m.Children, func(c *Submodule) { Walk(m.Children, func(c *Submodule) {
if c.Status == nil && c.Err != nil { if c.Status == nil && c.Err != nil {
count++ h.Danger++
worst = max(worst, LevelDanger)
} }
for _, is := range c.Status.Issues() { for _, is := range c.Status.Issues() {
count++ switch is.Level {
worst = max(worst, is.Level) case LevelDanger:
h.Danger++
case LevelWarn:
h.Warn++
}
} }
}) })
return count, worst return h
} }
// Walk calls fn for every submodule in depth-first order. // Walk calls fn for every submodule in depth-first order.
+4 -5
View File
@@ -46,9 +46,8 @@ func TestHiddenIssues(t *testing.T) {
loading := &Submodule{} loading := &Submodule{}
root := &Submodule{Status: ok, Children: []*Submodule{child, {Status: ok}, loading}} root := &Submodule{Status: ok, Children: []*Submodule{child, {Status: ok}, loading}}
count, worst := root.HiddenIssues() if got := root.HiddenIssues(); got != (Hidden{Warn: 1, Danger: 1}) {
if count != 2 || worst != LevelDanger { t.Errorf("HiddenIssues() = %+v; want 1 warn, 1 danger", got)
t.Errorf("HiddenIssues() = %d, %v; want 2, LevelDanger", count, worst)
} }
} }
@@ -75,7 +74,7 @@ func TestHiddenIssuesCountLoadErrors(t *testing.T) {
bad := &Submodule{Err: errors.New("boom")} bad := &Submodule{Err: errors.New("boom")}
loading := &Submodule{} loading := &Submodule{}
parent := &Submodule{Children: []*Submodule{bad, loading}} parent := &Submodule{Children: []*Submodule{bad, loading}}
if n, w := parent.HiddenIssues(); n != 1 || w != LevelDanger { if got := parent.HiddenIssues(); got != (Hidden{Danger: 1}) {
t.Errorf("got %d, %v; want 1, danger", n, w) t.Errorf("got %+v; want 1 danger", got)
} }
} }
+10 -2
View File
@@ -138,8 +138,16 @@ func (m Model) lines(s *submodule.Submodule) []string {
out = append(out, "") out = append(out, "")
if n := len(s.Children); n > 0 { if n := len(s.Children); n > 0 {
children := fmt.Sprintf("%d direct", n) children := fmt.Sprintf("%d direct", n)
if k, worst := s.HiddenIssues(); k > 0 { h := s.HiddenIssues()
children += t.LevelStyle(worst, lipgloss.NewStyle()).Render(fmt.Sprintf(" · %d issue(s) below", k)) var parts []string
if h.Danger > 0 {
parts = append(parts, t.LevelStyle(submodule.LevelDanger, lipgloss.NewStyle()).Render(fmt.Sprintf("%d error(s)", h.Danger)))
}
if h.Warn > 0 {
parts = append(parts, t.LevelStyle(submodule.LevelWarn, lipgloss.NewStyle()).Render(fmt.Sprintf("%d warning(s)", h.Warn)))
}
if len(parts) > 0 {
children += muted.Render(" · ") + strings.Join(parts, muted.Render(", ")) + muted.Render(" below")
} }
out = append(out, label("Children")+children) out = append(out, label("Children")+children)
} }
+13 -5
View File
@@ -15,7 +15,7 @@ import (
) )
const ( const (
statusWidth = 14 // "unpushed +1 ·2" statusWidth = 16 // "unpushed +1 ·2 ·3"
deltaWidth = 6 deltaWidth = 6
maxRefWidth = 22 maxRefWidth = 22
) )
@@ -414,11 +414,19 @@ func (m Model) nodeLine(r row, nameColW, refW int) string {
default: default:
status = seg(t.Muted, theme.InSync) status = seg(t.Muted, theme.InSync)
} }
if k, worst := s.HiddenIssues(); collapsed && k > 0 { if h := s.HiddenIssues(); collapsed && h.Total() > 0 {
if status != "" { for _, c := range []struct {
status += pad(1) level submodule.Level
n int
}{{submodule.LevelDanger, h.Danger}, {submodule.LevelWarn, h.Warn}} {
if c.n == 0 {
continue
}
if status != "" {
status += pad(1)
}
status += t.LevelStyle(c.level, base).Render(fmt.Sprintf("%s%d", t.Glyphs.Problems, c.n))
} }
status += t.LevelStyle(worst, base).Render(fmt.Sprintf("%s%d", t.Glyphs.Problems, k))
} }
status = ansi.Truncate(status, statusWidth, "") status = ansi.Truncate(status, statusWidth, "")
status += pad(statusWidth - lipgloss.Width(status)) status += pad(statusWidth - lipgloss.Width(status))