Skip to content

Commit graph: branch lanes drift horizontally when neighbouring branches end #2649

Description

@74nu5

What I'm seeing

In the history view, a branch does not keep a stable horizontal position. As soon as a
path to its left ends, every path to the right shifts one column left. On a repository
with many short-lived branches, following a single branch means tracking it across
several columns as you scroll.

SourceGit Another client, same repository
Image Image

Why it happens

Models/CommitGraph.Generate() derives a path's X coordinate from its rank in the
unsolved list, and that rank is recomputed on every row:

var offsetX = 4 - halfWidth;
foreach (var l in unsolved)
{
    if (l.Next.Equals(commit.SHA, StringComparison.Ordinal)) { /* ... */ }
    else
    {
        offsetX += unitWidth;   // position derives from rank in the list
        l.Pass(offsetX, offsetY, halfHeight);
    }
}

So a lane is a side effect of iteration order rather than a property of the path. When a
path is removed from unsolved, everything after it slides one column left for the rest
of its life. The drift is a direct consequence, not a rendering artefact.

What I would like to propose

Two independent changes, both behind an opt-in setting:

1. Stable lanes. Give PathHelper an immutable Lane assigned when the path is
created and released when it ends — the same recycling pattern ColorPicker.Recycle()
already uses for colours, applied to positions instead. A released lane is only reused
after a quarantine of ~25 rows, so two unrelated branches never appear back to back in
the same column.

2. A dedicated BRANCH / TAG column. Move CommitRefsPresenter out of the
graph-and-subject cell into its own column, and give the graph its own column too. Commit
subjects then align on a single X instead of each row carrying its own LeftMargin.
CommitRefsPresenter is already self-contained (it measures itself, draws its own chips
and hit-tests through DecoratorAt), so this is mostly a XAML move plus a horizontal
offset on the graph overlay, which currently assumes it starts at x = 0.

Current behaviour would remain the default. The setting would sit in the history options
menu next to Commit order, as Compact (default) / Stable lanes.

What it costs

Measured on a synthetic history of 2,000 commits with power-law branch lifetimes, so
mostly short branches and a few long ones:

Branches in window Compact (current) Stable lanes
10 4 lanes 4 lanes
100 26 lanes 30 lanes
200 51 lanes 54 lanes

The extra width does not scale with the branch count: it is bounded by how many branches
happen to end inside the quarantine window, which stays small regardless of repository
size. Worth noting that at 200 concurrent branches the current compact layout already
needs ~600 px of graph, so a width budget for the graph column would help both modes.

Rendering gets slightly cheaper rather than more expensive: with a fixed lane, Pass()
stops emitting a point per row, so a straight branch is two points instead of one per
commit.

How I would split it

If the direction is acceptable, I would send this as separate PRs against develop
rather than one large change:

  1. Add StartX to CommitGraphLayout and stop hard-coding the graph column index.
    No behaviour change at all — StartX is 0 until a graph column exists.
  2. Separate graph column, aligned subjects.
  3. BRANCH / TAG column with the existing refs presenter.
  4. Stable lane allocation and the settings entry.

Each one builds and is testable on its own, and the first is small enough to judge the
whole direction cheaply.

Questions before I write anything

  1. Is this something you would consider, or is the compact layout a deliberate design
    choice you would rather keep?
  2. If you are open to it, is an opt-in setting the right call, or would you prefer the
    new layout to simply replace the current one?
  3. Would separate PRs be welcome, or is that more review churn than you want for this?

Happy to drop it if the direction does not fit — I would rather ask before writing the
code than after.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions