feat(repository): add support for retrieving git diff between 2 refs
+ 135
- 2
app/controllers/repository/getRepositoryDetailsView.ts
@@ -30,10 +30,18 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
   const path = "/";
   const ref = "HEAD";
   const repo = await repoService.getRepository(orgSlug, repoSlug);
+
   if (repo == null) {
     return reply.status(404).callNotFound();
   }
 
+  const lastDiff = await repoService.getRepositoryRefDiff(
+    repo,
+    "HEAD",
+    "HEAD^^"
+  );
+  console.log("lastDiff:", lastDiff);
+
   const readmeFiles = await repoService.isFileInRepositoryPath(
     repo,
     "",

app/controllers/repository/getRepositoryExploreView.ts
@@ -9,6 +9,7 @@ import RepositoryExploreView, {
 
 const getRepositoryExploreView: ReqHandler = async (request, reply) => {
   const repoService = makeRepositoryService({ request });
+
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<RepositoryExploreViewProps>(RepositoryExploreView.name, {
     repositories: await repoService.getRepositoryExploreCollection(),

new file
app/services/repository/getRepositoryRefDiff.ts
@@ -0,0 +1,68 @@
+// std
+import { existsSync } from "node:fs";
+import { spawn } from "node:child_process";
+// 1st-party
+import { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// 3rd-party
+import parseDiff from "diffparser";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import type { RepositoryFileDiff } from "../../types";
+import { Env } from "../../env";
+// service
+import { RepositoryServiceDeps } from "./types";
+
+const makeGetRepositoryRefDiff: ServiceMethodFactory<
+  RepositoryServiceDeps,
+  [Repository, string, string | undefined],
+  Promise<RepositoryFileDiff[]>
+> = ({ request }) => {
+  return async (repo, refA, refB = undefined) => {
+    const parentOrg = await request.prisma.organization.findUnique({
+      where: {
+        id: repo.organizationId,
+      },
+    });
+
+    if (parentOrg == null) {
+      throw new Error(
+        `Could not find the parent organization for project "${repo.slug}".`
+      );
+    }
+
+    try {
+      const repoPath = `${Env.GIT_REPOSITORIES_ROOT}/${parentOrg.slug}/${repo.slug}.git`;
+      if (existsSync(repoPath) === false) {
+        throw new Error(
+          `Could not find a valid git repository at: ${repoPath}`
+        );
+      }
+
+      const gitDiffRefsProcess = spawn(
+        "git",
+        ["diff", `${refA}${refB != null ? `..${refB}` : ""}`],
+        {
+          cwd: repoPath,
+        }
+      );
+
+      const gitDiffRefsResult = await new Promise<string>((resolve, reject) => {
+        let buffer = [] as string[];
+        gitDiffRefsProcess.stdout.on("data", (data) => buffer.push(data));
+        gitDiffRefsProcess.stderr.on("data", (data) => {
+          reject(new Error(Buffer.from(data).toString("utf-8")));
+        });
+        gitDiffRefsProcess.stdout.on("close", () => {
+          resolve(buffer.join(""));
+        });
+      });
+
+      return parseDiff(gitDiffRefsResult);
+    } catch (_) {
+      return [];
+    }
+  };
+};
+
+export default makeGetRepositoryRefDiff;

app/services/repository/index.ts
@@ -13,6 +13,7 @@ import { default as makeGetRepositoryFiles } from "./getRepositoryFiles";
 import { default as makeGetRepositoryHead } from "./getRepositoryHead";
 import { default as makeGetRepositoryHTTPCloneUrl } from "./getRepositoryHTTPCloneUrl";
 import { default as makeGetRepositorySSHCloneUrl } from "./getRepositorySSHCloneUrl";
+import { default as makeGetRepositoryRefDiff } from "./getRepositoryRefDiff";
 import { default as makeIsFileInRepositoryPath } from "./isFileInRepositoryPath";
 
 export const makeRepositoryService = makeService<

...
@@ -29,5 +30,6 @@ export const makeRepositoryService = makeService<
   getRepositoryHead: makeGetRepositoryHead,
   getRepositoryHTTPCloneUrl: makeGetRepositoryHTTPCloneUrl,
   getRepositorySSHCloneUrl: makeGetRepositorySSHCloneUrl,
+  getRepositoryRefDiff: makeGetRepositoryRefDiff,
   isFileInRepositoryPath: makeIsFileInRepositoryPath,
 });

app/services/repository/types.ts
@@ -10,6 +10,7 @@ import type { Organization, Repository } from "@prisma/client";
 import type {
   RepositoryFile,
   RepositoryFileContent,
+  RepositoryFileDiff,
   RepositoryHead,
   RepositoryLog,
 } from "../../types";

...
@@ -62,6 +63,11 @@ export interface RepositoryServiceAPI extends ServiceApiContract {
   ): Promise<RepositoryHead>;
   getRepositoryHTTPCloneUrl(repository: Repository): Promise<string>;
   getRepositorySSHCloneUrl(repository: Repository): Promise<string>;
+  getRepositoryRefDiff(
+    repository: Repository,
+    refA: string,
+    refB?: string
+  ): Promise<RepositoryFileDiff[]>;
   isFileInRepositoryPath(
     repository: Repository,
     path: string,

@@ -88,6 +88,33 @@ export interface RepositoryFileContent {
   mimeType: string;
 }
 
+export interface RepositoryFileDiffChunkChange {
+  type: string;
+  del: boolean;
+  oldLine?: number;
+  newLine?: number;
+  position: number;
+  content: string;
+}
+
+export interface RepositoryFileDiffChunk {
+  content: string;
+  changes: RepositoryFileDiffChunkChange[];
+  oldStart: number;
+  oldLines: number;
+  newStart: number;
+  newLines: number;
+}
+
+export interface RepositoryFileDiff {
+  additions: number;
+  deletions: number;
+  index: string[];
+  from: string;
+  to: string;
+  chunks: RepositoryFileDiffChunk[];
+}
+
 export interface RepositoryLog {
   commit: string;
   abbreviated_commit: string;

@@ -35,6 +35,7 @@
     "@prisma/client": "3.15.2",
     "cross-fetch": "^3.1.5",
     "cuid": "^2.1.8",
+    "diffparser": "^2.0.1",
     "dotenv-flow": "^3.2.0",
     "fastify": "^3.27.4",
     "fastify-static": "^4.6.1",

patches/language-detect+1.1.0.patch
@@ -1,5 +1,5 @@
 diff --git a/node_modules/language-detect/vendor/extensions.json b/node_modules/language-detect/vendor/extensions.json
-index 903ba4e..283f806 100644
+index 903ba4e..ac2dd82 100644
 --- a/node_modules/language-detect/vendor/extensions.json
 +++ b/node_modules/language-detect/vendor/extensions.json
 @@ -696,7 +696,8 @@

...
@@ -13,7 +13,7 @@ index 903ba4e..283f806 100644
    ".anim": "Unity3D Asset",
    ".asset": "Unity3D Asset",
 diff --git a/node_modules/language-detect/vendor/filenames.json b/node_modules/language-detect/vendor/filenames.json
-index 18b7307..66fa064 100644
+index 18b7307..8000059 100644
 --- a/node_modules/language-detect/vendor/filenames.json
 +++ b/node_modules/language-detect/vendor/filenames.json
 @@ -1,4 +1,5 @@

...
@@ -22,3 +22,12 @@ index 18b7307..66fa064 100644
    "ant.xml": "Ant Build System",
    "build.xml": "Ant Build System",
    "CMakeLists.txt": "CMake",
+@@ -16,6 +17,8 @@
+   "Fakefile": "Fancy",
+   "ROOT": "Isabelle ROOT",
+   ".jshintrc": "JSON",
++  ".eslintrc": "JSON",
++  ".prettierrc": "JSON",
+   "composer.lock": "JSON",
+   "Jakefile": "JavaScript",
+   "ld.script": "Linker Script",

new file
types/diffparser.d.ts
@@ -0,0 +1,7 @@
+import type { RepositoryFileDiff } from "../app/types";
+
+declare module "diffparser";
+
+export declare const parse: (diff: string) => RepositoryFileDiff[];
+
+export default parse;

@@ -1723,6 +1723,10 @@ diff@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
 
+diffparser@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/diffparser/-/diffparser-2.0.1.tgz#4228d5688ab2f05832c320231deda048fcfce8e7"
+
 domexception@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"