Archived
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:
co-authored by
Claude Opus 5.5
parent
19c6f17eb2
commit
27f0c0b5d2
@@ -0,0 +1,161 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"submarine/internal/submodule"
|
||||
)
|
||||
|
||||
// ownFixture builds a fixture the test may modify. It returns the provider
|
||||
// (after Tree) and the fixture root (superproject in root/super, work
|
||||
// clones of the remotes in root/work).
|
||||
func ownFixture(t *testing.T) (*Provider, string) {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
root := filepath.Join(t.TempDir(), "fx")
|
||||
if err := makeFixture(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, err := New(filepath.Join(root, "super"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Tree(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p, root
|
||||
}
|
||||
|
||||
func git(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
args = append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@t"}, args...)
|
||||
if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// pushUpstream adds a commit to a fixture remote, as if someone else pushed.
|
||||
func pushUpstream(t *testing.T, root, remote string) {
|
||||
t.Helper()
|
||||
work := filepath.Join(root, "work", remote)
|
||||
git(t, work, "fetch", "-q")
|
||||
git(t, work, "reset", "-q", "--hard", "origin/main")
|
||||
git(t, work, "commit", "--allow-empty", "-m", "Upstream change")
|
||||
git(t, work, "push", "-q", "origin", "main")
|
||||
}
|
||||
|
||||
func TestFetch(t *testing.T) {
|
||||
p, root := ownFixture(t)
|
||||
ctx := context.Background()
|
||||
|
||||
before, err := p.Status(ctx, "src/core")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pushUpstream(t, root, "core")
|
||||
if err := p.Fetch(ctx, "src/core"); err != nil {
|
||||
t.Fatalf("Fetch: %v", err)
|
||||
}
|
||||
after, err := p.Status(ctx, "src/core")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Pin.Behind <= before.Pin.Behind {
|
||||
t.Errorf("after fetch: ↓%d, want more than ↓%d", after.Pin.Behind, before.Pin.Behind)
|
||||
}
|
||||
if after.FetchedAt.IsZero() {
|
||||
t.Error("after fetch: FetchedAt is zero")
|
||||
}
|
||||
|
||||
if err := p.Fetch(ctx, ""); err != nil {
|
||||
t.Errorf("Fetch(superproject without remotes): %v", err)
|
||||
}
|
||||
if p.Fetchable("libs/missing") {
|
||||
t.Error("Fetchable(libs/missing) = true for an uninitialized submodule")
|
||||
}
|
||||
if err := p.Fetch(ctx, "libs/missing"); err == nil {
|
||||
t.Error("Fetch(libs/missing): want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchError(t *testing.T) {
|
||||
p, root := ownFixture(t)
|
||||
git(t, filepath.Join(root, "super", "src", "ui"), "remote", "set-url", "origin", filepath.Join(root, "nowhere.git"))
|
||||
|
||||
err := p.Fetch(context.Background(), "src/ui")
|
||||
if err == nil {
|
||||
t.Fatal("Fetch with a bad remote: want error")
|
||||
}
|
||||
if strings.Contains(err.Error(), "\n") || strings.HasPrefix(err.Error(), "git ") {
|
||||
t.Errorf("error should be git's first line only, got %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
// next waits for a change or fails after timeout.
|
||||
func next(t *testing.T, ch <-chan submodule.Change) submodule.Change {
|
||||
t.Helper()
|
||||
select {
|
||||
case c := <-ch:
|
||||
return c
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("no change reported")
|
||||
return submodule.Change{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
p, root := ownFixture(t)
|
||||
super := filepath.Join(root, "super")
|
||||
p.WatchInterval = 20 * time.Millisecond
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ch := p.Watch(ctx)
|
||||
|
||||
// our own reads must not look like changes
|
||||
for _, path := range []string{"src/core", "libs/work", "libs/conflict"} {
|
||||
if _, err := p.Status(ctx, path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case c := <-ch:
|
||||
t.Fatalf("change while idle: %+v", c)
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
|
||||
// a fetch in another shell
|
||||
pushUpstream(t, root, "net")
|
||||
git(t, filepath.Join(super, "src", "net"), "fetch", "-q")
|
||||
if c := next(t, ch); c != (submodule.Change{Path: "src/net"}) {
|
||||
t.Errorf("after fetch: %+v, want remote change of src/net", c)
|
||||
}
|
||||
|
||||
// a commit in another shell
|
||||
git(t, filepath.Join(super, "libs", "store"), "commit", "-q", "--allow-empty", "-m", "Local")
|
||||
if c := next(t, ch); c != (submodule.Change{Path: "libs/store", Local: true}) {
|
||||
t.Errorf("after commit: %+v, want local change of libs/store", c)
|
||||
}
|
||||
|
||||
cancel()
|
||||
for range ch { // closed after cancel
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitDirs(t *testing.T) {
|
||||
p, _ := loadFixture(t)
|
||||
gd, cd, err := gitDirs(p.root)
|
||||
if err != nil || gd != filepath.Join(p.root, ".git") || cd != gd {
|
||||
t.Errorf("superproject: %q, %q, %v", gd, cd, err)
|
||||
}
|
||||
gd, cd, err = gitDirs(filepath.Join(p.root, "src", "core"))
|
||||
if err != nil || gd != filepath.Join(p.root, ".git", "modules", "src", "core") || cd != gd {
|
||||
t.Errorf("submodule: %q, %q, %v", gd, cd, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user