Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
radicle-explorer src views repos router.ts
import type {
  ErrorRoute,
  LoadedRoute,
  NotFoundRoute,
} from "@app/lib/router/definitions";
import type {
  BaseUrl,
  Blob,
  Commit,
  CommitBlob,
  CommitHeader,
  Diff,
  DiffBlob,
  Issue,
  IssueState,
  Node,
  Patch,
  PatchState,
  PeerRefs,
  Remote,
  Repo,
  Revision,
  SeedingPolicy,
  Tree,
} from "@http-client";

import * as Syntax from "@app/lib/syntax";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { cached } from "@app/lib/cache";
import { handleError, unreachableError } from "@app/views/repos/error";
import {
  getBranchesFromRefs,
  getTagsFromRefs,
  unreachable,
} from "@app/lib/utils";
import { nodePath } from "@app/views/nodes/router";

export const PATCHES_PER_PAGE = 10;
export const ISSUES_PER_PAGE = 10;

function peerHasBranches(peer: PeerRefs): boolean {
  return Object.keys(peer.refs).some(name => name.startsWith("refs/heads/"));
}

function canonicalOids(
  refs: Repo["refs"] | undefined,
): Array<[string, string]> {
  return [
    ...Object.entries(refs?.refs ?? {}),
    ...Object.entries(refs?.tags ?? {}).map(
      ([name, info]): [string, string] => [name, info.commit],
    ),
  ];
}

function remoteToPeerRefs(remote: Remote): PeerRefs {
  if (remote.refs) {
    return {
      id: remote.id,
      alias: remote.alias,
      delegate: remote.delegate,
      refs: remote.refs,
    };
  }

  const refs: Record<string, string> = {};

  for (const [name, oid] of Object.entries(remote.heads)) {
    refs[`refs/heads/${name}`] = oid;
  }

  return {
    id: remote.id,
    alias: remote.alias,
    delegate: remote.delegate,
    refs,
  };
}

export type RepoRoute =
  | RepoTreeRoute
  | RepoHistoryRoute
  | {
      resource: "repo.commit";
      node: BaseUrl;
      repo: string;
      commit: string;
    }
  | RepoIssuesRoute
  | RepoIssueRoute
  | RepoPatchesRoute
  | RepoPatchRoute;

interface RepoIssuesRoute {
  resource: "repo.issues";
  node: BaseUrl;
  repo: string;
  status?: "open" | "closed";
}

interface RepoIssueRoute {
  resource: "repo.issue";
  node: BaseUrl;
  repo: string;
  issue: string;
}

interface RepoTreeRoute {
  resource: "repo.source";
  node: BaseUrl;
  repo: string;
  path?: string;
  peer?: string;
  revision?: string;
  route?: string;
}

interface RepoHistoryRoute {
  resource: "repo.history";
  node: BaseUrl;
  repo: string;
  peer?: string;
  revision?: string;
}

interface RepoPatchRoute {
  resource: "repo.patch";
  node: BaseUrl;
  repo: string;
  patch: string;
  view?:
    | {
        name: "activity";
      }
    | {
        name: "changes";
        revision?: string;
      }
    | {
        name: "diff";
        fromCommit: string;
        toCommit: string;
      };
}

interface RepoPatchesRoute {
  resource: "repo.patches";
  node: BaseUrl;
  repo: string;
  search?: string;
}

