daf5b7be92
- Changes panel: a working file opens in a CodeMirror editor with its diff inline (DiffEditor.tsx, @codemirror/merge, lazy-loaded); Changes folds the unchanged lines, Whole file shows them; side by side at 50/50 and full width; a Revert per block; original from HEAD or the branch's merge base (`mergeBase` on GET /git/changes?range=branch) - Every diff: green = line added whole, red = removed whole, blue = edited in place (--diff-mod) - History drawn as a commit graph (#58: `parents` on GET /git/log, --date-order; lib/commitGraph.ts) - Per-workspace never-stage list, drawn orange in the Files tree (--kept); closed Terminal rows past three deleted - Status bar plan quota for Claude (per account) and Codex (GET /api/pricing/quota, server/lib/quota.js); compact hover panel that stays open, Refresh button with a spinner - Settings → Appearance holds language, theme and pet; Ctrl+Shift+C copies; compact sidebar navigation and short session times
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
/**
|
|
* @file Lays out the Changes panel's History as a commit graph (the JetBrains /
|
|
* `git log --graph` shape): each commit gets a column, and each row lists the
|
|
* line segments drawn through it — a lane passing by, the lanes converging on
|
|
* the commit from above, and the lines leaving it for its parents below.
|
|
* Expects the commits in an order where no parent precedes a child
|
|
* (`git log --date-order`); a parent outside the list leaves its lane running
|
|
* off the bottom.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
/** One line in a row, in column units; y runs 0 (top) → 0.5 (node) → 1 (bottom). */
|
|
export interface GraphSeg {
|
|
x1: number;
|
|
y1: number;
|
|
x2: number;
|
|
y2: number;
|
|
color: number;
|
|
}
|
|
|
|
export interface GraphRow {
|
|
col: number;
|
|
color: number;
|
|
segs: GraphSeg[];
|
|
}
|
|
|
|
export function commitGraph(commits: { sha: string; parents: string[] }[]): {
|
|
rows: GraphRow[];
|
|
width: number;
|
|
} {
|
|
// lanes[j] = the sha the lane in column j is waiting for; colors[j] its hue.
|
|
const lanes: (string | null)[] = [];
|
|
const colors: number[] = [];
|
|
const hue = (j: number) => colors[j] ?? 0;
|
|
let nextColor = 0;
|
|
let width = 1;
|
|
const open = () => {
|
|
const free = lanes.indexOf(null);
|
|
const j = free < 0 ? lanes.length : free;
|
|
colors[j] = nextColor++;
|
|
return j;
|
|
};
|
|
|
|
const rows = commits.map((c) => {
|
|
const segs: GraphSeg[] = [];
|
|
let col = lanes.indexOf(c.sha);
|
|
// A tip nothing above points at: a lane of its own, no line from the top.
|
|
if (col < 0) {
|
|
col = open();
|
|
lanes[col] = null;
|
|
}
|
|
lanes.forEach((s, j) => {
|
|
if (s === null) return;
|
|
segs.push(
|
|
s === c.sha
|
|
? { x1: j, y1: 0, x2: col, y2: 0.5, color: hue(j) }
|
|
: { x1: j, y1: 0, x2: j, y2: 1, color: hue(j) }
|
|
);
|
|
});
|
|
const color = hue(col);
|
|
lanes.forEach((s, j) => {
|
|
if (s === c.sha) lanes[j] = null;
|
|
});
|
|
|
|
c.parents.forEach((p, i) => {
|
|
let j = i === 0 ? col : lanes.indexOf(p);
|
|
if (j < 0) j = open();
|
|
lanes[j] = p;
|
|
segs.push({ x1: col, y1: 0.5, x2: j, y2: 1, color: hue(j) });
|
|
});
|
|
while (lanes.length && lanes[lanes.length - 1] === null) lanes.pop();
|
|
width = Math.max(width, lanes.length, col + 1);
|
|
return { col, color, segs };
|
|
});
|
|
return { rows, width };
|
|
}
|