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/model.go
T
Simon FrankenandClaude Opus 5.5 19c6f17eb2 Preview uncommitted changes in the detail pane
For a dirty submodule, list the changed files so the user can remember
what they were doing there:

  Changes   5 files · 2M 1A 1D 1R · +37 −12
            ──────────────────────────────────
            M  render.go  src/render/  +20 −4  3d ago
            ?  scratch.txt                     5m ago

- Status.Changes: files changed against HEAD (staged and unstaged
  combined) with kind, line counts and modification time; sorted by kind,
  then by size; capped at 200 with totals over all files.
- Git provider: parse `status --porcelain=v2 -z` and one
  `diff HEAD --numstat -z -M`, only for dirty submodules.
- Detail pane: flat list with a colored status letter per file, like IDE
  source-control views; counts and age in compact columns; up to 10 files,
  every kind keeps a line, the rest is summarized as "n more".
- Fixture gains libs/work with every kind of change; mock data extended.
- Docs: F24, change preview in the UX guide, D17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:55:46 +02:00

183 lines
4.5 KiB
Go

// Package submodule holds the domain model for a superproject and its
// (recursively nested) git submodules. It has no UI dependencies.
package submodule
import (
"cmp"
"slices"
"strings"
"time"
)
// Repo is the superproject.
type Repo struct {
Path string
Branch string
Ahead int
Behind int
FetchedAt time.Time
Submodules []*Submodule
}
// Submodule is one node in the submodule tree.
type Submodule struct {
Name string
Path string // relative to the superproject root; unique ID
RelPath string // relative to the parent repository, e.g. "modules/foo"
URL string
Parent *Submodule
Children []*Submodule
Status *Status // nil while loading
Err error // set if loading the status failed
}
// Presence describes whether the submodule exists on disk.
type Presence int
const (
PresenceOK Presence = iota
NotInitialized
DirMissing
)
// Checkout compares the submodule's HEAD with the pinned commit.
type Checkout int
const (
CheckoutMatch Checkout = iota
CheckoutDiffers
CheckoutDetached // on the pinned commit, but detached (informational)
)
// Worktree describes uncommitted state inside the submodule.
type Worktree int
const (
WorktreeClean Worktree = iota
WorktreeModified
WorktreeUntracked
WorktreeConflict
)
// RefSource tells which rule resolved the pin to a ref.
type RefSource int
const (
RefFromGitmodules RefSource = iota
RefFromCurrentBranch
RefFromDefaultBranch
RefFromContains
RefFromTag
RefFromSHA
)
func (r RefSource) String() string {
switch r {
case RefFromGitmodules:
return ".gitmodules branch"
case RefFromCurrentBranch:
return "checked out branch"
case RefFromDefaultBranch:
return "remote default branch"
case RefFromContains:
return "branch containing pin"
case RefFromTag:
return "tag"
default:
return "commit sha"
}
}
// PinInfo relates the pinned commit to a ref.
type PinInfo struct {
Ref string // e.g. "origin/main"; empty if unresolved
Source RefSource
Ahead int // commits in pin not in ref
Behind int // commits in ref not in pin
Pushed bool // pin is reachable from some remote ref
}
// Commit is a short commit description.
type Commit struct {
SHA string
Subject string
Author string
When time.Time
}
// ChangeKind classifies one changed file. The order is the display order.
type ChangeKind int
const (
ChangeConflicted ChangeKind = iota
ChangeModified
ChangeAdded
ChangeRemoved
ChangeRenamed
ChangeUntracked
NumChangeKinds
)
// MaxChangeFiles limits Changes.Files; Changes.Total keeps the real count.
const MaxChangeFiles = 200
// FileChange is one changed file, relative to HEAD of the submodule
// (staged and unstaged changes are combined).
type FileChange struct {
Path string
OrigPath string // old path, only for renames
Kind ChangeKind
Added int // added lines; 0 for untracked, binary and nested submodules
Removed int // removed lines
Binary bool // line counts are unknown
Submodule bool // a nested submodule pointer changed
Modified time.Time // modification time of the file; zero if it doesn't exist
}
// Changes describes what differs in the worktree of a submodule.
type Changes struct {
Files []FileChange // at most MaxChangeFiles, sorted by kind, then by changed lines
Total int // all changed files; may exceed len(Files)
Added int // added lines in all files
Removed int // removed lines in all files
}
// Counts returns the number of files per kind among files (all of
// c.Files if files is nil).
func (c *Changes) Counts(files []FileChange) [NumChangeKinds]int {
if files == nil && c != nil {
files = c.Files
}
var n [NumChangeKinds]int
for _, f := range files {
n[f.Kind]++
}
return n
}
// Sort orders Files by kind, then by changed lines (largest first), then by
// path.
func (c *Changes) Sort() {
slices.SortStableFunc(c.Files, func(a, b FileChange) int {
return cmp.Or(
cmp.Compare(a.Kind, b.Kind),
cmp.Compare(b.Added+b.Removed, a.Added+a.Removed),
strings.Compare(a.Path, b.Path),
)
})
}
// Status is the full state of one submodule.
type Status struct {
Presence Presence
Checkout Checkout
Worktree Worktree
PinStaged bool // pin changed in the superproject index, not committed
Pin PinInfo
Pinned *Commit
CheckedOut *Commit
Tip *Commit
Between []Commit // commits that make up the pin <-> tip difference
Changes *Changes // nil if the worktree is clean or the details are unknown
}