syntaxai/tdd.md · commit 497cea9

Redirect bare /<owner>/<repo>.git URLs to the clean scoreboard

Pasting the clone URL into a browser previously fell through the
isGitProtocol check (path ends with .git) and got proxied straight
to Forgejo, which happily rendered its own repo page — Forgejo's
chrome was bleeding onto tdd.md.

Real git operations always have sub-paths (/info/refs?service=...,
/git-upload-pack, /objects/...). The plain /<owner>/<repo>.git URL
is only ever visited by humans. So: 302 it to /<owner>/<repo>
before the protocol check kicks in. Clones, fetches and pushes
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-07 12:27:27 +01:00
parent
8fbd29d
commit
497cea9585e2078632f372ad0499ecb7f693b04d

1 file changed · +17 −0

modified src/server.ts +17 −0
@@ -1087,6 +1087,23 @@ When you push, the judge replays your commits and posts the verdict at [/agents/
10871087 async fetch(req) {
10881088 const url = new URL(req.url);
10891089
1090+ // Bare /<owner>/<repo>.git (no sub-path) is what someone gets when
1091+ // they paste the clone URL into a browser. Without intervention our
1092+ // proxy hands it to Forgejo, which renders its own repo page —
1093+ // Forgejo's chrome leaks onto tdd.md. Redirect to the clean URL
1094+ // so the visitor lands on our Bun-native scoreboard instead. Real
1095+ // git operations always have sub-paths (/info/refs, /git-upload-pack,
1096+ // /objects/...) and continue to be proxied below.
1097+ const bareGitUrl = url.pathname.match(
1098+ /^\/([A-Za-z0-9][A-Za-z0-9-]*)\/([A-Za-z0-9][A-Za-z0-9._-]*)\.git\/?$/,
1099+ );
1100+ if (bareGitUrl) {
1101+ return new Response(null, {
1102+ status: 302,
1103+ headers: { Location: `/${bareGitUrl[1]}/${bareGitUrl[2]}` },
1104+ });
1105+ }
1106+
10901107 // Git smart-HTTP and dumb-HTTP — proxy raw to Forgejo.
10911108 if (isGitProtocol(url.pathname, url.searchParams)) {
10921109 return proxyToForgejo(req, url.pathname + url.search);