gitoria
All repositories: gitoria
9.3 KB
// lib/code.hl — BROWSING CODE (ticket gitoria#9): /code, /branch/<name>, /commit/<id> of a repo address.// /code = the repo's main branch (the owner's setting, else `main`, else the first branch);// /branch/<name> = that branch at its last commit; /commit/<id> = the whole project at that commit.// Behind each: the folder tree, a file's lines, the branches, the latest commits. Read on the server while the page is// built (browseNow, components/codebrowser.hl); "Make main" (the owner) is makeMain.import { localStamp } from './util.hl'import { git, branchNames, pickDefault, maxTree } from './git.hl'import { safePath, enc, isHexId, tabAt, tabs, sizeText } from './git-helpers.hl'import { setMainBranch, repoAt } from './repos.hl'import { withRefParts } from './tickets-helpers.hl'// a line of valid UTF-8, as bytes (@ stands for a backslash: a string ending in a backslash cannot be written, so it is cut from '\u005cx')static utf8Line = '^(?:[@x00-@x7F]|[@xC2-@xDF][@x80-@xBF]|@xE0[@xA0-@xBF][@x80-@xBF]|[@xE1-@xEC@xEE@xEF][@x80-@xBF]{2}|@xED[@x80-@x9F][@x80-@xBF]|@xF0[@x90-@xBF][@x80-@xBF]{2}|[@xF1-@xF3][@x80-@xBF]{3}|@xF4[@x80-@x8F][@x80-@xBF]{2})*$'.replaceAll('@', '\u005cx'.slice(0, 1))static maxBrowseLines = 2000static maxBlobBytes = 1000000static maxCommits = 20// the view, SYNCHRONOUSLY (git() calls back before it returns): what the code pages' root usesstatic browseNow = (slug, kind, rest, setting, owner) => {let out = nullbrowse(slug, kind, rest, setting, owner, (result) => {out = resultreturn null})return out}// the answer's fixed shape: the page reads these names, whatever the statestatic shape = (state, message) => {return { state = state isMessage = message != '' message = message isTree = false isFile = false refLabel = '' kindLabel = ''sha = '' short = '' commitHref = '' subject = '' author = '' date = '' hasCrumbs = false crumbs = [] entries = [] noEntries = false lines = [] binary = false tooBig = false truncated = false fileSize = ''branches = [] commits = [] hasCommits = false defaultName = '' isCommitView = false canSet = false }}// the answer for one view → done(result). kind: 'code' | 'branch' | 'commit'; rest = what follows in the URL;// setting = the repo's saved main branch ('' = none); owner = the viewer may change itstatic browse = (slug, kind, rest, setting, owner, done) => {segs = []for (x of rest.split('/')) { if (x != '') { segs.push(x) } }branchNames(slug, (names) => {if (names.length == 0) {done(shape('empty', 'This repository has no commits yet. See the Code page to find out how to push code.'))return null}dflt = pickDefault(names, setting)let ref = nulllet label = ''let base = ''let pathSegs = segslet sha = ''go = () => {path = pathSegs.join('/')let bad = falsefor (sg of pathSegs) { if (sg == '.' || sg == '..' || !safePath(sg)) { bad = true } }if (bad) {done(shape('nopath', 'No such path in ' + label + '.'))return null}here = (p) => { return p == '' ? (kind == 'code' ? '/code' : base.slice(0, base.length - 1)) : base + enc(p) }branchLinks = []for (n of names) { branchLinks.push({ name = n href = '/branch/' + enc(n) isDefault = n == dflt isCurrent = kind == 'branch' && n == label notCurrent = !(kind == 'branch' && n == label) canSet = owner && n != dflt }) }git(slug, ['log', '-' + maxCommits, '--format=%H%x09%ct%x09%an%x09%s', ref], maxCommits, (lg) => {commits = []for (l of lg.lines) {let f = tabs(l)if (f.length >= 4) {let sub = f.slice(3).join(' ')commits.push({ sha = f[0] short = f[0].slice(0, 8) date = localStamp(toNumber(f[1]) * 1000) author = f[2] subject = sub href = '/commit/' + f[0] })}}if (commits.length == 0) {done(shape('empty', 'This repository has no commits yet. See the Code page to find out how to push code.'))return null}r = shape('ok', '')r.refLabel = labelr.kindLabel = kind == 'commit' ? 'Commit' : (kind == 'code' ? 'Main branch' : 'Branch')r.isCommitView = kind == 'commit'r.sha = commits[0].shar.short = commits[0].shortr.commitHref = '/commit/' + commits[0].shar.subject = commits[0].subjectr.author = commits[0].authorr.date = commits[0].dater.branches = branchLinksr.commits = commitsr.hasCommits = truer.defaultName = dfltr.canSet = ownercrumbs = [{ name = label href = here('') last = path == '' linked = path != '' }]let acc = ''let ci = 0while (ci < pathSegs.length) {acc = acc == '' ? pathSegs[ci] : acc + '/' + pathSegs[ci]crumbs.push({ name = pathSegs[ci] href = here(acc) last = ci == pathSegs.length - 1 linked = ci != pathSegs.length - 1 })ci = ci + 1}r.crumbs = crumbsr.hasCrumbs = truespec = ref + ':' + pathgit(slug, ['cat-file', '-t', spec], 1, (ty) => {t = ty.ok && ty.lines.length > 0 ? ty.lines[0] : ''if (t == 'tree') {git(slug, ['ls-tree', '-l', spec], maxTree, (ls) => {dirs = []files = []for (l of ls.ok ? ls.lines : []) {let tab = tabAt(l)if (tab > 0) {let meta = []for (m of l.slice(0, tab).split(' ')) { if (m != '') { meta.push(m) } }let name = l.slice(tab + 1)if (meta.length >= 3 && safePath(name)) {let full = path == '' ? name : path + '/' + namelet isDir = meta[1] == 'tree'let isBlob = meta[1] == 'blob'let e = { name = name isDir = isDir isFile = isBlob isOther = !isDir && !isBlob href = (isDir || isBlob) ? here(full) : '' size = (isBlob && meta.length >= 4 && meta[3] != '-') ? sizeText(toNumber(meta[3])) : '' isSub = meta[1] == 'commit' }if (isDir) { dirs.push(e) } else { files.push(e) }}}}for (e of files) { dirs.push(e) }r.isTree = truer.entries = dirsr.noEntries = dirs.length == 0done(r)return null})} else {if (t == 'blob') {git(slug, ['cat-file', '-s', spec], 1, (sz) => {bytes = sz.ok && sz.lines.length > 0 ? toNumber(sz.lines[0]) : 0r.isFile = truer.fileSize = sizeText(bytes)if (bytes > maxBlobBytes) {r.tooBig = truedone(r)return null}// only UTF-8 text is read as lines: any other bytes cannot travel to the page. git's own verdict// for text (`grep -I`), then a PCRE check that every line is valid UTF-8 (`-v` finds the others).git(slug, ['--literal-pathspecs', 'grep', '-I', '-l', '-e', '', ref, '--', path], 1, (tx) => {if (bytes == 0) {done(r)return null}if (!tx.ok) {r.binary = truedone(r)return null}git(slug, ['--literal-pathspecs', 'grep', '-I', '-P', '-v', '-c', '-e', utf8Line, ref, '--', path], 1, (bad) => {if (bad.ok) {r.binary = truedone(r)return null}git(slug, ['cat-file', 'blob', spec], maxBrowseLines + 1, (b) => {lines = []let n = 1for (l of b.ok ? b.lines : []) {if (n <= maxBrowseLines) { lines.push({ n = n text = l }) }n = n + 1}if (n - 1 > maxBrowseLines) { r.truncated = true }r.lines = linesdone(r)return null})return null})return null})return null})} else {done(shape('nopath', 'No such path in ' + label + '.'))}}return null})return null})return null}if (kind == 'code') {ref = dfltlabel = dfltbase = '/code/'go()return null}if (kind == 'branch') {let k = segs.lengthwhile (k >= 1 && ref == null) {let cand = segs.slice(0, k).join('/')if (names.includes(cand)) {ref = candlabel = candpathSegs = segs.slice(k)}k = k - 1}if (ref == null) {done(shape('nobranch', segs.length == 0 ? 'Choose a branch.' : "This repository has no branch '" + segs.join('/') + "'."))return null}base = '/branch/' + enc(ref) + '/'go()return null}// a commitif (segs.length == 0 || !isHexId(segs[0])) {done(shape('nocommit', 'No such commit.'))return null}git(slug, ['rev-parse', '--verify', '--quiet', segs[0] + '^{commit}'], 1, (rp) => {if (!rp.ok || rp.lines.length == 0 || !isHexId(rp.lines[0]) || rp.lines[0].length != 40) {done(shape('nocommit', "No commit '" + segs[0] + "' in this repository."))return null}sha = rp.lines[0]ref = shalabel = sha.slice(0, 8)base = '/commit/' + sha + '/'pathSegs = segs.slice(1)go()return null})})}// "MAKE MAIN" (the owner, components/codebrowser.hl): the branch must exist; the setting is stored and the view is read// again with it, with the ticket links of the subjects → { result } | { error }static makeMain = (userId, slug, name, kind, rest) => {let known = []branchNames(slug, (names) => {known = namesreturn null})if (!known.includes(name)) { return { error = 'no such branch' } }r = setMainBranch(userId, slug, name)if (r.error != null) { return { error = r.error } }return { result = withRefParts(browseNow(slug, kind, rest, name, true), repoAt(slug).ticketsProject) }}
Branches
- mainmain branch
Latest commits
- 4f47843egate: ticket links use the tickets short URL (/<slug>/<n>, tickets#25)mre
- 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