export type RepoLoadedRoute =
  | {
      resource: "repo.source";
      params: {
        baseUrl: BaseUrl;
        seedingPolicy: SeedingPolicy;
        commit: string;
        repo: Repo;
        peers: PeerRefs[];
        peer: string | undefined;
        revision: string | undefined;
        tree: Tree;
        path: string;
        rawPath: (commit?: string) => string;
        blobResult: BlobResult;
        nodeAvatarUrl: string | undefined;
      };
    }
  | {
      resource: "repo.history";
      params: {
        baseUrl: BaseUrl;
        seedingPolicy: SeedingPolicy;
        commit: string;
        repo: Repo;
        peers: PeerRefs[];
        peer: string | undefined;
        revision: string | undefined;
        tree: Tree;
        commitHeaders: CommitHeader[];
        nodeAvatarUrl: string | undefined;
      };
    }
  | {
      resource: "repo.commit";
      params: {
        baseUrl: BaseUrl;
        repo: Repo;
        commit: Commit;
        nodeAvatarUrl: string | undefined;
      };
    }
  | {
      resource: "repo.issue";
      params: {
        baseUrl: BaseUrl;
        repo: Repo;
        rawPath: (commit?: string) => string;
        issue: Issue;
        nodeAvatarUrl: string | undefined;
      };
    }
  | {
      resource: "repo.issues";
      params: {
        baseUrl: BaseUrl;
        repo: Repo;
        issues: Issue[];
        status: IssueState["status"];
        nodeAvatarUrl: string | undefined;
      };
    }
  | {
      resource: "repo.patches";
      params: {
        baseUrl: BaseUrl;
        repo: Repo;
        patches: Patch[];
        status: PatchState["status"];
        nodeAvatarUrl: string | undefined;
      };
    }
  | {
      resource: "repo.patch";
      params: {
        baseUrl: BaseUrl;
        repo: Repo;
        rawPath: (commit?: string) => string;
        patch: Patch;
        stats: Diff["stats"];
        view: PatchView;
        nodeAvatarUrl: string | undefined;
      };
    };

export type BlobResult =
  | { ok: true; blob: Blob; highlighted: Syntax.Root | undefined }
  | { ok: false; error: { status?: number; message: string; path: string } };

export type PatchView =
  | {
      name: "activity";
      revision: string;
    }
  | {
      name: "changes";
      revision: string;
      oid: string;
      diff: Diff;
      commits: CommitHeader[];
      files: Record<string, CommitBlob>;
    }
  | {
      name: "diff";
      diff: Diff;
      files: Record<string, DiffBlob>;
      fromCommit: string;
      toCommit: string;
    };

// Check whether the input is a SHA1 commit.
function isOid(input: string): boolean {
  return /^[a-fA-F0-9]{40}$/.test(input);
}

export const cachedGetDiff = cached(
  async (baseUrl: BaseUrl, rid: string, base: string, oid: string) => {
    const api = new HttpdClient(baseUrl);
    return await api.repo.getDiff(rid, base, oid);
  },
  (...args) => JSON.stringify(args),
  { max: 200 },
);

function parseRevisionToOid(
  revision: string | undefined,
  defaultBranch: string,
  branches: Record<string, string>,
): string {
  if (revision) {
    if (isOid(revision)) {
      return revision;
    } else {
      const oid = branches[revision];
      if (oid) {
        return oid;
      } else {
        throw new Error(`Revision ${revision} not found`);
      }
    }
  } else {
    return branches[defaultBranch];
  }
}

export async function loadRepoRoute(
  route: RepoRoute,
  previousLoaded: LoadedRoute,
): Promise<RepoLoadedRoute | ErrorRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);

  try {
    if (route.resource === "repo.source") {
      return await loadTreeView(route, previousLoaded);
    } else if (route.resource === "repo.history") {
      return await loadHistoryView(route, previousLoaded);
    } else if (route.resource === "repo.commit") {
      const [repo, commit, node] = await Promise.all([
        api.repo.getByRid(route.repo),
        api.repo.getCommitBySha(route.repo, route.commit),
        api.getNode(),
      ]);

      return {
        resource: "repo.commit",
        params: {
          baseUrl: route.node,
          repo,
          commit,
          nodeAvatarUrl: node.avatarUrl,
        },
      };
    } else if (route.resource === "repo.issue") {
      return await loadIssueView(route);
    } else if (route.resource === "repo.patch") {
      return await loadPatchView(route, previousLoaded);
    } else if (route.resource === "repo.issues") {
      return await loadIssuesView(route);
    } else if (route.resource === "repo.patches") {
      return await loadPatchesView(route);
    } else {
      return unreachable(route);
    }
  } catch (error) {
    if (
      error instanceof Error ||
      error instanceof ResponseError ||
      error instanceof ResponseParseError
    ) {
      return handleError(error, route);
    } else {
      return unreachableError();
    }
  }
}

