Archived
Draw diverged commits on the spine as a fork
A tip, checkout or committed pin that has diverged from the pin gets its own lane, which joins the main lane at a "base" node: the merge base of the two, with its SHA and subject. Only one diverged commit is drawn as a fork; with two, or without a merge base, the nodes stay in a line with the "diverged" gap. Add Status.Bases, filled by the git provider with `merge-base` for each diverged commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,6 +208,20 @@ func (m Model) spineLines(nodes []submodule.SpineNode, offPin bool) []string {
|
||||
pinAt := slices.IndexFunc(nodes, func(n submodule.SpineNode) bool { return hasRole(n, submodule.RolePin) })
|
||||
stagedAt := slices.IndexFunc(nodes, func(n submodule.SpineNode) bool { return hasRole(n, submodule.RoleStaged) })
|
||||
|
||||
// a fork has its own lane to the right of the main one; it starts at the
|
||||
// first node and joins the main lane at the base
|
||||
fork, baseAt := len(nodes) > 0 && nodes[0].Fork, -1
|
||||
if fork {
|
||||
baseAt = slices.IndexFunc(nodes, func(n submodule.SpineNode) bool { return hasRole(n, submodule.RoleBase) })
|
||||
}
|
||||
line := muted.Render(t.Glyphs.Gap)
|
||||
lanes := func(main, side string) string {
|
||||
if !fork {
|
||||
return main
|
||||
}
|
||||
return main + " " + side
|
||||
}
|
||||
|
||||
var out []string
|
||||
for i, n := range nodes {
|
||||
label, sha := muted, theme.Fg(t.Primary)
|
||||
@@ -217,10 +231,30 @@ func (m Model) spineLines(nodes []submodule.SpineNode, offPin bool) []string {
|
||||
if offPin && len(n.Roles) == 1 && n.Roles[0] == submodule.RoleYou {
|
||||
label, sha = theme.Fg(t.Warn), theme.Fg(t.Warn)
|
||||
}
|
||||
out = append(out, label.Width(width).Render(labels[i])+sha.Render(t.Glyphs.Node+" "+n.Commit.SHA)+" "+text.Render(n.Commit.Subject))
|
||||
node := sha.Render(t.Glyphs.Node)
|
||||
var cells string
|
||||
switch {
|
||||
case n.Fork:
|
||||
cells = lanes(" ", node)
|
||||
case i == baseAt:
|
||||
cells = node + muted.Render(t.Glyphs.Rule+t.Glyphs.Join)
|
||||
case fork && i < baseAt:
|
||||
cells = lanes(node, line)
|
||||
default:
|
||||
cells = lanes(node, " ")
|
||||
}
|
||||
out = append(out, label.Width(width).Render(labels[i])+cells+" "+sha.Render(n.Commit.SHA)+" "+text.Render(n.Commit.Subject))
|
||||
if i < len(nodes)-1 {
|
||||
gap := muted.Render(t.Glyphs.Gap + m.linkText(n.Below))
|
||||
if i+1 == stagedAt && pinAt >= 0 && pinAt < stagedAt && n.Below.Linear() {
|
||||
// the main lane starts below the fork's head, the fork's lane ends at the base
|
||||
main, side := " ", " "
|
||||
if !fork || i > 0 {
|
||||
main = line
|
||||
}
|
||||
if fork && i < baseAt {
|
||||
side = line
|
||||
}
|
||||
gap := lanes(main, side) + muted.Render(m.linkText(n.Below))
|
||||
if !fork && i+1 == stagedAt && pinAt >= 0 && pinAt < stagedAt && n.Below.Linear() {
|
||||
gap += theme.Fg(t.Info).Render(" (staged pin moves back)")
|
||||
}
|
||||
out = append(out, strings.Repeat(" ", width)+gap)
|
||||
|
||||
@@ -73,3 +73,28 @@ func TestSpineLinesInSync(t *testing.T) {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpineLinesFork(t *testing.T) {
|
||||
m := New(theme.Default(), nil)
|
||||
c := func(sha, subject string) *submodule.Commit { return &submodule.Commit{SHA: sha, Subject: subject} }
|
||||
st := submodule.Status{
|
||||
Pinned: c("c4d5e6f", "Experimental"), CheckedOut: c("c4d5e6f", "Experimental"), Tip: c("e1f2a3b", "Drop Go 1.21"),
|
||||
Pin: submodule.PinInfo{Ahead: 1, Behind: 2},
|
||||
Bases: submodule.Bases{Tip: c("a1b2c3d", "Release 1.4")},
|
||||
}
|
||||
var got []string
|
||||
for _, l := range m.spineLines(st.Spine(), false) {
|
||||
got = append(got, ansi.Strip(l))
|
||||
}
|
||||
g := theme.Default().Glyphs
|
||||
want := []string{
|
||||
"tip " + " " + g.Node + " e1f2a3b Drop Go 1.21",
|
||||
" " + " " + g.Gap + " 2 commits",
|
||||
"pin · you " + g.Node + " " + g.Gap + " c4d5e6f Experimental",
|
||||
" " + g.Gap + " " + g.Gap + " 1 commit",
|
||||
"base " + g.Node + g.Rule + g.Join + " a1b2c3d Release 1.4",
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("got\n%q\nwant\n%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ func RoleWord(r submodule.Role) string {
|
||||
return "HEAD"
|
||||
case submodule.RoleStaged:
|
||||
return Staged
|
||||
case submodule.RoleBase:
|
||||
return "base"
|
||||
case submodule.RoleTip:
|
||||
return "tip"
|
||||
default:
|
||||
|
||||
@@ -54,17 +54,17 @@ func Fg(c lipgloss.TerminalColor) lipgloss.Style {
|
||||
|
||||
// Glyphs are the few symbols used next to the state words.
|
||||
type Glyphs struct {
|
||||
Staged, Ahead, Behind, Collapsed, Expanded, Problems, Minus, From, Rule, Node, Gap string
|
||||
Staged, Ahead, Behind, Collapsed, Expanded, Problems, Minus, From, Rule, Node, Gap, Join string
|
||||
}
|
||||
|
||||
func Unicode() Glyphs {
|
||||
return Glyphs{
|
||||
Staged: "●", Ahead: "↑", Behind: "↓", Collapsed: "▸", Expanded: "▾", Problems: "·", Minus: "−", From: "←", Rule: "─", Node: "●", Gap: "┊",
|
||||
Staged: "●", Ahead: "↑", Behind: "↓", Collapsed: "▸", Expanded: "▾", Problems: "·", Minus: "−", From: "←", Rule: "─", Node: "●", Gap: "┊", Join: "╯",
|
||||
}
|
||||
}
|
||||
|
||||
func ASCII() Glyphs {
|
||||
return Glyphs{
|
||||
Staged: "*", Ahead: "^", Behind: "v", Collapsed: ">", Expanded: "v", Problems: ".", Minus: "-", From: "<-", Rule: "-", Node: "o", Gap: ":",
|
||||
Staged: "*", Ahead: "^", Behind: "v", Collapsed: ">", Expanded: "v", Problems: ".", Minus: "-", From: "<-", Rule: "-", Node: "o", Gap: ":", Join: "/",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user