This repository has been archived on 2026-10-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
simonfrankenandClaude Sonnet 5.5 e8d366d88f Compare pins with a target branch and flag pins off it
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>
2026-10-23 17:35:16 +02:00

192 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}
}
}