Resolve pins to the closest branch

A pin on a pushed feature branch showed as diverged from origin/main
because the .gitmodules branch won before any distance was compared.

Now the branch containing the pin with the fewest commits after it wins;
the .gitmodules branch, the upstream and the remote default branch only
break ties and are always compared. An unpushed pin compares just those
preferred refs by commits on either side.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:57:55 +02:00
co-authored by Claude Sonnet 5.5
parent 5cb809428b
commit 8aa12774eb
5 changed files with 153 additions and 65 deletions
+14 -1
View File
@@ -7,7 +7,8 @@ Each entry: context, decision, consequences. Newest last.
**Context.** Hashes don't tell users whether a pin is current.
**Decision.** Resolve each pin to a ref and show the distance (`origin/main ↓1`).
The resolution order is fixed and the applied rule is shown in the detail pane
(see [state-model.md](state-model.md#resolving-a-pin-to-a-ref)).
(see [state-model.md](state-model.md#resolving-a-pin-to-a-ref); amended:
the closest branch wins, see "Pin resolves to the closest branch").
**Consequences.** Resolution is a heuristic; showing the rule keeps it
transparent.
@@ -218,3 +219,15 @@ shown in a palette in the detail pane, next to what they act on; global keys
(hidden below 90 columns, or under 12 lines) the footer shows these keys
instead, so small terminals need no extra UI (no overlay or action menu).
Keys never depend on what is visible.
## Pin resolves to the closest branch
A pin on a pushed feature branch showed as diverged from `origin/main`,
because `branch = main` in `.gitmodules` won before any distance was looked
at. Now the closest branch containing the pin wins; the `.gitmodules`
branch, the upstream and the remote default branch only break ties (and are
always compared, even past the 16-branch cap). An unpushed pin compares only
those preferred refs, by commits on either side: scanning every branch's
merge base would be too slow. Cost: a pin on main far behind its tip shows a
younger branch that contains it if that branch's tip is closer, so "n behind
main" is no longer visible there; `via <rule>` still tells which ref it is.
+28 -14
View File
@@ -104,17 +104,30 @@ A descendant whose load failed (`error`) counts as one danger.
## Resolving a pin to a ref
The pin is displayed as a ref, not a hash. A commit can be on several
branches, on none, or deep in the history, so the git provider resolves it by
this order and records which rule applied in `PinInfo.Source`:
branches, on none, or deep in the history. The git provider shows the
**closest** branch and records which rule proposed it in `PinInfo.Source`.
Preferred refs, in this order (each only if it exists):
| Preference | Ref | `RefSource` |
|---|---|---|
| a | `branch = …` in `.gitmodules` | `RefFromGitmodules` |
| b | Upstream of the branch checked out in the submodule | `RefFromCurrentBranch` |
| c | Remote default branch (`<remote>/HEAD`) | `RefFromDefaultBranch` |
Resolution:
| # | Rule | `RefSource` |
|---|---|---|
| 1 | `branch = …` in `.gitmodules` | `RefFromGitmodules` |
| 2 | Branch currently checked out in the submodule | `RefFromCurrentBranch` |
| 3 | Remote default branch, if it contains the pin | `RefFromDefaultBranch` |
| 4 | Any remote branch containing the pin, preferring the closest tip (`git branch -r --contains`) | `RefFromContains` |
| 5 | A tag (`git describe`) | `RefFromTag` |
| 6 | Short SHA | `RefFromSHA` |
| 1 | Pin pushed: the branch containing the pin with the fewest commits after it. Candidates are the preferred refs that contain the pin plus any remote branch containing it (`for-each-ref --contains`); a tie goes to the preferred ref | preferred ref's, else `RefFromContains` |
| 2 | Pin unpushed: the preferred ref with the fewest commits on either side (`↑n↓m`), ties by preference | preferred ref's |
| 3 | A tag (`git describe`) | `RefFromTag` |
| 4 | Short SHA | `RefFromSHA` |
So a `.gitmodules` branch is shown when it is as close as any other branch,
but a pin that sits on a feature branch shows that branch rather than
"diverged from main". The trade-off: a pin on main that a younger branch
also contains shows the younger branch if its tip is fewer commits away.
Distances: `git rev-list --left-right --count <pin>...<ref>` gives
`Ahead` (left) and `Behind` (right).
@@ -143,12 +156,13 @@ are local; `<pin>` is the index gitlink.
Details:
- Rule 1: `branch = .` means the superproject's current branch. The rule
applies only if `<remote>/<branch>` exists locally.
- Rule 2 uses the checked-out branch's upstream (`<branch>@{upstream}`).
- Rule 4 compares at most 16 containing branches (oldest tip first) and
picks the one with the fewest commits after the pin.
- Rule 6 shows the short SHA; there is no tip and no distance.
- Preference a: `branch = .` means the superproject's current branch. The
ref counts only if `<remote>/<branch>` exists locally.
- Preference b uses the checked-out branch's upstream (`<branch>@{upstream}`).
- Rule 1 counts `rev-list --count <pin>..<ref>`. Besides the preferred refs
it compares at most 16 containing branches (oldest tip first).
- Rule 2 counts `rev-list --count <pin>...<ref>`.
- Rule 4 shows the short SHA; there is no tip and no distance.
- `--ignore-submodules=dirty`: changes inside a nested submodule do not make
its parent `changed` (D7), but a moved nested pin does.
- Renames are detected only for staged moves (`git mv`); an unstaged move