GitFOSS
feat(repository): display latest commit + abbreviated hash for each files
+ 125
- 57
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1664503855283,
+  "_generatedAtUnix": 1664668875140,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -52,7 +52,7 @@
       "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
     },
     "RepositoryTreeView": {
-      "hash": "36df89a99666e3eff40838de53b1ee8cf83c053e",
+      "hash": "1cd46be545be92bd5bf44b4052ed2ba830e3923d",
       "pathSource": "./app/islands/RepositoryTreeView.tsx",
       "pathBundle": "./public/.islands/RepositoryTreeView.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryTreeView.bundle.js.map"

app/controllers/repository/getRepositoryDetailsView.ts
@@ -78,24 +78,33 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
   const branches = await repoService.getRepositoryBranches(repo);
   const tags = await repoService.getRepositoryTags(repo);
 
+  const cloneUrl = {
+    http: await repoService.getRepositoryHTTPCloneUrl(repo),
+    ssh: await repoService.getRepositorySSHCloneUrl(repo),
+  };
+
   const reqHandler = reply.makeRequestHandler(request, reply);
 
   try {
+    const repoHead = await repoService.getRepositoryHead(repo, currentRef);
+    const repoFiles = await repoService.getRepositoryFiles(
+      repo,
+      "",
+      currentRef
+    );
+
     return reqHandler<RepositoryDetailsViewProps>(RepositoryDetailsView.name, {
       branches,
       currentRef,
       currentUser,
-      cloneUrl: {
-        http: await repoService.getRepositoryHTTPCloneUrl(repo),
-        ssh: await repoService.getRepositorySSHCloneUrl(repo),
-      },
+      cloneUrl,
       lastCommit,
       parentOrg,
       path,
       readmeFileContent,
       repo,
-      repoHead: await repoService.getRepositoryHead(repo, currentRef),
-      repoFiles: await repoService.getRepositoryFiles(repo, "", currentRef),
+      repoHead,
+      repoFiles,
       tags,
     });
   } catch (err) {

...
@@ -107,10 +116,7 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
           branches,
           currentRef,
           currentUser,
-          cloneUrl: {
-            http: await repoService.getRepositoryHTTPCloneUrl(repo),
-            ssh: await repoService.getRepositorySSHCloneUrl(repo),
-          },
+          cloneUrl,
           parentOrg,
           path,
           readmeFileContent,

app/controllers/syntax_highlight/highlightCodeAction.ts
@@ -2,7 +2,7 @@
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // 3rd-party
 import Prism from "prismjs";
-import { parse as parseHtmlToJson } from "himalaya";
+import { parse as parseHtmlToJson, TextNode, RealNode } from "himalaya";
 // app
 import type { AppThemeScheme } from "../../types";
 import { AppRoute, AppRoutesParams } from "../../routes";

...
@@ -37,6 +37,36 @@ function getHighlightedCode(
   };
 }
 
+const getNodeTextRecursive = (node: TextNode | RealNode, depth = 0): string => {
+  if (depth > 1000) throw new Error("Too much recursion.");
+  return node.type === "text"
+    ? node.content.replace(/\r\n/i, "\n")
+    : getNodeTextRecursive(node.children[0], depth + 1);
+};
+
+const getNodesRecursive = (
+  node: TextNode | RealNode,
+  depth = 0
+): { text: string; type: string }[] => {
+  if (depth > 1000) throw new Error("Too much recursion.");
+  return node.type === "text"
+    ? [
+        {
+          text: node.content.replace(/\r\n/i, "\n"),
+          type: "text",
+        },
+      ]
+    : node.children.map((childNode) => ({
+        text: getNodeTextRecursive(childNode, depth + 1),
+        type:
+          childNode.type === "text"
+            ? "text"
+            : childNode.attributes[0].key === "class"
+            ? childNode.attributes[0].value.replace(/^token /i, "")
+            : "attr",
+      }));
+};
+
 const highlightCodeAction: ReqHandler = async (request, reply) => {
   const { outputFormat = "html" } =
     request.params as AppRoutesParams[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]["params"];

...
@@ -72,34 +102,6 @@ const highlightCodeAction: ReqHandler = async (request, reply) => {
     console.log("parsedJson:", parsedJson);
 
     const tokens = parsedJson.reduce((acc, node) => {
-      const getNodeTextRecursive = (n: typeof node, depth = 0): string => {
-        if (depth > 1000) throw new Error("Too much recursion.");
-        return n.type === "text"
-          ? n.content.replace(/\r\n/i, "\n")
-          : getNodeTextRecursive(n.children[0], depth + 1);
-      };
-      const getNodesRecursive = (
-        n: typeof node,
-        depth = 0
-      ): { text: string; type: string }[] => {
-        if (depth > 1000) throw new Error("Too much recursion.");
-        return n.type === "text"
-          ? [
-              {
-                text: n.content.replace(/\r\n/i, "\n"),
-                type: "text",
-              },
-            ]
-          : n.children.map((childNode) => ({
-              text: getNodeTextRecursive(childNode),
-              type:
-                childNode.type === "text"
-                  ? "text"
-                  : childNode.attributes[0].key === "class"
-                  ? childNode.attributes[0].value.replace(/^token /i, "")
-                  : "attr",
-            }));
-      };
       if (node.type === "text") {
         acc = [
           ...acc,

app/islands/RepositoryTreeView.tsx
@@ -5,7 +5,7 @@ import React, { useCallback } from "react";
 import styled from "styled-components";
 // app
 import type { RepositoryFile, RepositoryLog } from "../types";
-import { Grid } from "../components";
+import { Grid, TextEllipsis } from "../components";
 // import RepositoryCommitSummaryLine from "./RepositoryCommitSummaryLine";
 
 export interface RepositoryTreeViewProps {

...
@@ -32,7 +32,9 @@ const RepositoryTreeView: ReactIsland<RepositoryTreeViewProps> = ({
         text: fileName,
         href:
           currentPath === "/"
-            ? `/${orgSlug}/${repoSlug}/${encodeURIComponent(currentRef)}/tree/${encodeURIComponent(fileName)}`
+            ? `/${orgSlug}/${repoSlug}/${encodeURIComponent(
+                currentRef
+              )}/tree/${encodeURIComponent(fileName)}`
             : `/${orgSlug}/${repoSlug}/${encodeURIComponent(currentRef)}/tree/${
                 currentPath.endsWith("/") || currentPath === ""
                   ? currentPath

...
@@ -66,32 +68,75 @@ const RepositoryTreeView: ReactIsland<RepositoryTreeViewProps> = ({
           gap={8}
           alignItems={"center"}
           justifyContent={"flex-end"}
-          style={{ marginTop: 8, width: "100%" }}
+          style={{
+            marginTop: 8,
+            width: "100%",
+            padding: "0 8px 8px 8px",
+            borderBottom: "1px solid gray",
+          }}
         >
-          <a href={`/${orgSlug}/${repoSlug}/${encodeURIComponent(currentRef)}/commits`}>Commits History</a>
+          <a
+            href={`/${orgSlug}/${repoSlug}/${encodeURIComponent(
+              currentRef
+            )}/commits`}
+          >
+            Commits History
+          </a>
         </Grid.Row>
-        <div>
-          <ul>
+        <Grid.Col fluid nowrap>
+          <ul style={{ listStyle: "none", padding: 0, width: "100%" }}>
             {shouldShowPrevPath && (
               <li key={"go-previous"}>
-                <a href={prevPathLink}>..</a>
+                <StyledTreeViewAnchorItem href={prevPathLink}>
+                  ..
+                </StyledTreeViewAnchorItem>
               </li>
             )}
             {repoFiles.map((file) => {
               const fileLink = buildRepoFileLink(file);
               return (
                 <li key={[file.id, file.name].join(":")}>
-                  <a href={fileLink.href}>{fileLink.text}</a>
+                  <StyledTreeViewAnchorItem href={fileLink.href}>
+                    <span style={{ flex: "0 0 240px" }}>{fileLink.text}</span>
+                    {file.lastCommit != null && (
+                      <>
+                        <span style={{ flex: 1, marginLeft: 16 }}>
+                          <TextEllipsis>{file.lastCommit.subject}</TextEllipsis>
+                        </span>
+                        <span style={{ marginLeft: 16 }}>
+                          {file.lastCommit.abbreviated_commit}
+                        </span>
+                      </>
+                    )}
+                  </StyledTreeViewAnchorItem>
                 </li>
               );
             })}
           </ul>
-        </div>
+        </Grid.Col>
       </Grid.Col>
     </StyledRepositoryTreeViewContainer>
   );
 };
 
+const StyledTreeViewAnchorItem = styled.a`
+  display: flex;
+  flex-flow: row nowrap;
+  justify-content: flex-start;
+  align-items: center;
+
+  height: 30px;
+  width: 100%;
+
+  padding: 0 8px;
+
+  border-bottom: 1px solid gray;
+
+  &:hover {
+    background-color: rgba(0, 0, 0, 0.2);
+  }
+`;
+
 const StyledRepositoryTreeViewContainer = styled.div`
   width: 100%;
 `;

app/services/repository/getRepositoryCommitLog.ts
@@ -99,7 +99,7 @@ const makeGetRepositoryCommitLog: ServiceMethodFactory<
               );
             } catch (err) {
               // console.log("escapedJson:", escapedJson);
-              reject(err);
+              resolve([]);
             }
           });
         }

app/services/repository/getRepositoryFiles.ts
@@ -1,4 +1,5 @@
 // std
+import { join } from "node:path";
 import { existsSync } from "node:fs";
 import { spawn } from "node:child_process";
 // 1st-party

...
@@ -11,6 +12,7 @@ import { Const } from "../../const";
 import { Env } from "../../env";
 // service
 import type { RepositoryServiceDeps } from "./types";
+import { default as makeGetRepositoryCommitLog } from "./getRepositoryCommitLog";
 
 const GIT_LS_TREE_REGEXP =
   /^([\d]+)[\s]+(blob|tree)[\s]+([a-z0-9]+)[\s]+(.*)$/i;

...
@@ -19,8 +21,10 @@ const makeGetRepositoryFiles: ServiceMethodFactory<
   RepositoryServiceDeps,
   [Repository, string | undefined, string | undefined],
   Promise<RepositoryFile[]>
-> = ({ request }) => {
+> = (deps) => {
+  const { request } = deps;
   return async (repo, path = "", ref = Const.DEFAULT_HEAD_REF) => {
+    const getRepositoryCommitLog = makeGetRepositoryCommitLog(deps);
     const parentOrg = await request.prisma.organization.findUnique({
       where: {
         id: repo.organizationId,

...
@@ -56,21 +60,31 @@ const makeGetRepositoryFiles: ServiceMethodFactory<
         });
       });
 
-      const repoFiles = gitLsTreeResult
-        .split("\n")
-        .map((line) => {
+      const files = gitLsTreeResult.split("\n");
+      const repoFilesP: (RepositoryFile | null)[] = await Promise.all(
+        files.map(async (line) => {
           const matches = GIT_LS_TREE_REGEXP.exec(line);
           if (matches == null || Array.isArray(matches) === false) {
             return null;
           }
           const [_, permissions, type, id, name] = matches;
+          const commitLogs = await getRepositoryCommitLog(
+            repo,
+            join(path, name),
+            ref,
+            true
+          );
           return {
             id,
             name,
             permissions,
             type,
+            lastCommit: commitLogs.length >= 1 ? commitLogs[0] : null,
           } as RepositoryFile;
         })
+      );
+
+      const repoFiles = repoFilesP
         .filter((x): x is RepositoryFile => x != null)
         .sort((a, b) => {
           if (a.type === "blob" && b.type === "tree") {

@@ -81,6 +81,7 @@ export interface RepositoryFile {
   name: string;
   permissions: string;
   type: "blob" | "tree";
+  lastCommit: RepositoryLog | null;
 }
 
 export interface RepositoryFileContent {

@@ -1,11 +1,11 @@
 declare module "himalaya";
 
-interface TextNode {
+export interface TextNode {
   type: "text";
   content: string;
 }
 
-interface RealNode {
+export interface RealNode {
   type: "element";
   tagName: keyof HTMLElementTagNameMap;
   attributes: Array<{