gitoria
All repositories: gitoria
7.7 KB
// lib/pulls.hl — PULL REQUESTS (ticket gitoria#11): /pulls of a repo address.// 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).import { localStamp } from './util.hl'import { git, gitRaw, branchNames, pickDefault } from './git.hl'import { enc, isHexId, tabs, safeName, parsePull } from './git-helpers.hl'import { withPullParts } from './tickets-helpers.hl'static maxPulls = 50static pullScan = 500// the list, SYNCHRONOUSLY (git() calls back before it returns): what the pulls page's root usesstatic pullsNow = (slug, setting) => {let out = nullpulls(slug, setting, (result) => {out = resultreturn null})return out}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}dflt = pickDefault(names, setting)git(slug, ['log', '-' + pullScan, '--branches', '--format=%H%x09%ct%x09%an%x09%s'], pullScan, (lg) => {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 })}}}out = pullShape('ok', '')out.defaultName = dfltseen = []next = (i) => {if (i >= found.length) {out.hasPulls = out.pulls.length > 0out.noPulls = out.pulls.length == 0done(out)return null}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) }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) => {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 mergePullNow = (slug, setting, sha, who) => {let found = nullif (sha == null || hlTypeName(sha) != 'String' || !isHexId(sha) || sha.length != 40) { return { error = 'no such pull request' } }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' } }target = 'refs/heads/' + found.targetold = gitRaw(slug, ['rev-parse', '--verify', '-q', target])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' } }oldSha = old.lines[0]tipSha = tip.lines[0]if (!isHexId(oldSha) || !isHexId(tipSha)) { return { error = 'could not read the branches' } }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) {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' } }name = who != null && who != '' ? who : 'gitoria'msg = "Merge branch '" + found.source + "' into " + found.targetc = 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' } }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 }}// the list read again, with the Merge buttons and the ticket links: the answer of the Merge face (components/pulls.hl)static pullsView = (slug, setting, owner, project) => {got = pullsNow(slug, setting)return { result = got list = withPullParts(withMerge(got.pulls, owner), project) }}
Branches
- mainmain branch
Latest commits
- 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