Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
radicle-desktop src views repo router.ts
import type { Action as IssueAction } from "@bindings/cob/issue/Action";
import type { Issue } from "@bindings/cob/issue/Issue";
import type { Operation } from "@bindings/cob/Operation";
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
import type { Action as PatchAction } from "@bindings/cob/patch/Action";
import type { Patch } from "@bindings/cob/patch/Patch";
import type { Review } from "@bindings/cob/patch/Review";
import type { Revision } from "@bindings/cob/patch/Revision";
import type { Thread } from "@bindings/cob/thread/Thread";
import type { Config } from "@bindings/config/Config";
import type { Diff } from "@bindings/diff/Diff";
import type { Commit } from "@bindings/repo/Commit";
import type { Readme } from "@bindings/repo/Readme";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { Tree } from "@bindings/source/Tree";

import type { DraftReview } from "@app/lib/draftReviewStorage";
import { draftReviewStorage } from "@app/lib/draftReviewStorage";
import { cachedGetCommitDiff, invoke } from "@app/lib/invoke";
import type { SidebarData } from "@app/lib/router/definitions";
import { loadSidebarData } from "@app/lib/router/definitions";
import { didFromPublicKey, unreachable } from "@app/lib/utils";

export type IssueStatus = "all" | Issue["state"]["status"];

export const DEFAULT_TAKE = 20;
export const COMMITS_PAGE_SIZE = 300;

export interface RepoHomeRoute {
  resource: "repo.home";
  sha?: string;
  rid: string;
}

export interface RepoCommitsRoute {
  resource: "repo.commits";
  rid: string;
}

export interface RepoCommitRoute {
  resource: "repo.commit";
  rid: string;
  commit: string;
}

export interface RepoIssueRoute {
  resource: "repo.issue";
  rid: string;
  issue: string;
  status: IssueStatus;
}

export interface LoadedRepoHomeRoute {
  resource: "repo.home";
  params: {
    repo: RepoInfo;
    sha?: string;
    tree: Tree;
    readme: Readme | null;
    sidebarData: SidebarData;
  };
}

export interface LoadedRepoCommitsRoute {
  resource: "repo.commits";
  params: {
    repo: RepoInfo;
    commits: PaginatedQuery<Commit[]>;
    sidebarData: SidebarData;
  };
}

export interface LoadedRepoCommitRoute {
  resource: "repo.commit";
  params: {
    repo: RepoInfo;
    commit: Commit;
    diff: Diff;
    sidebarData: SidebarData;
  };
}

export interface LoadedRepoIssueRoute {
  resource: "repo.issue";
  params: {
    repo: RepoInfo;
    config: Config;
    issue: Issue;
    issues: Issue[];
    status: IssueStatus;
    activity: Operation<IssueAction>[];
    threads: Thread[];
    sidebarData: SidebarData;
  };
}

export interface RepoIssuesRoute {
  resource: "repo.issues";
  rid: string;
  status: IssueStatus;
}

export interface LoadedRepoIssuesRoute {
  resource: "repo.issues";
  params: {
    repo: RepoInfo;
    issues: Issue[];
    status: IssueStatus;
    sidebarData: SidebarData;
  };
}

export type PatchStatus = Patch["state"]["status"];

export interface RepoPatchRoute {
  resource: "repo.patch";
  rid: string;
  patch: string;
  status: PatchStatus | undefined;
  reviewId: string | undefined;
}

export interface LoadedRepoPatchRoute {
  resource: "repo.patch";
  params: {
    repo: RepoInfo;
    config: Config;
    patch: Patch;
    patches: PaginatedQuery<Patch[]>;
    status: PatchStatus | undefined;
    review: Review | DraftReview | undefined;
    revisions: Revision[];
    activity: Operation<PatchAction>[];
    sidebarData: SidebarData;
  };
}

export interface RepoPatchesRoute {
  resource: "repo.patches";
  rid: string;
  status: PatchStatus | undefined;
}

export interface LoadedRepoPatchesRoute {
  resource: "repo.patches";
  params: {
    repo: RepoInfo;
    patches: PaginatedQuery<Patch[]>;
    status: PatchStatus | undefined;
    sidebarData: SidebarData;
  };
}

export type RepoRoute =
  | RepoHomeRoute
  | RepoCommitsRoute
  | RepoCommitRoute
  | RepoIssueRoute
  | RepoIssuesRoute
  | RepoPatchRoute
  | RepoPatchesRoute;
export type LoadedRepoRoute =
  | LoadedRepoHomeRoute
  | LoadedRepoCommitsRoute
  | LoadedRepoCommitRoute
  | LoadedRepoIssueRoute
  | LoadedRepoIssuesRoute
  | LoadedRepoPatchRoute
  | LoadedRepoPatchesRoute;

export async function loadPatch(
  route: RepoPatchRoute,
): Promise<LoadedRepoPatchRoute> {
  const [sidebarData, repo, patches, patch, revisions, activity] =
    await Promise.all([
      loadSidebarData(),
      invoke<RepoInfo>("repo_by_id", {
        rid: route.rid,
      }),
      invoke<PaginatedQuery<Patch[]>>("list_patches", {
        rid: route.rid,
        status: route.status,
        take: DEFAULT_TAKE,
      }),
      invoke<Patch>("patch_by_id", {
        rid: route.rid,
        id: route.patch,
      }),
      invoke<Revision[]>("revisions_by_patch", {
        rid: route.rid,
        id: route.patch,
      }),
      invoke<Operation<PatchAction>[]>("activity_by_patch", {
        rid: route.rid,
        id: route.patch,
      }),
    ]);

  const config = sidebarData.config;

  const draftReview =
    route.reviewId !== undefined &&
    draftReviewStorage.get(route.reviewId, {
      did: didFromPublicKey(config.publicKey),
      alias: config.alias,
    });

  const review =
    draftReview ||
    revisions
      .flatMap(r => r.reviews || [])
      .find(review => review.id === route.reviewId);

  return {
    resource: "repo.patch",
    params: {
      repo,
      config,
      patch,
      patches,
      revisions,
      status: route.status,
      review,
      activity,
      sidebarData,
    },
  };
}

