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>
109 lines
3.4 KiB
Go
109 lines
3.4 KiB
Go
// Package header renders the superproject summary line.
|
|
package header
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/charmbracelet/x/ansi"
|
|
|
|
"submarine/internal/submodule"
|
|
"submarine/internal/tui/theme"
|
|
)
|
|
|
|
// Fetch is the progress of the application's own background fetch.
|
|
type Fetch struct {
|
|
Running bool
|
|
Done int // repositories fetched (or failed)
|
|
Total int // repositories to fetch
|
|
Failed int // failed fetches of the last run
|
|
}
|
|
|
|
// View renders repo info, state counts and fetch state on one line. spinner
|
|
// is the current spinner frame. The header stands for the superproject: with
|
|
// selected it is drawn like a selected tree row.
|
|
func View(t theme.Theme, repo *submodule.Repo, fetch Fetch, spinner string, width int, selected bool) string {
|
|
bar := lipgloss.NewStyle().Background(t.Bar)
|
|
if selected {
|
|
bar = lipgloss.NewStyle().Background(t.Selected).Bold(true)
|
|
}
|
|
seg := func(c lipgloss.TerminalColor, s string) string { return bar.Foreground(c).Render(s) }
|
|
|
|
if repo == nil {
|
|
return bar.Width(width).Render(seg(t.Muted, " loading…"))
|
|
}
|
|
|
|
left := seg(t.Text, " "+repo.Path) + seg(t.Muted, " ") + seg(t.Primary, repo.Branch)
|
|
if d := t.Delta(submodule.PinInfo{Ahead: repo.Ahead, Behind: repo.Behind, Pushed: true}, bar); d != "" {
|
|
left += seg(t.Text, " ") + d
|
|
}
|
|
if repo.Root != nil {
|
|
left += seg(t.Text, " ") + t.StatusWord(repo.Root, bar, spinner)
|
|
}
|
|
|
|
// count submodules per issue kind (most severe first), plus staged pins
|
|
var total, loading, errored, staged int
|
|
perKind := map[submodule.IssueKind]int{}
|
|
level := map[submodule.IssueKind]submodule.Level{}
|
|
submodule.Walk(repo.Submodules, func(s *submodule.Submodule) {
|
|
total++
|
|
if s.Status == nil {
|
|
if s.Err == nil {
|
|
loading++
|
|
} else {
|
|
errored++
|
|
}
|
|
return
|
|
}
|
|
if s.Status.PinStaged {
|
|
staged++
|
|
}
|
|
for _, is := range s.Status.Issues() {
|
|
perKind[is.Kind]++
|
|
level[is.Kind] = is.Level
|
|
}
|
|
})
|
|
|
|
// most important first: errors, issues by severity, staged pins, then the
|
|
// total
|
|
sep := seg(t.Muted, " · ")
|
|
var parts []string
|
|
if errored > 0 {
|
|
parts = append(parts, t.LevelStyle(submodule.LevelDanger, bar).Render(fmt.Sprintf("%d error", errored)))
|
|
}
|
|
for _, k := range []submodule.IssueKind{
|
|
submodule.IssueUnpushed, submodule.IssueConflict, submodule.IssueMissing,
|
|
submodule.IssueOffPin, submodule.IssueChanged, submodule.IssueOffTarget,
|
|
} {
|
|
if n := perKind[k]; n > 0 {
|
|
parts = append(parts, t.LevelStyle(level[k], bar).Render(fmt.Sprintf("%d %s", n, theme.IssueWord(k))))
|
|
}
|
|
}
|
|
if staged > 0 {
|
|
parts = append(parts, seg(t.Info, fmt.Sprintf("%d pin staged", staged)))
|
|
}
|
|
if loading > 0 {
|
|
parts = append(parts, seg(t.Muted, fmt.Sprintf("%d loading", loading)))
|
|
}
|
|
parts = append(parts, seg(t.Muted, fmt.Sprintf("%d submodules", total)))
|
|
mid := strings.Join(parts, sep)
|
|
|
|
// the summary and the fetch state form one group at the right edge
|
|
right := mid
|
|
switch {
|
|
case fetch.Running:
|
|
right += sep + seg(t.Primary, spinner) + seg(t.Muted, fmt.Sprintf(" fetching %d/%d", fetch.Done, fetch.Total))
|
|
case fetch.Failed > 0:
|
|
right += sep + seg(t.Warn, fmt.Sprintf("%d failed", fetch.Failed))
|
|
}
|
|
right += seg(t.Muted, " ")
|
|
|
|
gap := width - lipgloss.Width(left) - lipgloss.Width(right)
|
|
if gap < 4 {
|
|
// too narrow: the least important part, on the right, is cut
|
|
return bar.Width(width).Render(ansi.Truncate(left+seg(t.Text, " ")+mid, width, "…"))
|
|
}
|
|
return left + bar.Render(fmt.Sprintf("%*s", gap, "")) + right
|
|
}
|