async function loadPatchesView(
  route: RepoPatchesRoute,
): Promise<RepoLoadedRoute> {
  const api = new HttpdClient(route.node);
  const searchParams = new URLSearchParams(route.search || "");
  const status = (searchParams.get("status") as PatchState["status"]) || "open";

  const [repo, patches, node] = await Promise.all([
    api.repo.getByRid(route.repo),
    api.repo.getAllPatches(route.repo, {
      status,
      page: 0,
      perPage: PATCHES_PER_PAGE,
    }),
    api.getNode(),
  ]);

  return {
    resource: "repo.patches",
    params: {
      baseUrl: route.node,
      patches,
      status,
      repo,
      nodeAvatarUrl: node.avatarUrl,
    },
  };
}

async function loadIssuesView(
  route: RepoIssuesRoute,
): Promise<RepoLoadedRoute> {
  const api = new HttpdClient(route.node);
  const status = route.status || "open";

  const [repo, issues, node] = await Promise.all([
    api.repo.getByRid(route.repo),
    api.repo.getAllIssues(route.repo, {
      status,
      page: 0,
      perPage: ISSUES_PER_PAGE,
    }),
    api.getNode(),
  ]);

  return {
    resource: "repo.issues",
    params: {
      baseUrl: route.node,
      issues,
      status,
      repo,
      nodeAvatarUrl: node.avatarUrl,
    },
  };
}

async function loadTreeView(
  route: RepoTreeRoute,
  previousLoaded: LoadedRoute,
): Promise<RepoLoadedRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
    `${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
      route.repo
    }${commit ? `/${commit}` : ""}`;

  let repoPromise: Promise<Repo>;
  let seedingPolicyPromise: Promise<SeedingPolicy>;
  let nodePromise: Promise<Partial<Node>>;
  if (
    (previousLoaded.resource === "repo.source" ||
      previousLoaded.resource === "repo.history") &&
    previousLoaded.params.repo.rid === route.repo &&
    previousLoaded.params.peer === route.peer
  ) {
    repoPromise = Promise.resolve(previousLoaded.params.repo);
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
    nodePromise = Promise.resolve({
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
    });
  } else {
    repoPromise = api.repo.getByRid(route.repo);
    seedingPolicyPromise = api.getPolicyByRid(route.repo);
    nodePromise = api.getNode();
  }

  const [repo, seedingPolicy, node] = await Promise.all([
    repoPromise,
    seedingPolicyPromise,
    nodePromise,
  ]);

  const remotes = await api.repo.getAllRemotes(route.repo);
  const peers: PeerRefs[] = remotes.map(remoteToPeerRefs);

  if (!repo["payloads"]["xyz.radicle.project"]) {
    throw new Error(
      `Repository ${repo.rid} does not have a xyz.radicle.project payload.`,
    );
  }

  const project = repo["payloads"]["xyz.radicle.project"];
  let branchMap: Record<string, string> = {
    [project.data.defaultBranch]: project.meta.head,
  };

  for (const [refName, oid] of canonicalOids(repo.refs)) {
    const shortName = refName.startsWith("refs/heads/")
      ? refName.slice("refs/heads/".length)
      : refName.startsWith("refs/tags/")
        ? refName.slice("refs/tags/".length)
        : refName;
    branchMap[shortName] = oid;
    branchMap[encodeURIComponent(shortName)] = oid;
  }

  for (const peer of peers) {
    const tags = getTagsFromRefs(peer.refs);
    for (const [tagName, oid] of Object.entries(tags)) {
      branchMap[tagName] = oid;
      branchMap[encodeURIComponent(tagName)] = oid;
    }
  }

  if (route.peer) {
    const peer = peers.find(peer => peer.id === route.peer);
    if (!peer) {
      return {
        resource: "notFound",
        params: { title: `Peer ${route.peer} could not be found` },
      };
    } else {
      branchMap = { ...getBranchesFromRefs(peer.refs) };
      for (const [tagName, oid] of Object.entries(getTagsFromRefs(peer.refs))) {
        branchMap[tagName] = oid;
        branchMap[encodeURIComponent(tagName)] = oid;
      }
    }
  }

  if (route.route) {
    const { revision, path } = detectRevision(route.route, branchMap);
    route.revision = revision;
    route.path = path;
  }

  const commit = parseRevisionToOid(
    route.revision,
    project.data.defaultBranch,
    branchMap,
  );
  const path = route.path || "/";
  const [tree, blobResult] = await Promise.all([
    api.repo.getTree(route.repo, commit),
    loadBlob(api, repo.rid, commit, path),
  ]);
  return {
    resource: "repo.source",
    params: {
      baseUrl: route.node,
      seedingPolicy,
      commit,
      repo,
      peers: peers.filter(peerHasBranches),
      peer: route.peer,
      rawPath,
      revision: route.revision,
      tree,
      path,
      blobResult,
      nodeAvatarUrl: node.avatarUrl,
    },
  };
}

