Archived
The ref of a pin was the closest branch containing it, so a pin on a feature branch looked like a perfect one, and the ref changed with the branch checked out in the submodule. Split it in two: - Pin.Target is the branch the pin should be on: `branch =` of .gitmodules (none if it does not exist locally), else the remote's default branch. - Pin.Ref is shown: the target if the pin is on it, else the branch containing the pin with the fewest commits after it. A tie goes to the upstream of the checked-out branch, then to the first name; no branch is a favorite. A pushed pin that is not on its target is `off target`, a new issue at the new level Info (blue), counted in the header and below collapsed rows. The spine shows the target's tip, labelled with its branch name, so the distance from it is visible; a diverged one is the fork. The role words in a spine label are styled separately and `you` is bold. Add Status.TargetTip, TargetDrift and Bases.Target, Pin.Target and Pin.Branches; drop RefSource.IsTarget. The detail pane's label column is 11 wide for the word `off target`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
144 lines
4.2 KiB
Go
144 lines
4.2 KiB
Go
package submodule
|
|
|
|
import "fmt"
|
|
|
|
// Level is how much attention an issue deserves.
|
|
type Level int
|
|
|
|
const (
|
|
LevelQuiet Level = iota // nothing to do
|
|
LevelInfo // good to know
|
|
LevelWarn // look at it
|
|
LevelDanger // act now
|
|
)
|
|
|
|
// IssueKind is the summarized category of a problem.
|
|
type IssueKind int
|
|
|
|
const (
|
|
IssueMissing IssueKind = iota // not initialized or directory missing
|
|
IssueUnpushed // pin not reachable from any remote ref
|
|
IssueConflict // merge conflict in the worktree
|
|
IssueOffPin // checkout differs from pin
|
|
IssueChanged // uncommitted changes or untracked files
|
|
IssueOffTarget // pin pushed, but not on the target branch
|
|
)
|
|
|
|
// Issue is one problem of a submodule. Detail distinguishes the states that
|
|
// are summarized into one kind.
|
|
type Issue struct {
|
|
Kind IssueKind
|
|
Level Level
|
|
Detail string
|
|
}
|
|
|
|
// Issues summarizes the status, most severe first. Being behind or ahead of
|
|
// the remote, a staged pin and a detached HEAD are not issues.
|
|
func (s *Status) Issues() []Issue {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
switch s.Presence {
|
|
case NotInitialized:
|
|
return []Issue{{IssueMissing, LevelWarn, "not initialized"}}
|
|
case DirMissing:
|
|
return []Issue{{IssueMissing, LevelWarn, "directory missing"}}
|
|
}
|
|
|
|
var out []Issue
|
|
if !s.Pin.Pushed {
|
|
detail := "pin not pushed to any remote"
|
|
if s.Root {
|
|
detail = "HEAD not pushed to any remote"
|
|
}
|
|
out = append(out, Issue{IssueUnpushed, LevelDanger, detail})
|
|
}
|
|
if s.Worktree == WorktreeConflict {
|
|
out = append(out, Issue{IssueConflict, LevelDanger, "merge conflict"})
|
|
}
|
|
if s.Checkout == CheckoutDiffers {
|
|
out = append(out, Issue{IssueOffPin, LevelWarn, "checkout differs from pin"})
|
|
}
|
|
switch s.Worktree {
|
|
case WorktreeModified:
|
|
out = append(out, Issue{IssueChanged, LevelWarn, "uncommitted changes"})
|
|
case WorktreeUntracked:
|
|
out = append(out, Issue{IssueChanged, LevelWarn, s.untrackedText()})
|
|
}
|
|
// a pin that is pushed, but not on the target (usually main); an
|
|
// unpushed pin is not on it either, but has the worse issue
|
|
if !s.Root && s.Pin.Pushed && s.Pin.Target != "" && s.Pin.Ref != s.Pin.Target {
|
|
out = append(out, Issue{IssueOffTarget, LevelInfo, s.offTargetText()})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// offTargetText says which branch the pin is not on and where it is instead.
|
|
func (s *Status) offTargetText() string {
|
|
text := fmt.Sprintf("pin not on %s, on %s", s.Pin.Target, s.Pin.Ref)
|
|
if n := len(s.Pin.Branches); n > 0 {
|
|
text += fmt.Sprintf(" +%d", n)
|
|
}
|
|
return text
|
|
}
|
|
|
|
// Hidden counts issues below a submodule per level, plus staged pins (which
|
|
// are not issues).
|
|
type Hidden struct{ Info, Warn, Danger, Staged int }
|
|
|
|
// Total is the number of issues of all levels; staged pins are not counted.
|
|
func (h Hidden) Total() int { return h.Info + h.Warn + h.Danger }
|
|
|
|
// HiddenIssues counts the issues and staged pins of all loaded descendants. A
|
|
// descendant whose load failed counts as one danger, since its state is
|
|
// unknown.
|
|
func (m *Submodule) HiddenIssues() Hidden { return HiddenIn(m.Children) }
|
|
|
|
// HiddenIn counts the issues and staged pins of mods and all their loaded
|
|
// descendants, like HiddenIssues.
|
|
func HiddenIn(mods []*Submodule) Hidden {
|
|
var h Hidden
|
|
Walk(mods, func(c *Submodule) {
|
|
if c.Status == nil && c.Err != nil {
|
|
h.Danger++
|
|
}
|
|
if c.Status != nil && c.Status.PinStaged {
|
|
h.Staged++
|
|
}
|
|
for _, is := range c.Status.Issues() {
|
|
switch is.Level {
|
|
case LevelDanger:
|
|
h.Danger++
|
|
case LevelWarn:
|
|
h.Warn++
|
|
case LevelInfo:
|
|
h.Info++
|
|
}
|
|
}
|
|
})
|
|
return h
|
|
}
|
|
|
|
// Walk calls fn for every submodule in depth-first order.
|
|
func Walk(mods []*Submodule, fn func(*Submodule)) {
|
|
for _, m := range mods {
|
|
fn(m)
|
|
Walk(m.Children, fn)
|
|
}
|
|
}
|
|
|
|
// untrackedText words the untracked-files issue: "untracked repository" if
|
|
// everything untracked is a git repository of its own, e.g. a clone that was
|
|
// never added as a submodule.
|
|
func (s *Status) untrackedText() string {
|
|
if s.Changes == nil || s.Changes.Total != len(s.Changes.Files) {
|
|
return "untracked files"
|
|
}
|
|
for _, f := range s.Changes.Files {
|
|
if f.Kind != ChangeUntracked || !f.Repo {
|
|
return "untracked files"
|
|
}
|
|
}
|
|
return "untracked repository"
|
|
}
|