Fold a whole run of plain commits, so only its labelled ends stay
This commit is contained in:
+5
-5
@@ -156,15 +156,15 @@ async function copyHash() {
|
||||
}
|
||||
|
||||
function toggleRun(hash: string) {
|
||||
const id = built.value.runOf.get(hash);
|
||||
if (!id) return;
|
||||
const ids = built.value.runsAt.get(hash);
|
||||
if (!ids) return;
|
||||
const next = new Set(toggled.value);
|
||||
if (!next.delete(id)) next.add(id);
|
||||
for (const id of ids) if (!next.delete(id)) next.add(id);
|
||||
toggled.value = next;
|
||||
}
|
||||
|
||||
function toggleCollapseAll() {
|
||||
if (!built.value.collapsible) return showToast("nothing to fold: no run of 3 or more plain commits");
|
||||
if (!built.value.collapsible) return showToast("nothing to fold: no 2 or more plain commits in a row");
|
||||
collapseAll.value = !collapseAll.value;
|
||||
toggled.value = new Set();
|
||||
}
|
||||
@@ -249,7 +249,7 @@ onUnmounted(() => {
|
||||
class="collapse"
|
||||
:class="{ on: collapseAll && built.collapsible }"
|
||||
:disabled="!built.collapsible"
|
||||
:title="built.collapsible ? 'c' : 'Nothing to fold: no run of 3 or more commits without a branch, tag or merge'"
|
||||
:title="built.collapsible ? 'c' : 'Nothing to fold: no 2 or more commits in a row without a branch, tag, fork or merge'"
|
||||
@click="toggleCollapseAll"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
||||
|
||||
+28
-17
@@ -15,43 +15,54 @@ describe("buildRows", () => {
|
||||
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", () => {
|
||||
it("hides the whole run and joins its two ends 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);
|
||||
expect(hashes(r)).toEqual(["top", "root"]);
|
||||
expect(r.rows[0].parents).toEqual([{ hash: "root", dotted: true }]);
|
||||
expect(r.rows[0].hiddenCount).toBe(4);
|
||||
});
|
||||
|
||||
it("gives every commit of the run the same run id", () => {
|
||||
it("finds the run from its members and from the commit above it", () => {
|
||||
const r = buildRows(commits, labeled, true, new Set());
|
||||
const id = r.runOf.get("p1");
|
||||
const [id] = r.runsAt.get("top")!;
|
||||
expect(id).toBeDefined();
|
||||
expect(["p2", "p3", "p4"].map((h) => r.runOf.get(h))).toEqual([id, id, id]);
|
||||
expect(r.runOf.get("top")).toBeUndefined();
|
||||
expect(["p1", "p2", "p3", "p4"].map((h) => r.runsAt.get(h))).toEqual([[id], [id], [id], [id]]);
|
||||
expect(r.runsAt.get("root")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("a toggled run does the opposite of the global setting", () => {
|
||||
const id = buildRows(commits, labeled, true, new Set()).runOf.get("p1")!;
|
||||
const [id] = buildRows(commits, labeled, true, new Set()).runsAt.get("top")!;
|
||||
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"]);
|
||||
expect(hashes(buildRows(commits, labeled, false, new Set([id])))).toEqual(["top", "root"]);
|
||||
});
|
||||
|
||||
it("does not collapse runs shorter than three", () => {
|
||||
const short = chain("top:p1", "p1:p2", "p2:root", "root");
|
||||
it("does not fold a single plain commit", () => {
|
||||
const short = chain("top:p1", "p1:root", "root");
|
||||
const r = buildRows(short, new Set(["top"]), true, new Set());
|
||||
expect(hashes(r)).toHaveLength(4);
|
||||
expect(hashes(r)).toHaveLength(3);
|
||||
expect(r.collapsible).toBe(false);
|
||||
});
|
||||
|
||||
it("folds two plain commits", () => {
|
||||
const two = chain("top:p1", "p1:p2", "p2:root", "root");
|
||||
expect(hashes(buildRows(two, new Set(["top"]), true, new Set()))).toEqual(["top", "root"]);
|
||||
});
|
||||
|
||||
it("reports when a run can collapse, whether collapsed or not", () => {
|
||||
expect(buildRows(commits, labeled, false, new Set()).collapsible).toBe(true);
|
||||
expect(buildRows(commits, labeled, true, new Set()).collapsible).toBe(true);
|
||||
});
|
||||
|
||||
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");
|
||||
it("stops runs at merges, forks and labels, and folds each side of a merge", () => {
|
||||
// m merges x1 and y1; b is a fork point (two children).
|
||||
const c = chain("m:x1,y1", "y1:y2", "y2: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"]);
|
||||
expect(hashes(r)).toEqual(["m", "b", "z"]);
|
||||
expect(r.rows[0].parents).toEqual([
|
||||
{ hash: "b", dotted: true },
|
||||
{ hash: "b", dotted: true },
|
||||
]);
|
||||
expect(r.rows[0].hiddenCount).toBe(5);
|
||||
expect(r.runsAt.get("m")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
+29
-20
@@ -2,16 +2,20 @@ 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>;
|
||||
/**
|
||||
* Commit hash → ids of the runs it can fold: the run a plain commit belongs to,
|
||||
* or the runs right below a commit that is not plain.
|
||||
*/
|
||||
runsAt: Map<string, string[]>;
|
||||
/** Whether any run is long enough to collapse. */
|
||||
collapsible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* no label. A run of two or more is collapsed when `collapseAll` differs from
|
||||
* `toggled.has(runId)`: all of it is hidden, and the commits at both ends are joined
|
||||
* by a dotted edge.
|
||||
*/
|
||||
export function buildRows(
|
||||
commits: Commit[],
|
||||
@@ -33,39 +37,44 @@ export function buildRows(
|
||||
return !!c && c.parents.length === 1 && childCount.get(c.hash) === 1 && !labeled.has(c.hash);
|
||||
};
|
||||
|
||||
const runOf = new Map<string, string>();
|
||||
const runsAt = new Map<string, string[]>();
|
||||
const hidden = new Set<string>();
|
||||
/** First commit of a collapsed run → [last commit, number hidden]. */
|
||||
/** First commit of a collapsed run → [the commit below the run, number hidden]. */
|
||||
const jump = new Map<string, [string, number]>();
|
||||
let collapsible = false;
|
||||
|
||||
for (const c of commits) {
|
||||
if (!plain(c.hash) || plain(childOf.get(c.hash))) continue;
|
||||
const above = childOf.get(c.hash);
|
||||
if (!plain(c.hash) || plain(above)) 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)) {
|
||||
while (plain(next)) {
|
||||
run.push(next);
|
||||
next = byHash.get(next)!.parents[0];
|
||||
}
|
||||
if (run.length < 2) continue;
|
||||
collapsible = true;
|
||||
const id = c.hash;
|
||||
for (const h of run) runOf.set(h, id);
|
||||
if (run.length >= 3) collapsible = true;
|
||||
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]);
|
||||
for (const h of run) runsAt.set(h, [id]);
|
||||
runsAt.set(above!, [...(runsAt.get(above!) ?? []), id]);
|
||||
if (collapseAll !== toggled.has(id)) {
|
||||
for (const h of run) hidden.add(h);
|
||||
jump.set(c.hash, [next, run.length]);
|
||||
}
|
||||
}
|
||||
|
||||
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 })) },
|
||||
);
|
||||
let hiddenCount = 0;
|
||||
const parents = c.parents.map((hash) => {
|
||||
const j = jump.get(hash);
|
||||
if (!j) return { hash, dotted: false };
|
||||
hiddenCount += j[1];
|
||||
return { hash: j[0], dotted: true };
|
||||
});
|
||||
rows.push(hiddenCount ? { commit: c, parents, hiddenCount } : { commit: c, parents });
|
||||
}
|
||||
return { rows, runOf, collapsible };
|
||||
return { rows, runsAt, collapsible };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user