export async function loadPatches(
  route: RepoPatchesRoute,
): Promise<LoadedRepoPatchesRoute> {
  const [sidebarData, repo, patches] = await Promise.all([
    loadSidebarData(),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
    }),
    invoke<PaginatedQuery<Patch[]>>("list_patches", {
      rid: route.rid,
      status: route.status,
      take: DEFAULT_TAKE,
    }),
  ]);

  return {
    resource: "repo.patches",
    params: { sidebarData, repo, patches, status: route.status },
  };
}

export async function loadRepoHome(
  route: RepoHomeRoute,
): Promise<LoadedRepoHomeRoute> {
  const [sidebarData, repo, readme, tree] = await Promise.all([
    loadSidebarData(),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
    }),
    invoke<Readme | null>("repo_readme", {
      rid: route.rid,
    }),
    invoke<Tree>("repo_tree", {
      rid: route.rid,
      path: "",
      sha: route.sha,
    }),
  ]);

  return {
    resource: "repo.home",
    params: { sidebarData, repo, sha: route.sha, readme, tree },
  };
}

export async function loadRepoCommits(
  route: RepoCommitsRoute,
): Promise<LoadedRepoCommitsRoute> {
  const [sidebarData, repo, commits] = await Promise.all([
    loadSidebarData(),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
    }),
    invoke<PaginatedQuery<Commit[]>>("list_repo_commits", {
      rid: route.rid,
      skip: 0,
      take: COMMITS_PAGE_SIZE,
    }),
  ]);

  return {
    resource: "repo.commits",
    params: { sidebarData, repo, commits },
  };
}

export async function loadRepoCommit(
  route: RepoCommitRoute,
): Promise<LoadedRepoCommitRoute> {
  const [sidebarData, repo, commit, diff] = await Promise.all([
    loadSidebarData(),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
    }),
    invoke<Commit>("repo_commit", {
      rid: route.rid,
      sha: route.commit,
    }),
    cachedGetCommitDiff(route.rid, route.commit, 3, true),
  ]);

  return {
    resource: "repo.commit",
    params: { sidebarData, repo, commit, diff },
  };
}

export async function loadIssue(
  route: RepoIssueRoute,
): Promise<LoadedRepoIssueRoute> {
  const [sidebarData, repo, issue, activity, issues, threads] =
    await Promise.all([
      loadSidebarData(),
      invoke<RepoInfo>("repo_by_id", {
        rid: route.rid,
      }),
      invoke<Issue>("issue_by_id", {
        rid: route.rid,
        id: route.issue,
      }),
      invoke<Operation<IssueAction>[]>("activity_by_issue", {
        rid: route.rid,
        id: route.issue,
      }),
      invoke<Issue[]>("list_issues", {
        rid: route.rid,
        status: route.status,
      }),
      invoke<Thread[]>("comment_threads_by_issue_id", {
        rid: route.rid,
        id: route.issue,
      }),
    ]);

  return {
    resource: "repo.issue",
    params: {
      sidebarData,
      repo,
      config: sidebarData.config,
      issue,
      activity,
      issues,
      threads,
      status: route.status,
    },
  };
}

export async function loadIssues(
  route: RepoIssuesRoute,
): Promise<LoadedRepoIssuesRoute> {
  const [sidebarData, repo, issues] = await Promise.all([
    loadSidebarData(),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
    }),
    invoke<Issue[]>("list_issues", {
      rid: route.rid,
      status: route.status,
    }),
  ]);

  return {
    resource: "repo.issues",
    params: { sidebarData, repo, issues, status: route.status },
  };
}

export function repoRouteToPath(route: RepoRoute): string {
  const pathSegments = ["/repos", route.rid];
  const searchParams = new URLSearchParams();

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

export function repoUrlToRoute(
  segments: string[],
  searchParams: URLSearchParams,
): RepoRoute | null {
  const rid = segments.shift();
  const resource = segments.shift();

  if (rid) {
    if (resource === "home") {
      return { resource: "repo.home", rid, sha: segments.shift() };
    } else if (resource === "commits") {
      const sha = segments.shift();

      if (sha) {
        return { resource: "repo.commit", rid, commit: sha };
      }

      return { resource: "repo.commits", rid };
    } else if (resource === "issues") {
      const idOrAction = segments.shift();
      if (idOrAction) {
        const status = (searchParams.get("status") ?? "all") as IssueStatus;
        if (idOrAction !== "create") {
          return {
            resource: "repo.issue",
            rid,
            issue: idOrAction,
            status,
          };
        }
        return null;
      } else {
        const status = searchParams.get("status");
        if (status === "open" || status === "closed") {
          return { resource: "repo.issues", rid, status };
        } else {
          return { resource: "repo.issues", rid, status: "all" };
        }
      }
    } else if (resource === "patches") {
      const id = segments.shift();
      const status = (searchParams.get("status") ?? undefined) as
        | PatchStatus
        | undefined;
      const reviewId = searchParams.get("review") ?? undefined;
      if (id) {
        return {
          resource: "repo.patch",
          rid,
          patch: id,
          status,
          reviewId,
        };
      } else {
        return { resource: "repo.patches", rid, status };
      }
    } else {
      return null;
    }
  } else {
    return null;
  }
}