gitoria
All repositories: gitoria
29.2 KB
// git.hl — READING A REPO'S FILES for its homepage (ticket gitoria#8). Each repo is one BARE git// repository, <gitRoot>/<slug>.git (created with the repo, repos.hl createRepo). Reads go through the// `git` binary with hl:proc (docs/git-backend.md): an argv list, never a shell, the slug is// validated by the caller (repos.hl slugError), a path only ever comes from `git ls-tree`.// Every read calls git with hl:proc run() (hybriel#80: it waits), so the readers answer through their callback// before they return; the page components use the `...Now` forms at construction (server-rendered, gitoria#16).//// THE HOMEPAGE (the creator, ticket #1): the repo's `/` shows README.md as a page; if there is a// `$docs` folder, all Markdown files inside it form the homepage instead. Read from the repo's// HEAD (its default branch; the setting to switch it comes with #9).import { run, env, cwd } from 'hl:proc'import { markdownBlocks } from './markdown.hl'import { localStamp } from './localtime.hl'static NL = ""// where the bare repositories live: GITORIA_GIT, else ./storage/git under the launch directorystatic gitRoot = env('GITORIA_GIT') != null && env('GITORIA_GIT') != '' ? env('GITORIA_GIT') : cwd() + '/storage/git'static repoDir = (slug) => { return gitRoot + '/' + slug + '.git' }static maxFiles = 30 // Markdown files of a $docs folderstatic maxLines = 5000 // lines of one filestatic maxTree = 20000 // paths looked atstatic seconds = 15 // one git call// one git call: argv (after `git --git-dir=<dir>`), `done({ ok, lines })` exactly once — BEFORE git() returns:// hl:proc run() waits for the program (hybriel#80), so a page's root can read git while it is built on the server// and the callback style of the readers below is synchronous. Past `seconds` git is killed; run() is then an Error// (absorbed by project.hl `on Error`) and yields null → ok = false.static git = (slug, rest, limit, done) => {let argv = ['git', '--git-dir=' + repoDir(slug), '-c', 'core.quotePath=false']for (a of rest) { argv.push(a) }let r = run(argv, { timeout = seconds })let lines = []if (r != null) {for (l of r.lines) { if (lines.length < limit) { lines.push(l) } }}done({ ok = r != null && r.exit == 0 lines = lines })return null}// the SYNCHRONOUS answers of the readers (git() calls back before it returns): what a page's root usesstatic homepageNow = (slug, setting) => {let out = { files = [] found = false truncated = false }defaultBranch(slug, setting, (ref) => {loadHomepage(slug, ref, (result) => {out = resultreturn null})return null})return out}static browseNow = (slug, kind, rest, setting, owner) => {let out = nullbrowse(slug, kind, rest, setting, owner, (result) => {out = resultreturn null})return out}static pullsNow = (slug, setting) => {let out = nullpulls(slug, setting, (result) => {out = resultreturn null})return out}static releasesNow = (slug, setting) => {let out = nullreleases(slug, setting, (result) => {out = resultreturn null})return out}// a path we let through: printable, no leading dash, no backslash / quote / control charactersstatic safePath = (path) => {if (path == null || path == '' || path.length > 300 || path.startsWith('-')) { return false }let i = 0while (i < path.length) {let c = path.charCodeAt(i)if (c < 32 || c == 127 || c == 34 || c == 92 || c == 58) { return false }i = i + 1}return true}static isMarkdown = (path) => { return path.toLowerCase().endsWith('.md') }// the files that make the homepage: [] (none), or the paths in tree orderstatic homepagePaths = (tree) => {let docs = []let readme = nullfor (path of tree) {if (safePath(path)) {if (path.startsWith('$docs/') && isMarkdown(path) && docs.length < maxFiles) { docs.push(path) }if (readme == null && !path.includes('/') && path.toLowerCase() == 'readme.md') { readme = path }}}if (docs.length > 0) { return docs }return readme != null ? [readme] : []}// the homepage of a branch (its ref) → done({ files: [{ path, isDocs, blocks }], found, truncated })static loadHomepage = (slug, ref, done) => {if (ref == null) {done({ files = [] found = false truncated = false })return null}git(slug, ['ls-tree', '-r', '--name-only', ref], maxTree, (t) => {let paths = t.ok ? homepagePaths(t.lines) : []let files = []let truncated = falselet next = (i) => {if (i >= paths.length) {done({ files = files found = files.length > 0 truncated = truncated })return null}git(slug, ['cat-file', 'blob', ref + ':' + paths[i]], maxLines + 1, (r) => {if (r.ok) {if (r.lines.length > maxLines) { truncated = true }let lines = r.lines.length > maxLines ? r.lines.slice(0, maxLines) : r.linesfiles.push({ path = paths[i] isDocs = paths[i].startsWith('$docs/') blocks = markdownBlocks(lines.join(NL)) })}next(i + 1)return null})return null}next(0)return null})}// ---- BROWSING CODE (ticket gitoria#9): /code, /branch/<name>, /commit/<id> ----------------------// /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.// 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// a URL path piece: percent-encoded, the slashes kept (encodeURIComponent is a global since hybriel #14)static enc = (s) => { return encodeURIComponent(s).replaceAll('%2F', '/') }static isHexId = (s) => {if (s == null || s.length < 4 || s.length > 40) { return false }let i = 0while (i < s.length) {let c = s.charCodeAt(i)if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) { return false }i = i + 1}return true}static safeName = (n) => { return safePath(n) && n.length <= 100 && !n.endsWith('/') && !n.includes('//') }// the branch names of a repo → done([name])static branchNames = (slug, done) => {git(slug, ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], 500, (r) => {let out = []if (r.ok) { for (l of r.lines) { if (safeName(l)) { out.push(l) } } }done(out)return null})}// which branch is "main" for a repo: the setting if it exists, else `main`, else the first one; null = no branchstatic pickDefault = (names, setting) => {if (setting != null && setting != '' && names.includes(setting)) { return setting }if (names.includes('main')) { return 'main' }return names.length > 0 ? names[0] : null}static defaultBranch = (slug, setting, done) => {branchNames(slug, (names) => {done(pickDefault(names, setting))return null})}static sizeText = (n) => {if (n < 1024) { return n + ' B' }let unit = n < 1048576 ? 1024 : 1048576let t = (n * 10 - (n * 10) % unit) / unitreturn ((t - t % 10) / 10) + '.' + (t % 10) + (n < 1048576 ? ' KB' : ' MB')}// which view a page path is: { view: 'home' | 'code' | 'tickets' | 'pulls' | 'releases', kind, rest }static parseView = (path) => {let none = { view = 'home' kind = '' rest = '' }if (path == null || hlTypeName(path) != 'String' || path.length > 500 || path.slice(0, 1) != '/') { return none }let body = path.slice(1)let slash = body.indexOf('/')let head = slash < 0 ? body : body.slice(0, slash)let rest = slash < 0 ? '' : body.slice(slash + 1)if (head == 'code' || head == 'branch' || head == 'commit') { return { view = 'code' kind = head rest = rest } }if (head == 'tickets' && rest == '') { return { view = 'tickets' kind = '' rest = '' } }if (head == 'pulls' && rest == '') { return { view = 'pulls' kind = '' rest = '' } }if (head == 'releases' && rest == '') { return { view = 'releases' kind = '' rest = '' } }return none}// tab-separated fields of a line git wrote. (split() / indexOf() do not find a tab in a line that// came from hl:proc, so scan by character code.)static tabAt = (line) => {let i = 0while (i < line.length) {if (line.charCodeAt(i) == 9) { return i }i = i + 1}return -1}static tabs = (line) => {let out = []let start = 0let i = 0while (i < line.length) {if (line.charCodeAt(i) == 9) {out.push(line.slice(start, i))start = i + 1}i = i + 1}out.push(line.slice(start))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) => {let 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}let dflt = pickDefault(names, setting)let ref = nulllet label = ''let base = ''let pathSegs = segslet sha = ''let go = () => {let 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}let here = (p) => { return p == '' ? (kind == 'code' ? '/code' : base.slice(0, base.length - 1)) : base + enc(p) }let 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) => {let 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}let 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 = ownerlet crumbs = [{ 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 = truelet spec = ref + ':' + pathgit(slug, ['cat-file', '-t', spec], 1, (ty) => {let t = ty.ok && ty.lines.length > 0 ? ty.lines[0] : ''if (t == 'tree') {git(slug, ['ls-tree', '-l', spec], maxTree, (ls) => {let dirs = []let 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) => {let 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) => {let 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})})}// ---- PULL REQUESTS (ticket gitoria#11): /pulls -------------------------------------------------// A pull request is a COMMIT whose subject starts `|||PR ` (target = the repo's main branch) or// `|||PR|<branch>] ` (target = <branch>). Nothing is stored: the list is read from git each time.// Title = the text after the marker (else the source branch's name). Source = the branch holding the// commit that is not the target. State: `merged` when the commit is in the target branch, else `open`.// One request per source branch (its newest marker commit). Merging happens with git itself (push to the target).static maxPulls = 50static pullScan = 500// a commit subject → null (not a request) or { target: '' (= default) | name, title }static parsePull = (subject) => {if (subject == null) { return null }if (subject == '|||PR') { return { target = '' title = '' } }if (subject.startsWith('|||PR ')) { return { target = '' title = subject.slice(6).trim() } }if (subject.startsWith('|||PR|')) {let close = subject.indexOf(']')if (close < 7) { return null }let target = subject.slice(6, close)let rest = subject.slice(close + 1)if (rest != '' && !rest.startsWith(' ')) { return null }if (!safeName(target)) { return null }return { target = target title = rest.trim() }}return null}static pullShape = (state, message) => {return { state = state isMessage = message != '' message = message pulls = [] hasPulls = false noPulls = false defaultName = '' openCount = 0 mergedCount = 0 }}// every pull request of a repo → done(result). setting = the repo's saved main branch ('' = none)static pulls = (slug, setting, done) => {branchNames(slug, (names) => {if (names.length == 0) {done(pullShape('empty', 'This repository has no commits yet.'))return null}let dflt = pickDefault(names, setting)git(slug, ['log', '-' + pullScan, '--branches', '--format=%H%x09%ct%x09%an%x09%s'], pullScan, (lg) => {let found = []for (l of lg.ok ? lg.lines : []) {let f = tabs(l)if (f.length >= 4) {let sub = f.slice(3).join(' ')let pr = parsePull(sub)if (pr != null && isHexId(f[0]) && found.length < maxPulls) {found.push({ sha = f[0] short = f[0].slice(0, 8) date = localStamp(toNumber(f[1]) * 1000) author = f[2] target = pr.target == '' ? dflt : pr.target title = pr.title })}}}let out = pullShape('ok', '')out.defaultName = dfltlet seen = []let next = (i) => {if (i >= found.length) {out.hasPulls = out.pulls.length > 0out.noPulls = out.pulls.length == 0done(out)return null}let c = found[i]git(slug, ['for-each-ref', '--contains', c.sha, '--format=%(refname:short)', 'refs/heads'], 500, (ct) => {let source = ''for (n of ct.ok ? ct.lines : []) { if (source == '' && safeName(n) && n != c.target) { source = n } }if (source != '' && seen.includes(source)) {next(i + 1)return null}if (source != '') { seen.push(source) }let finish = (merged, targetMissing) => {let title = c.title != '' ? c.title : (source != '' ? source : c.short)out.pulls.push({ sha = c.sha short = c.short href = '/commit/' + c.sha title = title source = source hasSource = source != '' noSource = source == '' sourceHref = source != '' ? '/branch/' + enc(source) : '' target = c.target targetHref = targetMissing ? '' : '/branch/' + enc(c.target) targetLinked = !targetMissing state = merged ? 'merged' : 'open' isMerged = merged isOpen = !merged mergeable = !merged && source != '' && !targetMissing targetMissing = targetMissing author = c.author date = c.date })if (merged) { out.mergedCount = out.mergedCount + 1 } else { out.openCount = out.openCount + 1 }next(i + 1)return null}if (!names.includes(c.target)) { return finish(false, true) }git(slug, ['merge-base', '--is-ancestor', c.sha, c.target], 1, (mb) => {return finish(mb.ok, false)})return null})return null}next(0)return null})})}// the rows with `canMerge` (the owner, an open request whose branches exist) and the button's class: an `if` inside a// `for` row is not re-evaluated when the list is reassigned (the old button stayed), an attribute is — so the button// is always there and `.nomerge` hides it (styles.hl)static withMerge = (list, owner) => {let out = []for (p of list) {let q = pq.canMerge = owner && p.mergeableq.mergeClass = q.canMerge ? 'quiet mergepr' : 'nomerge'out.push(q)}return out}// ---- MERGE a pull request (ticket gitoria#17): only when the owner clicks the button ---------------------------// The repo is BARE (no work tree), so the merge is made with plumbing: `merge-tree --write-tree` merges the two// branches into a tree (exit 1 = conflict: nothing is written to any branch), `commit-tree` makes the merge commit// (parents: target, source), `update-ref` moves the target — with the old value given, so a push that arrived meanwhile// makes it fail instead of being overwritten. Always a merge commit (like GitHub's "Create a merge commit").// The pull request is found again on the SERVER by the sha of its `|||PR` commit (the client only names that).// → { ok } | { error }. who = the user's name (author of the merge commit).static gitRaw = (slug, rest) => {let argv = ['git', '--git-dir=' + repoDir(slug), '-c', 'core.quotePath=false']for (a of rest) { argv.push(a) }return run(argv, { timeout = seconds })}static mergePullNow = (slug, setting, sha, who) => {let found = nullif (sha == null || hlTypeName(sha) != 'String' || !isHexId(sha) || sha.length != 40) { return { error = 'no such pull request' } }let got = pullsNow(slug, setting)if (got == null || got.isMessage) { return { error = 'no such pull request' } }for (p of got.pulls) { if (p.sha == sha) { found = p } }if (found == null) { return { error = 'no such pull request' } }if (found.isMerged) { return { error = 'this pull request is already merged' } }if (found.noSource) { return { error = 'the branch of this pull request is gone' } }if (found.targetMissing) { return { error = 'the target branch ' + found.target + ' does not exist' } }let target = 'refs/heads/' + found.targetlet old = gitRaw(slug, ['rev-parse', '--verify', '-q', target])let tip = gitRaw(slug, ['rev-parse', '--verify', '-q', 'refs/heads/' + found.source])if (old == null || old.exit != 0 || old.lines.length == 0 || tip == null || tip.exit != 0 || tip.lines.length == 0) { return { error = 'could not read the branches' } }let oldSha = old.lines[0]let tipSha = tip.lines[0]if (!isHexId(oldSha) || !isHexId(tipSha)) { return { error = 'could not read the branches' } }let m = gitRaw(slug, ['merge-tree', '--write-tree', '--name-only', '--no-messages', oldSha, tipSha])if (m == null) { return { error = 'could not merge (git did not answer)' } }if (m.exit == 1) {let files = []let i = 1while (i < m.lines.length && m.lines[i] != '') {if (files.length < 5) { files.push(m.lines[i]) }i = i + 1}return { error = 'Not merged: ' + found.source + ' conflicts with ' + found.target + (files.length > 0 ? ' in ' + files.join(', ') : '') + '. Resolve it with git, then push.' }}if (m.exit != 0 || m.lines.length == 0 || !isHexId(m.lines[0])) { return { error = 'could not merge' } }let name = who != null && who != '' ? who : 'gitoria'let msg = "Merge branch '" + found.source + "' into " + found.targetlet c = gitRaw(slug, ['-c', 'user.name=' + name, '-c', 'user.email=noreply@gitoria', 'commit-tree', m.lines[0], '-p', oldSha, '-p', tipSha, '-m', msg])if (c == null || c.exit != 0 || c.lines.length == 0 || !isHexId(c.lines[0])) { return { error = 'could not write the merge commit' } }let u = gitRaw(slug, ['update-ref', '-m', msg, target, c.lines[0], oldSha])if (u == null || u.exit != 0) { return { error = 'The branch ' + found.target + ' changed meanwhile — nothing merged, try again.' } }return { ok = true }}// ---- RELEASES (ticket gitoria#12): /releases ---------------------------------------------------// A release is a COMMIT on the repo's main branch whose subject starts `|||RL ` (patch +1), `|||RL|med `// (minor +1, patch 0), `|||RL|mj ` (major +1, minor and patch 0) or `|||RL|<version> ` (set by hand,// e.g. 1.1.1a). Nothing is stored: the main branch's history is read oldest → newest and the versions are// counted up from 0.0.0 (so the first `|||RL ` is 0.0.1). A hand-set version that was released before// is not a release. A release = its version, the text after the marker, the commit (link), author, date.static maxReleases = 200static releaseScan = 5000static isDigits = (s) => {if (s == '' || s.length > 9) { return false }let i = 0while (i < s.length) {let c = s.charCodeAt(i)if (c < 48 || c > 57) { return false }i = i + 1}return true}// the part of a version after its patch digits: only letters, digits, `.` and `-`static suffixOk = (s) => {let i = 0while (i < s.length) {let c = s.charCodeAt(i)if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 45 || c == 46)) { return false }i = i + 1}return true}// `1.2.3` or `1.2.3a` → { major, minor, patch, text } or nullstatic parseVersion = (t) => {if (t == null || t.length > 32) { return null }let parts = t.split('.')if (parts.length < 3) { return null }if (!isDigits(parts[0]) || !isDigits(parts[1])) { return null }let rest = parts.slice(2).join('.')let n = 0while (n < rest.length && rest.charCodeAt(n) >= 48 && rest.charCodeAt(n) <= 57) { n = n + 1 }if (n == 0 || n > 9 || !suffixOk(rest.slice(n))) { return null }return { major = toNumber(parts[0]) minor = toNumber(parts[1]) patch = toNumber(rest.slice(0, n)) text = t }}// a commit subject → null (not a release) or { kind: 'patch' | 'med' | 'mj' | 'set', version, title }static parseRelease = (subject) => {if (subject == null) { return null }if (subject == '|||RL') { return { kind = 'patch' version = null title = '' } }if (subject.startsWith('|||RL ')) { return { kind = 'patch' version = null title = subject.slice(6).trim() } }if (subject.startsWith('|||RL|')) {let body = subject.slice(6)let sp = body.indexOf(' ')let token = sp < 0 ? body : body.slice(0, sp)let title = sp < 0 ? '' : body.slice(sp + 1).trim()if (token == 'med') { return { kind = 'med' version = null title = title } }if (token == 'mj') { return { kind = 'mj' version = null title = title } }let v = parseVersion(token)if (v == null) { return null }return { kind = 'set' version = v title = title }}return null}static releaseShape = (state, message) => {return { state = state isMessage = message != '' message = message releases = [] hasReleases = false noReleases = false defaultName = '' latest = '' }}// every release of a repo → done(result). setting = the repo's saved main branch ('' = none)static releases = (slug, setting, done) => {branchNames(slug, (names) => {if (names.length == 0) {done(releaseShape('empty', 'This repository has no commits yet.'))return null}let dflt = pickDefault(names, setting)git(slug, ['log', '--topo-order', '--reverse', '--fixed-strings', '--grep=|||RL', '--format=%H%x09%ct%x09%an%x09%s', dflt, '--'], releaseScan, (lg) => {let out = releaseShape('ok', '')out.defaultName = dfltlet major = 0let minor = 0let patch = 0let seen = []let oldest = []for (l of lg.ok ? lg.lines : []) {let f = tabs(l)if (f.length >= 4 && isHexId(f[0])) {let r = parseRelease(f.slice(3).join(' '))if (r != null) {let nmaj = majorlet nmin = minorlet npat = patchlet text = ''if (r.kind == 'patch') { npat = patch + 1 }if (r.kind == 'med') { nmin = minor + 1 npat = 0 }if (r.kind == 'mj') { nmaj = major + 1 nmin = 0 npat = 0 }if (r.kind == 'set') { nmaj = r.version.major nmin = r.version.minor npat = r.version.patch text = r.version.text }if (text == '') { text = nmaj + '.' + nmin + '.' + npat }if (!seen.includes(text)) {seen.push(text)major = nmajminor = nminpatch = npatoldest.push({ version = text sha = f[0] short = f[0].slice(0, 8) href = '/commit/' + f[0] title = r.title hasTitle = r.title != '' noTitle = r.title == '' author = f[2] date = localStamp(toNumber(f[1]) * 1000) isLatest = false })}}}}let i = oldest.length - 1while (i >= 0 && out.releases.length < maxReleases) {let rel = oldest[i]rel.isLatest = i == oldest.length - 1out.releases.push(rel)i = i - 1}if (out.releases.length > 0) { out.latest = out.releases[0].version }out.hasReleases = out.releases.length > 0out.noReleases = out.releases.length == 0done(out)return null})})}// a new empty bare repository for a repo (a failure is not fatal: a repo without one just has no files)static initRepo = (slug) => {return run(['git', 'init', '--bare', '-q', '-b', 'main', repoDir(slug)], { timeout = seconds })}// does the repo have no branch yet (nothing pushed)? — the "how to add code" box opens itself then (gitoria#7)static isEmptyNow = (slug) => {let empty = truegit(slug, ['for-each-ref', '--count=1', 'refs/heads'], 1, (r) => {empty = !r.ok || r.lines.length == 0return null})return empty}
Branches
- mainmain branch
Latest commits
- 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