X dropped staged pins without a question, including the selected submodule's own pin, which the loss check didn't count. A staged pin is a decision, and X sits one Shift away from x, so dropped staged pins now count as a loss and make X ask (D31, amends D30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
Requirements
Problem
In repositories with many submodules, git submodule status shows pinned
commit hashes. It doesn't answer the questions users actually have:
- Which branch is this submodule on, and is the pin current?
- Which submodules need my attention, and which are fine?
- Is anything dangerous, e.g. a pin that other people can't fetch?
Goals
- 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.
Functional requirements
Tree
- F1 Show all submodules of the superproject, including nested submodules of submodules, at any depth.
- F2 Group submodules by the directory of their path relative to the
parent repository. A directory with submodules is shown as a non-selectable
header line (e.g.
src/,modules/). Submodules at the repository root have no header. - F3 Nested submodules are shown under their parent submodule. A
directory header inside a submodule (e.g.
modules/) means the nested submodule lives in that subdirectory of the parent. - F4 On startup only the first level is shown; every submodule with nested submodules is collapsed.
- F5 A collapsed submodule shows the number of its direct children,
e.g.
[2]. - F6 A collapsed submodule shows the number of issues hidden in all
its descendants (
·1 ·2), one count per level: errors in red, then warnings in yellow. A staged pin below shows as a blue●before them. - F7 Expansion state of descendants is remembered when a parent is collapsed and restored when it is expanded again (except after "collapse all", which resets it).
State per submodule
- F10 Each submodule shows at most one state word: the most severe
issue, plus
+nif there are more. Submodules without issues show a mutedin sync, or a bluestagedif their pin change is staged. With issues, a staged pin adds a blue●after the word. See state-model.md for the full rules. - F11 Each submodule shows its pin as a ref and the distance to it
(
↑n,↓n,↑n↓m). The ref is the target branch (branch =of.gitmodules, else the remote's default branch) if the pin is on it, else the branch the pin is on that it is closest to; only without any a tag or a short SHA. - F11a A pin that is pushed but not on its target branch (for example
only on a feature branch) is
off target, an information (blue) naming the branch it is on. It does not depend on the superproject's branch. - F12 A pin that is not reachable from any remote ref ("unpushed") is the most prominent state.
- F13 States load asynchronously per submodule; a submodule shows a
spinner until its state is known. Loading errors are shown as
error.
Detail pane
-
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 the pin (and the staged pin, if there is one), the tip and the checkout (you) as one line of history, newest on top: a node per commit (SHA + subject) with its roles, and the number of commits between two nodes. Roles on the same commit share a node. A commit that has diverged from the pin is drawn as a fork that joins at the commit where the histories split (
base). A checkout that differs from the pin is highlighted. -
F22 (dropped) The commits between pin and tip are not listed; only their number is shown (F21).
-
F23 Shows child count, issues below, and the remote URL.
-
F24 For a submodule with uncommitted changes, previews the changed files so the user can remember what they were doing: a flat list with a colored letter per file (
Mmodified,Aadded,Dremoved,Rrenamed,?untracked,!conflict), line counts (+n −m) per file and in total, and how long ago each file was edited. Only a handful of files are 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 pin and you, a summary of all submodules instead of Children). See state-model.md.
Header and footer
- F30 The header shows the superproject path, its branch and 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 temporarily shows short messages (e.g. "copied ").
Navigation
- F40 Keyboard navigation as specified in ux-guide.md.
- F41 Mouse: click a row to select it, click
▸/▾to toggle, scroll wheel moves the selection.
Fetch and refresh
-
F50 Nothing is fetched on startup. The detail pane shows when the selected repository was last fetched (
FETCH_HEAD), whoever did it.ffetches the superproject and every initialized submodule in the background (all remotes, with prune). -
F51 While fetching, the header shows progress (
fetching 12/174) instead of the fetch age; afterwards the age and the number of failed fetches. The detail pane shows each submodule's last fetch and, if it failed, git's message. -
F52 The view follows changes in the repositories, no matter who made them: a fetch, commit, checkout or
git addin another shell shows up within about two seconds. Refreshing is driven by the repositories, not by the application's own fetch. -
F53
ffetches again;rreloads everything (e.g. after editing files, which is not watched). -
F54
/filters the tree by name/path (case-insensitive substring). A text input above the footer takes the term;esccloses 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 (the superproject if none is left). See Filter. -
F55
!toggles "problems only": the tree shows only submodules with an issue (F10) or an unknown state (loading,error), plus their ancestors, expanded. It combines with the filter (F54) and stays when the filter closes. A line above the footer saysproblems only, so a short list is not mistaken for a healthy repository. See Problems only.
Actions
-
F56
xrestores the selection (a submodule, or the superproject when the header is selected): its worktree becomes exactly what git records, and so do all submodules below it, recursively. Staged pins are kept at every depth. For a submodule with a staged pin it asks whether to restore to the staged or the committed pin (the latter unstages it). A restored submodule whose pin is a branch tip ends on that branch, not detached. It asks for confirmation only if something would be lost. See actions.md.Xis a hard restore: it goes to the committed pins at every depth, without the pin question, and drops all staged pins below the selection too. Dropped staged pins count as a loss, so it asks first. See actions.md. -
F57
bopens a popup in the middle of the screen with a search line and the local and remote branches of the selection (a submodule, or the superproject).↑/↓select,enterswitches,esccancels. For a submodule the branch tip becomes its staged pin and it is restored to it, with everything below; HEAD ends on the branch. It asks for confirmation only if something would be lost. See actions.md. -
F59
uupdates the pin of the selection to the tip of its ref, when the pin is only behind it (origin/main ↓2): a switch (F57) to that branch, with exactly the tip the row shows. For the superproject that is a fast-forward of its branch to its upstream (git pull --ff-only). Nothing is fetched; if the ref moved meanwhile, nothing happens. A local branch that is only behind is fast-forwarded, in a switch too. Otherwise the footer says why not. See actions.md.
Usage
submarine [dir] # the repository containing dir (default: current directory)
submarine --mock # built-in mock data
- F58 On every start the tool looks for a newer release in the
background (never blocking, silent on failure; off for dev builds,
--mockandSUBMARINE_NO_UPDATE_CHECK=1). If there is one, the footer showsq quit and update to vXright of help and quit, highlighted;qthen quits and installs the release, and plain quit isQ.
Non-functional requirements
- N1 Startup must not block on the network. The tool shows the local state at once and uses the network only when the user asks (F50).
- N2 Loading many submodules must not freeze the UI (async, per submodule).
- N3 Colors follow the user's terminal theme (ANSI 16 colors); the UI works on dark and light backgrounds.
- N4 Meaning never depends on color alone: every state has a word.
- N5 Works without a Nerd Font.
SUBMARINE_ASCII=1replaces the few remaining Unicode symbols with ASCII. - N6 Works in narrow terminals (detail pane hides below 90 columns).
- N7 The UI is independent of the data source: the mock and the git provider are interchangeable without UI changes.
- N8 The git provider only uses the network in
Fetchand, for objects that are missing, inRestoreandSwitch. It never prompts (no password, passphrase or host-key questions; those fail with git's message). It uses thegitcommand line, so the user's git configuration applies. Apart from fetching and the actions of actions.md, it is read-only: it writes only when the user asks.