feat(pull_requests): make it possible to create a PR from a branch
+ 718
- 158
in same repository or from forks

@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1665177333861,
+  "_generatedAtUnix": 1665327556839,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -15,6 +15,12 @@
       "pathBundle": "./public/.islands/InstantRouterIndicator.bundle.js",
       "pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
     },
+    "PullRequestSourceSelect": {
+      "hash": "0bf2070d8f31e61476388b8b753d1979ccda9f5d",
+      "pathSource": "./app/islands/PullRequestSourceSelect.tsx",
+      "pathBundle": "./public/.islands/PullRequestSourceSelect.bundle.js",
+      "pathSourceMap": "./public/.islands/PullRequestSourceSelect.bundle.js.map"
+    },
     "RepositoriesList": {
       "hash": "123782350476918ef2f540ddaca91ef9c82bcc8f",
       "pathSource": "./app/islands/RepositoriesList.tsx",

...
@@ -58,7 +64,7 @@
       "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
     },
     "RepositoryPullRequestCreateForm": {
-      "hash": "a0a6064032fc7cc6034090bb18cdf52b6de02d16",
+      "hash": "da23a4714b31aff0f9a61eaf5635080ce8755859",
       "pathSource": "./app/islands/RepositoryPullRequestCreateForm.tsx",
       "pathBundle": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js.map"

...
@@ -120,7 +126,7 @@
       "pathSource": "./app/views/repository/RepositoryForkView.tsx"
     },
     "RepositoryPullRequestCreateView": {
-      "hash": "84f98ab3cd89b3a3842a5e0118a08ae141179a73",
+      "hash": "c35bc6337734776b13969010051805f4dc4c4d3a",
       "pathSource": "./app/views/repository/RepositoryPullRequestCreateView.tsx"
     },
     "RepositoryPullRequestsView": {

app/controllers/repository/getRepositoryPullRequestCreateView.ts
@@ -1,14 +1,18 @@
 // 1st-party
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
+import type { RepositoryWithParentAndForkedFromRepos } from "../../types";
+import { Const } from "../../const";
 import { AppRoute, AppRoutesParams } from "../../routes";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";
+import { makeUsersService } from "../../services/user";
 // app islands
 import {
   PullRequestFormState,
   RepositoryPullRequestCreateFormVariant,
+  RepositoryPullRequestTarget,
 } from "../../islands/RepositoryPullRequestCreateForm";
 // app views
 import RepositoryPullRequestCreateView, {

...
@@ -19,6 +23,14 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
   request,
   reply
 ) => {
+  if (
+    request.session.data.authenticated === false ||
+    request.session.data.curr_user_uid == null
+  ) {
+    reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+    return reply;
+  }
+
   const { from_branch } =
     request.query as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["querystring"];
   const { orgSlug, repoSlug } =

...
@@ -26,6 +38,8 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });
+  const usersService = makeUsersService({ request });
+  usersService;
 
   const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const repo = await repoService.getRepository(orgSlug, repoSlug);

...
@@ -35,26 +49,138 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
   }
 
   const branches = await repoService.getRepositoryBranches(repo);
+  const currentUserForks = await repoService.getCurrentUserRepositoryForks(
+    repo
+  );
 
