From dd59da662c804dcd1b824d114d598b4cb778ab87 Mon Sep 17 00:00:00 2001 From: sailong Date: Wed, 11 Jun 2025 11:57:05 +0800 Subject: [PATCH 1/3] refactor(UI):Display the data returned by the file-changed page in chunks --- .../apps/web/components/DiffView/FileDiff.tsx | 140 ++++++++++++++++++ .../web/components/DiffView/parsedDiffs.ts | 79 ++++++++++ moon/apps/web/package.json | 1 + moon/apps/web/pages/[org]/mr/[id].tsx | 29 +--- moon/apps/web/pages/_app.tsx | 1 + moon/pnpm-lock.yaml | 112 +++++++++++++- 6 files changed, 338 insertions(+), 24 deletions(-) create mode 100644 moon/apps/web/components/DiffView/FileDiff.tsx create mode 100644 moon/apps/web/components/DiffView/parsedDiffs.ts diff --git a/moon/apps/web/components/DiffView/FileDiff.tsx b/moon/apps/web/components/DiffView/FileDiff.tsx new file mode 100644 index 000000000..a92dbea41 --- /dev/null +++ b/moon/apps/web/components/DiffView/FileDiff.tsx @@ -0,0 +1,140 @@ +import { useMemo, useRef, useState } from 'react' +import { DiffFile, DiffModeEnum, DiffView } from '@git-diff-view/react' + +import { ExpandIcon, SparklesIcon } from '@gitmono/ui/Icons' +import { cn } from '@gitmono/ui/src/utils' +import { parsedDiffs } from '@/components/DiffView/parsedDiffs' + +function calculateDiffStatsFromRawDiff(diffText: string): { additions: number; deletions: number } { + const lines = diffText.split('\n'); + + let additions = 0; + + let deletions = 0; + + for (const line of lines) { + if (line.startsWith('+') && !line.startsWith('+++')) { + additions++ + } else if (line.startsWith('-') && !line.startsWith('---')) { + deletions++ + } + } + + return { additions, deletions } +} + +function generateParsedFiles(diffFiles: { path: string; lang: string; diff: string }[]): { + file: { path: string; lang: string; diff: string }; + instance: DiffFile; + stats: { additions: number; deletions: number } +}[] { + return diffFiles.map((file) => { + const instance = new DiffFile('', '', '', '', [file.diff], file.lang); + + instance.init(); + instance.buildSplitDiffLines(); + instance.buildUnifiedDiffLines(); + const stats = calculateDiffStatsFromRawDiff(file.diff); + + return { file, instance, stats } + }) +} + +export default function FileDiff({ diffs }: { diffs: string }) { + const diffFiles = useMemo(() => parsedDiffs(diffs), [diffs]); + + const parsedFiles = useMemo(() => generateParsedFiles(diffFiles), [diffFiles]); + + const [selectedPath, setSelectedPath] = useState(null); + + const [expandedMap, setExpandedMap] = useState>(() => + Object.fromEntries(diffFiles.map((f) => [f.path, false])) + ); + + const fileRefs = useRef>({}); + + const toggleExpanded = (path: string) => { + setExpandedMap((prev) => ({ ...prev, [path]: !prev[path] })) + }; + + const RenderDiffView = ({ file, instance }: { + file: { path: string; lang: string; diff: string }; + instance: DiffFile; + }) => { + if (file.lang === 'plaintext') { + return
Binary file
+ } + + return ( + + ) + } + + return ( +
+
+
    + {parsedFiles.map(({ file }) => ( +
  • { + setSelectedPath(file.path) + setExpandedMap((prev) => ({ ...prev, [file.path]: true })) + const el = fileRefs.current[file.path] + + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + }} + className={cn('px-2 py-1 text-sm cursor-pointer rounded-md mb-1', selectedPath === file.path ? 'bg-[#e6f0ff]' : 'bg-transparent')} + > + {file.path} +
  • + ))} +
+
+ +
+ {parsedFiles.map(({ file, instance, stats }) => { + const isExpanded = expandedMap[file.path] + + return ( +
void (fileRefs.current[file.path] = el)} + className='mb-4 rounded-lg border border-gray-300' + > +
toggleExpanded(file.path)} + className={cn('flex items-center justify-between px-4 py-2 text-sm', isExpanded && 'border-b border-gray-300')} + > + + {isExpanded ? ( + + ) : ( + + )} + {file.path} + + + +{stats.additions}{' '} + −{stats.deletions} + +
+ + {isExpanded && } +
+ ) + })} +
+
+ ) +} diff --git a/moon/apps/web/components/DiffView/parsedDiffs.ts b/moon/apps/web/components/DiffView/parsedDiffs.ts new file mode 100644 index 000000000..f55432519 --- /dev/null +++ b/moon/apps/web/components/DiffView/parsedDiffs.ts @@ -0,0 +1,79 @@ +const extensionToLangMap: Record = { + ".ts": "typescript", + ".tsx": "typescriptreact", + ".js": "javascript", + ".jsx": "javascriptreact", + ".json": "json", + ".md": "markdown", + ".py": "python", + ".rs": "rust", + ".cpp": "cpp", + ".c": "c", + ".h": "cpp", + ".java": "java", + ".go": "go", + ".sh": "bash", + ".yml": "yaml", + ".yaml": "yaml", + ".css": "css", + ".scss": "scss", + ".html": "html", + ".vue": "vue", + ".toml": "toml", +}; + +function getLangFromPath(path: string): string { + const ext = path.match(/\.[^./\\]+$/)?.[0]?.toLowerCase(); + + return ext ? extensionToLangMap[ext] ?? "plaintext" : "plaintext"; +} + +export function parsedDiffs(diffText: string): { path: string; lang: string; diff: string }[] { + if (!diffText) return []; + + const parts = diffText + .split(/(?=^diff --git )/gm) + .map((block) => block.trim()) + .filter(Boolean); + + return parts.map((block) => { + let path = ""; + + const plusMatch = block.match(/^\+\+\+ b\/([^\n\r]+)/m); + + if (plusMatch) { + path = plusMatch[1].trim(); + } else { + const diffGitMatch = block.match(/^diff --git a\/[^\s]+ b\/([^\s]+)/m); + + if (diffGitMatch) { + path = diffGitMatch[1].trim(); + } + } + + if (getLangFromPath(path) === "plaintext") { + return { + path, + lang: getLangFromPath(path), + diff: block, + }; + } + + const hunkIndex = block.indexOf("@@"); + + let prefix = `--- a/${path}\n+++ b/${path}\n`; + let diffWithHeader = hunkIndex >= 0 + ? block.slice(0, hunkIndex) + prefix + block.slice(hunkIndex) + : prefix + block; + + if (!diffWithHeader.endsWith("\n")) { + diffWithHeader += "\n"; + } + + return { + path, + lang: getLangFromPath(path), + diff: diffWithHeader, + }; + }); +} diff --git a/moon/apps/web/package.json b/moon/apps/web/package.json index 0d5be9d66..7c4019d28 100644 --- a/moon/apps/web/package.json +++ b/moon/apps/web/package.json @@ -21,6 +21,7 @@ "@ant-design/icons": "catalog:", "@emoji-mart/data": "catalog:", "@emotion/styled": "catalog:", + "@git-diff-view/react": "0.0.26", "@gitmono/config": "workspace:*", "@gitmono/editor": "workspace:*", "@gitmono/regex": "workspace:*", diff --git a/moon/apps/web/pages/[org]/mr/[id].tsx b/moon/apps/web/pages/[org]/mr/[id].tsx index 8d0fc1c17..0da401103 100644 --- a/moon/apps/web/pages/[org]/mr/[id].tsx +++ b/moon/apps/web/pages/[org]/mr/[id].tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { Card, Tabs, TabsProps,Timeline,} from 'antd'; // import { CommentOutlined, MergeOutlined, CloseCircleOutlined, PullRequestOutlined } from '@ant-design/icons'; import { ChevronRightCircleIcon, ChevronSelectIcon,AlarmIcon,ClockIcon} from '@gitmono/ui/Icons' @@ -8,9 +8,8 @@ import { formatDistance, fromUnixTime } from 'date-fns'; import RichEditor from '@/components/MrView/rich-editor/RichEditor'; import MRComment from '@/components/MrView/MRComment'; import { useRouter } from 'next/router'; -import * as Diff2Html from 'diff2html'; import 'diff2html/bundles/css/diff2html.min.css'; -import FilesChanged from '@/components/MrView/files-changed'; +import FileDiff from '@/components/DiffView/FileDiff'; import { Button } from '@gitmono/ui'; // import { ReloadIcon } from '@radix-ui/react-icons'; import {DownloadIcon } from '@gitmono/ui' @@ -46,7 +45,6 @@ const MRDetailPage:PageWithLayout = () =>{ const [editorState, setEditorState] = useState(""); const [editorHasText, setEditorHasText] = useState(false); const [login, _setLogin] = useState(true); - const [outputHtml, setOutputHtml] = useState(''); const id = typeof tempId === 'string' ? tempId : ''; const { data: MrDetailData } = useGetMrDetail(id) @@ -58,17 +56,6 @@ const MRDetailPage:PageWithLayout = () =>{ } const { data: MrFilesChangedData} = useGetMrFilesChanged(id) - const get_diff_content = useCallback(() => { - const content = MrFilesChangedData?.data?.content; - - if (typeof content !== 'string') return; - const diff = Diff2Html.html(content, { - drawFileList: true, - matching: 'lines', - }); - - setOutputHtml(diff); - }, [MrFilesChangedData]); const { mutate: approveMr, isPending : mrMergeIsPending } = usePostMrMerge(id) const handleMrApprove = () => { @@ -128,12 +115,6 @@ const MRDetailPage:PageWithLayout = () =>{ return element }); - const onTabsChange = (key: string) => { - if (key === '2') { - get_diff_content() - } - }; - const buttonClasses= 'cursor-pointer'; const tab_items: TabsProps['items'] = [ @@ -183,7 +164,9 @@ const MRDetailPage:PageWithLayout = () =>{ { key: '2', label: 'Files Changed', - children: + children: MrFilesChangedData?.data?.content ? + : +
No files changed
} ]; @@ -200,7 +183,7 @@ const MRDetailPage:PageWithLayout = () =>{ Merge MR } - + ) } diff --git a/moon/apps/web/pages/_app.tsx b/moon/apps/web/pages/_app.tsx index 4f2352de6..8124dacae 100644 --- a/moon/apps/web/pages/_app.tsx +++ b/moon/apps/web/pages/_app.tsx @@ -4,6 +4,7 @@ import 'styles/editor.css' import 'styles/global.css' // web only import 'styles/prose.css' import '@radix-ui/themes/styles.css' +import "@git-diff-view/react/styles/diff-view.css"; import { useEffect } from 'react' import { IS_PRODUCTION, LAST_CLIENT_JS_BUILD_ID_LS_KEY } from '@gitmono/config' diff --git a/moon/pnpm-lock.yaml b/moon/pnpm-lock.yaml index ea6ea7f8d..29c3c10c4 100644 --- a/moon/pnpm-lock.yaml +++ b/moon/pnpm-lock.yaml @@ -806,6 +806,9 @@ importers: '@emotion/styled': specifier: 'catalog:' version: 11.14.0(@emotion/react@11.14.0(@types/react@18.2.74)(react@18.2.0))(@types/react@18.2.74)(react@18.2.0) + '@git-diff-view/react': + specifier: 0.0.26 + version: 0.0.26(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@gitmono/config': specifier: workspace:* version: link:../../packages/config @@ -2884,6 +2887,18 @@ packages: '@formatjs/intl-localematcher@0.4.0': resolution: {integrity: sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==} + '@git-diff-view/core@0.0.26': + resolution: {integrity: sha512-s0sRGZMg+tmqf8dwJCBIkiPf9rUkc63fG576pzBMzS+LlN0Ks+xFeq+9GHO5/v3xuSfPmWo6C9MgpgVGJp1F2Q==} + + '@git-diff-view/lowlight@0.0.26': + resolution: {integrity: sha512-ETBOUarC0A7gOBeQVcFa/6H3d2yb+k3Jf960FZABK8WRUq0V9+hcBuGzELl14VYolun4z6naIzlLMsRzFR4DxA==} + + '@git-diff-view/react@0.0.26': + resolution: {integrity: sha512-bCLq0rXKfKwGXoZi+xx2bUf32px254YnSmcSwKYAHmYcP9uYqlN7uxSRaQwZUDvz3XahjeJ4/CI3P1jM6+L6ag==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@heroicons/react@2.2.0': resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} peerDependencies: @@ -3244,24 +3259,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@14.2.5': resolution: {integrity: sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@14.2.5': resolution: {integrity: sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@14.2.5': resolution: {integrity: sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@14.2.5': resolution: {integrity: sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==} @@ -5305,46 +5324,55 @@ packages: resolution: {integrity: sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.16.4': resolution: {integrity: sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.16.4': resolution: {integrity: sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.16.4': resolution: {integrity: sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.16.4': resolution: {integrity: sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.16.4': resolution: {integrity: sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.16.4': resolution: {integrity: sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.16.4': resolution: {integrity: sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.16.4': resolution: {integrity: sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.16.4': resolution: {integrity: sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==} @@ -6625,6 +6653,9 @@ packages: '@vue/reactivity@3.5.12': resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==} + '@vue/reactivity@3.5.16': + resolution: {integrity: sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==} + '@vue/runtime-core@3.5.12': resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==} @@ -6639,6 +6670,9 @@ packages: '@vue/shared@3.5.12': resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==} + '@vue/shared@3.5.16': + resolution: {integrity: sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -7805,6 +7839,9 @@ packages: resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} hasBin: true + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -8282,6 +8319,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -8741,6 +8781,10 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + highlight.js@11.9.0: resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} engines: {node: '>=12.0.0'} @@ -9239,6 +9283,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsan@3.1.14: + resolution: {integrity: sha512-wStfgOJqMv4QKktuH273f5fyi3D3vy2pHOiSDGPvpcS/q+wb/M7AK3vkCcaHbkZxDOlDU/lDJgccygKSG2OhtA==} + jscodeshift@0.15.2: resolution: {integrity: sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==} hasBin: true @@ -9497,6 +9544,9 @@ packages: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -11352,6 +11402,11 @@ packages: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} + reactivity-store@0.3.9: + resolution: {integrity: sha512-ADKscagTECPmc1zWkCbj6iuJgan3+DX8FcB3FwG4k00B+07kz+HWfp9hup5uyaElYnCrZsuvoClxzWV6AqP91A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -14362,6 +14417,31 @@ snapshots: dependencies: tslib: 2.6.2 + '@git-diff-view/core@0.0.26': + dependencies: + '@git-diff-view/lowlight': 0.0.26 + fast-diff: 1.3.0 + highlight.js: 11.11.1 + lowlight: 3.3.0 + + '@git-diff-view/lowlight@0.0.26': + dependencies: + '@types/hast': 3.0.4 + highlight.js: 11.11.1 + lowlight: 3.3.0 + + '@git-diff-view/react@0.0.26(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@git-diff-view/core': 0.0.26 + '@types/hast': 3.0.4 + fast-diff: 1.3.0 + highlight.js: 11.11.1 + lowlight: 3.3.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + reactivity-store: 0.3.9(react@18.2.0) + use-sync-external-store: 1.5.0(react@18.2.0) + '@heroicons/react@2.2.0(react@18.2.0)': dependencies: react: 18.2.0 @@ -19494,6 +19574,10 @@ snapshots: '@vue/shared': 3.5.12 optional: true + '@vue/reactivity@3.5.16': + dependencies: + '@vue/shared': 3.5.16 + '@vue/runtime-core@3.5.12': dependencies: '@vue/reactivity': 3.5.12 @@ -19518,6 +19602,8 @@ snapshots: '@vue/shared@3.5.12': optional: true + '@vue/shared@3.5.16': {} + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -20855,6 +20941,10 @@ snapshots: transitivePeerDependencies: - supports-color + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} diff-sequences@29.6.3: {} @@ -21556,6 +21646,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} fast-glob@3.3.1: @@ -22079,6 +22171,8 @@ snapshots: hex-rgb@4.3.0: {} + highlight.js@11.11.1: {} + highlight.js@11.9.0: optional: true @@ -22550,6 +22644,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsan@3.1.14: {} + jscodeshift@0.15.2(@babel/preset-env@7.24.4(@babel/core@7.24.6)): dependencies: '@babel/core': 7.24.6 @@ -22888,6 +22984,12 @@ snapshots: lowercase-keys@2.0.0: {} + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -25206,6 +25308,14 @@ snapshots: dependencies: loose-envify: 1.4.0 + reactivity-store@0.3.9(react@18.2.0): + dependencies: + '@vue/reactivity': 3.5.16 + '@vue/shared': 3.5.16 + jsan: 3.1.14 + react: 18.2.0 + use-sync-external-store: 1.5.0(react@18.2.0) + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -26041,7 +26151,7 @@ snapshots: dependencies: client-only: 0.0.1 react: 18.2.0 - use-sync-external-store: 1.2.2(react@18.2.0) + use-sync-external-store: 1.5.0(react@18.2.0) symbol-tree@3.2.4: {} From 873a7393e1b2da69b9d1c4c22f4bc319782749eb Mon Sep 17 00:00:00 2001 From: sailong Date: Wed, 11 Jun 2025 15:44:25 +0800 Subject: [PATCH 2/3] fix(UI):Fixed the /dev/null situation, variable refresh issues, and style changes --- .../apps/web/components/DiffView/FileDiff.tsx | 8 +++- .../web/components/DiffView/parsedDiffs.ts | 39 ++++++++++--------- moon/apps/web/pages/_app.tsx | 2 +- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/moon/apps/web/components/DiffView/FileDiff.tsx b/moon/apps/web/components/DiffView/FileDiff.tsx index a92dbea41..2f0b8ee5b 100644 --- a/moon/apps/web/components/DiffView/FileDiff.tsx +++ b/moon/apps/web/components/DiffView/FileDiff.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { DiffFile, DiffModeEnum, DiffView } from '@git-diff-view/react' import { ExpandIcon, SparklesIcon } from '@gitmono/ui/Icons' @@ -57,6 +57,10 @@ export default function FileDiff({ diffs }: { diffs: string }) { setExpandedMap((prev) => ({ ...prev, [path]: !prev[path] })) }; + useEffect(() => { + setExpandedMap(Object.fromEntries(diffFiles.map((f) => [f.path, false]))); + }, [diffFiles]); + const RenderDiffView = ({ file, instance }: { file: { path: string; lang: string; diff: string }; instance: DiffFile; @@ -77,7 +81,7 @@ export default function FileDiff({ diffs }: { diffs: string }) { } return ( -
+
diff --git a/moon/apps/web/components/DiffView/parsedDiffs.ts b/moon/apps/web/components/DiffView/parsedDiffs.ts index f55432519..53ad162d2 100644 --- a/moon/apps/web/components/DiffView/parsedDiffs.ts +++ b/moon/apps/web/components/DiffView/parsedDiffs.ts @@ -39,35 +39,38 @@ export function parsedDiffs(diffText: string): { path: string; lang: string; dif return parts.map((block) => { let path = ""; - const plusMatch = block.match(/^\+\+\+ b\/([^\n\r]+)/m); - - if (plusMatch) { - path = plusMatch[1].trim(); - } else { - const diffGitMatch = block.match(/^diff --git a\/[^\s]+ b\/([^\s]+)/m); + const diffGitMatch = block.match(/^diff --git a\/[^\s]+ b\/([^\s]+)/m); - if (diffGitMatch) { + if (diffGitMatch) { + if (diffGitMatch[2] && diffGitMatch[2] !== '/dev/null') { + path = diffGitMatch[2].trim(); + } else { path = diffGitMatch[1].trim(); } } if (getLangFromPath(path) === "plaintext") { return { - path, - lang: getLangFromPath(path), - diff: block, - }; + path, + lang: getLangFromPath(path), + diff: block, + }; } - const hunkIndex = block.indexOf("@@"); + let diffWithHeader = block; + const plusMatch = block.match(/^\+\+\+ b\/([^\n\r]+)/m); + + if(!plusMatch){ + const hunkIndex = block.indexOf("@@"); - let prefix = `--- a/${path}\n+++ b/${path}\n`; - let diffWithHeader = hunkIndex >= 0 - ? block.slice(0, hunkIndex) + prefix + block.slice(hunkIndex) - : prefix + block; + let prefix = `--- a/${path}\n+++ b/${path}\n`; + diffWithHeader = hunkIndex >= 0 + ? block.slice(0, hunkIndex) + prefix + block.slice(hunkIndex) + : prefix + block; - if (!diffWithHeader.endsWith("\n")) { - diffWithHeader += "\n"; + if (!diffWithHeader.endsWith("\n")) { + diffWithHeader += "\n"; + } } return { diff --git a/moon/apps/web/pages/_app.tsx b/moon/apps/web/pages/_app.tsx index 8124dacae..4ac09dca7 100644 --- a/moon/apps/web/pages/_app.tsx +++ b/moon/apps/web/pages/_app.tsx @@ -4,7 +4,7 @@ import 'styles/editor.css' import 'styles/global.css' // web only import 'styles/prose.css' import '@radix-ui/themes/styles.css' -import "@git-diff-view/react/styles/diff-view.css"; +import '@git-diff-view/react/styles/diff-view.css'; import { useEffect } from 'react' import { IS_PRODUCTION, LAST_CLIENT_JS_BUILD_ID_LS_KEY } from '@gitmono/config' From 3c762c5523bb30ccd90c4c40a440fca5b02409f2 Mon Sep 17 00:00:00 2001 From: sailong Date: Wed, 11 Jun 2025 15:55:42 +0800 Subject: [PATCH 3/3] chore(UI):Regular expression writing norms --- moon/apps/web/components/DiffView/parsedDiffs.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/moon/apps/web/components/DiffView/parsedDiffs.ts b/moon/apps/web/components/DiffView/parsedDiffs.ts index 53ad162d2..32d48f792 100644 --- a/moon/apps/web/components/DiffView/parsedDiffs.ts +++ b/moon/apps/web/components/DiffView/parsedDiffs.ts @@ -42,10 +42,13 @@ export function parsedDiffs(diffText: string): { path: string; lang: string; dif const diffGitMatch = block.match(/^diff --git a\/[^\s]+ b\/([^\s]+)/m); if (diffGitMatch) { - if (diffGitMatch[2] && diffGitMatch[2] !== '/dev/null') { - path = diffGitMatch[2].trim(); + const originalPath = diffGitMatch[1]?.trim(); + const newPath = diffGitMatch[2]?.trim(); + + if (newPath && newPath !== '/dev/null') { + path = newPath; } else { - path = diffGitMatch[1].trim(); + path = originalPath; } } @@ -64,6 +67,7 @@ export function parsedDiffs(diffText: string): { path: string; lang: string; dif const hunkIndex = block.indexOf("@@"); let prefix = `--- a/${path}\n+++ b/${path}\n`; + diffWithHeader = hunkIndex >= 0 ? block.slice(0, hunkIndex) + prefix + block.slice(hunkIndex) : prefix + block;