feat(repository): add complete support for git diff/compare/history
+ 261
- 13
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663803924923,
+  "_generatedAtUnix": 1663839168820,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -21,6 +21,12 @@
       "pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"
     },
+    "RepositoryFilesDiffsList": {
+      "hash": "9b4c9043730e3c41c7d7f4f86b90745331cd91b2",
+      "pathSource": "./app/islands/RepositoryFilesDiffsList.tsx",
+      "pathBundle": "./public/.islands/RepositoryFilesDiffsList.bundle.js",
+      "pathSourceMap": "./public/.islands/RepositoryFilesDiffsList.bundle.js.map"
+    },
     "RepositoryInitialSetup": {
       "hash": "ade4c6a8e8d2b5abef10d1e4281cf6888d128b5c",
       "pathSource": "./app/islands/RepositoryInitialSetup.tsx",

...
@@ -28,7 +34,7 @@
       "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
     },
     "RepositoryTreeView": {
-      "hash": "f1affa1ed5c3dc2c543bf07bd799d582bb1292de",
+      "hash": "d044fd8b6334961b63150969b638b2f74addade9",
       "pathSource": "./app/islands/RepositoryTreeView.tsx",
       "pathBundle": "./public/.islands/RepositoryTreeView.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryTreeView.bundle.js.map"

...
@@ -60,9 +66,13 @@
       "pathSource": "./app/views/repository/RepositoryBrowserView.tsx"
     },
     "RepositoryCommitsLogView": {
-      "hash": "2cae832cc936ac1d69fda4ad0ef1a470f1424933",
+      "hash": "9c2b20bc4bc30e627457e6d48b353ff0d9816b09",
       "pathSource": "./app/views/repository/RepositoryCommitsLogView.tsx"
     },
