gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain4f47843egate: ticket links use the tickets short URL (/<slug>/<n>, tickets#25)mremain/lib/code.hl

9.3 KB

  1. // lib/code.hl — BROWSING CODE (ticket gitoria#9): /code, /branch/<name>, /commit/<id> of a repo address.
  2. // /code = the repo's main branch (the owner's setting, else `main`, else the first branch);
  3. // /branch/<name> = that branch at its last commit; /commit/<id> = the whole project at that commit.
  4. // Behind each: the folder tree, a file's lines, the branches, the latest commits. Read on the server while the page is
  5. // built (browseNow, components/codebrowser.hl); "Make main" (the owner) is makeMain.
  6. import { localStamp } from './util.hl'
  7. import { git, branchNames, pickDefault, maxTree } from './git.hl'
  8. import { safePath, enc, isHexId, tabAt, tabs, sizeText } from './git-helpers.hl'
  9. import { setMainBranch, repoAt } from './repos.hl'
  10. import { withRefParts } from './tickets-helpers.hl'
  11. // 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')
  12. 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))
  13. static maxBrowseLines = 2000
  14. static maxBlobBytes = 1000000
  15. static maxCommits = 20
  16. // the view, SYNCHRONOUSLY (git() calls back before it returns): what the code pages' root uses
  17. static browseNow = (slug, kind, rest, setting, owner) => {
  18. let out = null
  19. browse(slug, kind, rest, setting, owner, (result) => {
  20. out = result
  21. return null
  22. })
  23. return out
  24. }
  25. // the answer's fixed shape: the page reads these names, whatever the state
  26. static shape = (state, message) => {
  27. return { state = state isMessage = message != '' message = message isTree = false isFile = false refLabel = '' kindLabel = ''
  28. sha = '' short = '' commitHref = '' subject = '' author = '' date = '' hasCrumbs = false crumbs = [] entries = [] noEntries = false lines = [] binary = false tooBig = false truncated = false fileSize = ''
  29. branches = [] commits = [] hasCommits = false defaultName = '' isCommitView = false canSet = false }
  30. }
  31. // the answer for one view → done(result). kind: 'code' | 'branch' | 'commit'; rest = what follows in the URL;
  32. // setting = the repo's saved main branch ('' = none); owner = the viewer may change it
  33. static browse = (slug, kind, rest, setting, owner, done) => {
  34. segs = []
  35. for (x of rest.split('/')) { if (x != '') { segs.push(x) } }
  36. branchNames(slug, (names) => {
  37. if (names.length == 0) {
  38. done(shape('empty', 'This repository has no commits yet. See the Code page to find out how to push code.'))
  39. return null
  40. }
  41. dflt = pickDefault(names, setting)
  42. let ref = null
  43. let label = ''
  44. let base = ''
  45. let pathSegs = segs
  46. let sha = ''
  47. go = () => {
  48. path = pathSegs.join('/')
  49. let bad = false
  50. for (sg of pathSegs) { if (sg == '.' || sg == '..' || !safePath(sg)) { bad = true } }
  51. if (bad) {
  52. done(shape('nopath', 'No such path in ' + label + '.'))
  53. return null
  54. }
  55. here = (p) => { return p == '' ? (kind == 'code' ? '/code' : base.slice(0, base.length - 1)) : base + enc(p) }
  56. branchLinks = []
  57. 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 }) }
  58. git(slug, ['log', '-' + maxCommits, '--format=%H%x09%ct%x09%an%x09%s', ref], maxCommits, (lg) => {
  59. commits = []
  60. for (l of lg.lines) {
  61. let f = tabs(l)
  62. if (f.length >= 4) {
  63. let sub = f.slice(3).join(' ')
  64. 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] })
  65. }
  66. }
  67. if (commits.length == 0) {
  68. done(shape('empty', 'This repository has no commits yet. See the Code page to find out how to push code.'))
  69. return null
  70. }
  71. r = shape('ok', '')
  72. r.refLabel = label
  73. r.kindLabel = kind == 'commit' ? 'Commit' : (kind == 'code' ? 'Main branch' : 'Branch')
  74. r.isCommitView = kind == 'commit'
  75. r.sha = commits[0].sha
  76. r.short = commits[0].short
  77. r.commitHref = '/commit/' + commits[0].sha
  78. r.subject = commits[0].subject
  79. r.author = commits[0].author
  80. r.date = commits[0].date
  81. r.branches = branchLinks
  82. r.commits = commits
  83. r.hasCommits = true
  84. r.defaultName = dflt
  85. r.canSet = owner
  86. crumbs = [{ name = label href = here('') last = path == '' linked = path != '' }]
  87. let acc = ''
  88. let ci = 0
  89. while (ci < pathSegs.length) {
  90. acc = acc == '' ? pathSegs[ci] : acc + '/' + pathSegs[ci]
  91. crumbs.push({ name = pathSegs[ci] href = here(acc) last = ci == pathSegs.length - 1 linked = ci != pathSegs.length - 1 })
  92. ci = ci + 1
  93. }
  94. r.crumbs = crumbs
  95. r.hasCrumbs = true
  96. spec = ref + ':' + path
  97. git(slug, ['cat-file', '-t', spec], 1, (ty) => {
  98. t = ty.ok && ty.lines.length > 0 ? ty.lines[0] : ''
  99. if (t == 'tree') {
  100. git(slug, ['ls-tree', '-l', spec], maxTree, (ls) => {
  101. dirs = []
  102. files = []
  103. for (l of ls.ok ? ls.lines : []) {
  104. let tab = tabAt(l)
  105. if (tab > 0) {
  106. let meta = []
  107. for (m of l.slice(0, tab).split(' ')) { if (m != '') { meta.push(m) } }
  108. let name = l.slice(tab + 1)
  109. if (meta.length >= 3 && safePath(name)) {
  110. let full = path == '' ? name : path + '/' + name
  111. let isDir = meta[1] == 'tree'
  112. let isBlob = meta[1] == 'blob'
  113. 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' }
  114. if (isDir) { dirs.push(e) } else { files.push(e) }
  115. }
  116. }
  117. }
  118. for (e of files) { dirs.push(e) }
  119. r.isTree = true
  120. r.entries = dirs
  121. r.noEntries = dirs.length == 0
  122. done(r)
  123. return null
  124. })
  125. } else {
  126. if (t == 'blob') {
  127. git(slug, ['cat-file', '-s', spec], 1, (sz) => {
  128. bytes = sz.ok && sz.lines.length > 0 ? toNumber(sz.lines[0]) : 0
  129. r.isFile = true
  130. r.fileSize = sizeText(bytes)
  131. if (bytes > maxBlobBytes) {
  132. r.tooBig = true
  133. done(r)
  134. return null
  135. }
  136. // only UTF-8 text is read as lines: any other bytes cannot travel to the page. git's own verdict
  137. // for text (`grep -I`), then a PCRE check that every line is valid UTF-8 (`-v` finds the others).
  138. git(slug, ['--literal-pathspecs', 'grep', '-I', '-l', '-e', '', ref, '--', path], 1, (tx) => {
  139. if (bytes == 0) {
  140. done(r)
  141. return null
  142. }
  143. if (!tx.ok) {
  144. r.binary = true
  145. done(r)
  146. return null
  147. }
  148. git(slug, ['--literal-pathspecs', 'grep', '-I', '-P', '-v', '-c', '-e', utf8Line, ref, '--', path], 1, (bad) => {
  149. if (bad.ok) {
  150. r.binary = true
  151. done(r)
  152. return null
  153. }
  154. git(slug, ['cat-file', 'blob', spec], maxBrowseLines + 1, (b) => {
  155. lines = []
  156. let n = 1
  157. for (l of b.ok ? b.lines : []) {
  158. if (n <= maxBrowseLines) { lines.push({ n = n text = l }) }
  159. n = n + 1
  160. }
  161. if (n - 1 > maxBrowseLines) { r.truncated = true }
  162. r.lines = lines
  163. done(r)
  164. return null
  165. })
  166. return null
  167. })
  168. return null
  169. })
  170. return null
  171. })
  172. } else {
  173. done(shape('nopath', 'No such path in ' + label + '.'))
  174. }
  175. }
  176. return null
  177. })
  178. return null
  179. })
  180. return null
  181. }
  182. if (kind == 'code') {
  183. ref = dflt
  184. label = dflt
  185. base = '/code/'
  186. go()
  187. return null
  188. }
  189. if (kind == 'branch') {
  190. let k = segs.length
  191. while (k >= 1 && ref == null) {
  192. let cand = segs.slice(0, k).join('/')
  193. if (names.includes(cand)) {
  194. ref = cand
  195. label = cand
  196. pathSegs = segs.slice(k)
  197. }
  198. k = k - 1
  199. }
  200. if (ref == null) {
  201. done(shape('nobranch', segs.length == 0 ? 'Choose a branch.' : "This repository has no branch '" + segs.join('/') + "'."))
  202. return null
  203. }
  204. base = '/branch/' + enc(ref) + '/'
  205. go()
  206. return null
  207. }
  208. // a commit
  209. if (segs.length == 0 || !isHexId(segs[0])) {
  210. done(shape('nocommit', 'No such commit.'))
  211. return null
  212. }
  213. git(slug, ['rev-parse', '--verify', '--quiet', segs[0] + '^{commit}'], 1, (rp) => {
  214. if (!rp.ok || rp.lines.length == 0 || !isHexId(rp.lines[0]) || rp.lines[0].length != 40) {
  215. done(shape('nocommit', "No commit '" + segs[0] + "' in this repository."))
  216. return null
  217. }
  218. sha = rp.lines[0]
  219. ref = sha
  220. label = sha.slice(0, 8)
  221. base = '/commit/' + sha + '/'
  222. pathSegs = segs.slice(1)
  223. go()
  224. return null
  225. })
  226. })
  227. }
  228. // "MAKE MAIN" (the owner, components/codebrowser.hl): the branch must exist; the setting is stored and the view is read
  229. // again with it, with the ticket links of the subjects → { result } | { error }
  230. static makeMain = (userId, slug, name, kind, rest) => {
  231. let known = []
  232. branchNames(slug, (names) => {
  233. known = names
  234. return null
  235. })
  236. if (!known.includes(name)) { return { error = 'no such branch' } }
  237. r = setMainBranch(userId, slug, name)
  238. if (r.error != null) { return { error = r.error } }
  239. return { result = withRefParts(browseNow(slug, kind, rest, name, true), repoAt(slug).ticketsProject) }
  240. }

Branches

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