heyquilt 0.3.121 → 0.3.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASES.md +292 -17
- package/bin/quilt.js +72 -5
- package/desktop/main.js +16 -2
- package/package.json +3 -1
- package/src/agent-task-workflow.js +2 -2
- package/src/agents/common.js +13 -0
- package/src/ai-vm.js +9 -1
- package/src/api/activity.js +5 -0
- package/src/api/memory-store.js +21 -10
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/github.js +0 -13
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/seam-connect.js +103 -0
- package/src/api/routes/session-migrate.js +39 -0
- package/src/api/routes/sessions.js +73 -1
- package/src/api/server.js +46 -12
- package/src/api/supabase-store.js +53 -4
- package/src/autowork.js +182 -16
- package/src/connection.js +39 -9
- package/src/control.js +139 -13
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/gitstate.js +38 -2
- package/src/history.js +25 -1
- package/src/mcp.js +93 -7
- package/src/merge3.js +11 -3
- package/src/now-summary.js +113 -0
- package/src/pathrules.js +12 -0
- package/src/protocol.js +10 -2
- package/src/quilt-default.js +84 -0
- package/src/quilt-history.js +106 -0
- package/src/quilt-remote.js +78 -0
- package/src/relay-commit.js +5 -0
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +172 -8
- package/src/relay-outbound.js +1 -1
- package/src/relay-prune.js +4 -1
- package/src/release-words.js +2 -2
- package/src/runner.js +20 -7
- package/src/seam-access.js +97 -0
- package/src/seam-claims.js +53 -0
- package/src/seam-url.js +15 -0
- package/src/server.js +1767 -33
- package/src/session-follow.js +515 -0
- package/src/session-layout3.js +466 -0
- package/src/session-migrate.js +123 -0
- package/src/session-quilt.js +145 -0
- package/src/session-seam.js +284 -0
- package/src/session.js +899 -46
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +507 -35
- package/src/source-live/live-square.js +17 -6
- package/src/status.js +5 -0
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +85 -20
- package/src/ui/app.js +4 -1
- package/src/ui/chat.js +9 -3
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -2
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -0
- package/src/ui/home.js +0 -147
- package/src/ui/loom-model.js +15 -1
- package/src/ui/quiltview.js +213 -0
- package/src/ui/seam-model.js +251 -0
- package/src/ui/session.js +413 -31
- package/src/ui/voice-call.js +83 -14
- package/src/ui/voice-model.js +39 -0
- package/src/ui/voice.js +167 -17
- package/src/ui-server.js +80 -28
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/git.js +0 -147
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- package/src/quilts/archiver.js +0 -175
- package/src/quilts/autorevert.js +0 -49
- package/src/quilts/bisect-run.js +0 -31
- package/src/quilts/blame.js +0 -103
- package/src/quilts/block.js +0 -347
- package/src/quilts/bucket.js +0 -25
- package/src/quilts/cbor.js +0 -92
- package/src/quilts/claims.js +0 -144
- package/src/quilts/cli.js +0 -449
- package/src/quilts/client.js +0 -128
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -266
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -160
- package/src/quilts/export-git.js +0 -130
- package/src/quilts/export.js +0 -81
- package/src/quilts/fastmerge.js +0 -41
- package/src/quilts/hash.js +0 -44
- package/src/quilts/history.js +0 -266
- package/src/quilts/ids.js +0 -12
- package/src/quilts/import-git.js +0 -291
- package/src/quilts/import.js +0 -134
- package/src/quilts/index.js +0 -72
- package/src/quilts/limits.js +0 -67
- package/src/quilts/lines.js +0 -49
- package/src/quilts/live.js +0 -50
- package/src/quilts/merge-smart.js +0 -114
- package/src/quilts/merge.js +0 -486
- package/src/quilts/objects.js +0 -144
- package/src/quilts/pack.js +0 -118
- package/src/quilts/records.js +0 -211
- package/src/quilts/resolutions.js +0 -69
- package/src/quilts/reviews.js +0 -195
- package/src/quilts/routes.js +0 -126
- package/src/quilts/service.js +0 -923
- package/src/quilts/setup.js +0 -34
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -683
- package/src/quilts/store-rpc.js +0 -79
- package/src/quilts/store-supabase.js +0 -13
- package/src/quilts/tags.js +0 -64
- package/src/quilts/textdiff.js +0 -96
- package/src/quilts/tokens.js +0 -66
- package/src/quilts/tools.js +0 -375
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -119
- package/src/quilts/xdiff.js +0 -435
- package/src/ui/branches.js +0 -111
package/src/quilts/import.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// The server side of a git import (plans/quilt-source-control/14): the importer (import-git.js,
|
|
2
|
-
// run where the repository is) uploads packs of blobs and commit records, then sends the
|
|
3
|
-
// commits in order through importBatch. Each commit on a branch's first-parent chain becomes a
|
|
4
|
-
// patch on its line (HEAD's branch is main) with its own square; a merge commit's side commits
|
|
5
|
-
// become the commits of the merge's square. importKnown lets a second import add only new commits.
|
|
6
|
-
// The snapshot of every patch is derived here, eagerly, as for any merge into a line.
|
|
7
|
-
import { cleanCommit, commitId } from './records.js'
|
|
8
|
-
import { QuiltError } from './service.js'
|
|
9
|
-
|
|
10
|
-
const MAX_BATCH = 500
|
|
11
|
-
const MAX_KNOWN = 5000
|
|
12
|
-
// A git branch keeps its name as a line: git's own characters, no `..`, no trailing slash.
|
|
13
|
-
const BRANCH = /^[A-Za-z0-9._-][A-Za-z0-9._/-]{0,99}$/
|
|
14
|
-
|
|
15
|
-
export function importLineName (raw) {
|
|
16
|
-
const s = String(raw || '')
|
|
17
|
-
if (!BRANCH.test(s) || /\.\.|\/\/|\/$|\.lock$/.test(s) || s === 'main') throw new QuiltError(400, 'bad_request', `a git branch named ${s} cannot be a line`)
|
|
18
|
-
return s
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function importOps (svc) {
|
|
22
|
-
const { store, now, quiltFor, getCommit, treeGrower, seal, snapshot, fromStore } = svc.internals
|
|
23
|
-
|
|
24
|
-
/** The quilt, if an import made it: only those take imported commits. */
|
|
25
|
-
async function importQuilt (me, ref) {
|
|
26
|
-
const q = await quiltFor(me, ref, 'admin')
|
|
27
|
-
const quilt = await store.getQuilt(q.id)
|
|
28
|
-
if (!quilt.settings?.importing) throw new QuiltError(409, 'bad_request', 'this quilt was not made by an import (seam import git makes a new quilt, or adds to one it made)')
|
|
29
|
-
return q
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* lines: [{ name, tip }], new lines made first, each at a commit already imported (a git branch).
|
|
34
|
-
* items: [{ id, commit (JSON record), kind: 'patch' | 'side', line (default main), square: { id,
|
|
35
|
-
* title, base, holder }, git_sha, payload }], in an order where parents come first.
|
|
36
|
-
*/
|
|
37
|
-
async function importBatch (me, ref, { items = [], lines = [] } = {}) {
|
|
38
|
-
const q = await importQuilt(me, ref)
|
|
39
|
-
if (items.length > MAX_BATCH) throw new QuiltError(400, 'bad_request', `at most ${MAX_BATCH} commits a batch`)
|
|
40
|
-
for (const l of lines) {
|
|
41
|
-
const name = importLineName(l.name)
|
|
42
|
-
if (!l.tip || !(await store.getCommit(q.id, l.tip))) throw new QuiltError(422, 'bad_request', `line ${name} starts at a commit that is not imported`)
|
|
43
|
-
try { await store.createLine({ quilt_id: q.id, name, tip: l.tip, actor: me.member_id, at: now() }) } catch (err) { throw fromStore(err) }
|
|
44
|
-
}
|
|
45
|
-
// Check everything first: ids, sealed records and blobs.
|
|
46
|
-
const prepared = []
|
|
47
|
-
for (const it of items) {
|
|
48
|
-
const c = cleanCommit({ ...it.commit, quilt_id: q.id })
|
|
49
|
-
const id = commitId(c)
|
|
50
|
-
if (id !== it.id) throw new QuiltError(422, 'bad_request', `commit ${String(it.id).slice(0, 12)} does not match its record (the server computes ${id.slice(0, 12)})`)
|
|
51
|
-
if (await store.getCommit(q.id, id)) { prepared.push({ it, c, id, done: true }); continue }
|
|
52
|
-
if (!(await store.getObject(q.id, id))) throw new QuiltError(422, 'pack_unsealed', `commit record ${id.slice(0, 12)} is not uploaded`, { ids: [id] })
|
|
53
|
-
const blobIds = [...new Set(c.changes.filter((x) => x.op === 'put').map((x) => x.blob))]
|
|
54
|
-
const have = new Set(await store.hasBlobs(q.id, blobIds))
|
|
55
|
-
const missing = blobIds.filter((b) => !have.has(b))
|
|
56
|
-
if (missing.length) throw new QuiltError(422, 'pack_unsealed', `${missing.length} blobs of commit ${id.slice(0, 12)} are not uploaded`, { ids: missing.slice(0, 50) })
|
|
57
|
-
prepared.push({ it, c, id })
|
|
58
|
-
}
|
|
59
|
-
// Snapshots of the patches, in order, sealed in one server pack before any row names them.
|
|
60
|
-
const heads = new Map() // line → { tip, root }
|
|
61
|
-
const grow = treeGrower(q.id)
|
|
62
|
-
const nodes = []
|
|
63
|
-
for (const p of prepared) {
|
|
64
|
-
if (p.done || p.it.kind !== 'patch') continue
|
|
65
|
-
const ln = p.it.line || 'main'
|
|
66
|
-
if (!heads.has(ln)) {
|
|
67
|
-
const row = await store.getLine(q.id, ln)
|
|
68
|
-
if (!row) throw new QuiltError(404, 'not_found', `no line ${ln}`)
|
|
69
|
-
heads.set(ln, { tip: row.tip, root: await snapshot(q.id, row.tip) })
|
|
70
|
-
}
|
|
71
|
-
const h = heads.get(ln)
|
|
72
|
-
if ((p.c.parents[0] || null) !== (h.tip || null)) throw new QuiltError(422, 'bad_request', `patch ${p.id.slice(0, 12)} does not follow ${ln}'s tip`)
|
|
73
|
-
const t = await grow(h.root, p.c.changes)
|
|
74
|
-
h.root = t.root
|
|
75
|
-
nodes.push(...t.objects)
|
|
76
|
-
p.root = t.root
|
|
77
|
-
h.tip = p.id
|
|
78
|
-
}
|
|
79
|
-
await seal(q.id, nodes)
|
|
80
|
-
// Stub members for the authors (14): one row per distinct stub id, before any commit names it.
|
|
81
|
-
const stubs = new Map()
|
|
82
|
-
for (const p of prepared) {
|
|
83
|
-
if (p.done) continue
|
|
84
|
-
const a = p.c.author || {}
|
|
85
|
-
if (String(a.member_id || '').startsWith('stub:') && !stubs.has(a.member_id)) {
|
|
86
|
-
const rest = a.member_id.slice(5)
|
|
87
|
-
stubs.set(a.member_id, { member_id: a.member_id, kind: a.kind === 'agent' ? 'agent' : 'person', name: a.name || null, email: rest.includes('@') ? rest : null })
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (stubs.size) await store.addStubs({ quilt_id: q.id, stubs: [...stubs.values()], at: now() })
|
|
91
|
-
let patches = 0; let commits = 0
|
|
92
|
-
for (const p of prepared) {
|
|
93
|
-
if (p.done) continue
|
|
94
|
-
const at = now()
|
|
95
|
-
const sq = await store.getSquare(p.it.square.id)
|
|
96
|
-
if (!sq) {
|
|
97
|
-
await store.openSquare({ square: { id: p.it.square.id, quilt_id: q.id, title: String(p.it.square.title || '').slice(0, 200), line: p.it.line || 'main', base: p.it.square.base || null, holder: p.it.square.holder || me.member_id, opened_by: me.member_id, writers: [], import: true }, at })
|
|
98
|
-
}
|
|
99
|
-
await store.stageFiles({ quilt_id: q.id, commit_id: p.id, files: p.c.changes })
|
|
100
|
-
const record = { ...p.c, id: p.id, at }
|
|
101
|
-
const payload = { ...(p.it.payload || {}), ...(p.it.git_sha ? { git_sha: p.it.git_sha } : {}) }
|
|
102
|
-
if (p.it.kind === 'patch') {
|
|
103
|
-
const ln = p.it.line || 'main'
|
|
104
|
-
const cur = await store.getLine(q.id, ln)
|
|
105
|
-
await store.merge({ commit: record, line: ln, expected_tip: cur.tip, expect_head: null, snapshot_root: p.root, actor: me.member_id, at, payload, import: true })
|
|
106
|
-
await store.fillLinePaths({ quilt_id: q.id })
|
|
107
|
-
patches++
|
|
108
|
-
} else {
|
|
109
|
-
await store.importCommit({ commit: record, actor: me.member_id, at, payload })
|
|
110
|
-
}
|
|
111
|
-
commits++
|
|
112
|
-
if (p.it.git_sha) await store.setGitSha({ quilt_id: q.id, sha: p.it.git_sha, commit_id: p.id })
|
|
113
|
-
}
|
|
114
|
-
const after = await store.getLine(q.id, 'main')
|
|
115
|
-
return { ok: true, commits, patches, tip: after.tip, patch_count: after.patch_count }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** POST /import/known { shas }: which git commits this quilt already has → { known: { sha: commit id } }. */
|
|
119
|
-
async function importKnown (me, ref, { shas = [] } = {}) {
|
|
120
|
-
const q = await importQuilt(me, ref)
|
|
121
|
-
if (!Array.isArray(shas) || shas.length > MAX_KNOWN) throw new QuiltError(400, 'bad_request', `at most ${MAX_KNOWN} shas a request`)
|
|
122
|
-
const list = shas.map((s) => String(s).toLowerCase()).filter((s) => /^[0-9a-f]{40,64}$/.test(s))
|
|
123
|
-
return { known: list.length ? await store.gitShas(q.id, list) : {} }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Marks the import finished: the quilt takes ordinary merges from now on. */
|
|
127
|
-
async function importDone (me, ref) {
|
|
128
|
-
const q = await quiltFor(me, ref, 'admin')
|
|
129
|
-
const line = await store.getLine(q.id, 'main')
|
|
130
|
-
return { ok: true, tip: line.tip, patch_count: line.patch_count, tip_commit: line.tip ? (await getCommit(q.id, line.tip)).id : null }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { importBatch, importDone, importKnown }
|
|
134
|
-
}
|
package/src/quilts/index.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// Quilt source control, assembled: the server's operations over a store and an object store.
|
|
2
|
-
import { createQuiltService } from './service.js'
|
|
3
|
-
import { mergeOps } from './merge.js'
|
|
4
|
-
import { blameOps } from './blame.js'
|
|
5
|
-
import { importOps } from './import.js'
|
|
6
|
-
import { squareOps } from './squares.js'
|
|
7
|
-
import { eventOps } from './events.js'
|
|
8
|
-
import { exportOps } from './export.js'
|
|
9
|
-
import { createArchiver } from './archiver.js'
|
|
10
|
-
import { windowOps } from './window.js'
|
|
11
|
-
import { autoRevertOps } from './autorevert.js'
|
|
12
|
-
import { QuiltError } from './service.js'
|
|
13
|
-
import { lineOps } from './lines.js'
|
|
14
|
-
import { tagOps } from './tags.js'
|
|
15
|
-
import { claimOps } from './claims.js'
|
|
16
|
-
import { reviewOps } from './reviews.js'
|
|
17
|
-
import { resolutionOps } from './resolutions.js'
|
|
18
|
-
import { liveOps } from './live.js'
|
|
19
|
-
import { historyOps } from './history.js'
|
|
20
|
-
import { signingOps } from './signing.js'
|
|
21
|
-
import { tokenOps } from './tokens.js'
|
|
22
|
-
import { createLimiter } from './limits.js'
|
|
23
|
-
|
|
24
|
-
// Writes after which the events job runs (in the background; GET /events runs it too).
|
|
25
|
-
const WRITES = ['commit', 'amend', 'mergeIntoLine', 'rebase', 'cherryPick', 'revert', 'resolve', 'handoff', 'abandon', 'reopen', 'mergeSquare', 'fold', 'split', 'newLine', 'freezeLine', 'unfreezeLine', 'createTag', 'createRelease', 'reportCheck', 'review', 'setLinePolicy', 'reportSquareCheck']
|
|
26
|
-
|
|
27
|
-
export function createQuilts ({ store, objects, now = Date.now, events: eventOpts = {}, archive: archiveOpts = {}, mergeWindow: windowOpts = {}, rateLimits = {} }) {
|
|
28
|
-
const svc = createQuiltService({ store, objects, now })
|
|
29
|
-
// The merge window (07): `mergeWindow: false` turns the check off, `{ max }` sets the default.
|
|
30
|
-
const win = windowOps(svc, windowOpts || {})
|
|
31
|
-
if (windowOpts !== false) svc.internals.mergeWindow = win.checkWindow
|
|
32
|
-
const { guardReview, ...reviewApi } = reviewOps(svc)
|
|
33
|
-
svc.internals.guardReview = guardReview
|
|
34
|
-
const merges = mergeOps(svc)
|
|
35
|
-
const { mergeInternals, ...mergeApi } = merges
|
|
36
|
-
const squares = squareOps(svc, mergeInternals)
|
|
37
|
-
const ev = eventOps(svc, squares, eventOpts)
|
|
38
|
-
// The log archiver (03): after each write, in the background, writes the segments that are due.
|
|
39
|
-
const archiver = archiveOpts === false ? null : createArchiver({ store, objects, now, ...archiveOpts })
|
|
40
|
-
const { checkWindow, ...windowApi } = win
|
|
41
|
-
// Per-token rate limits (04), applied by the HTTP routes: `rateLimits: false` turns them off.
|
|
42
|
-
const rateLimiter = rateLimits === false ? null : createLimiter({ now, ...rateLimits })
|
|
43
|
-
const api = { archiver, rateLimiter, ...windowApi, ...svc, ...mergeApi, ...blameOps(svc), ...importOps(svc), ...exportOps(svc), ...lineOps(svc), ...tagOps(svc), ...claimOps(svc), ...reviewApi, ...resolutionOps(svc), ...tokenOps(svc), ...liveOps(svc), ...historyOps(svc, { mergeBase: squares.mergeBase }), ...squares, ...ev }
|
|
44
|
-
// Signed commits (signing.js): log { show_signature } checks each commit's signature.
|
|
45
|
-
const { withSignatures, ...signApi } = signingOps(svc)
|
|
46
|
-
Object.assign(api, signApi)
|
|
47
|
-
const plainLog = api.log
|
|
48
|
-
api.log = async (me, ref, opts = {}) => {
|
|
49
|
-
const { show_signature: show, ...rest } = opts
|
|
50
|
-
const r = await plainLog(me, ref, rest)
|
|
51
|
-
return show ? withSignatures(me, ref, r) : r
|
|
52
|
-
}
|
|
53
|
-
// A red check that names the patch it failed at reverts that patch by itself (07, autorevert.js).
|
|
54
|
-
const auto = autoRevertOps(svc, api)
|
|
55
|
-
const report = api.reportCheck
|
|
56
|
-
api.reportCheck = async (me, ref, line, body = {}) => {
|
|
57
|
-
const n = body.patch == null ? null : Number(body.patch)
|
|
58
|
-
if (n != null && (!Number.isInteger(n) || n < 1)) throw new QuiltError(400, 'bad_request', 'patch: the number of the patch on the line the check failed at')
|
|
59
|
-
const w = await report(me, ref, line, body)
|
|
60
|
-
if (n == null || body.status !== 'red') return w
|
|
61
|
-
return { ...w, revert: await auto.revertCulprit(me, ref, line, String(body.check).trim(), n) }
|
|
62
|
-
}
|
|
63
|
-
for (const name of WRITES) {
|
|
64
|
-
const fn = api[name]
|
|
65
|
-
api[name] = async (me, ref, ...rest) => {
|
|
66
|
-
const r = await fn(me, ref, ...rest)
|
|
67
|
-
svc.internals.quiltFor(me, ref).then((q) => { archiver?.maybe(q.id); mergeApi.sweepMerges(q.id).catch(() => {}); return ev.pumpEvents(q.id) }).catch(() => {})
|
|
68
|
-
return r
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return api
|
|
72
|
-
}
|
package/src/quilts/limits.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// Per-token rate limits (plans/quilt-source-control/04 "Rate limiting and load shedding",
|
|
2
|
-
// decision 40): counted per token, so an agent's key and the person it works for are limited
|
|
3
|
-
// apart; a person's token gets five times an agent's numbers (11). A limit answers at once with
|
|
4
|
-
// 429 rate_limited, `limit` naming its row and `retry_after` seconds; nothing waits.
|
|
5
|
-
// Counted in this API process (sliding windows of request times): with several API machines a
|
|
6
|
-
// token can reach up to one allowance per machine (NOT-DONE.md).
|
|
7
|
-
import crypto from 'node:crypto'
|
|
8
|
-
import { QuiltError } from './service.js'
|
|
9
|
-
|
|
10
|
-
const MIN = 60 * 1000
|
|
11
|
-
const HOUR = 60 * MIN
|
|
12
|
-
const DAY = 24 * HOUR
|
|
13
|
-
|
|
14
|
-
/** 04's rows for an agent token: [name, count, window ms, which requests]. */
|
|
15
|
-
export const LIMITS = [
|
|
16
|
-
['token_rate', 600, MIN, 'rpc'],
|
|
17
|
-
['commit_rate', 60, MIN, 'commit'],
|
|
18
|
-
['commit_rate', 2000, DAY, 'commit'],
|
|
19
|
-
['merge_rate', 30, HOUR, 'merge']
|
|
20
|
-
]
|
|
21
|
-
export const PERSON_FACTOR = 5
|
|
22
|
-
|
|
23
|
-
/** Which counters a request takes: object traffic (packs, blob and object reads) takes none. */
|
|
24
|
-
export function classify (method, url, body) {
|
|
25
|
-
const p = String(url).split('?')[0]
|
|
26
|
-
if (/\/(packs|objects|blobs)(\/|$)/.test(p)) return []
|
|
27
|
-
if (method === 'POST' && /\/squares\/[^/]+\/commit$/.test(p)) return ['rpc', 'commit']
|
|
28
|
-
if (method === 'POST' && /\/lines\/[^/]+\/merge$/.test(p)) return body && body.dry_run ? ['rpc'] : ['rpc', 'merge'] // a dry run is never merge-limited
|
|
29
|
-
return ['rpc']
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The token's key: a hash of its Authorization header (the token itself is never kept). */
|
|
33
|
-
export const tokenKey = (req, me) => {
|
|
34
|
-
const h = req && req.headers && req.headers.authorization
|
|
35
|
-
return h ? crypto.createHash('sha256').update(h).digest('hex').slice(0, 32) : `member:${me.member_id}:${me.kind}`
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createLimiter ({ now = Date.now, limits = LIMITS, personFactor = PERSON_FACTOR } = {}) {
|
|
39
|
-
const seen = new Map() // token + row index → request times, oldest first
|
|
40
|
-
|
|
41
|
-
/** Counts one request of `kinds` for `key`, or throws 429 rate_limited without counting it. */
|
|
42
|
-
function take (key, kinds, person) {
|
|
43
|
-
const t = now()
|
|
44
|
-
const rows = []
|
|
45
|
-
for (let i = 0; i < limits.length; i++) {
|
|
46
|
-
const [name, count, windowMs, kind] = limits[i]
|
|
47
|
-
if (!kinds.includes(kind)) continue
|
|
48
|
-
const max = person ? count * personFactor : count
|
|
49
|
-
const id = `${key}\u0000${i}`
|
|
50
|
-
let times = seen.get(id)
|
|
51
|
-
if (!times) { times = []; seen.set(id, times) }
|
|
52
|
-
let drop = 0
|
|
53
|
-
while (drop < times.length && times[drop] <= t - windowMs) drop++
|
|
54
|
-
if (drop) times.splice(0, drop)
|
|
55
|
-
if (times.length >= max) {
|
|
56
|
-
const retry = Math.max(1, Math.ceil((times[0] + windowMs - t) / 1000))
|
|
57
|
-
throw new QuiltError(429, 'rate_limited', `rate limit ${name}: ${max} in ${windowMs >= DAY ? 'a day' : windowMs >= HOUR ? 'an hour' : 'a minute'} for this token; try again in ${retry} s`, { limit: name, retry_after: retry })
|
|
58
|
-
}
|
|
59
|
-
rows.push(times)
|
|
60
|
-
}
|
|
61
|
-
for (const times of rows) times.push(t)
|
|
62
|
-
// Forget tokens with nothing left in any window, so the map stays the size of the active ones.
|
|
63
|
-
if (seen.size > 10000) for (const [id, times] of seen) if (!times.length || times[times.length - 1] <= t - DAY) seen.delete(id)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return { take }
|
|
67
|
-
}
|
package/src/quilts/lines.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Lines (plans/quilt-source-control/06): main, and release/<name> lines made from any revision.
|
|
2
|
-
// A square opens from any line and merges into any line; a frozen line refuses merges with
|
|
3
|
-
// line_frozen until it is unfrozen. Anyone who can write makes a line; an admin freezes one.
|
|
4
|
-
import { QuiltError } from './service.js'
|
|
5
|
-
|
|
6
|
-
const RELEASE = /^release\/[A-Za-z0-9._-]{1,100}$/
|
|
7
|
-
|
|
8
|
-
/** `1.0` or `release/1.0` → `release/1.0`; anything else is refused. */
|
|
9
|
-
export function lineName (raw) {
|
|
10
|
-
const s = String(raw || '').trim()
|
|
11
|
-
const name = s === 'main' || s.startsWith('release/') ? s : `release/${s}`
|
|
12
|
-
if (name !== 'main' && !RELEASE.test(name)) throw new QuiltError(400, 'bad_request', 'a new line is release/<name>: letters, digits, dots, dashes and underscores')
|
|
13
|
-
return name
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function lineOps (svc) {
|
|
17
|
-
const { store, now, quiltFor, resolve, fromStore } = svc.internals
|
|
18
|
-
|
|
19
|
-
const view = (l) => ({ name: l.name, tip: l.tip, patch_count: l.patch_count, frozen: !!l.frozen })
|
|
20
|
-
|
|
21
|
-
/** GET /lines: every line of the quilt, main first. */
|
|
22
|
-
async function lines (me, ref) {
|
|
23
|
-
const q = await quiltFor(me, ref, 'read')
|
|
24
|
-
const all = (await store.lines(q.id)).map(view)
|
|
25
|
-
return { lines: [...all.filter((l) => l.name === 'main'), ...all.filter((l) => l.name !== 'main')] }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** POST /lines { name, from? }: a new line at a revision (default the tip of main). */
|
|
29
|
-
async function newLine (me, ref, { name, from } = {}) {
|
|
30
|
-
const q = await quiltFor(me, ref, 'write')
|
|
31
|
-
const n = lineName(name)
|
|
32
|
-
if (n === 'main') throw new QuiltError(409, 'line_exists', 'there is already a line main')
|
|
33
|
-
const tip = await resolve(q.id, from || 'main')
|
|
34
|
-
if (!tip) throw new QuiltError(400, 'bad_request', `${from || 'main'} has no commits to start a line from`)
|
|
35
|
-
try { return view(await store.createLine({ quilt_id: q.id, name: n, tip, actor: me.member_id, at: now() })) } catch (err) { throw fromStore(err) }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function setFrozen (me, ref, line, frozen) {
|
|
39
|
-
const q = await quiltFor(me, ref, 'admin')
|
|
40
|
-
try { return view(await store.setLineFrozen({ quilt_id: q.id, name: String(line), frozen, actor: me.member_id, at: now() })) } catch (err) { throw fromStore(err) }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
lines,
|
|
45
|
-
newLine,
|
|
46
|
-
freezeLine: (me, ref, line) => setFrozen(me, ref, line, true),
|
|
47
|
-
unfreezeLine: (me, ref, line) => setFrozen(me, ref, line, false)
|
|
48
|
-
}
|
|
49
|
-
}
|
package/src/quilts/live.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// A square's live work (02, sq:<id>:live): the uncommitted changes its holder shares on the server,
|
|
2
|
-
// so others (people, agents, CI) can read work in progress before it is committed. It is whole
|
|
3
|
-
// files (each put or delete replaces what the square's head has), replaced at once by each PUT,
|
|
4
|
-
// and never history: a commit is still how work is kept. Any host can feed it, a block folder
|
|
5
|
-
// (`seam live share`), an editor or a live-editing service, through the same route.
|
|
6
|
-
import { QuiltError, squareShort } from './service.js'
|
|
7
|
-
import { guardClaims } from './claims.js'
|
|
8
|
-
|
|
9
|
-
export function liveOps (svc) {
|
|
10
|
-
const { store, now, quiltFor, squareByRef, writable, prepareChanges, seal, snapshot, reader, fromStore } = svc.internals
|
|
11
|
-
|
|
12
|
-
// Only what still differs from the square's head: a change committed since drops out.
|
|
13
|
-
async function view (q, sq, row) {
|
|
14
|
-
const head = sq.head || sq.base
|
|
15
|
-
const root = await snapshot(q, head)
|
|
16
|
-
const changes = []
|
|
17
|
-
for (const c of row?.changes || []) {
|
|
18
|
-
const e = await reader(q).lookup(root, c.path)
|
|
19
|
-
if (c.op === 'delete' ? !e : (e && e.blob === c.blob && e.mode === c.mode)) continue
|
|
20
|
-
changes.push({ path: c.path, op: c.op, ...(c.op === 'put' ? { blob: c.blob, mode: c.mode, size: c.size, status: e ? 'modified' : 'added' } : { status: 'deleted' }) })
|
|
21
|
-
}
|
|
22
|
-
return { square: squareShort(sq.id), square_id: sq.id, head, made_on: row?.head || null, changes, by: row?.actor || null, at: row?.at || null }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** GET /squares/:id/live: the square's shared uncommitted changes. */
|
|
26
|
-
async function live (me, ref, sqRef) {
|
|
27
|
-
const q = await quiltFor(me, ref, 'read')
|
|
28
|
-
const sq = await squareByRef(q.id, sqRef)
|
|
29
|
-
return view(q.id, sq, await store.getLive(q.id, sq.id))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** PUT /squares/:id/live { changes: [{ path, op: 'put', content (base64) | blob, mode? } | { path, op: 'delete' }] }: replaces it; [] clears. */
|
|
33
|
-
async function setLive (me, ref, sqRef, body = {}) {
|
|
34
|
-
const q = await quiltFor(me, ref, 'write')
|
|
35
|
-
const sq = await writable(me, q, sqRef)
|
|
36
|
-
if (sq.state !== 'open') throw new QuiltError(409, 'square_closed', `the square is ${sq.state}`)
|
|
37
|
-
if (!Array.isArray(body.changes || [])) throw new QuiltError(400, 'bad_request', 'changes is a list')
|
|
38
|
-
if ((body.changes || []).length > 5000) throw new QuiltError(400, 'bad_request', 'up to 5000 changes')
|
|
39
|
-
const prep = await prepareChanges(q.id, sq, body.changes || [])
|
|
40
|
-
await guardClaims(store, q.id, sq, prep.changes.map((x) => x.path))
|
|
41
|
-
if (prep.blobs.length) await seal(q.id, prep.blobs, { by: me.member_id })
|
|
42
|
-
const changes = prep.changes.map(({ copy_from: _, ...c }) => c)
|
|
43
|
-
try {
|
|
44
|
-
const row = await store.setLive({ quilt_id: q.id, square_id: sq.id, head: sq.head || sq.base, changes, actor: me.member_id, at: now() })
|
|
45
|
-
return view(q.id, sq, row)
|
|
46
|
-
} catch (err) { throw fromStore(err) }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { live, setLive }
|
|
50
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// The merge ladder seam settles a clash with (plans/quilt-source-control/07, steps 1 and 2): line
|
|
2
|
-
// by line as git does, then two inserts at one place, a one-line import both sides changed,
|
|
3
|
-
// whitespace-only changes, and word by word within the clashing lines. Seam's own copy, so the
|
|
4
|
-
// Quilt app's merges can change without changing how a quilt merges.
|
|
5
|
-
import { diff3Merge } from 'node-diff3'
|
|
6
|
-
|
|
7
|
-
const lines = (s) => s.split('\n')
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A text's line endings: 'crlf' (every line ends \r\n), 'lf' (none does), 'mixed', or null (one line:
|
|
11
|
-
* nothing to tell).
|
|
12
|
-
*/
|
|
13
|
-
function eolOf (s) {
|
|
14
|
-
if (typeof s !== 'string' || !s.includes('\n')) return null
|
|
15
|
-
if (!s.includes('\r\n')) return 'lf'
|
|
16
|
-
return s.replace(/\r\n/g, '').includes('\n') ? 'mixed' : 'crlf'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The merges compare lines, so a side saved with Windows line endings (\r\n) against a base and other
|
|
21
|
-
* side with \n differs on every line: diff3 then reads it as "ours deleted the whole file and inserted
|
|
22
|
-
* it again", the delete loses to theirs' edits and the insert stays, and the file comes out as both
|
|
23
|
-
* versions one after the other (task 7b480c8b07c4fa02: web/lib/docs.js, CRLF old + LF new, every
|
|
24
|
-
* export twice). When the sides' line endings differ, all three are merged with \n and the result is
|
|
25
|
-
* written in the line endings of the side that changed them from the base (ours when both did, or
|
|
26
|
-
* when there is no base to tell), as any other edit to the file would be kept.
|
|
27
|
-
*/
|
|
28
|
-
function eolAware (merge) {
|
|
29
|
-
return function (base, ours, theirs, ...rest) {
|
|
30
|
-
const eols = [base, ours, theirs].map(eolOf)
|
|
31
|
-
const seen = new Set(eols.filter(Boolean))
|
|
32
|
-
if (!seen.has('crlf') || seen.size < 2) return merge(base, ours, theirs, ...rest)
|
|
33
|
-
const [eb, eo, et] = eols
|
|
34
|
-
const target = (eo && eo !== eb) ? eo : (et && et !== eb) ? et : (eo || et || eb)
|
|
35
|
-
const lf = (s) => typeof s === 'string' ? s.replace(/\r\n/g, '\n') : s
|
|
36
|
-
const back = (t) => target === 'crlf' && typeof t === 'string' ? t.replace(/\n/g, '\r\n') : t
|
|
37
|
-
const r = merge(lf(base), lf(ours), lf(theirs), ...rest)
|
|
38
|
-
return typeof r === 'string' ? back(r) : { ...r, text: back(r.text) }
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Two blocks inserted at one place, as one: shared leading and trailing lines once, the middles in order. */
|
|
43
|
-
function addedBoth (a, b) {
|
|
44
|
-
let pre = 0
|
|
45
|
-
while (pre < a.length && pre < b.length && a[pre] === b[pre]) pre++
|
|
46
|
-
let post = 0
|
|
47
|
-
while (post < a.length - pre && post < b.length - pre && a[a.length - 1 - post] === b[b.length - 1 - post]) post++
|
|
48
|
-
return [...a.slice(0, pre), ...a.slice(pre, a.length - post), ...b.slice(pre, b.length - post), ...a.slice(a.length - post)]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const IMPORT = /^(\s*import\s*\{)([^}]*)(\}\s*from\s*(['"][^'"]+['"]).*)$/
|
|
52
|
-
|
|
53
|
-
/** Both sides changed the same one-line import of the same module: the union of its names, or null. */
|
|
54
|
-
function sameImport (o, a, b) {
|
|
55
|
-
if (o.length !== 1 || a.length !== 1 || b.length !== 1) return null
|
|
56
|
-
const [mo, ma, mb] = [IMPORT.exec(o[0]), IMPORT.exec(a[0]), IMPORT.exec(b[0])]
|
|
57
|
-
if (!mo || !ma || !mb || mo[4] !== ma[4] || ma[4] !== mb[4]) return null
|
|
58
|
-
const names = (m) => m[2].split(',').map((s) => s.trim()).filter(Boolean)
|
|
59
|
-
const [no, na, nb] = [names(mo), names(ma), names(mb)]
|
|
60
|
-
const gone = new Set(no.filter((n) => !na.includes(n) || !nb.includes(n)))
|
|
61
|
-
const all = [...na, ...nb.filter((n) => !na.includes(n))].filter((n) => !gone.has(n))
|
|
62
|
-
return [`${ma[1]} ${all.join(', ')} ${ma[3]}`]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const normWs = (l) => l.replace(/\s+/g, ' ').trim()
|
|
66
|
-
const sameIgnoringWs = (a, b) => a.length === b.length && a.every((l, i) => normWs(l) === normWs(b[i]))
|
|
67
|
-
const TOKEN = /\s+|\w+|[^\w\s]/g
|
|
68
|
-
|
|
69
|
-
/** Two blocks of lines merged word by word against their base, or null when the same words changed on both sides. */
|
|
70
|
-
function mergeWords (o, a, b) {
|
|
71
|
-
const tok = (ls) => ls.join('\n').match(TOKEN) || []
|
|
72
|
-
const regions = diff3Merge(tok(a), tok(o), tok(b), { excludeFalseConflicts: true })
|
|
73
|
-
if (regions.some((r) => !r.ok)) return null
|
|
74
|
-
return regions.map((r) => r.ok.join('')).join('').split('\n')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The clash ladder's mechanical and structural steps (plan §7.3, steps 1 and 2): mergeAdditive, then
|
|
79
|
-
* each region still clashing is tried again ignoring whitespace (a side that only reindented loses
|
|
80
|
-
* to the side that changed something), then word by word within the clashing lines (two edits to
|
|
81
|
-
* different words of one line both land); a file marked merge=union takes both sides, ours first.
|
|
82
|
-
* Returns { text, conflicts, how } with `how` the step that settled it: 'lines', 'whitespace',
|
|
83
|
-
* 'words' or 'union'; `conflicts` lists what no step could settle (text then holds ours there).
|
|
84
|
-
*/
|
|
85
|
-
export const mergeSmart = eolAware(function mergeSmart (base, ours, theirs, { union = false } = {}) {
|
|
86
|
-
const regions = diff3Merge(lines(ours), lines(base), lines(theirs), { excludeFalseConflicts: true })
|
|
87
|
-
const out = []
|
|
88
|
-
const conflicts = []
|
|
89
|
-
let how = 'lines'
|
|
90
|
-
const rank = { lines: 0, whitespace: 1, words: 2, union: 3 }
|
|
91
|
-
const step = (h) => { if (rank[h] > rank[how]) how = h }
|
|
92
|
-
for (const r of regions) {
|
|
93
|
-
if (r.ok) { out.push(...r.ok); continue }
|
|
94
|
-
const c = r.conflict
|
|
95
|
-
const settled = ladder(c, { union })
|
|
96
|
-
if (settled) { out.push(...settled.lines); step(settled.how); continue }
|
|
97
|
-
conflicts.push({ base: c.o, ours: c.a, theirs: c.b })
|
|
98
|
-
out.push(...c.a)
|
|
99
|
-
}
|
|
100
|
-
return { text: out.join('\n'), conflicts, how }
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
/** mergeSmart's steps for one region both sides changed ({ o, a, b }): { lines, how }, or null when none settles it. */
|
|
104
|
-
function ladder (c, { union = false } = {}) {
|
|
105
|
-
if (union) return { lines: [...c.a, ...c.b], how: 'union' }
|
|
106
|
-
const both = c.o.length === 0 ? addedBoth(c.a, c.b) : sameImport(c.o, c.a, c.b)
|
|
107
|
-
if (both) return { lines: both, how: 'lines' }
|
|
108
|
-
if (sameIgnoringWs(c.a, c.b)) return { lines: c.a, how: 'whitespace' } // the same change, spaced differently
|
|
109
|
-
if (sameIgnoringWs(c.a, c.o)) return { lines: c.b, how: 'whitespace' } // ours only reindented
|
|
110
|
-
if (sameIgnoringWs(c.b, c.o)) return { lines: c.a, how: 'whitespace' } // theirs only reindented
|
|
111
|
-
const words = mergeWords(c.o, c.a, c.b)
|
|
112
|
-
if (words) return { lines: words, how: 'words' }
|
|
113
|
-
return null
|
|
114
|
-
}
|