✨ feat: update [...path].astro page

swadhinbiswas committed 3 months ago
e27c69f
1 parent 8674de0
commit e27c69f
1 file changed
+54 -25
▼src/pages/[owner]/[repo]/blame/[branch]/[...path].astro
+54-25
7
7
import { getBlame, getLastCommit } from "@/lib/git";
8
8
import { resolveRepoPath } from "@/lib/git-storage";
9
9
import { GitBranch, ChevronDown, FileCode } from "lucide-react";
10
-import { createHighlighter } from "shiki";
11
10
12
11
const { owner: ownerName, repo: repoName, branch, path } = Astro.params;
13
12
const db = getDatabase();
14
13
15
-// Fetch Owner
16
14
const user = await db.query.users.findFirst({
17
15
where: eq(schema.users.username, ownerName!),
18
16
});
27
25
});
28
26
if (!repoData) return Astro.redirect("/404");
29
27
30
-// Check permissions
31
28
const currentUser = Astro.locals.user;
32
29
const hasAccess = await canReadRepo(currentUser?.id, repoData);
33
30
if (!hasAccess) return Astro.redirect("/404");
34
31
35
32
const currentBranch = branch || repoData.defaultBranch;
36
33
const currentPath = path || "";
37
-
38
34
const resolvedLocalPath = await resolveRepoPath(repoData.diskPath);
39
35
40
-// Fetch blame data
41
36
const blameLines = await getBlame(resolvedLocalPath, currentPath, currentBranch);
42
-
43
37
if (!blameLines || blameLines.length === 0) {
44
38
return Astro.redirect("/404");
45
39
}
46
40
47
-// Syntax highlighting
48
41
const extension = currentPath.split(".").pop()?.toLowerCase() || "";
49
42
const commonLangs: Record<string, string> = {
50
43
ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
56
49
};
57
50
const lang = commonLangs[extension] || "text";
58
51
59
-// Highlight each line individually for blame row alignment
60
52
let highlightedLines: string[] = [];
61
53
try {
54
+ const { createHighlighter } = await import("shiki");
62
55
const highlighter = await createHighlighter({
63
56
themes: ["github-dark"],
64
57
langs: [lang, "text"],
68
61
lang: Object.values(commonLangs).includes(lang) ? lang : "text",
69
62
theme: "github-dark",
70
63
});
71
- // Extract individual lines from the shiki output
72
64
const lineMatches = html.match(/<span class="line">.*?<\/span>/gs);
73
65
highlightedLines = lineMatches || blameLines.map((b) => `<span class="line">${escapeHtml(b.content)}</span>`);
74
66
} catch {
76
68
}
77
69
78
70
function escapeHtml(text: string): string {
79
- return text
80
- .replace(/&/g, "&amp;")
81
- .replace(/</g, "&lt;")
82
- .replace(/>/g, "&gt;")
83
- .replace(/"/g, "&quot;");
71
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
84
72
}
85
73
86
-// Group consecutive lines by same commit SHA to show commit info only once per block
87
74
interface BlameBlock {
88
75
sha: string;
89
76
author: string;
90
77
email: string;
91
78
date: Date;
79
+ message: string;
92
80
lines: { lineNum: number; content: string; highlighted: string }[];
93
81
}
94
82
95
83
const blameBlocks: BlameBlock[] = [];
96
84
let currentBlock: BlameBlock | null = null;
97
85
86
+// Fetch commit messages for unique SHAs
87
+const uniqueShas = [...new Set(blameLines.map(b => b.sha))];
88
+const commitMessages = new Map<string, string>();
89
+for (const sha of uniqueShas.slice(0, 50)) {
90
+ try {
91
+ const commit = await getLastCommit(resolvedLocalPath, sha, "");
92
+ if (commit) commitMessages.set(sha, commit.message.split('\n')[0]);
93
+ } catch {}
94
+}
95
+
98
96
for (let i = 0; i < blameLines.length; i++) {
99
97
const bl = blameLines[i];
100
98
if (!currentBlock || currentBlock.sha !== bl.sha) {
103
101
author: bl.author,
104
102
email: bl.email,
105
103
date: bl.date,
104
+ message: commitMessages.get(bl.sha) || "",
106
105
lines: [],
107
106
};
108
107
blameBlocks.push(currentBlock);
129
128
return `${Math.floor(months / 12)}y ago`;
130
129
}
131
130
131
+function formatDate(date: Date): string {
132
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
133
+}
134
+
132
135
const repo = {
133
136
owner: repoData.owner.username,
134
137
name: repoData.name,
149
152
<RepoHeader repo={repo} activeTab="code" />
150
153
151
154
<div class="space-y-4">
152
- <!-- Breadcrumbs -->
155
+ <!-- Breadcrumbs & Actions -->
153
156
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
154
157
<div class="flex items-center gap-2">
155
158
<div class="relative">
184
187
</div>
185
188
</div>
186
189
187
- <!-- Header -->
190
+ <!-- Blame Header -->
188
191
<div class="rounded-t-lg border bg-muted/30 p-3 border-b-0 flex items-center gap-2 text-sm text-muted-foreground">
189
192
<FileCode className="h-4 w-4" />
190
193
<span class="font-medium text-foreground">{blameLines.length} lines</span>
199
202
{blameBlocks.map((block, blockIdx) => (
200
203
block.lines.map((line, lineIdx) => (
201
204
<tr class:list={[
202
- "hover:bg-accent transition-colors",
203
- lineIdx === 0 && blockIdx > 0 ? "border-t border-gray-700" : "",
205
+ "group/line hover:bg-accent/50 transition-colors relative",
206
+ lineIdx === 0 && blockIdx > 0 ? "border-t border-border" : "",
204
207
]}>
205
208
{/* Blame info column — show only on first line of block */}
206
209
<td class:list={[
207
- "align-top whitespace-nowrap px-3 py-0 text-xs text-muted-foreground select-none border-r border-gray-800 min-w-[220px] max-w-[280px]",
210
+ "align-top whitespace-nowrap px-3 py-0 text-xs select-none border-r border-border min-w-[220px] max-w-[280px] relative",
208
211
lineIdx === 0 ? "pt-1" : "",
209
212
]}>
210
- {lineIdx === 0 && (
213
+ {lineIdx === 0 ? (
211
214
<div class="flex items-center gap-2 leading-6">
212
215
<a
213
216
href={`/${repo.owner}/${repo.name}/commit/${block.sha}`}
214
217
class="text-blue-400 hover:text-blue-300 hover:underline font-mono"
215
218
title={block.sha}
216
219
>
217
- {block.sha.substring(0, 8)}
220
+ {block.sha.substring(0, 7)}
218
221
</a>
219
- <span class="truncate max-w-[100px] text-muted-foreground" title={block.author}>
222
+ <span class="truncate max-w-[80px] text-muted-foreground" title={block.author}>
220
223
{block.author}
221
224
</span>
222
- <span class="text-muted-foreground shrink-0">
225
+ <span class="text-muted-foreground shrink-0 text-[10px]">
223
226
{timeAgo(block.date)}
224
227
</span>
228
+ {/* Hover Popup */}
229
+ <div class="hidden group-hover/line:block absolute left-0 top-full z-50 mt-1 w-80 rounded-lg border border-border bg-card shadow-xl p-3">
230
+ <div class="flex items-start gap-3">
231
+ <div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary text-xs font-bold flex-shrink-0">
232
+ {block.author[0]?.toUpperCase() || "?"}
233
+ </div>
234
+ <div class="min-w-0">
235
+ <p class="text-sm font-semibold text-foreground truncate">{block.author}</p>
236
+ <p class="text-xs text-muted-foreground">{formatDate(block.date)}</p>
237
+ </div>
238
+ </div>
239
+ {block.message && (
240
+ <p class="mt-2 text-xs text-foreground font-mono bg-muted/50 rounded p-2 line-clamp-2">
241
+ {block.message}
242
+ </p>
243
+ )}
244
+ <div class="mt-2 flex items-center gap-2 text-[10px] text-muted-foreground">
245
+ <a href={`/${repo.owner}/${repo.name}/commit/${block.sha}`} class="text-blue-400 hover:underline font-mono">
246
+ {block.sha.substring(0, 7)}
247
+ </a>
248
+ <span>·</span>
249
+ <span>{block.lines.length} line(s)</span>
250
+ </div>
251
+ </div>
225
252
</div>
253
+ ) : (
254
+ <div class="leading-6 text-muted-foreground/30">·</div>
226
255
)}
227
256
</td>
228
257
{/* Line number */}
229
- <td class="text-right px-3 py-0 text-xs text-muted-foreground select-none border-r border-gray-800 w-[1%]">
258
+ <td class="text-right px-3 py-0 text-xs text-muted-foreground select-none border-r border-border w-[1%]">
230
259
<div class="leading-6">
231
260
<a
232
261
id={`L${line.lineNum}`}
233
262