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>
192 lines
6.3 KiB
Go
192 lines
6.3 KiB
Go
package submodule
|
||
|
||
import "slices"
|
||
|
||
// Role is what a commit means for a submodule.
|
||
type Role int
|
||
|
||
// The order is the order in which roles are listed for a commit.
|
||
const (
|
||
RolePin Role = iota // the commit recorded in the superproject's HEAD
|
||
RoleHead // the superproject's HEAD, which takes the pin's place
|
||
RoleStaged // the pin in the index, if it differs from RolePin
|
||
RoleTip // the tip of the ref the pin was resolved to
|
||
RoleTarget // the tip of the target branch, if the pin is not on it
|
||
RoleYou // the commit checked out in the submodule
|
||
RoleBase // where a diverged commit's history splits from the pin's
|
||
)
|
||
|
||
// SpineNode is one commit on the spine and the roles that sit on it.
|
||
type SpineNode struct {
|
||
Roles []Role
|
||
Commit *Commit
|
||
// Fork marks the head of a diverged branch. It is the first node; its
|
||
// Below is the number of commits down to the RoleBase node, on its own
|
||
// lane, and the other nodes follow on the main lane.
|
||
Fork bool
|
||
// Below describes the history between this node and the next one down;
|
||
// it is the zero Link for the last node.
|
||
Below Link
|
||
}
|
||
|
||
// InSync reports whether the pin, the tip and the checkout are one commit.
|
||
func (n SpineNode) InSync() bool {
|
||
return slices.Contains(n.Roles, RoleTip) &&
|
||
(slices.Contains(n.Roles, RoleHead) || slices.Contains(n.Roles, RolePin) && slices.Contains(n.Roles, RoleYou))
|
||
}
|
||
|
||
// Link is the history between two spine nodes. Above counts the commits only
|
||
// the upper node has, Below those only the lower node has.
|
||
type Link struct{ Above, Below int }
|
||
|
||
// Linear reports whether the lower node is an ancestor of the upper one, so
|
||
// that Above is the number of commits between them.
|
||
func (l Link) Linear() bool { return l.Above > 0 && l.Below == 0 }
|
||
|
||
// Diverged reports whether the nodes are on different branches.
|
||
func (l Link) Diverged() bool { return l.Above > 0 && l.Below > 0 }
|
||
|
||
// spineItem is a commit with its roles on its way to becoming a node.
|
||
type spineItem struct {
|
||
roles []Role
|
||
commit *Commit
|
||
height int // commits above (+) or below (−) the pin; unused if div
|
||
div bool // diverged from the pin
|
||
divAbove bool // where a diverged item goes if it is not drawn as a fork
|
||
base *Commit // merge base with the pin, if div and known
|
||
own int // commits only this item has
|
||
pin int // commits only the pin has
|
||
isPin bool
|
||
}
|
||
|
||
// Spine arranges the pin (and the staged pin, if any), the tip and the
|
||
// checkout as one line of history, newest on top. Roles on the same commit
|
||
// share a node.
|
||
//
|
||
// A commit that has diverged from the pin is drawn as a fork if it is the
|
||
// only one and its merge base is known: it is the first node, and the
|
||
// RoleBase node where the histories split follows on the main lane. Otherwise
|
||
// a diverged tip is placed above the pin and a diverged checkout below it,
|
||
// with a diverged Link. Missing commits are left out.
|
||
func (s *Status) Spine() []SpineNode {
|
||
if s.Pinned == nil {
|
||
return nil
|
||
}
|
||
pinRole := RolePin
|
||
switch {
|
||
case s.Root:
|
||
pinRole = RoleHead
|
||
case s.PinStaged:
|
||
pinRole = RoleStaged // the committed pin gets its own node below
|
||
}
|
||
pin := spineItem{roles: []Role{pinRole}, commit: s.Pinned, isPin: true}
|
||
|
||
var above, below, divs []spineItem
|
||
place := func(role Role, c, base *Commit, own, pinOnly int, divAbove bool) {
|
||
it := spineItem{roles: []Role{role}, commit: c, own: own, pin: pinOnly}
|
||
switch {
|
||
case own == 0 && pinOnly == 0:
|
||
pin.roles = append(pin.roles, role)
|
||
case own > 0 && pinOnly == 0:
|
||
it.height = own
|
||
above = append(above, it)
|
||
case own == 0:
|
||
it.height = -pinOnly
|
||
below = append(below, it)
|
||
default:
|
||
it.div, it.divAbove, it.base = true, divAbove, base
|
||
divs = append(divs, it)
|
||
}
|
||
}
|
||
if s.Tip != nil {
|
||
place(RoleTip, s.Tip, s.Bases.Tip, s.Pin.Behind, s.Pin.Ahead, true)
|
||
}
|
||
if s.TargetTip != nil {
|
||
place(RoleTarget, s.TargetTip, s.Bases.Target, s.TargetDrift.Ahead, s.TargetDrift.Behind, true)
|
||
}
|
||
if s.PinStaged && s.Committed != nil {
|
||
place(RolePin, s.Committed, s.Bases.Committed, s.CommittedDrift.Ahead, s.CommittedDrift.Behind, false)
|
||
}
|
||
switch {
|
||
case s.CheckedOut == nil, s.Root: // HEAD is the superproject's pin and checkout
|
||
case s.CheckedOut.SHA == s.Pinned.SHA:
|
||
pin.roles = append(pin.roles, RoleYou)
|
||
default:
|
||
place(RoleYou, s.CheckedOut, s.Bases.Checkout, s.Drift.Ahead, s.Drift.Behind, false)
|
||
}
|
||
|
||
var fork *spineItem
|
||
if len(divs) == 1 && divs[0].base != nil {
|
||
fork = &divs[0]
|
||
// the main lane continues to the base, where the fork joins it
|
||
below = append(below, spineItem{roles: []Role{RoleBase}, commit: fork.base, height: -fork.pin})
|
||
} else {
|
||
for _, it := range divs {
|
||
if it.divAbove {
|
||
above = append(above, it)
|
||
} else {
|
||
below = append(below, it)
|
||
}
|
||
}
|
||
}
|
||
|
||
// diverged items are outermost, the rest is ordered by height
|
||
byHeight := func(a, b spineItem) int { return b.height - a.height }
|
||
slices.SortStableFunc(above, func(a, b spineItem) int {
|
||
if a.div != b.div {
|
||
if a.div {
|
||
return -1
|
||
}
|
||
return 1
|
||
}
|
||
return byHeight(a, b)
|
||
})
|
||
slices.SortStableFunc(below, func(a, b spineItem) int {
|
||
if a.div != b.div {
|
||
if a.div {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
return byHeight(a, b)
|
||
})
|
||
|
||
slices.Sort(pin.roles)
|
||
items := append(append(above, pin), below...)
|
||
var nodes []SpineNode
|
||
var last spineItem
|
||
for i, it := range items {
|
||
// the same commit twice, e.g. a tip and a checkout side by side
|
||
if i > 0 && !it.div && !last.div && it.height == last.height && it.commit.SHA == last.commit.SHA {
|
||
n := &nodes[len(nodes)-1]
|
||
n.Roles = append(n.Roles, it.roles...)
|
||
slices.Sort(n.Roles)
|
||
continue
|
||
}
|
||
if i > 0 {
|
||
nodes[len(nodes)-1].Below = link(last, it)
|
||
}
|
||
nodes = append(nodes, SpineNode{Roles: it.roles, Commit: it.commit})
|
||
last = it
|
||
}
|
||
if fork != nil {
|
||
head := SpineNode{Roles: fork.roles, Commit: fork.commit, Fork: true, Below: Link{Above: fork.own}}
|
||
nodes = append([]SpineNode{head}, nodes...)
|
||
}
|
||
return nodes
|
||
}
|
||
|
||
// link describes the history between an upper and a lower item.
|
||
func link(upper, lower spineItem) Link {
|
||
switch {
|
||
case upper.div && lower.isPin:
|
||
return Link{Above: upper.own, Below: upper.pin}
|
||
case lower.div && upper.isPin:
|
||
return Link{Above: lower.pin, Below: lower.own}
|
||
case upper.div || lower.div:
|
||
return Link{} // unknown
|
||
default:
|
||
return Link{Above: upper.height - lower.height}
|
||
}
|
||
}
|