Skip to content

Commit bd0eab3

Browse files
committed
fix(graph): cycle overflow lane colors instead of collapsing to blue (#59)
When more lanes were active than the 12-color palette, pickColor fell through its loop and always returned color 0, so every lane past the 12th rendered blue. Use a rotating fallback index so overflow lanes keep cycling through the palette.
1 parent 760ba0f commit bd0eab3

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

src/git/__tests__/git-graph-builder.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,26 @@ describe('buildFullGraph color palette exhaustion', () => {
264264
const colors = graph.paths.map(p => p.color);
265265
expect(new Set(colors).size).toBeLessThan(colors.length);
266266
});
267+
268+
it('cycles overflow colors instead of collapsing them all to color 0', () => {
269+
// Regression for #59: with more simultaneously-active lanes than palette
270+
// colors, pickColor used to return 0 for every overflow lane, so all lanes
271+
// past the 12th rendered the same (blue) color. An octopus merge with 14
272+
// parents creates 14 lanes over a 12-color palette; each color should be
273+
// reused at most twice instead of color 0 absorbing every overflow lane.
274+
const parents = Array.from({ length: 14 }, (_, i) => `p${i + 1}`);
275+
const commits = [
276+
makeCommit('M', parents),
277+
...parents.map(p => makeCommit(p, [])),
278+
];
279+
const graph = buildFullGraph(commits);
280+
const counts = new Map<number, number>();
281+
for (const path of graph.paths) {
282+
counts.set(path.color, (counts.get(path.color) ?? 0) + 1);
283+
}
284+
const maxReuse = Math.max(...counts.values());
285+
expect(maxReuse).toBeLessThanOrEqual(2);
286+
});
267287
});
268288

269289
describe('buildFullGraph upstream-based remote-only detection', () => {

src/git/git-graph-builder.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ function buildPushedSet(commits: Commit[], hashIndex: Map<string, number>): Set<
245245
return pushed;
246246
}
247247

248-
function pickColor(unsolved: PathHelper[]): number {
248+
function pickColor(unsolved: PathHelper[], overflow: { n: number }): number {
249249
// Track used colors in a bitmask (palette is < 32 colors) instead of allocating an
250250
// array + Set on every call. O(lanes), allocation-free. This runs once per new
251251
// branch head and per merge parent, so it adds up on graphs with many lanes.
@@ -257,7 +257,10 @@ function pickColor(unsolved: PathHelper[]): number {
257257
for (let i = 0; i < COLOR_PALETTE.length; i++) {
258258
if ((mask & (1 << i)) === 0) return i;
259259
}
260-
return 0;
260+
// All palette colors are in use by simultaneously-active lanes (more lanes
261+
// than colors). Keep cycling through the palette instead of always returning
262+
// 0, so overflow lanes stay visually distinct rather than all turning blue.
263+
return overflow.n++ % COLOR_PALETTE.length;
261264
}
262265

263266
// ── Main parse function (SourceGit CommitGraph.Parse port) ──
@@ -281,6 +284,9 @@ export function buildFullGraph(
281284

282285
const unsolved: PathHelper[] = [];
283286
const ended: PathHelper[] = [];
287+
// Rotating fallback index for pickColor: advances each time the palette is
288+
// fully in use so overflow lanes cycle through colors instead of all reusing 0.
289+
const colorOverflow = { n: 0 };
284290
// Track the rail (PathHelper) each dot sits on so we can backfill the dot's
285291
// pattern color after the loop. A rail's override may be set by a tip that
286292
// appears lower on the rail than commits already processed top-to-bottom;
@@ -363,7 +369,7 @@ export function buildFullGraph(
363369
if (major === null) {
364370
offsetX += UNIT_W;
365371
if (commit.parents.length > 0) {
366-
major = new PathHelper(commit.parents[0], pickColor(unsolved), { x: offsetX, y: offsetY });
372+
major = new PathHelper(commit.parents[0], pickColor(unsolved, colorOverflow), { x: offsetX, y: offsetY });
367373
unsolved.push(major);
368374
trackNext(major);
369375
result.paths.push(major.path);
@@ -408,7 +414,7 @@ export function buildFullGraph(
408414
} else {
409415
// New path for merge parent
410416
offsetX += UNIT_W;
411-
const l = new PathHelper(parentHash, pickColor(unsolved), position, { x: offsetX, y: position.y + HALF_H });
417+
const l = new PathHelper(parentHash, pickColor(unsolved, colorOverflow), position, { x: offsetX, y: position.y + HALF_H });
412418
unsolved.push(l);
413419
trackNext(l);
414420
result.paths.push(l.path);

0 commit comments

Comments
 (0)