✨ feat: update [sha].astro page

swadhinbiswas committed 3 months ago
173e539
1 parent e27c69f
commit 173e539
1 file changed
+209 -84
Showing 1 changed file+209-84
▼src/pages/[owner]/[repo]/commit/[sha].astro
+209-84
3
3
import { getDatabase, schema } from "@/db";
4
4
import { eq, and } from "drizzle-orm";
5
5
import { resolveRepoPath } from "@/lib/git-storage";
6
-import { getCommitDiff } from "@/lib/git";
6
+import { getCommit, getCommitDiff, getCommitPatchDiff } from "@/lib/git";
7
7
import RepoHeader from "@/components/repo/RepoHeader.astro";
8
8
import { canReadRepo } from "@/lib/permissions";
9
+import DiffView from "@/components/diff/DiffView";
9
10
10
-const { owner: ownerName, repo: repoName,sha } = Astro.params;
11
+const { owner: ownerName, repo: repoName, sha } = Astro.params;
11
12
const db = getDatabase();
12
13
13
-// Fetch Repo & Owner
14
14
const user = await db.query.users.findFirst({
15
15
where: eq(schema.users.username, ownerName!),
16
16
});
17
-
18
17
if (!user) return Astro.redirect("/404");
19
18
20
19
const repoData = await db.query.repositories.findFirst({
22
21
eq(schema.repositories.ownerId, user.id),
23
22
eq(schema.repositories.name, repoName!),
24
23
),
25
- with: {
26
- owner: true,
27
- },
24
+ with: { owner: true },
28
25
});
29
-
30
26
if (!repoData) return Astro.redirect("/404");
31
27
32
-// Check permissions
33
28
const currentUser = Astro.locals.user;
34
29
const hasAccess = await canReadRepo(currentUser?.id, repoData);
35
-
36
-if (!hasAccess) {
37
- return Astro.redirect("/404");
38
-}
30
+if (!hasAccess) return Astro.redirect("/404");
39
31
40
32
const repo = {
41
33
owner: repoData.owner.username,
57
49
website: repoData.website,
58
50
};
59
51
60
-// Get commit diff
61
52
const resolvedLocalPath = await resolveRepoPath(repoData.diskPath);
62
-let commit: any = null;
53
+let commitInfo: any = null;
54
+let fileStats: any[] = [];
55
+let patchDiff = "";
63
56
let error: string | null = null;
64
57
65
58
try {
66
- commit = await getCommitDiff(resolvedLocalPath, sha!);
59
+ commitInfo = await getCommit(resolvedLocalPath, sha!);
60
+ fileStats = await getCommitDiff(resolvedLocalPath, sha!);
61
+ patchDiff = await getCommitPatchDiff(resolvedLocalPath, sha!);
67
62
} catch (e: any) {
68
- console.error("Error fetching commit:", e);
69
63
error = e.message || "Failed to fetch commit";
70
64
}
71
65
72
-if (!commit && !error) {
73
- return Astro.redirect("/404");
66
+if (!commitInfo && !error) return Astro.redirect("/404");
67
+
68
+let authorUser: any = null;
69
+if (commitInfo?.authorEmail) {
70
+ try {
71
+ authorUser = await db.query.users.findFirst({
72
+ where: eq(schema.users.email, commitInfo.authorEmail),
73
+ });
74
+ } catch {}
74
75
}
75
76
77
+const totalAdditions = fileStats.reduce((sum: number, f: any) => sum + (f.additions || 0), 0);
78
+const totalDeletions = fileStats.reduce((sum: number, f: any) => sum + (f.deletions || 0), 0);
79
+
80
+// Build file tree structure for sidebar
81
+interface FileNode {
82
+ name: string;
83
+ path: string;
84
+ type: "file" | "dir";
85
+ status: string;
86
+ additions: number;
87
+ deletions: number;
88
+ children?: FileNode[];
89
+}
90
+
91
+const fileTree: FileNode[] = [];
92
+for (const file of fileStats) {
93
+ const parts = file.file.split("/");
94
+ let current = fileTree;
95
+ for (let i = 0; i < parts.length - 1; i++) {
96
+ let dir = current.find(n => n.name === parts[i] && n.type === "dir");
97
+ if (!dir) {
98
+ dir = { name: parts[i], path: parts.slice(0, i + 1).join("/"), type: "dir", status: "modified", additions: 0, deletions: 0, children: [] };
99
+ current.push(dir);
100
+ }
101
+ current = dir.children!;
102
+ }
103
+ current.push({
104
+ name: parts[parts.length - 1],
105
+ path: file.file,
106
+ type: "file",
107
+ status: file.status,
108
+ additions: file.additions,
109
+ deletions: file.deletions,
110
+ });
111
+}
112
+
113
+function formatDate(date: Date): string {
114
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
115
+}
116
+
117
+function renderFileTree(nodes: FileNode[], depth: number = 0): string {
118
+ let html = "";
119
+ const sorted = [...nodes].sort((a, b) => {
120
+ if (a.type === "dir" && b.type !== "dir") return -1;
121
+ if (a.type !== "dir" && b.type === "dir") return 1;
122
+ return a.name.localeCompare(b.name);
123
+ });
124
+ for (const node of sorted) {
125
+ const indent = depth * 16;
126
+ if (node.type === "dir") {
127
+ html += `<div class="flex items-center gap-1.5 py-0.5 px-2 text-xs text-muted-foreground hover:bg-accent/50 cursor-pointer" style="padding-left:${indent}px">
128
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
129
+ <span>${node.name}</span>
130
+ </div>`;
131
+ if (node.children) html += renderFileTree(node.children, depth + 1);
132
+ } else {
133
+ const statusColor = node.status === "added" ? "text-green-500" : node.status === "deleted" ? "text-red-500" : "text-muted-foreground";
134
+ html += `<a href="#file-${node.path.replace(/[^a-zA-Z0-9]/g, '-')}" class="flex items-center gap-1.5 py-0.5 px-2 text-xs hover:bg-accent/50 group" style="padding-left:${indent}px">
135
+ <span class="w-2 h-2 rounded-full flex-shrink-0 ${node.status === 'added' ? 'bg-green-500' : node.status === 'deleted' ? 'bg-red-500' : node.status === 'renamed' ? 'bg-yellow-500' : 'bg-muted-foreground/30'}"></span>
136
+ <span class="text-foreground truncate group-hover:text-primary transition-colors">${node.name}</span>
137
+ <span class="ml-auto text-[10px] ${statusColor} flex-shrink-0">${node.additions > 0 ? `+${node.additions}` : ""} ${node.deletions > 0 ? `-${node.deletions}` : ""}</span>
138
+ </a>`;
139
+ }
140
+ }
141
+ return html;
142
+}
143
+
144
+const fileTreeHtml = renderFileTree(fileTree);
145
+
76
146
function timeAgo(date: Date | string | number) {
77
147
if (!date) return "";
78
- const seconds = Math.floor(
79
- (new Date().getTime() - new Date(date).getTime()) / 1000,
80
- );
81
- let interval = seconds / 31536000;
82
- if (interval > 1) return Math.floor(interval) + " years ago";
83
- interval = seconds / 2592000;
84
- if (interval > 1) return Math.floor(interval) + " months ago";
85
- interval = seconds / 86400;
86
- if (interval > 1) return Math.floor(interval) + " days ago";
87
- interval = seconds / 3600;
88
- if (interval > 1) return Math.floor(interval) + " hours ago";
89
- interval = seconds / 60;
90
- if (interval > 1) return Math.floor(interval) + " minutes ago";
91
- return Math.floor(seconds) + " seconds ago";
148
+ const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000);
149
+ if (seconds < 60) return "just now";
150
+ const minutes = Math.floor(seconds / 60);
151
+ if (minutes < 60) return `${minutes} minutes ago`;
152
+ const hours = Math.floor(minutes / 60);
153
+ if (hours < 24) return `${hours} hours ago`;
154
+ const days = Math.floor(hours / 24);
155
+ if (days < 30) return `${days} days ago`;
156
+ const months = Math.floor(days / 30);
157
+ if (months < 12) return `${months} months ago`;
158
+ return `${Math.floor(months / 12)} years ago`;
92
159
}
93
160
---
94
161
100
167
<div class="p-6 text-center">
101
168
<p class="text-destructive">{error}</p>
102
169
</div>
103
- ) : commit && (
104
- <div class="space-y-6">
170
+ ) : commitInfo && (
171
+ <div class="space-y-4">
105
172
<!-- Commit Header -->
106
- <div class="rounded-lg border bg-card p-6">
107
- <div class="mb-4">
108
- <h1 class="text-2xl font-bold mb-2">{commit.message}</h1>
109
- {commit.body && (
110
- <pre class="text-sm text-muted-foreground whitespace-pre-wrap font-sans">{commit.body}</pre>
111
- )}
112
- </div>
113
-
114
- <div class="flex items-center gap-4 text-sm text-muted-foreground border-t pt-4">
115
- <div class="flex items-center gap-2">
116
- <div class="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
117
- <span class="text-sm font-medium">
118
- {commit.authorName?.[0]?.toUpperCase() || "?"}
119
- </span>
120
- </div>
121
- <div>
122
- <div class="font-medium text-foreground">{commit.authorName}</div>
123
- <div>{commit.authorEmail}</div>
173
+ <div class="border border-border bg-card rounded-lg p-4">
174
+ <div class="flex items-start justify-between gap-4">
175
+ <div class="flex-1 min-w-0">
176
+ <h1 class="text-xl font-semibold text-foreground mb-2">{commitInfo.message}</h1>
177
+ {commitInfo.body && (
178
+ <pre class="text-sm text-muted-foreground whitespace-pre-wrap font-sans mb-3">{commitInfo.body}</pre>
179
+ )}
180
+ <div class="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
181
+ <div class="flex items-center gap-2">
182
+ {authorUser ? (
183
+ <a href={`/${authorUser.username}`} class="flex items-center gap-1.5 hover:opacity-80">
184
+ {authorUser.avatarUrl ? (
185
+ <img src={authorUser.avatarUrl} alt="" class="h-5 w-5 rounded-full" />
186
+ ) : (
187
+ <div class="h-5 w-5 rounded-full bg-primary/20 flex items-center justify-center text-[10px] text-primary font-bold">{commitInfo.authorName?.[0]?.toUpperCase()}</div>
188
+ )}
189
+ <span class="font-medium text-foreground">{commitInfo.authorName}</span>
190
+ </a>
191
+ ) : (
192
+ <span class="font-medium text-foreground">{commitInfo.authorName}</span>
193
+ )}
194
+ <span>committed {timeAgo(commitInfo.authorDate)}</span>
195
+ </div>
124
196
</div>
125
197
</div>
126
- <div class="flex-1"></div>
127
- <div>
128
- committed {timeAgo(commit.authorDate)}
198
+ <div class="flex items-center gap-2 flex-shrink-0">
199
+ <div class="font-mono text-xs bg-muted px-2 py-1 rounded border border-border">{(commitInfo.sha || sha)?.substring(0, 7)}</div>
200
+ <button class="p-1.5 rounded hover:bg-accent text-muted-foreground" title="Copy SHA" onclick="navigator.clipboard.writeText('{commitInfo.sha || sha}')">
201
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
202
+ </button>
129
203
</div>
130
204
</div>
131
205
132
- <div class="mt-4 flex items-center gap-4 text-sm">
133
- <div class="font-mono bg-muted px-3 py-1 rounded">
134
- {(commit.sha || sha)?.substring(0, 7)}
206
+ <!-- Parent commit & branch info -->
207
+ <div class="flex items-center gap-4 mt-3 pt-3 border-t border-border text-xs text-muted-foreground">
208
+ {commitInfo.parentShas && commitInfo.parentShas.length > 0 && (
209
+ <div class="flex items-center gap-1.5">
210
+ <span>{commitInfo.parentShas.length} parent{commitInfo.parentShas.length > 1 ? 's' : ''}</span>
211
+ {commitInfo.parentShas.map((parentSha: string) => (
212
+ <a href={`/${repo.owner}/${repo.name}/commit/${parentSha}`} class="font-mono text-blue-400 hover:underline">{parentSha.substring(0, 7)}</a>
213
+ ))}
214
+ </div>
215
+ )}
216
+ <div class="flex items-center gap-1.5">
217
+ <span>commit</span>
218
+ <span class="font-mono text-foreground">{(commitInfo.sha || sha)?.substring(0, 7)}</span>
135
219
</div>
136
- <div class="text-muted-foreground">
137
- {commit.stats?.additions || 0} additions, {commit.stats?.deletions || 0} deletions
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Files Changed Stats Bar -->
224
+ <div class="border border-border bg-card rounded-lg px-4 py-2 flex items-center justify-between">
225
+ <div class="flex items-center gap-3">
226
+ <span class="text-sm font-semibold text-foreground">{fileStats.length} file{fileStats.length !== 1 ? 's' : ''} changed</span>
227
+ </div>
228
+ <div class="flex items-center gap-3 text-xs">
229
+ <span class="text-green-500 font-medium">+{totalAdditions}</span>
230
+ <span class="text-red-500 font-medium">-{totalDeletions}</span>
231
+ <div class="flex gap-0.5">
232
+ {fileStats.slice(0, 20).map((f: any) => (
233
+ <div class:list={[
234
+ "w-2 h-2 rounded-sm",
235
+ f.status === "added" ? "bg-green-500" :
236
+ f.status === "deleted" ? "bg-red-500" :
237
+ "bg-muted-foreground/30"
238
+ ]} />
239
+ ))}
138
240
</div>
139
241
</div>
140
242
</div>
141
243
142
- <!-- Changed Files -->
143
- <div class="space-y-4">
144
- <h2 class="text-lg font-semibold">Changed files</h2>
145
-
146
- {commit.files && commit.files.length > 0 ? (
147
- commit.files.map((file: any) => (
148
- <div class="rounded-lg border overflow-hidden">
149
- <div class="flex items-center justify-between bg-muted/30 px-4 py-2 border-b">
150
- <span class="font-mono text-sm">{file.path}</span>
151
- <span class="text-sm text-muted-foreground">
152
- +{file.additions || 0} -{file.deletions || 0}
153
- </span>
154
- </div>
155
- {file.diff && (
156
- <div class="overflow-x-auto">
157
- <pre class="text-xs p-4 bg-background font-mono"><code>{file.diff}</code></pre>
244
+ <!-- Main Content: File Tree + Diff -->
245
+ <div class="flex gap-4">
246
+ <!-- File Tree Sidebar -->
247
+ <div class="hidden lg:block w-64 flex-shrink-0">
248
+ <div class="border border-border bg-card rounded-lg p-2 sticky top-20 max-h-[calc(100vh-100px)] overflow-y-auto custom-scrollbar">
249
+ <div class="px-2 py-1 text-xs font-semibold text-muted-foreground border-b border-border mb-1">Files</div>
250
+ <div set:html={fileTreeHtml} />
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Diff Content -->
255
+ <div class="flex-1 min-w-0">
256
+ {patchDiff ? (
257
+ <div class="border border-border bg-card rounded-lg overflow-hidden">
258
+ <DiffView client:load rawDiff={patchDiff} />
259
+ </div>
260
+ ) : fileStats.length > 0 ? (
261
+ <div class="border border-border bg-card rounded-lg">
262
+ {fileStats.map((file: any) => (
263
+ <div id={`file-${file.file.replace(/[^a-zA-Z0-9]/g, '-')}`} class="border-b border-border last:border-0">
264
+ <div class="flex items-center justify-between px-4 py-2 bg-muted/30 border-b border-border">
265
+ <div class="flex items-center gap-2">
266
+ <span class:list={[
267
+ "inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold",
268
+ file.status === "added" ? "bg-green-500/10 text-green-500" :
269
+ file.status === "deleted" ? "bg-red-500/10 text-red-500" :
270
+ file.status === "renamed" ? "bg-yellow-500/10 text-yellow-500" :
271
+ "bg-muted text-muted-foreground"
272
+ ]}>
273
+ {file.status === "added" ? "A" : file.status === "deleted" ? "D" : file.status === "renamed" ? "R" : "M"}
274
+ </span>
275
+ <span class="font-mono text-sm text-foreground">{file.file}</span>
276
+ {file.oldPath && <span class="text-xs text-muted-foreground">← {file.oldPath}</span>}
277
+ </div>
278
+ <div class="flex items-center gap-2 text-xs">
279
+ {file.additions > 0 && <span class="text-green-500">+{file.additions}</span>}
280
+ {file.deletions > 0 && <span class="text-red-500">-{file.deletions}</span>}
281
+ </div>
282
+ </div>
158
283
</div>
159
- )}
284
+ ))}
160
285
</div>
161
- ))
162
- ) : (
163
- <div class="text-center text-muted-foreground py-8">
164
- No files changed
165
- </div>
166
- )}
286
+ ) : (
287
+ <div class="text-center text-muted-foreground py-8 border border-border bg-card rounded-lg">
288
+ No file changes in this commit.
289
+ </div>
290
+ )}
291
+ </div>
167
292
</div>
168
293
</div>
169
294
)}
170
295