This repository has been archived on 2026-10-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
submarine/docs/state-model.md
T
Simon FrankenandClaude Opus 5.5 64fe34b5b0 Add read-only git provider
Implement submodule.Provider with the git CLI so submarine reads real
repositories: `submarine [dir]`, with `--mock` for the built-in data.

- Tree reads .gitmodules recursively; Status derives presence, checkout,
  worktree, staged pin, pushed state and pin -> ref resolution from local
  git commands only (no network).
- Concurrent Status calls are bounded by runtime.NumCPU().
- scripts/make-fixture.sh builds a superproject covering every state; the
  integration tests run against it.
- theme.Ago shows "never" for a zero time.
- Docs updated; decisions D14-D16 added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:32:21 +02:00

5.6 KiB

State model

Code: internal/submodule/model.go, internal/submodule/status.go.

Three commits per submodule

Every submodule involves three commits. All state is derived from comparing them.

Commit Source
Pinned The gitlink recorded in the superproject index (differs from HEAD → PinStaged)
Checked out The submodule's HEAD
Tip The tip of the ref the pin was resolved to, e.g. origin/main

This gives two independent questions:

  • Is the checkout on the pin? → off pin if not.
  • Where is the pin relative to its ref? → ↑n / ↓n / ↑n↓m.

Raw status dimensions

submodule.Status stores the raw, independent dimensions. A submodule can be in several at once.

Field Values
Presence PresenceOK, NotInitialized, DirMissing
Checkout CheckoutMatch, CheckoutDiffers, CheckoutDetached (on the pin, detached HEAD; informational)
Worktree WorktreeClean, WorktreeModified, WorktreeUntracked, WorktreeConflict
PinStaged Pin changed in the superproject index but not committed
Pin Ref, Source, Ahead, Behind, Pushed
Pinned, CheckedOut, Tip Commits (SHA, subject, author, time)
Between Commits that make up the pin ↔ tip difference

Status == nil means "still loading". Submodule.Err is set if loading failed.

Issues: summarizing the dimensions

The UI never shows raw dimensions in the tree. Status.Issues() summarizes them into issue kinds, each with a level. This is the single source of truth for what counts as a problem.

Issue (word) Combines Level
unpushed Pin not reachable from any remote ref Danger
conflict Merge conflict in the worktree Danger
missing Not initialized; directory missing Warn
off pin Checkout differs from pin Warn
changed Uncommitted changes; untracked files Warn

Rules:

  • If the submodule is missing, that is its only issue (nothing else can be determined).
  • Issues are returned most severe first: unpushed, conflict, off pin, changed.
  • Not issues (quiet): in sync, pin behind/ahead/diverged but pushed, staged pin, detached HEAD on the pin. They are shown as information only.
  • A loading error is shown as error (Danger) in the UI; it is not an issue kind.

Levels

Level Meaning Presentation
LevelQuiet Nothing to do Whole row muted, no word
LevelWarn Look at it Yellow word
LevelDanger Act now Red bold word

Hidden issues

Submodule.HiddenIssues() counts the issues of all loaded descendants and returns the most severe level. A collapsed submodule shows this as ·k in that level's color, so a danger can never hide inside a collapsed subtree.

Resolving a pin to a ref

The pin is displayed as a ref, not a hash. A commit can be on several branches, on none, or deep in the history, so the git provider resolves it by this order and records which rule applied in PinInfo.Source:

# Rule RefSource
1 branch = … in .gitmodules RefFromGitmodules
2 Branch currently checked out in the submodule RefFromCurrentBranch
3 Remote default branch, if it contains the pin RefFromDefaultBranch
4 Any remote branch containing the pin, preferring the closest tip (git branch -r --contains) RefFromContains
5 A tag (git describe) RefFromTag
6 Short SHA RefFromSHA

Distances: git rev-list --left-right --count <pin>...<ref> gives Ahead (left) and Behind (right).

Pushed is true if the pin is reachable from any remote ref. A pin that is ahead of its ref can still be pushed (e.g. on another branch); only Pushed == false is dangerous.

Git commands

How the git provider (internal/submodule/git) fills Status. All commands are local; <pin> is the index gitlink.

Field Command (in the parent repo / in the submodule)
Pinned, PinStaged parent: ls-files -s -- <path> vs ls-tree HEAD -- <path>
Presence directory missing → DirMissing; no .git in it → NotInitialized
Checkout rev-parse HEAD, symbolic-ref -q HEAD (no branch → detached)
Worktree status --porcelain=v2 --ignore-submodules=dirty
Pushed for-each-ref --contains <pin> refs/remotes is not empty
Pin.Ref rules above; the remote is origin, else the first remote
Ahead, Behind rev-list --left-right --count <pin>...<ref>
Between log <pin>...<ref>, at most 50 commits

Details:

  • Rule 1: branch = . means the superproject's current branch. The rule applies only if <remote>/<branch> exists locally.
  • Rule 2 uses the checked-out branch's upstream (<branch>@{upstream}).
  • Rule 4 compares at most 16 containing branches (oldest tip first) and picks the one with the fewest commits after the pin.
  • Rule 6 shows the short SHA; there is no tip and no distance.
  • --ignore-submodules=dirty: changes inside a nested submodule do not make its parent changed (D7), but a moved nested pin does.
  • A missing submodule only gets a Pinned SHA; nested submodules of an uninitialized submodule are unknown and not shown.
  • If the pinned commit doesn't exist in the submodule (not fetched), loading fails with "pinned commit … not fetched" (shown as error) instead of guessing unpushed.

Distance notation

Display Meaning
↓n The ref has n commits the pin doesn't have; the pin is behind
↑n The pin has n commits the ref doesn't have
↑n↓m Diverged

The arrows follow the convention of git status, lazygit and shell prompts.