Implement Insights page and getContributors function

swadhinbiswas committed 10 months ago
bb60a17
1 parent f402be5
commit bb60a17
3 files changed
+278 -1
▼src/lib/git.ts
+38-0
1058
1058
});
1059
1059
}
1060
1060
1061
+
1062
+/**
1063
+ * Get contributors list with commit counts
1064
+ */
1065
+export async function getContributors(
1066
+ repoPath: string,
1067
+ ref: string = "HEAD"
1068
+): Promise<{ name: string; email: string; commits: number }[]> {
1069
+ const git = getGit(repoPath);
1070
+
1071
+ try {
1072
+ // git shortlog -sne (summary, numbered, email)
1073
+ const output = await git.raw(["shortlog", "-sne", ref]);
1074
+
1075
+ const contributors: { name: string; email: string; commits: number }[] = [];
1076
+
1077
+ output
1078
+ .trim()
1079
+ .split("\n")
1080
+ .filter(Boolean)
1081
+ .forEach((line) => {
1082
+ const match = line.match(/^\s*(\d+)\s+(.+)\s+<(.+)>$/);
1083
+ if (match) {
1084
+ contributors.push({
1085
+ commits: parseInt(match[1], 10),
1086
+ name: match[2].trim(),
1087
+ email: match[3].trim(),
1088
+ });
1089
+ }
1090
+ });
1091
+
1092
+ return contributors;
1093
+ } catch (error) {
1094
+ logger.error("Error getting contributors:", error);
1095
+ return [];
1096
+ }
1097
+}
1098
+
1061
1099
export async function installHooks(repoPath: string) {
1062
1100
const hooksDir = join(repoPath, "hooks");
1063
1101
const fs = await import("fs/promises");
1064
1102
▼src/pages/[owner]/[repo]/blob/[branch]/[...path].astro
+2-1
15
15
File as FileIcon,
16
16
Trash2,
17
17
Edit,
18
- Monitor
18
+ Monitor,
19
+ Clock
19
20
} from "lucide-react";
20
21
import { Button } from "@/components/ui/button";
21
22
import { createHighlighter } from 'shiki';
22
23
▼src/pages/[owner]/[repo]/insights/index.astro
+238-0
1
+---
2
+import BaseLayout from "@/layouts/BaseLayout.astro";
3
+import RepoHeader from "@/components/repo/RepoHeader.astro";
4
+import { getDatabase, schema } from "@/db";
5
+import { eq, and, sql, desc, gte } from "drizzle-orm";
6
+import { canReadRepo } from "@/lib/permissions";
7
+import { getContributors, getCommits } from "@/lib/git";
8
+import {
9
+ GitCommit,
10
+ GitPullRequest,
11
+ AlertCircle,
12
+ Users,
13
+ Activity
14
+} from "lucide-react";
15
+
16
+const { owner: ownerName, repo: repoName } = Astro.params;
17
+const db = getDatabase();
18
+
19
+// 1. Fetch Repo & Owner
20
+const user = await db.query.users.findFirst({
21
+ where: eq(schema.users.username, ownerName!),
22
+});
23
+
24
+if (!user) return Astro.redirect("/404");
25
+
26
+const repoData = await db.query.repositories.findFirst({
27
+ where: and(
28
+ eq(schema.repositories.ownerId, user.id),
29
+ eq(schema.repositories.name, repoName!)
30
+ ),
31
+ with: {
32
+ owner: true,
33
+ },
34
+});
35
+
36
+if (!repoData) return Astro.redirect("/404");
37
+
38
+// Check permissions
39
+const currentUser = Astro.locals.user;
40
+const hasAccess = await canReadRepo(currentUser?.id, repoData);
41
+
42
+if (!hasAccess) {
43
+ return Astro.redirect("/404");
44
+}
45
+
46
+const repo = {
47
+ owner: repoData.owner.username,
48
+ name: repoData.name,
49
+ description: repoData.description,
50
+ visibility: repoData.visibility,
51
+ defaultBranch: repoData.defaultBranch,
52
+ stars: repoData.starCount,
53
+ forks: repoData.forkCount,
54
+ watchers: repoData.watchCount,
55
+ language: repoData.language || "Unknown",
56
+ license: repoData.licenseType || "None",
57
+ topics: repoData.topics ? JSON.parse(repoData.topics) : [],
58
+ updatedAt: repoData.updatedAt,
59
+ openPrCount: repoData.openPrCount,
60
+ openIssueCount: 0 // Placeholder, fetched below
61
+};
62
+
63
+// Fetch real issue count
64
+const issueCountResult = await db
65
+ .select({ count: sql<number>`count(*)` })
66
+ .from(schema.issues)
67
+ .where(
68
+ and(
69
+ eq(schema.issues.repositoryId, repoData.id),
70
+ eq(schema.issues.state, "open")
71
+ )
72
+ )
73
+ .get();
74
+
75
+repo.openIssueCount = issueCountResult?.count || 0;
76
+
77
+// Gather Insights Data
78
+
79
+// 1. Contributors
80
+const contributors = await getContributors(repoData.diskPath);
81
+
82
+// 2. Recent Commits (Pulse - e.g., last month)
83
+// For now, simple last 50 commits
84
+const recentCommits = await getCommits(repoData.diskPath, { limit: 20 });
85
+
86
+// 3. Issue/PR Stats (Last 30 days)
87
+const thirtyDaysAgo = new Date();
88
+thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
89
+
90
+const recentIssues = await db.select({ count: sql<number>`count(*)` })
91
+ .from(schema.issues)
92
+ .where(and(
93
+ eq(schema.issues.repositoryId, repoData.id),
94
+ gte(schema.issues.createdAt, thirtyDaysAgo.toISOString())
95
+ )).get();
96
+
97
+const recentPRs = await db.select({ count: sql<number>`count(*)` })
98
+ .from(schema.pullRequests)
99
+ .where(and(
100
+ eq(schema.pullRequests.repositoryId, repoData.id),
101
+ gte(schema.pullRequests.createdAt, thirtyDaysAgo.toISOString())
102
+ )).get();
103
+
104
+const closedIssues = await db.select({ count: sql<number>`count(*)` })
105
+ .from(schema.issues)
106
+ .where(and(
107
+ eq(schema.issues.repositoryId, repoData.id),
108
+ eq(schema.issues.state, "closed"),
109
+ gte(schema.issues.updatedAt, thirtyDaysAgo.toISOString())
110
+ )).get();
111
+
112
+const mergedPRs = await db.select({ count: sql<number>`count(*)` })
113
+ .from(schema.pullRequests)
114
+ .where(and(
115
+ eq(schema.pullRequests.repositoryId, repoData.id),
116
+ eq(schema.pullRequests.state, "merged"),
117
+ gte(schema.pullRequests.updatedAt, thirtyDaysAgo.toISOString())
118
+ )).get();
119
+
120
+---
121
+
122
+<BaseLayout title={`Insights · ${repo.owner}/${repo.name}`}>
123
+ <div class="container py-6">
124
+ <RepoHeader repo={repo} activeTab="insights" />
125
+
126
+ <div class="grid gap-8 md:grid-cols-[240px_1fr]">
127
+ <!-- Sidebar -->
128
+ <aside class="space-y-1">
129
+ <a href="#" class="flex items-center gap-2 px-3 py-2 text-sm font-medium bg-muted text-primary rounded-md">
130
+ <Activity class="h-4 w-4" />
131
+ Pulse
132
+ </a>
133
+ <a href="#" class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted/50 rounded-md">
134
+ <Users class="h-4 w-4" />
135
+ Contributors
136
+ </a>
137
+ <div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase mt-4">
138
+ Graphs
139
+ </div>
140
+ <span class="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground cursor-not-allowed opacity-60">
141
+ Code Frequency
142
+ </span>
143
+ <span class="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground cursor-not-allowed opacity-60">
144
+ Commits
145
+ </span>
146
+ <span class="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground cursor-not-allowed opacity-60">
147
+ Network
148
+ </span>
149
+ </aside>
150
+
151
+ <!-- Content -->
152
+ <div class="space-y-8">
153
+ <!-- Pulse Section -->
154
+ <section>
155
+ <div class="mb-6">
156
+ <h2 class="text-2xl font-bold tracking-tight">Pulse</h2>
157
+ <p class="text-muted-foreground">
158
+ Activity overview for the last 30 days.
159
+ </p>
160
+ </div>
161
+
162
+ <div class="grid gap-4 md:grid-cols-2">
163
+ <div class="rounded-lg border bg-card text-card-foreground shadow-sm p-6">
164
+ <div class="flex items-center gap-4">
165
+ <div class="p-2 bg-blue-100 dark:bg-blue-900/20 rounded-full">
166
+ <GitPullRequest class="h-6 w-6 text-blue-600 dark:text-blue-400" />
167
+ </div>
168
+ <div>
169
+ <p class="text-sm font-medium text-muted-foreground">Active Pull Requests</p>
170
+ <div class="flex items-baseline gap-2">
171
+ <h3 class="text-2xl font-bold">{recentPRs?.count || 0}</h3>
172
+ <span class="text-sm text-muted-foreground">opened</span>
173
+ </div>
174
+ <div class="text-sm text-muted-foreground mt-1">
175
+ {mergedPRs?.count || 0} merged
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <div class="rounded-lg border bg-card text-card-foreground shadow-sm p-6">
182
+ <div class="flex items-center gap-4">
183
+ <div class="p-2 bg-purple-100 dark:bg-purple-900/20 rounded-full">
184
+ <AlertCircle class="h-6 w-6 text-purple-600 dark:text-purple-400" />
185
+ </div>
186
+ <div>
187
+ <p class="text-sm font-medium text-muted-foreground">Active Issues</p>
188
+ <div class="flex items-baseline gap-2">
189
+ <h3 class="text-2xl font-bold">{recentIssues?.count || 0}</h3>
190
+ <span class="text-sm text-muted-foreground">opened</span>
191
+ </div>
192
+ <div class="text-sm text-muted-foreground mt-1">
193
+ {closedIssues?.count || 0} closed
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </section>
200
+
201
+ <!-- Contributors Section -->
202
+ <section>
203
+ <div class="mb-6 flex items-center justify-between">
204
+ <h2 class="text-xl font-bold tracking-tight">Contributors</h2>
205
+ </div>
206
+
207
+ <div class="rounded-lg border">
208
+ {contributors.length > 0 ? (
209
+ <div class="divide-y">
210
+ {contributors.map((contributor, i) => (
211
+ <div class="flex items-center justify-between p-4 hover:bg-muted/30">
212
+ <div class="flex items-center gap-4">
213
+ <div class="h-10 w-10 rounded-full bg-muted flex items-center justify-center text-lg font-medium text-muted-foreground">
214
+ {contributor.name[0]?.toUpperCase()}
215
+ </div>
216
+ <div>
217
+ <div class="font-medium">{contributor.name}</div>
218
+ <div class="text-sm text-muted-foreground">{contributor.email}</div>
219
+ </div>
220
+ </div>
221
+ <div class="text-right">
222
+ <div class="text-sm font-bold">{contributor.commits}</div>
223
+ <div class="text-xs text-muted-foreground">commits</div>
224
+ </div>
225
+ </div>
226
+ ))}
227
+ </div>
228
+ ) : (
229
+ <div class="p-8 text-center text-muted-foreground">
230
+ No contributor data available.
231
+ </div>
232
+ )}
233
+ </div>
234
+ </section>
235
+ </div>
236
+ </div>
237
+ </div>
238
+</BaseLayout>
0
239