gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit68dcb60368dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre68dcb603/repos.hl

7.7 KB

  1. // repos.hl — THE REPOS (ticket gitoria#6). A repo is a SLUG that is unique in the whole system;
  2. // it lives at <slug>.<domain> (users.hl repoOrigin). One hl:mpackdb table, storage/mpackdb/repos.db:
  3. // pk @id index !slug, owner { slug, description, owner (users @id), created, branch (the main branch; '' / absent = default),
  4. // tktKey, tktProject, tktTitle, tktApi (the connected tickets project, gitoria#18; the key is a secret and never leaves the server) }
  5. // The unique index makes the slug unique for good (a second put of a taken slug fails); the
  6. // check below answers it in words first. A new repo also gets an empty bare git repository (git.hl); pushing arrives with gitoria#7.
  7. import { MPackDB } from 'hl:mpackdb'
  8. import { now } from 'hl:time'
  9. import { storageDir, userOfSession, countOfList, firstOf, plainError, userRecord, nameOfUser, repoOrigin, publicUrl } from './users.hl'
  10. import { localStamp } from './localtime.hl'
  11. import { initRepo } from './git.hl'
  12. static reposTable = new MPackDB(file = storageDir + '/repos.db', primaryKey = '@id', indexes = ['!slug', 'owner'])
  13. // names that are addresses of the system itself, or that would read as one (never a repo)
  14. // a newline (the language has no \n escape in a string: a literal line break, as in tickets mdview.hl)
  15. static NL = "
  16. "
  17. // only technical host names; the creator's own apps (ident, tickets, hybriel, …) are repos here too (creator 2026-09-25)
  18. static reserved = ['www' 'api' 'git' 'ssh' 'http' 'https' 'ftp' 'mail' 'smtp' 'imap' 'admin' 'root' 'cdn' 'static' 'assets' 'login' 'logout' 'new' 'repos']
  19. static isLower = (c) => { return c >= 97 && c <= 122 }
  20. static isDigit = (c) => { return c >= 48 && c <= 57 }
  21. // the slug's rules → null (fine) or the complaint. A DNS label: 2–40 characters, a–z 0–9 and
  22. // single hyphens, starting and ending with a letter or digit ("--" is refused: xn-- is punycode)
  23. static slugError = (slug) => {
  24. if (slug == null || hlTypeName(slug) != 'String') { return 'the address must be text' }
  25. if (slug.length < 2 || slug.length > 40) { return 'the address must have 2–40 characters' }
  26. let i = 0
  27. while (i < slug.length) {
  28. let c = slug.charCodeAt(i)
  29. let ok = isLower(c) || isDigit(c) || (c == 45 && i > 0 && i < slug.length - 1)
  30. if (!ok) { return 'the address may only have lowercase letters a–z, digits and hyphens, and starts and ends with a letter or digit' }
  31. i = i + 1
  32. }
  33. if (slug.includes('--')) { return 'the address must not have two hyphens in a row' }
  34. if (reserved.includes(slug)) { return "'" + slug + "' is reserved for the system" }
  35. return null
  36. }
  37. static repoBySlug = (slug) => {
  38. if (slug == null || hlTypeName(slug) != 'String' || slug == '') { return null }
  39. return firstOf(reposTable.find('slug', slug))
  40. }
  41. static rowOf = (r) => {
  42. return { id = r.id slug = r.slug description = r.description described = r.description != '' owner = nameOfUser(r.owner) branch = r.branch != null ? r.branch : '' ticketsOn = r.tktKey != null && r.tktKey != '' ticketsOff = r.tktKey == null || r.tktKey == '' ticketsProject = r.tktProject != null ? r.tktProject : '' ticketsTitle = r.tktTitle != null ? r.tktTitle : '' created = localStamp(r.created) createdMs = r.created address = repoOrigin(r.slug) + '/' }
  43. }
  44. // every repo, newest first (no sort(): hybriel #1)
  45. static repoRows = () => {
  46. let out = []
  47. let all = reposTable.find(null, null)
  48. if (countOfList(all) == 0) { return out }
  49. for (r of all) { out.push(rowOf(r)) }
  50. let i = 1
  51. while (i < out.length) {
  52. let cur = out[i]
  53. let j = i - 1
  54. while (j >= 0 && out[j].createdMs < cur.createdMs) {
  55. out[j + 1] = out[j]
  56. j = j - 1
  57. }
  58. out[j + 1] = cur
  59. i = i + 1
  60. }
  61. return out
  62. }
  63. static repoRow = (slug) => {
  64. let r = repoBySlug(slug)
  65. return r == null ? null : rowOf(r)
  66. }
  67. // the repo a page stands on (users.hl slugOfHost): its row, or null for the main address / an unknown address
  68. static repoAt = (slug) => {
  69. if (slug == null || slug == '' || slugError(slug) != null) { return null }
  70. return repoRow(slug)
  71. }
  72. // a tab title: 'Code · alpha · gitoria', 'alpha · gitoria', 'gitoria'
  73. static pageTitle = (site, slug, what) => {
  74. let t = slug != null && slug != '' ? slug + ' · ' + site : site
  75. return what != null && what != '' ? what + ' · ' + t : t
  76. }
  77. // a new repo for a user: { repo } (the row) or { error, field }
  78. static createRepo = (userId, slug, description) => {
  79. let u = userRecord(userId)
  80. if (u == null) { return { error = 'log in with ident (top right) to create a repository' field = '' } }
  81. if (u.name == '') { return { error = 'choose a display name first (top of the page)' field = '' } }
  82. let s = slug == null ? '' : ('' + slug).trim()
  83. let bad = slugError(s)
  84. if (bad != null) { return { error = bad field = 'slug' } }
  85. let d = description == null ? '' : ('' + description).trim()
  86. let dbad = plainError(d, 200, 'the description')
  87. if (dbad != null) { return { error = dbad field = 'description' } }
  88. if (repoBySlug(s) != null) { return { error = "the address '" + s + "' is taken — every repository address is unique in the whole system" field = 'slug' } }
  89. let id = reposTable.put({ slug = s description = d owner = u.id created = now() })
  90. if (id == null) { return { error = 'could not store the repository (is the address taken?): ' + reposTable.lastError() field = 'slug' } }
  91. initRepo(s)
  92. return { repo = rowOf(reposTable.fetch(id)) }
  93. }
  94. // does this (real) session's user own the repo?
  95. static ownsRepo = (slug, session) => {
  96. let u = userOfSession(session)
  97. let row = repoBySlug(slug)
  98. return u != null && row != null && row.owner == u.id
  99. }
  100. // a repo's whole record with some fields changed (update() replaces the record: every field must be given again)
  101. static withChange = (r, change) => {
  102. let o = { id = r.id slug = r.slug description = r.description owner = r.owner created = r.created branch = r.branch != null ? r.branch : '' tktKey = r.tktKey != null ? r.tktKey : '' tktProject = r.tktProject != null ? r.tktProject : '' tktTitle = r.tktTitle != null ? r.tktTitle : '' tktApi = r.tktApi != null ? r.tktApi : '' }
  103. for (k of change.keys()) { o[k] = change[k] }
  104. return o
  105. }
  106. // the connection to a tickets project (gitoria#18); key '' = disconnected. The caller checked the owner.
  107. static setTicketsConnection = (slug, key, project, title, api) => {
  108. let r = repoBySlug(slug)
  109. if (r == null) { return { error = 'no such repository' } }
  110. reposTable.update(r.id, withChange(r, { tktKey = key tktProject = project tktTitle = title tktApi = api }))
  111. let after = reposTable.fetch(r.id)
  112. if (after == null || after.tktKey != key) { return { error = 'could not store the connection: ' + reposTable.lastError() } }
  113. return { repo = rowOf(after) }
  114. }
  115. // the owner's setting of the main branch (/code, the homepage). The caller checked the branch exists.
  116. static setMainBranch = (userId, slug, name) => {
  117. let r = repoBySlug(slug)
  118. if (r == null) { return { error = 'no such repository' } }
  119. if (userId == null || r.owner != userId) { return { error = 'only the owner can change the main branch' } }
  120. reposTable.update(r.id, withChange(r, { branch = name }))
  121. let after = reposTable.fetch(r.id)
  122. if (after == null || after.branch != name) { return { error = 'could not store the setting: ' + reposTable.lastError() } }
  123. return { repo = rowOf(after) }
  124. }
  125. // ---- the Markdown read view (Accept: text/markdown) — for LLM readers ---------------------------
  126. static repoDocument = (row) => {
  127. let out = '# ' + row.slug + NL + NL
  128. if (row.described) { out = out + row.description + NL + NL }
  129. out = out + '- address: ' + row.address + NL + '- owner: ' + row.owner + NL + '- created: ' + row.created + NL
  130. return out
  131. }
  132. static listDocument = (rows) => {
  133. let out = '# Repositories (' + rows.length + ')' + NL + NL
  134. if (rows.length == 0) { out = out + 'No repository yet.' + NL }
  135. for (r of rows) { out = out + '- [' + r.slug + '](' + r.address + ')' + (r.described ? ' — ' + r.description : '') + NL }
  136. return out
  137. }

Branches

Latest commits

  • 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