-  const variant: RepositoryPullRequestCreateFormVariant = {
-    state: PullRequestFormState.CONFIGURE,
-    data: {
-      source: {
-        branches,
-        parentOrg,
-        repo,
-        isFork: repo.forkedFromRepoId != null,
-      },
-      initialTarget:
-        from_branch != null
-          ? {
-              parentOrg,
-              repo,
-              branch: from_branch,
-            }
-          : undefined,
+  const repos = await currentUserForks.reduce(
+    async (accP, r) => {
+      let acc = await accP;
+      acc = {
+        ...acc,
+        [r.slug]: {
+          repo: r as any,
+          branches: await repoService.getRepositoryBranches(r),
+        },
+      };
+      return acc;
     },
-  };
+    Promise.resolve({}) as Promise<{
+      [repoSlug: string]: {
+        repo: RepositoryWithParentAndForkedFromRepos;
+        branches: string[];
+      };
+    }>
+  );
+
+  // Inject PR target's repository first.
+  // const [personalOrg] = await usersService.getUserOrganizations(
+  //   request.session.data.curr_user_uid,
+  //   true // personalOnly
+  // );
+
+  //  const personalRepos = {
+  //    [repo.slug]: { repo: repo as any, branches },
+  //  };
+
+  let variant: RepositoryPullRequestCreateFormVariant;
+  if (from_branch == null) {
+    variant = {
+      state: PullRequestFormState.CONFIGURE,
+      data: {
+        sources: {
+          // In case user has forks on its own, add its personal org (where forks **should** be)
+          // ...(currentUserForks.length >= 1 && {
+          //   [personalOrg.slug]: {
+          //     org: personalOrg,
+          //     repos: personalRepos,
+          //   },
+          // }),
+          // target repo's source
+          [parentOrg.slug]: {
+            org: parentOrg,
+            repos: {
+              [repo.slug]: { repo: repo as any, branches },
+              ...repos,
+            },
+          },
+        },
+        target: {
+          branch: from_branch || Const.PRIMARY_BRANCH_REF,
+          parentOrg,
+          repo,
+        },
+      },
+    };
+  } else {
+    let target: RepositoryPullRequestTarget = {
+      branch: from_branch,
+      parentOrg,
+      repo,
+    };
+
+    const orgRepoBranchRegExp = /^([\w\-_\.]+)\/([\w\-_\.]+)@(.*)/i;
+    const matches = from_branch.match(orgRepoBranchRegExp);
+
+    if (matches != null && Array.isArray(matches)) {
+      const [_, targetOrgSlug, targetRepoSlug, targetFromBranch] = matches;
+      const targetParentOrg = await orgService.getOrganizationBySlug(
+        targetOrgSlug
+      );
+      const targetRepo = await repoService.getRepository(
+        targetOrgSlug,
+        targetRepoSlug
+      );
+
+      if (targetParentOrg == null || targetRepo == null) {
+        return reply.status(404).callNotFound();
+      }
+
+      const fileDiffs = await repoService.getRepositoryRefDiff(
+        repo,
+        Const.PRIMARY_BRANCH_REF,
+        targetFromBranch
+      );
+
+      target = {
+        branch: from_branch,
+        parentOrg: targetParentOrg,
+        repo: targetRepo,
+      };
+
+      variant = {
+        state: PullRequestFormState.COMPARE,
+        data: {
+          source: {
+            branch: Const.PRIMARY_BRANCH_REF,
+            parentOrg,
+            repo,
+          },
+          target,
+          fileDiffs,
+        },
+      };
+    } else {
+      const fileDiffs = await repoService.getRepositoryRefDiff(
+        repo,
+        Const.PRIMARY_BRANCH_REF,
+        from_branch
+      );
+
+      variant = {
+        state: PullRequestFormState.COMPARE,
+        data: {
+          source: {
+            branch: Const.PRIMARY_BRANCH_REF,
+            parentOrg,
+            repo,
+          },
+          target,
+          fileDiffs,
+        },
+      };
+    }
+  }
 
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<RepositoryPullRequestCreateViewProps>(

app/controllers/repository/postRepositoryPullRequestCreateAction.ts
@@ -9,7 +9,9 @@ import {
 } from "../../islands/RepositoryPullRequestCreateForm";
 // app services
 import { makeOrganizationService } from "../../services/organization";
+import { makePullRequestService } from "../../services/pullRequest";
 import { makeRepositoryService } from "../../services/repository";
+import { makeUsersService } from "../../services/user";
 // app views
 import RepositoryPullRequestCreateView, {
   RepositoryPullRequestCreateViewProps,

...
@@ -19,9 +21,19 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
   request,
   reply
 ) => {
+  if (
+    request.session.data.authenticated === false ||
+    request.session.data.curr_user_uid == null
+  ) {
+    reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+    return reply;
+  }
+
   const { orgSlug, repoSlug } =
     request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]["params"];
   const {
+    description,
+    summary,
     state_from: fromState,
     state_dest: desiredState,
     source_parent_org_slug: sourceParentOrgSlug,

...
@@ -34,6 +46,17 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });
+  const pullsService = makePullRequestService({ request });
+  const usersService = makeUsersService({ request });
+
+  const currentUser = await usersService.getUserById(
+    request.session.data.curr_user_uid
+  );
+
+  if (currentUser == null) {
+    reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+    return reply;
+  }
 
   const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const repo = await repoService.getRepository(orgSlug, repoSlug);

...
@@ -107,10 +130,40 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
       },
     };
   } else if (desiredState === PullRequestFormState.DETAILS) {
+    // source
+    const sourceRepo = await repoService.getRepository(
+      sourceParentOrgSlug,
+      sourceRepoSlug
+    );
+    // target
+    const targetRepo = await repoService.getRepository(
+      targetParentOrgSlug,
+      targetRepoSlug
+    );
+
+    if (sourceRepo == null || targetRepo == null) {
+      return reply.status(404).callNotFound();
+    }
+
+    const pullRequest = await pullsService.createPullRequest({
+      summary,
+      textMd: description,
+      author: currentUser,
+      source: {
+        repository: targetRepo,
+        fromBranch: targetRepoDestBranch,
+      },
+      target: {
+        repository: sourceRepo,
+        destBranch: sourceRepoFromBranch,
+      },
+    });
+
     variant = {
       state: desiredState,
       data: {
         canCurrentUserSubmitPullRequest: false,
+        pullRequest,
       },
     };
   }

