mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* 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>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/** A line segment drawn across one row, addressed by lane index. */
|
|
export interface CommitGraphLine {
|
|
column: number;
|
|
color: number;
|
|
}
|
|
|
|
export interface CommitGraphRow {
|
|
/** Lane holding this commit's node. */
|
|
column: number;
|
|
/** Colour index of the node and of the lane it continues. */
|
|
color: number;
|
|
/** Lanes entering the node from the row above (own lane and merged children). */
|
|
incoming: CommitGraphLine[];
|
|
/** Lanes leaving the node toward the row below (one per parent). */
|
|
outgoing: CommitGraphLine[];
|
|
/** Lanes that cross this row without touching the node. */
|
|
through: CommitGraphLine[];
|
|
/** Lanes still open below this row; drawn straight through expanded rows. */
|
|
after: CommitGraphLine[];
|
|
/** Highest lane index touched on this row plus one. */
|
|
width: number;
|
|
}
|
|
|
|
interface Lane {
|
|
sha: string;
|
|
color: number;
|
|
}
|
|
|
|
function firstFree(lanes: Array<Lane | null>): number {
|
|
const free = lanes.indexOf(null);
|
|
return free === -1 ? lanes.length : free;
|
|
}
|
|
|
|
/**
|
|
* Lay out commits (newest first, parents never before children) into lanes.
|
|
*
|
|
* Each lane waits for one commit. A commit takes the lowest lane waiting for
|
|
* it, or a fresh lane when none is; every other lane waiting for it merges in
|
|
* at its node, so a mainline never jogs sideways to meet a fork. Its first
|
|
* parent continues the lane; further parents each get the lane already
|
|
* waiting for them or a new one.
|
|
*/
|
|
export function layoutCommitGraph(
|
|
entries: ReadonlyArray<{ sha: string; parents: string[] }>,
|
|
): CommitGraphRow[] {
|
|
const lanes: Array<Lane | null> = [];
|
|
let nextColor = 0;
|
|
const rows: CommitGraphRow[] = [];
|
|
|
|
for (const entry of entries) {
|
|
const found = lanes.findIndex((lane) => lane?.sha === entry.sha);
|
|
const column = found === -1 ? firstFree(lanes) : found;
|
|
const color = found === -1 ? nextColor++ : lanes[found]!.color;
|
|
|
|
const incoming: CommitGraphLine[] = [];
|
|
const through: CommitGraphLine[] = [];
|
|
lanes.forEach((lane, index) => {
|
|
if (!lane) return;
|
|
if (lane.sha === entry.sha) {
|
|
incoming.push({ column: index, color: lane.color });
|
|
lanes[index] = null;
|
|
} else {
|
|
through.push({ column: index, color: lane.color });
|
|
}
|
|
});
|
|
|
|
const outgoing: CommitGraphLine[] = [];
|
|
entry.parents.forEach((parent, parentIndex) => {
|
|
if (parentIndex === 0) {
|
|
lanes[column] = { sha: parent, color };
|
|
outgoing.push({ column, color });
|
|
return;
|
|
}
|
|
const existing = lanes.findIndex((lane) => lane?.sha === parent);
|
|
if (existing !== -1) {
|
|
outgoing.push({ column: existing, color: lanes[existing]!.color });
|
|
} else {
|
|
const fresh = firstFree(lanes);
|
|
const freshColor = nextColor++;
|
|
lanes[fresh] = { sha: parent, color: freshColor };
|
|
outgoing.push({ column: fresh, color: freshColor });
|
|
}
|
|
});
|
|
|
|
while (lanes.length > 0 && lanes[lanes.length - 1] === null) lanes.pop();
|
|
|
|
const after: CommitGraphLine[] = [];
|
|
lanes.forEach((lane, index) => {
|
|
if (lane) after.push({ column: index, color: lane.color });
|
|
});
|
|
const width =
|
|
1 +
|
|
Math.max(
|
|
column,
|
|
...incoming.map((l) => l.column),
|
|
...outgoing.map((l) => l.column),
|
|
...through.map((l) => l.column),
|
|
);
|
|
rows.push({ column, color, incoming, outgoing, through, after, width });
|
|
}
|
|
return rows;
|
|
}
|