Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165// the pull page's url grammar, where <aturi> goes in raw and `reroute` in// src/hooks.ts folds its segments back together before routing://// /pulls/<aturi>/latest diff of the newest version// /pulls/<aturi>/1 diff of version 1// /pulls/<aturi>/1/c1..c2 that diff narrowed to a commit range// /pulls/<aturi>/0..1 interdiff between two versions// /pulls/<aturi>/0..1/<change-id> that interdiff narrowed to one change
export interface Revspec { base: string; head: string;}
// parses `<base>..<head>`, where either side may be missingexport const parseRange = (input: string): Revspec | null => { const spec = input.trim(); if (spec === "") return { base: "", head: "" }; // `...` is a symmetric difference in git, and two ranges are not a range if (spec.includes("...") || spec.split("..").length > 2) return null; if (!spec.includes("..")) return { base: "", head: spec };
const [base, head] = spec.split("..", 2).map((part) => part.trim()); if (base === "" && head === "") return null; return { base, head };};
export interface PullDiffView { mode: "diff"; version: number; base: string; head: string; // true when that side was defaulted, which is what makes the commit card's // "See all changes" row the active one isDiffBase: boolean; isDiffHead: boolean;}
export interface PullInterdiffView { mode: "interdiff"; version1: number; version2: number; // the two versions' own ends, resolved here so nothing downstream re-indexes from: Revspec; to: Revspec; // "" means all changes changeId: string;}
export type PullView = PullDiffView | PullInterdiffView;
// a bad version sends you to the newest one, a bad range only drops itselfexport type PullRouteRedirect = { redirect: "latest" | "version" };
export const isRedirect = <T extends object>( result: T | PullRouteRedirect): result is PullRouteRedirect => "redirect" in result;
const index = (raw: string, count: number): number | null => { if (!/^\d+$/.test(raw)) return null; const value = Number(raw); return value < count ? value : null;};
// `/<v1>..<v2>[/<change-id>]`export const parseInterdiffRoute = ( versions: readonly Revspec[], versionParam: string, rangeParam?: string): PullInterdiffView | PullRouteRedirect => { const range = parseRange(versionParam); if (!range) return { redirect: "latest" }; const version1 = index(range.base, versions.length); const version2 = index(range.head, versions.length); // the appview indexes its versions raw here and panics on a bad number if (version1 === null || version2 === null) return { redirect: "latest" }; const changeId = rangeParam === "all" ? "" : (rangeParam ?? ""); return { mode: "interdiff", version1, version2, // copied field by field: a version carries its comments too, and `view` is // serialized to the page from: { base: versions[version1].base, head: versions[version1].head }, to: { base: versions[version2].base, head: versions[version2].head }, changeId };};
// `/<version>[/<base>..<head>]`export const parseDiffRoute = ( versions: readonly Revspec[], versionParam: string, rangeParam?: string): PullDiffView | PullRouteRedirect => { const version = versionParam === "latest" ? versions.length - 1 : index(versionParam, versions.length); if (version === null || version < 0) return { redirect: "latest" };
const spec = parseRange(rangeParam ?? ""); if (!spec) return { redirect: "version" };
// `base` and `head` are the appview's magic words for the version's own ends const isDiffBase = spec.base === "" || spec.base === "base"; const isDiffHead = spec.head === "" || spec.head === "head"; return { mode: "diff", version, base: isDiffBase ? versions[version].base : spec.base, head: isDiffHead ? versions[version].head : spec.head, isDiffBase, isDiffHead };};
// the version whose commits are listed, and whose card the rail highlightsexport const activeVersion = (view: PullView): number => view.mode === "diff" ? view.version : view.version2;
// the commit row to mark, compared on short hashes like the appview doesexport const activeCommitId = (view: PullView): string => view.mode === "diff" ? view.head : view.changeId;
export const seeAll = (view: PullView): boolean => view.mode === "diff" ? view.isDiffBase && view.isDiffHead : view.changeId === "";
// only the fields a change-id lookup reads, so this module keeps no depsinterface ChangeCommit { hash: string; parent?: string; changeId?: string;}
export type ChangeTarget = | { kind: "missing" } // the change is new in the second version, so there is nothing to interdiff | { kind: "diff"; spec: Revspec } | { kind: "interdiff"; from: Revspec; to: Revspec };
// `/<v1>..<v2>/<change-id>` narrows an interdiff to the one commit carrying that// jj change id on each side, exactly as `appview/pulls/single.go` doesexport const resolveChange = ( changeId: string, fromCommits: readonly ChangeCommit[], toCommits: readonly ChangeCommit[], bases: { from: string; to: string }): ChangeTarget => { const find = (commits: readonly ChangeCommit[]) => commits.find((commit) => commit.changeId === changeId); const to = find(toCommits); // the row was picked from the second version's log, so this is a typed url if (!to) return { kind: "missing" };
// the appview takes the first parent, which is the zero hash on a root commit // and would be rejected; the version's own base stands in for that const spec = (commit: ChangeCommit, base: string): Revspec => ({ base: commit.parent ?? base, head: commit.hash }); const from = find(fromCommits); return from ? { kind: "interdiff", from: spec(from, bases.from), to: spec(to, bases.to) } : { kind: "diff", spec: spec(to, bases.to) };};