new file
app/islands/PullRequestSourceSelect.tsx
@@ -0,0 +1,224 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+// generated via script[generate:prisma]
+import { Organization } from "@prisma/client";
+// app
+import { RepositoryWithParentAndForkedFromRepos } from "../types";
+
+export interface PullRequestSourceSelectionChangeEvent {
+  data: {
+    selectedOrg: {
+      org: Organization;
+      repos: {
+        [repoSlug: string]: {
+          repo: RepositoryWithParentAndForkedFromRepos;
+          branches: string[];
+        };
+      };
+    };
+    selectedRepo: {
+      repo: RepositoryWithParentAndForkedFromRepos;
+      branches: string[];
+    };
+    selectedBranch: string;
+  };
+}
+
+export interface PullRequestSourceSelectProps {
+  defaultSource: {
+    org: Organization;
+    repo: RepositoryWithParentAndForkedFromRepos;
+    branch: string;
+  };
+  sources: {
+    [orgSlug: string]: {
+      org: Organization;
+      repos: {
+        [repoSlug: string]: {
+          repo: RepositoryWithParentAndForkedFromRepos;
+          branches: string[];
+        };
+      };
+    };
+  };
+  namePrefix: "source" | "target";
+  onSelectionChange?: (
+    ev: PullRequestSourceSelectionChangeEvent
+  ) => void | Promise<void>;
+}
+
+const PullRequestSourceSelect: ReactIsland<PullRequestSourceSelectProps> = ({
+  defaultSource,
+  sources,
+  namePrefix,
+  onSelectionChange,
+}) => {
+  const [selectedOrgSlug, setSelectedOrgSlug] = useState<string>(
+    defaultSource.org.slug
+  );
+  const [selectedRepoSlug, setSelectedRepoSlug] = useState<string>(
+    defaultSource.repo.slug
+  );
+  const [selectedBranch, setSelectedBranch] = useState<string>(
+    defaultSource.branch
+  );
+
+  const allOrganizations = useMemo(() => {
+    const orgsBySlug = Object.entries(sources);
+    return orgsBySlug.reduce((acc, [orgSlug, { org }]) => {
+      acc = [
+        ...acc,
+        {
+          key: orgSlug,
+          value: org.displayName || org.slug,
+        },
+      ];
+      return acc;
+    }, [] as { key: string; value: string }[]);
+  }, [sources]);
+
+  const allReposBySelectedOrgSlug = useMemo(() => {
+    const orgsBySlug = Object.entries(sources);
+    const org = orgsBySlug.find(([_, o]) => o.org.slug === selectedOrgSlug);
+    if (org == null) return [];
+    const [_selectedOrgSlug, { repos: orgRepositories }] = org;
+    const reposBySlug = Object.entries(orgRepositories);
+    return reposBySlug.reduce((acc, [repoSlug, { repo }]) => {
+      acc = [
+        ...acc,
+        {
+          key: repoSlug,
+          value: repo.displayName || repo.slug,
+        },
+      ];
+      return acc;
+    }, [] as { key: string; value: string }[]);
+  }, [sources, selectedOrgSlug]);
+
+  const allBranchesBySelectedOrgAndRepoSlugs = useMemo(() => {
+    const orgsBySlug = Object.entries(sources);
+    const org = orgsBySlug.find(([_, o]) => o.org.slug === selectedOrgSlug);
+    if (org == null) return [];
+    const [_selectedOrgSlug, { repos: orgRepositories }] = org;
+    const reposBySlug = Object.entries(orgRepositories);
+    const repo = reposBySlug.find(([_, r]) => r.repo.slug === selectedRepoSlug);
+    if (repo == null) return [];
+    const [_selectedRepoSlug, { branches: repoBranches }] = repo;
+    return repoBranches.map((branch) => ({
+      key: branch,
+      value: branch,
+    })) as { key: string; value: string }[];
+  }, [sources, selectedOrgSlug, selectedRepoSlug]);
+
+  const selectedOrg = useMemo(() => {
+    const orgsBySlug = Object.entries(sources);
+    const org = orgsBySlug.find(([_, o]) => o.org.slug === selectedOrgSlug);
+    return org != null ? org[1] : null;
+  }, [sources, selectedOrgSlug]);
+
+  const selectedRepo = useMemo(() => {
+    const orgsBySlug = Object.entries(sources);
+    const parentOrg = orgsBySlug.find(
+      ([_, o]) => o.org.slug === selectedOrgSlug
+    );
+    if (parentOrg == null) return null;
+    const reposBySlug = Object.entries(parentOrg[1].repos);
+    const repo = reposBySlug.find(([_, r]) => r.repo.slug === selectedRepoSlug);
+    return repo != null ? repo[1] : null;
+  }, [sources, selectedOrgSlug, selectedRepoSlug]);
+
+  const onOrgSlugChange = useCallback(
+    (ev: React.ChangeEvent<HTMLSelectElement>) => {
+      if (ev == null) return;
+      if (ev.target == null) return;
+      if (ev.target.value == null) return;
+      setSelectedOrgSlug(ev.target.value);
+    },
+    [setSelectedOrgSlug]
+  );
+
+  const onRepoSlugChange = useCallback(
+    (ev: React.ChangeEvent<HTMLSelectElement>) => {
+      if (ev == null) return;
+      if (ev.target == null) return;
+      if (ev.target.value == null) return;
+      setSelectedRepoSlug(ev.target.value);
+    },
+    [setSelectedRepoSlug]
+  );
+
+  const onBranchChange = useCallback(
+    (ev: React.ChangeEvent<HTMLSelectElement>) => {
+      if (ev == null) return;
+      if (ev.target == null) return;
+      if (ev.target.value == null) return;
+      setSelectedBranch(ev.target.value);
+    },
+    [setSelectedBranch]
+  );
+
+  const keyValueToOptionMapFn = useMemo(
+    () =>
+      ({ key, value }: { key: string; value: string }) =>
+        (
+          <option key={key} value={key}>
+            {value}
+          </option>
+        ),
+    []
+  );
+
+  useEffect(() => {
+    if (selectedOrg != null && selectedRepo != null && selectedBranch != null) {
+      if (
+        onSelectionChange != null &&
+        typeof onSelectionChange === "function" &&
+        selectedOrg != null &&
+        selectedRepo != null
+      ) {
+        onSelectionChange({
+          data: {
+            selectedOrg,
+            selectedRepo,
+            selectedBranch,
+          },
+        });
+      }
+    }
+  }, [onSelectionChange, selectedOrg, selectedRepo, selectedBranch]);
+
+  return (
+    <div>
+      <select
+        name={`${namePrefix}_parent_org_slug`}
+        value={selectedOrgSlug}
+        onChange={onOrgSlugChange}
+      >
+        {allOrganizations.map(keyValueToOptionMapFn)}
+      </select>
+      {" / "}
+      <select
+        name={`${namePrefix}_repository_slug`}
+        value={selectedRepoSlug}
+        onChange={onRepoSlugChange}
+      >
+        {allReposBySelectedOrgSlug.map(keyValueToOptionMapFn)}
+      </select>
+      {" / "}
+      <select
+        name={`${namePrefix}_repository_${
+          namePrefix === "source" ? "from" : "dest"
+        }_branch`}
+        value={selectedBranch}
+        onChange={onBranchChange}
+      >
+        {allBranchesBySelectedOrgAndRepoSlugs.map(keyValueToOptionMapFn)}
+      </select>
+    </div>
+  );
+};
+
+PullRequestSourceSelect.displayName = "PullRequestSourceSelect";
+export default PullRequestSourceSelect;

