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.
Files changed (129) hide show
  1. package/RELEASES.md +292 -17
  2. package/bin/quilt.js +72 -5
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -1
  5. package/src/agent-task-workflow.js +2 -2
  6. package/src/agents/common.js +13 -0
  7. package/src/ai-vm.js +9 -1
  8. package/src/api/activity.js +5 -0
  9. package/src/api/memory-store.js +21 -10
  10. package/src/api/notify.js +134 -0
  11. package/src/api/routes/browsers.js +29 -0
  12. package/src/api/routes/github.js +0 -13
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/seam-connect.js +103 -0
  15. package/src/api/routes/session-migrate.js +39 -0
  16. package/src/api/routes/sessions.js +73 -1
  17. package/src/api/server.js +46 -12
  18. package/src/api/supabase-store.js +53 -4
  19. package/src/autowork.js +182 -16
  20. package/src/connection.js +39 -9
  21. package/src/control.js +139 -13
  22. package/src/force-date.js +27 -0
  23. package/src/fsutil.js +13 -3
  24. package/src/gitstate.js +38 -2
  25. package/src/history.js +25 -1
  26. package/src/mcp.js +93 -7
  27. package/src/merge3.js +11 -3
  28. package/src/now-summary.js +113 -0
  29. package/src/pathrules.js +12 -0
  30. package/src/protocol.js +10 -2
  31. package/src/quilt-default.js +84 -0
  32. package/src/quilt-history.js +106 -0
  33. package/src/quilt-remote.js +78 -0
  34. package/src/relay-commit.js +5 -0
  35. package/src/relay-lease.js +6 -2
  36. package/src/relay-mcp.js +172 -8
  37. package/src/relay-outbound.js +1 -1
  38. package/src/relay-prune.js +4 -1
  39. package/src/release-words.js +2 -2
  40. package/src/runner.js +20 -7
  41. package/src/seam-access.js +97 -0
  42. package/src/seam-claims.js +53 -0
  43. package/src/seam-url.js +15 -0
  44. package/src/server.js +1767 -33
  45. package/src/session-follow.js +515 -0
  46. package/src/session-layout3.js +466 -0
  47. package/src/session-migrate.js +123 -0
  48. package/src/session-quilt.js +145 -0
  49. package/src/session-seam.js +284 -0
  50. package/src/session.js +899 -46
  51. package/src/source-live/bring-in.js +283 -0
  52. package/src/source-live/live-block.js +507 -35
  53. package/src/source-live/live-square.js +17 -6
  54. package/src/status.js +5 -0
  55. package/src/ui/agent-guide.js +1 -1
  56. package/src/ui/app.css +85 -20
  57. package/src/ui/app.js +4 -1
  58. package/src/ui/chat.js +9 -3
  59. package/src/ui/cloud-ai-model.js +13 -0
  60. package/src/ui/cloud-ai.js +10 -2
  61. package/src/ui/common.js +6 -2
  62. package/src/ui/fileview.js +20 -7
  63. package/src/ui/force-words.js +19 -0
  64. package/src/ui/git-rule.js +9 -0
  65. package/src/ui/home.js +0 -147
  66. package/src/ui/loom-model.js +15 -1
  67. package/src/ui/quiltview.js +213 -0
  68. package/src/ui/seam-model.js +251 -0
  69. package/src/ui/session.js +413 -31
  70. package/src/ui/voice-call.js +83 -14
  71. package/src/ui/voice-model.js +39 -0
  72. package/src/ui/voice.js +167 -17
  73. package/src/ui-server.js +80 -28
  74. package/src/update-check.js +25 -1
  75. package/src/voice.js +13 -6
  76. package/src/git.js +0 -147
  77. package/src/quilts/AGENTS.md +0 -1
  78. package/src/quilts/CLAUDE.md +0 -1
  79. package/src/quilts/archiver.js +0 -175
  80. package/src/quilts/autorevert.js +0 -49
  81. package/src/quilts/bisect-run.js +0 -31
  82. package/src/quilts/blame.js +0 -103
  83. package/src/quilts/block.js +0 -347
  84. package/src/quilts/bucket.js +0 -25
  85. package/src/quilts/cbor.js +0 -92
  86. package/src/quilts/claims.js +0 -144
  87. package/src/quilts/cli.js +0 -449
  88. package/src/quilts/client.js +0 -128
  89. package/src/quilts/conflict-format.js +0 -190
  90. package/src/quilts/describe.js +0 -266
  91. package/src/quilts/dogfood.js +0 -82
  92. package/src/quilts/errors.js +0 -8
  93. package/src/quilts/events.js +0 -160
  94. package/src/quilts/export-git.js +0 -130
  95. package/src/quilts/export.js +0 -81
  96. package/src/quilts/fastmerge.js +0 -41
  97. package/src/quilts/hash.js +0 -44
  98. package/src/quilts/history.js +0 -266
  99. package/src/quilts/ids.js +0 -12
  100. package/src/quilts/import-git.js +0 -291
  101. package/src/quilts/import.js +0 -134
  102. package/src/quilts/index.js +0 -72
  103. package/src/quilts/limits.js +0 -67
  104. package/src/quilts/lines.js +0 -49
  105. package/src/quilts/live.js +0 -50
  106. package/src/quilts/merge-smart.js +0 -114
  107. package/src/quilts/merge.js +0 -486
  108. package/src/quilts/objects.js +0 -144
  109. package/src/quilts/pack.js +0 -118
  110. package/src/quilts/records.js +0 -211
  111. package/src/quilts/resolutions.js +0 -69
  112. package/src/quilts/reviews.js +0 -195
  113. package/src/quilts/routes.js +0 -126
  114. package/src/quilts/service.js +0 -923
  115. package/src/quilts/setup.js +0 -34
  116. package/src/quilts/signing.js +0 -149
  117. package/src/quilts/squares.js +0 -239
  118. package/src/quilts/store-memory.js +0 -683
  119. package/src/quilts/store-rpc.js +0 -79
  120. package/src/quilts/store-supabase.js +0 -13
  121. package/src/quilts/tags.js +0 -64
  122. package/src/quilts/textdiff.js +0 -96
  123. package/src/quilts/tokens.js +0 -66
  124. package/src/quilts/tools.js +0 -375
  125. package/src/quilts/tree.js +0 -351
  126. package/src/quilts/webhook.js +0 -116
  127. package/src/quilts/window.js +0 -119
  128. package/src/quilts/xdiff.js +0 -435
  129. package/src/ui/branches.js +0 -111
