Archived
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c84f34ee2b
commit
64fe34b5b0
+8
-7
@@ -17,13 +17,14 @@ bare commit hash.
|
||||
|
||||
## Status
|
||||
|
||||
**Prototype.** The UI is complete for browsing and runs on mock data
|
||||
(`internal/submodule/mock`). There is no git backend yet, and actions
|
||||
(`u`, `b`, `f`, `/`, `!`) only show a "not implemented" message.
|
||||
**Prototype.** The UI is complete for browsing. It reads real repositories
|
||||
through a read-only git provider (`internal/submodule/git`):
|
||||
`submarine [dir]`. `submarine --mock` shows the built-in mock data
|
||||
(`internal/submodule/mock`). Actions (`u`, `b`, `f`, `/`, `!`) only show a
|
||||
"not implemented" message.
|
||||
|
||||
Next steps, roughly in order:
|
||||
|
||||
1. Git provider implementing `submodule.Provider` (read-only).
|
||||
2. Actions: fetch, checkout pin (`submodule update`), bump pin to tip, init.
|
||||
3. Filter (`/`) and "problems only" (`!`) views.
|
||||
4. Refresh after actions and on demand.
|
||||
1. Actions: fetch, checkout pin (`submodule update`), bump pin to tip, init.
|
||||
2. Filter (`/`) and "problems only" (`!`) views.
|
||||
3. Refresh after actions and on demand.
|
||||
|
||||
+27
-2
@@ -8,11 +8,15 @@ Go, [Bubble Tea](https://github.com/charmbracelet/bubbletea) (v1),
|
||||
## Package layout
|
||||
|
||||
```
|
||||
cmd/submarine/main.go wiring: choose provider (--mock), start the program
|
||||
cmd/submarine/main.go wiring: choose provider (git or --mock), start the program
|
||||
internal/submodule/ domain model; NO UI imports
|
||||
model.go Repo, Submodule, Status, PinInfo, Commit, enums
|
||||
status.go Issues(), HiddenIssues(), Walk()
|
||||
provider.go Provider interface
|
||||
internal/submodule/git/ read-only provider using the git CLI
|
||||
exec.go git runner (environment, errors)
|
||||
parse.go pure parsers for git output
|
||||
provider.go Tree, Status, pin → ref resolution
|
||||
internal/submodule/mock/ fixture provider with artificial latency
|
||||
internal/tui/ Bubble Tea application
|
||||
app.go root model: layout, message routing, loading
|
||||
@@ -52,6 +56,23 @@ type Provider interface {
|
||||
The mock provider (`internal/submodule/mock`) covers every state and sleeps
|
||||
100–800 ms per `Status` call to exercise the loading path.
|
||||
|
||||
### Git provider
|
||||
|
||||
`internal/submodule/git` shells out to `git -C <dir> …` (see D14).
|
||||
|
||||
- Environment: `GIT_TERMINAL_PROMPT=0`, `GIT_OPTIONAL_LOCKS=0` (reads never
|
||||
take `index.lock`), `LC_ALL=C`. No command touches the network.
|
||||
- `Tree` reads each repository's `.gitmodules` (`git config -f
|
||||
.gitmodules`) and recurses into initialized submodules. It records, per
|
||||
path, the parent repository and the `.gitmodules` branch for `Status`.
|
||||
`Name` is the last element of `RelPath`.
|
||||
- `Status` runs about ten small git commands per submodule (see
|
||||
[state-model.md](state-model.md#git-commands)). At most `runtime.NumCPU()`
|
||||
`Status` calls run at once; the others wait on a semaphore and still
|
||||
respect `ctx` (D16).
|
||||
- Superproject: branch from `symbolic-ref`, ahead/behind against
|
||||
`@{upstream}`, "fetched" from the mtime of `FETCH_HEAD`.
|
||||
|
||||
## Loading flow
|
||||
|
||||
1. `Init` runs `Tree()` → `treeLoadedMsg`.
|
||||
@@ -91,14 +112,18 @@ go test ./...
|
||||
```
|
||||
|
||||
- `internal/submodule`: issue classification and hidden-issue aggregation.
|
||||
- `internal/submodule/git`: parser unit tests, and integration tests
|
||||
against a fixture repository built by `scripts/make-fixture.sh` (skipped
|
||||
if `git` is not installed).
|
||||
- `internal/tui/tree`: navigation rules. Tests need `zone.NewGlobal()` (see
|
||||
`TestMain`).
|
||||
|
||||
Manual check in a headless terminal:
|
||||
|
||||
```sh
|
||||
scripts/make-fixture.sh /tmp/subfix # superproject with every state
|
||||
go build -o /tmp/submarine ./cmd/submarine
|
||||
tmux new-session -d -s sub -x 130 -y 24 /tmp/submarine
|
||||
tmux new-session -d -s sub -x 130 -y 24 "/tmp/submarine /tmp/subfix/super"
|
||||
tmux send-keys -t sub Down Right
|
||||
tmux capture-pane -pt sub # add -e to see colors
|
||||
tmux kill-session -t sub
|
||||
|
||||
@@ -99,3 +99,32 @@ explicit action (`f`).
|
||||
|
||||
**Decision.** Keep mouse support (click to select/toggle, wheel to move)
|
||||
using bubblezone markers per row.
|
||||
|
||||
## D14 — Git provider shells out to the git CLI
|
||||
|
||||
**Context.** The provider can use a Go library (go-git) or the `git` binary.
|
||||
**Decision.** Run `git -C <dir> …` with a fixed environment
|
||||
(`GIT_TERMINAL_PROMPT=0`, `GIT_OPTIONAL_LOCKS=0`, `LC_ALL=C`) and parse
|
||||
stable output formats (`--porcelain=v2`, custom `--format`).
|
||||
**Consequences.** Behaves exactly like the user's git (config, `gitdir:`
|
||||
files, worktrees, partial clones), no new dependency, fast on large repos.
|
||||
Requires `git` on `PATH`; one process per query.
|
||||
|
||||
## D15 — Git provider semantics
|
||||
|
||||
**Decision.** The pin is the **index** gitlink, because that is what the
|
||||
next commit records and what `submodule update` checks out; a difference to
|
||||
HEAD is `PinStaged`. Worktree state uses `--ignore-submodules=dirty`, so dirt
|
||||
in a nested submodule doesn't mark its parent (consistent with D7). A pin
|
||||
whose commit isn't present locally is a loading `error`, not `unpushed`,
|
||||
because without the commit nothing can be said about it.
|
||||
`--mock` is no longer the default; `submarine [dir]` reads a real repository.
|
||||
|
||||
## D16 — Bounded concurrency inside the provider
|
||||
|
||||
**Context.** The app starts one `Status` command per submodule at once
|
||||
(D11); with hundreds of submodules that would spawn thousands of git
|
||||
processes.
|
||||
**Decision.** The git provider limits concurrent `Status` calls to
|
||||
`runtime.NumCPU()` with a semaphore that respects `ctx`.
|
||||
**Consequences.** The UI stays unchanged; rows fill in steadily.
|
||||
|
||||
+11
-2
@@ -88,6 +88,13 @@ commit hashes. It doesn't answer the questions users actually have:
|
||||
| `/` | Filter by name/path |
|
||||
| `!` | Show only submodules with issues |
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
submarine [dir] # the repository containing dir (default: current directory)
|
||||
submarine --mock # built-in mock data
|
||||
```
|
||||
|
||||
## Non-functional requirements
|
||||
|
||||
- **N1** Startup must not block on the network. The tool works from local
|
||||
@@ -100,5 +107,7 @@ commit hashes. It doesn't answer the questions users actually have:
|
||||
- **N5** Works without a Nerd Font. `SUBMARINE_ASCII=1` replaces 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: replacing the mock
|
||||
provider with a git provider requires no UI changes.
|
||||
- **N7** The UI is independent of the data source: the mock and the git
|
||||
provider are interchangeable without UI changes.
|
||||
- **N8** The git provider is read-only and never uses the network; it uses
|
||||
the `git` command line, so the user's git configuration applies.
|
||||
|
||||
+33
-1
@@ -9,7 +9,7 @@ them.
|
||||
|
||||
| Commit | Source |
|
||||
|---|---|
|
||||
| **Pinned** | The gitlink recorded in the superproject (HEAD or index) |
|
||||
| **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` |
|
||||
|
||||
@@ -97,6 +97,38 @@ Distances: `git rev-list --left-right --count <pin>...<ref>` gives
|
||||
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 |
|
||||
|
||||
+3
-1
@@ -72,7 +72,9 @@ indent ▸ name [n] status ref delta
|
||||
`path branch ↑↓ · n submodules · <count> <issue>… · n pin staged · n loading · fetched <age>`
|
||||
|
||||
Issue counts are ordered by severity (unpushed, conflict, missing, off pin,
|
||||
changed), each in its level color. Zero counts are omitted.
|
||||
changed), each in its level color. Zero counts are omitted. `<age>` is
|
||||
`never` if the superproject has never been fetched; a detached superproject
|
||||
shows `(detached)` as its branch.
|
||||
|
||||
## Colors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user