app/islands/RepositoryPullRequestCreateForm.tsx
@@ -1,19 +1,23 @@
 // 1st-party
 import type { ReactIsland } from "@ethicdevs/react-monolith";
 // 3rd-party
-import React from "react";
+import React, { useCallback } from "react";
 // generated via script[generate:prisma]
-import type { Organization, Repository } from "@prisma/client";
+import type { Organization, PullRequest, Repository } from "@prisma/client";
 // app
 import type {
   AppThemeScheme,
   RepositoryFileDiff,
-  RepositoryForkedFromRepoMeta,
+  RepositoryWithParentAndForkedFromRepos,
 } from "../types";
+import { Card } from "../components/Card.styled";
 import { Grid } from "../components/Grid";
 import { IslandWrapper } from "../components/IslandWrapper.styled";
 // app islands
 import RepositoryFilesDiffsList from "./RepositoryFilesDiffsList";
+import PullRequestSourceSelect, {
+  PullRequestSourceSelectionChangeEvent,
+} from "./PullRequestSourceSelect";
 
 export enum PullRequestFormState {
   CONFIGURE = "configure",

...
@@ -35,14 +39,18 @@ export type RepositoryPullRequestCreateFormPropsByState<
   S extends PullRequestFormState = PullRequestFormState
 > = S extends PullRequestFormState.CONFIGURE
   ? {
-      source: {
-        branches: string[];
-        parentOrg: Organization;
-        repo: Repository;
-        isFork: boolean;
-        forkedFromRepoMetas?: RepositoryForkedFromRepoMeta;
+      sources: {
+        [orgSlug: string]: {
+          org: Organization;
+          repos: {
+            [repoSlug: string]: {
+              repo: RepositoryWithParentAndForkedFromRepos;
+              branches: string[];
+            };
+          };
+        };
       };
-      initialTarget?: RepositoryPullRequestTarget;
+      target: RepositoryPullRequestTarget;
     }
   : S extends PullRequestFormState.COMPARE
   ? {

...
@@ -53,6 +61,7 @@ export type RepositoryPullRequestCreateFormPropsByState<
   : S extends PullRequestFormState.DETAILS
   ? {
       canCurrentUserSubmitPullRequest: boolean;
+      pullRequest: PullRequest;
     }
   : S extends PullRequestFormState.ERROR
   ? {

...
@@ -80,6 +89,8 @@ export type RepositoryPullRequestCreateFormVariant =
 
 export interface RepositoryPullRequestCreateFormProps
   extends RepositoryPullRequestCreateFormPropsCommon {
+  parentOrgSlug: string;
+  repoSlug: string;
   themeScheme: AppThemeScheme;
   variant: RepositoryPullRequestCreateFormVariant;
 }

...
@@ -126,21 +137,94 @@ const isErrorStateData = (
   typeof i !== "undefined" && i != null && s === PullRequestFormState.ERROR;
 
 const RepositoryPullRequestCreateForm: ReactIsland<RepositoryPullRequestCreateFormProps> =
-  ({ themeScheme, variant: { state, data } }) => {
+  ({ parentOrgSlug, repoSlug, themeScheme, variant: { state, data } }) => {
+    /*const [selectedSourceOrgSlug, setSelectedSourceOrgSlug] =
+      useState<null | string>(null);*/
+
+    const onSourceSelectionChange = useCallback(
+      (ev: PullRequestSourceSelectionChangeEvent) => {
+        console.warn(
+          `Selected: ${ev.data.selectedOrg.org.slug} / ${ev.data.selectedRepo.repo.slug} / ${ev.data.selectedBranch}`
+        );
+      },
+      []
+    );
+
     // PullRequestFormState.CONFIGURE
     if (isConfigureState(state) && isConfigureStateData(state, data)) {
       return (
         <Grid.Col fluid nowrap>
-          <h1>Configure input and output (org, repo, branch)</h1>
           <form
             method={"POST"}
-            action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}
+            action={`/${parentOrgSlug}/${repoSlug}/pulls/new`}
+            style={{ width: "100%" }}
+          >
+            <Card themeScheme={themeScheme}>
+              <input type={"hidden"} name={"state_from"} value={state} />
+              <input
+                type={"hidden"}
+                name={"state_dest"}
+                value={PullRequestFormState.COMPARE}
+              />
+              <Grid.Row fluid alignItems={"center"}>
+                <div data-islandid={`${PullRequestSourceSelect.name}$$0`}>
+                  <PullRequestSourceSelect
+                    namePrefix={"source"}
+                    defaultSource={{
+                      org: Object.values(data.sources)[0].org,
+                      repo: Object.values(
+                        Object.values(data.sources)[0].repos
+                      )[0].repo,
+                      branch: Object.values(
+                        Object.values(data.sources)[0].repos
+                      )[0].branches[0],
+                    }}
+                    sources={data.sources}
+                    onSelectionChange={onSourceSelectionChange}
+                  />
+                </div>
+                <div style={{ margin: "0 8px" }}>
+                  <span>vs.</span>
+                </div>
+                <div data-islandid={`${PullRequestSourceSelect.name}$$1`}>
+                  <PullRequestSourceSelect
+                    namePrefix={"target"}
+                    defaultSource={{
+                      org: Object.values(data.sources)[0].org,
+                      repo: Object.values(
+                        Object.values(data.sources)[0].repos
+                      )[0].repo,
+                      branch: Object.values(
+                        Object.values(data.sources)[0].repos
+                      )[0].branches[0],
+                    }}
+                    sources={data.sources}
+                    onSelectionChange={onSourceSelectionChange}
+                  />
+                </div>
+                <button type={"submit"} style={{ marginLeft: 8 }}>
+                  Compare
+                </button>
+              </Grid.Row>
+            </Card>
+          </form>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.COMPARE
+    if (isCompareState(state) && isCompareStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <form
+            method={"POST"}
+            action={`/${parentOrgSlug}/${repoSlug}/pulls/new`}
+            style={{ width: "100%" }}
           >
             <input type={"hidden"} name={"state_from"} value={state} />
             <input
               type={"hidden"}
               name={"state_dest"}
-              value={PullRequestFormState.COMPARE}
+              value={PullRequestFormState.DETAILS}
             />
             <input
               type={"hidden"}

...
@@ -154,124 +238,67 @@ const RepositoryPullRequestCreateForm: ReactIsland<RepositoryPullRequestCreateFo
             />
             <input
               type={"hidden"}
-              name={"target_parent_org_slug"}
-              value={
-                data.initialTarget?.parentOrg.slug || data.source.parentOrg.slug
-              }
+              name={"source_repository_from_branch"}
+              value={data.source.branch}
             />
             <input
               type={"hidden"}
-              name={"target_repository_slug"}
-              value={
-                data.initialTarget?.parentOrg.slug || data.source.repo.slug
-              }
-            />
-
-            <div>
-              <span>Source:</span>
-            </div>
-            <input
-              disabled
-              type={"text"}
-              name={"source_parent_org_display_name"}
-              value={
-                data.source.parentOrg.displayName || data.source.parentOrg.slug
-              }
-            />
-            {" / "}
-            <input
-              disabled
-              type={"text"}
-              name={"source_repository_display_name"}
-              value={data.source.repo.displayName || data.source.repo.slug}
-            />
-            {" / "}
-            <select name={"source_repository_from_branch"}>
-              {data.source.branches.map((branch, idx) => (
-                <option key={[branch, idx].join(":")} value={branch}>
-                  {branch}
-                </option>
-              ))}
-            </select>
-
-            {/* VS. */}
-
-            <div>
-              <span>Target:</span>
-            </div>
-
-            <input
-              disabled
-              type={"text"}
-              name={"target_parent_org_display_name"}
-              value={
-                data.initialTarget?.parentOrg.displayName ||
-                data.initialTarget?.parentOrg.slug ||
-                data.source.parentOrg.displayName ||
-                data.source.parentOrg.slug
-              }
+              name={"target_parent_org_slug"}
+              value={data.target.parentOrg.slug}
             />
-            {" / "}
             <input
-              disabled
-              type={"text"}
-              name={"target_repository_display_name"}
-              value={
-                data.initialTarget?.repo.displayName ||
-                data.initialTarget?.repo.slug ||
-                data.source.repo.displayName ||
-                data.source.repo.slug
-              }
+              type={"hidden"}
+              name={"target_repository_slug"}
+              value={data.target.repo.slug}
             />
-            {" / "}
-            <select
-              name={"target_repository_dest_branch"}
-              defaultValue={
-                data.initialTarget != null
-                  ? data.initialTarget.branch
-                  : data.source.branches.length >= 1
-                  ? data.source.branches[0]
-                  : undefined
-              }
-            >
-              {data.initialTarget != null && (
-                <option
-                  key={data.initialTarget.branch}
-                  value={data.initialTarget.branch}
-                >
-                  {data.initialTarget.branch}
-                </option>
-              )}
-              {data.source.branches.map((branch, idx) => (
-                <option key={[branch, idx].join(":")} value={branch}>
-                  {branch}
-                </option>
-              ))}
-            </select>
-
-            <button type={"submit"}>Go to COMPARE step</button>
-          </form>
-        </Grid.Col>
-      );
-    }
-    // PullRequestFormState.COMPARE
-    if (isCompareState(state) && isCompareStateData(state, data)) {
-      return (
-        <Grid.Col fluid nowrap>
-          <h1>Compare input and output (org, repo, branch)</h1>
-          <form
-            method={"POST"}
-            action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}
-          >
-            <input type={"hidden"} name={"state_from"} value={state} />
             <input
               type={"hidden"}
-              name={"state_dest"}
-              value={PullRequestFormState.DETAILS}
+              name={"target_repository_dest_branch"}
+              value={data.target.branch}
             />
-            <button type={"submit"}>Go to DETAILS step</button>
+            <Grid.Row fluid>
+              <Grid.Col fluid>
+                <Card themeScheme={themeScheme} style={{ width: "100%" }}>
+                  <Grid.Col fluid>
+                    <label htmlFor={"summary"}>Summary</label>
+                    <input
+                      type={"text"}
+                      name={"summary"}
+                      style={{ width: "100%" }}
+                    />
+                  </Grid.Col>
+                  <Grid.Col fluid style={{ marginTop: 8 }}>
+                    <label htmlFor={"description"}>Description</label>
+                    <textarea
+                      name={"description"}
+                      style={{
+                        width: "100%",
+                        maxWidth: "100%",
+                        minWidth: "100%",
+                        minHeight: 180,
+                      }}
+                    ></textarea>
+                  </Grid.Col>
+                  <Grid.Col
+                    fluid
+                    style={{ marginTop: 8 }}
+                    alignItems={"flex-end"}
+                  >
+                    <button type={"submit"}>Create Pull Request</button>
+                  </Grid.Col>
+                </Card>
+              </Grid.Col>
+              <Grid.Col flex={0.3} style={{ marginLeft: 16 }}>
+                <Card themeScheme={themeScheme} style={{ width: "100%" }}>
+                  <span>fuck the police</span>
+                </Card>
+              </Grid.Col>
+            </Grid.Row>
           </form>
-          <IslandWrapper data-islandid={`${RepositoryFilesDiffsList.name}$$0`}>
+          <IslandWrapper
+            data-islandid={`${RepositoryFilesDiffsList.name}$$0`}
+            style={{ marginTop: 16 }}
+          >
             <RepositoryFilesDiffsList
               filesDiffs={data.fileDiffs}
               themeScheme={themeScheme}

...
@@ -287,7 +314,6 @@ const RepositoryPullRequestCreateForm: ReactIsland<RepositoryPullRequestCreateFo
     if (isDetailsState(state) && isDetailsStateData(state, data)) {
       return (
         <Grid.Col fluid nowrap>
-          <h1>Add details about the Pull Request:</h1>
           <form
             method={"POST"}
             // action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}

new file
app/services/repository/getCurrentUserRepositoryForks.ts
@@ -0,0 +1,77 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { Repository } from "@prisma/client";
+// app
+import { RepositoryWithParentAndForkedFromRepos } from "../../types";
+// service
+import type { RepositoryServiceDeps } from "./types";
+
+const makeGetCurrentUserRepositoryForks: ServiceMethodFactory<
+  RepositoryServiceDeps,
+  [Repository],
+  Promise<RepositoryWithParentAndForkedFromRepos[]>
+> = ({ request }) => {
+  return async (repository) => {
+    if (request.session.data.curr_user_uid == null) {
+      return [];
+    }
+
+    const currentUserForks = await request.prisma.repository.findMany({
+      include: {
+        organization: true,
+        forkedFromRepo: {
+          select: {
+            id: true,
+            slug: true,
+            displayName: true,
+            organization: {
+              select: {
+                id: true,
+                slug: true,
+                displayName: true,
+              },
+            },
+          },
+        },
+        forks: {
+          select: {
+            _count: true,
+          },
+        },
+      },
+      where: {
+        isFork: true,
+        forkedFromRepo: {
+          id: {
+            equals: repository.id,
+          },
+        },
+        organization: {
+          OR: [
+            {
+              owner: {
+                id: {
+                  equals: request.session.data.curr_user_uid,
+                },
+              },
+            },
+            {
+              memberships: {
+                some: {
+                  userId: {
+                    equals: request.session.data.curr_user_uid,
+                  },
+                },
+              },
+            },
+          ],
+        },
+      },
+    });
+
+    return currentUserForks;
+  };
+};
+
+export default makeGetCurrentUserRepositoryForks;

app/services/repository/getRepositoryBranches.ts
@@ -51,10 +51,13 @@ const makeGetRepositoryBranches: ServiceMethodFactory<
         });
       });
 
-      return gitBranchResult
+      const branches = gitBranchResult
         .split("\n")
         .map((branch) => branch.trim().replace(/^\* /i, ""))
         .filter((x) => x != null && x.trim() !== "");
+
+      console.log("branches:", parentOrg.slug, repo.slug, branches);
+      return branches;
     } catch (_) {
       return [];
     }

app/services/repository/index.ts
@@ -6,6 +6,7 @@ import type { RepositoryServiceAPI, RepositoryServiceDeps } from "./types";
 import { default as makeCanUserAccessRepository } from "./canUserAccessRepository";
 import { default as makeCreateRepository } from "./createRepository";
 import { default as makeForkRepository } from "./forkRepository";
+import { default as makeGetCurrentUserRepositoryForks } from "./getCurrentUserRepositoryForks";
 import { default as makeGetRepository } from "./getRepository";
 import { default as makeGetRepositoryBranches } from "./getRepositoryBranches";
 import { default as makeGetRepositoryCommitLog } from "./getRepositoryCommitLog";

...
@@ -28,6 +29,7 @@ export const makeRepositoryService = makeService<
   canUserAccessRepository: makeCanUserAccessRepository,
   createRepository: makeCreateRepository,
   forkRepository: makeForkRepository,
+  getCurrentUserRepositoryForks: makeGetCurrentUserRepositoryForks,
   getRepository: makeGetRepository,
   getRepositoryBranches: makeGetRepositoryBranches,
   getRepositoryCommitLog: makeGetRepositoryCommitLog,

app/services/repository/types.ts
@@ -15,6 +15,7 @@ import type {
   RepositoryLog,
   RepositoryObject,
   RepositoryWithForkedFromRepo,
+  RepositoryWithParentAndForkedFromRepos,
 } from "../../types";
 
 export interface CreateRepositoryDTO {

...
@@ -54,6 +55,9 @@ export interface RepositoryServiceAPI extends ServiceApiContract {
     orgSlug: string,
     repoSlug: string
   ): Promise<RepositoryWithForkedFromRepo | null>;
+  getCurrentUserRepositoryForks(
+    repository: Repository
+  ): Promise<RepositoryWithParentAndForkedFromRepos[]>;
   getRepositoryBranches(repository: Repository): Promise<string[]>;
   getRepositoryCommitLog(
     repository: Repository,

app/services/user/getUserOrganizations.ts
@@ -1,30 +1,52 @@
 // 1st-party
 import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
-import { Organization } from "@prisma/client";
+import { Organization, OrganizationKind } from "@prisma/client";
 // app
 import type { UsersServiceDeps } from "./types";
 
 const getUserOrganizations: ServiceMethodFactory<
   UsersServiceDeps,
-  [string],
+  [string, undefined | boolean],
   Promise<Organization[]>
 > = ({ request }) => {
-  return async (userId) => {
+  return async (userId, personalOnly = false) => {
     const userOrgs = await request.prisma.organization.findMany({
       where: {
-        OR: [
-          {
-            ownerId: userId,
+        AND: [
+          personalOnly === true && {
+            kind: OrganizationKind.PERSONAL,
           },
           {
-            memberships: {
-              some: {
-                userId,
+            OR: [
+              {
+                ownerId: userId,
+              },
+              {
+                memberships: {
+                  some: {
+                    userId,
+                  },
+                },
               },
-            },
+            ],
           },
-        ],
+        ].filter(
+          (
+            x
+          ): x is
+            | { kind: "PERSONAL"; OR?: undefined }
+            | {
+                OR: (
+                  | { ownerId: string; memberships?: undefined }
+                  | {
+                      memberships: { some: { userId: string } };
+                      ownerId?: undefined;
+                    }
+                )[];
+                kind?: undefined;
+              } => x != null && x !== false
+        ),
       },
       orderBy: {
         kind: "asc", // private first then company

app/services/user/types.ts
@@ -18,7 +18,10 @@ export interface UsersServiceAPI extends ServiceApiContract {
   getUserOrganizationMemberships(
     userId: string
   ): Promise<OrganizationMembership[]>;
-  getUserOrganizations(userId: string): Promise<Organization[]>;
+  getUserOrganizations(
+    userId: string,
+    personalOnly?: boolean
+  ): Promise<Organization[]>;
   getUserRepositories(
     user: User,
     where?: Prisma.RepositoryWhereInput

@@ -1,4 +1,10 @@
-import type { Prisma, GlobalRole, Repository } from "@prisma/client";
+// generated via script[generate:prisma]
+import type {
+  Prisma,
+  GlobalRole,
+  Repository,
+  Organization,
+} from "@prisma/client";
 
 export type AppThemeScheme = "light" | "dark";
 export type WithThemeSchemeProp = {

...
@@ -175,3 +181,8 @@ export type RepositoryWithForkedFromRepo = Repository & {
     _count: Prisma.RepositoryCountOutputType;
   }[];
 };
+
+export type RepositoryWithParentAndForkedFromRepos =
+  RepositoryWithForkedFromRepo & {
+    organization: Organization;
+  };

app/views/repository/RepositoryPullRequestCreateView.tsx
@@ -37,8 +37,11 @@ const RepositoryPullRequestCreateView: ReactView<RepositoryPullRequestCreateView
           </IslandWrapper>
           <IslandWrapper
             data-islandid={`${RepositoryPullRequestCreateForm.name}$$0`}
+            style={{ marginTop: 24 }}
           >
             <RepositoryPullRequestCreateForm
+              parentOrgSlug={parentOrg.slug}
+              repoSlug={repo.slug}
               themeScheme={commonProps.themeScheme}
               variant={variant}
             />

GitFOSS