@heyquilt/seam 0.0.0-stage → 0.1.0
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/README.md +25 -2
- package/bin/seam.js +80 -0
- package/lib/account.js +120 -0
- package/lib/quilts/block.js +228 -0
- package/lib/quilts/cbor.js +92 -0
- package/lib/quilts/cli.js +254 -0
- package/lib/quilts/client.js +92 -0
- package/lib/quilts/describe.js +120 -0
- package/lib/quilts/export-git.js +126 -0
- package/lib/quilts/hash.js +44 -0
- package/lib/quilts/ids.js +12 -0
- package/lib/quilts/import-git.js +193 -0
- package/lib/quilts/pack.js +118 -0
- package/lib/quilts/records.js +210 -0
- package/lib/quilts/textdiff.js +96 -0
- package/lib/quilts/tools.js +201 -0
- package/lib/quilts/xdiff.js +435 -0
- package/package.json +27 -4
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Quilt source control's MCP tools (plans/quilt-source-control/12), served by `seam mcp` (seam/),
|
|
2
|
+
// not by the Quilt app's MCP: one description and one schema per tool, answered by the same
|
|
3
|
+
// server routes the CLI uses, so Claude Code, Cursor, Codex and every other tool get the same
|
|
4
|
+
// behaviour and the same sentences.
|
|
5
|
+
//
|
|
6
|
+
// Locally, in a folder made by `seam checkout`, the quilt and square come from the folder and
|
|
7
|
+
// seam_commit seals the folder's changes; anywhere else (and always for hosted agents) name the
|
|
8
|
+
// quilt and square, and seam_commit takes the files' new contents.
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import { describeLog, describeShow, describeDiff, describeBlame, describeStatus, describeMerge, describeWrite, describeSquare, describeSquareStatus, describeEvents } from './describe.js'
|
|
11
|
+
import { status as blockStatus, commitFolder, syncTo } from './block.js'
|
|
12
|
+
|
|
13
|
+
export const SEAM_TOOLS = ['seam_quilts', 'seam_open_square', 'seam_squares', 'seam_log', 'seam_show', 'seam_diff', 'seam_blame', 'seam_cat', 'seam_ls', 'seam_commit', 'seam_merge', 'seam_rebase', 'seam_revert', 'seam_cherry_pick', 'seam_resolve', 'seam_square_status', 'seam_square_handoff', 'seam_abandon', 'seam_reopen', 'seam_fold', 'seam_split', 'seam_events', 'seam_events_webhook']
|
|
14
|
+
|
|
15
|
+
/** What every agent is told about source control, in each MCP's instructions. */
|
|
16
|
+
export const SEAM_GUIDE = 'Quilt source control (quilts): a quilt is a repository on the Quilt server; you work in a square (your own branch and working copy), commit save points in it, and merge it into main, where it becomes a patch. ' +
|
|
17
|
+
'Open a square with seam_open_square, commit with seam_commit, merge with seam_merge (into: "main"). A merge never stops half way: a real conflict is refused, kept in your square as conflict markers, and you settle it (edit the file and commit it without the markers, or seam_resolve) and merge again. ' +
|
|
18
|
+
'Read history with seam_log, seam_show, seam_diff, seam_blame and seam_cat; revisions are main, main~3, patch:12, sq:<id>, a commit id or git:<sha>. ' +
|
|
19
|
+
'In a folder checked out from a square (it holds .seam/block.json), edit the files with your own tools and check them with seam_square_status.'
|
|
20
|
+
|
|
21
|
+
const QUILT = z.string().max(100).optional().describe('The quilt (its slug or id); in a folder made by seam checkout, that folder\'s quilt')
|
|
22
|
+
const SQUARE = z.string().max(100).optional().describe('The square (sq:<id>); in a block folder, that folder\'s square')
|
|
23
|
+
const REV = z.string().max(200).optional()
|
|
24
|
+
const PATHS = z.array(z.string().max(1000)).max(200).optional().describe('Only these paths (files or directories)')
|
|
25
|
+
|
|
26
|
+
export const SEAM_DESCRIPTIONS = {
|
|
27
|
+
seam_quilts: 'Lists the quilts (repositories) you can reach, or makes a new one (new: its slug).',
|
|
28
|
+
seam_open_square: 'Opens a square: your own unit of work, branched from the tip of main (or line). Returns its id (sq:…). Agents open squares, never lines.',
|
|
29
|
+
seam_squares: 'The open squares of a quilt, with holder, head, how far behind main each is and its conflicted paths. all: true for merged and abandoned ones too.',
|
|
30
|
+
seam_square_status: 'One square as the server sees it (head, behind_by, conflicted paths), the patches merged under it that changed its files, and which files would conflict if merged now; in a block folder also the files changed since the last commit.',
|
|
31
|
+
seam_square_handoff: 'Hands your square to another member (to), with a note on what is done and what is left. You stop being able to commit to it.',
|
|
32
|
+
seam_abandon: 'Abandons a square whose work is not wanted. Its commits stay readable and seam_reopen brings it back.',
|
|
33
|
+
seam_reopen: 'Reopens an abandoned square; you become its holder.',
|
|
34
|
+
seam_events: 'What happened to your work since you last looked: patch.merged (a patch changed files your open square changed, and whether your merge would now conflict), conflict.added (a conflict was kept in your square), square.handoff. Pass after (the cursor from the last answer) to get only new ones.',
|
|
35
|
+
seam_events_webhook: 'Has the quilt server POST your events to a URL (signed with HMAC-SHA256 over <timestamp>.<body>, retried 3 times), so you are woken instead of polling: url, events (default all), secret (made for you if left out). url: "off" removes it.',
|
|
36
|
+
seam_fold: 'Folds a square (square) into another square you hold (into): its work becomes one commit there and the square closes. Overlapping changes become conflict markers in the target.',
|
|
37
|
+
seam_split: 'Splits your square by paths: groups: [[paths], ...], each group moves to a new square (same base) as one commit; the largest group, and paths no group names, stay in this square.',
|
|
38
|
+
seam_log: 'History, newest first: rev (default main, or your square), paths to limit it, limit (default 20). Each patch on main is one line\'s merge.',
|
|
39
|
+
seam_show: 'One commit or patch: who, when, why, which square, and its diff. rev: patch:12, main~1, sq:…, a commit id, git:<sha>. format: unified (default), stat or names.',
|
|
40
|
+
seam_diff: 'The difference between two revisions (from, to); with only to, against its parent (for a square: against where it branched). format: unified, stat or names.',
|
|
41
|
+
seam_blame: 'Each line of a file at a revision, with the commit and patch that last changed it and who made it. Computed by the server.',
|
|
42
|
+
seam_cat: 'A file\'s content at any revision (path, rev). To change a file, read it from your own square (rev sq:<id>), not from main: main moves while you work, and a commit built from a different version of main than your square started from can undo work that merged in between. A conflicted file comes back with its conflict markers: format diff (default: one side in full after +++++++, each other side as a diff from its base after %%%%%%%), snapshot (every side in full) or git (<<<<<<< ======= >>>>>>>, two sides only).',
|
|
43
|
+
seam_ls: 'The files at a revision, under paths (directories).',
|
|
44
|
+
seam_commit: 'Seals your square\'s changes as a commit (a save point). In a block folder: the folder\'s changed files (paths: only these). Elsewhere: files: [{ path, content }] (text; base64: true for bytes) and delete: [paths]. message says why. Committing a conflicted file without its markers settles the conflict.',
|
|
45
|
+
seam_merge: 'Merges your square into a line (into: "main") or, with from, merges another square or revision into your square (a two-parent commit; conflicts stay in your square as markers). Into a line: the server rebases it onto the tip as one commit and moves main, retrying lost races itself. The result is a patch. A real conflict is refused: the square is rebased with the conflict kept in it (your folder gets the markers); settle it and merge again. dry_run: true says what would happen.',
|
|
46
|
+
seam_rebase: 'Moves your square onto the tip of its line (or onto a revision). Conflicts become conflict markers in the square, never a stop.',
|
|
47
|
+
seam_revert: 'Undoes a patch (or commit): a new square from the tip whose one commit inverts it. Merge that square with seam_merge.',
|
|
48
|
+
seam_cherry_pick: 'Applies one commit\'s change (from) to your square as a new commit.',
|
|
49
|
+
seam_resolve: 'Settles conflicted files in your square by choosing a side: entries [{ path, choose: ours | theirs | base | suggestion | delete }]; suggestion takes the word-level merge a conflict offers (its suggestion, shown when the merge is refused). (Or edit the file and commit it without the markers.)'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SEAM_INPUTS = {
|
|
53
|
+
seam_quilts: { new: z.string().max(63).optional().describe('Make a quilt with this slug') },
|
|
54
|
+
seam_open_square: { quilt: QUILT, title: z.string().max(200).describe('What the work is'), line: z.string().max(100).optional(), task_id: z.string().max(64).optional() },
|
|
55
|
+
seam_squares: { quilt: QUILT, all: z.boolean().optional(), mine: z.boolean().optional() },
|
|
56
|
+
seam_square_status: { quilt: QUILT, square: SQUARE },
|
|
57
|
+
seam_log: { quilt: QUILT, rev: REV, paths: PATHS, limit: z.number().int().min(1).max(500).optional() },
|
|
58
|
+
seam_show: { quilt: QUILT, rev: REV, format: z.enum(['unified', 'stat', 'names']).optional(), paths: PATHS },
|
|
59
|
+
seam_diff: { quilt: QUILT, from: REV, to: REV, format: z.enum(['unified', 'stat', 'names']).optional(), paths: PATHS },
|
|
60
|
+
seam_blame: { quilt: QUILT, path: z.string().max(1000), rev: REV },
|
|
61
|
+
seam_cat: { quilt: QUILT, path: z.string().max(1000), rev: REV, format: z.enum(['diff', 'snapshot', 'git']).optional().describe('How a conflicted file is written (default diff)') },
|
|
62
|
+
seam_ls: { quilt: QUILT, rev: REV, paths: PATHS },
|
|
63
|
+
seam_commit: {
|
|
64
|
+
quilt: QUILT, square: SQUARE, message: z.string().max(65536).describe('Why'),
|
|
65
|
+
files: z.array(z.object({ path: z.string().max(1000), content: z.string(), base64: z.boolean().optional(), mode: z.enum(['file', 'exec']).optional() })).max(2000).optional(),
|
|
66
|
+
delete: z.array(z.string().max(1000)).max(2000).optional(),
|
|
67
|
+
paths: PATHS
|
|
68
|
+
},
|
|
69
|
+
seam_square_handoff: { quilt: QUILT, square: SQUARE, to: z.string().max(200).describe('The member to hand it to'), note: z.string().max(2000).optional() },
|
|
70
|
+
seam_abandon: { quilt: QUILT, square: SQUARE },
|
|
71
|
+
seam_reopen: { quilt: QUILT, square: SQUARE },
|
|
72
|
+
seam_events: { quilt: QUILT, after: z.number().int().min(0).optional(), kinds: z.array(z.enum(['patch.merged', 'conflict.added', 'square.handoff'])).optional() },
|
|
73
|
+
seam_events_webhook: { quilt: QUILT, url: z.string().max(2000), events: z.array(z.enum(['patch.merged', 'conflict.added', 'square.handoff'])).optional(), secret: z.string().min(16).max(200).optional() },
|
|
74
|
+
seam_fold: { quilt: QUILT, square: SQUARE, into: z.string().max(100).describe('The square (sq:…) that takes the work'), message: z.string().max(65536).optional() },
|
|
75
|
+
seam_split: { quilt: QUILT, square: SQUARE, groups: z.array(z.array(z.string().max(1000)).min(1).max(2000)).min(1).max(20).describe('Paths (files or directories) per new square'), titles: z.array(z.string().max(200)).max(20).optional() },
|
|
76
|
+
seam_merge: { quilt: QUILT, square: SQUARE, from: z.string().max(200).optional().describe('Merge this square (sq:…) or revision into your square instead of merging your square into a line'), into: z.string().max(100).optional().describe('The line, default main'), message: z.string().max(65536).optional(), dry_run: z.boolean().optional(), on_conflict: z.enum(['rebase', 'leave']).optional() },
|
|
77
|
+
seam_rebase: { quilt: QUILT, square: SQUARE, onto: REV },
|
|
78
|
+
seam_revert: { quilt: QUILT, rev: z.string().max(200).describe('patch:12, a commit id, …'), line: z.string().max(100).optional() },
|
|
79
|
+
seam_cherry_pick: { quilt: QUILT, square: SQUARE, from: z.string().max(200) },
|
|
80
|
+
seam_resolve: { quilt: QUILT, square: SQUARE, entries: z.array(z.object({ path: z.string().max(1000), choose: z.union([z.enum(['ours', 'theirs', 'base', 'suggestion', 'delete', 'keep_edit', 'keep_delete']), z.number().int().min(0).max(8)]) })).min(1).max(500) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runs one tool. `client` is a quilt client (client.js) acting as the agent; `found` is the
|
|
85
|
+
* block folder the local MCP runs in ({ dir, block }) or null. Returns the text to answer.
|
|
86
|
+
*/
|
|
87
|
+
export async function runSeamTool (name, args, { client, found = null }) {
|
|
88
|
+
const block = found?.block || null
|
|
89
|
+
const quilt = args.quilt || block?.quilt
|
|
90
|
+
const square = args.square || block?.square
|
|
91
|
+
const needQuilt = () => { if (!quilt) throw new Error('Name the quilt (quilt: its slug or id); seam_quilts lists yours.'); return quilt }
|
|
92
|
+
const needSquare = () => { if (!square) throw new Error('Name the square (square: sq:…); seam_open_square opens one, seam_squares lists them.'); return square }
|
|
93
|
+
const inFolder = found && block && square === block.square && quilt === block.quilt
|
|
94
|
+
const sync = async (head, sq) => { if (inFolder && head && head !== block.head) { const s = await syncTo(client, found.dir, block, head, { square: sq }); return `\nThe folder now holds the square's files (${s.written} updated).` } return '' }
|
|
95
|
+
try {
|
|
96
|
+
switch (name) {
|
|
97
|
+
case 'seam_quilts': {
|
|
98
|
+
if (args.new) { const q = await client.createQuilt({ slug: args.new }); return `Made quilt ${q.slug} (${q.id}).` }
|
|
99
|
+
const list = await client.quilts()
|
|
100
|
+
return list.length ? list.map((q) => `${q.slug} (${q.id}), ${q.level}`).join('\n') : 'No quilts yet.'
|
|
101
|
+
}
|
|
102
|
+
case 'seam_open_square': {
|
|
103
|
+
const sq = await client.openSquare(needQuilt(), { title: args.title, line: args.line, task_id: args.task_id })
|
|
104
|
+
return `Opened ${describeSquare(sq)}.\nCommit in it with seam_commit (square: "${sq.short}") and merge it with seam_merge.`
|
|
105
|
+
}
|
|
106
|
+
case 'seam_squares': {
|
|
107
|
+
const list = await client.squares(needQuilt(), { state: args.all ? undefined : 'open', mine: !!args.mine })
|
|
108
|
+
return list.length ? list.map(describeSquare).join('\n') : 'No open squares.'
|
|
109
|
+
}
|
|
110
|
+
case 'seam_square_status': {
|
|
111
|
+
if (inFolder) return describeStatus(await blockStatus(client, found.dir, block))
|
|
112
|
+
return describeSquareStatus(await client.square(needQuilt(), needSquare()))
|
|
113
|
+
}
|
|
114
|
+
case 'seam_log': return describeLog(await client.log(needQuilt(), { rev: args.rev || (block?.square ? block.square_short : 'main'), paths: args.paths, limit: args.limit }), { oneline: true })
|
|
115
|
+
case 'seam_show': return describeShow(await client.show(needQuilt(), args.rev || 'main', { format: args.format || 'unified', paths: args.paths }), { format: args.format || 'unified' })
|
|
116
|
+
case 'seam_diff': return describeDiff(await client.diff(needQuilt(), { from: args.from, to: args.to, paths: args.paths, format: args.format || 'unified' }), { format: args.format || 'unified' })
|
|
117
|
+
case 'seam_blame': return describeBlame(await client.blame(needQuilt(), args.rev || 'main', args.path))
|
|
118
|
+
case 'seam_cat': { const r = await client.cat(needQuilt(), args.rev || 'main', args.path, { format: args.format || 'diff' }); return Buffer.from(r.content, 'base64').toString('utf8') }
|
|
119
|
+
case 'seam_ls': { const r = await client.tree(needQuilt(), args.rev || 'main', args.paths); return r.entries.map((e) => `${e.mode}\t${e.path}`).join('\n') || 'No files.' }
|
|
120
|
+
case 'seam_commit': {
|
|
121
|
+
if (inFolder && !args.files && !args.delete) {
|
|
122
|
+
const r = await commitFolder(client, found.dir, block, { message: args.message, paths: (args.paths || []).map((p) => `${found.dir}/${p}`) })
|
|
123
|
+
return r.nothing ? 'Nothing to commit: no file in this folder changed since the last commit.' : describeWrite(r, 'Committed')
|
|
124
|
+
}
|
|
125
|
+
const changes = [
|
|
126
|
+
...(args.files || []).map((f) => ({ path: f.path, op: 'put', content: f.base64 ? f.content : Buffer.from(f.content, 'utf8').toString('base64'), ...(f.mode ? { mode: f.mode } : {}) })),
|
|
127
|
+
...(args.delete || []).map((p) => ({ path: p, op: 'delete' }))
|
|
128
|
+
]
|
|
129
|
+
if (!changes.length) return 'Nothing to commit: give files (and delete), or run in a block folder.'
|
|
130
|
+
const sq = await client.square(needQuilt(), needSquare())
|
|
131
|
+
return describeWrite(await client.commit(quilt, sq.id, { expect_head: sq.head, message: args.message, changes }), 'Committed')
|
|
132
|
+
}
|
|
133
|
+
case 'seam_square_handoff': { const sq = await client.handoff(needQuilt(), needSquare(), { to: args.to, note: args.note }); return `Handed ${sq.short} to ${sq.holder}.` }
|
|
134
|
+
case 'seam_abandon': { const sq = await client.abandon(needQuilt(), needSquare(), inFolder ? { expect_head: block.head } : {}); return `Abandoned ${sq.short}; seam_reopen brings it back.` }
|
|
135
|
+
case 'seam_reopen': { const sq = await client.reopen(needQuilt(), needSquare()); return `Reopened ${describeSquare(sq)}.` }
|
|
136
|
+
case 'seam_events': return describeEvents(await client.events(needQuilt(), { after: args.after, kinds: args.kinds }))
|
|
137
|
+
case 'seam_events_webhook': {
|
|
138
|
+
if (args.url === 'off') { await client.subscribe(needQuilt(), { url: null }); return 'Webhook removed.' }
|
|
139
|
+
const r = await client.subscribe(needQuilt(), { url: args.url, events: args.events, secret: args.secret })
|
|
140
|
+
return `Events (${r.events.join(', ')}) now POST to ${r.url}.${r.secret ? ` Signing secret (shown once): ${r.secret}` : ''}`
|
|
141
|
+
}
|
|
142
|
+
case 'seam_fold': {
|
|
143
|
+
const r = await client.fold(needQuilt(), needSquare(), { into: args.into, message: args.message, ...(inFolder ? { expect_head: block.head } : {}) })
|
|
144
|
+
return describeWrite(r, `Folded ${r.folded.short} into ${r.square.short}:`)
|
|
145
|
+
}
|
|
146
|
+
case 'seam_split': {
|
|
147
|
+
const r = await client.split(needQuilt(), needSquare(), { by: 'paths', groups: args.groups, titles: args.titles, ...(inFolder ? { expect_head: block.head } : {}) })
|
|
148
|
+
return [`Kept in ${r.square.short}: ${r.kept.join(', ') || '(nothing)'}`, ...r.squares.map((q) => `New ${describeSquare(q)}: ${q.paths.join(', ')}`)].join('\n') + await sync(r.square.head, r.square)
|
|
149
|
+
}
|
|
150
|
+
case 'seam_merge': {
|
|
151
|
+
if (args.from) {
|
|
152
|
+
const r = await client.mergeSquare(needQuilt(), needSquare(), { from: args.from, message: args.message, ...(inFolder ? { expect_head: block.head } : {}) })
|
|
153
|
+
return r.noop ? `Already up to date: ${args.from} is in this square.` : describeWrite(r, `Merged ${args.from} in:`) + await sync(r.square.head, r.square)
|
|
154
|
+
}
|
|
155
|
+
const r = await client.merge(needQuilt(), args.into || block?.line || 'main', { square: needSquare(), ...(inFolder ? { expect_head: block.head } : {}), message: args.message, dry_run: !!args.dry_run, on_conflict: args.on_conflict })
|
|
156
|
+
return describeMerge(r) + (!r.ok && r.head ? await sync(r.head, r.square) : '')
|
|
157
|
+
}
|
|
158
|
+
case 'seam_rebase': {
|
|
159
|
+
const r = await client.rebase(needQuilt(), needSquare(), { ...(inFolder ? { expect_head: block.head } : {}), onto: args.onto })
|
|
160
|
+
return (r.noop ? 'Already on the tip.' : describeWrite(r, 'Rebased')) + (r.square ? await sync(r.square.head || r.square.base, r.square) : '')
|
|
161
|
+
}
|
|
162
|
+
case 'seam_revert': {
|
|
163
|
+
const r = await client.revert(needQuilt(), { rev: args.rev, line: args.line })
|
|
164
|
+
return describeWrite(r, 'Reverted in a new square') + `\nMerge it with seam_merge (square: "${r.square.short}").`
|
|
165
|
+
}
|
|
166
|
+
case 'seam_cherry_pick': {
|
|
167
|
+
const r = await client.cherryPick(needQuilt(), needSquare(), { from: args.from, ...(inFolder ? { expect_head: block.head } : {}) })
|
|
168
|
+
return describeWrite(r, 'Picked') + await sync(r.square.head, r.square)
|
|
169
|
+
}
|
|
170
|
+
case 'seam_resolve': {
|
|
171
|
+
const r = await client.resolve(needQuilt(), needSquare(), { ...(inFolder ? { expect_head: block.head } : {}), entries: args.entries })
|
|
172
|
+
return describeWrite(r, 'Resolved') + await sync(r.square.head, r.square)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return `Unknown tool ${name}.`
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e.code === 'head_moved') return `${e.message}: the square's head is now ${String(e.head || '').slice(0, 12)}. Look again (seam_square_status) and retry.`
|
|
178
|
+
throw e
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether this process offers the source-control tools (QUILT_SOURCE, as the API's routes). */
|
|
183
|
+
export const quiltToolsOn = (env = process.env) => /^(1|true|yes)$/i.test(env.QUILT_SOURCE || '')
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Registers every tool on an MCP server. `client()` makes the quilt client for a call (it may
|
|
187
|
+
* throw when the caller is not signed in); `found()` the block folder, or null.
|
|
188
|
+
*/
|
|
189
|
+
export function registerSeamTools (server, { client, found = () => null, extraInput = {} }) {
|
|
190
|
+
for (const name of SEAM_TOOLS) {
|
|
191
|
+
server.registerTool(name, { description: SEAM_DESCRIPTIONS[name], inputSchema: { ...SEAM_INPUTS[name], ...extraInput } }, async (args = {}) => {
|
|
192
|
+
try {
|
|
193
|
+
const { session, ...rest } = args
|
|
194
|
+
void session
|
|
195
|
+
return { content: [{ type: 'text', text: await runSeamTool(name, rest, { client: client(), found: found() }) }] }
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return { content: [{ type: 'text', text: e.message }], isError: true }
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// A port of git's xdiff line diff (Myers with xdiff's heuristics, record cleanup, and change
|
|
2
|
+
// compaction with the indent heuristic), so `blame` and `diff` choose the same lines git does.
|
|
3
|
+
//
|
|
4
|
+
// Ported from git's xdiff/xprepare.c, xdiff/xdiffi.c and xdiff-interface.c (trim_common_tail).
|
|
5
|
+
// LibXDiff by Davide Libenzi, Copyright (C) 2003, LGPL-2.1-or-later; git's changes are under the
|
|
6
|
+
// same terms. Structure and names follow the C so the two can be compared line by line.
|
|
7
|
+
|
|
8
|
+
const XDL_KPDIS_RUN = 4
|
|
9
|
+
const XDL_MAX_EQLIMIT = 1024
|
|
10
|
+
const XDL_SIMSCAN_WINDOW = 100
|
|
11
|
+
const XDL_MAX_COST_MIN = 256
|
|
12
|
+
const XDL_HEUR_MIN_COST = 256
|
|
13
|
+
const XDL_LINE_MAX = Number.MAX_SAFE_INTEGER
|
|
14
|
+
const XDL_SNAKE_CNT = 20
|
|
15
|
+
const XDL_K_HEUR = 4
|
|
16
|
+
const DISCARD = 0; const KEEP = 1; const INVESTIGATE = 2
|
|
17
|
+
|
|
18
|
+
function bogosqrt (n) { let i = 1; for (; n > 0; n = Math.floor(n / 4)) i *= 2; return i }
|
|
19
|
+
|
|
20
|
+
/** git's trim_common_tail: the byte length to drop from both ends' tails (blame only). */
|
|
21
|
+
function commonTail (a, b) {
|
|
22
|
+
const blk = 1024
|
|
23
|
+
let trimmed = 0; let recovered = 0
|
|
24
|
+
const smaller = Math.min(a.length, b.length)
|
|
25
|
+
while (blk + trimmed <= smaller &&
|
|
26
|
+
a.compare(b, b.length - trimmed - blk, b.length - trimmed, a.length - trimmed - blk, a.length - trimmed) === 0) trimmed += blk
|
|
27
|
+
const ap = a.length - trimmed
|
|
28
|
+
while (recovered < trimmed) if (a[ap + recovered++] === 10) break
|
|
29
|
+
return trimmed - recovered
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Records of a buffer: [start, end) per line, each with its \n. */
|
|
33
|
+
function records (buf, size) {
|
|
34
|
+
const starts = []
|
|
35
|
+
let s = 0
|
|
36
|
+
while (s < size) {
|
|
37
|
+
let e = buf.indexOf(10, s)
|
|
38
|
+
e = e < 0 || e >= size ? size : e + 1
|
|
39
|
+
starts.push(s, e)
|
|
40
|
+
s = e
|
|
41
|
+
}
|
|
42
|
+
return starts
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function prepare (buf, size, classes, counts, pass) {
|
|
46
|
+
const r = records(buf, size)
|
|
47
|
+
const n = r.length / 2
|
|
48
|
+
const mph = new Int32Array(n)
|
|
49
|
+
for (let i = 0; i < n; i++) {
|
|
50
|
+
const key = buf.toString('latin1', r[2 * i], r[2 * i + 1])
|
|
51
|
+
let id = classes.get(key)
|
|
52
|
+
if (id === undefined) { id = classes.size; classes.set(key, id); counts.push(0, 0) }
|
|
53
|
+
counts[2 * id + pass]++
|
|
54
|
+
mph[i] = id
|
|
55
|
+
}
|
|
56
|
+
return { buf, r, nrec: n, mph, changed: new Uint8Array(n + 2), ref: null, nreff: 0, dstart: 0, dend: n - 1 }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cleanMmatch (action, i, len) {
|
|
60
|
+
let s = 0; let e = len - 1
|
|
61
|
+
if (i - s > XDL_SIMSCAN_WINDOW) s = i - XDL_SIMSCAN_WINDOW
|
|
62
|
+
if (e - i > XDL_SIMSCAN_WINDOW) e = i + XDL_SIMSCAN_WINDOW
|
|
63
|
+
let r; let rdis0 = 0; let rpdis0 = 1; let rdis1 = 0; let rpdis1 = 1
|
|
64
|
+
for (r = 1; i - r >= s; r++) {
|
|
65
|
+
const a = action[i - r]
|
|
66
|
+
if (a === DISCARD) rdis0++
|
|
67
|
+
else if (a === INVESTIGATE) rpdis0++
|
|
68
|
+
else break
|
|
69
|
+
}
|
|
70
|
+
if (rdis0 === 0) return false
|
|
71
|
+
for (r = 1; i + r <= e; r++) {
|
|
72
|
+
const a = action[i + r]
|
|
73
|
+
if (a === DISCARD) rdis1++
|
|
74
|
+
else if (a === INVESTIGATE) rpdis1++
|
|
75
|
+
else break
|
|
76
|
+
}
|
|
77
|
+
if (rdis1 === 0) return false
|
|
78
|
+
rdis1 += rdis0
|
|
79
|
+
rpdis1 += rpdis0
|
|
80
|
+
return rpdis1 * XDL_KPDIS_RUN < rpdis1 + rdis1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function trimEnds (x1, x2) {
|
|
84
|
+
const lim = Math.min(x1.nrec, x2.nrec)
|
|
85
|
+
let i = 0
|
|
86
|
+
while (i < lim && x1.mph[i] === x2.mph[i]) i++
|
|
87
|
+
x1.dstart = x2.dstart = i
|
|
88
|
+
const lim2 = lim - i
|
|
89
|
+
let j = 0
|
|
90
|
+
while (j < lim2 && x1.mph[x1.nrec - 1 - j] === x2.mph[x2.nrec - 1 - j]) j++
|
|
91
|
+
x1.dend = x1.nrec - j - 1
|
|
92
|
+
x2.dend = x2.nrec - j - 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cleanupRecords (counts, x1, x2) {
|
|
96
|
+
const off = x1.dstart
|
|
97
|
+
const len1 = x1.dend - off + 1
|
|
98
|
+
const len2 = x2.dend - off + 1
|
|
99
|
+
const one = (x, len, other) => {
|
|
100
|
+
const action = new Uint8Array(Math.max(len, 0))
|
|
101
|
+
let mlim = bogosqrt(x.nrec)
|
|
102
|
+
if (mlim > XDL_MAX_EQLIMIT) mlim = XDL_MAX_EQLIMIT
|
|
103
|
+
for (let i = 0; i < len; i++) {
|
|
104
|
+
const nm = counts[2 * x.mph[i + off] + other]
|
|
105
|
+
action[i] = nm === 0 ? DISCARD : nm < mlim ? KEEP : INVESTIGATE
|
|
106
|
+
}
|
|
107
|
+
x.ref = new Int32Array(Math.max(len, 0))
|
|
108
|
+
x.nreff = 0
|
|
109
|
+
for (let i = 0; i < len; i++) {
|
|
110
|
+
let a = action[i]
|
|
111
|
+
if (a === INVESTIGATE) a = cleanMmatch(action, i, len) ? DISCARD : KEEP
|
|
112
|
+
if (a === KEEP) x.ref[x.nreff++] = i + off
|
|
113
|
+
else x.changed[i + off + 1] = 1
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
one(x1, len1, 1)
|
|
117
|
+
one(x2, len2, 0)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function doDiff (x1, x2) {
|
|
121
|
+
const h1 = new Int32Array(x1.nreff); for (let i = 0; i < x1.nreff; i++) h1[i] = x1.mph[x1.ref[i]]
|
|
122
|
+
const h2 = new Int32Array(x2.nreff); for (let i = 0; i < x2.nreff; i++) h2[i] = x2.mph[x2.ref[i]]
|
|
123
|
+
const ndiags = x1.nreff + x2.nreff + 3
|
|
124
|
+
const kvd = new Float64Array(2 * ndiags + 2)
|
|
125
|
+
const F = x2.nreff + 1
|
|
126
|
+
const B = ndiags + x2.nreff + 1
|
|
127
|
+
let mxcost = bogosqrt(ndiags)
|
|
128
|
+
if (mxcost < XDL_MAX_COST_MIN) mxcost = XDL_MAX_COST_MIN
|
|
129
|
+
const snakeCnt = XDL_SNAKE_CNT; const heurMin = XDL_HEUR_MIN_COST
|
|
130
|
+
const spl = { i1: 0, i2: 0, minLo: 0, minHi: 0 }
|
|
131
|
+
|
|
132
|
+
function split (off1, lim1, off2, lim2, needMin) {
|
|
133
|
+
const dmin = off1 - lim2; const dmax = lim1 - off2
|
|
134
|
+
const fmid = off1 - off2; const bmid = lim1 - lim2
|
|
135
|
+
const odd = (fmid - bmid) & 1
|
|
136
|
+
let fmin = fmid; let fmax = fmid; let bmin = bmid; let bmax = bmid
|
|
137
|
+
let i1, i2, prev1, best, dd, v, k, d
|
|
138
|
+
kvd[F + fmid] = off1
|
|
139
|
+
kvd[B + bmid] = lim1
|
|
140
|
+
for (let ec = 1; ; ec++) {
|
|
141
|
+
let gotSnake = false
|
|
142
|
+
if (fmin > dmin) kvd[F + (--fmin) - 1] = -1; else ++fmin
|
|
143
|
+
if (fmax < dmax) kvd[F + (++fmax) + 1] = -1; else --fmax
|
|
144
|
+
for (d = fmax; d >= fmin; d -= 2) {
|
|
145
|
+
if (kvd[F + d - 1] >= kvd[F + d + 1]) i1 = kvd[F + d - 1] + 1
|
|
146
|
+
else i1 = kvd[F + d + 1]
|
|
147
|
+
prev1 = i1
|
|
148
|
+
i2 = i1 - d
|
|
149
|
+
while (i1 < lim1 && i2 < lim2 && h1[i1] === h2[i2]) { i1++; i2++ }
|
|
150
|
+
if (i1 - prev1 > snakeCnt) gotSnake = true
|
|
151
|
+
kvd[F + d] = i1
|
|
152
|
+
if (odd && bmin <= d && d <= bmax && kvd[B + d] <= i1) {
|
|
153
|
+
spl.i1 = i1; spl.i2 = i2; spl.minLo = spl.minHi = 1
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (bmin > dmin) kvd[B + (--bmin) - 1] = XDL_LINE_MAX; else ++bmin
|
|
158
|
+
if (bmax < dmax) kvd[B + (++bmax) + 1] = XDL_LINE_MAX; else --bmax
|
|
159
|
+
for (d = bmax; d >= bmin; d -= 2) {
|
|
160
|
+
if (kvd[B + d - 1] < kvd[B + d + 1]) i1 = kvd[B + d - 1]
|
|
161
|
+
else i1 = kvd[B + d + 1] - 1
|
|
162
|
+
prev1 = i1
|
|
163
|
+
i2 = i1 - d
|
|
164
|
+
while (i1 > off1 && i2 > off2 && h1[i1 - 1] === h2[i2 - 1]) { i1--; i2-- }
|
|
165
|
+
if (prev1 - i1 > snakeCnt) gotSnake = true
|
|
166
|
+
kvd[B + d] = i1
|
|
167
|
+
if (!odd && fmin <= d && d <= fmax && i1 <= kvd[F + d]) {
|
|
168
|
+
spl.i1 = i1; spl.i2 = i2; spl.minLo = spl.minHi = 1
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (needMin) continue
|
|
173
|
+
if (gotSnake && ec > heurMin) {
|
|
174
|
+
for (best = 0, d = fmax; d >= fmin; d -= 2) {
|
|
175
|
+
dd = d > fmid ? d - fmid : fmid - d
|
|
176
|
+
i1 = kvd[F + d]
|
|
177
|
+
i2 = i1 - d
|
|
178
|
+
v = (i1 - off1) + (i2 - off2) - dd
|
|
179
|
+
if (v > XDL_K_HEUR * ec && v > best && off1 + snakeCnt <= i1 && i1 < lim1 && off2 + snakeCnt <= i2 && i2 < lim2) {
|
|
180
|
+
for (k = 1; h1[i1 - k] === h2[i2 - k]; k++) {
|
|
181
|
+
if (k === snakeCnt) { best = v; spl.i1 = i1; spl.i2 = i2; break }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (best > 0) { spl.minLo = 1; spl.minHi = 0; return }
|
|
186
|
+
for (best = 0, d = bmax; d >= bmin; d -= 2) {
|
|
187
|
+
dd = d > bmid ? d - bmid : bmid - d
|
|
188
|
+
i1 = kvd[B + d]
|
|
189
|
+
i2 = i1 - d
|
|
190
|
+
v = (lim1 - i1) + (lim2 - i2) - dd
|
|
191
|
+
if (v > XDL_K_HEUR * ec && v > best && off1 < i1 && i1 <= lim1 - snakeCnt && off2 < i2 && i2 <= lim2 - snakeCnt) {
|
|
192
|
+
for (k = 0; h1[i1 + k] === h2[i2 + k]; k++) {
|
|
193
|
+
if (k === snakeCnt - 1) { best = v; spl.i1 = i1; spl.i2 = i2; break }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (best > 0) { spl.minLo = 0; spl.minHi = 1; return }
|
|
198
|
+
}
|
|
199
|
+
if (ec >= mxcost) {
|
|
200
|
+
let fbest = -1; let fbest1 = -1
|
|
201
|
+
for (d = fmax; d >= fmin; d -= 2) {
|
|
202
|
+
i1 = Math.min(kvd[F + d], lim1)
|
|
203
|
+
i2 = i1 - d
|
|
204
|
+
if (lim2 < i2) { i1 = lim2 + d; i2 = lim2 }
|
|
205
|
+
if (fbest < i1 + i2) { fbest = i1 + i2; fbest1 = i1 }
|
|
206
|
+
}
|
|
207
|
+
let bbest = XDL_LINE_MAX; let bbest1 = XDL_LINE_MAX
|
|
208
|
+
for (d = bmax; d >= bmin; d -= 2) {
|
|
209
|
+
i1 = Math.max(off1, kvd[B + d])
|
|
210
|
+
i2 = i1 - d
|
|
211
|
+
if (i2 < off2) { i1 = off2 + d; i2 = off2 }
|
|
212
|
+
if (i1 + i2 < bbest) { bbest = i1 + i2; bbest1 = i1 }
|
|
213
|
+
}
|
|
214
|
+
if ((lim1 + lim2) - bbest < fbest - (off1 + off2)) {
|
|
215
|
+
spl.i1 = fbest1; spl.i2 = fbest - fbest1; spl.minLo = 1; spl.minHi = 0
|
|
216
|
+
} else {
|
|
217
|
+
spl.i1 = bbest1; spl.i2 = bbest - bbest1; spl.minLo = 0; spl.minHi = 1
|
|
218
|
+
}
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// xdl_recs_cmp, with its recursion on an explicit stack (depth can reach thousands).
|
|
225
|
+
const stack = [[0, x1.nreff, 0, x2.nreff, 0]]
|
|
226
|
+
while (stack.length) {
|
|
227
|
+
let [off1, lim1, off2, lim2, needMin] = stack.pop()
|
|
228
|
+
while (off1 < lim1 && off2 < lim2 && h1[off1] === h2[off2]) { off1++; off2++ }
|
|
229
|
+
while (off1 < lim1 && off2 < lim2 && h1[lim1 - 1] === h2[lim2 - 1]) { lim1--; lim2-- }
|
|
230
|
+
if (off1 === lim1) {
|
|
231
|
+
for (; off2 < lim2; off2++) x2.changed[x2.ref[off2] + 1] = 1
|
|
232
|
+
} else if (off2 === lim2) {
|
|
233
|
+
for (; off1 < lim1; off1++) x1.changed[x1.ref[off1] + 1] = 1
|
|
234
|
+
} else {
|
|
235
|
+
spl.i1 = spl.i2 = 0
|
|
236
|
+
split(off1, lim1, off2, lim2, needMin)
|
|
237
|
+
const { i1, i2, minLo, minHi } = spl
|
|
238
|
+
// Low half first, as the C recursion does (it only marks, so order matters not for the
|
|
239
|
+
// result, but keep it for fidelity).
|
|
240
|
+
stack.push([i1, lim1, i2, lim2, minHi])
|
|
241
|
+
stack.push([off1, i1, off2, i2, minLo])
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- change compaction (xdl_change_compact) with the indent heuristic ---
|
|
247
|
+
|
|
248
|
+
const MAX_INDENT = 200
|
|
249
|
+
const MAX_BLANKS = 20
|
|
250
|
+
const START_OF_FILE_PENALTY = 1
|
|
251
|
+
const END_OF_FILE_PENALTY = 21
|
|
252
|
+
const TOTAL_BLANK_WEIGHT = -30
|
|
253
|
+
const POST_BLANK_WEIGHT = 6
|
|
254
|
+
const RELATIVE_INDENT_PENALTY = -4
|
|
255
|
+
const RELATIVE_INDENT_WITH_BLANK_PENALTY = 10
|
|
256
|
+
const RELATIVE_OUTDENT_PENALTY = 24
|
|
257
|
+
const RELATIVE_OUTDENT_WITH_BLANK_PENALTY = 17
|
|
258
|
+
const RELATIVE_DEDENT_PENALTY = 23
|
|
259
|
+
const RELATIVE_DEDENT_WITH_BLANK_PENALTY = 17
|
|
260
|
+
const INDENT_WEIGHT = 60
|
|
261
|
+
const INDENT_HEURISTIC_MAX_SLIDING = 100
|
|
262
|
+
|
|
263
|
+
function getIndent (x, i) {
|
|
264
|
+
let ret = 0
|
|
265
|
+
const buf = x.buf
|
|
266
|
+
for (let p = x.r[2 * i], e = x.r[2 * i + 1]; p < e; p++) {
|
|
267
|
+
const c = buf[p]
|
|
268
|
+
// isspace: space, \t, \n, \v, \f, \r
|
|
269
|
+
if (!(c === 32 || (c >= 9 && c <= 13))) return ret
|
|
270
|
+
else if (c === 32) ret += 1
|
|
271
|
+
else if (c === 9) ret += 8 - ret % 8
|
|
272
|
+
if (ret >= MAX_INDENT) return MAX_INDENT
|
|
273
|
+
}
|
|
274
|
+
return -1
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function measureSplit (x, split) {
|
|
278
|
+
const m = { eof: 0, indent: -1, preBlank: 0, preIndent: -1, postBlank: 0, postIndent: -1 }
|
|
279
|
+
if (split >= x.nrec) { m.eof = 1; m.indent = -1 } else m.indent = getIndent(x, split)
|
|
280
|
+
for (let i = split - 1; i >= 0; i--) {
|
|
281
|
+
m.preIndent = getIndent(x, i)
|
|
282
|
+
if (m.preIndent !== -1) break
|
|
283
|
+
m.preBlank += 1
|
|
284
|
+
if (m.preBlank === MAX_BLANKS) { m.preIndent = 0; break }
|
|
285
|
+
}
|
|
286
|
+
for (let i = split + 1; i < x.nrec; i++) {
|
|
287
|
+
m.postIndent = getIndent(x, i)
|
|
288
|
+
if (m.postIndent !== -1) break
|
|
289
|
+
m.postBlank += 1
|
|
290
|
+
if (m.postBlank === MAX_BLANKS) { m.postIndent = 0; break }
|
|
291
|
+
}
|
|
292
|
+
return m
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function scoreAddSplit (m, s) {
|
|
296
|
+
if (m.preIndent === -1 && m.preBlank === 0) s.penalty += START_OF_FILE_PENALTY
|
|
297
|
+
if (m.eof) s.penalty += END_OF_FILE_PENALTY
|
|
298
|
+
const postBlank = m.indent === -1 ? 1 + m.postBlank : 0
|
|
299
|
+
const totalBlank = m.preBlank + postBlank
|
|
300
|
+
s.penalty += TOTAL_BLANK_WEIGHT * totalBlank
|
|
301
|
+
s.penalty += POST_BLANK_WEIGHT * postBlank
|
|
302
|
+
const indent = m.indent !== -1 ? m.indent : m.postIndent
|
|
303
|
+
const anyBlanks = totalBlank !== 0
|
|
304
|
+
s.effectiveIndent += indent
|
|
305
|
+
if (indent === -1 || m.preIndent === -1) {
|
|
306
|
+
// no adjustment
|
|
307
|
+
} else if (indent > m.preIndent) {
|
|
308
|
+
s.penalty += anyBlanks ? RELATIVE_INDENT_WITH_BLANK_PENALTY : RELATIVE_INDENT_PENALTY
|
|
309
|
+
} else if (indent === m.preIndent) {
|
|
310
|
+
// same level
|
|
311
|
+
} else if (m.postIndent !== -1 && m.postIndent > indent) {
|
|
312
|
+
s.penalty += anyBlanks ? RELATIVE_OUTDENT_WITH_BLANK_PENALTY : RELATIVE_OUTDENT_PENALTY
|
|
313
|
+
} else {
|
|
314
|
+
s.penalty += anyBlanks ? RELATIVE_DEDENT_WITH_BLANK_PENALTY : RELATIVE_DEDENT_PENALTY
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function scoreCmp (s1, s2) {
|
|
319
|
+
const c = (s1.effectiveIndent > s2.effectiveIndent) - (s1.effectiveIndent < s2.effectiveIndent)
|
|
320
|
+
return INDENT_WEIGHT * c + (s1.penalty - s2.penalty)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// `changed` is offset by one: changed[i + 1] is record i; changed[0] and changed[nrec + 1] are 0.
|
|
324
|
+
function changeCompact (x, xo) {
|
|
325
|
+
const ch = x.changed; const cho = xo.changed
|
|
326
|
+
const C = (i) => ch[i + 1]
|
|
327
|
+
const CO = (i) => cho[i + 1]
|
|
328
|
+
const g = { start: 0, end: 0 }; const go = { start: 0, end: 0 }
|
|
329
|
+
while (C(g.end)) g.end++
|
|
330
|
+
while (CO(go.end)) go.end++
|
|
331
|
+
const next = (xx, c, gg) => {
|
|
332
|
+
if (gg.end === xx.nrec) return -1
|
|
333
|
+
gg.start = gg.end + 1
|
|
334
|
+
for (gg.end = gg.start; c(gg.end); gg.end++);
|
|
335
|
+
return 0
|
|
336
|
+
}
|
|
337
|
+
const previous = (c, gg) => {
|
|
338
|
+
if (gg.start === 0) return -1
|
|
339
|
+
gg.end = gg.start - 1
|
|
340
|
+
for (gg.start = gg.end; c(gg.start - 1); gg.start--);
|
|
341
|
+
return 0
|
|
342
|
+
}
|
|
343
|
+
const slideDown = () => {
|
|
344
|
+
if (g.end < x.nrec && x.mph[g.start] === x.mph[g.end]) {
|
|
345
|
+
ch[g.start++ + 1] = 0
|
|
346
|
+
ch[g.end++ + 1] = 1
|
|
347
|
+
while (C(g.end)) g.end++
|
|
348
|
+
return 0
|
|
349
|
+
}
|
|
350
|
+
return -1
|
|
351
|
+
}
|
|
352
|
+
const slideUp = () => {
|
|
353
|
+
if (g.start > 0 && x.mph[g.start - 1] === x.mph[g.end - 1]) {
|
|
354
|
+
ch[--g.start + 1] = 1
|
|
355
|
+
ch[--g.end + 1] = 0
|
|
356
|
+
while (C(g.start - 1)) g.start--
|
|
357
|
+
return 0
|
|
358
|
+
}
|
|
359
|
+
return -1
|
|
360
|
+
}
|
|
361
|
+
const bug = (s) => { throw new Error('xdiff: ' + s) }
|
|
362
|
+
for (;;) {
|
|
363
|
+
if (g.end !== g.start) {
|
|
364
|
+
let groupsize, earliestEnd, endMatchingOther
|
|
365
|
+
do {
|
|
366
|
+
groupsize = g.end - g.start
|
|
367
|
+
endMatchingOther = -1
|
|
368
|
+
while (!slideUp()) if (previous(CO, go)) bug('group sync broken sliding up')
|
|
369
|
+
earliestEnd = g.end
|
|
370
|
+
if (go.end > go.start) endMatchingOther = g.end
|
|
371
|
+
for (;;) {
|
|
372
|
+
if (slideDown()) break
|
|
373
|
+
if (next(xo, CO, go)) bug('group sync broken sliding down')
|
|
374
|
+
if (go.end > go.start) endMatchingOther = g.end
|
|
375
|
+
}
|
|
376
|
+
} while (groupsize !== g.end - g.start)
|
|
377
|
+
if (g.end === earliestEnd) {
|
|
378
|
+
// no shifting possible
|
|
379
|
+
} else if (endMatchingOther !== -1) {
|
|
380
|
+
while (go.end === go.start) {
|
|
381
|
+
if (slideUp()) bug('match disappeared')
|
|
382
|
+
if (previous(CO, go)) bug('group sync broken sliding to match')
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
let shift = earliestEnd
|
|
386
|
+
let bestShift = -1; let bestScore = null
|
|
387
|
+
if (g.end - groupsize - 1 > shift) shift = g.end - groupsize - 1
|
|
388
|
+
if (g.end - INDENT_HEURISTIC_MAX_SLIDING > shift) shift = g.end - INDENT_HEURISTIC_MAX_SLIDING
|
|
389
|
+
for (; shift <= g.end; shift++) {
|
|
390
|
+
const score = { effectiveIndent: 0, penalty: 0 }
|
|
391
|
+
scoreAddSplit(measureSplit(x, shift), score)
|
|
392
|
+
scoreAddSplit(measureSplit(x, shift - groupsize), score)
|
|
393
|
+
if (bestShift === -1 || scoreCmp(score, bestScore) <= 0) { bestScore = score; bestShift = shift }
|
|
394
|
+
}
|
|
395
|
+
while (g.end > bestShift) {
|
|
396
|
+
if (slideUp()) bug('best shift unreached')
|
|
397
|
+
if (previous(CO, go)) bug('group sync broken sliding to blank line')
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (next(x, C, g)) break
|
|
402
|
+
if (next(xo, CO, go)) bug('group sync broken moving to next group')
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* git's diff of two byte buffers: hunks [{ a: [start, len], b: [start, len] }] over their lines
|
|
408
|
+
* (a final line without \n counts as different from the same line with one). `trimTail`
|
|
409
|
+
* reproduces what git does for a zero-context diff (blame): drop the common tail in 1 KiB blocks.
|
|
410
|
+
*/
|
|
411
|
+
export function xdiff (a, b, { trimTail = false } = {}) {
|
|
412
|
+
let sa = a.length; let sb = b.length
|
|
413
|
+
if (trimTail) { const t = commonTail(a, b); sa -= t; sb -= t }
|
|
414
|
+
const classes = new Map(); const counts = []
|
|
415
|
+
const x1 = prepare(a, sa, classes, counts, 0)
|
|
416
|
+
const x2 = prepare(b, sb, classes, counts, 1)
|
|
417
|
+
trimEnds(x1, x2)
|
|
418
|
+
cleanupRecords(counts, x1, x2)
|
|
419
|
+
doDiff(x1, x2)
|
|
420
|
+
changeCompact(x1, x2)
|
|
421
|
+
changeCompact(x2, x1)
|
|
422
|
+
// xdl_build_script, forwards.
|
|
423
|
+
const out = []
|
|
424
|
+
const c1 = x1.changed; const c2 = x2.changed
|
|
425
|
+
let i1 = 0; let i2 = 0
|
|
426
|
+
while (i1 < x1.nrec || i2 < x2.nrec) {
|
|
427
|
+
if (c1[i1 + 1] || c2[i2 + 1]) {
|
|
428
|
+
const s1 = i1; const s2 = i2
|
|
429
|
+
while (c1[i1 + 1]) i1++
|
|
430
|
+
while (c2[i2 + 1]) i2++
|
|
431
|
+
out.push({ a: [s1, i1 - s1], b: [s2, i2 - s2] })
|
|
432
|
+
} else { i1++; i2++ }
|
|
433
|
+
}
|
|
434
|
+
return out
|
|
435
|
+
}
|