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 |