gitoria
All repositories: gitoria
25.2 KB
// tests/realdata-baseline.mjs (gitoria mission 002, code order): every read the app answers + a fixed write sequence, on a// COPY of the LIVE storage, saved for diffing two code trees (a cleanup must answer the same). Compare two outputs with// tests/realdata-compare.py. README "Test" → "Same output".// node tests/realdata-baseline.mjs <tree> <port> <outdir> (kills only the server and the sshd container it started)// The live copy: GITORIA_REALDATA (default .scratch/realdata) must hold storage/mpackdb + storage/git, e.g. from Byrodin:// tar -C /CONTAINERS/projects/gitoria.worldapi.org -cf - storage/mpackdb storage/git | ssh loreana tar -C <repo>/.scratch/realdata -xf -// Each run works on a fresh copy of it (<outdir>.run, removed after). Signed in as the creator (identity az5b2 → users @id// 0mugibaf6fds) through a session file written before the start; every page is fetched signed in AND signed out.// Ports: <port> = the app, <port>+8 = the sshd container (docker, image built from docker/sshd, name gitoria-sshd-rd<port>).// Git commits are made with fixed dates (client and server: GIT_AUTHOR_DATE / GIT_COMMITTER_DATE), so their ids match// between runs; the ssh keys are made once in <GITORIA_REALDATA>/keys and reused (same fingerprints).// Tickets: the connected repo's tickets API is a SNAPSHOT of the live answer (<GITORIA_REALDATA>/tickets-snapshot.json,// fetched once), served on <port>+9; the copy's stored `tktApi` is rewritten to it (same length, the msgpack stays valid).// So nothing reaches the live tickets, and a change on live tickets between two runs does not show up as a difference.import { spawn, spawnSync, execSync, execFileSync } from 'node:child_process';import { request, createServer } from 'node:http';import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';import { createHash, randomBytes } from 'node:crypto';import { join, resolve } from 'node:path';const [tree, portArg, outArg] = process.argv.slice(2);const PORT = Number(portArg), SSHPORT = PORT + 8;const W = resolve(process.env.GITORIA_REALDATA || '.scratch/realdata'), OUT = resolve(outArg), RUN = OUT + '.run';const USER = '0mugibaf6fds', SECRET = 'rd-secret-fixed', CONTAINER = 'gitoria-sshd-rd' + PORT;rmSync(RUN, { recursive: true, force: true }); rmSync(OUT, { recursive: true, force: true });mkdirSync(join(RUN, 'sessions'), { recursive: true }); mkdirSync(join(RUN, 'home'), { recursive: true }); mkdirSync(OUT, { recursive: true });execSync(`cp -a ${W}/storage ${RUN}/storage`);const GIT = join(RUN, 'storage/git');const LIVE_TICKETS = 'https://tickets.worldapi.org', FAKE_TICKETS = `http://127.0.0.1:${PORT + 9}/fake00`; // the same lengthif (LIVE_TICKETS.length !== FAKE_TICKETS.length) throw new Error('fake tickets URL must have the length of the live one');const reposFile = join(RUN, 'storage/mpackdb/repos.mpack');const rb = readFileSync(reposFile, 'latin1');writeFileSync(reposFile, rb.split(LIVE_TICKETS + '/api/projects/').join(FAKE_TICKETS + '/api/projects/'), 'latin1');const SNAP = join(W, 'tickets-snapshot.json');if (!existsSync(SNAP)) writeFileSync(SNAP, await (await fetch(LIVE_TICKETS + '/api/projects/gitoria.worldapi.org/tickets')).text());const fake = createServer((req, res) => {if (req.method === 'GET' && req.url === '/fake00/api/projects/gitoria.worldapi.org/tickets') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(readFileSync(SNAP)); return; }res.writeHead(404, { 'content-type': 'application/json' }); res.end('{"error":"fake tickets: not here"}');});fake.listen(PORT + 9, '127.0.0.1');// ---- the keys (once) ---------------------------------------------------------------------------------------------const KEYS = join(W, 'keys');if (!existsSync(join(KEYS, 'ed'))) {mkdirSync(KEYS, { recursive: true });spawnSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'rd@w082', '-f', join(KEYS, 'ed')]);spawnSync('ssh-keygen', ['-q', '-t', 'rsa', '-b', '1024', '-N', '', '-C', 'small@w082', '-f', join(KEYS, 'rsa1024')]);}const edPub = readFileSync(join(KEYS, 'ed.pub'), 'utf8').trim(), rsaSmall = readFileSync(join(KEYS, 'rsa1024.pub'), 'utf8').trim();// ---- the session -------------------------------------------------------------------------------------------------const sid = randomBytes(16).toString('hex'), now = Date.now();writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid).digest('hex')),JSON.stringify({ created: now, data: { tag: 'w082tag' }, handled: [], id: sid, seen: now, user: { id: USER } }), { mode: 0o600 });const COOKIE = 'gitoriasid=' + sid;// a second, signed-out session (for the failed-login note)const sid2 = randomBytes(16).toString('hex');writeFileSync(join(RUN, 'sessions', createHash('sha256').update(sid2).digest('hex')),JSON.stringify({ created: now, data: { tag: 'w082tag2' }, handled: [], id: sid2, seen: now, user: null }), { mode: 0o600 });const sleep = ms => new Promise(r => setTimeout(r, ms));const MAIN = `gitoria.test:${PORT}`, HOST = (slug) => `${slug}.gitoria.test:${PORT}`;const DATES = { GIT_AUTHOR_DATE: '2026-10-03T12:00:00+0200', GIT_COMMITTER_DATE: '2026-10-03T12:00:00+0200' };const srv = spawn(resolve(tree, 'bin/hybriel'), ['project.hl'], { cwd: resolve(tree), stdio: ['ignore', 'pipe', 'pipe'],env: { ...process.env, ...DATES, GITORIA_PORT: String(PORT), GITORIA_PUBLIC_URL: `http://${MAIN}`, GITORIA_STORAGE: join(RUN, 'storage/mpackdb'),GITORIA_GIT: GIT, GITORIA_SESSIONS: join(RUN, 'sessions') + '/', GITORIA_WATCH: '0', IDENT_URL: 'http://127.0.0.1:9', IDENT_EXCHANGE_URL: 'http://127.0.0.1:9',IDENT_API_KEY: 'pk_w082', IDENT_API_SECRET: '', GITORIA_SSH_SECRET: SECRET, GITORIA_SSH_PORT: String(SSHPORT), GITORIA_TICKETS_URL: 'https://tickets.worldapi.org' } });let log = ''; srv.stdout.on('data', d => log += d); srv.stderr.on('data', d => log += d);const hreq = (method, host, path, headers = {}, body = null) => new Promise((res) => {const rq = request({ host: '127.0.0.1', port: PORT, path, method, headers: { host, ...headers } }, (r) => {const parts = []; r.on('data', d => parts.push(d)); r.on('end', () => res({ status: r.statusCode, headers: r.headers, body: Buffer.concat(parts).toString('utf8') }));});rq.on('error', (e) => res({ status: 0, headers: {}, body: 'ERROR ' + e.message }));if (body != null) { rq.setHeader('content-length', Buffer.byteLength(body)); rq.write(body); }rq.end();});let n = 0;const file = (name, text) => writeFileSync(join(OUT, String(++n).padStart(4, '0') + '-' + name.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120)), text);const save = async (name, method, host, path, headers = {}, body = null) => {const r = await hreq(method, host, path, headers, body);const head = [r.status, r.headers['content-type'] || '', r.headers['location'] || '', r.headers['www-authenticate'] || '', r.headers['cache-control'] || '', r.headers['set-cookie'] ? 'set-cookie' : ''].join(' | ');file(name, head + '\n' + r.body);return r;};let ei = 0;const emitF = async (name, event, payload, cookie = COOKIE) => {const r = await hreq('POST', MAIN, '/__hl/emit', { 'content-type': 'application/json', cookie }, JSON.stringify({ t: 'emit', i: ++ei, event, payload }));file('w-face-' + name, r.status + '\n' + r.body);try { return JSON.parse(r.body).value; } catch { return null; }};const gitIn = (dir, args) => execFileSync('git', args.split(' '), { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();const GENV = { ...process.env, ...DATES, HOME: join(RUN, 'home'), GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', LC_ALL: 'C' };const sshCmd = `ssh -i ${join(KEYS, 'ed')} -p ${SSHPORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR`;// the real git client; `slug` maps <slug>.gitoria.test to 127.0.0.1 for httpconst git = (cwd, args, slug = null, extra = {}) => new Promise((res) => {const pre = slug ? ['-c', `http.curloptResolve=${slug}.gitoria.test:${PORT}:127.0.0.1`] : [];const c = spawn('git', [...pre, '-c', 'user.name=W082', '-c', '[email protected]', '-c', 'init.defaultBranch=main', ...args], { cwd, env: { ...GENV, ...extra } });let out = ''; c.stdout.on('data', d => out += d); c.stderr.on('data', d => out += d);const t = setTimeout(() => c.kill('SIGKILL'), 180000);c.on('close', (code) => { clearTimeout(t); res({ code, out }); });});// what a git run says, without what differs between runs (temp paths, transfer speeds)const gitText = (r) => 'exit ' + r.code + '\n' + r.out.split(/\r|\n/).filter(l => l.trim() && !/Receiving objects|Resolving deltas|Counting objects|Compressing objects|Writing objects|Total \d|remote: (Enumerating|Counting|Compressing|Total)/.test(l)).map(l => l.split(RUN).join('<RUN>')).join('\n');try {for (let i = 0; i < 240; i++) { if ((await hreq('GET', MAIN, '/api/repos')).status === 200) break; await sleep(500); }const repos = JSON.parse((await save('api-repos', 'GET', MAIN, '/api/repos')).body).repos;// ---- READS ------------------------------------------------------------------------------------------------------const pages = [[MAIN, '/'], [MAIN, '/login/failed'], [MAIN, '/code'], [MAIN, '/pulls'], [MAIN, '/settings'], [MAIN, '/nope'],[HOST('nosuchrepo'), '/'], [HOST('nosuchrepo'), '/code'], [HOST('nosuchrepo'), '/pulls'], [HOST('x--y'), '/']];const apis = ['/api/repos', '/api/repos/nosuch', '/api/repos/x--y'];const advert = [];for (const r of repos) {const s = r.slug, H = HOST(s), dir = join(GIT, s + '.git');apis.push('/api/repos/' + s);for (const p of ['/', '/code', '/pulls', '/releases', '/tickets', '/settings', '/code/nosuch.txt', '/branch/nosuch', '/branch', '/commit/zzzz', '/commit/deadbeef', '/commit']) pages.push([H, p]);const branches = gitIn(dir, 'for-each-ref --format=%(refname:short) refs/heads').split('\n').filter(Boolean).slice(0, 6);for (const b of branches) pages.push([H, '/branch/' + encodeURIComponent(b).replaceAll('%2F', '/')]);if (branches.length) {const b0 = branches.includes(r.branch) ? r.branch : branches.includes('main') ? 'main' : branches[0];const all = gitIn(dir, `ls-tree -r --name-only ${b0}`).split('\n').filter(Boolean);const enc = (p) => p.split('/').map(encodeURIComponent).join('/');const pick = [all[0], all.find(p => /readme\.md$/i.test(p)), all.find(p => p.includes('/')), all.find(p => p.split('/').length > 2), all[all.length - 1], all.find(p => /\.(png|ico|jpg|gz|zip)$/i.test(p))].filter(Boolean);for (const p of [...new Set(pick)]) pages.push([H, '/code/' + enc(p)]);const dirs = [...new Set(all.filter(p => p.includes('/')).map(p => p.split('/')[0]))].slice(0, 2);for (const d of dirs) pages.push([H, '/code/' + enc(d)]);const sha = gitIn(dir, `rev-parse ${b0}`);pages.push([H, '/commit/' + sha], [H, '/commit/' + sha.slice(0, 8)]);if (dirs[0]) pages.push([H, '/commit/' + sha + '/' + enc(dirs[0])]);const old = gitIn(dir, `log --format=%H -1 --skip=5 ${b0}`);if (old) pages.push([H, '/commit/' + old]);}advert.push(s);}const modules = new Set();for (const [h, p] of pages) {const r = await save('in-' + h + p, 'GET', h, p, { cookie: COOKIE }); await save('out-' + h + p, 'GET', h, p);for (const m of r.body.matchAll(/(\/(?:components|__hl)\/[\w./-]+\.(?:hl|js|css))\?v=[0-9a-f]+/g)) modules.add(m[0]);}// the browser half: every component module, the runtime, the client, the stylesheet the pages loadfor (const m of [...modules].sort()) await save('module-' + m.replace(/\?v=.*/, ''), 'GET', MAIN, m);for (const u of apis) { await save('json' + u, 'GET', MAIN, u); await save('md' + u, 'GET', MAIN, u, { accept: 'text/markdown' }); }await save('api-post', 'POST', MAIN, '/api/repos', { 'content-type': 'application/json' }, '{}');await save('api-post-one', 'POST', MAIN, '/api/repos/tracker', { 'content-type': 'application/json' }, '{}');// the git protocol: the refs advertisement of every repo, and the refusalsfor (const s of advert) {await save('refs-upload-' + s, 'GET', HOST(s), `/${s}.git/info/refs?service=git-upload-pack`);await save('refs-upload-v2-' + s, 'GET', HOST(s), `/${s}.git/info/refs?service=git-upload-pack`, { 'git-protocol': 'version=2' });}await save('refs-receive-noauth', 'GET', HOST('email'), '/email.git/info/refs?service=git-receive-pack');await save('refs-receive-badtoken', 'GET', HOST('email'), '/email.git/info/refs?service=git-receive-pack', { authorization: 'Basic ' + Buffer.from('x:gtr_' + '0'.repeat(40)).toString('base64') });await save('refs-receive-junk', 'GET', HOST('email'), '/email.git/info/refs?service=git-receive-pack', { authorization: 'Basic !!!' });await save('refs-dumb', 'GET', HOST('email'), '/email.git/info/refs');await save('refs-wronghost', 'GET', HOST('tracker'), '/email.git/info/refs?service=git-upload-pack');await save('refs-main', 'GET', MAIN, '/email.git/info/refs?service=git-upload-pack');await save('refs-post', 'POST', HOST('email'), '/email.git/info/refs?service=git-upload-pack', {}, 'x');await save('upload-get', 'GET', HOST('email'), '/email.git/git-upload-pack');// the internal ssh endpointsconst S = { 'x-gitoria-secret': SECRET };await save('keys-nosecret', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx');await save('keys-wrong', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', { 'x-gitoria-secret': 'nope' });await save('keys-proxied', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', { ...S, 'x-forwarded-for': '1.2.3.4' });await save('keys-unknown', 'GET', MAIN, '/__git/keys?type=ssh-ed25519&key=AAAAC3NzaC1lZDI1NTE5AAAAIx', S);await save('keys-post', 'POST', MAIN, '/__git/keys', S, 'x');for (const q of [`user=${USER}&slug=tracker&write=0`, `user=${USER}&slug=tracker&write=1`, 'user=nobody&slug=tracker&write=1', 'user=nobody&slug=tracker&write=0', `user=${USER}&slug=nosuch&write=0`, `user=${USER}&slug=X..&write=0`, 'slug=tracker'])await save('access-' + q, 'GET', MAIN, '/__git/access?' + q, S);// the login's and the tickets connection's function routes (ident is down on purpose: port 9)for (const q of ['', '?ident_code=zz', '?ident_code=abc123', '?ident_code=abc&next=/code']) await save('callback' + q, 'GET', MAIN, '/login/callback' + q);await save('callback-post', 'POST', MAIN, '/login/callback', {}, 'x');await save('page-loginfailed-after', 'GET', MAIN, '/login/failed');// a failed login in a browser that has a session: the reason is parked in THAT session (the route writes it by reference)const C2 = 'gitoriasid=' + sid2;await save('callback-cookie', 'GET', MAIN, '/login/callback?ident_code=abc123', { cookie: C2 });await save('page-loginfailed-cookie', 'GET', MAIN, '/login/failed', { cookie: C2 });await save('callback-cookie-nocode', 'GET', MAIN, '/login/callback', { cookie: C2 });await save('page-loginfailed-cookie2', 'GET', MAIN, '/login/failed', { cookie: C2 });await save('connect-out', 'GET', HOST('gitoria'), '/settings/connect');await save('connect-main', 'GET', MAIN, '/settings/connect', { cookie: COOKIE });await save('connect-nosuch', 'GET', HOST('nosuchrepo'), '/settings/connect', { cookie: COOKIE });await save('connected-out', 'GET', HOST('email'), '/settings/connected?code=x&state=y');await save('connected-error', 'GET', HOST('email'), '/settings/connected?error=nope+said+tickets', { cookie: COOKIE });await save('settings-note1', 'GET', HOST('email'), '/settings', { cookie: COOKIE });await save('connected-nostate', 'GET', HOST('email'), '/settings/connected?code=x&state=y', { cookie: COOKIE });await save('settings-note2', 'GET', HOST('email'), '/settings', { cookie: COOKIE });await save('connect-start', 'GET', HOST('email'), '/settings/connect', { cookie: COOKIE });await save('connected-wrongstate', 'GET', HOST('email'), '/settings/connected?code=x&state=y', { cookie: COOKIE });await save('settings-note3', 'GET', HOST('email'), '/settings', { cookie: COOKIE });// ---- WRITES ('w-' files; realdata-compare.py masks the new ids, times and token secrets) ------------------------------await emitF('saveName-again', 'gitoriaSaveName', ['someone']);await emitF('create', 'gitoriaCreate', ['w082-new', 'W082 test repo']);await emitF('create-dup', 'gitoriaCreate', ['w082-new', '']);await emitF('create-bad', 'gitoriaCreate', ['Bad Slug', '']);await emitF('create-reserved', 'gitoriaCreate', ['www', '']);await emitF('create-longdesc', 'gitoriaCreate', ['w082-other', 'x'.repeat(201)]);await emitF('create-out', 'gitoriaCreate', ['w082-out', ''], 'gitoriasid=none');const tok = (await emitF('token', 'gitoriaMakeToken', ['w082'])) || {};await emitF('token-empty', 'gitoriaMakeToken', ['']);const key = (await emitF('key', 'gitoriaAddKey', ['w082 key', edPub])) || {};await emitF('key-dup', 'gitoriaAddKey', ['again', edPub]);await emitF('key-private', 'gitoriaAddKey', ['p', '-----BEGIN OPENSSH PRIVATE KEY-----']);await emitF('key-small', 'gitoriaAddKey', ['small', rsaSmall]);await emitF('key-junk', 'gitoriaAddKey', ['junk', 'ssh-ed25519 AAAAnotakeyatallxxxxxxxxx']);const [kt, kb] = edPub.split(' ');await save('w-keys-known', 'GET', MAIN, `/__git/keys?type=${kt}&key=${encodeURIComponent(kb)}`, S);await save('w-access-new', 'GET', MAIN, `/__git/access?user=${USER}&slug=w082-new&write=1`, S);for (const p of ['/', '/code', '/pulls', '/releases', '/settings']) await save('w-page-empty' + p, 'GET', HOST('w082-new'), p, { cookie: COOKIE });await save('w-page-main', 'GET', MAIN, '/', { cookie: COOKIE });// over HTTP: clone the empty repo, commit, push with the token (and refused without / with a wrong one)const W1 = join(RUN, 'home', 'w1'); mkdirSync(W1);const urlOf = (slug, pass) => `http://${pass ? 'x:' + pass + '@' : ''}${slug}.gitoria.test:${PORT}/${slug}.git`;file('w-git-clone-empty', gitText(await git(W1, ['clone', urlOf('w082-new'), 'r'], 'w082-new')));const R = join(W1, 'r');writeFileSync(join(R, 'README.md'), '# w082\n\nA test repo. fixes #1\n\n| a | b |\n|---|---|\n| 1 | 2 |\n');mkdirSync(join(R, 'src')); writeFileSync(join(R, 'src/a.txt'), 'line one\nline two\n');writeFileSync(join(R, 'src/bin.dat'), Buffer.from([0, 1, 2, 255, 254]));await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', 'first commit #1']);writeFileSync(join(R, 'src/b.txt'), 'b\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||RL first release']);file('w-git-push-noauth', gitText(await git(R, ['push', urlOf('w082-new'), 'main'], 'w082-new')));file('w-git-push-badtoken', gitText(await git(R, ['push', urlOf('w082-new', 'gtr_' + '1'.repeat(40)), 'main'], 'w082-new')));file('w-git-push', gitText(await git(R, ['push', urlOf('w082-new', tok.token), 'main'], 'w082-new')));await git(R, ['checkout', '-q', '-b', 'feature']); writeFileSync(join(R, 'src/c.txt'), 'c\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||PR feature work, fixes #2']);await git(R, ['checkout', '-q', '-b', 'side', 'main']); writeFileSync(join(R, 'src/d.txt'), 'd\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||PR|nosuch] bad target']);await git(R, ['checkout', '-q', 'main']); writeFileSync(join(R, 'src/b.txt'), 'b2\n'); await git(R, ['add', '-A']); await git(R, ['commit', '-q', '-m', '|||RL|med a minor one']);file('w-git-push-branches', gitText(await git(R, ['push', urlOf('w082-new', tok.token), 'main', 'feature', 'side'], 'w082-new')));const prSha = gitIn(R, 'rev-parse feature');for (const p of ['/', '/code', '/code/src', '/code/src/a.txt', '/code/src/bin.dat', '/pulls', '/releases', '/branch/feature', '/branch/side/src', '/commit/' + prSha])await save('w-page' + p.replace(prSha, 'PR'), 'GET', HOST('w082-new'), p, { cookie: COOKIE });await emitF('setBranch', 'gitoriaSetBranch', ['w082-new', 'feature', 'code', '']);await save('w-page-code-feature', 'GET', HOST('w082-new'), '/code', { cookie: COOKIE });await emitF('setBranch-back', 'gitoriaSetBranch', ['w082-new', 'main', 'code', 'src']);await emitF('setBranch-nosuch', 'gitoriaSetBranch', ['w082-new', 'nosuch', 'code', '']);await emitF('setBranch-badkind', 'gitoriaSetBranch', ['w082-new', 'main', 'x', '']);await emitF('setBranch-out', 'gitoriaSetBranch', ['w082-new', 'main', 'code', ''], 'gitoriasid=none');await emitF('merge', 'gitoriaMergePull', ['w082-new', prSha]);await emitF('merge-again', 'gitoriaMergePull', ['w082-new', prSha]);await emitF('merge-bad', 'gitoriaMergePull', ['w082-new', 'zz']);await emitF('merge-target-missing', 'gitoriaMergePull', ['w082-new', gitIn(R, 'rev-parse side')]);await emitF('openTicket', 'gitoriaOpenTicket', ['w082-new', 'subject', 'text']);await emitF('openTicket-nosuch', 'gitoriaOpenTicket', ['nosuchrepo', 'subject', 'text']);await emitF('disconnect', 'gitoriaDisconnectTickets', ['w082-new']);await emitF('disconnect-out', 'gitoriaDisconnectTickets', ['w082-new'], 'gitoriasid=none');for (const p of ['/', '/code', '/pulls', '/releases', '/settings']) await save('w-page-after' + p, 'GET', HOST('w082-new'), p, { cookie: COOKIE });await save('w-api-new', 'GET', MAIN, '/api/repos/w082-new'); await save('w-api-new-md', 'GET', MAIN, '/api/repos/w082-new', { accept: 'text/markdown' });// a real repo: clone over HTTP, push a branch with the tokenfile('w-git-clone-tracker', gitText(await git(W1, ['clone', '-q', urlOf('tracker'), 'tracker'], 'tracker')) + '\n' + gitIn(join(W1, 'tracker'), 'log --format=%H -3') + '\n' + gitIn(join(W1, 'tracker'), 'ls-tree -r HEAD').split('\n').length);file('w-git-clone-email', gitText(await git(W1, ['clone', '-q', urlOf('email'), 'email'], 'email')));const E = join(W1, 'email');await git(E, ['checkout', '-q', '-b', 'w082http']); writeFileSync(join(E, 'w082.txt'), 'http\n'); await git(E, ['add', '-A']); await git(E, ['commit', '-q', '-m', 'w082 over http']);file('w-git-push-email', gitText(await git(E, ['push', urlOf('email', tok.token), 'w082http'], 'email')));await save('w-page-email-branch', 'GET', HOST('email'), '/branch/w082http', { cookie: COOKIE });// over SSH: the real sshd container (docker/sshd) on a copy of the reposspawnSync('docker', ['rm', '-f', CONTAINER]);const built = spawnSync('docker', ['build', '-q', '-t', 'gitoria-sshd-gate', resolve(tree, 'docker/sshd')], { encoding: 'utf8' });const started = spawnSync('docker', ['run', '-d', '--rm', '--name', CONTAINER, '--network', 'host', '-e', 'GITORIA_SSH_SECRET=' + SECRET, '-e', 'GITORIA_SSH_PORT=' + SSHPORT, '-e', `GITORIA_URL=http://127.0.0.1:${PORT}`, '-v', GIT + ':/repos', 'gitoria-sshd-gate'], { encoding: 'utf8' });file('w-ssh-container', `build ${built.status} run ${started.status}`);for (let i = 0; i < 60; i++) { const r = spawnSync('sh', ['-c', `ssh-keyscan -p ${SSHPORT} 127.0.0.1 2>/dev/null | grep -c ssh-`], { encoding: 'utf8' }); if (Number(r.stdout) > 0) break; await sleep(250); }const SURL = (slug) => `ssh://[email protected]:${SSHPORT}/${slug}.git`;const W2 = join(RUN, 'home', 'w2'); mkdirSync(W2);file('w-ssh-clone-tracker', gitText(await git(W2, ['clone', '-q', SURL('tracker'), 'tracker'], null, { GIT_SSH_COMMAND: sshCmd })) + '\n' + gitIn(join(W2, 'tracker'), 'log --format=%H -3'));file('w-ssh-clone-email', gitText(await git(W2, ['clone', '-q', SURL('email'), 'email'], null, { GIT_SSH_COMMAND: sshCmd })));const E2 = join(W2, 'email');await git(E2, ['checkout', '-q', '-b', 'w082ssh']); writeFileSync(join(E2, 'w082ssh.txt'), 'ssh\n'); await git(E2, ['add', '-A']); await git(E2, ['commit', '-q', '-m', 'w082 over ssh #3']);file('w-ssh-push-email', gitText(await git(E2, ['push', SURL('email'), 'w082ssh'], null, { GIT_SSH_COMMAND: sshCmd })));file('w-ssh-push-nosuch', gitText(await git(E2, ['push', SURL('nosuchrepo'), 'w082ssh'], null, { GIT_SSH_COMMAND: sshCmd })));await save('w-page-email-ssh', 'GET', HOST('email'), '/branch/w082ssh', { cookie: COOKIE });await save('w-page-email-code', 'GET', HOST('email'), '/code', { cookie: COOKIE });// removing: the key stops at once, the token tooawait emitF('removeKey', 'gitoriaRemoveKey', [key.row ? key.row.id : 'none']);file('w-ssh-after-remove', gitText(await git(W2, ['clone', '-q', SURL('email'), 'email2'], null, { GIT_SSH_COMMAND: sshCmd })));await emitF('removeKey-again', 'gitoriaRemoveKey', [key.row ? key.row.id : 'none']);await emitF('removeToken', 'gitoriaRemoveToken', [tok.row ? tok.row.id : 'none']);file('w-git-push-removed', gitText(await git(E, ['push', urlOf('email', tok.token), 'w082http'], 'email')));await emitF('removeToken-again', 'gitoriaRemoveToken', [tok.row ? tok.row.id : 'none']);await save('w-page-main-after', 'GET', MAIN, '/', { cookie: COOKIE });await save('w-api-repos', 'GET', MAIN, '/api/repos');// the login faces (ident down) and the logoutawait emitF('login-bad', 'gitoriaLogin', ['zz']);await emitF('logout', 'gitoriaLogOut', []);await save('w-page-main-loggedout', 'GET', MAIN, '/', { cookie: COOKIE });console.log(`${n} responses saved to ${OUT} (${pages.length} pages x2, ${apis.length} api urls x2, + writes)`);} finally {fake.close();spawnSync('docker', ['rm', '-f', CONTAINER]);srv.kill('SIGTERM'); await sleep(1000); try { process.kill(srv.pid, 'SIGKILL'); } catch {}}writeFileSync(join(OUT, '0000-server-log'), log.split('\n').filter(l => !/listening|watch/i.test(l)).join('\n'));rmSync(RUN, { recursive: true, force: true });
Branches
- mainmain branch
Latest commits
- 86605446mission 002 (code order) 4/4: README file map + import order + 'Same output' test + gate run with a tickets HEAD copy, STATUS (entry, lessons), LOG, report; tests/realdata-baseline.mjs + realdata-compare.py (a cleanup answers the same on live data: pages, modules, API, git over HTTPS and SSH, faces), tests/letcount.pymre
- 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