Make the superproject selectable via the header

The header stands for the root repository: it is selected on startup,
shows the root's own state word, and the detail pane shows its state
like a submodule's (HEAD vs. upstream, changes, unpushed, leftover
submodule clones as untracked repositories). Submodules are ignored in
the root's changes; their rows show them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 11:54:40 +02:00
co-authored by Claude Sonnet 5.5
parent f9838c0c99
commit 72c060882b
18 changed files with 688 additions and 92 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
# submarine — documentation
submarine is a terminal UI for git repositories with many (recursively nested)
submodules. It shows **only the submodules**, as a collapsible tree, and
submodules. It shows **the submodules**, as a collapsible tree (plus the state
of the superproject itself in the header), and
describes each pin as a **branch plus distance** (`origin/main ↓1`) instead of a
bare commit hash.
+24
View File
@@ -258,3 +258,27 @@ one word, D3). It is not counted in `+n`, because `+n` and the header count
issues and a staged pin stays quiet (D4). A collapsed row shows `●` if a
staged pin is hidden below it (`Hidden.Staged`), placed before the `·k`
counts so truncation of the 16-column cell cuts the counts first.
## Superproject is selectable in the header
The state of the root repository was invisible: a submodule the current
branch no longer has stays on disk as an untracked repository, root files
can be modified, HEAD can be unpushed. The root becomes a node with an
empty path (the convention `Fetcher`/`Change` already use), selected on
startup, and the **header** is its row: it already holds the root's path,
branch and ahead/behind, and a tree row would cost a line and an indent
level for an item that is not part of the tree. Selecting the header shows
the usual detail pane; the header also shows the root's state word, so a
problem is visible while a submodule is selected.
- *Changes ignore submodules* (`--ignore-submodules=all`): off-pin and staged
pins already have their rows; counting them again would make the root
`changed` in most real repositories.
- *Unpushed stays Danger*: same issue kind and level as a submodule, one
rule for "work that exists only here" (D4).
- *The ref is the upstream*, not the closest branch: the header shows
ahead/behind of the upstream, and "up to date" means relative to the
tracked branch. A nearer feature branch would make the two disagree.
Without an upstream the usual resolution applies.
- The root is not in `Walk`, so counts, fetch and loading logic stay
submodule-only; the app tracks it by the path `""`.
+14 -4
View File
@@ -11,7 +11,8 @@ commit hashes. It doesn't answer the questions users actually have:
## Goals
- Show **only submodules** (not files), recursively nested, as a tree.
- Show **only submodules** (not files), recursively nested, as a tree; the
superproject itself is shown by the header, which is selectable.
- Describe pins as **ref + distance** instead of a commit hash.
- Let **problems stand out** and keep healthy submodules quiet.
- Work comfortably with **dozens to hundreds** of submodules.
@@ -56,7 +57,7 @@ commit hashes. It doesn't answer the questions users actually have:
### Detail pane
- **F20** Shows the selected submodule: full path, name, all issues in
- **F20** Shows the selected submodule (or the superproject): full path, name, all issues in
words, staged-pin and detached-HEAD notes.
- **F21** Shows the resolved ref, how it was resolved, and three commits:
**Pinned**, **Checkout** and **Tip** (SHA + subject). A checkout that
@@ -72,10 +73,19 @@ commit hashes. It doesn't answer the questions users actually have:
shown; the rest is summarized as "n more" with their kinds. Staged and
unstaged changes are combined.
- **F25** The superproject is selectable (the header; selected on startup)
and has a state of its own, so a stale checkout shows up: files changed
or untracked, untracked repositories (e.g. a submodule clone the current
branch no longer has), conflicts, and HEAD not pushed. Its HEAD is compared
with the upstream of the branch. Submodules do not count for it, their rows
show them. The detail pane shows it like a submodule (HEAD and Tip instead
of Pinned and Checkout, a summary of all submodules instead of Children).
See [state-model.md](state-model.md#superproject).
### Header and footer
- **F30** The header shows the superproject path, its branch and
ahead/behind, the number of submodules, a count per issue kind (most
ahead/behind, its own state word (**F25**), the number of submodules, a count per issue kind (most
severe first), staged pins, how many are still loading, and the time since
the last fetch.
- **F31** The footer shows key hints; `?` toggles the full help. It
@@ -108,7 +118,7 @@ commit hashes. It doesn't answer the questions users actually have:
A text input above the footer takes the term; `esc` closes it and removes
the filter. A selection that is filtered out moves to the next visible
submodule (none if nothing matches); closing the filter keeps the
selection. See [Filter](ux-guide.md#filter).
selection (the superproject if none is left). See [Filter](ux-guide.md#filter).
### Actions (planned, not implemented)
+18
View File
@@ -103,6 +103,24 @@ 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.
## Superproject
The superproject is a node with an empty `Path` (`Repo.Root`, no children;
`Submodule.IsRoot()`) and a `Status` with `Root` set. It reuses the pin
fields with **pin = HEAD**: `Pinned` and `CheckedOut` are HEAD, `Checkout`
is never `CheckoutDiffers` (`CheckoutDetached` for a detached HEAD),
`PinStaged` is false. `Issues()` is the same code; only `unpushed` says
"HEAD not pushed to any remote" (Danger, like a submodule's pin).
- **Ref** is the upstream of the current branch (`RefFromCurrentBranch`), so
ahead/behind agrees with the header. Closest-branch resolution is only the
fallback without an upstream (detached, or no upstream configured).
- **Changes** ignore submodules (`status --ignore-submodules=all`): an off-pin
or staged submodule is shown by its own row, not twice. A leftover
directory of a submodule the branch no longer has is an untracked
repository.
- `Walk` and the header counts do not include the root.
## Resolving a pin to a ref
The pin is displayed as a ref, not a hash. A commit can be on several
+19 -7
View File
@@ -75,6 +75,11 @@ indent ▸ name [n] status ref delta
### Detail pane
For the superproject (header selected), `Pinned` / `Checkout` become one
`HEAD` line, the commit list says "HEAD is n commits behind …", and
`Children` becomes `Modules` (count and issues of all submodules). See
[Superproject](state-model.md#superproject).
1. Directory (muted) and name (bold)
2. One line per issue: word (colored, 10 columns) + explanation;
`in sync nothing to do` if there are none and no pin is staged;
@@ -136,7 +141,13 @@ Changes 5 files · 2M 1A 1D 1R · +37 −12
- Untracked directories are shown as one entry (`tmp/`).
### Header
`path branch ↑↓ · n submodules · n error · <count> <issue>… · n pin staged · n loading · <fetch>`
The header is the superproject's row: it is selected on startup and drawn
with the selected background (bold) while selected. After the branch it
shows the superproject's own state word like a tree row (`in sync`,
`changed`, `unpushed +1`, spinner, `error`). The counts that follow are
about submodules only.
`path branch ↑↓ state · n submodules · n error · <count> <issue>… · n pin staged · n loading · <fetch>`
Issue counts are ordered by severity (unpushed, conflict, missing, off pin,
changed), each in its level color. Zero counts are omitted. `n loading`
@@ -195,13 +206,13 @@ colors in components.
| Key | Behavior |
|---|---|
| `↑` / `k`, `↓` / `j` | Previous / next submodule; directory headers are skipped |
| `↑` / `k`, `↓` / `j` | Previous / next submodule; directory headers are skipped. `↑` from the first submodule selects the header (superproject) |
| `space` | Toggle expand/collapse of the selected submodule |
| `→` / `l` | Collapsed with children: expand and select the first child. Expanded: select the first child. Leaf: nothing |
| `←` / `h` | Expanded: collapse it (stays selected). Otherwise: collapse the parent and select it. Top level: nothing |
| `→` / `l` | Header selected: first submodule. Collapsed with children: expand and select the first child. Expanded: select the first child. Leaf: nothing |
| `←` / `h` | Expanded: collapse it (stays selected). Otherwise: collapse the parent and select it. Top level: select the header (superproject) |
| `L` / `shift+→` / `ctrl+→` | Expand all; then follow the first child from the selection down to a submodule without children |
| `H` / `shift+←` / `ctrl+←` | Collapse all (resets remembered expansion); select the top-level ancestor |
| `home` / `g`, `end` / `G` | First / last submodule |
| `home` / `g`, `end` / `G` | Header (superproject) / last submodule |
| `pgup`, `pgdn` | Move by one screen |
| `?` | Toggle full help |
| `q`, `ctrl+c` | Quit |
@@ -235,6 +246,7 @@ match vanish. If nothing matches the tree says `no submodule matches`.
`/` edits the term again.
- **`esc`** closes the input and removes the filter, in both modes. The
selection stays; its ancestors are expanded so it is still visible.
- The superproject is never filtered out; if nothing matches it is selected.
- The filter line shows the keys that apply on its right.
- The header counts are not filtered.
@@ -252,7 +264,7 @@ where the palette can't be shown, see [Key bar and palette](#key-bar-and-palette
| `←` / `h`, `→` / `l` | Fold / unfold, parent / first child | Navigation | Bar, Help | yes |
| `H` / `⇧←`, `L` / `⇧→` | Collapse / expand all | Navigation | Bar, Help | yes |
| `space` | Toggle expand/collapse | Navigation | Bar, Help | yes |
| `home` / `g`, `end` / `G` | First / last submodule | Navigation | Help | yes |
| `home` / `g`, `end` / `G` | Header / last submodule | Navigation | Help | yes |
| `pgup`, `pgdn` | Move by one screen | Navigation | Help | yes |
| `c` | Copy path | Submodule | Palette, Help | yes |
| `u` | Checkout pin | Submodule | Help | no (flashes "not implemented") |
@@ -266,7 +278,7 @@ where the palette can't be shown, see [Key bar and palette](#key-bar-and-palette
### Mouse
- Click a row: select it.
- Click a row: select it. Click the header: select the superproject.
- Click `▸` / `▾`: select and toggle.
- Scroll wheel: move the selection.