This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Copy, FileText, Link2, RotateCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
import { PageTitle } from '../../layout/AppLayout';
|
||||
@@ -58,6 +58,13 @@ type DiffFile = {
|
||||
removed: number;
|
||||
};
|
||||
|
||||
type DiffTreeNode = {
|
||||
name: string;
|
||||
path: string;
|
||||
children: DiffTreeNode[];
|
||||
file?: DiffFile;
|
||||
};
|
||||
|
||||
function repoFullName(repo: GiteaRepo) {
|
||||
return (repo as GiteaRepo & { fullName?: string }).fullName ?? repo.full_name ?? normalizeRepository(repo.name);
|
||||
}
|
||||
@@ -222,8 +229,96 @@ function diffLineClass(line: string) {
|
||||
return 'text-slate-300';
|
||||
}
|
||||
|
||||
function buildDiffTree(files: DiffFile[]) {
|
||||
const root: DiffTreeNode = { name: '', path: '', children: [] };
|
||||
|
||||
files.forEach((file) => {
|
||||
const parts = file.path.split('/').filter(Boolean);
|
||||
if (parts.length === 0) parts.push(file.path);
|
||||
|
||||
let current = root;
|
||||
parts.forEach((part, index) => {
|
||||
const path = parts.slice(0, index + 1).join('/');
|
||||
let next = current.children.find((child) => child.name === part);
|
||||
if (!next) {
|
||||
next = { name: part, path, children: [] };
|
||||
current.children.push(next);
|
||||
}
|
||||
if (index === parts.length - 1) next.file = file;
|
||||
current = next;
|
||||
});
|
||||
});
|
||||
|
||||
const sortNodes = (nodes: DiffTreeNode[]) => {
|
||||
nodes.sort((a, b) => {
|
||||
if (Boolean(a.file) !== Boolean(b.file)) return a.file ? 1 : -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
nodes.forEach((node) => sortNodes(node.children));
|
||||
};
|
||||
|
||||
sortNodes(root.children);
|
||||
return root.children;
|
||||
}
|
||||
|
||||
function DiffTree({
|
||||
nodes,
|
||||
depth = 0,
|
||||
onSelect
|
||||
}: {
|
||||
nodes: DiffTreeNode[];
|
||||
depth?: number;
|
||||
onSelect: (file: DiffFile) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={depth === 0 ? 'space-y-0.5' : 'space-y-0.5'}>
|
||||
{nodes.map((node) => (
|
||||
<div key={node.path || node.name}>
|
||||
{node.file ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-start justify-between gap-2 rounded py-1.5 pr-2 text-left text-slate-300 transition-colors hover:bg-slate-800/80 hover:text-slate-50"
|
||||
style={{ paddingLeft: `${8 + depth * 14}px` }}
|
||||
onClick={() => onSelect(node.file as DiffFile)}
|
||||
title={node.file.path}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-[12px]">{node.name}</span>
|
||||
<span className="flex shrink-0 items-center gap-1 font-mono text-[11px]">
|
||||
<span className="text-emerald-300">+{node.file.added}</span>
|
||||
<span className="text-rose-300">-{node.file.removed}</span>
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
className="truncate py-1 pr-2 font-mono text-[12px] font-semibold text-slate-400"
|
||||
style={{ paddingLeft: `${8 + depth * 14}px` }}
|
||||
title={node.path}
|
||||
>
|
||||
{node.name}
|
||||
</div>
|
||||
)}
|
||||
{node.children.length ? <DiffTree nodes={node.children} depth={depth + 1} onSelect={onSelect} /> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffViewer({ diff }: { diff: string }) {
|
||||
const files = parseDiffFiles(diff);
|
||||
const tree = useMemo(() => buildDiffTree(files), [files]);
|
||||
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
||||
const sectionRefs = useRef<Record<string, HTMLElement | null>>({});
|
||||
|
||||
const scrollToFile = (file: DiffFile) => {
|
||||
const scroller = scrollerRef.current;
|
||||
const section = sectionRefs.current[file.id];
|
||||
if (!scroller || !section) return;
|
||||
scroller.scrollTo({
|
||||
top: section.offsetTop - scroller.offsetTop - 12,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid max-h-[56vh] overflow-hidden rounded-md border border-border bg-[#080b10] text-xs leading-5 lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||
@@ -233,27 +328,20 @@ function DiffViewer({ diff }: { diff: string }) {
|
||||
<p className="mt-0.5 text-[11px] text-slate-400">총 {files.length}개</p>
|
||||
</div>
|
||||
<div className="max-h-44 overflow-y-auto p-2 lg:max-h-[calc(56vh-50px)]">
|
||||
{files.map((file) => (
|
||||
<button
|
||||
key={`tree-${file.id}`}
|
||||
type="button"
|
||||
className="flex w-full items-start justify-between gap-2 rounded px-2 py-1.5 text-left text-slate-300 transition-colors hover:bg-slate-800/80 hover:text-slate-50"
|
||||
onClick={() => document.getElementById(`diff-file-${file.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })}
|
||||
title={file.path}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-[12px]">{file.path}</span>
|
||||
<span className="flex shrink-0 items-center gap-1 font-mono text-[11px]">
|
||||
<span className="text-emerald-300">+{file.added}</span>
|
||||
<span className="text-rose-300">-{file.removed}</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
<DiffTree nodes={tree} onSelect={scrollToFile} />
|
||||
</div>
|
||||
</aside>
|
||||
<div className="min-w-0 overflow-auto p-3 font-mono">
|
||||
<div ref={scrollerRef} className="min-w-0 overflow-auto p-3 font-mono">
|
||||
<div className="space-y-4">
|
||||
{files.map((file) => (
|
||||
<section id={`diff-file-${file.id}`} key={file.id} className="min-w-max scroll-mt-3 overflow-hidden rounded-md border border-slate-700/80 bg-[#0b0f14]">
|
||||
<section
|
||||
id={`diff-file-${file.id}`}
|
||||
key={file.id}
|
||||
ref={(element) => {
|
||||
sectionRefs.current[file.id] = element;
|
||||
}}
|
||||
className="min-w-max scroll-mt-3 overflow-hidden rounded-md border border-slate-700/80 bg-[#0b0f14]"
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-700/80 bg-[#111827] px-4 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[13px] font-semibold text-slate-100">{file.path}</p>
|
||||
|
||||
Reference in New Issue
Block a user