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/archiver.js
DELETED
|
@@ -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
|
-
}
|
package/src/quilts/autorevert.js
DELETED
|
@@ -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
|
-
}
|
package/src/quilts/bisect-run.js
DELETED
|
@@ -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
|
-
}
|
package/src/quilts/blame.js
DELETED
|
@@ -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
|
-
}
|