async function loadBlob(
  api: HttpdClient,
  repo: string,
  commit: string,
  path: string,
): Promise<BlobResult> {
  try {
    let blob: Blob;
    if (path === "" || path === "/") {
      blob = await api.repo.getReadme(repo, commit);
    } else {
      blob = await api.repo.getBlob(repo, commit, path);
    }
    return {
      ok: true,
      blob,
      highlighted: blob.content
        ? await Syntax.highlight(blob.content, blob.path.split(".").pop() ?? "")
        : undefined,
    };
  } catch (e: unknown) {
    if (e instanceof ResponseError) {
      return {
        ok: false,
        error: {
          status: e.status,
          message: "Not able to load file",
          path,
        },
      };
    } else if (path === "/") {
      return {
        ok: false,
        error: {
          message: "The README could not be loaded",
          path,
        },
      };
    } else {
      return {
        ok: false,
        error: {
          message: "Not able to load file",
          path,
        },
      };
    }
  }
}
async function loadHistoryView(
  route: RepoHistoryRoute,
  previousLoaded: LoadedRoute,
): Promise<RepoLoadedRoute> {
  const api = new HttpdClient(route.node);

  let repoPromise: Promise<Repo>;
  let seedingPolicyPromise: Promise<SeedingPolicy>;
  let nodePromise: Promise<Partial<Node>>;
  if (
    (previousLoaded.resource === "repo.source" ||
      previousLoaded.resource === "repo.history") &&
    previousLoaded.params.repo.rid === route.repo &&
    previousLoaded.params.peer === route.peer
  ) {
    repoPromise = Promise.resolve(previousLoaded.params.repo);
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
    nodePromise = Promise.resolve({
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
    });
  } else {
    repoPromise = api.repo.getByRid(route.repo);
    seedingPolicyPromise = api.getPolicyByRid(route.repo);
    nodePromise = api.getNode();
  }

  const [repo, seedingPolicy, node] = await Promise.all([
    repoPromise,
    seedingPolicyPromise,
    nodePromise,
  ]);

  const remotes = await api.repo.getAllRemotes(route.repo);
  const peers: PeerRefs[] = remotes.map(remoteToPeerRefs);

  const branchMap = await getPeerBranches(
    api,
    route.repo,
    route.peer,
    repo,
    peers,
  );

  if (!repo["payloads"]["xyz.radicle.project"]) {
    throw new Error(
      `Repository ${repo.rid} does not have a xyz.radicle.project payload.`,
    );
  }

  const project = repo["payloads"]["xyz.radicle.project"];
  let commitId;
  if (route.revision && isOid(route.revision)) {
    commitId = route.revision;
  } else if (branchMap) {
    commitId = branchMap[route.revision || project.data.defaultBranch];
  }

  if (!commitId) {
    throw new Error(
      `Revision ${route.revision} not found for repo ${repo.rid}`,
    );
  }

  let treePromise: Promise<Tree>;

  if (
    (previousLoaded.resource === "repo.source" ||
      previousLoaded.resource === "repo.history") &&
    previousLoaded.params.repo.rid === route.repo &&
    previousLoaded.params.commit === commitId
  ) {
    treePromise = Promise.resolve(previousLoaded.params.tree);
  } else {
    treePromise = api.repo.getTree(route.repo, commitId);
  }

  const [tree, commitHeaders] = await Promise.all([
    treePromise,
    api.repo.getAllCommits(repo.rid, {
      parent: commitId,
      page: 0,
      perPage: config.source.commitsPerPage,
    }),
  ]);

  return {
    resource: "repo.history",
    params: {
      baseUrl: route.node,
      seedingPolicy,
      commit: commitId,
      repo,
      peers: peers.filter(peerHasBranches),
      peer: route.peer,
      revision: route.revision,
      tree,
      commitHeaders,
      nodeAvatarUrl: node.avatarUrl,
    },
  };
}

