gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit4a2d71254a2d7125initial commitmre4a2d7125/repos.hl

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

Branches

Latest commits

  • 4a2d7125initial commitmre