Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
/**
|
|
* Pure git-graph lane layout (L8 / git-graph view).
|
|
*
|
|
* `computeGraphRows` turns a newest-first commit list into render-ready rows that
|
|
* a renderer can draw as a GitKraken-style graph: **continuous coloured lanes**
|
|
* with curved connectors at branch points (forks) and merges.
|
|
*
|
|
* ## Model
|
|
*
|
|
* We keep an ordered list of "active lanes": `lanes[i]` is the hash of the commit
|
|
* we expect next on lane `i` (or `null` when the lane is free). A lane index is a
|
|
* stable column; we never re-index existing lanes (a freed lane is reused in
|
|
* place), so a branch that doesn't bend draws a **straight vertical line**.
|
|
*
|
|
* Each row is split into two halves around the commit node (drawn at mid-height):
|
|
* - `incoming` links go from the **top** boundary to the node's mid level,
|
|
* - `outgoing` links go from the node's mid level to the **bottom** boundary.
|
|
*
|
|
* Straight links (`fromLane === toLane`) are pass-through lane lines; bent links
|
|
* are the fork/merge connectors. Because lane x-positions are identical across
|
|
* rows, a row's `outgoing` meets the next row's `incoming` at the shared
|
|
* boundary, yielding unbroken lanes top-to-bottom.
|
|
*/
|
|
|
|
import type { GraphCommit } from "@/domain";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** A line segment within one half of a row. */
|
|
export interface GraphLink {
|
|
/** Lane index at the start of the segment. */
|
|
fromLane: number;
|
|
/** Lane index at the end of the segment. */
|
|
toLane: number;
|
|
/** Stroke colour. */
|
|
color: string;
|
|
}
|
|
|
|
export interface GraphRow {
|
|
commit: GraphCommit;
|
|
/** Lane (column) of this commit's node. */
|
|
lane: number;
|
|
/** Node colour (its lane's colour). */
|
|
color: string;
|
|
/** Links in the top half: top boundary → node mid. */
|
|
incoming: GraphLink[];
|
|
/** Links in the bottom half: node mid → bottom boundary. */
|
|
outgoing: GraphLink[];
|
|
/** Number of lane columns occupied around this row (for SVG width). */
|
|
laneCount: number;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Colour palette (dark-theme-friendly)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const LANE_COLORS: readonly string[] = [
|
|
"#4f9cf9", // blue
|
|
"#a78bfa", // violet
|
|
"#34d399", // emerald
|
|
"#fbbf24", // amber
|
|
"#f87171", // rose
|
|
"#38bdf8", // sky
|
|
"#fb923c", // orange
|
|
"#c084fc", // purple
|
|
"#f472b6", // pink
|
|
"#2dd4bf", // teal
|
|
];
|
|
|
|
/** Stable colour for a lane index. */
|
|
export function laneColor(laneIndex: number): string {
|
|
return LANE_COLORS[laneIndex % LANE_COLORS.length];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Core algorithm
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Index of the first free (null) lane, or -1. */
|
|
function firstFree(lanes: (string | null)[]): number {
|
|
return lanes.indexOf(null);
|
|
}
|
|
|
|
/** Places `hash` on a lane: an existing free slot if any, else a new column. */
|
|
function allocLane(lanes: (string | null)[], hash: string): number {
|
|
const free = firstFree(lanes);
|
|
if (free !== -1) {
|
|
lanes[free] = hash;
|
|
return free;
|
|
}
|
|
lanes.push(hash);
|
|
return lanes.length - 1;
|
|
}
|
|
|
|
/**
|
|
* Assigns lanes + links to each commit.
|
|
*
|
|
* @param commits Commits ordered **newest-first** (as `git log`).
|
|
*/
|
|
export function computeGraphRows(commits: GraphCommit[]): GraphRow[] {
|
|
// lanes[i] = hash expected next on lane i (null = free).
|
|
const lanes: (string | null)[] = [];
|
|
const rows: GraphRow[] = [];
|
|
|
|
for (const commit of commits) {
|
|
const { hash, parents } = commit;
|
|
const top = lanes.slice();
|
|
|
|
// ── Resolve the commit's lane ────────────────────────────────────────
|
|
let commitLane = lanes.indexOf(hash);
|
|
if (commitLane === -1) {
|
|
// A tip nobody pointed at yet: open a lane for it.
|
|
commitLane = allocLane(lanes, hash);
|
|
}
|
|
|
|
// Other lanes also expecting this hash converge into the node and end.
|
|
for (let i = 0; i < top.length; i++) {
|
|
if (i !== commitLane && top[i] === hash) lanes[i] = null;
|
|
}
|
|
|
|
// ── Route parents ────────────────────────────────────────────────────
|
|
const parentLanes: number[] = [];
|
|
if (parents.length === 0) {
|
|
lanes[commitLane] = null; // root: lane ends here.
|
|
} else {
|
|
lanes[commitLane] = parents[0]; // first parent keeps the lane.
|
|
parentLanes.push(commitLane);
|
|
for (let k = 1; k < parents.length; k++) {
|
|
const ph = parents[k];
|
|
const existing = lanes.indexOf(ph);
|
|
parentLanes.push(existing !== -1 ? existing : allocLane(lanes, ph));
|
|
}
|
|
}
|
|
|
|
const bottom = lanes.slice();
|
|
|
|
// ── Top half: every active top lane → node mid ───────────────────────
|
|
const incoming: GraphLink[] = [];
|
|
for (let i = 0; i < top.length; i++) {
|
|
if (top[i] === null) continue;
|
|
if (i === commitLane || top[i] === hash) {
|
|
// Into the node (straight if i === commitLane, a converging curve else).
|
|
incoming.push({ fromLane: i, toLane: commitLane, color: laneColor(i) });
|
|
} else {
|
|
incoming.push({ fromLane: i, toLane: i, color: laneColor(i) }); // pass-through
|
|
}
|
|
}
|
|
|
|
// ── Bottom half: node mid → every active bottom lane ─────────────────
|
|
const outgoing: GraphLink[] = [];
|
|
for (let j = 0; j < bottom.length; j++) {
|
|
if (bottom[j] === null) continue;
|
|
if (parentLanes.includes(j)) {
|
|
outgoing.push({ fromLane: commitLane, toLane: j, color: laneColor(j) }); // node → parent
|
|
} else {
|
|
outgoing.push({ fromLane: j, toLane: j, color: laneColor(j) }); // pass-through
|
|
}
|
|
}
|
|
|
|
rows.push({
|
|
commit,
|
|
lane: commitLane,
|
|
color: laneColor(commitLane),
|
|
incoming,
|
|
outgoing,
|
|
laneCount: Math.max(top.length, bottom.length, commitLane + 1),
|
|
});
|
|
}
|
|
|
|
return rows;
|
|
}
|