// the pull page's url grammar:
//
// /pulls/<rkey>/latest diff of the newest version
// /pulls/<rkey>/1 diff of version 1
// /pulls/<rkey>/1/c1..c2 that diff narrowed to a commit range
// /pulls/<rkey>/0..1 interdiff between two versions
// /pulls/<rkey>/0..1/<change-id> that interdiff narrowed to one change
export interface Revspec {
base: string;
head: string;
// a pull version's source repo, when it isn't the target repo
repo?: string;
}
// parses `<base>..<head>`, where either side may be missing
export 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;
repo?: string;
}
export interface PullInterdiffView {
mode: "interdiff";
oldVersion: number;
version: number;
from: Revspec; // oldVersion {base, head}
to: Revspec; // version {base, head}
// "" means all changes
changeId: string;
}
export type PullView = PullDiffView | PullInterdiffView;
export const versionSegment = (view: PullView): string =>
view.mode === "diff" ? `${view.version}` : `${view.oldVersion}..${view.version}`;
// a bad version sends you to the newest one, a bad range only drops itself
export 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);
if (version1 === null || version2 === null) return { redirect: "latest" };
const changeId = rangeParam === "all" ? "" : (rangeParam ?? "");
return {
mode: "interdiff",
oldVersion: version1,
version: version2,
from: versions[version1],
to: versions[version2],
changeId
};
};
// `/<version>[/<base>..<head>]`
export const parseDiffRoute = (
versions: readonly Revspec[],
versionParam: string,
rangeParam?: string
): PullDiffView | PullRouteRedirect => {
const version =
versionParam === "latest"
? Math.max(versions.length - 1, 0)
: index(versionParam, versions.length);
if (version === null) return { redirect: "latest" };
const spec = parseRange(rangeParam ?? "");
if (!spec) return { redirect: "version" };
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,
repo: versions[version]?.repo
};
};