@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyIndentDelta,
|
|
3
|
+
computeIndentDelta,
|
|
4
|
+
detectIndentStyle,
|
|
5
|
+
expandWhitespace,
|
|
6
|
+
getLeadingWhitespace,
|
|
7
|
+
inferTabSizeFromPairs,
|
|
8
|
+
} from './normalize.ts';
|
|
9
|
+
import type { PatchHunk } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export function adjustReplacementIndentation(
|
|
12
|
+
hunk: PatchHunk,
|
|
13
|
+
matchedFileLines: string[],
|
|
14
|
+
allFileLines?: string[],
|
|
15
|
+
): string[] {
|
|
16
|
+
const result: string[] = [];
|
|
17
|
+
let expectedIdx = 0;
|
|
18
|
+
let lastDelta = 0;
|
|
19
|
+
let lastFileIndentExpanded = 0;
|
|
20
|
+
let lastPatchIndentExpanded = 0;
|
|
21
|
+
let hasDelta = false;
|
|
22
|
+
let hasStyleMismatch = false;
|
|
23
|
+
let fileIndentChar: 'tab' | 'space' = 'space';
|
|
24
|
+
const deltas: number[] = [];
|
|
25
|
+
let hasAddStyleMismatch = false;
|
|
26
|
+
let fileIndentDetected = false;
|
|
27
|
+
|
|
28
|
+
for (const fl of matchedFileLines) {
|
|
29
|
+
const ws = getLeadingWhitespace(fl);
|
|
30
|
+
if (ws.length > 0) {
|
|
31
|
+
fileIndentChar = detectIndentStyle(ws);
|
|
32
|
+
fileIndentDetected = true;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!fileIndentDetected && allFileLines) {
|
|
38
|
+
for (const fl of allFileLines) {
|
|
39
|
+
const ws = getLeadingWhitespace(fl);
|
|
40
|
+
if (ws.length > 0) {
|
|
41
|
+
fileIndentChar = detectIndentStyle(ws);
|
|
42
|
+
fileIndentDetected = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const patchContextLines = hunk.lines
|
|
49
|
+
.filter((l) => l.kind === 'context' || l.kind === 'remove')
|
|
50
|
+
.map((l) => l.content);
|
|
51
|
+
const tabSize = inferTabSizeFromPairs(patchContextLines, matchedFileLines);
|
|
52
|
+
|
|
53
|
+
let tempIdx = 0;
|
|
54
|
+
for (const line of hunk.lines) {
|
|
55
|
+
if (line.kind === 'context' || line.kind === 'remove') {
|
|
56
|
+
const fileLine = matchedFileLines[tempIdx];
|
|
57
|
+
if (fileLine !== undefined) {
|
|
58
|
+
const d = computeIndentDelta(line.content, fileLine, tabSize);
|
|
59
|
+
if (d !== 0) deltas.push(d);
|
|
60
|
+
}
|
|
61
|
+
tempIdx++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const sortedDeltas = [...deltas].sort((a, b) => a - b);
|
|
65
|
+
const medianDelta =
|
|
66
|
+
sortedDeltas.length > 0
|
|
67
|
+
? sortedDeltas[Math.floor(sortedDeltas.length / 2)]
|
|
68
|
+
: 0;
|
|
69
|
+
|
|
70
|
+
for (const line of hunk.lines) {
|
|
71
|
+
if (line.kind === 'add' && line.content.trim() !== '') {
|
|
72
|
+
const ws = getLeadingWhitespace(line.content);
|
|
73
|
+
if (ws.length > 0 && detectIndentStyle(ws) !== fileIndentChar) {
|
|
74
|
+
hasAddStyleMismatch = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const line of hunk.lines) {
|
|
81
|
+
if (line.kind === 'context') {
|
|
82
|
+
const fileLine = matchedFileLines[expectedIdx];
|
|
83
|
+
if (fileLine !== undefined) {
|
|
84
|
+
lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
|
|
85
|
+
lastFileIndentExpanded = expandWhitespace(
|
|
86
|
+
getLeadingWhitespace(fileLine),
|
|
87
|
+
tabSize,
|
|
88
|
+
);
|
|
89
|
+
lastPatchIndentExpanded = expandWhitespace(
|
|
90
|
+
getLeadingWhitespace(line.content),
|
|
91
|
+
tabSize,
|
|
92
|
+
);
|
|
93
|
+
if (lastDelta !== 0) hasDelta = true;
|
|
94
|
+
if (
|
|
95
|
+
detectIndentStyle(getLeadingWhitespace(fileLine)) !==
|
|
96
|
+
detectIndentStyle(getLeadingWhitespace(line.content)) &&
|
|
97
|
+
getLeadingWhitespace(fileLine).length > 0
|
|
98
|
+
) {
|
|
99
|
+
hasStyleMismatch = true;
|
|
100
|
+
}
|
|
101
|
+
result.push(fileLine);
|
|
102
|
+
} else {
|
|
103
|
+
result.push(line.content);
|
|
104
|
+
}
|
|
105
|
+
expectedIdx++;
|
|
106
|
+
} else if (line.kind === 'remove') {
|
|
107
|
+
const fileLine = matchedFileLines[expectedIdx];
|
|
108
|
+
if (fileLine !== undefined) {
|
|
109
|
+
lastDelta = computeIndentDelta(line.content, fileLine, tabSize);
|
|
110
|
+
lastFileIndentExpanded = expandWhitespace(
|
|
111
|
+
getLeadingWhitespace(fileLine),
|
|
112
|
+
tabSize,
|
|
113
|
+
);
|
|
114
|
+
lastPatchIndentExpanded = expandWhitespace(
|
|
115
|
+
getLeadingWhitespace(line.content),
|
|
116
|
+
tabSize,
|
|
117
|
+
);
|
|
118
|
+
if (lastDelta !== 0) hasDelta = true;
|
|
119
|
+
if (
|
|
120
|
+
detectIndentStyle(getLeadingWhitespace(fileLine)) !==
|
|
121
|
+
detectIndentStyle(getLeadingWhitespace(line.content)) &&
|
|
122
|
+
getLeadingWhitespace(fileLine).length > 0
|
|
123
|
+
) {
|
|
124
|
+
hasStyleMismatch = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
expectedIdx++;
|
|
128
|
+
} else if (line.kind === 'add') {
|
|
129
|
+
const addIndent = expandWhitespace(
|
|
130
|
+
getLeadingWhitespace(line.content),
|
|
131
|
+
tabSize,
|
|
132
|
+
);
|
|
133
|
+
const addWs = getLeadingWhitespace(line.content);
|
|
134
|
+
const addStyle =
|
|
135
|
+
addWs.length > 0 ? detectIndentStyle(addWs) : fileIndentChar;
|
|
136
|
+
const styleMismatch =
|
|
137
|
+
addStyle !== fileIndentChar && line.content.trim() !== '';
|
|
138
|
+
if (styleMismatch) {
|
|
139
|
+
const relativeOffset = addIndent - lastPatchIndentExpanded;
|
|
140
|
+
const targetIndent = lastFileIndentExpanded + relativeOffset;
|
|
141
|
+
const actualDelta = targetIndent - addIndent;
|
|
142
|
+
result.push(
|
|
143
|
+
applyIndentDelta(line.content, actualDelta, fileIndentChar, tabSize),
|
|
144
|
+
);
|
|
145
|
+
} else if (Math.abs(medianDelta) > tabSize) {
|
|
146
|
+
result.push(
|
|
147
|
+
applyIndentDelta(line.content, medianDelta, fileIndentChar, tabSize),
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
result.push(line.content);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!hasDelta && !hasStyleMismatch && !hasAddStyleMismatch) {
|
|
156
|
+
return hunk.lines.filter((l) => l.kind !== 'remove').map((l) => l.content);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NORMALIZATION_LEVELS, normalizeWhitespace } from './normalize.ts';
|
|
2
|
+
|
|
3
|
+
export function linesMatch(
|
|
4
|
+
line: string,
|
|
5
|
+
pattern: string,
|
|
6
|
+
useFuzzy: boolean,
|
|
7
|
+
): boolean {
|
|
8
|
+
if (line === pattern) return true;
|
|
9
|
+
if (!useFuzzy) return false;
|
|
10
|
+
for (const level of NORMALIZATION_LEVELS.slice(1)) {
|
|
11
|
+
if (
|
|
12
|
+
normalizeWhitespace(line, level) === normalizeWhitespace(pattern, level)
|
|
13
|
+
) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function findLineIndex(
|
|
21
|
+
lines: string[],
|
|
22
|
+
pattern: string,
|
|
23
|
+
start: number,
|
|
24
|
+
useFuzzy: boolean,
|
|
25
|
+
): number {
|
|
26
|
+
for (let i = Math.max(0, start); i < lines.length; i++) {
|
|
27
|
+
if (linesMatch(lines[i], pattern, useFuzzy)) return i;
|
|
28
|
+
}
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findSubsequence(
|
|
33
|
+
lines: string[],
|
|
34
|
+
pattern: string[],
|
|
35
|
+
startIndex: number,
|
|
36
|
+
useFuzzy: boolean,
|
|
37
|
+
): number {
|
|
38
|
+
if (pattern.length === 0) return -1;
|
|
39
|
+
const start = Math.max(0, startIndex);
|
|
40
|
+
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
41
|
+
let matches = true;
|
|
42
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
43
|
+
if (linesMatch(lines[i + j], pattern[j], useFuzzy)) continue;
|
|
44
|
+
matches = false;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (matches) return i;
|
|
48
|
+
}
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function lineExists(
|
|
53
|
+
lines: string[],
|
|
54
|
+
target: string,
|
|
55
|
+
useFuzzy: boolean,
|
|
56
|
+
): boolean {
|
|
57
|
+
return findLineIndex(lines, target, 0, useFuzzy) !== -1;
|
|
58
|
+
}
|
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
PATCH_UPDATE_PREFIX,
|
|
9
9
|
PATCH_WITH_MARKER,
|
|
10
10
|
} from './constants.ts';
|
|
11
|
+
import { parseHunkHeader } from './hunk-header.ts';
|
|
12
|
+
import {
|
|
13
|
+
createReplaceBuilder,
|
|
14
|
+
flushReplaceBuilder,
|
|
15
|
+
flushReplacePair,
|
|
16
|
+
type ReplaceBuilder,
|
|
17
|
+
} from './replace-builder.ts';
|
|
11
18
|
import type {
|
|
12
19
|
PatchAddOperation,
|
|
13
20
|
PatchDeleteOperation,
|
|
@@ -28,70 +35,6 @@ function parseDirectivePath(line: string, prefix: string): string {
|
|
|
28
35
|
return filePath;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
function parseHunkHeader(raw: string) {
|
|
32
|
-
const match = raw.match(
|
|
33
|
-
/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
|
|
34
|
-
);
|
|
35
|
-
if (match) {
|
|
36
|
-
const [, oldStart, oldCount, newStart, newCount, context] = match;
|
|
37
|
-
return {
|
|
38
|
-
oldStart: Number.parseInt(oldStart, 10),
|
|
39
|
-
oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
|
|
40
|
-
newStart: Number.parseInt(newStart, 10),
|
|
41
|
-
newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
|
|
42
|
-
context: context?.trim() || undefined,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const context = raw.replace(/^@@/, '').trim();
|
|
46
|
-
return context ? { context } : {};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface ReplaceBuilder {
|
|
50
|
-
kind: 'replace';
|
|
51
|
-
filePath: string;
|
|
52
|
-
hunks: PatchHunk[];
|
|
53
|
-
phase: 'idle' | 'find' | 'with';
|
|
54
|
-
findLines: string[];
|
|
55
|
-
withLines: string[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function flushReplacePair(builder: ReplaceBuilder) {
|
|
59
|
-
if (builder.findLines.length === 0 && builder.withLines.length === 0) return;
|
|
60
|
-
if (builder.findLines.length === 0) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`Replace in ${builder.filePath}: *** Find: block is empty.`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const lines: PatchHunkLine[] = [];
|
|
66
|
-
for (const line of builder.findLines) {
|
|
67
|
-
lines.push({ kind: 'remove', content: line });
|
|
68
|
-
}
|
|
69
|
-
for (const line of builder.withLines) {
|
|
70
|
-
lines.push({ kind: 'add', content: line });
|
|
71
|
-
}
|
|
72
|
-
builder.hunks.push({ header: {}, lines });
|
|
73
|
-
builder.findLines = [];
|
|
74
|
-
builder.withLines = [];
|
|
75
|
-
builder.phase = 'idle';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function flushReplaceBuilder(builder: ReplaceBuilder): PatchUpdateOperation {
|
|
79
|
-
flushReplacePair(builder);
|
|
80
|
-
if (builder.hunks.length === 0) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Replace in ${builder.filePath} does not contain any *** Find:/*** With: pairs.`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
kind: 'update',
|
|
87
|
-
filePath: builder.filePath,
|
|
88
|
-
hunks: builder.hunks.map((hunk) => ({
|
|
89
|
-
header: { ...hunk.header },
|
|
90
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
91
|
-
})),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
38
|
export function parseEnvelopedPatch(patch: string): PatchOperation[] {
|
|
96
39
|
const normalized = patch.replace(/\r\n/g, '\n');
|
|
97
40
|
const lines = normalized.split('\n');
|
|
@@ -205,14 +148,9 @@ export function parseEnvelopedPatch(patch: string): PatchOperation[] {
|
|
|
205
148
|
|
|
206
149
|
if (line.startsWith(PATCH_REPLACE_PREFIX)) {
|
|
207
150
|
flushBuilder();
|
|
208
|
-
builder =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
hunks: [],
|
|
212
|
-
phase: 'idle',
|
|
213
|
-
findLines: [],
|
|
214
|
-
withLines: [],
|
|
215
|
-
};
|
|
151
|
+
builder = createReplaceBuilder(
|
|
152
|
+
parseDirectivePath(line, PATCH_REPLACE_PREFIX),
|
|
153
|
+
);
|
|
216
154
|
continue;
|
|
217
155
|
}
|
|
218
156
|
|
|
@@ -1,111 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from './
|
|
9
|
-
|
|
10
|
-
function stripPath(raw: string): string | null {
|
|
11
|
-
let trimmed = raw.trim();
|
|
12
|
-
if (!trimmed || trimmed === '/dev/null') return null;
|
|
13
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
14
|
-
trimmed = trimmed.slice(1, -1).replace(/\\"/g, '"');
|
|
15
|
-
}
|
|
16
|
-
if (trimmed.startsWith('a/') || trimmed.startsWith('b/')) {
|
|
17
|
-
trimmed = trimmed.slice(2);
|
|
18
|
-
}
|
|
19
|
-
if (trimmed.startsWith('./')) {
|
|
20
|
-
trimmed = trimmed.slice(2);
|
|
21
|
-
}
|
|
22
|
-
return trimmed || null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function parseHunkHeader(raw: string) {
|
|
26
|
-
const match = raw.match(
|
|
27
|
-
/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
|
|
28
|
-
);
|
|
29
|
-
if (match) {
|
|
30
|
-
const [, oldStart, oldCount, newStart, newCount, context] = match;
|
|
31
|
-
return {
|
|
32
|
-
oldStart: Number.parseInt(oldStart, 10),
|
|
33
|
-
oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
|
|
34
|
-
newStart: Number.parseInt(newStart, 10),
|
|
35
|
-
newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
|
|
36
|
-
context: context?.trim() || undefined,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const context = raw.replace(/^@@/, '').trim();
|
|
40
|
-
return context ? { context } : {};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function shouldIgnoreMetadata(line: string) {
|
|
44
|
-
const trimmed = line.trim();
|
|
45
|
-
if (trimmed === '') return true;
|
|
46
|
-
if (trimmed === '\') return true;
|
|
47
|
-
const prefixes = [
|
|
48
|
-
'diff --git',
|
|
49
|
-
'index ',
|
|
50
|
-
'similarity index',
|
|
51
|
-
'dissimilarity index',
|
|
52
|
-
'rename from',
|
|
53
|
-
'rename to',
|
|
54
|
-
'copy from',
|
|
55
|
-
'copy to',
|
|
56
|
-
'new file mode',
|
|
57
|
-
'deleted file mode',
|
|
58
|
-
'old mode',
|
|
59
|
-
'new mode',
|
|
60
|
-
'Binary files',
|
|
61
|
-
];
|
|
62
|
-
return prefixes.some((prefix) => trimmed.startsWith(prefix));
|
|
63
|
-
}
|
|
1
|
+
import type { PatchHunk, PatchHunkLine, PatchOperation } from './types.ts';
|
|
2
|
+
import { parseHunkHeader } from './hunk-header.ts';
|
|
3
|
+
import {
|
|
4
|
+
flushUnifiedBuilder,
|
|
5
|
+
shouldIgnoreUnifiedMetadata,
|
|
6
|
+
stripUnifiedPath,
|
|
7
|
+
type UnifiedBuilder,
|
|
8
|
+
} from './unified-state.ts';
|
|
64
9
|
|
|
65
10
|
export function parseUnifiedPatch(patch: string): PatchOperation[] {
|
|
66
11
|
const normalized = patch.replace(/\r\n/g, '\n');
|
|
67
12
|
const lines = normalized.split('\n');
|
|
68
13
|
const operations: PatchOperation[] = [];
|
|
69
14
|
|
|
70
|
-
|
|
71
|
-
| (PatchAddOperation & { kind: 'add' })
|
|
72
|
-
| (PatchDeleteOperation & { kind: 'delete' })
|
|
73
|
-
| (PatchUpdateOperation & {
|
|
74
|
-
kind: 'update';
|
|
75
|
-
currentHunk: PatchHunk | null;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
let builder: Builder | null = null;
|
|
15
|
+
let builder: UnifiedBuilder | null = null;
|
|
79
16
|
|
|
80
17
|
const flush = () => {
|
|
81
|
-
|
|
82
|
-
if (builder.kind === 'update') {
|
|
83
|
-
if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
|
|
84
|
-
builder.hunks.pop();
|
|
85
|
-
}
|
|
86
|
-
if (builder.hunks.length === 0) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Update for ${builder.filePath} does not contain any diff hunks.`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
operations.push({
|
|
92
|
-
kind: 'update',
|
|
93
|
-
filePath: builder.filePath,
|
|
94
|
-
hunks: builder.hunks.map((hunk) => ({
|
|
95
|
-
header: { ...hunk.header },
|
|
96
|
-
lines: hunk.lines.map((line) => ({ ...line })),
|
|
97
|
-
})),
|
|
98
|
-
});
|
|
99
|
-
} else if (builder.kind === 'add') {
|
|
100
|
-
operations.push({
|
|
101
|
-
kind: 'add',
|
|
102
|
-
filePath: builder.filePath,
|
|
103
|
-
lines: [...builder.lines],
|
|
104
|
-
});
|
|
105
|
-
} else {
|
|
106
|
-
operations.push({ kind: 'delete', filePath: builder.filePath });
|
|
107
|
-
}
|
|
108
|
-
builder = null;
|
|
18
|
+
builder = flushUnifiedBuilder(builder, operations);
|
|
109
19
|
};
|
|
110
20
|
|
|
111
21
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -124,8 +34,8 @@ export function parseUnifiedPatch(patch: string): PatchOperation[] {
|
|
|
124
34
|
|
|
125
35
|
flush();
|
|
126
36
|
|
|
127
|
-
const oldPath =
|
|
128
|
-
const newPath =
|
|
37
|
+
const oldPath = stripUnifiedPath(oldPathRaw);
|
|
38
|
+
const newPath = stripUnifiedPath(newPathRaw);
|
|
129
39
|
|
|
130
40
|
if (!oldPath && !newPath) {
|
|
131
41
|
throw new Error(
|
|
@@ -171,13 +81,13 @@ export function parseUnifiedPatch(patch: string): PatchOperation[] {
|
|
|
171
81
|
}
|
|
172
82
|
|
|
173
83
|
if (!builder) {
|
|
174
|
-
if (
|
|
84
|
+
if (shouldIgnoreUnifiedMetadata(line)) continue;
|
|
175
85
|
if (line.trim() === '') continue;
|
|
176
86
|
throw new Error(`Unrecognized content in patch: "${line}"`);
|
|
177
87
|
}
|
|
178
88
|
|
|
179
89
|
if (builder.kind === 'add') {
|
|
180
|
-
if (
|
|
90
|
+
if (shouldIgnoreUnifiedMetadata(line) || line.startsWith('@@')) continue;
|
|
181
91
|
if (line.startsWith('+')) {
|
|
182
92
|
builder.lines.push(line.slice(1));
|
|
183
93
|
}
|
|
@@ -188,7 +98,7 @@ export function parseUnifiedPatch(patch: string): PatchOperation[] {
|
|
|
188
98
|
continue;
|
|
189
99
|
}
|
|
190
100
|
|
|
191
|
-
if (
|
|
101
|
+
if (shouldIgnoreUnifiedMetadata(line)) continue;
|
|
192
102
|
|
|
193
103
|
if (line.startsWith('@@')) {
|
|
194
104
|
const hunk: PatchHunk = { header: parseHunkHeader(line), lines: [] };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PatchHunk,
|
|
3
|
+
PatchHunkLine,
|
|
4
|
+
PatchUpdateOperation,
|
|
5
|
+
} from './types.ts';
|
|
6
|
+
|
|
7
|
+
export interface ReplaceBuilder {
|
|
8
|
+
kind: 'replace';
|
|
9
|
+
filePath: string;
|
|
10
|
+
hunks: PatchHunk[];
|
|
11
|
+
phase: 'idle' | 'find' | 'with';
|
|
12
|
+
findLines: string[];
|
|
13
|
+
withLines: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createReplaceBuilder(filePath: string): ReplaceBuilder {
|
|
17
|
+
return {
|
|
18
|
+
kind: 'replace',
|
|
19
|
+
filePath,
|
|
20
|
+
hunks: [],
|
|
21
|
+
phase: 'idle',
|
|
22
|
+
findLines: [],
|
|
23
|
+
withLines: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function flushReplacePair(builder: ReplaceBuilder) {
|
|
28
|
+
if (builder.findLines.length === 0 && builder.withLines.length === 0) return;
|
|
29
|
+
if (builder.findLines.length === 0) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Replace in ${builder.filePath}: *** Find: block is empty.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const lines: PatchHunkLine[] = [];
|
|
35
|
+
for (const line of builder.findLines) {
|
|
36
|
+
lines.push({ kind: 'remove', content: line });
|
|
37
|
+
}
|
|
38
|
+
for (const line of builder.withLines) {
|
|
39
|
+
lines.push({ kind: 'add', content: line });
|
|
40
|
+
}
|
|
41
|
+
builder.hunks.push({ header: {}, lines });
|
|
42
|
+
builder.findLines = [];
|
|
43
|
+
builder.withLines = [];
|
|
44
|
+
builder.phase = 'idle';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function flushReplaceBuilder(
|
|
48
|
+
builder: ReplaceBuilder,
|
|
49
|
+
): PatchUpdateOperation {
|
|
50
|
+
flushReplacePair(builder);
|
|
51
|
+
if (builder.hunks.length === 0) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Replace in ${builder.filePath} does not contain any *** Find:/*** With: pairs.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
kind: 'update',
|
|
58
|
+
filePath: builder.filePath,
|
|
59
|
+
hunks: builder.hunks.map((hunk) => ({
|
|
60
|
+
header: { ...hunk.header },
|
|
61
|
+
lines: hunk.lines.map((line) => ({ ...line })),
|
|
62
|
+
})),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PatchAddOperation,
|
|
3
|
+
PatchDeleteOperation,
|
|
4
|
+
PatchHunk,
|
|
5
|
+
PatchOperation,
|
|
6
|
+
PatchUpdateOperation,
|
|
7
|
+
} from './types.ts';
|
|
8
|
+
|
|
9
|
+
export type UnifiedBuilder =
|
|
10
|
+
| (PatchAddOperation & { kind: 'add' })
|
|
11
|
+
| (PatchDeleteOperation & { kind: 'delete' })
|
|
12
|
+
| (PatchUpdateOperation & {
|
|
13
|
+
kind: 'update';
|
|
14
|
+
currentHunk: PatchHunk | null;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function stripUnifiedPath(raw: string): string | null {
|
|
18
|
+
let trimmed = raw.trim();
|
|
19
|
+
if (!trimmed || trimmed === '/dev/null') return null;
|
|
20
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
21
|
+
trimmed = trimmed.slice(1, -1).replace(/\\"/g, '"');
|
|
22
|
+
}
|
|
23
|
+
if (trimmed.startsWith('a/') || trimmed.startsWith('b/')) {
|
|
24
|
+
trimmed = trimmed.slice(2);
|
|
25
|
+
}
|
|
26
|
+
if (trimmed.startsWith('./')) {
|
|
27
|
+
trimmed = trimmed.slice(2);
|
|
28
|
+
}
|
|
29
|
+
return trimmed || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldIgnoreUnifiedMetadata(line: string) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (trimmed === '') return true;
|
|
35
|
+
if (trimmed === '\') return true;
|
|
36
|
+
const prefixes = [
|
|
37
|
+
'diff --git',
|
|
38
|
+
'index ',
|
|
39
|
+
'similarity index',
|
|
40
|
+
'dissimilarity index',
|
|
41
|
+
'rename from',
|
|
42
|
+
'rename to',
|
|
43
|
+
'copy from',
|
|
44
|
+
'copy to',
|
|
45
|
+
'new file mode',
|
|
46
|
+
'deleted file mode',
|
|
47
|
+
'old mode',
|
|
48
|
+
'new mode',
|
|
49
|
+
'Binary files',
|
|
50
|
+
];
|
|
51
|
+
return prefixes.some((prefix) => trimmed.startsWith(prefix));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function flushUnifiedBuilder(
|
|
55
|
+
builder: UnifiedBuilder | null,
|
|
56
|
+
operations: PatchOperation[],
|
|
57
|
+
): UnifiedBuilder | null {
|
|
58
|
+
if (!builder) return null;
|
|
59
|
+
if (builder.kind === 'update') {
|
|
60
|
+
if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
|
|
61
|
+
builder.hunks.pop();
|
|
62
|
+
}
|
|
63
|
+
if (builder.hunks.length === 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Update for ${builder.filePath} does not contain any diff hunks.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
operations.push({
|
|
69
|
+
kind: 'update',
|
|
70
|
+
filePath: builder.filePath,
|
|
71
|
+
hunks: builder.hunks.map((hunk) => ({
|
|
72
|
+
header: { ...hunk.header },
|
|
73
|
+
lines: hunk.lines.map((line) => ({ ...line })),
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
} else if (builder.kind === 'add') {
|
|
77
|
+
operations.push({
|
|
78
|
+
kind: 'add',
|
|
79
|
+
filePath: builder.filePath,
|
|
80
|
+
lines: [...builder.lines],
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
operations.push({ kind: 'delete', filePath: builder.filePath });
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|