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,175 +0,0 @@
1
- // The log archiver (plans/quilt-source-control/03, "The log order and the archiver"; 15
2
- // invariant 5): copies a quilt's log rows, with the commit rows they write, into immutable WAL
3
- // segments in object storage, so the database is rebuildable by replay. A segment is written when
4
- // 1,000 rows are waiting or the oldest waiting row is 5 minutes old, holds at most 1,000 rows, and
5
- // names the segment before it by hash, so a quilt's segments are one chain. archive_seq then moves
6
- // by compare-and-swap (setArchive), so two archivers never both advance one quilt; a loser's
7
- // segment has the same name and the same bytes as the winner's (rows are immutable and the
8
- // encoding is deterministic), and object storage keeps the first.
9
- //
10
- // Segment bytes: 'QWS1', a flag byte (0 deflate-raw, 1 zstd), the compressed body (one canonical
11
- // JSON record a line: { t: 'log', row } or { t: 'commit', row }), the trailer JSON
12
- // { quilt_id, from_seq, to_seq, row_count, prev, blake3 } and its length as a u32. `blake3` is
13
- // the hash of everything before the trailer; `prev` the previous segment's (64 zeros first).
14
- import zlib from 'node:zlib'
15
- import { blake3 } from '@noble/hashes/blake3.js'
16
-
17
- export const SEGMENT_ROWS = 1000
18
- export const SEGMENT_AGE_MS = 5 * 60 * 1000
19
- const MAGIC = Buffer.from('QWS1')
20
- const ZERO = '0'.repeat(64)
21
- const HAS_ZSTD = typeof zlib.zstdCompressSync === 'function'
22
-
23
- const pad = (n) => String(n).padStart(12, '0')
24
- /** 03's key w/<from_seq>-<to_seq>.qws, zero-padded so names sort in seq order. */
25
- export const segmentName = (from, to) => `${pad(from)}-${pad(to)}.qws`
26
- const hashOf = (bytes) => Buffer.from(blake3(bytes)).toString('hex')
27
-
28
- /** Canonical JSON: keys sorted at every level, so the same rows always give the same bytes. */
29
- function canonical (v) {
30
- if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']'
31
- if (v && typeof v === 'object') return '{' + Object.keys(v).sort().filter((key) => v[key] !== undefined).map((key) => JSON.stringify(key) + ':' + canonical(v[key])).join(',') + '}'
32
- return JSON.stringify(v ?? null)
33
- }
34
-
35
- // Commit-row columns that change after the row is written (03's replay table derives them):
36
- // hidden_seq from successors' `predecessor`, patch from merge rows, snapshot_root from the merge
37
- // row or the deriver. A segment keeps only what never changes.
38
- const LATER = ['hidden_seq', 'patch', 'snapshot_root']
39
- export const frozenCommit = (c) => { const out = { ...c }; for (const key of LATER) delete out[key]; return out }
40
-
41
- export function encodeSegment ({ quilt_id: q, from_seq: from, to_seq: to, prev, records }) {
42
- const body = Buffer.from(records.map(canonical).join('\n'), 'utf8')
43
- const head = Buffer.concat([MAGIC, Buffer.from([HAS_ZSTD ? 1 : 0]), HAS_ZSTD ? zlib.zstdCompressSync(body) : zlib.deflateRawSync(body, { level: 6 })])
44
- const trailer = { quilt_id: q, from_seq: from, to_seq: to, row_count: records.filter((r) => r.t === 'log').length, prev, blake3: hashOf(head) }
45
- const tb = Buffer.from(canonical(trailer), 'utf8')
46
- const len = Buffer.alloc(4); len.writeUInt32BE(tb.length)
47
- return { bytes: Buffer.concat([head, tb, len]), trailer }
48
- }
49
-
50
- export function decodeSegment (bytes) {
51
- if (bytes.length < 9 || !bytes.subarray(0, 4).equals(MAGIC)) throw new Error('not a WAL segment')
52
- const tl = bytes.readUInt32BE(bytes.length - 4)
53
- const head = bytes.subarray(0, bytes.length - 4 - tl)
54
- const trailer = JSON.parse(bytes.subarray(head.length, bytes.length - 4).toString('utf8'))
55
- if (hashOf(head) !== trailer.blake3) throw new Error(`segment ${trailer.from_seq}-${trailer.to_seq}: its bytes do not match its hash`)
56
- const flag = head[4]
57
- if (flag === 1 && !HAS_ZSTD) throw new Error('this segment is zstd and this Node cannot read zstd (needs Node 22.15 or later)')
58
- const body = (flag === 1 ? zlib.zstdDecompressSync : zlib.inflateRawSync)(head.subarray(5)).toString('utf8')
59
- return { trailer, records: body ? body.split('\n').map((l) => JSON.parse(l)) : [] }
60
- }
61
-
62
- /**
63
- * The archiver over a store and an object store. `archive(q, { force })` writes every segment
64
- * that is due (with `force`, the rest too, in segments of at most 1,000 rows) and returns
65
- * { segments, archive_seq }. `maybe(q)` runs it in the background, once at a time per quilt.
66
- */
67
- export function createArchiver ({ store, objects, now = Date.now, rows = SEGMENT_ROWS, ageMs = SEGMENT_AGE_MS }) {
68
- const running = new Map()
69
-
70
- async function writeOne (q, quilt, to) {
71
- const from = (quilt.archive_seq || 0) + 1
72
- const records = []
73
- for (let after = from - 1; after < to;) {
74
- const page = await store.logRows(q, { after, limit: Math.min(1000, to - after) })
75
- if (!page.length) throw new Error(`log rows ${after + 1}..${to} are missing: the archiver never writes past a gap`)
76
- for (const row of page) {
77
- if (row.seq !== after + 1) throw new Error(`log row ${after + 1} is missing (found ${row.seq}): the archiver never writes past a gap`)
78
- records.push({ t: 'log', row })
79
- after = row.seq
80
- // The commit row this log row wrote (a commit, or the rebased commit of a merge).
81
- const id = row.kind === 'commit' || row.kind === 'merge' ? row.commit || row.commit_id : null
82
- if (id) {
83
- const c = await store.getCommit(q, id)
84
- if (c && c.seq === row.seq) records.push({ t: 'commit', row: frozenCommit(c) })
85
- }
86
- }
87
- }
88
- const prev = quilt.last_segment || ZERO
89
- const { bytes, trailer } = encodeSegment({ quilt_id: q, from_seq: from, to_seq: to, prev, records })
90
- await objects.putFile(q, 'w', segmentName(from, to), bytes)
91
- const r = await store.setArchive({ quilt_id: q, expect_seq: from - 1, to_seq: to, segment: trailer.blake3 })
92
- return { ...trailer, archive_seq: r.archive_seq }
93
- }
94
-
95
- async function archive (q, { force = false } = {}) {
96
- const segments = []
97
- for (;;) {
98
- const quilt = await store.getQuilt(q)
99
- if (!quilt) throw new Error('no such quilt')
100
- const done = quilt.archive_seq || 0
101
- const waiting = quilt.head_seq - done
102
- if (waiting <= 0) return { segments, archive_seq: done }
103
- let due = force || waiting >= rows
104
- if (!due) {
105
- const [oldest] = await store.logRows(q, { after: done, limit: 1 })
106
- due = !!oldest && now() - Number(oldest.at) >= ageMs
107
- }
108
- if (!due) return { segments, archive_seq: done }
109
- try {
110
- segments.push(await writeOne(q, quilt, done + Math.min(rows, waiting)))
111
- } catch (err) {
112
- if (err.code === 'archive_moved') continue // another archiver advanced it: read again
113
- throw err
114
- }
115
- }
116
- }
117
-
118
- function maybe (q) {
119
- if (running.has(q)) return running.get(q)
120
- const p = archive(q).catch(() => null).finally(() => running.delete(q))
121
- running.set(q, p)
122
- return p
123
- }
124
-
125
- return { archive, maybe }
126
- }
127
-
128
- /**
129
- * Replay (03's table, for the rows segments carry): reads a quilt's segments from object storage
130
- * alone, checks the chain end to end (each segment's hash, `prev`, and seq continuity from 1), and
131
- * rebuilds the log, the commit rows (with hidden_seq and patch derived again) and every line's tip.
132
- */
133
- export async function replaySegments (objects, q) {
134
- const names = await objects.listFiles(q, 'w')
135
- const log = []
136
- const commits = new Map()
137
- const lines = new Map()
138
- let prev = ZERO
139
- let at = 0
140
- for (const name of names) {
141
- const { trailer, records } = decodeSegment(await objects.readFile(q, 'w', name))
142
- if (trailer.quilt_id !== q) throw new Error(`segment ${name} belongs to quilt ${trailer.quilt_id}`)
143
- if (trailer.prev !== prev) throw new Error(`segment ${name} does not follow the one before it (chain broken)`)
144
- if (trailer.from_seq !== at + 1) throw new Error(`segment ${name} starts at ${trailer.from_seq}, expected ${at + 1} (a gap)`)
145
- if (name !== segmentName(trailer.from_seq, trailer.to_seq)) throw new Error(`segment ${name} says it holds ${trailer.from_seq}-${trailer.to_seq}`)
146
- for (const r of records) {
147
- if (r.t === 'commit') { commits.set(r.row.id, { ...r.row, hidden_seq: null }); continue }
148
- const row = r.row
149
- if (row.seq !== at + 1) throw new Error(`segment ${name}: row ${row.seq} out of order`)
150
- at = row.seq
151
- log.push(row)
152
- }
153
- if (at !== trailer.to_seq || log.length < trailer.row_count) throw new Error(`segment ${name} ends at ${at}, its trailer says ${trailer.to_seq}`)
154
- prev = trailer.blake3
155
- }
156
- for (const c of commits.values()) {
157
- const p = c.predecessor && commits.get(c.predecessor)
158
- if (p && p.hidden_seq == null) p.hidden_seq = c.seq
159
- }
160
- for (const row of log) {
161
- // A line made from a commit, and a line frozen or unfrozen (policy rows naming the line).
162
- if (row.kind === 'policy' && row.line && row.payload) {
163
- const op = row.payload.op
164
- if (op === 'line') lines.set(row.line, { name: row.line, tip: row.payload.tip || null, tip_seq: row.seq, patch_count: 0, frozen: false })
165
- else if ((op === 'freeze' || op === 'unfreeze') && lines.has(row.line)) lines.get(row.line).frozen = op === 'freeze'
166
- continue
167
- }
168
- if (row.kind !== 'merge') continue
169
- const id = row.commit || row.to || row.commit_id
170
- lines.set(row.line, { ...lines.get(row.line), name: row.line, tip: id, tip_seq: row.seq, patch_count: row.patch_no })
171
- const c = commits.get(id)
172
- if (c) { c.patch = { line: row.line, patch_no: row.patch_no }; if (row.snapshot_root) c.snapshot_root = row.snapshot_root }
173
- }
174
- return { archive_seq: at, last_segment: prev === ZERO ? null : prev, log, commits, lines }
175
- }
@@ -1,49 +0,0 @@
1
- // The automatic revert (plans/quilt-source-control/07 "The post-merge pipeline", steps 3 and 4).
2
- // A red post-merge check that names the patch it failed at (`patch`, found by the runner, for
3
- // example with `seam bisect`) reverts that patch on the line by itself: a revert square is opened
4
- // and merged as the reporter, and a fix square holding the change again is opened for the
5
- // patch's holder, so fixing forward is "edit, commit, merge". The patch stays in history. The
6
- // revert is an ordinary revert and merge, so a frozen line or a full merge window refuses it like
7
- // any merge and the revert square is left open; a revert that conflicts with later patches is
8
- // left open too, with its terms.
9
- import { QuiltError } from './service.js'
10
-
11
- export function autoRevertOps (svc, { revert, mergeIntoLine, cherryPick, openSquare, handoff }) {
12
- const { store, quiltFor, resolve, getCommit, squareView } = svc.internals
13
-
14
- async function revertCulprit (me, ref, line, check, n) {
15
- const q = await quiltFor(me, ref, 'write')
16
- const row = await store.patch(q.id, line, n)
17
- if (!row || !row.commit) throw new QuiltError(404, 'not_found', `${line} has no patch ${n}`)
18
- const culprit = await resolve(q.id, `${line}:${n}`)
19
- const x = await getCommit(q.id, culprit)
20
- const first = String(x.message || '').split('\n')[0]
21
- let r
22
- try {
23
- r = await revert(me, ref, { rev: `${line}:${n}`, line, message: `Revert patch ${line}:${n}: ${check} failed\n\nThis reverts patch ${line}:${n} (${first}).` })
24
- } catch (err) {
25
- if (err.code === 'bad_request') return { reverted: false, patch_no: n, refused: { code: 'nothing_to_revert', detail: err.message } }
26
- throw err
27
- }
28
- const out = { reverted: false, patch_no: n, square: r.square }
29
- if (r.conflicted) return { ...out, refused: { code: 'conflict', detail: `the revert conflicts with later patches in ${r.terms.map((t) => t.path).join(', ')}; resolve and merge the revert square` } }
30
- try {
31
- const m = await mergeIntoLine(me, ref, line, { square: r.square.id }, { reverts: n })
32
- if (!m.ok) return { ...out, square: m.square || r.square, refused: m.refused }
33
- Object.assign(out, { reverted: true, revert_patch_no: m.merged.patch_no, square: m.square })
34
- } catch (err) {
35
- if (!err.code || !err.status) throw err
36
- return { ...out, refused: { code: err.code, detail: err.message } }
37
- }
38
- // The fix square: the change again over the tip after the revert, held by the patch's holder.
39
- const orig = row.square ? await store.getSquare(row.square) : null
40
- const holder = orig && !orig.import ? orig.holder : null
41
- const fix = await openSquare(me, ref, { title: `Fix ${first || `patch ${line}:${n}`}`.slice(0, 200), line })
42
- await cherryPick(me, ref, fix.id, { from: culprit, expect_head: null, message: x.message })
43
- if (holder && holder !== me.member_id) await handoff(me, ref, fix.id, { to: holder, context: `Patch ${line}:${n} was reverted: ${check} failed. This square holds the change again: fix it, commit, merge.` })
44
- out.fix_square = await squareView(await store.getSquare(fix.id))
45
- return out
46
- }
47
-
48
- return { revertCulprit }
49
- }
@@ -1,31 +0,0 @@
1
- // The check side of bisect (the server narrows, history.js): each commit the server names is
2
- // checked out into a fresh folder and the check command runs there. Exit 0 is good, 125 is skip,
3
- // 1 to 127 is bad, anything else (a signal, 128 and up) stops, as git bisect run does.
4
- import fs from 'node:fs'
5
- import os from 'node:os'
6
- import path from 'node:path'
7
- import { spawn } from 'node:child_process'
8
- import { checkout } from './block.js'
9
-
10
- const runIn = (cmd, cwd) => new Promise((resolve, reject) => {
11
- const child = spawn(cmd, { cwd, shell: true, stdio: 'ignore' })
12
- child.on('error', reject)
13
- child.on('exit', (code, signal) => resolve(signal ? null : code))
14
- })
15
-
16
- export async function bisectRun (client, { quilt, bad, good, run, marks = {}, paths = [''], api, log = () => {} }) {
17
- const m = { ...marks }
18
- for (let i = 0; i < 200; i++) {
19
- const r = await client.bisect(quilt, { bad, good, marks: m })
20
- if (r.done) return { ...r, marks: m }
21
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'seam-bisect-'))
22
- try {
23
- await checkout(client, { quilt, rev: r.next.id, paths, dir, api })
24
- const code = await runIn(run, dir)
25
- if (code == null || code > 127) throw new Error(`The check stopped on ${r.next.short} (exit ${code ?? 'by a signal'}): bisect stopped.`)
26
- m[r.next.id] = code === 0 ? 'good' : code === 125 ? 'skip' : 'bad'
27
- log(`${r.next.short} ${m[r.next.id]}`)
28
- } finally { fs.rmSync(dir, { recursive: true, force: true }) }
29
- }
30
- throw new Error('bisect took more than 200 steps')
31
- }
@@ -1,103 +0,0 @@
1
- // Blame, computed on request (plans/quilt-source-control/08): each line of a file at a revision
2
- // is attributed to the commit that last changed it, walking first parents back from the
3
- // revision. Whole-file renames and the derived, eager-for-main version come later; this walk
4
- // is what that type computes, without the cache.
5
- import { cleanPath } from './records.js'
6
- import { isText, splitLines, lineMap } from './textdiff.js'
7
- import { QuiltError, squareShort } from './service.js'
8
-
9
- const MAX_WALK = 100000
10
-
11
- export function blameOps (svc) {
12
- const { quiltFor, resolve, getCommit, snapshot, reader, readBlob } = svc.internals
13
-
14
- async function blame (me, ref, rev, path) {
15
- const q = await quiltFor(me, ref)
16
- const p = cleanPath(path)
17
- const start = await resolve(q.id, rev)
18
- const at = start ? await reader(q.id).lookup(await snapshot(q.id, start), p) : null
19
- if (!at || !at.blob) throw new QuiltError(404, 'not_found', `${p} is not in ${rev || 'main'}`)
20
- const bytes = await readBlob(q.id, at.blob)
21
- if (!isText(bytes)) throw new QuiltError(400, 'bad_request', `${p} is not text`)
22
- const lines = splitLines(bytes.toString('utf8'))
23
- const owner = new Array(lines.length).fill(null)
24
- // `track[i]` is the index, in the version being looked at, of the result's line i (or -1 once owned).
25
- let track = lines.map((_, i) => i)
26
- let text = lines
27
- let buf = bytes
28
- let id = start
29
- let blob = at.blob
30
- let walked = 0
31
- let cur = p
32
- const chains = new Map()
33
- while (id && walked++ < MAX_WALK && track.some((x) => x >= 0)) {
34
- const c = await getCommit(q.id, id)
35
- const ch = c.changes.find((x) => x.path === cur)
36
- const parent = c.parents[0] || null
37
- if (!ch) {
38
- // On a line, jump straight to the last patch that touched the file (line_paths).
39
- if (c.patch && c.patch.line) {
40
- const rows = await svc.internals.store.linePaths(q.id, c.patch.line, cur, { upTo: c.patch.patch_no, limit: 1 })
41
- if (rows.length) { id = rows[0].commit_id; continue }
42
- } else if (!c.patch) {
43
- // Inside a square, jump to its last commit on this chain that touched the file, or out of it.
44
- const { chain, exit } = await svc.internals.squareChain(q.id, c, chains)
45
- const hit = (await svc.internals.store.squarePathCommits(q.id, c.square_id, cur, { upTo: c.seq, limit: chain.size })).find((r) => chain.has(r.commit_id))
46
- id = hit ? hit.commit_id : exit
47
- continue
48
- }
49
- id = parent
50
- continue
51
- }
52
- // This commit changed the file: lines it kept come from its parent's version; the rest are its own.
53
- let before = ch.op === 'put' && parent ? await reader(q.id).lookup(await snapshot(q.id, parent), cur) : null
54
- if ((!before || !before.blob) && parent) {
55
- // Added here: a whole-file rename when a path this commit deleted held at least half its lines.
56
- const from = ch.copy_from ? ch.copy_from.path : await renamedFrom(q.id, c, parent, text)
57
- if (from) { before = await reader(q.id).lookup(await snapshot(q.id, parent), from); if (before) cur = from }
58
- }
59
- if (!before || !before.blob) {
60
- for (let i = 0; i < track.length; i++) if (track[i] >= 0) { owner[i] = c; track[i] = -1 }
61
- break
62
- }
63
- const prevBuf = before.blob === blob ? buf : await readBlob(q.id, before.blob)
64
- const prevText = before.blob === blob ? text : splitLines(prevBuf.toString('utf8'))
65
- const map = lineMap(prevBuf, buf)
66
- track = track.map((x, i) => {
67
- if (x < 0) return x
68
- if (map[x] < 0) { owner[i] = c; return -1 }
69
- return map[x]
70
- })
71
- text = prevText
72
- buf = prevBuf
73
- blob = before.blob
74
- id = parent
75
- }
76
- const out = lines.map((line, i) => {
77
- const c = owner[i]
78
- return { line: i + 1, text: line, commit: c ? c.id : null, short: c ? c.id.slice(0, 12) : null, author: c ? c.author.name : null, member: c ? c.author.member_id : null, at: c ? (c.authored_at || c.at) : null, square: c ? squareShort(c.square_id) : null, ...(c && c.patch ? { patch: c.patch } : {}) }
79
- })
80
- return { path: p, rev: rev || 'main', commit: start, lines: out }
81
- }
82
-
83
- /** The path a commit deleted whose parent version shares at least half of `text`'s lines, or null. */
84
- async function renamedFrom (q, c, parent, text) {
85
- const gone = c.changes.filter((x) => x.op === 'delete')
86
- if (!gone.length || !text.length) return null
87
- const want = new Set(text)
88
- let best = null; let score = 0.5
89
- const root = await snapshot(q, parent)
90
- for (const g of gone.slice(0, 50)) {
91
- const e = await reader(q).lookup(root, g.path)
92
- if (!e || !e.blob) continue
93
- const b = await readBlob(q, e.blob)
94
- if (!isText(b)) continue
95
- const lines = splitLines(b.toString('utf8'))
96
- const shared = lines.filter((l) => want.has(l)).length / Math.max(lines.length, text.length)
97
- if (shared >= score) { best = g.path; score = shared }
98
- }
99
- return best
100
- }
101
-
102
- return { blame }
103
- }