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/internal/submodule/mock/mock.go
T
simonfrankenandClaude Sonnet 5.5 72c060882b Make the superproject selectable via the header
The header stands for the root repository: it is selected on startup,
shows the root's own state word, and the detail pane shows its state
like a submodule's (HEAD vs. upstream, changes, unpushed, leftover
submodule clones as untracked repositories). Submodules are ignored in
the root's changes; their rows show them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 11:54:40 +02:00

302 lines
10 KiB
Go

// Package mock provides a submodule.Provider backed by fixture data. It
// covers every state the UI can display and simulates loading latency.
package mock
import (
"context"
"errors"
"fmt"
"math/rand/v2"
"path"
"sync"
"time"
"submarine/internal/submodule"
)
// Provider serves a fixed fixture tree.
type Provider struct {
MinDelay, MaxDelay time.Duration
mu sync.RWMutex // Tree can run again (reload) while Status runs
statuses map[string]*submodule.Status
}
func New() *Provider {
return &Provider{
MinDelay: 100 * time.Millisecond,
MaxDelay: 800 * time.Millisecond,
}
}
type node struct {
relPath string
status *submodule.Status
children []node
}
func (p *Provider) Tree(ctx context.Context) (*submodule.Repo, error) {
p.mu.Lock()
defer p.mu.Unlock()
p.statuses = map[string]*submodule.Status{}
repo := &submodule.Repo{
Path: "~/coding/my_project",
Branch: "main",
Behind: 2,
FetchedAt: time.Now().Add(-2 * time.Minute),
}
repo.Root = &submodule.Submodule{Name: "my_project", URL: "git@github.com:acme/my_project.git"}
p.statuses[""] = rootStatus()
repo.Submodules = p.build(fixture(), nil)
return repo, nil
}
func (p *Provider) build(nodes []node, parent *submodule.Submodule) []*submodule.Submodule {
var out []*submodule.Submodule
for _, n := range nodes {
full := n.relPath
if parent != nil {
full = path.Join(parent.Path, n.relPath)
}
m := &submodule.Submodule{
Name: path.Base(n.relPath),
Path: full,
RelPath: n.relPath,
URL: fmt.Sprintf("git@github.com:acme/%s.git", path.Base(n.relPath)),
Parent: parent,
}
m.Children = p.build(n.children, m)
if n.status.Presence == submodule.PresenceOK && n.status.FetchedAt.IsZero() {
n.status.FetchedAt = now.Add(-2 * time.Minute)
}
p.statuses[full] = n.status
out = append(out, m)
}
return out
}
func (p *Provider) Status(ctx context.Context, path string) (*submodule.Status, error) {
if err := p.sleep(ctx, p.MinDelay, p.MaxDelay); err != nil {
return nil, err
}
p.mu.RLock()
s, ok := p.statuses[path]
p.mu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown submodule %q", path)
}
return s, nil
}
// Fetchable is true for the superproject and initialized submodules.
func (p *Provider) Fetchable(path string) bool {
if path == "" {
return true
}
p.mu.RLock()
defer p.mu.RUnlock()
s, ok := p.statuses[path]
return ok && s.Presence == submodule.PresenceOK
}
// Fetch pretends to fetch; one submodule always fails. It changes no data.
func (p *Provider) Fetch(ctx context.Context, path string) error {
if err := p.sleep(ctx, 300*time.Millisecond, 2*time.Second); err != nil {
return err
}
if path == "src/my_submodule_3" {
return errors.New("git@github.com: Permission denied (publickey).")
}
return nil
}
// sleep waits a random duration in [min, max) or until ctx is done.
func (p *Provider) sleep(ctx context.Context, min, max time.Duration) error {
d := min
if span := max - min; span > 0 {
d += rand.N(span)
}
select {
case <-time.After(d):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// --- fixture -----------------------------------------------------------------
var now = time.Now()
func commit(sha, subject, author string, ago time.Duration) *submodule.Commit {
return &submodule.Commit{SHA: sha, Subject: subject, Author: author, When: now.Add(-ago)}
}
func inSync(ref string, c *submodule.Commit) *submodule.Status {
return &submodule.Status{
Pin: submodule.PinInfo{Ref: ref, Pushed: true},
Pinned: c, CheckedOut: c, Tip: c,
}
}
// changes builds a Changes value like the git provider: sorted, with
// totals. The last file was edited age ago, earlier ones increasingly
// longer ago.
func changes(age time.Duration, files []submodule.FileChange) *submodule.Changes {
c := &submodule.Changes{Files: files, Total: len(files)}
for i := range files {
f := &files[i]
if f.Kind != submodule.ChangeRemoved {
n := time.Duration(len(files) - 1 - i)
f.Modified = time.Now().Add(-age - n*n*90*time.Minute)
}
c.Added += f.Added
c.Removed += f.Removed
}
c.Sort()
return c
}
// rootStatus is a superproject that is behind its upstream and still has a
// clone of a submodule the current branch no longer has.
func rootStatus() *submodule.Status {
tip := commit("e4f5a6b", "Move docs into a submodule", "anna", 3*time.Hour)
head := commit("0a1b2c3", "Update README", "ben", 2*24*time.Hour)
st := &submodule.Status{
Root: true,
Worktree: submodule.WorktreeModified,
Pin: submodule.PinInfo{Ref: "origin/main", Behind: 2, Pushed: true, Source: submodule.RefFromCurrentBranch},
Pinned: head,
CheckedOut: head,
Tip: tip,
Between: []submodule.Commit{
*tip,
*commit("9d8c7b6", "Drop the legacy module", "chris", 20*time.Hour),
},
FetchedAt: now.Add(-2 * time.Minute),
}
st.Changes = changes(15*time.Minute, []submodule.FileChange{
{Path: "README.md", Kind: submodule.ChangeModified, Added: 4, Removed: 1},
{Path: "old_module/", Kind: submodule.ChangeUntracked, Repo: true},
})
return st
}
func fixture() []node {
c1 := commit("3f9a1c2", "Bump golangci-lint to v2", "anna", 26*time.Hour)
c2 := commit("a1b2c3d", "Initial import", "ben", 40*24*time.Hour)
srcSub1Pin := commit("5d6e7f8", "Add retry to client", "chris", 3*24*time.Hour)
srcSub1Head := commit("0c1d2e3", "WIP: experiment with backoff", "you", 2*time.Hour)
srcSub2 := inSync("origin/main", commit("77aa88b", "Merge PR #42", "dana", 5*time.Hour))
srcSub2.Worktree = submodule.WorktreeModified
srcSub2.Changes = changes(20*time.Minute, []submodule.FileChange{
{Path: "internal/client/retry.go", Kind: submodule.ChangeModified, Added: 84, Removed: 12},
{Path: "internal/client/backoff.go", Kind: submodule.ChangeModified, Added: 41, Removed: 3},
{Path: "internal/server/handler.go", Kind: submodule.ChangeModified, Added: 17, Removed: 17},
{Path: "README.md", Kind: submodule.ChangeModified, Added: 6, Removed: 2},
{Path: "go.mod", Kind: submodule.ChangeModified, Added: 2, Removed: 2},
{Path: "go.sum", Kind: submodule.ChangeModified, Added: 9, Removed: 4},
{Path: "Makefile", Kind: submodule.ChangeModified, Added: 1, Removed: 1},
{Path: "internal/client/retry_test.go", Kind: submodule.ChangeAdded, Added: 63},
{Path: "docs/design/backoff-notes.md", Kind: submodule.ChangeAdded, Added: 28},
{Path: "internal/legacy/old_client.go", Kind: submodule.ChangeRemoved, Removed: 212},
{Path: "assets/logo.png", Kind: submodule.ChangeModified, Binary: true},
{Path: "internal/client/transport.go", OrigPath: "internal/net/transport.go", Kind: submodule.ChangeRenamed, Added: 5, Removed: 1},
{Path: "scratch.txt", Kind: submodule.ChangeUntracked},
{Path: "tmp/", Kind: submodule.ChangeUntracked},
{Path: "profile.out", Kind: submodule.ChangeUntracked},
})
subSubPin := commit("4e5f6a7", "Add feature flag", "eve", 30*time.Hour)
subSubTip := commit("9f8e7d6", "Fix login timeout", "anna", 2*time.Hour)
subSub := &submodule.Status{
Checkout: submodule.CheckoutDiffers,
Pin: submodule.PinInfo{Ref: "origin/feat/02", Behind: 1, Pushed: true},
Pinned: subSubPin,
CheckedOut: subSubTip,
Tip: subSubTip,
Between: []submodule.Commit{*subSubTip},
}
schema := inSync("origin/main", commit("1a2b3c4", "Add v3 schema", "ben", 6*24*time.Hour))
schema.Worktree = submodule.WorktreeUntracked
schema.Changes = changes(40*time.Minute, []submodule.FileChange{
{Path: "v3/user.schema.json", Kind: submodule.ChangeUntracked},
{Path: "v3/order.schema.json", Kind: submodule.ChangeUntracked},
})
unpushed := commit("b7c8d9e", "Hotfix null pointer in parser", "you", 20*time.Minute)
sub3Tip := commit("6f5e4d3", "Release 2.1.0", "chris", 4*24*time.Hour)
logPin := commit("2b3c4d5", "Add structured fields", "dana", 9*24*time.Hour)
logTip := commit("8e9f0a1", "Support slog handler", "eve", 1*24*time.Hour)
httpPin := commit("c4d5e6f", "Experimental HTTP/3 support", "frank", 8*24*time.Hour)
httpTip := commit("e1f2a3b", "Drop Go 1.21 support", "anna", 12*time.Hour)
cryptoNew := commit("d2e3f4a", "Release v1.4.2", "ben", 3*24*time.Hour)
crypto := inSync("v1.4.2", cryptoNew)
crypto.Pin.Source = submodule.RefFromTag
crypto.PinStaged = true
zlib := inSync("v1.3.1", commit("51f2c3e", "zlib 1.3.1", "madler", 300*24*time.Hour))
zlib.Pin.Source = submodule.RefFromTag
zlib.Checkout = submodule.CheckoutDetached
return []node{
{relPath: "docs", status: inSync("origin/main", c2)},
{relPath: "misc/tools/my_submodule_1", status: inSync("origin/main", c1)},
{relPath: "misc/tools/my_submodule_2", status: &submodule.Status{
Presence: submodule.NotInitialized,
Pin: submodule.PinInfo{Pushed: true, Source: submodule.RefFromSHA},
Pinned: &submodule.Commit{SHA: "a1b2c3d"},
}},
{relPath: "src/my_submodule_1", status: &submodule.Status{
Checkout: submodule.CheckoutDiffers,
Pin: submodule.PinInfo{Ref: "origin/main", Pushed: true},
Pinned: srcSub1Pin,
CheckedOut: srcSub1Head,
Tip: srcSub1Pin,
}},
{relPath: "src/my_submodule_2", status: srcSub2, children: []node{
{relPath: "modules/my_sub_submodule", status: subSub},
{relPath: "modules/protocol", status: inSync("origin/main", commit("aa11bb2", "Regenerate stubs", "frank", 50*time.Hour)), children: []node{
{relPath: "vendor/schema", status: schema},
}},
}},
{relPath: "src/my_submodule_3", status: &submodule.Status{
Pin: submodule.PinInfo{Ref: "origin/main", Ahead: 1, Pushed: false, Source: submodule.RefFromCurrentBranch},
Pinned: unpushed,
CheckedOut: unpushed,
Tip: sub3Tip,
Between: []submodule.Commit{*unpushed},
}},
{relPath: "libs/logging", status: &submodule.Status{
Pin: submodule.PinInfo{Ref: "origin/main", Behind: 3, Pushed: true},
Pinned: logPin,
CheckedOut: logPin,
Tip: logTip,
Between: []submodule.Commit{
*logTip,
*commit("7d8e9f0", "Fix race in buffered writer", "dana", 3*24*time.Hour),
*commit("6c7d8e9", "Update dependencies", "renovate", 5*24*time.Hour),
},
}},
{relPath: "libs/http", status: &submodule.Status{
Pin: submodule.PinInfo{Ref: "origin/develop", Ahead: 1, Behind: 2, Pushed: true, Source: submodule.RefFromContains},
Pinned: httpPin,
CheckedOut: httpPin,
Tip: httpTip,
Between: []submodule.Commit{
*httpTip,
*commit("d0e1f2a", "Refactor transport", "frank", 2*24*time.Hour),
*httpPin,
},
}},
{relPath: "libs/crypto", status: crypto},
{relPath: "third_party/zlib", status: zlib},
}
}