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:
2026-10-23 16:40:25 +02:00
co-authored by Claude Sonnet 5.5
parent 4c1be3f9ab
commit f287d6009f
14 changed files with 283 additions and 37 deletions
+10
View File
@@ -414,3 +414,13 @@ header keeps the progress and `n failed`. `Repo.FetchedAt` is gone.
**Amendment (with D25).** A staged pin shows both pins on the spine: `staged`
(the index) and `pin` (the superproject's HEAD, `Status.Committed`), so a
bump reads as a distance. Before, only the index pin was shown at all.
**Amendment (with D25): forks.** A diverged tip, checkout or committed pin is
drawn as a second lane that joins the main one at a `base` node (SHA +
subject of `git merge-base`), instead of a straight line with
`diverged ↑n ↓m`. The base was chosen over a bare join mark: where the split
happened is what decides between a merge and a rebase. Cost: one
`merge-base` and one `log -1` per diverged commit, only when it is diverged.
Only one fork is drawn; with two diverged commits (rare) the old line with
the diverged gap remains, since nodes on three lanes would need the
merge bases between all of them.
+2
View File
@@ -64,6 +64,8 @@ commit hashes. It doesn't answer the questions users actually have:
**tip** and the checkout (**you**) as one line of history, newest on top:
a node per commit (SHA + subject) with its roles, and the number of
commits between two nodes. Roles on the same commit share a node. A
commit that has diverged from the pin is drawn as a fork that joins at
the commit where the histories split (`base`). A
checkout that differs from the pin is highlighted.
- **F22** *(dropped)* The commits between pin and tip are not listed; only
their number is shown (F21).
+16 -4
View File
@@ -32,6 +32,7 @@ in several at once.
| `Pin` | `Ref`, `Source`, `Ahead`, `Behind`, `Pushed` |
| `Pinned`, `CheckedOut`, `Tip` | Commits (SHA, subject, author, time) |
| `Drift` | Checkout compared with the pin: `Ahead` = commits only the checkout has, `Behind` = commits only the pin has |
| `Bases` | `Tip`, `Checkout`, `Committed`: `merge-base <pin> <that commit>` for each commit that is diverged from the pin (ahead and behind); nil otherwise or without a common ancestor |
| `Committed`, `CommittedDrift` | With a staged pin: the pin in the superproject's HEAD (nil if it is not on disk or the submodule was only added) and how it compares with the staged pin, like `Drift` |
| `Changes` | Changed files of the worktree (nil if clean), see below |
| `FetchedAt` | Last fetch of the submodule (`FETCH_HEAD` mtime); zero if never |
@@ -174,6 +175,7 @@ are local; `<pin>` is the index gitlink.
| `Pin.Ref` | rules above; the remote is `origin`, else the first remote |
| `Ahead`, `Behind` | `rev-list --left-right --count <pin>...<ref>` |
| `Drift` | `rev-list --left-right --count <head>...<pin>`, only if the checkout differs from the pin |
| `Bases` | `merge-base <pin> <ref>` / `<head>` / `<committed>`, only when diverged; then `log -1` of it |
| `Committed`, `CommittedDrift` | `ls-tree HEAD` gitlink, if it differs from the pin and exists locally; `rev-list --left-right --count <committed>...<pin>` |
Details:
@@ -212,10 +214,20 @@ roles (`pin`, `tip`, `you`; `HEAD` for the superproject). It uses `Pin.Ahead` /
*behind* below it; the `Link` between two nodes counts the commits
separating them. The count is the distance, so a tip 4 commits ahead
of the pin shows `4 commits`, and an adjacent one `1 commit`.
- A diverged tip is placed above the pin, a diverged checkout below it,
with a diverged `Link` (`Above` and `Below` count the commits only the
upper or lower node has). The link between a diverged node and a node other
than the pin is unknown (zero `Link`).
- A commit that has **diverged** from the pin (tip, checkout or committed
pin) is drawn as a **fork** if it is the only one and its merge base is
known (`Status.Bases`): it is the first node (`SpineNode.Fork`), with the
number of commits down to the base as its `Below`; the main lane follows
(pin and the linear nodes), and a `RoleBase` node where the two histories
split. Linear nodes between the pin and the base stay on the main lane
above it, older ones continue below it, and one that is the base commit
shares its node (`you · base`). The pin-to-base distance is the pin's side
of the divergence.
- Otherwise (two diverged commits, or no merge base) there is no fork: a
diverged tip is placed above the pin, a diverged checkout below it, with a
diverged `Link` (`Above` and `Below` count the commits only the upper or
lower node has). The link between a diverged node and a node other than the
pin is unknown (zero `Link`).
- With a staged pin the node of `Pinned` (the index) is labelled `staged`, and
the committed pin gets its own `pin` node, placed by `CommittedDrift` like
the checkout. Both can share nodes with `tip` and `you`.
+17 -3
View File
@@ -118,9 +118,23 @@ you ● 48005ac Merge branch 'feat/ethercat…
sits below it, and the gap above it says `(staged pin moves back)` (blue).
- The checkout (`you`) is yellow (label and SHA) when it is on its own node,
i.e. off pin. Everything else is the default text color, the gap muted.
- A diverged pair shows `┊ diverged ↑n ↓m` (arrows from the lower node's
view, like the tree). A real fork is not drawn yet.
- The nodes are `●` (`o` with `SUBMARINE_ASCII`), the gap `┊` (`:`).
- A commit that has **diverged** from the pin gets its own lane to the right
of the main one. The two lanes join at a `base` node, the commit where the
histories split; the counts say how many commits each side has since:
```
tip ● e1f2a3b Drop Go 1.21 support
┊ 2 commits
pin · you ● ┊ c4d5e6f Experimental HTTP/3
┊ ┊ 1 commit
base ●─╯ a9b8c7d Release 1.9.0
```
Only one diverged commit is drawn as a fork, and only if the base is known.
Otherwise the nodes stay in a line and the gap says `┊ diverged ↑n ↓m`
(arrows from the lower node's view, like the tree).
- The nodes are `●` (`o` with `SUBMARINE_ASCII`), the gap `┊` (`:`), the
join `─╯` (`-/`).
- A missing submodule has only the pin node (SHA, no subject).
#### Change preview