+    "RepositoryCompareView": {
+      "hash": "2e9f1d15bbd826f1a6d89bb9d19ae6172062071c",
+      "pathSource": "./app/views/repository/RepositoryCompareView.tsx"
+    },
     "RepositoryCreateView": {
       "hash": "f141b710674ecd55db0fa429ab73901e30001a39",
       "pathSource": "./app/views/repository/RepositoryCreateView.tsx"

new file
app/controllers/repository/getRepositoryCompareView.ts
@@ -0,0 +1,50 @@
+// 1st-party
+import { ReqHandler } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { User } from "@prisma/client";
+import { AppRoute, AppRoutesParams } from "app/routes";
+// app services
+import { makeOrganizationService } from "../../services/organization";
+import { makeRepositoryService } from "../../services/repository";
+import { makeUsersService } from "../../services/user";
+// app views
+import RepositoryCompareView, {
+  RepositoryCompareViewProps,
+} from "../../views/repository/RepositoryCompareView";
+
+const getRepositoryCompareView: ReqHandler = async (request, reply) => {
+  const { curr_user_uid } = request.session.data;
+  const { orgSlug, repoSlug, refA, refB } =
+    request.params as AppRoutesParams[AppRoute.REPOSITORY_COMPARE]["params"];
+
+  const orgService = makeOrganizationService({ request });
+  const repoService = makeRepositoryService({ request });
+  const userService = makeUsersService({ request });
+
+  const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
+  const repo = await repoService.getRepository(orgSlug, repoSlug);
+
+  if (parentOrg == null || repo == null) {
+    return reply.status(404).callNotFound();
+  }
+
+  let currentUser: null | User = null;
+
+  if (curr_user_uid != null) {
+    currentUser = await userService.getUserById(curr_user_uid);
+  }
+
+  const filesDiffs = await repoService.getRepositoryRefDiff(repo, refA, refB);
+
+  const reqHandler = reply.makeRequestHandler(request, reply);
+  reqHandler<RepositoryCompareViewProps>(RepositoryCompareView.name, {
+    currentUser,
+    filesDiffs,
+    parentOrg,
+    repo,
+    refA,
+    refB,
+  });
+};
+
+export default getRepositoryCompareView;

app/controllers/repository/index.ts
@@ -1,5 +1,6 @@
 import { default as getRepositoryBrowserView } from "./getRepositoryBrowserView";
 import { default as getRepositoryCommitsLogView } from "./getRepositoryCommitsLogView";
+import { default as getRepositoryCompareView } from "./getRepositoryCompareView";
 import { default as getRepositoryCreateView } from "./getRepositoryCreateView";
 import { default as getRepositoryDetailsView } from "./getRepositoryDetailsView";
 import { default as getRepositoryExploreView } from "./getRepositoryExploreView";

...
@@ -8,6 +9,7 @@ import { default as postRepositoryCreateAction } from "./postRepositoryCreateAct
 export const RepositoryController = {
   getRepositoryBrowserView,
   getRepositoryCommitsLogView,
+  getRepositoryCompareView,
   getRepositoryCreateView,
   getRepositoryDetailsView,
   getRepositoryExploreView,

new file
app/islands/RepositoryFilesDiffsList.tsx
@@ -0,0 +1,83 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// app
+import type {
+  RepositoryFileDiff,
+  RepositoryFileDiffChunk,
+  WithThemeSchemeProp,
+} from "../types";
+import { Code, Card, Grid, getThemedCodeCss } from "../components";
+
+export interface RepositoryFilesDiffsList {
+  filesDiffs: RepositoryFileDiff[];
+  orgSlug: string;
+  repoSlug: string;
+  commitHash: string;
+}
+
+const getChunkContent = (chunk: RepositoryFileDiffChunk): string => {
+  let sb = [] as string[];
+  sb.push(chunk.content);
+  chunk.changes.forEach((change) => sb.push(change.content));
+  return `${sb.join("\n")}\n`;
+};
+
+const RepositoryFilesDiffsList: ReactIsland<
+  RepositoryFilesDiffsList & WithThemeSchemeProp
+> = ({ commitHash, filesDiffs, orgSlug, repoSlug, themeScheme }) => (
+  <>
+    {getThemedCodeCss(themeScheme)}
+    <div style={{ marginTop: 24 }}>
+      {filesDiffs.map(({ chunks, ...diff }) => (
+        <Card
+          key={[diff.from, diff.to].join(":")}
+          style={{ marginTop: 16 }}
+          themeScheme={themeScheme}
+        >
+          <Grid.Col fluid nowrap>
+            <Grid.Row fluid nowrap>
+              <strong>{diff.from}</strong>
+              <span style={{ marginLeft: 16 }}>{" -> "}</span>
+              <strong style={{ marginLeft: 16 }}>{diff.to}</strong>
+            </Grid.Row>
+            <Grid.Row
+              fluid
+              nowrap
+              alignItems={"center"}
+              style={{ marginTop: 8 }}
+            >
+              <div>
+                <strong>additions:</strong> <span>{diff.additions}</span>
+              </div>
+              <div style={{ marginLeft: 16 }}>
+                <strong>deletions:</strong> <span>{diff.deletions}</span>
+              </div>
+              <div style={{ marginLeft: 16 }}>
+                <a
+                  href={`/${orgSlug}/${repoSlug}/${commitHash}/tree/${diff.to}`}
+                >
+                  View file (current ref)
+                </a>
+              </div>
+            </Grid.Row>
+          </Grid.Col>
+          <div>
+            {chunks.map((chunk, idx) => (
+              <Code
+                key={[idx, chunk.content].join(":")}
+                code={getChunkContent(chunk)}
+                language={"diff"}
+                themeScheme={themeScheme}
+              />
+            ))}
+          </div>
+        </Card>
+      ))}
+    </div>
+  </>
+);
+
+RepositoryFilesDiffsList.displayName = "RepositoryFilesDiffsList";
+export default RepositoryFilesDiffsList;

app/islands/RepositoryTreeView.tsx
@@ -60,12 +60,16 @@ const RepositoryTreeView: ReactIsland<RepositoryTreeViewProps> = ({
         {" ∙ "}
         <span>{lastCommit.subject}</span>
         {" - "}
-        <span>
-          {lastCommit.abbreviated_commit}
-          {lastCommit.abbreviated_parent.trim() != ""
-            ? ` ∙ parent ${lastCommit.abbreviated_parent}`
-            : ""}
-        </span>
+        <a
+          href={`/${orgSlug}/${repoSlug}/compare/${lastCommit.abbreviated_parent}..${lastCommit.abbreviated_commit}`}
+        >
+          <span>
+            {lastCommit.abbreviated_commit}
+            {lastCommit.abbreviated_parent.trim() != ""
+              ? ` (parent ${lastCommit.abbreviated_parent})`
+              : ""}
+          </span>
+        </a>
         {" ∙ "}
         <span>{new Date(lastCommit.author.date).toUTCString()}</span>
         <a href={`/${orgSlug}/${repoSlug}/commits`}>History</a>

@@ -29,6 +29,7 @@ export enum AppRoute {
   ORGANIZATION_DETAILS = "organization.details",
   REPOSITORY_EXPLORE = "repository.explore",
   REPOSITORY_COMMITS_LOG = "repository.commits_log",
+  REPOSITORY_COMPARE = "repository.compare",
   REPOSITORY_CREATE = "repository.create",
   REPOSITORY_CREATE_ACTION = "repository.create.action",
   REPOSITORY_DETAILS = "repository.details",

...
@@ -74,6 +75,14 @@ export interface AppRoutesParams extends IRouteParams {
       repoSlug: string;
     };
   };
+  [AppRoute.REPOSITORY_COMPARE]: {
+    params: {
+      orgSlug: string;
+      repoSlug: string;
+      refA: string;
+      refB: string;
+    };
+  };
   [AppRoute.REPOSITORY_CREATE]: undefined;
   [AppRoute.REPOSITORY_CREATE_ACTION]: {
     body: {

...
@@ -188,6 +197,27 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
       },
     },
   },
+  [AppRoute.REPOSITORY_COMPARE]: {
+    params: {
+      type: "object",
+      required: ["orgSlug", "repoSlug", "refA", "refB"],
+      additionalProperties: false,
+      properties: {
+        orgSlug: {
+          type: "string",
+        },
+        repoSlug: {
+          type: "string",
+        },
+        refA: {
+          type: "string",
+        },
+        refB: {
+          type: "string",
+        },
+      },
+    },
+  },
   [AppRoute.REPOSITORY_CREATE]: undefined,
   [AppRoute.REPOSITORY_CREATE_ACTION]: {
     body: {

...
@@ -386,10 +416,16 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_COMMITS_LOG}
           method={"GET"}
           path={"/:orgSlug/:repoSlug/commits"}
-          preHandler={loggedOrLoginRedirect}
           schema={AppRoutesSchemas[AppRoute.REPOSITORY_COMMITS_LOG]}
           handler={RepositoryController.getRepositoryCommitsLogView}
         />
+        <Router.Route
+          name={AppRoute.REPOSITORY_COMPARE}
+          method={"GET"}
+          path={"/:orgSlug/:repoSlug/compare/:refA..:refB"}
+          schema={AppRoutesSchemas[AppRoute.REPOSITORY_COMPARE]}
+          handler={RepositoryController.getRepositoryCompareView}
+        />
         <Router.Route
           name={AppRoute.REPOSITORY_CREATE}
           method={"GET"}

app/services/repository/getRepositoryRefDiff.ts
@@ -58,7 +58,7 @@ const makeGetRepositoryRefDiff: ServiceMethodFactory<
         });
       });
 
