Archived
Replace the Pinned/Checkout/Tip rows and the list of commits between pin and tip with one line of history, newest on top: a node per commit with its roles (pin, tip, you, staged, HEAD) and the number of commits between two nodes. A staged pin shows the committed pin as its own node and notes when it moves back. All three on one commit read "in sync". Add Status.Drift, Committed and CommittedDrift and Status.Spine(); drop Status.Between and Repo.FetchedAt. The fetch age moves from the header to the detail pane (right-aligned in the name's line). The header summary is right-aligned, ordered by importance and cut with an ellipsis if too narrow. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
289 lines
10 KiB
Go
289 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,
|
|
}
|
|
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,
|
|
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,
|
|
Drift: submodule.Distance{Ahead: 1},
|
|
}
|
|
|
|
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
|
|
crypto.Committed = commit("e0f1a2b", "Release v1.4.1", "ben", 40*24*time.Hour)
|
|
crypto.CommittedDrift = submodule.Distance{Behind: 3}
|
|
|
|
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,
|
|
Drift: submodule.Distance{Ahead: 2},
|
|
}},
|
|
{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,
|
|
}},
|
|
{relPath: "libs/logging", status: &submodule.Status{
|
|
Pin: submodule.PinInfo{Ref: "origin/main", Behind: 3, Pushed: true},
|
|
Pinned: logPin,
|
|
CheckedOut: logPin,
|
|
Tip: logTip,
|
|
}},
|
|
{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,
|
|
}},
|
|
{relPath: "libs/crypto", status: crypto},
|
|
{relPath: "third_party/zlib", status: zlib},
|
|
}
|
|
}
|