feat(repository): display latest commit + abbreviated hash for each files@@ -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"
@@ -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,
@@ -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,
@@ -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%;
`;
@@ -99,7 +99,7 @@ const makeGetRepositoryCommitLog: ServiceMethodFactory<
);
} catch (err) {
// console.log("escapedJson:", escapedJson);
- reject(err);
+ resolve([]);
}
});
}
@@ -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<{