-      return parseDiff(gitDiffRefsResult);
+      return parseDiff(gitDiffRefsResult, { findRenames: true });
     } catch (_) {
       return [];
     }

app/views/repository/RepositoryCommitsLogView.tsx
@@ -38,7 +38,7 @@ const RepositoryCommitsLogView: ReactView<RepositoryCommitsLogViewProps> = ({
             {history.map((log) => (
               <li key={log.tree}>
                 <a
-                  href={`/${parentOrg.slug}/${repo.slug}/commits/${log.commit}`}
+                  href={`/${parentOrg.slug}/${repo.slug}/compare/${log.abbreviated_parent}..${log.abbreviated_commit}`}
                 >
                   <strong>{log.author.name}</strong>
                   {" ∙ "}

new file
app/views/repository/RepositoryCompareView.tsx
@@ -0,0 +1,60 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[prisma:generate]
+import type { Organization, Repository, User } from "@prisma/client";
+// app
+import type { CommonProps, RepositoryFileDiff } from "../../types";
+import { Grid, Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoryFilesDiffsList from "../../islands/RepositoryFilesDiffsList";
+
+export interface RepositoryCompareViewProps extends CommonProps {
+  currentUser: null | User;
+  filesDiffs: RepositoryFileDiff[];
+  parentOrg: Organization;
+  repo: Repository;
+  refA: string;
+  refB: string;
+}
+
+const RepositoryCompareView: ReactView<RepositoryCompareViewProps> = ({
+  commonProps,
+  filesDiffs,
+  parentOrg,
+  repo,
+  refA,
+  refB,
+}) => {
+  return (
+    <Layout {...commonProps}>
+      <PageWrapper>
+        <h1>
+          <a href={`/${parentOrg.slug}`}>
+            {parentOrg.displayName || parentOrg.slug}
+          </a>
+          {" / "}
+          <a href={`/${parentOrg.slug}/${repo.slug}`}>
+            {repo.displayName || repo.slug}
+          </a>
+          {" / "}
+          {refA}..{refB}
+        </h1>
+        {/*getThemedCodeCss(commonProps.themeScheme)*/}
+        <Grid.Col fluid data-islandid={`${RepositoryFilesDiffsList.name}$$0`}>
+          <RepositoryFilesDiffsList
+            filesDiffs={filesDiffs}
+            themeScheme={commonProps.themeScheme}
+            orgSlug={parentOrg.slug}
+            repoSlug={repo.slug}
+            commitHash={refB}
+          />
+        </Grid.Col>
+      </PageWrapper>
+    </Layout>
+  );
+};
+
+RepositoryCompareView.displayName = "RepositoryCompareView";
+export default RepositoryCompareView;

@@ -2,6 +2,9 @@ import type { RepositoryFileDiff } from "../app/types";
 
 declare module "diffparser";
 
-export declare const parse: (diff: string) => RepositoryFileDiff[];
+export declare const parse: (
+  diff: string,
+  opts?: { findRenames: boolean }
+) => RepositoryFileDiff[];
 
 export default parse;