async function loadIssueView(route: RepoIssueRoute): Promise<RepoLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
    `${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
      route.repo
    }${commit ? `/${commit}` : ""}`;

  const [repo, issue, node] = await Promise.all([
    api.repo.getByRid(route.repo),
    api.repo.getIssueById(route.repo, route.issue),
    api.getNode(),
  ]);
  return {
    resource: "repo.issue",
    params: {
      baseUrl: route.node,
      repo,
      rawPath,
      issue,
      nodeAvatarUrl: node.avatarUrl,
    },
  };
}

async function loadPatchView(
  route: RepoPatchRoute,
  previousLoaded: LoadedRoute,
): Promise<RepoLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
    `${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
      route.repo
    }${commit ? `/${commit}` : ""}`;

  let repoPromise: Promise<Repo>;
  let patchPromise: Promise<Patch>;
  let nodePromise: Promise<Partial<Node>>;

  if (
    previousLoaded.resource === "repo.patch" &&
    previousLoaded.params.repo.rid === route.repo &&
    previousLoaded.params.patch.id === route.patch
  ) {
    repoPromise = Promise.resolve(previousLoaded.params.repo);
    patchPromise = Promise.resolve(previousLoaded.params.patch);
    nodePromise = Promise.resolve({
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
    });
  } else {
    repoPromise = api.repo.getByRid(route.repo);
    patchPromise = api.repo.getPatchById(route.repo, route.patch);
    nodePromise = api.getNode();
  }
  const [repo, patch, { avatarUrl }] = await Promise.all([
    repoPromise,
    patchPromise,
    nodePromise,
  ]);

  const latestRevision = patch.revisions.at(-1) as Revision;
  const {
    diff: { stats },
  } = await cachedGetDiff(
    api.baseUrl,
    route.repo,
    latestRevision.base,
    latestRevision.oid,
  );

  let view: PatchView;
  switch (route.view?.name) {
    case "activity":
    case undefined: {
      view = { name: "activity", revision: latestRevision.id };
      break;
    }
    case "changes": {
      const revisionId = route.view.revision;
      const revision =
        patch.revisions.find(r => r.id === revisionId) || latestRevision;
      if (!revision) {
        throw new Error(
          `revision ${revisionId} of patch ${route.patch} not found`,
        );
      }
      const { diff, commits, files } = await cachedGetDiff(
        api.baseUrl,
        route.repo,
        revision.base,
        revision.oid,
      );
      view = {
        name: route.view?.name,
        revision: revision.id,
        oid: revision.oid,
        diff,
        commits,
        files,
      };
      break;
    }
    case "diff": {
      const { fromCommit, toCommit } = route.view;
      const { diff, files } = await cachedGetDiff(
        api.baseUrl,
        route.repo,
        fromCommit,
        toCommit,
      );

      view = { name: "diff", fromCommit, toCommit, files, diff };
      break;
    }
  }
  return {
    resource: "repo.patch",
    params: {
      baseUrl: route.node,
      repo,
      rawPath,
      patch,
      stats,
      view,
      nodeAvatarUrl: avatarUrl,
    },
  };
}

