Files
qwen-code/packages/web-shell/client/utils/commitGraph.test.ts
Shaojin Wenandprobe f61cd4bfaa feat(web-shell): draw the commit history as a lane graph, across all branches, with search (#12152)
* feat(web-shell): draw the commit history as a lane graph, across all branches, with search

The history tab listed HEAD's commits as a flat list: branch structure was invisible, other branches were unreachable without a checkout, and there was no way to find a commit by message, author, or hash.

The daemon log route now takes `all=1` to walk HEAD plus every local branch, remote branch, and tag, and `search=<text>` to keep only commits whose message, author, or hash prefix matches (case-insensitive fixed strings; git ANDs `--grep` with `--author`, so the two walks are unioned and paged by commit date). Every walk is `--date-order`, so a parent never precedes a child and the client can lay out lanes from `parents` alone.

The history tab gains a search field, an "All branches" toggle, and a lane graph column: one lane per concurrent line of development, S-curves at forks and merges, hollow nodes for merge commits, and lanes drawn straight through an expanded commit's detail block. The branch chip offers a "History" action next to "View Changes".

Part of #11941.

* fix(web-shell): keep the History action visible under the branch picker search

The picker hides its action list while the search box has text unless one of the known action labels matches the query; the new History entry was missing from that list, so typing part of its name hid it instead of finding it.

---------

Co-authored-by: probe <probe@local>
2026-09-18 08:32:35 +00:00

95 lines
3.1 KiB
TypeScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { layoutCommitGraph } from './commitGraph';
const c = (sha: string, ...parents: string[]) => ({ sha, parents });
describe('layoutCommitGraph', () => {
it('keeps a linear history in one lane with one colour', () => {
const rows = layoutCommitGraph([c('c', 'b'), c('b', 'a'), c('a')]);
expect(rows.map((r) => r.column)).toEqual([0, 0, 0]);
expect(rows.map((r) => r.color)).toEqual([0, 0, 0]);
expect(rows[0].incoming).toEqual([]);
expect(rows[0].outgoing).toEqual([{ column: 0, color: 0 }]);
expect(rows[1].incoming).toEqual([{ column: 0, color: 0 }]);
expect(rows[2].outgoing).toEqual([]);
expect(rows[2].after).toEqual([]);
expect(rows.every((r) => r.width === 1)).toBe(true);
});
it('opens a second lane for a merge and closes it at the fork point', () => {
// m merges f into main; both descend from base.
const rows = layoutCommitGraph([
c('m', 'main1', 'f'),
c('f', 'base'),
c('main1', 'base'),
c('base'),
]);
const [merge, feature, main1, base] = rows;
expect(merge.column).toBe(0);
expect(merge.outgoing).toEqual([
{ column: 0, color: 0 },
{ column: 1, color: 1 },
]);
expect(merge.width).toBe(2);
expect(feature.column).toBe(1);
expect(feature.color).toBe(1);
expect(feature.through).toEqual([{ column: 0, color: 0 }]);
expect(feature.outgoing).toEqual([{ column: 1, color: 1 }]);
expect(main1.column).toBe(0);
expect(main1.through).toEqual([{ column: 1, color: 1 }]);
expect(main1.width).toBe(2);
// Both lanes wait for base; they converge on the lowest one at its node.
expect(base.column).toBe(0);
expect(base.incoming).toEqual([
{ column: 0, color: 0 },
{ column: 1, color: 1 },
]);
expect(base.after).toEqual([]);
});
it('merges every lane waiting for a commit into its node', () => {
// Two branch tips share the same parent.
const rows = layoutCommitGraph([c('x', 'p'), c('y', 'p'), c('p')]);
expect(rows[1].column).toBe(1);
expect(rows[1].outgoing).toEqual([{ column: 1, color: 1 }]);
expect(rows[2].column).toBe(0);
expect(rows[2].incoming).toEqual([
{ column: 0, color: 0 },
{ column: 1, color: 1 },
]);
expect(rows[2].through).toEqual([]);
});
it('reuses freed lanes before opening new ones', () => {
const rows = layoutCommitGraph([
c('a', 'b'),
c('t1', 'old'),
c('b', 'c'),
c('t2', 'old2'),
c('c'),
]);
expect(rows.map((r) => r.column)).toEqual([0, 1, 0, 2, 0]);
// c has no parents: lane 0 closes and the trailing lanes stay open.
expect(rows[4].after).toEqual([
{ column: 1, color: 1 },
{ column: 2, color: 2 },
]);
});
it('gives an unseen parent of a merge a fresh lane and colour', () => {
const rows = layoutCommitGraph([c('m', 'a', 'b'), c('a'), c('b')]);
expect(rows[0].outgoing).toEqual([
{ column: 0, color: 0 },
{ column: 1, color: 1 },
]);
expect(rows[2].column).toBe(1);
expect(rows[2].color).toBe(1);
});
});