gitoria
All repositories: gitoria
5.8 KB
// lib/transport.hl — CLONE, FETCH AND PUSH OVER HTTPS (ticket gitoria#7; docs/git-backend.md). Git's "smart HTTP" protocol,// answered by this app itself with the `git` binary: on a repo address `<slug>.<domain>` the paths// /<slug>.git/info/refs?service=git-upload-pack | git-receive-pack (what a client asks first)// /<slug>.git/git-upload-pack (fetch / clone) /<slug>.git/git-receive-pack (push)// spawn `git upload-pack | receive-pack --stateless-rpc` and hand the request body over and the answer back.// The clone URL is https://<slug>.<domain>/<slug>.git — the folder git makes is named like the repo.// * READ (clone, fetch) needs no login: every repo is public (the concept has no private repos yet).// * WRITE (push) needs a token as the password (tokens.hl) of THE REPO'S OWNER — nobody else may push (decision below).// * The body travels through temp files, byte for byte (a String here holds raw bytes; hl:fs writes and reads them as// they are): hl:proc run() has no stdin, so the child reads `< body` and writes `> answer` in a tiny `sh -c` with// the paths as positional arguments (no user text in the script). A gzip body (git compresses big requests) is// unpacked first. Git protocol v2 is passed through (`Git-Protocol` → GIT_PROTOCOL, digits only).// * No hook: pull requests and releases are read from the commits when their page is built.// The checks, the plain answers and the pkt-line are in transport-helpers.hl.import { Response } from 'hl:http1'import { run } from 'hl:proc'import { readFile, writeFile, remove, exists } from 'hl:fs'import { randomBytes } from 'hl:crypto'import { NL, slugOfHost, publicUrl } from './util.hl'import { repoBySlug, mayPush } from './repos.hl'import { slugError } from './repos-helpers.hl'import { userOfToken } from './tokens.hl'import { repoDir, tmpDir } from './git.hl'import { notifyPush } from './tickets.hl'import { passwordOf, plain, protocolEnv, pktLine } from './transport-helpers.hl'static seconds = 300 // one upload-pack / receive-pack callstatic maxBody = 500000000 // a push or fetch request above this is refused (bytes)// ONE GIT CALL: the request body (or none) in, the answer out. → the answer's bytes as a String, or null (git failed and said nothing)static callGit = (slug, service, advertise, req) => {dir = tmpDir()name = dir + '/' + randomBytes(12, 'hex')inFile = name + '.in'outFile = name + '.out'let zipped = falselet script = 'exec git ' + service.slice(4) + ' --stateless-rpc --advertise-refs "$1" > "$3"'if (!advertise) {body = req.body == null ? '' : req.bodywriteFile(inFile, body, 384)enc = req.headers['content-encoding']zipped = enc != null && hlTypeName(enc) == 'String' && (enc.toLowerCase() == 'gzip' || enc.toLowerCase() == 'x-gzip')script = zipped ? 'gzip -dc < "$2" | git ' + service.slice(4) + ' --stateless-rpc "$1" > "$3"' : 'exec git ' + service.slice(4) + ' --stateless-rpc "$1" < "$2" > "$3"'}r = run(['sh', '-c', script, 'sh', repoDir(slug), inFile, outFile], { timeout = seconds env = protocolEnv(req) })out = exists(outFile) ? readFile(outFile) : nullif (exists(inFile)) { remove(inFile) }if (exists(outFile)) { remove(outFile) }// git answers nothing to the client's "0000" probe of a big push (exit 0): that is a 200 with an empty body, as git http-backend doesif (out == null || (out == '' && (r == null || r.exit != 0))) { return null }return out}// THE ROUTE (project.hl, a function route): `/:repo/info/refs`, `/:repo/git-upload-pack`, `/:repo/git-receive-pack`static gitTransport = (route, req) => {hostHeader = req.headers['host']slug = slugOfHost(hostHeader == null ? '' : hostHeader.split(':')[0])repoName = route.params.repoif (slug == '' || slugError(slug) != null || repoName != slug + '.git' || repoBySlug(slug) == null) { return plain(404, 'no such repository') }last = req.path.slice(req.path.lastIndexOf('/') + 1)advertise = last == 'refs'service = advertise ? (req.query != null ? req.query.service : null) : lastif (service != 'git-upload-pack' && service != 'git-receive-pack') { return plain(403, 'only the smart git protocol is served here: use git clone / fetch / push') }if (advertise && req.method != 'GET') { return plain(405, 'GET only') }if (!advertise && req.method != 'POST') { return plain(405, 'POST only') }if (req.body != null && req.body.length > maxBody) { return plain(413, 'that request is too big') }if (service == 'git-receive-pack') {realm = { 'WWW-Authenticate' = 'Basic realm="gitoria"' }pw = passwordOf(req.headers['authorization'])if (pw == null) { return plain(401, 'pushing needs a token: log in at ' + publicUrl + ', make one under "Access tokens", and use it as the password', realm) }userId = userOfToken(pw)if (userId == null) { return plain(401, 'that token is not valid (removed, or mistyped)', realm) }repo = repoBySlug(slug)if (!mayPush(userId, repo)) { return plain(403, 'only the owner of this repository can push to it') }}let out = callGit(slug, service, advertise, req)if (out == null) { return plain(500, 'git gave no answer') }let headers = { 'Cache-Control' = 'no-cache, max-age=0, must-revalidate' 'Pragma' = 'no-cache' }if (advertise) {headers['Content-Type'] = 'application/x-' + service + '-advertisement'// protocol v2 answers without the service banner; v0/v1 starts with it (as git http-backend does)v2 = protocolEnv(req).GIT_PROTOCOL != null && protocolEnv(req).GIT_PROTOCOL.includes('version=2')if (!v2) { out = pktLine('# service=' + service + NL) + '0000' + out }} else {headers['Content-Type'] = 'application/x-' + service + '-result'// a push arrived: tell the connected tickets project about commits naming a ticket (tickets.hl; never fails the push)if (service == 'git-receive-pack') { notifyPush(slug, false) }}return new Response(out, { headers = headers })}
Branches
- mainmain branch
Latest commits
- cc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mre
- 090a20c6mission 002 (code order) 2/4: one lib/ file per topic — git.hl split into git (calls, branches, init, temp folder) / homepage / code / pulls / releases (+ git-helpers: paths, ids, |||PR/|||RL markers); repos-helpers, tickets-helpers, transport-helpers; util.hl = localtime + env, storage dir, addresses, lists, text checks, one newest-first sort (was 3 copies); the function routes out of project.hl into lib/api.hl (thin; plumbing in api-helpers.hl), sshgate.hl folded into api.hl + sshkeys.hl keyLine + repos.hl mayPush; 'Make main' and the merge answer out of the faces (code.hl makeMain, pulls.hl pullsView), one login helper (users.hl userOfLoginCode); project.hl is the map. Session-writing routes get &req + &server.sessions. gates 200/0, 46/0, 44/0; real-data identical except /login/failed now shows the parked reason for a browser that already had a session (the old copy-of-req lost it)mre
- 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
- fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
- 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
- 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
- e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
- 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
- 3dc43108antcolony#40: mission references point to the moved missionsmre
- 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
- 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
- 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
- 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
- fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
- 4a2d7125initial commitmre