@ottocode/sdk 0.1.265 → 0.1.267
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/package.json +2 -2
- package/src/config/src/index.ts +4 -0
- package/src/config/src/manager.ts +8 -14
- package/src/config/src/paths.ts +4 -0
- package/src/core/src/providers/resolver.ts +29 -70
- package/src/core/src/tools/bin-manager/cache.ts +13 -0
- package/src/core/src/tools/bin-manager/filesystem.ts +32 -0
- package/src/core/src/tools/bin-manager/paths.ts +36 -0
- package/src/core/src/tools/bin-manager/vendor.ts +80 -0
- package/src/core/src/tools/bin-manager.ts +14 -140
- package/src/core/src/tools/builtin/patch/apply-hunk.ts +308 -0
- package/src/core/src/tools/builtin/patch/apply-report.ts +99 -0
- package/src/core/src/tools/builtin/patch/apply.ts +6 -663
- package/src/core/src/tools/builtin/patch/hunk-header.ts +17 -0
- package/src/core/src/tools/builtin/patch/indentation.ts +160 -0
- package/src/core/src/tools/builtin/patch/matching.ts +58 -0
- package/src/core/src/tools/builtin/patch/parse-enveloped.ts +10 -72
- package/src/core/src/tools/builtin/patch/parse-unified.ts +15 -105
- package/src/core/src/tools/builtin/patch/replace-builder.ts +64 -0
- package/src/core/src/tools/builtin/patch/unified-state.ts +86 -0
- package/src/core/src/tools/builtin/websearch-strategies.ts +197 -0
- package/src/core/src/tools/builtin/websearch.ts +9 -187
- package/src/core/src/tools/loader.ts +6 -49
- package/src/core/src/tools/plugin-discovery.ts +86 -0
- package/src/core/src/utils/logger/format.ts +50 -0
- package/src/core/src/utils/logger/sinks.ts +61 -0
- package/src/core/src/utils/logger.ts +2 -119
- package/src/index.ts +3 -0
- package/src/providers/src/index.ts +4 -0
- package/src/providers/src/model-resolution.ts +21 -0
- package/src/providers/src/zai-client.ts +5 -2
|
@@ -1,37 +1,23 @@
|
|
|
1
1
|
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
NORMALIZATION_LEVELS,
|
|
6
|
-
normalizeWhitespace,
|
|
7
|
-
computeIndentDelta,
|
|
8
|
-
applyIndentDelta,
|
|
9
|
-
getLeadingWhitespace,
|
|
10
|
-
detectIndentStyle,
|
|
11
|
-
expandWhitespace,
|
|
12
|
-
inferTabSizeFromPairs,
|
|
13
|
-
} from './normalize.ts';
|
|
14
|
-
import {
|
|
15
|
-
PATCH_ADD_PREFIX,
|
|
16
|
-
PATCH_DELETE_PREFIX,
|
|
17
|
-
PATCH_UPDATE_PREFIX,
|
|
18
|
-
PATCH_BEGIN_MARKER,
|
|
19
|
-
PATCH_END_MARKER,
|
|
20
|
-
} from './constants.ts';
|
|
4
|
+
import { applyHunkToLines } from './apply-hunk.ts';
|
|
21
5
|
import type {
|
|
22
6
|
AppliedPatchHunk,
|
|
23
7
|
AppliedPatchOperation,
|
|
24
8
|
PatchAddOperation,
|
|
25
9
|
PatchApplicationResult,
|
|
26
10
|
PatchDeleteOperation,
|
|
27
|
-
PatchHunk,
|
|
28
|
-
PatchHunkLine,
|
|
29
11
|
PatchOperation,
|
|
30
|
-
PatchSummary,
|
|
31
12
|
PatchUpdateOperation,
|
|
32
13
|
RejectedPatch,
|
|
33
14
|
} from './types.ts';
|
|
34
15
|
import { ensureTrailingNewline, joinLines, splitLines } from './text.ts';
|
|
16
|
+
import {
|
|
17
|
+
formatNormalizedPatch,
|
|
18
|
+
makeAppliedRecord,
|
|
19
|
+
makeSummary,
|
|
20
|
+
} from './apply-report.ts';
|
|
35
21
|
|
|
36
22
|
export function resolveProjectPath(
|
|
37
23
|
projectRoot: string,
|
|
@@ -45,649 +31,6 @@ export function resolveProjectPath(
|
|
|
45
31
|
return fullPath;
|
|
46
32
|
}
|
|
47
33
|
|
|
48
|
-
function makeAppliedRecord(
|
|
49
|
-
kind: AppliedPatchOperation['kind'],
|
|
50
|
-
filePath: string,
|
|
51
|
-
hunks: AppliedPatchHunk[],
|
|
52
|
-
): AppliedPatchOperation {
|
|
53
|
-
const stats = hunks.reduce(
|
|
54
|
-
(acc, hunk) => ({
|
|
55
|
-
additions: acc.additions + hunk.additions,
|
|
56
|
-
deletions: acc.deletions + hunk.deletions,
|
|
57
|
-
}),
|
|
58
|
-
{ additions: 0, deletions: 0 },
|
|
59
|
-
);
|
|
60
|
-
return {
|
|
61
|
-
kind,
|
|
62
|
-
filePath,
|
|
63
|
-
stats,
|
|
64
|
-
hunks,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function makeSummary(operations: AppliedPatchOperation[]): PatchSummary {
|
|
69
|
-
return operations.reduce<PatchSummary>(
|
|
70
|
-
(acc, op) => ({
|
|
71
|
-
files: acc.files + 1,
|
|
72
|
-
additions: acc.additions + op.stats.additions,
|
|
73
|
-
deletions: acc.deletions + op.stats.deletions,
|
|
74
|
-
}),
|
|
75
|
-
{ files: 0, additions: 0, deletions: 0 },
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function serializePatchLine(line: PatchHunkLine): string {
|
|
80
|
-
switch (line.kind) {
|
|
81
|
-
case 'add':
|
|
82
|
-
return `+${line.content}`;
|
|
83
|
-
case 'remove':
|
|
84
|
-
return `-${line.content}`;
|
|
85
|
-
default:
|
|
86
|
-
return ` ${line.content}`;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function formatRange(start: number, count: number) {
|
|
91
|
-
const normalizedStart = Math.max(0, start);
|
|
92
|
-
if (count === 0) return `${normalizedStart},0`;
|
|
93
|
-
if (count === 1) return `${normalizedStart}`;
|
|
94
|
-
return `${normalizedStart},${count}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function formatHunkHeader(hunk: AppliedPatchHunk) {
|
|
98
|
-
const oldRange = formatRange(hunk.oldStart, hunk.oldLines);
|
|
99
|
-
const newRange = formatRange(hunk.newStart, hunk.newLines);
|
|
100
|
-
const context = hunk.header.context?.trim();
|
|
101
|
-
return context
|
|
102
|
-
? `@@ -${oldRange} +${newRange} @@ ${context}`
|
|
103
|
-
: `@@ -${oldRange} +${newRange} @@`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function formatNormalizedPatch(operations: AppliedPatchOperation[]): string {
|
|
107
|
-
const lines: string[] = [PATCH_BEGIN_MARKER];
|
|
108
|
-
for (const op of operations) {
|
|
109
|
-
switch (op.kind) {
|
|
110
|
-
case 'add':
|
|
111
|
-
lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
|
|
112
|
-
break;
|
|
113
|
-
case 'delete':
|
|
114
|
-
lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
|
|
115
|
-
break;
|
|
116
|
-
case 'update':
|
|
117
|
-
lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (op.kind === 'add' || op.kind === 'delete') {
|
|
122
|
-
for (const hunk of op.hunks) {
|
|
123
|
-
lines.push(formatHunkHeader(hunk));
|
|
124
|
-
for (const line of hunk.lines) {
|
|
125
|
-
lines.push(serializePatchLine(line));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
for (const hunk of op.hunks) {
|
|
132
|
-
lines.push(formatHunkHeader(hunk));
|
|
133
|
-
for (const line of hunk.lines) {
|
|
134
|
-
lines.push(serializePatchLine(line));
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
lines.push(PATCH_END_MARKER);
|
|
139
|
-
return lines.join('\n');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function findLineIndex(
|
|
143
|
-
lines: string[],
|
|
144
|
-
pattern: string,
|
|
145
|
-
start: number,
|
|
146
|
-
useFuzzy: boolean,
|
|
147
|
-
): number {
|
|
148
|
-
for (let i = Math.max(0, start); i < lines.length; i++) {
|
|
149
|
-
if (lines[i] === pattern) return i;
|
|
150
|
-
if (!useFuzzy) continue;
|
|
151
|
-
for (const level of NORMALIZATION_LEVELS.slice(1)) {
|
|
152
|
-
if (
|
|
153
|
-
normalizeWhitespace(lines[i], level) ===
|
|
154
|
-
normalizeWhitespace(pattern, level)
|
|
155
|
-
) {
|
|
156
|
-
return i;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return -1;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function findSubsequence(
|
|
164
|
-
lines: string[],
|
|
165
|
-
pattern: string[],
|
|
166
|
-
startIndex: number,
|
|
167
|
-
useFuzzy: boolean,
|
|
168
|
-
): number {
|
|
169
|
-
if (pattern.length === 0) return -1;
|
|
170
|
-
const start = Math.max(0, startIndex);
|
|
171
|
-
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
172
|
-
let matches = true;
|
|
173
|
-
for (let j = 0; j < pattern.length; j++) {
|
|
174
|
-
const line = lines[i + j];
|
|
175
|
-
const target = pattern[j];
|
|
176
|
-
if (line === target) continue;
|
|
177
|
-
if (!useFuzzy) {
|
|
178
|
-
matches = false;
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
let matched = false;
|
|
182
|
-
for (const level of NORMALIZATION_LEVELS.slice(1)) {
|
|
183
|
-
if (
|
|
184
|
-
normalizeWhitespace(line, level) ===
|
|
185
|
-
normalizeWhitespace(target, level)
|
|
186
|
-
) {
|
|
187
|
-
matched = true;
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (!matched) {
|
|
192
|
-
matches = false;
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (matches) return i;
|
|
197
|
-
}
|
|
198
|
-
return -1;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function computeInsertionIndex(
|
|
202
|
-
lines: string[],
|
|
203
|
-
header: PatchHunk['header'],
|
|
204
|
-
hint: number,
|
|
205
|
-
): number {
|
|
206
|
-
if (header.context) {
|
|
207
|
-
const contextIndex = findLineIndex(lines, header.context, 0, true);
|
|
208
|
-
if (contextIndex !== -1) return contextIndex + 1;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (typeof header.oldStart === 'number') {
|
|
212
|
-
const zeroBased = Math.max(0, header.oldStart - 1);
|
|
213
|
-
return Math.min(lines.length, zeroBased);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (typeof header.newStart === 'number') {
|
|
217
|
-
const zeroBased = Math.max(0, header.newStart - 1);
|
|
218
|
-
return Math.min(lines.length, zeroBased);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return Math.min(lines.length, Math.max(0, hint));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function lineExists(
|
|
225
|
-
lines: string[],
|
|
226
|
-
target: string,
|
|
227
|
-
useFuzzy: boolean,
|
|
228
|
-
): boolean {
|
|
229
|
-
return findLineIndex(lines, target, 0, useFuzzy) !== -1;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function isHunkAlreadyApplied(
|
|
233
|
-
lines: string[],
|
|
234
|
-
hunk: PatchHunk,
|
|
235
|
-
useFuzzy: boolean,
|
|
236
|
-
): boolean {
|
|
237
|
-
const replacement = hunk.lines
|
|
238
|
-
.filter((line) => line.kind !== 'remove')
|
|
239
|
-
.map((line) => line.content);
|
|
240
|
-
|
|
241
|
-
if (replacement.length > 0) {
|
|
242
|
-
return findSubsequence(lines, replacement, 0, useFuzzy) !== -1;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const removals = hunk.lines.filter((line) => line.kind === 'remove');
|
|
246
|
-
const _additions = hunk.lines
|
|
247
|
-
.filter((line) => line.kind === 'add')
|
|
248
|
-
.map((line) => line.content);
|
|
249
|
-
const _contextLines = hunk.lines
|
|
250
|
-
.filter((line) => line.kind === 'context')
|
|
251
|
-
.map((line) => line.content);
|
|
252
|
-
if (removals.length === 0) return false;
|
|
253
|
-
return removals.every((line) => !lineExists(lines, line.content, useFuzzy));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function adjustReplacementIndentation(
|
|
257
|
-
hunk: PatchHunk,
|
|
258
|
-
matchedFileLines: string[],
|
|
259
|
-
allFileLines?: string[],
|
|
260
|
-
): string[] {
|
|
261
|
-
const result: string[] = [];
|
|
262
|
-
let expectedIdx = 0;
|
|
263
|
-
let lastDelta = 0;
|
|
264
|
-
let lastFileIndentExpanded = 0;
|
|
265
|
-
let lastPatchIndentExpanded = 0;
|
|
266
|
-
let hasDelta = false;
|
|
267
|
-
let hasStyleMismatch = false;
|
|
268
|
-
let fileIndentChar: 'tab' | 'space' = 'space';
|
|
269
|
-
const deltas: number[] = [];
|
|
270
|
-
let hasAddStyleMismatch = false;
|
|
271
|
-
let fileIndentDetected = false;
|
|
272
|
-
|
|
273
|
-
for (const fl of matchedFileLines) {
|
|
274
|
-
const ws = getLeadingWhitespace(fl);
|
|
275
|
-
if (ws.length > 0) {
|
|
276
|
-
fileIndentChar = detectIndentStyle(ws);
|
|
277
|
-
fileIndentDetected = true;
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!fileIndentDetected && allFileLines) {
|
|
283
|
-
for (const fl of allFileLines) {
|
|
284
|
-
const ws = getLeadingWhitespace(fl);
|
|
285
|
-
if (ws.length > 0) {
|
|
286
|
-
fileIndentChar = detectIndentStyle(ws);
|
|
287
|
-
fileIndentDetected = true;
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const patchContextLines = hunk.lines
|
|
294
|
-
.filter((l) => l.kind === 'context' || l.kind === 'remove')
|
|
295
|
-
.map((l) => l.content);
|
|
296
|
-
const tabSize = inferTabSizeFromPairs(patchContextLines, matchedFileLines);
|
|
297
|
-
|
|
298
|
-
let tempIdx = 0;
|
|
299
|
-
for (const line of hunk.lines) {
|
|
300
|
-
if (line.kind === 'context' || line.kind === 'remove') {
|
|
301
|
-
const fileLine = matchedFileLines[tempIdx];
|
|
302
|
-
if (fileLine !== undefined) {
|
|
303
|
-
const d = computeIndentDelta(line.content, fileLine, tabSize);
|
|
304
|
-
if (d !== 0) deltas.push(d);
|
|
305
|
-
}
|
|
306
|
-
tempIdx++;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const sortedDeltas = [...deltas].sort((a, b) => a - b);
|
|
310
|
-
const medianDelta =
|
|
311
|
-
sortedDeltas.length > 0
|
|
312
|
-
? sortedDeltas[Math.floor(sortedDeltas.length / 2)]
|
|
313
|
-
: 0;
|
|
314
|
-
|
|
315
|
-
for (const line of hunk.lines) {
|
|
316
|
-
if (line.kind === 'add' && line.content.trim() !== '') {
|
|
317
|
-
const ws = getLeadingWhitespace(line.content);
|
|
318
|
-
if (ws.length > 0 && detectIndentStyle(ws) !== fileIndentChar) {
|
|
319
|
-
hasAddStyleMismatch = true;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
for (const line of hunk.lines) {
|
|
326
|
-
if (line.kind === 'context') {
|
|
327
|
-
const fileLine = matchedFileLines[expectedIdx];
|
|
328
|
-
if (fileLine !== undefined) {
|
|
329
|
-
lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
|
|
330
|
-
lastFileIndentExpanded = expandWhitespace(
|
|
331
|
-
getLeadingWhitespace(fileLine),
|
|
332
|
-
tabSize,
|
|
333
|
-
);
|
|
334
|
-
lastPatchIndentExpanded = expandWhitespace(
|
|
335
|
-
getLeadingWhitespace(line.content),
|
|
336
|
-
tabSize,
|
|
337
|
-
);
|
|
338
|
-
if (lastDelta !== 0) hasDelta = true;
|
|
339
|
-
if (
|
|
340
|
-
detectIndentStyle(getLeadingWhitespace(fileLine)) !==
|
|
341
|
-
detectIndentStyle(getLeadingWhitespace(line.content)) &&
|
|
342
|
-
getLeadingWhitespace(fileLine).length > 0
|
|
343
|
-
) {
|
|
344
|
-
hasStyleMismatch = true;
|
|
345
|
-
}
|
|
346
|
-
result.push(fileLine);
|
|
347
|
-
} else {
|
|
348
|
-
result.push(line.content);
|
|
349
|
-
}
|
|
350
|
-
expectedIdx++;
|
|
351
|
-
} else if (line.kind === 'remove') {
|
|
352
|
-
const fileLine = matchedFileLines[expectedIdx];
|
|
353
|
-
if (fileLine !== undefined) {
|
|
354
|
-
lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
|
|
355
|
-
lastFileIndentExpanded = expandWhitespace(
|
|
356
|
-
getLeadingWhitespace(fileLine),
|
|
357
|
-
tabSize,
|
|
358
|
-
);
|
|
359
|
-
lastPatchIndentExpanded = expandWhitespace(
|
|
360
|
-
getLeadingWhitespace(line.content),
|
|
361
|
-
tabSize,
|
|
362
|
-
);
|
|
363
|
-
if (lastDelta !== 0) hasDelta = true;
|
|
364
|
-
if (
|
|
365
|
-
detectIndentStyle(getLeadingWhitespace(fileLine)) !==
|
|
366
|
-
detectIndentStyle(getLeadingWhitespace(line.content)) &&
|
|
367
|
-
getLeadingWhitespace(fileLine).length > 0
|
|
368
|
-
) {
|
|
369
|
-
hasStyleMismatch = true;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
expectedIdx++;
|
|
373
|
-
} else if (line.kind === 'add') {
|
|
374
|
-
const addIndent = expandWhitespace(
|
|
375
|
-
getLeadingWhitespace(line.content),
|
|
376
|
-
tabSize,
|
|
377
|
-
);
|
|
378
|
-
const addWs = getLeadingWhitespace(line.content);
|
|
379
|
-
const addStyle =
|
|
380
|
-
addWs.length > 0 ? detectIndentStyle(addWs) : fileIndentChar;
|
|
381
|
-
const styleMismatch =
|
|
382
|
-
addStyle !== fileIndentChar && line.content.trim() !== '';
|
|
383
|
-
if (styleMismatch) {
|
|
384
|
-
const relativeOffset = addIndent - lastPatchIndentExpanded;
|
|
385
|
-
const targetIndent = lastFileIndentExpanded + relativeOffset;
|
|
386
|
-
const actualDelta = targetIndent - addIndent;
|
|
387
|
-
result.push(
|
|
388
|
-
applyIndentDelta(line.content, actualDelta, fileIndentChar, tabSize),
|
|
389
|
-
);
|
|
390
|
-
} else if (Math.abs(medianDelta) > tabSize) {
|
|
391
|
-
result.push(
|
|
392
|
-
applyIndentDelta(line.content, medianDelta, fileIndentChar, tabSize),
|
|
393
|
-
);
|
|
394
|
-
} else {
|
|
395
|
-
result.push(line.content);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (!hasDelta && !hasStyleMismatch && !hasAddStyleMismatch) {
|
|
401
|
-
return hunk.lines.filter((l) => l.kind !== 'remove').map((l) => l.content);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return result;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function applyHunkToLines(
|
|
408
|
-
lines: string[],
|
|
409
|
-
originalLines: string[],
|
|
410
|
-
hunk: PatchHunk,
|
|
411
|
-
hint: number,
|
|
412
|
-
useFuzzy: boolean,
|
|
413
|
-
): AppliedPatchHunk | null {
|
|
414
|
-
const expected = hunk.lines
|
|
415
|
-
.filter((line) => line.kind !== 'add')
|
|
416
|
-
.map((line) => line.content);
|
|
417
|
-
const replacement = hunk.lines
|
|
418
|
-
.filter((line) => line.kind !== 'remove')
|
|
419
|
-
.map((line) => line.content);
|
|
420
|
-
|
|
421
|
-
const removals = hunk.lines.filter((line) => line.kind === 'remove');
|
|
422
|
-
const additions = hunk.lines
|
|
423
|
-
.filter((line) => line.kind === 'add')
|
|
424
|
-
.map((line) => line.content);
|
|
425
|
-
const contextLines = hunk.lines
|
|
426
|
-
.filter((line) => line.kind === 'context')
|
|
427
|
-
.map((line) => line.content);
|
|
428
|
-
|
|
429
|
-
const hasExpected = expected.length > 0;
|
|
430
|
-
const initialHint =
|
|
431
|
-
typeof hunk.header.oldStart === 'number'
|
|
432
|
-
? Math.max(0, hunk.header.oldStart - 1)
|
|
433
|
-
: hint;
|
|
434
|
-
|
|
435
|
-
let matchIndex = hasExpected
|
|
436
|
-
? findSubsequence(lines, expected, Math.max(0, initialHint - 3), useFuzzy)
|
|
437
|
-
: -1;
|
|
438
|
-
let matchedExpected = expected;
|
|
439
|
-
|
|
440
|
-
if (hasExpected && matchIndex === -1) {
|
|
441
|
-
matchIndex = findSubsequence(lines, expected, 0, useFuzzy);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (matchIndex === -1 && removals.length > 0) {
|
|
445
|
-
const allContextPresent = contextLines.every((line) =>
|
|
446
|
-
lineExists(lines, line, useFuzzy),
|
|
447
|
-
);
|
|
448
|
-
if (!allContextPresent) {
|
|
449
|
-
matchIndex = -1;
|
|
450
|
-
} else {
|
|
451
|
-
const expectedWithoutMissingRemovals = hunk.lines
|
|
452
|
-
.filter((line) => {
|
|
453
|
-
if (line.kind === 'add') return false;
|
|
454
|
-
if (line.kind === 'remove') {
|
|
455
|
-
return lineExists(lines, line.content, useFuzzy);
|
|
456
|
-
}
|
|
457
|
-
return true;
|
|
458
|
-
})
|
|
459
|
-
.map((line) => line.content);
|
|
460
|
-
const includedRemovalCount = hunk.lines.filter(
|
|
461
|
-
(line) =>
|
|
462
|
-
line.kind === 'remove' && lineExists(lines, line.content, useFuzzy),
|
|
463
|
-
).length;
|
|
464
|
-
const minRequired = Math.max(contextLines.length, 2);
|
|
465
|
-
if (
|
|
466
|
-
includedRemovalCount > 0 &&
|
|
467
|
-
expectedWithoutMissingRemovals.length >= minRequired
|
|
468
|
-
) {
|
|
469
|
-
matchIndex = findSubsequence(
|
|
470
|
-
lines,
|
|
471
|
-
expectedWithoutMissingRemovals,
|
|
472
|
-
Math.max(0, initialHint - 3),
|
|
473
|
-
useFuzzy,
|
|
474
|
-
);
|
|
475
|
-
if (matchIndex === -1) {
|
|
476
|
-
matchIndex = findSubsequence(
|
|
477
|
-
lines,
|
|
478
|
-
expectedWithoutMissingRemovals,
|
|
479
|
-
0,
|
|
480
|
-
useFuzzy,
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
if (matchIndex !== -1) {
|
|
484
|
-
matchedExpected = expectedWithoutMissingRemovals;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
matchIndex === -1 &&
|
|
492
|
-
useFuzzy &&
|
|
493
|
-
removals.length >= 2 &&
|
|
494
|
-
contextLines.length === 0
|
|
495
|
-
) {
|
|
496
|
-
const firstRemoval = removals[0].content;
|
|
497
|
-
const lastRemoval = removals[removals.length - 1].content;
|
|
498
|
-
const firstIdx = findLineIndex(lines, firstRemoval, 0, true);
|
|
499
|
-
if (firstIdx !== -1) {
|
|
500
|
-
const rangeEnd = firstIdx + expected.length - 1;
|
|
501
|
-
if (rangeEnd < lines.length) {
|
|
502
|
-
const lastInRange = lines[rangeEnd];
|
|
503
|
-
let lastMatches = lastInRange === lastRemoval;
|
|
504
|
-
if (!lastMatches) {
|
|
505
|
-
for (const level of NORMALIZATION_LEVELS.slice(1)) {
|
|
506
|
-
if (
|
|
507
|
-
normalizeWhitespace(lastInRange, level) ===
|
|
508
|
-
normalizeWhitespace(lastRemoval, level)
|
|
509
|
-
) {
|
|
510
|
-
lastMatches = true;
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (lastMatches) {
|
|
516
|
-
let matchCount = 0;
|
|
517
|
-
for (let k = 0; k < expected.length; k++) {
|
|
518
|
-
const fileLine = lines[firstIdx + k];
|
|
519
|
-
const expLine = expected[k];
|
|
520
|
-
if (fileLine === expLine) {
|
|
521
|
-
matchCount++;
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
for (const level of NORMALIZATION_LEVELS.slice(1)) {
|
|
525
|
-
if (
|
|
526
|
-
normalizeWhitespace(fileLine, level) ===
|
|
527
|
-
normalizeWhitespace(expLine, level)
|
|
528
|
-
) {
|
|
529
|
-
matchCount++;
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
const matchRatio = matchCount / expected.length;
|
|
535
|
-
if (matchRatio >= 0.5) {
|
|
536
|
-
matchIndex = firstIdx;
|
|
537
|
-
matchedExpected = lines.slice(firstIdx, firstIdx + expected.length);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (matchIndex === -1 && isHunkAlreadyApplied(lines, hunk, useFuzzy)) {
|
|
545
|
-
const skipStart =
|
|
546
|
-
initialHint >= 0 && initialHint < lines.length ? initialHint + 1 : 1;
|
|
547
|
-
return {
|
|
548
|
-
header: { ...hunk.header },
|
|
549
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
550
|
-
oldStart: skipStart,
|
|
551
|
-
oldLines: 0,
|
|
552
|
-
newStart: skipStart,
|
|
553
|
-
newLines: replacement.length,
|
|
554
|
-
additions: hunk.lines.filter((l) => l.kind === 'add').length,
|
|
555
|
-
deletions: hunk.lines.filter((l) => l.kind === 'remove').length,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (matchIndex === -1 && !hasExpected) {
|
|
560
|
-
matchIndex = computeInsertionIndex(lines, hunk.header, initialHint);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (matchIndex === -1) {
|
|
564
|
-
const contextInfo = hunk.header.context
|
|
565
|
-
? ` near context '${hunk.header.context}'`
|
|
566
|
-
: '';
|
|
567
|
-
|
|
568
|
-
if (additions.length > 0) {
|
|
569
|
-
const hasRemovals = removals.length > 0;
|
|
570
|
-
let anchorIndex = -1;
|
|
571
|
-
if (!hasRemovals && contextLines.length > 0) {
|
|
572
|
-
const anchorContext = contextLines[contextLines.length - 1];
|
|
573
|
-
anchorIndex = findLineIndex(lines, anchorContext, 0, useFuzzy);
|
|
574
|
-
} else if (!hasRemovals) {
|
|
575
|
-
anchorIndex = -1;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const insertionIndex =
|
|
579
|
-
anchorIndex !== -1
|
|
580
|
-
? anchorIndex + 1
|
|
581
|
-
: computeInsertionIndex(lines, hunk.header, initialHint);
|
|
582
|
-
|
|
583
|
-
if (
|
|
584
|
-
findSubsequence(
|
|
585
|
-
lines,
|
|
586
|
-
additions,
|
|
587
|
-
Math.max(0, insertionIndex - additions.length),
|
|
588
|
-
useFuzzy,
|
|
589
|
-
) !== -1
|
|
590
|
-
) {
|
|
591
|
-
const skipStart =
|
|
592
|
-
insertionIndex >= 0 && insertionIndex < lines.length
|
|
593
|
-
? insertionIndex + 1
|
|
594
|
-
: lines.length + 1;
|
|
595
|
-
return {
|
|
596
|
-
header: { ...hunk.header },
|
|
597
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
598
|
-
oldStart: skipStart,
|
|
599
|
-
oldLines: 0,
|
|
600
|
-
newStart: skipStart,
|
|
601
|
-
newLines: additions.length,
|
|
602
|
-
additions: additions.length,
|
|
603
|
-
deletions: 0,
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
let errorMsg = `Failed to apply patch hunk${contextInfo}.`;
|
|
609
|
-
if (expected.length > 0) {
|
|
610
|
-
errorMsg += `\nExpected to find:\n${expected
|
|
611
|
-
.map((l) => ` ${l}`)
|
|
612
|
-
.join('\n')}`;
|
|
613
|
-
}
|
|
614
|
-
if (removals.length > 0) {
|
|
615
|
-
const missing = removals
|
|
616
|
-
.filter((line) => !lineExists(lines, line.content, useFuzzy))
|
|
617
|
-
.map((line) => line.content);
|
|
618
|
-
if (missing.length === removals.length) {
|
|
619
|
-
errorMsg +=
|
|
620
|
-
'\nAll removal lines already absent; consider reading the file again to capture current state.';
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
throw new Error(errorMsg);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const deleteCount = hasExpected ? matchedExpected.length : 0;
|
|
627
|
-
const originalIndex = matchIndex;
|
|
628
|
-
const oldStart = Math.min(
|
|
629
|
-
originalLines.length,
|
|
630
|
-
Math.max(0, originalIndex) + 1,
|
|
631
|
-
);
|
|
632
|
-
const newStart = matchIndex + 1;
|
|
633
|
-
|
|
634
|
-
const adjustedReplacement =
|
|
635
|
-
useFuzzy && hasExpected && matchedExpected.length === expected.length
|
|
636
|
-
? adjustReplacementIndentation(
|
|
637
|
-
hunk,
|
|
638
|
-
lines.slice(matchIndex, matchIndex + matchedExpected.length),
|
|
639
|
-
originalLines,
|
|
640
|
-
)
|
|
641
|
-
: replacement;
|
|
642
|
-
|
|
643
|
-
const targetSlice = lines.slice(
|
|
644
|
-
matchIndex,
|
|
645
|
-
matchIndex + adjustedReplacement.length,
|
|
646
|
-
);
|
|
647
|
-
if (
|
|
648
|
-
adjustedReplacement.length > 0 &&
|
|
649
|
-
adjustedReplacement.length === targetSlice.length &&
|
|
650
|
-
adjustedReplacement.every((line, i) => {
|
|
651
|
-
if (line === targetSlice[i]) return true;
|
|
652
|
-
if (!useFuzzy) return false;
|
|
653
|
-
for (const level of NORMALIZATION_LEVELS.slice(1)) {
|
|
654
|
-
if (
|
|
655
|
-
normalizeWhitespace(line, level) ===
|
|
656
|
-
normalizeWhitespace(targetSlice[i], level)
|
|
657
|
-
) {
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
return false;
|
|
662
|
-
})
|
|
663
|
-
) {
|
|
664
|
-
const skipStart = matchIndex + 1;
|
|
665
|
-
return {
|
|
666
|
-
header: { ...hunk.header },
|
|
667
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
668
|
-
oldStart: skipStart,
|
|
669
|
-
oldLines: 0,
|
|
670
|
-
newStart: skipStart,
|
|
671
|
-
newLines: adjustedReplacement.length,
|
|
672
|
-
additions: 0,
|
|
673
|
-
deletions: 0,
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
lines.splice(matchIndex, deleteCount, ...adjustedReplacement);
|
|
678
|
-
|
|
679
|
-
return {
|
|
680
|
-
header: { ...hunk.header },
|
|
681
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
682
|
-
oldStart,
|
|
683
|
-
oldLines: deleteCount,
|
|
684
|
-
newStart,
|
|
685
|
-
newLines: adjustedReplacement.length,
|
|
686
|
-
additions: hunk.lines.filter((l) => l.kind === 'add').length,
|
|
687
|
-
deletions: hunk.lines.filter((l) => l.kind === 'remove').length,
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
|
|
691
34
|
async function applyAddOperation(
|
|
692
35
|
projectRoot: string,
|
|
693
36
|
operation: PatchAddOperation,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function parseHunkHeader(raw: string) {
|
|
2
|
+
const match = raw.match(
|
|
3
|
+
/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
|
|
4
|
+
);
|
|
5
|
+
if (match) {
|
|
6
|
+
const [, oldStart, oldCount, newStart, newCount, context] = match;
|
|
7
|
+
return {
|
|
8
|
+
oldStart: Number.parseInt(oldStart, 10),
|
|
9
|
+
oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
|
|
10
|
+
newStart: Number.parseInt(newStart, 10),
|
|
11
|
+
newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
|
|
12
|
+
context: context?.trim() || undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const context = raw.replace(/^@@/, '').trim();
|
|
16
|
+
return context ? { context } : {};
|
|
17
|
+
}
|