From a285a35c95b504e630648b2658887cc47e8be0d8 Mon Sep 17 00:00:00 2001 From: Simon Franken Date: Thu, 22 Oct 2026 12:13:01 +0200 Subject: [PATCH] Fetch only on request, remove --no-fetch Nothing is fetched on startup any more; the header and detail pane show the last fetch and f fetches everything. Records the change as D22. Co-Authored-By: Claude Sonnet 5.5 --- AGENTS.md | 1 - README.md | 6 +++--- cmd/submarine/main.go | 9 ++++----- docs/README.md | 3 +-- docs/architecture.md | 8 ++++---- docs/decisions.md | 19 ++++++++++++++++++- docs/requirements.md | 10 +++++----- internal/tui/app.go | 12 +----------- 8 files changed, 36 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c290b6..073092e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,6 @@ and decisions. Read the relevant document before changing behavior: ```sh go run ./cmd/submarine [dir] # run on a repository (default: cwd) go run ./cmd/submarine --mock # run on mock data -go run ./cmd/submarine --no-fetch [dir] # without background fetch scripts/make-fixture.sh DIR # build a test superproject in DIR make dist # cross-compile into dist/ GITEA_TOKEN=… make release # publish a tagged release (outward-facing) diff --git a/README.md b/README.md index aba8a5e..2426c58 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ the questions you actually have: (`·1`) and shows `●` if a staged pin is hidden. - **Words, not symbols.** Every state is a short word (`unpushed`, `conflict`, `missing`, `off pin`, `changed`), so nothing depends on color alone. -- **Live.** Fetches in the background on startup and follows changes you make - in other shells within about two seconds. +- **Live.** Follows changes you make in other shells within about two + seconds. It never fetches on its own: the header shows when you last did, + `f` fetches everything. - **Built for dozens to hundreds of submodules.** Each status loads asynchronously, so the UI never freezes. @@ -44,7 +45,6 @@ Update later with `sub update`. ```sh sub # the repository in the current directory sub ~/code/project # any directory inside a repository -sub --no-fetch # skip the background fetch sub --mock # built-in sample data, no repository needed ``` diff --git a/cmd/submarine/main.go b/cmd/submarine/main.go index 4c9c211..c20d7dd 100644 --- a/cmd/submarine/main.go +++ b/cmd/submarine/main.go @@ -35,9 +35,8 @@ func main() { } useMock := flag.Bool("mock", false, "use built-in mock data instead of a real repository") - noFetch := flag.Bool("no-fetch", false, "don't fetch remotes in the background on startup") flag.Usage = func() { - fmt.Fprintln(flag.CommandLine.Output(), "usage: submarine [--mock] [--no-fetch] [dir]\n submarine update [--check] [--force]\n submarine version") + fmt.Fprintln(flag.CommandLine.Output(), "usage: submarine [--mock] [dir]\n submarine update [--check] [--force]\n submarine version") flag.PrintDefaults() } flag.Parse() @@ -72,9 +71,9 @@ func main() { zone.NewGlobal() defer zone.Close() - notice := checkForUpdate(ctx, *noFetch || *useMock) + notice := checkForUpdate(ctx, *useMock) - p := tea.NewProgram(tui.New(ctx, provider, tui.Options{Fetch: !*noFetch}), tea.WithAltScreen(), tea.WithMouseCellMotion()) + p := tea.NewProgram(tui.New(ctx, provider), tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -90,7 +89,7 @@ func main() { // checkForUpdate looks for a newer release in the background (at most once a // day, cached) and delivers its version, or "" when there is none. Disabled -// for dev builds, --no-fetch, --mock and SUBMARINE_NO_UPDATE_CHECK. +// for dev builds, --mock and SUBMARINE_NO_UPDATE_CHECK. func checkForUpdate(ctx context.Context, off bool) <-chan string { out := make(chan string, 1) cache, err := os.UserCacheDir() diff --git a/docs/README.md b/docs/README.md index ce08103..ae4e1d3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,8 +31,7 @@ Installs `submarine` and the alias `sub` (linux/macOS, amd64/arm64) to **Prototype.** The UI is complete for browsing. It reads real repositories through a git provider (`internal/submodule/git`): `submarine [dir]`. It -fetches in the background on startup (`--no-fetch` to skip; `f` to fetch -again) and follows changes made in other shells. `submarine --mock` shows +fetches only when asked (`f`) and follows changes made in other shells. `submarine --mock` shows the built-in mock data (`internal/submodule/mock`). `!` shows only submodules with problems. The actions `u` and `b` only show a "not implemented" message ([drafts of the write actions](actions.md)). diff --git a/docs/architecture.md b/docs/architecture.md index b06ed84..06a4b94 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -119,9 +119,9 @@ The mock provider (`internal/submodule/mock`) covers every state and sleeps calling `Status()` → `statusLoadedMsg`. 3. Each result is stored on the submodule (`Status` or `Err`); the view updates incrementally, including parents' hidden-issue counts. -4. After the first tree: the watcher starts (`changeMsg`), and the - background fetch starts one `tea.Cmd` per fetchable repository - (`fetchDoneMsg`). +4. After the first tree: the watcher starts (`changeMsg`). Nothing is + fetched until the user presses `f`, which starts one `tea.Cmd` per + fetchable repository (`fetchDoneMsg`). 5. The spinner ticks only while something loads or fetches. ### Fetch and refresh are decoupled (D20) @@ -197,7 +197,7 @@ No CI runner: releases are built and published from a developer machine. so `sub` works too). `SUBMARINE_UPDATE_URL` overrides the registry. - After the TUI exits, a one-line notice is printed if a newer release exists. The check runs in the background at startup, at most once a day - (cached in the user cache dir) and is off for dev builds, `--no-fetch`, + (cached in the user cache dir) and is off for dev builds, `--mock` and `SUBMARINE_NO_UPDATE_CHECK=1`. ## Testing diff --git a/docs/decisions.md b/docs/decisions.md index 2ef781a..2dd405b 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -150,7 +150,7 @@ removed + untracked. **Consequences.** One extra `git diff` per dirty submodule, none for clean ones. Not included: staged/unstaged markers, diff bodies, stash count. -## D18 — Background fetch on startup +## D18 — Background fetch on startup (superseded by D22) **Context.** Without fetching, every answer is only as fresh as the user's last `git fetch`, which they rarely run in every submodule. @@ -296,3 +296,20 @@ count as problems because their state is unknown (D7 counts `error` as danger too); the cost is that rows vanish as statuses arrive, so the selection is refitted after each status. Staged pins are not shown by it, since they are not issues. + +## D22 — Fetch only on request + +**Context.** D18 fetched every repository on startup. With hundreds of +submodules that is hundreds of network round trips (and credential or +host-key failures) every time the tool opens, whether or not the user wants +fresh remote data. +**Decision.** Supersedes D18. Nothing is fetched on startup. The header and +the detail pane show the last fetch (`FETCH_HEAD`, also from fetches made in +other shells), so the user can judge how stale the remote refs are and press +`f` to fetch. The `--no-fetch` flag is removed with it (it still had the +side effect of switching off the update check; that has +`SUBMARINE_NO_UPDATE_CHECK`). +**Consequences.** Right after startup, `unpushed`, `↓n` and the like are as +fresh as the last fetch. `fetched never` appears for a repository that was +never fetched. The progress display and per-submodule fetch errors stay as +they are, they now belong to `f`. diff --git a/docs/requirements.md b/docs/requirements.md index 9015387..7742365 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -100,9 +100,10 @@ commit hashes. It doesn't answer the questions users actually have: ### Fetch and refresh -- **F50** On startup, after the local state is shown, the superproject and - every initialized submodule are fetched in the background (all remotes, - with prune). `--no-fetch` turns this off. +- **F50** Nothing is fetched on startup. The header and the detail pane show + when the repositories were last fetched (`FETCH_HEAD`), whoever did it. + `f` fetches 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 @@ -141,13 +142,12 @@ See [actions.md](actions.md) for the draft of the write actions. ```sh submarine [dir] # the repository containing dir (default: current directory) submarine --mock # built-in mock data -submarine --no-fetch [dir] # no background fetch on startup ``` ## Non-functional requirements - **N1** Startup must not block on the network. The tool shows the local - state at once and fetches in the background (F50). + 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 diff --git a/internal/tui/app.go b/internal/tui/app.go index 7205cd3..22d5339 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -29,17 +29,11 @@ const ( detailWidthRatio = 0.4 ) -// Options configure the application. -type Options struct { - Fetch bool // fetch all repositories in the background on startup -} - type Model struct { ctx context.Context provider submodule.Provider fetcher submodule.Fetcher // nil if the provider can't fetch watcher submodule.Watcher // nil if the provider can't watch - opts Options theme theme.Theme keys keyMap @@ -66,7 +60,7 @@ type Model struct { showDetail bool } -func New(ctx context.Context, p submodule.Provider, opts Options) Model { +func New(ctx context.Context, p submodule.Provider) Model { t := theme.Default() k := defaultKeys() sp := spinner.New(spinner.WithSpinner(spinner.MiniDot)) @@ -79,7 +73,6 @@ func New(ctx context.Context, p submodule.Provider, opts Options) Model { m := Model{ ctx: ctx, provider: p, - opts: opts, theme: t, keys: k, loads: newReloader(), @@ -289,9 +282,6 @@ func (m Model) treeLoaded(msg treeLoadedMsg) (tea.Model, tea.Cmd) { m.changes = m.watcher.Watch(m.ctx) cmds = append(cmds, m.waitForChange()) } - if m.opts.Fetch { - cmds = append(cmds, m.startFetch()) - } } else { m.tree.ReplaceRoots(m.repo.Submodules) }