fix(repository): ensure commit id is correct (not tree id, but real commit id)
+ 129
- 14
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663711620152,
+  "_generatedAtUnix": 1663723391190,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -22,7 +22,7 @@
       "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
     },
     "RepositoryTreeView": {
-      "hash": "d95f43002e5bcb46f965c529b3306eca87a533b2",
+      "hash": "a554d26afd55e725eeaedac229be2790d72bca85",
       "pathSource": "./app/islands/RepositoryTreeView.tsx",
       "pathBundle": "./public/.islands/RepositoryTreeView.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryTreeView.bundle.js.map"

...
@@ -58,7 +58,7 @@
       "pathSource": "./app/views/repository/RepositoryCreateView.tsx"
     },
     "RepositoryDetailsView": {
-      "hash": "fc8217689030be68927684cf6273eed9ea1550c5",
+      "hash": "96dd6b16b4a39ab559f92ca8489c25bbaa545567",
       "pathSource": "./app/views/repository/RepositoryDetailsView.tsx"
     },
     "RepositoryExploreView": {

app/controllers/repository/getRepositoryDetailsView.ts
@@ -45,6 +45,9 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
       ? await repoService.getRepositoryFileContent(repo, readmeFiles[0])
       : null;
 
+  const commitLogs = await repoService.getRepositoryCommitLog(repo, ref, true);
+  const lastCommit = commitLogs.length >= 1 ? commitLogs[0] : null;
+
   const reqHandler = reply.makeRequestHandler(request, reply);
 
   try {

...
@@ -54,6 +57,7 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
         http: await repoService.getRepositoryHTTPCloneUrl(repo),
         ssh: await repoService.getRepositorySSHCloneUrl(repo),
       },
+      lastCommit,
       parentOrg,
       path,
       readmeFileContent,

app/islands/RepositoryTreeView.tsx
@@ -4,21 +4,21 @@ import type { ReactIsland } from "@ethicdevs/react-monolith";
 import React, { useCallback } from "react";
 import styled from "styled-components";
 // app
-import { RepositoryFile, RepositoryHead } from "../types";
+import { RepositoryFile, RepositoryLog } from "../types";
 
 export interface RepositoryTreeViewProps {
   currPath: string;
+  lastCommit: RepositoryLog;
   orgSlug: string;
-  repoHead: RepositoryHead;
   repoFiles: RepositoryFile[];
   repoSlug: string;
 }
 
 const RepositoryTreeView: ReactIsland<RepositoryTreeViewProps> = ({
   currPath,
+  lastCommit,
   orgSlug,
   repoFiles,
-  repoHead,
   repoSlug,
 }) => {
   const buildRepoFileLink = useCallback(

...
@@ -40,18 +40,18 @@ const RepositoryTreeView: ReactIsland<RepositoryTreeViewProps> = ({
   return (
     <StyledRepositoryTreeViewContainer>
       <div>
-        <strong>{repoHead.author.name}</strong>
+        <strong>{lastCommit.author.name}</strong>
         {" ∙ "}
-        <span>{repoHead.commitMessage}</span>
+        <span>{lastCommit.subject}</span>
         {" - "}
         <span>
-          {repoHead.treeId.substring(0, 8)}
-          {repoHead.parentId
-            ? ` ∙ parent ${repoHead.parentId.substring(0, 8)}`
+          {lastCommit.abbreviated_commit}
+          {lastCommit.abbreviated_parent.trim() != ""
+            ? ` ∙ parent ${lastCommit.abbreviated_parent}`
             : ""}
         </span>
         {" ∙ "}
-        <span>{new Date(repoHead.author.timestamp * 1000).toUTCString()}</span>
+        <span>{new Date(lastCommit.author.date).toUTCString()}</span>
       </div>
       <div>
         <ul>

new file
app/services/repository/getRepositoryCommitLog.ts
@@ -0,0 +1,72 @@
+// std
+import { existsSync } from "node:fs";
+import { spawn } from "node:child_process";
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import { Env } from "../../env";
+// service
+import type { RepositoryLog } from "../../types";
+import type { RepositoryServiceDeps } from "./types";
+
+const makeGetRepositoryCommitLog: ServiceMethodFactory<
+  RepositoryServiceDeps,
+  [Repository, string | undefined, boolean | undefined],
+  Promise<RepositoryLog[]>
+> = ({ request }) => {
+  return async (repo, ref = "HEAD", onlyLast = false) => {
+    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}".`
+      );
+    }
+
+    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}`);
+    }
+
+    var format =
+      "{%n  ^@^commit^@^: ^@^%H^@^,%n  ^@^abbreviated_commit^@^: ^@^%h^@^,%n  ^@^tree^@^: ^@^%T^@^,%n  ^@^abbreviated_tree^@^: ^@^%t^@^,%n  ^@^parent^@^: ^@^%P^@^,%n  ^@^abbreviated_parent^@^: ^@^%p^@^,%n  ^@^refs^@^: ^@^%D^@^,%n  ^@^encoding^@^: ^@^%e^@^,%n  ^@^subject^@^: ^@^%s^@^,%n  ^@^sanitized_subject_line^@^: ^@^%f^@^,%n  ^@^body^@^: ^@^%b^@^,%n  ^@^commit_notes^@^: ^@^%N^@^,%n  ^@^verification_flag^@^: ^@^%G?^@^,%n  ^@^signer^@^: ^@^%GS^@^,%n  ^@^signer_key^@^: ^@^%GK^@^,%n  ^@^author^@^: {%n    ^@^name^@^: ^@^%aN^@^,%n    ^@^email^@^: ^@^%aE^@^,%n    ^@^date^@^: ^@^%aD^@^%n  },%n  ^@^commiter^@^: {%n    ^@^name^@^: ^@^%cN^@^,%n    ^@^email^@^: ^@^%cE^@^,%n    ^@^date^@^: ^@^%cD^@^%n  }%n},";
+
+    const gitLogProcess = spawn(
+      "git",
+      ["log", `--pretty=format:${format}`, onlyLast ? "-1" : "", ref],
+      {
+        cwd: repoPath,
+      }
+    );
+
+    const gitLogResult = await new Promise<RepositoryLog[]>(
+      (resolve, reject) => {
+        let buffer = [] as string[];
+        gitLogProcess.stdout.on("data", (data) => buffer.push(data));
+        gitLogProcess.stderr.on("data", (data) => {
+          reject(new Error(Buffer.from(data).toString("utf-8")));
+        });
+        gitLogProcess.stdout.on("close", () => {
+          const escapedJson = buffer
+            .join("")
+            .replace(/\n\^@\^/g, "\\n^@^") // Escape unterminated lines: \n^@\^ -> \\n^@\^
+            .replace(/"/gm, '\\"') // Escape double-quotes: " -> \"
+            .replace(/\^@\^/gm, '"'); // Escaped double-quotes back to double quotes
+          resolve(
+            JSON.parse(`[${escapedJson.substring(0, escapedJson.length - 1)}]`)
+          );
+        });
+      }
+    );
+
+    return gitLogResult as RepositoryLog[];
+  };
+};
+
+export default makeGetRepositoryCommitLog;

app/services/repository/index.ts
@@ -5,6 +5,7 @@ import type { RepositoryServiceAPI, RepositoryServiceDeps } from "./types";
 // service methods
 import { default as makeCreateRepository } from "./createRepository";
 import { default as makeGetRepository } from "./getRepository";
+import { default as makeGetRepositoryCommitLog } from "./getRepositoryCommitLog";
 import { default as makeGetRepositoryExploreCollection } from "./getRepositoryExploreCollection";
 import { default as makeGetRepositoryFileContent } from "./getRepositoryFileContent";
 import { default as makeGetRepositoryFiles } from "./getRepositoryFiles";

...
@@ -19,6 +20,7 @@ export const makeRepositoryService = makeService<
 >({
   createRepository: makeCreateRepository,
   getRepository: makeGetRepository,
+  getRepositoryCommitLog: makeGetRepositoryCommitLog,
   getRepositoryExploreCollection: makeGetRepositoryExploreCollection,
   getRepositoryFileContent: makeGetRepositoryFileContent,
   getRepositoryFiles: makeGetRepositoryFiles,

app/services/repository/types.ts
@@ -11,6 +11,7 @@ import type {
   RepositoryFile,
   RepositoryFileContent,
   RepositoryHead,
+  RepositoryLog,
 } from "../../types";
 
 export interface CreateRepositoryDTO {

...
@@ -31,6 +32,11 @@ export interface CreateRepositoryDTO {
 export interface RepositoryServiceAPI extends ServiceApiContract {
   createRepository(dto: CreateRepositoryDTO): Promise<Repository>;
   getRepository(orgSlug: string, repoSlug: string): Promise<Repository | null>;
+  getRepositoryCommitLog(
+    repository: Repository,
+    ref?: string | undefined,
+    onlyLast?: boolean
+  ): Promise<RepositoryLog[]>;
   getRepositoryExploreCollection(): Promise<
     (Repository & { parentOrg: Organization })[]
   >;

@@ -82,3 +82,31 @@ export interface RepositoryFileContent {
   content: string;
   mimeType: string;
 }
+
+export interface RepositoryLog {
+  commit: string;
+  abbreviated_commit: string;
+  tree: string;
+  abbreviated_tree: string;
+  parent: string;
+  abbreviated_parent: string;
+  refs: string;
+  encoding: string;
+  subject: string;
+  sanitized_subject_line: string;
+  body: string;
+  commit_notes: string;
+  verification_flag: string;
+  signer: string;
+  signer_key: string;
+  author: {
+    name: string;
+    email: string;
+    date: string;
+  };
+  commiter: {
+    name: string;
+    email: string;
+    date: string;
+  };
+}

app/views/repository/RepositoryDetailsView.tsx
@@ -10,6 +10,7 @@ import type {
   RepositoryHead,
   RepositoryFile,
   RepositoryFileContent,
+  RepositoryLog,
 } from "../../types";
 import {
   Card,

...
@@ -28,6 +29,7 @@ export interface RepositoryDetailsViewProps extends CommonProps {
     http: string;
     ssh: string;
   };
+  lastCommit: null | RepositoryLog;
   parentOrg: Organization;
   path: string;
   readmeFileContent: null | RepositoryFileContent;

...
@@ -41,6 +43,7 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
   currentUser,
   commonProps,
   cloneUrl,
+  lastCommit,
   parentOrg,
   path,
   readmeFileContent,

...
@@ -70,7 +73,7 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
         </h1>
         <Grid.Row fluid style={{ marginTop: 32 }}>
           <Grid.Col fluid flex={1}>
-            {repoHead == null ? (
+            {repoHead == null || lastCommit == null ? (
               <Card
                 data-islandid={`${RepositoryInitialSetup.name}$$0`}
                 style={{ width: "100%" }}

...
@@ -91,8 +94,8 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
               >
                 <RepositoryTreeView
                   currPath={path}
+                  lastCommit={lastCommit}
                   orgSlug={parentOrg.slug}
-                  repoHead={repoHead}
                   repoFiles={repoFiles}
                   repoSlug={repo.slug}
                 />