gites: colorful git tree viewer
Tauri 2 + Vue app that draws the commit graph of the repository given on the command line. Branches can be hidden one by one or by patterns, runs of plain commits fold into a dotted line, and everything is reachable from the keyboard.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildRows } from "./collapse";
|
||||
import { chain } from "./test-helpers";
|
||||
|
||||
const hashes = (r: ReturnType<typeof buildRows>) => r.rows.map((x) => x.commit.hash);
|
||||
|
||||
describe("buildRows", () => {
|
||||
// top is labeled; p1..p4 are plain; root has no parent so it is not plain.
|
||||
const commits = chain("top:p1", "p1:p2", "p2:p3", "p3:p4", "p4:root", "root");
|
||||
const labeled = new Set(["top"]);
|
||||
|
||||
it("shows everything when not collapsing", () => {
|
||||
const r = buildRows(commits, labeled, false, new Set());
|
||||
expect(hashes(r)).toEqual(["top", "p1", "p2", "p3", "p4", "root"]);
|
||||
expect(r.rows[1].parents).toEqual([{ hash: "p2", dotted: false }]);
|
||||
});
|
||||
|
||||
it("keeps the first and last commit of a run and joins them with a dotted edge", () => {
|
||||
const r = buildRows(commits, labeled, true, new Set());
|
||||
expect(hashes(r)).toEqual(["top", "p1", "p4", "root"]);
|
||||
expect(r.rows[1].parents).toEqual([{ hash: "p4", dotted: true }]);
|
||||
expect(r.rows[1].hiddenCount).toBe(2);
|
||||
});
|
||||
|
||||
it("gives every commit of the run the same run id", () => {
|
||||
const r = buildRows(commits, labeled, true, new Set());
|
||||
const id = r.runOf.get("p1");
|
||||
expect(id).toBeDefined();
|
||||
expect(["p2", "p3", "p4"].map((h) => r.runOf.get(h))).toEqual([id, id, id]);
|
||||
expect(r.runOf.get("top")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("a toggled run does the opposite of the global setting", () => {
|
||||
const id = buildRows(commits, labeled, true, new Set()).runOf.get("p1")!;
|
||||
expect(hashes(buildRows(commits, labeled, true, new Set([id])))).toHaveLength(6);
|
||||
expect(hashes(buildRows(commits, labeled, false, new Set([id])))).toEqual(["top", "p1", "p4", "root"]);
|
||||
});
|
||||
|
||||
it("does not collapse runs shorter than three", () => {
|
||||
const short = chain("top:p1", "p1:p2", "p2:root", "root");
|
||||
expect(hashes(buildRows(short, new Set(["top"]), true, new Set()))).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("stops a run at merges, forks and labels", () => {
|
||||
// b is a fork point (two children), m is a merge.
|
||||
const c = chain("m:x1,y1", "y1:b", "x1:x2", "x2:x3", "x3:b", "b:z", "z");
|
||||
const r = buildRows(c, new Set(["m"]), true, new Set());
|
||||
expect(hashes(r)).toEqual(["m", "y1", "x1", "x3", "b", "z"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Commit, DisplayRow } from "./types";
|
||||
|
||||
export interface RowsResult {
|
||||
rows: DisplayRow[];
|
||||
/** Commit hash → id of the run of plain commits it belongs to. */
|
||||
runOf: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the rows to show. A run is a chain of plain commits: one parent, one child,
|
||||
* no label. A run of three or more is collapsed when `collapseAll` differs from
|
||||
* `toggled.has(runId)`; only its first and last commits stay, joined by a dotted edge.
|
||||
*/
|
||||
export function buildRows(
|
||||
commits: Commit[],
|
||||
labeled: Set<string>,
|
||||
collapseAll: boolean,
|
||||
toggled: Set<string>,
|
||||
): RowsResult {
|
||||
const byHash = new Map(commits.map((c) => [c.hash, c]));
|
||||
const childCount = new Map<string, number>();
|
||||
const childOf = new Map<string, string>();
|
||||
for (const c of commits) {
|
||||
for (const p of c.parents) {
|
||||
childCount.set(p, (childCount.get(p) ?? 0) + 1);
|
||||
childOf.set(p, c.hash);
|
||||
}
|
||||
}
|
||||
const plain = (h: string | undefined) => {
|
||||
const c = h === undefined ? undefined : byHash.get(h);
|
||||
return !!c && c.parents.length === 1 && childCount.get(c.hash) === 1 && !labeled.has(c.hash);
|
||||
};
|
||||
|
||||
const runOf = new Map<string, string>();
|
||||
const hidden = new Set<string>();
|
||||
/** First commit of a collapsed run → [last commit, number hidden]. */
|
||||
const jump = new Map<string, [string, number]>();
|
||||
|
||||
for (const c of commits) {
|
||||
if (!plain(c.hash) || plain(childOf.get(c.hash))) continue;
|
||||
// c starts a run: follow first parents while they stay plain.
|
||||
const run = [c.hash];
|
||||
let next = c.parents[0];
|
||||
while (plain(next) && !runOf.has(next)) {
|
||||
run.push(next);
|
||||
next = byHash.get(next)!.parents[0];
|
||||
}
|
||||
const id = c.hash;
|
||||
for (const h of run) runOf.set(h, id);
|
||||
if (run.length >= 3 && collapseAll !== toggled.has(id)) {
|
||||
for (const h of run.slice(1, -1)) hidden.add(h);
|
||||
jump.set(c.hash, [run[run.length - 1], run.length - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
const rows: DisplayRow[] = [];
|
||||
for (const c of commits) {
|
||||
if (hidden.has(c.hash)) continue;
|
||||
const j = jump.get(c.hash);
|
||||
rows.push(
|
||||
j
|
||||
? { commit: c, parents: [{ hash: j[0], dotted: true }], hiddenCount: j[1] }
|
||||
: { commit: c, parents: c.parents.map((hash) => ({ hash, dotted: false })) },
|
||||
);
|
||||
}
|
||||
return { rows, runOf };
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { globMatch, isRefVisible, lineOwners, reachable, refKind, shortName } from "./filter";
|
||||
import { chain } from "./test-helpers";
|
||||
|
||||
describe("ref names", () => {
|
||||
it("knows the kind and the short name", () => {
|
||||
expect(refKind("refs/heads/main")).toBe("local");
|
||||
expect(refKind("refs/remotes/origin/main")).toBe("remote");
|
||||
expect(refKind("refs/tags/v1")).toBe("tag");
|
||||
expect(shortName("refs/heads/feat/x")).toBe("feat/x");
|
||||
expect(shortName("refs/remotes/origin/main")).toBe("origin/main");
|
||||
expect(shortName("refs/tags/v1")).toBe("v1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("globMatch", () => {
|
||||
it("matches * across any characters, including /", () => {
|
||||
expect(globMatch("prototypes/*", "prototypes/a")).toBe(true);
|
||||
expect(globMatch("prototypes/*", "prototypes/a/b")).toBe(true);
|
||||
expect(globMatch("prototypes/*", "main")).toBe(false);
|
||||
expect(globMatch("*/main", "origin/main")).toBe(true);
|
||||
});
|
||||
it("matches ? as one character and treats other characters literally", () => {
|
||||
expect(globMatch("v?.0", "v1.0")).toBe(true);
|
||||
expect(globMatch("v?.0", "v1x0")).toBe(false);
|
||||
expect(globMatch("a+b", "a+b")).toBe(true);
|
||||
});
|
||||
it("matches the whole name only", () => {
|
||||
expect(globMatch("main", "origin/main")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRefVisible", () => {
|
||||
it("hides refs hidden with the eye toggle", () => {
|
||||
const s = { hidden: ["refs/heads/x"], rules: [] };
|
||||
expect(isRefVisible("refs/heads/x", s)).toBe(false);
|
||||
expect(isRefVisible("refs/heads/y", s)).toBe(true);
|
||||
});
|
||||
it("hides refs that match a hide rule", () => {
|
||||
const s = { hidden: [], rules: [{ pattern: "prototypes/*", mode: "hide" as const }] };
|
||||
expect(isRefVisible("refs/heads/prototypes/a", s)).toBe(false);
|
||||
expect(isRefVisible("refs/heads/main", s)).toBe(true);
|
||||
});
|
||||
it("with only rules, shows only refs that match one, and hide rules still win", () => {
|
||||
const s = {
|
||||
hidden: [],
|
||||
rules: [
|
||||
{ pattern: "origin/*", mode: "only" as const },
|
||||
{ pattern: "origin/old", mode: "hide" as const },
|
||||
],
|
||||
};
|
||||
expect(isRefVisible("refs/remotes/origin/main", s)).toBe(true);
|
||||
expect(isRefVisible("refs/remotes/origin/old", s)).toBe(false);
|
||||
expect(isRefVisible("refs/heads/main", s)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reachable", () => {
|
||||
it("keeps only ancestors of the start commits, in order", () => {
|
||||
// d is a side branch off b; e is an unrelated branch tip.
|
||||
const commits = chain("e:a", "d:b", "c:b", "b:a", "a");
|
||||
expect(reachable(commits, ["c"]).map((c) => c.hash)).toEqual(["c", "b", "a"]);
|
||||
expect(reachable(commits, ["d", "e"]).map((c) => c.hash)).toEqual(["e", "d", "b", "a"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lineOwners", () => {
|
||||
it("gives each commit the ref whose first-parent line it is on, checked-out branch first", () => {
|
||||
const commits = chain("m:c,f", "f:b", "c:b", "b:a", "a");
|
||||
const refs = [
|
||||
{ name: "refs/heads/feature", target: "f" },
|
||||
{ name: "refs/heads/main", target: "m" },
|
||||
];
|
||||
const owners = lineOwners(commits, refs, "refs/heads/main");
|
||||
expect(owners.get("m")).toBe("refs/heads/main");
|
||||
expect(owners.get("c")).toBe("refs/heads/main");
|
||||
expect(owners.get("a")).toBe("refs/heads/main");
|
||||
expect(owners.get("f")).toBe("refs/heads/feature");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Commit, FilterState, Ref, RefKind } from "./types";
|
||||
|
||||
export function refKind(name: string): RefKind {
|
||||
if (name.startsWith("refs/remotes/")) return "remote";
|
||||
if (name.startsWith("refs/tags/")) return "tag";
|
||||
return "local";
|
||||
}
|
||||
|
||||
export function shortName(name: string): string {
|
||||
return name.replace(/^refs\/(heads|remotes|tags)\//, "");
|
||||
}
|
||||
|
||||
/** `*` matches any run of characters (including `/`), `?` matches one character. */
|
||||
export function globMatch(pattern: string, text: string): boolean {
|
||||
const re = pattern
|
||||
.split("")
|
||||
.map((ch) => (ch === "*" ? ".*" : ch === "?" ? "." : ch.replace(/[.+^${}()|[\]\\]/g, "\\$&")))
|
||||
.join("");
|
||||
return new RegExp(`^${re}$`).test(text);
|
||||
}
|
||||
|
||||
export function isRefVisible(name: string, state: FilterState): boolean {
|
||||
if (state.hidden.includes(name)) return false;
|
||||
const short = shortName(name);
|
||||
const only = state.rules.filter((r) => r.mode === "only");
|
||||
if (only.length > 0 && !only.some((r) => globMatch(r.pattern, short))) return false;
|
||||
return !state.rules.some((r) => r.mode === "hide" && globMatch(r.pattern, short));
|
||||
}
|
||||
|
||||
/** Commits reachable from `starts`, in the original order. */
|
||||
export function reachable(commits: Commit[], starts: Iterable<string>): Commit[] {
|
||||
const byHash = new Map(commits.map((c) => [c.hash, c]));
|
||||
const seen = new Set<string>();
|
||||
const stack = [...starts];
|
||||
while (stack.length > 0) {
|
||||
const h = stack.pop()!;
|
||||
if (seen.has(h)) continue;
|
||||
const c = byHash.get(h);
|
||||
if (!c) continue;
|
||||
seen.add(h);
|
||||
stack.push(...c.parents);
|
||||
}
|
||||
return commits.filter((c) => seen.has(c.hash));
|
||||
}
|
||||
|
||||
const kindOrder: Record<RefKind, number> = { local: 0, remote: 1, tag: 2 };
|
||||
|
||||
/** For each commit, the ref whose first-parent line it lies on. */
|
||||
export function lineOwners(commits: Commit[], refs: Ref[], headRef: string | null): Map<string, string> {
|
||||
const byHash = new Map(commits.map((c) => [c.hash, c]));
|
||||
const ordered = [...refs].sort(
|
||||
(a, b) =>
|
||||
Number(b.name === headRef) - Number(a.name === headRef) ||
|
||||
kindOrder[refKind(a.name)] - kindOrder[refKind(b.name)],
|
||||
);
|
||||
const owners = new Map<string, string>();
|
||||
for (const ref of ordered) {
|
||||
let c = byHash.get(ref.target);
|
||||
while (c && !owners.has(c.hash)) {
|
||||
owners.set(c.hash, ref.name);
|
||||
c = byHash.get(c.parents[0]);
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { layout } from "./layout";
|
||||
|
||||
const row = (hash: string, ...parents: string[]) => ({
|
||||
commit: { hash },
|
||||
parents: parents.map((p) => ({ hash: p.replace("~", ""), dotted: p.startsWith("~") })),
|
||||
});
|
||||
|
||||
describe("layout", () => {
|
||||
it("puts a straight line in one column with one color", () => {
|
||||
const r = layout([row("c", "b"), row("b", "a"), row("a")]);
|
||||
expect(r.rows.map((x) => x.col)).toEqual([0, 0, 0]);
|
||||
expect(new Set(r.rows.map((x) => x.color)).size).toBe(1);
|
||||
expect(r.width).toBe(1);
|
||||
});
|
||||
|
||||
it("opens a second column for a merged branch and joins it back", () => {
|
||||
const r = layout([row("m", "a", "b"), row("b", "a"), row("a")]);
|
||||
expect(r.rows.map((x) => x.col)).toEqual([0, 1, 0]);
|
||||
expect(r.rows[1].color).not.toBe(r.rows[0].color);
|
||||
expect(r.width).toBe(2);
|
||||
// Below b, its line bends from column 1 back into column 0.
|
||||
expect(r.rows[1].segments).toContainEqual(
|
||||
expect.objectContaining({ x1: 1, y1: 0.5, x2: 0, y2: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("draws a line that passes a row as a straight segment", () => {
|
||||
const r = layout([row("m", "a", "b"), row("b", "a"), row("a")]);
|
||||
// At row b, column 0 is still waiting for a.
|
||||
expect(r.rows[1].segments).toContainEqual(
|
||||
expect.objectContaining({ x1: 0, y1: 0, x2: 0, y2: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("starts a new column for a second branch tip", () => {
|
||||
const r = layout([row("x", "a"), row("y", "a"), row("a")]);
|
||||
expect(r.rows.map((x) => x.col)).toEqual([0, 1, 0]);
|
||||
});
|
||||
|
||||
it("carries dotted edges down to the parent", () => {
|
||||
const r = layout([row("m", "a", "~b"), row("z", "q"), row("q"), row("b", "a"), row("a")]);
|
||||
const dotted = r.rows.flatMap((x) => x.segments).filter((s) => s.dotted);
|
||||
expect(dotted.length).toBeGreaterThan(0);
|
||||
expect(r.rows[3].segments.some((s) => s.dotted && s.y2 === 0.5)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { LaidOutRow, Segment } from "./types";
|
||||
|
||||
type Lane = { hash: string; color: number; dotted: boolean } | null;
|
||||
|
||||
/**
|
||||
* Assigns each row a column and computes the line pieces drawn in it. A column
|
||||
* ("lane") holds the commit it is waiting for; rows must come children before parents.
|
||||
*/
|
||||
export function layout(rows: { commit: { hash: string }; parents: { hash: string; dotted: boolean }[] }[]): {
|
||||
rows: LaidOutRow[];
|
||||
width: number;
|
||||
} {
|
||||
const lanes: Lane[] = [];
|
||||
let nextColor = 0;
|
||||
let width = 0;
|
||||
const firstFree = () => {
|
||||
const i = lanes.indexOf(null);
|
||||
return i >= 0 ? i : lanes.length;
|
||||
};
|
||||
|
||||
const out = rows.map((row): LaidOutRow => {
|
||||
const segments: Segment[] = [];
|
||||
const hash = row.commit.hash;
|
||||
let col = lanes.findIndex((l) => l?.hash === hash);
|
||||
let color: number;
|
||||
if (col < 0) {
|
||||
col = firstFree();
|
||||
color = nextColor++;
|
||||
} else {
|
||||
color = lanes[col]!.color;
|
||||
}
|
||||
|
||||
// Top half: lines arriving at this commit, and lines passing by.
|
||||
lanes.forEach((l, i) => {
|
||||
if (!l) return;
|
||||
if (l.hash === hash) {
|
||||
segments.push({ x1: i, y1: 0, x2: col, y2: 0.5, color: l.color, dotted: l.dotted });
|
||||
lanes[i] = null;
|
||||
} else {
|
||||
segments.push({ x1: i, y1: 0, x2: i, y2: 1, color: l.color, dotted: l.dotted });
|
||||
}
|
||||
});
|
||||
|
||||
// Bottom half: one line per parent.
|
||||
row.parents.forEach((p, k) => {
|
||||
let j = lanes.findIndex((l) => l?.hash === p.hash);
|
||||
if (j < 0) {
|
||||
j = k === 0 ? col : firstFree();
|
||||
lanes[j] = { hash: p.hash, color: k === 0 ? color : nextColor++, dotted: p.dotted };
|
||||
}
|
||||
const lineColor = k === 0 ? color : lanes[j]!.color;
|
||||
segments.push({ x1: col, y1: 0.5, x2: j, y2: 1, color: lineColor, dotted: p.dotted });
|
||||
});
|
||||
|
||||
while (lanes.length > 0 && lanes[lanes.length - 1] === null) lanes.pop();
|
||||
for (const s of segments) width = Math.max(width, s.x1 + 1, s.x2 + 1);
|
||||
width = Math.max(width, col + 1);
|
||||
return { col, color, segments };
|
||||
});
|
||||
|
||||
return { rows: out, width };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Line colors, picked to stay bright and tell apart on the dark background. */
|
||||
export const palette = [
|
||||
"#4dabf7", "#ff6b6b", "#69db7c", "#ffa94d", "#b197fc", "#38d9a9",
|
||||
"#f783ac", "#ffd43b", "#748ffc", "#ff8787", "#63e6be", "#e599f7",
|
||||
];
|
||||
|
||||
export const colorOf = (i: number) => palette[i % palette.length];
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Commit } from "./types";
|
||||
|
||||
/** Builds commits from `"hash:parent1,parent2"` strings, newest first. */
|
||||
export function chain(...specs: string[]): Commit[] {
|
||||
return specs.map((s) => {
|
||||
const [hash, parents = ""] = s.split(":");
|
||||
return {
|
||||
hash,
|
||||
short: hash,
|
||||
parents: parents ? parents.split(",") : [],
|
||||
author: "a",
|
||||
time: 0,
|
||||
subject: hash,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTree, refsUnder } from "./tree";
|
||||
|
||||
const item = (short: string) => ({ short, ref: { name: `refs/heads/${short}`, target: short } });
|
||||
const shape = (nodes: ReturnType<typeof buildTree>): unknown =>
|
||||
nodes.map((n) => (n.children.length ? { [n.label]: shape(n.children) } : n.label));
|
||||
|
||||
describe("buildTree", () => {
|
||||
it("groups names by / with folders first", () => {
|
||||
const t = buildTree(["main", "feat/b", "feat/a", "prototypes/x/y", "dev"].map(item));
|
||||
expect(shape(t)).toEqual([{ feat: ["a", "b"] }, { prototypes: [{ x: ["y"] }] }, "dev", "main"]);
|
||||
});
|
||||
it("keeps a ref that is also a folder", () => {
|
||||
const t = buildTree(["a", "a/b"].map(item));
|
||||
expect(t).toHaveLength(1);
|
||||
expect(t[0].ref?.name).toBe("refs/heads/a");
|
||||
expect(t[0].children[0].ref?.name).toBe("refs/heads/a/b");
|
||||
});
|
||||
it("gives each node its full path", () => {
|
||||
const t = buildTree(["feat/x/y"].map(item));
|
||||
expect(t[0].children[0].children[0].path).toBe("feat/x/y");
|
||||
});
|
||||
});
|
||||
|
||||
describe("refsUnder", () => {
|
||||
it("collects every ref in a subtree", () => {
|
||||
const t = buildTree(["feat/a", "feat/b/c", "main"].map(item));
|
||||
expect(refsUnder(t[0]).map((r) => r.target).sort()).toEqual(["feat/a", "feat/b/c"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Ref } from "./types";
|
||||
|
||||
export interface TreeNode {
|
||||
/** One `/`-separated part of the name. */
|
||||
label: string;
|
||||
/** The name up to and including this part; unique within one tree. */
|
||||
path: string;
|
||||
children: TreeNode[];
|
||||
/** Set when a ref ends here. A node can be both a ref and a folder (`a` and `a/b`). */
|
||||
ref?: Ref;
|
||||
}
|
||||
|
||||
/** Splits short names by `/` into a tree. Folders come before refs, each sorted by name. */
|
||||
export function buildTree(items: { short: string; ref: Ref }[]): TreeNode[] {
|
||||
const root: TreeNode = { label: "", path: "", children: [] };
|
||||
for (const { short, ref } of items) {
|
||||
let node = root;
|
||||
for (const part of short.split("/")) {
|
||||
const path = node.path ? `${node.path}/${part}` : part;
|
||||
let child = node.children.find((c) => c.label === part);
|
||||
if (!child) {
|
||||
child = { label: part, path, children: [] };
|
||||
node.children.push(child);
|
||||
}
|
||||
node = child;
|
||||
}
|
||||
node.ref = ref;
|
||||
}
|
||||
const sort = (nodes: TreeNode[]) => {
|
||||
nodes.sort(
|
||||
(a, b) => Number(b.children.length > 0) - Number(a.children.length > 0) || a.label.localeCompare(b.label),
|
||||
);
|
||||
nodes.forEach((n) => sort(n.children));
|
||||
};
|
||||
sort(root.children);
|
||||
return root.children;
|
||||
}
|
||||
|
||||
/** Every ref in the subtree. */
|
||||
export function refsUnder(node: TreeNode): Ref[] {
|
||||
return [...(node.ref ? [node.ref] : []), ...node.children.flatMap(refsUnder)];
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
export interface Commit {
|
||||
hash: string;
|
||||
short: string;
|
||||
parents: string[];
|
||||
author: string;
|
||||
time: number;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export interface Ref {
|
||||
/** Full name, e.g. `refs/heads/main`. */
|
||||
name: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface RepoData {
|
||||
root: string;
|
||||
commits: Commit[];
|
||||
refs: Ref[];
|
||||
head_ref: string | null;
|
||||
head: string | null;
|
||||
}
|
||||
|
||||
export type RefKind = "local" | "remote" | "tag";
|
||||
|
||||
export interface FilterRule {
|
||||
pattern: string;
|
||||
/** `hide` hides matching refs; `only` shows only refs that match some `only` rule. */
|
||||
mode: "hide" | "only";
|
||||
}
|
||||
|
||||
export interface FilterState {
|
||||
/** Full ref names hidden with the eye toggle. */
|
||||
hidden: string[];
|
||||
rules: FilterRule[];
|
||||
}
|
||||
|
||||
/** A commit as shown on screen, with its parent edges after collapsing. */
|
||||
export interface DisplayRow {
|
||||
commit: Commit;
|
||||
parents: { hash: string; dotted: boolean }[];
|
||||
/** Set on the first row of a collapsed run: how many commits are hidden below it. */
|
||||
hiddenCount?: number;
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
/** Column numbers; y is 0 (row top), 0.5 (row middle) or 1 (row bottom). */
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
color: number;
|
||||
dotted: boolean;
|
||||
}
|
||||
|
||||
export interface LaidOutRow {
|
||||
col: number;
|
||||
color: number;
|
||||
segments: Segment[];
|
||||
}
|
||||
Reference in New Issue
Block a user