Archived
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>
124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
package submodule
|
|
|
|
// Level is how much attention an issue deserves.
|
|
type Level int
|
|
|
|
const (
|
|
LevelQuiet Level = iota // nothing to do
|
|
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
|
|
)
|
|
|
|
// 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()})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Hidden counts issues below a submodule per level, plus staged pins (which
|
|
// are not issues).
|
|
type Hidden struct{ 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.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++
|
|
}
|
|
}
|
|
})
|
|
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"
|
|
}
|