async function getPeerBranches(
  api: HttpdClient,
  repoId: string,
  peer?: string,
  repo?: Repo,
  loadedPeers?: PeerRefs[],
) {
  if (peer) {
    const remote = await api.repo.getRemoteByPeer(repoId, peer);
    const refs = remoteToPeerRefs(remote).refs;
    const map: Record<string, string> = { ...getBranchesFromRefs(refs) };
    for (const [tagName, oid] of Object.entries(getTagsFromRefs(refs))) {
      map[tagName] = oid;
      map[encodeURIComponent(tagName)] = oid;
    }
    return map;
  } else if (repo) {
    const branchMap: Record<string, string> = {};
    const peers = loadedPeers ?? [];

    const project = repo.payloads["xyz.radicle.project"];
    if (project) {
      branchMap[project.data.defaultBranch] = project.meta.head;
      branchMap[encodeURIComponent(project.data.defaultBranch)] =
        project.meta.head;
    }

    for (const [refName, oid] of canonicalOids(repo.refs)) {
      const shortName = refName.startsWith("refs/heads/")
        ? refName.slice("refs/heads/".length)
        : refName.startsWith("refs/tags/")
          ? refName.slice("refs/tags/".length)
          : refName;
      branchMap[shortName] = oid;
      branchMap[encodeURIComponent(shortName)] = oid;
    }

    for (const p of peers) {
      const tags = getTagsFromRefs(p.refs);
      for (const [tagName, oid] of Object.entries(tags)) {
        branchMap[tagName] = oid;
        branchMap[encodeURIComponent(tagName)] = oid;
      }
    }
    return branchMap;
  } else {
    return undefined;
  }
}

// Detects branch names and commit IDs at the start of `input` and extract it.
function detectRevision(
  input: string,
  branches: Record<string, string>,
): { path: string; revision?: string } {
  const commitPath = [input.slice(0, 40), input.slice(41)];
  const branch = Object.entries(branches).find(([branchName]) =>
    input.startsWith(branchName),
  );

  if (branch) {
    const [revision, path] = [
      input.slice(0, branch[0].length),
      input.slice(branch[0].length + 1),
    ];
    return {
      revision,
      path: path || "/",
    };
  } else if (isOid(commitPath[0])) {
    return {
      revision: commitPath[0],
      path: commitPath[1] || "/",
    };
  } else {
    return { path: input };
  }
}

function sanitizeQueryString(queryString: string): string {
  return queryString.startsWith("?") ? queryString.substring(1) : queryString;
}

export function resolveRepoRoute(
  node: BaseUrl,
  repo: string,
  segments: string[],
  urlSearch: string,
): RepoRoute | null {
  let content = segments.shift();
  let peer;
  if (content === "remotes") {
    peer = segments.shift();
    content = segments.shift();
  }

  if (!content || content === "tree") {
    return {
      resource: "repo.source",
      node,
      repo,
      peer,
      path: undefined,
      revision: undefined,
      route: segments.join("/"),
    };
  } else if (content === "history") {
    return {
      resource: "repo.history",
      node,
      repo,
      peer,
      revision: segments.join("/"),
    };
  } else if (content === "commits") {
    return {
      resource: "repo.commit",
      node,
      repo,
      commit: segments[0],
    };
  } else if (content === "issues") {
    const issueOrAction = segments.shift();
    if (issueOrAction) {
      return {
        resource: "repo.issue",
        node,
        repo,
        issue: issueOrAction,
      };
    } else {
      const rawStatus = new URLSearchParams(sanitizeQueryString(urlSearch)).get(
        "status",
      );
      let status: "open" | "closed" | undefined;
      if (rawStatus === "open" || rawStatus === "closed") {
        status = rawStatus;
      }
      return {
        resource: "repo.issues",
        node,
        repo,
        status,
      };
    }
  } else if (content === "patches") {
    return resolvePatchesRoute(node, repo, segments, urlSearch);
  } else {
    return null;
  }
}

function resolvePatchesRoute(
  node: BaseUrl,
  repo: string,
  segments: string[],
  urlSearch: string,
): RepoPatchRoute | RepoPatchesRoute {
  const patch = segments.shift();
  const revision = segments.shift();
  if (patch) {
    const searchParams = new URLSearchParams(sanitizeQueryString(urlSearch));
    const tab = searchParams.get("tab");
    const base = {
      resource: "repo.patch",
      node,
      repo,
      patch,
    } as const;
    const diff = searchParams.get("diff");
    if (diff) {
      const [fromCommit, toCommit] = diff.split("..");
      if (isOid(fromCommit) && isOid(toCommit)) {
        return {
          ...base,
          view: { name: "diff", fromCommit, toCommit },
        };
      }
    }

    if (tab === "changes") {
      return {
        ...base,
        view: { name: tab, revision },
      };
    } else if (tab === "activity") {
      return {
        ...base,
        view: { name: tab },
      };
    } else {
      return base;
    }
  } else {
    return {
      resource: "repo.patches",
      node,
      repo,
      search: sanitizeQueryString(urlSearch),
    };
  }
}

