Fetch in the background and follow changes in the repositories

- On startup, after the local state is shown, fetch the superproject and
  every initialized submodule (all remotes, prune, at most 8 at once).
  Fetches never prompt; failures show git's message. --no-fetch skips it,
  f fetches again.
- The header shows "fetching n/m", then the fetch age and failures; the
  detail pane shows each submodule's last fetch and fetch errors.
- The git provider polls each repository's metadata (refs, HEAD, index,
  .gitmodules) every 2 s and reports changes. The app reloads only what
  changed, keeping old values, expansion and cursor; loads are
  single-flight with coalesced reruns. Refresh is driven by the
  repositories, so a fetch in another shell updates the view like ours.
- r reloads everything (worktree edits are not watched).
- Docs: F50-F53, N1/N8 rewritten, D18-D20 (D12 superseded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Simon Franken
2026-10-21 16:15:22 +02:00
co-authored by Claude Opus 5.5
parent 19c6f17eb2
commit 27f0c0b5d2
27 changed files with 1097 additions and 90 deletions
+19 -4
View File
@@ -14,6 +14,7 @@ import (
"slices"
"strings"
"sync"
"time"
"submarine/internal/submodule"
)
@@ -27,8 +28,13 @@ const maxBetween = 50
// Provider reads a superproject and its submodules with git.
type Provider struct {
root string // absolute superproject root
sem chan struct{} // bounds concurrent Status calls
root string // absolute superproject root
sem chan struct{} // bounds concurrent Status calls
fetchSem chan struct{} // bounds concurrent fetches
sshEnv []string // extra environment for fetches
// WatchInterval is how often Watch checks for changes.
WatchInterval time.Duration
mu sync.RWMutex
mods map[string]info // by Submodule.Path; filled by Tree
@@ -49,8 +55,11 @@ func New(dir string) (*Provider, error) {
return nil, fmt.Errorf("%s is not a git repository", dir)
}
return &Provider{
root: root,
sem: make(chan struct{}, runtime.NumCPU()),
root: root,
sem: make(chan struct{}, runtime.NumCPU()),
fetchSem: make(chan struct{}, maxFetches),
sshEnv: sshBatchEnv(root),
WatchInterval: 2 * time.Second,
}, nil
}
@@ -168,6 +177,12 @@ func status(ctx context.Context, in info) (*submodule.Status, error) {
return st, nil
}
if gitDir, _, err := gitDirs(in.dir); err == nil {
if fi, err := os.Stat(filepath.Join(gitDir, "FETCH_HEAD")); err == nil {
st.FetchedAt = fi.ModTime()
}
}
// checkout and worktree
head, err := run(ctx, in.dir, "rev-parse", "HEAD")
if err != nil {