@@ -1,190 +0,0 @@
1
- // 07's default conflict materialisation, `diff` (Jujutsu's style): inside each marker block one
2
- // side as a snapshot (`+++++++`), then each other side as a diff from its base (`%%%%%%%`). With
3
- // two sides (diff3), more sides on one base (an N-way merge, `nway`) or on different bases (each
4
- // side's change laid onto the first side, `ontoFirst`) only the clashing regions are marked and the
5
- // rest of the file is merged text; a deleted side is an empty side labelled "(deleted by <who>)". Also the `git` format (diff3 markers with the `|||||||` base section, two sides
6
- // only). Every block's first line carries the term's tag, so a
7
- // file committed with these markers is still conflicted (service.js, block.js).
8
- import { diff3Merge } from 'node-diff3'
9
- import { hunks, splitLines } from './textdiff.js'
10
-
11
- /** `old` → `now` as `-`/`+`/` ` lines (every line of both, in order). */
12
- export function lineDiff (old, now) {
13
- const out = []
14
- let i = 0
15
- let j = 0
16
- for (const h of hunks(old, now)) {
17
- while (i < h.a[0]) { out.push(' ' + old[i++]); j++ }
18
- for (let k = 0; k < h.a[1]; k++) out.push('-' + old[i++])
19
- for (let k = 0; k < h.b[1]; k++) out.push('+' + now[j++])
20
- }
21
- while (i < old.length) { out.push(' ' + old[i++]); j++ }
22
- return out
23
- }
24
-
25
- /**
26
- * The term as `diff` markers. `sides` are [{ text, label, by }] (adds, text null for a delete);
27
- * `bases` the removes' texts (bases[i] is the base of sides[i + 1]). `tag` is
28
- * `quilt-conflict <hash>`, `kind` the term kind.
29
- */
30
- export function diffMarkers ({ sides, bases, tag, kind }) {
31
- const L = (t) => splitLines(t || '')
32
- // Ruling (2026-10-24): a side that deleted the file, or the region, is an empty side labelled so.
33
- const name = (s, lines, base) => s.text == null || (!lines.length && base && base.length) ? `${s.label} (deleted by ${s.by || 'someone'})` : s.label
34
- const block = (snap, others) => [
35
- `<<<<<<< ${tag} ${kind}`,
36
- `+++++++ ${name(snap.side, snap.lines, snap.base)}`, ...snap.lines,
37
- ...others.flatMap((o) => [`%%%%%%% ${name(o.side, o.lines, o.base)} (diff from base)`, ...lineDiff(o.base, o.lines)]),
38
- `>>>>>>> ${tag}`
39
- ]
40
- const whole = () => block({ side: sides[0], lines: L(sides[0].text), base: L(bases[0]) },
41
- sides.slice(1).map((s, i) => ({ side: s, base: L(bases[i]), lines: L(s.text) })))
42
- if (sides.length < 2) return whole().join('\n') + '\n'
43
- const out = []
44
- let marked = false
45
- if (sides.length === 2) {
46
- for (const r of diff3Merge(L(sides[0].text), L(bases[0]), L(sides[1].text), { excludeFalseConflicts: true })) {
47
- if (r.ok) { out.push(...r.ok); continue }
48
- const c = r.conflict
49
- marked = true
50
- out.push(...block({ side: sides[0], lines: c.a, base: c.o }, [{ side: sides[1], base: c.o, lines: c.b }]))
51
- }
52
- } else {
53
- const regions = bases.some((b) => (b || '') !== (bases[0] || '')) ? ontoFirst(L(sides[0].text), sides.slice(1).map((s, i) => ({ base: L(bases[i]), lines: L(s.text) }))) : null
54
- if (regions) {
55
- for (const r of regions) {
56
- if (r.ok) { out.push(...r.ok); continue }
57
- marked = true
58
- out.push(...block({ side: sides[0], lines: r.snap }, sides.slice(1).map((s, i) => ({ side: s, base: r.others[i].base, lines: r.others[i].lines }))))
59
- }
60
- } else for (const r of nway(L(bases[0]), sides.map((s) => L(s.text)))) {
61
- if (r.ok) { out.push(...r.ok); continue }
62
- marked = true
63
- out.push(...block({ side: sides[0], lines: r.sides[0], base: r.base }, sides.slice(1).map((s, i) => ({ side: s, base: r.base, lines: r.sides[i + 1] }))))
64
- }
65
- }
66
- return (marked ? out : whole()).join('\n') + '\n'
67
- }
68
-
69
- /**
70
- * An N-way line merge of `sides` (arrays of lines) that all start from `base`: regions
71
- * `{ ok: lines }` where at most one distinct change touches the base lines, and
72
- * `{ base, sides }` where two sides changed the same (or touching) base lines differently.
73
- */
74
- export function nway (base, sides) {
75
- const all = []
76
- sides.forEach((lines, s) => { for (const h of hunks(base, lines)) all.push({ s, a0: h.a[0], a1: h.a[0] + h.a[1], d: h.b[1] - h.a[1] }) })
77
- all.sort((x, y) => x.a0 - y.a0 || x.a1 - y.a1)
78
- const groups = []
79
- for (const h of all) {
80
- const g = groups[groups.length - 1]
81
- if (g && h.a0 <= g.hi) { g.hi = Math.max(g.hi, h.a1); g.hunks.push(h) } else groups.push({ lo: h.a0, hi: h.a1, hunks: [h] })
82
- }
83
- const shift = new Array(sides.length).fill(0) // each side's line offset before the current group
84
- const out = []
85
- let at = 0
86
- const ok = (lines) => { if (lines.length) out.push({ ok: lines }) }
87
- for (const g of groups) {
88
- ok(base.slice(at, g.lo))
89
- const changed = new Set(g.hunks.map((h) => h.s))
90
- const slice = sides.map((lines, s) => {
91
- const d = g.hunks.filter((h) => h.s === s).reduce((n, h) => n + h.d, 0)
92
- const part = lines.slice(g.lo + shift[s], g.hi + shift[s] + d)
93
- shift[s] += d
94
- return part
95
- })
96
- const versions = new Set([...changed].map((s) => slice[s].join('\n') + '\u0000' + slice[s].length))
97
- if (versions.size === 1) ok(slice[[...changed][0]])
98
- else out.push({ base: base.slice(g.lo, g.hi), sides: slice })
99
- at = g.hi
100
- }
101
- ok(base.slice(at))
102
- return out
103
- }
104
-
105
- /** Maps line positions of `a` to `b` through their hunks; inside a changed run (or on an insertion), `lo` snaps to its start and `hi` to its end. */
106
- function mapper (a, b) {
107
- const hs = hunks(a, b)
108
- const at = (p, end) => {
109
- let off = 0
110
- for (const h of hs) {
111
- const [a0, an] = h.a
112
- if (end && p === a0 && an === 0) return h.b[0] + h.b[1] // an end on an insertion takes it in
113
- if (p <= a0) return p + off
114
- if (p < a0 + an) return end ? h.b[0] + h.b[1] : h.b[0]
115
- off += h.b[1] - an
116
- }
117
- return p + off
118
- }
119
- return { hunks: hs, lo: (p) => at(p, false), hi: (p) => at(p, true) }
120
- }
121
-
122
- /**
123
- * Sides on different bases, each laid onto the first side (the snapshot): a side's change lands
124
- * where its base lines sit in the first side. Regions `{ ok: lines }` where at most one change
125
- * lands and it does not touch lines the first side changed from that base, and `{ snap, others }`
126
- * (each other side's base and text over the region) where they clash.
127
- */
128
- export function ontoFirst (first, others) {
129
- const regions = []
130
- others.forEach((o, i) => {
131
- const toFirst = mapper(o.base, first)
132
- const toSide = mapper(o.base, o.lines)
133
- for (const h of toSide.hunks) {
134
- let x0 = h.a[0]; let x1 = h.a[0] + h.a[1]; let clash = false
135
- for (let grew = true; grew;) {
136
- grew = false
137
- for (const c of toFirst.hunks) {
138
- const c0 = c.a[0]; const c1 = c.a[0] + c.a[1]
139
- if (c0 > x1 || x0 > c1) continue
140
- clash = true
141
- if (c0 < x0 || c1 > x1) { x0 = Math.min(x0, c0); x1 = Math.max(x1, c1); grew = true }
142
- }
143
- }
144
- regions.push({ i, lo: toFirst.lo(x0), hi: toFirst.hi(x1), clash, text: o.lines.slice(toSide.lo(x0), toSide.hi(x1)) })
145
- }
146
- })
147
- regions.sort((x, y) => x.lo - y.lo || x.hi - y.hi)
148
- const groups = []
149
- for (const r of regions) {
150
- const g = groups[groups.length - 1]
151
- if (g && r.lo <= g.hi) { g.hi = Math.max(g.hi, r.hi); g.rs.push(r) } else groups.push({ lo: r.lo, hi: r.hi, rs: [r] })
152
- }
153
- const out = []
154
- let at = 0
155
- const ok = (lines) => { if (lines.length) out.push({ ok: lines }) }
156
- for (const g of groups) {
157
- ok(first.slice(at, g.lo))
158
- const same = g.rs.every((r) => !r.clash && r.lo === g.lo && r.hi === g.hi && r.text.join('\n') === g.rs[0].text.join('\n') && r.text.length === g.rs[0].text.length)
159
- if (same) ok(g.rs[0].text)
160
- else {
161
- out.push({
162
- snap: first.slice(g.lo, g.hi),
163
- others: others.map((o) => {
164
- const back = mapper(first, o.base)
165
- const fwd = mapper(o.base, o.lines)
166
- const [b0, b1] = [back.lo(g.lo), back.hi(g.hi)]
167
- return { base: o.base.slice(b0, b1), lines: o.lines.slice(fwd.lo(b0), fwd.hi(b1)) }
168
- })
169
- })
170
- }
171
- at = g.hi
172
- }
173
- ok(first.slice(at))
174
- return out
175
- }
176
-
177
- /** The term as git's diff3 markers (07's `git` format): two sides, with the `|||||||` base. */
178
- export function gitMarkers ({ ours, base, theirs, tag }) {
179
- const L = (t) => splitLines(t || '')
180
- const name = (s, lines, o) => s.text == null || (!lines.length && o.length) ? `${s.label} (deleted by ${s.by || 'someone'})` : s.label
181
- const block = (a, o, b) => [`<<<<<<< ${name(ours, a, o)} ${tag}`, ...a, `||||||| base ${tag}`, ...o, '=======', ...b, `>>>>>>> ${name(theirs, b, o)} ${tag}`]
182
- const out = []
183
- let marked = false
184
- for (const r of diff3Merge(L(ours.text), L(base), L(theirs.text), { excludeFalseConflicts: true })) {
185
- if (r.ok) { out.push(...r.ok); continue }
186
- marked = true
187
- out.push(...block(r.conflict.a, r.conflict.o, r.conflict.b))
188
- }
189
- return (marked ? out : block(L(ours.text), L(base), L(theirs.text))).join('\n') + '\n'
190
- }
@@ -1,266 +0,0 @@
1
- // The server's records as plain English (plans/quilt-source-control/12, "Every write returns the
2
- // server's view"): one rendering for the CLI and both MCPs, so every tool says the same thing.
3
-
4
- const when = (ms) => (ms ? new Date(ms).toISOString().replace('T', ' ').slice(0, 16) + ' UTC' : '')
5
- const first = (m) => String(m || '').split('\n')[0]
6
- const patchName = (p) => (p ? `patch ${p.line}:${p.patch_no}` : '')
7
-
8
- export function describeSquare (s) {
9
- const bits = [`${s.short} "${s.title}"`, s.state]
10
- if (s.state === 'merged' && s.merged) bits.push(`as ${patchName(s.merged)}`)
11
- if (s.state === 'open') {
12
- bits.push(s.head ? `head ${s.head.slice(0, 12)}` : 'no commits yet')
13
- if (s.on) bits.push(`stacked on ${s.on}`)
14
- if (s.behind_by) bits.push(`${s.behind_by} patch${s.behind_by === 1 ? '' : 'es'} behind ${s.line}`)
15
- if (s.conflicted_paths) bits.push(`${s.conflicted_paths} conflicted path${s.conflicted_paths === 1 ? '' : 's'}`)
16
- }
17
- bits.push(`held by ${s.holder}`)
18
- return bits.join(' · ')
19
- }
20
-
21
- export function describeLog (r, { oneline = false } = {}) {
22
- if (!r.commits.length) return 'No commits.'
23
- const lines = r.commits.map((c) => oneline
24
- ? `${c.short} ${c.patch ? `[${c.patch.line}:${c.patch.patch_no}] ` : ''}${first(c.message)}${r.follow && c.path && c.path !== r.follow ? ` (${c.path})` : ''}${c.signature ? ` (signature: ${c.signature.status})` : ''}`
25
- : [`commit ${c.id}${c.patch ? ` (${patchName(c.patch)})` : ''}`, `Square: ${c.square}`, ...(r.follow && c.path && c.path !== r.follow ? [`Path: ${c.path}`] : []), `Author: ${c.author.name} <${c.author.member_id}>${c.author.tool ? ` via ${c.author.tool}` : ''}`, ...(c.signature ? [describeSignature(c.signature)] : []), `Date: ${when(c.authored_at)}`, '', ...String(c.message || '').split('\n').map((l) => ` ${l}`), ''].join('\n'))
26
- if (r.more) lines.push(oneline ? '…' : '(more: seam log -n <count>)')
27
- return lines.join('\n')
28
- }
29
-
30
- export function describeDiff (d, { format = 'unified' } = {}) {
31
- if (!d.files.length) return 'No differences.'
32
- if (format === 'names') return d.files.map((f) => `${f.status[0].toUpperCase()}\t${f.path}`).join('\n')
33
- if (format === 'stat') {
34
- const w = Math.max(...d.files.map((f) => f.path.length))
35
- return [...d.files.map((f) => ` ${f.path.padEnd(w)} | ${f.binary ? 'Bin' : `${(f.add || 0) + (f.del || 0)} ${'+'.repeat(Math.min(f.add || 0, 40))}${'-'.repeat(Math.min(f.del || 0, 40))}`}`), ` ${d.files.length} file${d.files.length === 1 ? '' : 's'} changed, ${d.add || 0} insertions(+), ${d.del || 0} deletions(-)`].join('\n')
36
- }
37
- return d.files.map((f) => f.binary ? `Binary file ${f.path} ${f.status}\n` : f.patch || `${f.path} ${f.status}\n`).join('')
38
- }
39
-
40
- export function describeShow (r, { format = 'unified' } = {}) {
41
- const c = r.commit
42
- const head = [`commit ${c.id}${c.patch ? ` (${patchName(c.patch)})` : ''}`]
43
- if (c.parents.length > 1) head.push(`Merge: ${c.parents.map((p) => p.slice(0, 12)).join(' ')}`)
44
- head.push(`Square: ${c.square}${r.square ? ` "${r.square.title}" (${r.square.state})` : ''}`)
45
- head.push(`Author: ${c.author.name} <${c.author.member_id}>${c.author.tool ? ` via ${c.author.tool}` : ''}`)
46
- head.push(`Date: ${when(c.authored_at)}`)
47
- if (c.predecessor) head.push(`Rewrites: ${c.predecessor.slice(0, 12)}`)
48
- if (c.conflicted) head.push(`Conflicted paths in this commit: ${c.conflicted}`)
49
- head.push('', ...String(c.message || '').split('\n').map((l) => ` ${l}`), '')
50
- return head.join('\n') + (r.diff ? '\n' + describeDiff(r.diff, { format }) : '')
51
- }
52
-
53
- export function describeBlame (r) {
54
- const w = Math.max(...r.lines.map((l) => (l.author || '').length), 4)
55
- return r.lines.map((l) => `${(l.short || '?').slice(0, 10)} ${l.patch ? `${l.patch.line}:${l.patch.patch_no}`.padEnd(9) : (l.square || '').padEnd(9)} (${(l.author || '?').padEnd(w)} ${when(l.at).slice(0, 10)} ${String(l.line).padStart(4)}) ${l.text}`).join('\n')
56
- }
57
-
58
- /** A square with 06's status fields: what merged under it and what would conflict on merge. */
59
- export function describeSquareStatus (s, { prefix = '' } = {}) {
60
- const lines = [prefix + describeSquare(s)]
61
- if (s.folded_into) lines.push(`Folded into ${s.folded_into}.`)
62
- const under = s.merged_under_you || []
63
- if (under.length) {
64
- lines.push(`Merged under you (patches since your base that changed your files)${s.merged_under_you_more ? ', newest 50' : ''}:`)
65
- for (const p of under) lines.push(` patch ${p.patch_no} by ${p.author}: ${p.message} (${p.paths.join(', ')})`)
66
- }
67
- const wc = s.would_conflict || []
68
- if (wc.length) lines.push(`Would conflict if merged now: ${wc.map((w) => `${w.path}${w.patch_no ? ` (with patch ${w.patch_no} by ${w.author})` : ''}`).join(', ')}. Rebase (seam rebase) to settle it in your square first.`)
69
- else if (under.length) lines.push('Would merge cleanly now.')
70
- return lines.join('\n')
71
- }
72
-
73
- export function describeStatus (st) {
74
- const lines = []
75
- if (st.square) lines.push(describeSquareStatus(st.square, { prefix: 'On square ' }))
76
- else lines.push(`A read-only checkout of ${st.at ? st.at.slice(0, 12) : 'an empty line'}`)
77
- lines.push(`Block: ${st.paths.map((p) => p || '/').join(', ')} at ${st.at ? st.at.slice(0, 12) : '(empty)'}`)
78
- if (st.stale) lines.push('The square moved on since this folder last looked: run seam checkout ' + st.square.short + ' after committing.')
79
- if (st.conflicted.length) lines.push(`Conflicted (edit and commit without the markers, or seam resolve --ours|--theirs): ${st.conflicted.join(', ')}`)
80
- if (!st.changes.length) lines.push('Nothing changed since the last commit.')
81
- else lines.push('Changes not yet committed:', ...st.changes.map((c) => ` ${c.status.padEnd(9)} ${c.path}`))
82
- return lines.join('\n')
83
- }
84
-
85
- export function describeWrite (r, verb = 'Done') {
86
- const c = r.result
87
- const bits = [`${verb}${c && c.id ? ` ${c.short}` : ''}${c && c.message ? ` "${first(c.message)}"` : ''}`]
88
- if (r.square) bits.push(`square ${describeSquare(r.square)}`)
89
- if (r.conflicted) bits.push(`${r.conflicted} conflicted path${r.conflicted === 1 ? '' : 's'}: ${(r.terms || []).map((t) => `${t.path} (${t.kind})`).join(', ')}`)
90
- const reused = (r.combined || []).filter((c) => c.how === 'reuse')
91
- if (reused.length) bits.push(`Settled the way it was settled before: ${reused.map((c) => `${c.path} (by ${c.by})`).join(', ')}`)
92
- return bits.join('\n')
93
- }
94
-
95
- export function describeMerge (r) {
96
- if (r.dry_run) return r.ok ? `Would merge as patch ${r.would_merge.line}:${r.would_merge.patch_no}${r.would_merge.fast ? ' (no file touched on both sides)' : ''}.` : `Would be refused: ${r.refused.detail}.`
97
- if (r.ok) {
98
- const lines = [`Merged as ${patchName(r.merged)} (${r.merged.commit.slice(0, 12)})${r.retries ? ` after ${r.retries} lost race${r.retries === 1 ? '' : 's'}` : ''}.`]
99
- const combined = (r.combined || []).filter((c) => c.how !== 'lines' || true)
100
- if (combined.length) lines.push(`Combined with changes already on ${r.merged.line}: ${combined.map((c) => c.how === 'reuse' ? `${c.path} (settled as ${c.by} settled it before)` : `${c.path} (${c.how})`).join(', ')}.`)
101
- for (const k of r.restacked || []) lines.push(k.error ? `${k.square} is stacked on it and could not be moved (${k.error}): its holder rebases it.` : `Moved ${k.square}, stacked on it, onto ${r.merged.line}${k.conflicted ? ` with ${k.conflicted} conflicted path${k.conflicted === 1 ? '' : 's'}` : ''}.`)
102
- return lines.join('\n')
103
- }
104
- const lines = [`Not merged: ${r.refused ? r.refused.detail : r.error}.`]
105
- if (r.terms && r.terms.length) {
106
- lines.push('Conflicts, now kept in the square:')
107
- for (const t of r.terms) lines.push(` ${t.path} (${t.kind})${t.sides ? `: ${t.sides.map((s) => s.by).join(' vs ')}` : ''}${t.suggestion ? ' (a word-level merge is offered: seam resolve --suggestion, or choose suggestion)' : ''}`)
108
- lines.push('Edit those files and commit them without the markers (or seam resolve <path> --ours|--theirs), then merge again.')
109
- }
110
- return lines.join('\n')
111
- }
112
-
113
- /** Events as one line each (12's inbox line: kind, who, square, paths, patch, would_conflict). */
114
- export function describeEvents (r) {
115
- if (!r.events.length) return `No new events (cursor ${r.after}).`
116
- const line = (e) => {
117
- const p = e.payload || {}
118
- if (e.kind === 'patch.merged') return `#${e.event_seq} patch.merged: patch ${e.patch_no} by ${e.by.member_id} "${first(p.message || '')}" changed ${e.paths.join(', ')} in your square ${e.square_short}${p.would_conflict ? `; would conflict in ${p.would_conflict_paths.join(', ')}` : '; merges cleanly'}`
119
- if (e.kind === 'conflict.added') return `#${e.event_seq} conflict.added: ${p.path} (${p.kind}) in ${e.square_short}${p.caused_by && p.caused_by.patch_no ? `, against patch ${p.caused_by.patch_no}` : ''}; settle it and merge again`
120
- if (e.kind === 'square.handoff') return `#${e.event_seq} square.handoff: ${e.square_short} from ${p.from} to ${p.to}${p.note ? `: ${p.note}` : ''}`
121
- return `#${e.event_seq} ${e.kind}`
122
- }
123
- return [...r.events.map(line), `(next: after=${r.after})`].join('\n')
124
- }
125
-
126
- /** One tag per line: name, commit, patch, and "release" when it is one. */
127
- export const describeTag = (x) => `${x.name} ${x.short}${x.patch ? ` ${x.patch.line}:${x.patch.patch_no}` : ''}${x.release ? ' release' : ''}`
128
- /** A release and its notes, indented under it. */
129
- export const describeRelease = (x) => `${x.name} ${x.short}${x.patch ? ` ${x.patch.line}:${x.patch.patch_no}` : ''} ${String(x.at && new Date(x.at).toISOString()).slice(0, 10)} ${x.by}` +
130
- (x.notes ? '\n' + x.notes.split('\n').map((l) => ` ${l}`).join('\n') : '')
131
-
132
- /** A git import's answer, the same words from the CLI and seam_import_git. */
133
- export function describeImport (r) {
134
- const said = [`${r.again ? 'Added' : 'Imported'} ${r.commits} commits (${r.patches} patches) into ${r.slug}.`]
135
- for (const l of r.lines) said.push(` ${l.name}${l.name !== l.git ? ` (git ${l.git})` : ''}: ${l.added} new patches`)
136
- if (r.tags) said.push(` tags: ${r.tags}`)
137
- for (const x of r.report) said.push(` left out: ${x.ref || x.path || x.sha.slice(0, 12)}: ${x.note}`)
138
- if (r.validated != null) said.push(`Validated ${r.validated} trees against git.`)
139
- return said.join('\n')
140
- }
141
-
142
- export function describeRestore (r) {
143
- return r.restored.length ? `Restored ${r.restored.length} file(s) to the last commit: ${r.restored.join(', ')}` : 'Nothing to restore: those files have no uncommitted edits.'
144
- }
145
-
146
- export function describeReset (r, block) {
147
- const n = r.written + r.removed
148
- const where = block.square ? 'the square' : block.line
149
- return `Reset ${n} file(s) to ${String(r.rev).slice(0, 12)}.` + (r.changes ? `\n${r.changes} change(s) from ${where} show as uncommitted (seam status); commit them to keep them, or seam reset to go back.` : '')
150
- }
151
-
152
- export function describeClean (r, again) {
153
- if (!r.removed.length) return 'Nothing to clean.'
154
- if (r.dry_run) return r.removed.map((p) => `Would remove ${p}`).join('\n') + `\n${again}`
155
- return `Removed ${r.removed.length} file(s): ${r.removed.join(', ')}`
156
- }
157
-
158
- export function describeLive (r) {
159
- if (!r.changes.length) return `No uncommitted work shared for ${r.square}: ${r.square}:live is its head.`
160
- return [`Uncommitted work in ${r.square}${r.by ? ` (shared by ${r.by})` : ''}, read as ${r.square}:live:`, ...r.changes.map((c) => ` ${c.status.padEnd(9)} ${c.path}`)].join('\n')
161
- }
162
-
163
- export function describeLiveSet (r) {
164
- return r.changes.length ? `Shared ${r.changes.length} uncommitted change(s) of ${r.square}: read them as ${r.square}:live.` : `Cleared the live work of ${r.square}.`
165
- }
166
-
167
- const at = (c) => `${c.short || String(c.id || c.commit).slice(0, 12)}${c.patch ? ` (${c.patch.line}:${c.patch.patch_no})` : ''}`
168
-
169
- export function describeMergeBase (r) {
170
- return r.commit ? `${r.commit}${r.patch ? ` (${r.patch.line}:${r.patch.patch_no})` : ''}` : 'No common history.'
171
- }
172
-
173
- export function describeGrep (r, { filesOnly = false } = {}) {
174
- if (!r.matches.length) return 'No matches.'
175
- const lines = filesOnly ? r.files : r.matches.map((m) => `${m.quilt ? m.quilt + ':' : ''}${m.path}:${m.line}:${m.text}`)
176
- if (r.more) lines.push('… (more: raise the limit, or narrow by paths)')
177
- if (r.skipped) lines.push(`(${r.skipped} large file(s) not searched)`)
178
- return lines.join('\n')
179
- }
180
-
181
- export function describeBisect (r) {
182
- if (r.done) {
183
- const said = [`First bad: ${at(r.first_bad)} ${first(r.first_bad.message)}`]
184
- if (r.maybe) said.push(`Or one of the skipped commits before it: ${r.maybe.map(at).join(', ')}`)
185
- return said.join('\n')
186
- }
187
- return `Test ${at(r.next)} ${first(r.next.message)}: ${r.remaining} commit(s) left, about ${r.steps} step(s). Mark it good, bad or skip and ask again.`
188
- }
189
-
190
- /** A list of claims, one per line. */
191
- export function describeClaims (claims) {
192
- if (!claims.length) return 'No claims.'
193
- return claims.map((c) => `${c.pattern} ${c.square} (${c.line}) ${c.holder}${c.note ? `: ${c.note}` : ''}`).join('\n')
194
- }
195
-
196
- export function describeCheck (r) {
197
- const lines = [`${r.line}: merge window ${r.window} an hour${r.red.length ? ` (red: ${r.red.join(', ')})` : ''}.`]
198
- const v = r.revert
199
- if (v && v.reverted) lines.push(`Reverted patch ${r.line}:${v.patch_no} as patch ${r.line}:${v.revert_patch_no}.${v.fix_square ? ` The change is again in square ${v.fix_square.short}, held by ${v.fix_square.holder}: fix it there, commit, merge.` : ''}`)
200
- else if (v) lines.push(`Did not revert patch ${r.line}:${v.patch_no}: ${(v.refused && (v.refused.detail || v.refused.code)) || 'refused'}.${v.square ? ` The revert waits in square ${v.square.short}.` : ''}`)
201
- return lines.join('\n')
202
- }
203
-
204
- /** Remembered resolutions, one per line. */
205
- export function describeResolutions (rows) {
206
- if (!rows.length) return 'No remembered resolutions.'
207
- return rows.map((r) => `${r.short} ${r.path} ${r.answer === 'delete' ? 'deleted' : `kept ${r.blob.slice(0, 12)}`} (${r.how}, ${r.by})`).join('\n')
208
- }
209
-
210
- /** Where a square stands in review: approvals against what its line needs. */
211
- function describeStanding (r) {
212
- const parts = [`${r.have} of ${r.need} approval${r.need === 1 ? '' : 's'}${r.approved_by.length ? ` (${r.approved_by.join(', ')})` : ''}`]
213
- if (r.changes_requested_by.length) parts.push(`changes asked for by ${r.changes_requested_by.join(', ')}`)
214
- for (const c of r.checks || []) if (c.required || c.status !== 'missing') parts.push(`check ${c.name} ${c.status}`)
215
- parts.push(r.ready ? 'ready to merge' : 'not ready to merge')
216
- return parts.join('; ')
217
- }
218
-
219
- /** A square's reviews, oldest first, then where it stands. */
220
- export function describeReviews (r) {
221
- const rows = r.reviews.map((x) => {
222
- const what = { approve: 'approved', request_changes: 'asked for changes', dismiss: 'dismissed their verdict', comment: 'commented' }[x.kind]
223
- const where = x.path ? ` on ${x.path}${x.line ? `:${x.line}` : ''}` : ''
224
- return `${x.author} ${what}${where}${x.outdated ? ' (an earlier commit)' : ''}${x.body ? `: ${x.body}` : ''}`
225
- })
226
- return `${r.square} on ${r.line}: ${describeStanding(r)}${rows.length ? '\n' + rows.join('\n') : '\nNo reviews yet.'}`
227
- }
228
-
229
- /** One review just given. */
230
- export function describeReviewed (r) {
231
- const x = r.review
232
- const what = { approve: 'Approved', request_changes: 'Asked for changes on', dismiss: 'Dismissed your verdict on', comment: 'Commented on' }[x.kind]
233
- return `${what} ${x.square}${x.path ? ` at ${x.path}${x.line ? `:${x.line}` : ''}` : ''}. ${describeStanding(r)}.`
234
- }
235
-
236
- /** What a line needs to take a merge. */
237
- export function describePolicy (p) {
238
- const parts = []
239
- if (p.approvals) parts.push(`${p.approvals} approval${p.approvals === 1 ? '' : 's'} of a square's latest commit from someone other than its holder`)
240
- if (p.checks?.length) parts.push(`check${p.checks.length === 1 ? '' : 's'} ${p.checks.join(', ')} green on it`)
241
- return parts.length ? `${p.line} needs ${parts.join(' and ')} to take a merge.` : `${p.line} takes merges without approvals or checks.`
242
- }
243
-
244
- /** A check reported on a square. */
245
- export function describeSquareCheck (r) {
246
- const req = r.checks.filter((c) => c.required)
247
- const left = req.filter((c) => c.status !== 'green').map((c) => `${c.name} ${c.status}`)
248
- return `${r.check} is ${r.status} on ${r.square} (${r.head.slice(0, 12)}).${req.length ? (left.length ? ` Its line still needs: ${left.join(', ')}.` : ' Every check its line requires is green.') : ''}`
249
- }
250
-
251
- const SIGNATURE_WORDS = { good: 'Good signature', bad: 'BAD signature', removed_key: 'Signature by a removed key', none: 'No signature' }
252
- export function describeSignature (s) {
253
- return `Signed: ${SIGNATURE_WORDS[s.status] || s.status}${s.key ? ` from ${s.signer} (${s.key})` : ''}`
254
- }
255
-
256
- export function describeKeys (r) {
257
- if (!r.keys.length) return `${r.member} has no signing keys.`
258
- return r.keys.map((k) => `${k.key}${k.name ? ` ${k.name}` : ''}${k.removed_at ? ` (removed ${when(k.removed_at)})` : ` added ${when(k.added_at)}`}`).join('\n')
259
- }
260
-
261
- export function describeTokens (r) {
262
- if (!r.tokens.length) return `${r.quilt} has no tokens. Make one with seam token new <name> [--level read|write].`
263
- return r.tokens.map((t) => `${t.id} ${t.name} ${t.level}${t.revoked_at ? ` (revoked ${when(t.revoked_at)})` : ` made ${when(t.created_at)} by ${t.created_by}`}`).join('\n')
264
- }
265
-
266
- export const describeNewToken = (t) => `Made token ${t.name} (${t.level} on ${t.quilt}, id ${t.id}). Its secret, shown only this once:\n\n ${t.token}\n\nSet it as SEAM_TOKEN (or send it as Authorization: Bearer <token>).`
@@ -1,82 +0,0 @@
1
- // The dogfood round trip (week 4, item 2): a git repository's full history into a new quilt on
2
- // whatever API the client points at, every imported commit's tree checked against git, then
3
- // `main` exported back to git and checked: one exported commit per first-parent commit, each
4
- // with git's tree and its `Quilt-Git-Sha` trailer naming that commit, and a second export from
5
- // scratch giving the same tip (the export is deterministic).
6
- import fs from 'node:fs'
7
- import os from 'node:os'
8
- import path from 'node:path'
9
- import { execFileSync } from 'node:child_process'
10
- import { importGit } from './import-git.js'
11
- import { exportGit } from './export-git.js'
12
-
13
- const git = (dir, args) => execFileSync('git', ['-C', dir, ...args], { maxBuffer: 1 << 30, env: { ...process.env, GIT_NO_REPLACE_OBJECTS: '1' } }).toString()
14
-
15
- /** `git rev-list` with one format line per commit: [[sha, field], ...]. */
16
- function revs (dir, args, format) {
17
- const out = git(dir, ['rev-list', `--format=${format}`, ...args]).split('\n')
18
- const rows = []
19
- for (let i = 0; i < out.length; i++) {
20
- if (!out[i].startsWith('commit ')) continue
21
- rows.push([out[i].slice(7), out[i + 1] || ''])
22
- }
23
- return rows
24
- }
25
-
26
- /**
27
- * Runs the round trip. `repo` is any git repository on this machine, `rev` the commit whose
28
- * history is imported (HEAD by default), `slug` the new quilt. Returns a report; throws on the
29
- * first mismatch.
30
- */
31
- export async function dogfood (client, { repo, rev = 'HEAD', slug, work, keep = false, batch = 200, log = () => {} }) {
32
- const sha = git(repo, ['rev-parse', '--verify', `${rev}^{commit}`]).trim()
33
- const dir = work || fs.mkdtempSync(path.join(os.tmpdir(), 'quilt-dogfood-'))
34
- const src = path.join(dir, 'src')
35
- const times = {}
36
- try {
37
- // A bare-ish copy whose HEAD is `rev`, so the import walks exactly that history.
38
- git(os.tmpdir(), ['clone', '-q', '--shared', '--no-checkout', repo, src])
39
- git(src, ['update-ref', '--no-deref', 'HEAD', sha])
40
-
41
- let t = Date.now()
42
- log(`Importing ${sha.slice(0, 12)} of ${repo} into ${slug}…`)
43
- const imp = await importGit(client, { repo: src, slug, batch, validate: 'all', log, onlyHead: true, fresh: true })
44
- times.import_ms = Date.now() - t
45
- log(`Imported ${imp.commits} commits (${imp.patches} patches); every commit's tree equals git's (${imp.validated}).`)
46
-
47
- t = Date.now()
48
- const out = path.join(dir, 'export.git')
49
- const exp = await exportGit(client, { q: imp.quilt, dir: out, log })
50
- times.export_ms = Date.now() - t
51
-
52
- const want = revs(src, ['--first-parent', '--reverse', sha], '%T')
53
- const got = revs(out, ['--reverse', 'refs/heads/main'], '%T')
54
- if (got.length !== want.length) throw new Error(`export: main has ${got.length} commits, git's first-parent history ${want.length}`)
55
- const trailers = revs(out, ['--reverse', 'refs/heads/main'], '%(trailers:key=Quilt-Git-Sha,valueonly,separator=)')
56
- for (let i = 0; i < want.length; i++) {
57
- if (got[i][1] !== want[i][1]) throw new Error(`export: commit ${i + 1} (git ${want[i][0].slice(0, 12)}) has tree ${got[i][1].slice(0, 12)}, git's is ${want[i][1].slice(0, 12)}`)
58
- if (trailers[i][1].trim() !== want[i][0]) throw new Error(`export: commit ${i + 1} names git ${trailers[i][1].trim() || 'nothing'} in Quilt-Git-Sha, expected ${want[i][0]}`)
59
- }
60
- log(`Exported ${exp.exported} patches; every tree and Quilt-Git-Sha matches git's first-parent history.`)
61
-
62
- const again = await exportGit(client, { q: imp.quilt, dir: path.join(dir, 'again.git'), page: 97 })
63
- if (again.sha !== exp.sha) throw new Error(`export is not deterministic: ${exp.sha} then ${again.sha}`)
64
- log(`Exported again from scratch: same tip ${exp.sha.slice(0, 12)}.`)
65
-
66
- return {
67
- quilt: imp.quilt,
68
- slug: imp.slug,
69
- rev: sha,
70
- commits: imp.commits,
71
- patches: imp.patches,
72
- validated: imp.validated,
73
- exported: exp.exported,
74
- export_sha: exp.sha,
75
- left_out: imp.report,
76
- ...times,
77
- ...(keep ? { work: dir } : {})
78
- }
79
- } finally {
80
- if (!keep && !work) fs.rmSync(dir, { recursive: true, force: true })
81
- }
82
- }
@@ -1,8 +0,0 @@
1
- // What both the server and the seam client need without loading the server (service.js).
2
- export class QuiltError extends Error {
3
- /** `code` is the wire name (04/07/12): head_moved, tip_moved, conflict, not_found, ... */
4
- constructor (status, code, message, extra = {}) { super(message); this.status = status; this.code = code; this.extra = extra }
5
- }
6
-
7
- /** A square's short handle: sq: + the last 8 hex of its uuid (02). */
8
- export const squareShort = (id) => `sq:${String(id).replace(/-/g, '').slice(-8)}`