export function repoRouteToPath(route: RepoRoute): string {
  const node = nodePath(route.node);

  const pathSegments = [node, route.repo];

  if (route.resource === "repo.source") {
    if (route.peer) {
      pathSegments.push("remotes", route.peer);
    }

    pathSegments.push("tree");
    let omitTree = true;

    if (route.route && route.route !== "/") {
      pathSegments.push(route.route);
      omitTree = false;
    } else {
      if (route.revision) {
        pathSegments.push(route.revision);
        omitTree = false;
      }

      if (route.path && route.path !== "/") {
        pathSegments.push(route.path);
        omitTree = false;
      }
    }
    if (omitTree) {
      pathSegments.pop();
    }

    return pathSegments.join("/");
  } else if (route.resource === "repo.history") {
    if (route.peer) {
      pathSegments.push("remotes", route.peer);
    }

    pathSegments.push("history");

    if (route.revision) {
      pathSegments.push(route.revision);
    }
    return pathSegments.join("/");
  } else if (route.resource === "repo.commit") {
    return [...pathSegments, "commits", route.commit].join("/");
  } else if (route.resource === "repo.issues") {
    let url = [...pathSegments, "issues"].join("/");
    const searchParams = new URLSearchParams();
    if (route.status) {
      searchParams.set("status", route.status);
    }
    if (searchParams.size > 0) {
      url += `?${searchParams}`;
    }
    return url;
  } else if (route.resource === "repo.issue") {
    return [...pathSegments, "issues", route.issue].join("/");
  } else if (route.resource === "repo.patches") {
    let url = [...pathSegments, "patches"].join("/");
    if (route.search) {
      url += `?${route.search}`;
    }
    return url;
  } else if (route.resource === "repo.patch") {
    return patchRouteToPath(route);
  } else {
    return unreachable(route);
  }
}

function patchRouteToPath(route: RepoPatchRoute): string {
  const node = nodePath(route.node);

  const pathSegments = [node, route.repo];

  pathSegments.push("patches", route.patch);
  if (route.view?.name === "changes") {
    if (route.view.revision) {
      pathSegments.push(route.view.revision);
    }
  }

  let url = pathSegments.join("/");
  if (!route.view) {
    return url;
  } else {
    const searchParams = new URLSearchParams();

    if (route.view.name === "diff") {
      searchParams.set(
        "diff",
        `${route.view.fromCommit}..${route.view.toCommit}`,
      );
    } else {
      searchParams.set("tab", route.view.name);
    }
    url += `?${searchParams.toString()}`;
    return url;
  }
}

export function repoTitle(loadedRoute: RepoLoadedRoute) {
  const title: string[] = [];

  if (!loadedRoute.params.repo["payloads"]["xyz.radicle.project"]) {
    throw new Error(
      `Repository ${loadedRoute.params.repo.rid} does not have a xyz.radicle.project payload.`,
    );
  }
  const project = loadedRoute.params.repo["payloads"]["xyz.radicle.project"];

  if (loadedRoute.resource === "repo.source") {
    title.push(project.data.name);
    if (project.data.description.length > 0) {
      title.push(project.data.description);
    }
  } else if (loadedRoute.resource === "repo.commit") {
    title.push(loadedRoute.params.commit.commit.summary);
    title.push("commit");
  } else if (loadedRoute.resource === "repo.history") {
    title.push(project.data.name);
    title.push("history");
  } else if (loadedRoute.resource === "repo.issue") {
    title.push(loadedRoute.params.issue.title);
    title.push("issue");
  } else if (loadedRoute.resource === "repo.issues") {
    title.push(project.data.name);
    title.push("issues");
  } else if (loadedRoute.resource === "repo.patch") {
    title.push(loadedRoute.params.patch.title);
    title.push("patch");
  } else if (loadedRoute.resource === "repo.patches") {
    title.push(project.data.name);
    title.push("patches");
  } else {
    return unreachable(loadedRoute);
  }

  return title;
}

export const testExports = { isOid };