274 lines · 7.3 KB
1 /**
2 * Request router.
3 * Matches URLs to handler functions for git protocol and REST API endpoints.
4 */
5
6 import type { Env } from './index.js';
7 import { handleInfoRefs } from './handlers/git/info-refs.js';
8 import { handleReceivePack } from './handlers/git/receive-pack.js';
9 import { handleUploadPack } from './handlers/git/upload-pack.js';
10 import { handleApiRepos, handleApiRepoDetail, handleApiCreateRepo } from './handlers/api/repos.js';
11 import { handleApiRegister, handleApiLogin, handleApiCreateToken } from './handlers/api/auth.js';
12 import { handleApiTree, handleApiBlob, handleApiCommits, handleApiCommitDetail, handleApiBranches } from './handlers/api/browse.js';
13 import { handleApiCreateMR, handleApiListMRs, handleApiGetMR, handleApiAddMRComment, handleApiMergeMR } from './handlers/api/merge-requests.js';
14 import { handleApiCreateWebhook, handleApiListWebhooks, handleApiDeleteWebhook } from './handlers/api/webhooks.js';
15 import { handleApiRepoActivity, handleApiCommitDiff } from './handlers/api/activity.js';
16 import { handleApiGetUser, handleApiListTokens, handleApiRevokeToken, handleApiUpdateRepo, handleApiDeleteRepo } from './handlers/api/settings.js';
17 import { corsHeaders } from './middleware/cors.js';
18
19 export type HandlerContext = {
20 env: Env;
21 ctx: ExecutionContext;
22 params: Record<string, string>;
23 userId?: string;
24 };
25
26 type Handler = (request: Request, hctx: HandlerContext) => Promise<Response>;
27
28 interface Route {
29 method: string;
30 pattern: RegExp;
31 paramNames: string[];
32 handler: Handler;
33 }
34
35 const routes: Route[] = [
36 // Git Smart HTTP Protocol
37 {
38 method: 'GET',
39 pattern: /^\/([^/]+)\/([^/]+?)(?:\.git)?\/info\/refs$/,
40 paramNames: ['owner', 'repo'],
41 handler: handleInfoRefs,
42 },
43 {
44 method: 'POST',
45 pattern: /^\/([^/]+)\/([^/]+?)(?:\.git)?\/git-receive-pack$/,
46 paramNames: ['owner', 'repo'],
47 handler: handleReceivePack,
48 },
49 {
50 method: 'POST',
51 pattern: /^\/([^/]+)\/([^/]+?)(?:\.git)?\/git-upload-pack$/,
52 paramNames: ['owner', 'repo'],
53 handler: handleUploadPack,
54 },
55
56 // REST API - Auth
57 {
58 method: 'POST',
59 pattern: /^\/api\/register$/,
60 paramNames: [],
61 handler: handleApiRegister,
62 },
63 {
64 method: 'POST',
65 pattern: /^\/api\/login$/,
66 paramNames: [],
67 handler: handleApiLogin,
68 },
69 {
70 method: 'POST',
71 pattern: /^\/api\/tokens$/,
72 paramNames: [],
73 handler: handleApiCreateToken,
74 },
75
76 // REST API - Repos
77 {
78 method: 'GET',
79 pattern: /^\/api\/repos$/,
80 paramNames: [],
81 handler: handleApiRepos,
82 },
83 {
84 method: 'POST',
85 pattern: /^\/api\/repos$/,
86 paramNames: [],
87 handler: handleApiCreateRepo,
88 },
89 {
90 method: 'GET',
91 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)$/,
92 paramNames: ['owner', 'repo'],
93 handler: handleApiRepoDetail,
94 },
95
96 // REST API - Browsing
97 {
98 method: 'GET',
99 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/tree\/(.+)$/,
100 paramNames: ['owner', 'repo', 'refAndPath'],
101 handler: handleApiTree,
102 },
103 {
104 method: 'GET',
105 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/blob\/(.+)$/,
106 paramNames: ['owner', 'repo', 'refAndPath'],
107 handler: handleApiBlob,
108 },
109 {
110 method: 'GET',
111 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/commits\/(.+)$/,
112 paramNames: ['owner', 'repo', 'ref'],
113 handler: handleApiCommits,
114 },
115 {
116 method: 'GET',
117 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/commit\/([0-9a-f]+)$/,
118 paramNames: ['owner', 'repo', 'sha'],
119 handler: handleApiCommitDetail,
120 },
121 {
122 method: 'GET',
123 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/branches$/,
124 paramNames: ['owner', 'repo'],
125 handler: handleApiBranches,
126 },
127
128 // REST API - Merge Requests
129 {
130 method: 'POST',
131 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/merge-requests$/,
132 paramNames: ['owner', 'repo'],
133 handler: handleApiCreateMR,
134 },
135 {
136 method: 'GET',
137 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/merge-requests$/,
138 paramNames: ['owner', 'repo'],
139 handler: handleApiListMRs,
140 },
141 {
142 method: 'GET',
143 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/merge-requests\/(\d+)$/,
144 paramNames: ['owner', 'repo', 'number'],
145 handler: handleApiGetMR,
146 },
147 {
148 method: 'POST',
149 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/merge-requests\/(\d+)\/comments$/,
150 paramNames: ['owner', 'repo', 'number'],
151 handler: handleApiAddMRComment,
152 },
153 {
154 method: 'POST',
155 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/merge-requests\/(\d+)\/merge$/,
156 paramNames: ['owner', 'repo', 'number'],
157 handler: handleApiMergeMR,
158 },
159
160 // REST API - Webhooks
161 {
162 method: 'POST',
163 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/webhooks$/,
164 paramNames: ['owner', 'repo'],
165 handler: handleApiCreateWebhook,
166 },
167 {
168 method: 'GET',
169 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/webhooks$/,
170 paramNames: ['owner', 'repo'],
171 handler: handleApiListWebhooks,
172 },
173 {
174 method: 'DELETE',
175 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/webhooks\/([^/]+)$/,
176 paramNames: ['owner', 'repo', 'webhookId'],
177 handler: handleApiDeleteWebhook,
178 },
179
180 // REST API - Activity & Commit Diff
181 {
182 method: 'GET',
183 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/activity$/,
184 paramNames: ['owner', 'repo'],
185 handler: handleApiRepoActivity,
186 },
187 {
188 method: 'GET',
189 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/commit\/([0-9a-f]+)\/diff$/,
190 paramNames: ['owner', 'repo', 'sha'],
191 handler: handleApiCommitDiff,
192 },
193
194 // REST API - User Settings
195 {
196 method: 'GET',
197 pattern: /^\/api\/user$/,
198 paramNames: [],
199 handler: handleApiGetUser,
200 },
201 {
202 method: 'GET',
203 pattern: /^\/api\/user\/tokens$/,
204 paramNames: [],
205 handler: handleApiListTokens,
206 },
207 {
208 method: 'DELETE',
209 pattern: /^\/api\/user\/tokens\/([^/]+)$/,
210 paramNames: ['tokenId'],
211 handler: handleApiRevokeToken,
212 },
213
214 // REST API - Repo Settings
215 {
216 method: 'PUT',
217 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)\/settings$/,
218 paramNames: ['owner', 'repo'],
219 handler: handleApiUpdateRepo,
220 },
221 {
222 method: 'DELETE',
223 pattern: /^\/api\/repos\/([^/]+)\/([^/]+)$/,
224 paramNames: ['owner', 'repo'],
225 handler: handleApiDeleteRepo,
226 },
227 ];
228
229 export async function router(
230 request: Request,
231 env: Env,
232 ctx: ExecutionContext
233 ): Promise<Response> {
234 // Handle CORS preflight
235 if (request.method === 'OPTIONS') {
236 return new Response(null, { status: 204, headers: corsHeaders() });
237 }
238
239 const url = new URL(request.url);
240 const path = url.pathname;
241
242 for (const route of routes) {
243 if (request.method !== route.method) continue;
244
245 const match = path.match(route.pattern);
246 if (!match) continue;
247
248 const params: Record<string, string> = {};
249 for (let i = 0; i < route.paramNames.length; i++) {
250 params[route.paramNames[i]] = match[i + 1];
251 }
252
253 const hctx: HandlerContext = { env, ctx, params };
254 const response = await route.handler(request, hctx);
255
256 // Add CORS headers to all responses
257 const headers = new Headers(response.headers);
258 for (const [key, value] of Object.entries(corsHeaders())) {
259 headers.set(key, value);
260 }
261
262 return new Response(response.body, {
263 status: response.status,
264 statusText: response.statusText,
265 headers,
266 });
267 }
268
269 return new Response(JSON.stringify({ error: 'Not found' }), {
270 status: 404,
271 headers: { 'Content-Type': 'application/json', ...corsHeaders() },
272 });
273 }
274