@gitsense/gsc-utils 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/LICENSE +21 -0
- package/dist/gitsense-chat-utils.cjs.js +10977 -0
- package/dist/gitsense-chat-utils.esm.js +10975 -0
- package/dist/gsc-utils.cjs.js +11043 -0
- package/dist/gsc-utils.esm.js +11041 -0
- package/package.json +37 -0
- package/src/AnalysisBlockUtils.js +151 -0
- package/src/ChatUtils.js +126 -0
- package/src/CodeBlockUtils/blockExtractor.js +277 -0
- package/src/CodeBlockUtils/blockProcessor.js +559 -0
- package/src/CodeBlockUtils/blockProcessor.js.rej +8 -0
- package/src/CodeBlockUtils/constants.js +62 -0
- package/src/CodeBlockUtils/continuationUtils.js +191 -0
- package/src/CodeBlockUtils/headerParser.js +175 -0
- package/src/CodeBlockUtils/headerUtils.js +236 -0
- package/src/CodeBlockUtils/index.js +83 -0
- package/src/CodeBlockUtils/lineNumberFormatter.js +117 -0
- package/src/CodeBlockUtils/markerRemover.js +89 -0
- package/src/CodeBlockUtils/patchIntegration.js +38 -0
- package/src/CodeBlockUtils/relationshipUtils.js +159 -0
- package/src/CodeBlockUtils/updateCodeBlock.js +372 -0
- package/src/CodeBlockUtils/uuidUtils.js +48 -0
- package/src/ContextUtils.js +180 -0
- package/src/GSToolBlockUtils.js +108 -0
- package/src/GitSenseChatUtils.js +386 -0
- package/src/JsonUtils.js +101 -0
- package/src/LLMUtils.js +31 -0
- package/src/MessageUtils.js +460 -0
- package/src/PatchUtils/constants.js +72 -0
- package/src/PatchUtils/diagnosticReporter.js +213 -0
- package/src/PatchUtils/enhancedPatchProcessor.js +390 -0
- package/src/PatchUtils/fuzzyMatcher.js +252 -0
- package/src/PatchUtils/hunkCorrector.js +204 -0
- package/src/PatchUtils/hunkValidator.js +305 -0
- package/src/PatchUtils/index.js +135 -0
- package/src/PatchUtils/patchExtractor.js +175 -0
- package/src/PatchUtils/patchHeaderFormatter.js +143 -0
- package/src/PatchUtils/patchParser.js +289 -0
- package/src/PatchUtils/patchProcessor.js +389 -0
- package/src/PatchUtils/patchVerifier/constants.js +23 -0
- package/src/PatchUtils/patchVerifier/detectAndFixOverlappingHunks.js +281 -0
- package/src/PatchUtils/patchVerifier/detectAndFixRedundantChanges.js +404 -0
- package/src/PatchUtils/patchVerifier/formatAndAddLineNumbers.js +165 -0
- package/src/PatchUtils/patchVerifier/index.js +25 -0
- package/src/PatchUtils/patchVerifier/verifyAndCorrectHunkHeaders.js +202 -0
- package/src/PatchUtils/patchVerifier/verifyAndCorrectLineNumbers.js +254 -0
- package/src/SharedUtils/timestampUtils.js +41 -0
- package/src/SharedUtils/versionUtils.js +58 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component: PatchUtils Processor
|
|
3
|
+
* Block-UUID: 96ec8bca-9fc3-441b-b81f-6fefc840c626
|
|
4
|
+
* Parent-UUID: 9e5b8ae2-e978-4a20-b1dd-c044d9a5277f
|
|
5
|
+
* Version: 1.3.0
|
|
6
|
+
* Description: Handles applying, creating, and converting patches. Applies traditional unified diffs, cleaning line number prefixes from diff content before using jsdiff.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-07-20T17:24:16.836Z
|
|
9
|
+
* Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Claude 3.7 Sonnet (v1.2.0), Gemini 2.5 Flash (v1.2.0), Gemini 2.5 Flash Thinking (v1.3.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const parser = require('./patchParser');
|
|
14
|
+
const verifier = require('./patchVerifier');
|
|
15
|
+
const jsdiff = require('diff');
|
|
16
|
+
const { removeLineNumbers } = require('../CodeBlockUtils/lineNumberFormatter');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cleans line number prefixes ('NNN: ') from relevant lines in unified diff content.
|
|
20
|
+
* @param {string} diffContent - The raw unified diff content string.
|
|
21
|
+
* @returns {string} The cleaned diff content string, ready for jsdiff.
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
function _cleanDiffContentLineNumbers(diffContent) {
|
|
25
|
+
if (!diffContent) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = diffContent.split('\n');
|
|
30
|
+
const cleanedLines = lines.map(line => {
|
|
31
|
+
// Only clean lines starting with '+', '-', or ' ' (context)
|
|
32
|
+
// Leave @@ headers and other markers alone.
|
|
33
|
+
if (
|
|
34
|
+
!line.startsWith('+++') &&
|
|
35
|
+
!line.startsWith('---') &&
|
|
36
|
+
(line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))
|
|
37
|
+
) {
|
|
38
|
+
const prefix = line[0]; // +, -, or space
|
|
39
|
+
// Preserve the prefix character in its own column
|
|
40
|
+
// The content starts after the prefix character
|
|
41
|
+
const contentWithoutPrefix = line.substring(1);
|
|
42
|
+
|
|
43
|
+
// Check if the content has a line number pattern (e.g., "12: ")
|
|
44
|
+
const lineNumberMatch = contentWithoutPrefix.match(/^\s*\d+:\s/);
|
|
45
|
+
if (lineNumberMatch) {
|
|
46
|
+
return prefix + contentWithoutPrefix.substring(lineNumberMatch[0].length);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return prefix + contentWithoutPrefix;
|
|
50
|
+
}
|
|
51
|
+
return line; // Return unchanged if not a diff content line
|
|
52
|
+
});
|
|
53
|
+
return cleanedLines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apply patch against source code. Expects traditional unified diff format.
|
|
58
|
+
* Cleans line number prefixes from patch content before applying with jsdiff.
|
|
59
|
+
* @param {string} sourceText - Original source code (MUST NOT contain line numbers).
|
|
60
|
+
* @param {string} patchText - Full patch block content (metadata + unified diff with line numbers on diff lines).
|
|
61
|
+
* @returns {{
|
|
62
|
+
* valid: boolean,
|
|
63
|
+
* patchedText: string | null,
|
|
64
|
+
* errors: string[]
|
|
65
|
+
* }} - Patch result with valid flag, patchedText (only if valid, without line numbers), and errors if any.
|
|
66
|
+
*/
|
|
67
|
+
function applyPatch(sourceText, patchText) {
|
|
68
|
+
if (sourceText === null || sourceText === undefined) { // Allow empty string source
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
patchedText: null,
|
|
72
|
+
errors: ["Source code is null or undefined."]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!patchText) {
|
|
77
|
+
return {
|
|
78
|
+
valid: false,
|
|
79
|
+
patchedText: null,
|
|
80
|
+
errors: ["Patch text is empty or null."]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let patchApplied = null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// 1. Determine format (should be traditional)
|
|
88
|
+
const format = parser.determinePatchFormat(patchText);
|
|
89
|
+
if (format !== 'traditional') {
|
|
90
|
+
// Also handle 'context' explicitly as an error now
|
|
91
|
+
const errorMsg = format === 'context'
|
|
92
|
+
? "Unsupported patch format: 'context'. Only 'traditional' unified diff is supported."
|
|
93
|
+
: `Unknown or invalid patch format detected: ${format}. Expected 'traditional' unified diff.`;
|
|
94
|
+
return { valid: false, patchedText: null, errors: [errorMsg] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 1.1. Play it safe and clean sourceText by removing potential line numbers
|
|
98
|
+
sourceText = removeLineNumbers(sourceText);
|
|
99
|
+
|
|
100
|
+
// 2. Extract metadata (optional for applying, but good practice)
|
|
101
|
+
const metadata = parser.extractPatchMetadata(patchText);
|
|
102
|
+
// Could add metadata validation here if needed, but applyPatch focuses on application
|
|
103
|
+
|
|
104
|
+
// 3. Extract the raw diff content
|
|
105
|
+
let rawDiffContent = parser.extractPatchContent(patchText, 'traditional');
|
|
106
|
+
|
|
107
|
+
if (!rawDiffContent && patchText.includes('@@')) {
|
|
108
|
+
// Check if content is missing but markers were present
|
|
109
|
+
console.warn("applyPatch: Patch text contains '@@' but no content was extracted between markers.");
|
|
110
|
+
// Proceed, jsdiff might handle empty diffs, or fail gracefully.
|
|
111
|
+
} else if (!rawDiffContent) {
|
|
112
|
+
return { valid: false, patchedText: null, errors: ["Could not extract diff content from patch text."] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 4. Apply the patch
|
|
116
|
+
const patchedResult = jsdiff.applyPatch(sourceText, rawDiffContent, { fuzzFactor: 0 });
|
|
117
|
+
|
|
118
|
+
if (patchedResult === false) {
|
|
119
|
+
// jsdiff.applyPatch returns false on failure
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
patchedText: null,
|
|
123
|
+
errors: ["Patch could not be applied. Hunk mismatch or invalid patch format likely."]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Success! patchedResult is the patched string
|
|
128
|
+
return {
|
|
129
|
+
valid: true,
|
|
130
|
+
patchedText: patchedResult,
|
|
131
|
+
errors: []
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Check if it's a jsdiff specific error if possible, otherwise generic
|
|
135
|
+
const errorMessage = error instanceof Error ? error.message : "Unexpected error during patch application.";
|
|
136
|
+
return {
|
|
137
|
+
valid: false,
|
|
138
|
+
patchedText: null,
|
|
139
|
+
errors: [errorMessage]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Creates a patch between two code versions
|
|
146
|
+
* @param {string} sourceText - Original source code (WITH line numbers NNN:)
|
|
147
|
+
* @param {string} targetCode - Modified source code (WITH line numbers NNN:)
|
|
148
|
+
* @param {Object} metadata - Patch metadata
|
|
149
|
+
* @param {string} filename - Optional filename for the diff header (defaults to 'Original')
|
|
150
|
+
* @returns {string} Patch in unified diff format with metadata and line numbers in diff lines
|
|
151
|
+
* @throws {Error} If patch creation fails.
|
|
152
|
+
*/
|
|
153
|
+
function createPatch(sourceText, targetCode, metadata = {}, filename = 'Original') {
|
|
154
|
+
if (sourceText === null || sourceText === undefined) {
|
|
155
|
+
throw new Error("createPatch: Source code is null or undefined");
|
|
156
|
+
}
|
|
157
|
+
if (targetCode === null || targetCode === undefined) {
|
|
158
|
+
throw new Error("createPatch: Target code is null or undefined");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// --- CRITICAL: jsdiff needs CLEAN text ---
|
|
163
|
+
const cleanSource = removeLineNumbers(sourceText);
|
|
164
|
+
const cleanTarget = removeLineNumbers(targetCode);
|
|
165
|
+
|
|
166
|
+
// --- Generate standard diff ---
|
|
167
|
+
// Provide filenames for standard diff headers (--- filename \n +++ filename)
|
|
168
|
+
const diffResult = jsdiff.createPatch(filename, cleanSource, cleanTarget, '', '', { context: 3 });
|
|
169
|
+
|
|
170
|
+
// --- Post-process the diff to add line numbers back ---
|
|
171
|
+
const diffLines = diffResult.split('\n');
|
|
172
|
+
const outputLines = [];
|
|
173
|
+
let sourceLineCounter = 0; // 0-based index for original source lines array
|
|
174
|
+
const originalSourceLines = sourceText.split('\n'); // Keep original with numbers
|
|
175
|
+
|
|
176
|
+
// Skip the header lines generated by jsdiff (---, +++, @@) initially
|
|
177
|
+
let diffLineIndex = 0;
|
|
178
|
+
while (diffLineIndex < diffLines.length && !diffLines[diffLineIndex].startsWith('@@ ')) {
|
|
179
|
+
// Keep the standard --- and +++ headers, but potentially adjust filename if needed
|
|
180
|
+
if (diffLines[diffLineIndex].startsWith('--- ') || diffLines[diffLineIndex].startsWith('+++ ')) {
|
|
181
|
+
// Use conceptual filenames as per system prompt format
|
|
182
|
+
if (diffLines[diffLineIndex].startsWith('--- ')) outputLines.push('--- Original');
|
|
183
|
+
if (diffLines[diffLineIndex].startsWith('+++ ')) outputLines.push('+++ Modified');
|
|
184
|
+
} else {
|
|
185
|
+
// Keep other potential header lines if any (e.g., index) - unlikely with createPatch defaults
|
|
186
|
+
outputLines.push(diffLines[diffLineIndex]);
|
|
187
|
+
}
|
|
188
|
+
diffLineIndex++;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Process hunks
|
|
192
|
+
while (diffLineIndex < diffLines.length) {
|
|
193
|
+
const line = diffLines[diffLineIndex];
|
|
194
|
+
|
|
195
|
+
if (line.startsWith('@@ ')) {
|
|
196
|
+
// Hunk header - Keep it as is, jsdiff generated it based on clean source,
|
|
197
|
+
// but the LLM expects it to reference the *numbered* source.
|
|
198
|
+
// This assumes the LLM correctly interprets these numbers against its numbered view.
|
|
199
|
+
outputLines.push(line);
|
|
200
|
+
// Extract starting line number from hunk to sync our sourceLineCounter
|
|
201
|
+
const match = line.match(/@@ -(\d+)/);
|
|
202
|
+
if (match && match[1]) {
|
|
203
|
+
// jsdiff line numbers are 1-based, adjust to 0-based index
|
|
204
|
+
sourceLineCounter = parseInt(match[1], 10) - 1;
|
|
205
|
+
} else {
|
|
206
|
+
// This shouldn't happen with valid diffs, but handle defensively
|
|
207
|
+
console.warn("Could not parse starting line number from hunk header:", line);
|
|
208
|
+
// Attempt to continue, but line numbers might be wrong
|
|
209
|
+
}
|
|
210
|
+
diffLineIndex++;
|
|
211
|
+
} else if (line.startsWith(' ')) { // Context line
|
|
212
|
+
// Find the corresponding original line with number prefix
|
|
213
|
+
const originalLine = originalSourceLines[sourceLineCounter] ?? line.substring(1); // Fallback if out of sync
|
|
214
|
+
outputLines.push(` ${originalLine}`); // Add space prefix back
|
|
215
|
+
sourceLineCounter++;
|
|
216
|
+
diffLineIndex++;
|
|
217
|
+
} else if (line.startsWith('-')) { // Deletion line
|
|
218
|
+
const originalLine = originalSourceLines[sourceLineCounter] ?? line.substring(1);
|
|
219
|
+
outputLines.push(`-${originalLine}`); // Add dash prefix back
|
|
220
|
+
sourceLineCounter++; // Consume a line from the source
|
|
221
|
+
diffLineIndex++;
|
|
222
|
+
} else if (line.startsWith('+')) { // Addition line
|
|
223
|
+
// Additions don't have a direct corresponding *original* line number prefix.
|
|
224
|
+
// We need to add the prefix based on the *target* line number.
|
|
225
|
+
// This is tricky. For simplicity matching the LLM example, let's find the *next*
|
|
226
|
+
// non-deleted line's number in the original source OR increment if at end.
|
|
227
|
+
// A truly robust solution might need the target lines array.
|
|
228
|
+
// Let's use the *current* source line number for the prefix, as seen in the example patch.
|
|
229
|
+
const currentLineNumStr = String(sourceLineCounter + 1).padStart(3, ' '); // Rough padding
|
|
230
|
+
outputLines.push(`+${currentLineNumStr}: ${line.substring(1)}`); // Add plus prefix and generated number
|
|
231
|
+
// Do NOT increment sourceLineCounter for additions
|
|
232
|
+
diffLineIndex++;
|
|
233
|
+
} else if (line.trim() === '' || line === '\') {
|
|
234
|
+
// Keep blank lines and the "No newline" marker
|
|
235
|
+
outputLines.push(line);
|
|
236
|
+
diffLineIndex++;
|
|
237
|
+
} else {
|
|
238
|
+
// Unexpected line format in diff
|
|
239
|
+
console.warn("Unexpected line format in diff output:", line);
|
|
240
|
+
outputLines.push(line); // Keep it anyway?
|
|
241
|
+
diffLineIndex++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const processedDiff = outputLines.join('\n');
|
|
246
|
+
|
|
247
|
+
// Format metadata
|
|
248
|
+
const formattedMetadata = _formatPatchMetadata(metadata); // Use internal helper
|
|
249
|
+
|
|
250
|
+
// Combine metadata, markers, and processed patch
|
|
251
|
+
return `${formattedMetadata}\n\n# --- PATCH START MARKER ---\n${processedDiff}\n# --- PATCH END MARKER ---`;
|
|
252
|
+
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error("Error creating patch:", error);
|
|
255
|
+
throw new Error(`Failed to create patch: ${error.message}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Creates a patch between two full code block strings (including their metadata headers).
|
|
261
|
+
* The generated patch will contain the custom metadata header and adjusted hunk line numbers
|
|
262
|
+
* that account for the original code block's header.
|
|
263
|
+
*
|
|
264
|
+
* @param {object} CodeBlockUtils - Instance of CodeBlockUtils to work around circular dependencies
|
|
265
|
+
* @param {string} sourceCodeBlockText - The complete string of the original code block (including its metadata header).
|
|
266
|
+
* @param {string} targetCodeBlockText - The complete string of the modified code block (including its metadata header).
|
|
267
|
+
* @param {object} patchMetadata - An object containing the necessary metadata for the patch header (e.g., Source-Block-UUID, Target-Block-UUID, etc.).
|
|
268
|
+
* @returns {object} Patch in unified diff format, patch with custom metadata and adjusted line numbers.
|
|
269
|
+
* @throws {Error} If inputs are invalid or patch creation fails.
|
|
270
|
+
*/
|
|
271
|
+
function createPatchFromCodeBlocks(CodeBlockUtils, sourceCodeBlockText, targetCodeBlockText, patchMetadata = {}) {
|
|
272
|
+
if (typeof sourceCodeBlockText !== 'string' || typeof targetCodeBlockText !== 'string') {
|
|
273
|
+
throw new Error("createPatchFromCodeBlocks: Source and target code block texts must be strings.");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const [ sourceHeaderText ] = sourceCodeBlockText.split('\n\n\n');
|
|
277
|
+
const [ targetHeaderText ] = targetCodeBlockText.split('\n\n\n');
|
|
278
|
+
|
|
279
|
+
const sourceContentTemp = sourceCodeBlockText.split('\n\n\n'); sourceContentTemp.shift();
|
|
280
|
+
const sourceContent = sourceContentTemp.join('\n\n');
|
|
281
|
+
|
|
282
|
+
const targetContentTemp = targetCodeBlockText.split('\n\n\n'); targetContentTemp.shift();
|
|
283
|
+
const targetContent = targetContentTemp.join('\n\n');
|
|
284
|
+
|
|
285
|
+
// Get the number of lines in the header + two blank lines
|
|
286
|
+
const sourceHeaderLineCount = sourceHeaderText.split('\n').length;
|
|
287
|
+
const targetHeaderLineCount = targetHeaderText.split('\n').length;
|
|
288
|
+
|
|
289
|
+
// To keep things simple we are going to require the source and target header line count to be the same.
|
|
290
|
+
// By doing so, we can can just add the line count to the hunks
|
|
291
|
+
if (sourceHeaderLineCount !== targetHeaderLineCount) {
|
|
292
|
+
throw new Error('Source and target header line count must be the same');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const cleanSourceContent = sourceContent.trimEnd();
|
|
296
|
+
const cleanTargetContent = targetContent.trimEnd();
|
|
297
|
+
|
|
298
|
+
// 2. Generate raw diff on content only
|
|
299
|
+
const unadjustedDiffPatch = jsdiff.createTwoFilesPatch('Original', 'Modified', cleanSourceContent, cleanTargetContent, '', '', { context: 3, stripTrailingCr: true });
|
|
300
|
+
|
|
301
|
+
// 3. Post-process Diff to Adjust Line Numbers and Add NNN: Prefixes
|
|
302
|
+
const diffLines = unadjustedDiffPatch.split('\n');
|
|
303
|
+
const outputLines = [];
|
|
304
|
+
const outputDiffLines = [];
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < diffLines.length; i++) {
|
|
307
|
+
const line = diffLines[i];
|
|
308
|
+
|
|
309
|
+
if (!line.startsWith('@@ '))
|
|
310
|
+
continue;
|
|
311
|
+
|
|
312
|
+
// Hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
313
|
+
const match = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
314
|
+
if (match) {
|
|
315
|
+
let oldStart = parseInt(match[1], 10);
|
|
316
|
+
let oldCount = match[2] ? parseInt(match[2], 10) : 1;
|
|
317
|
+
let newStart = parseInt(match[3], 10);
|
|
318
|
+
let newCount = match[4] ? parseInt(match[4], 10) : 1;
|
|
319
|
+
|
|
320
|
+
// Adjust line numbers to account for the header and 2 blank lines
|
|
321
|
+
oldStart += sourceHeaderLineCount + 2;
|
|
322
|
+
newStart += targetHeaderLineCount + 2;
|
|
323
|
+
|
|
324
|
+
const adjustedHeader =
|
|
325
|
+
`@@ `+
|
|
326
|
+
`-${oldStart}${oldCount !== 1 ? ',' + oldCount : ''} `+
|
|
327
|
+
`+${newStart}${newCount !== 1 ? ',' + newCount : ''} `+
|
|
328
|
+
`@@`;
|
|
329
|
+
|
|
330
|
+
diffLines[i] = adjustedHeader;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const adjustedDiffPatch = diffLines.join('\n');
|
|
335
|
+
|
|
336
|
+
// 4. Assemble Final Patch
|
|
337
|
+
const formattedMetadata = _formatPatchMetadata(patchMetadata);
|
|
338
|
+
const formatted = `${formattedMetadata}\n\n\n`+
|
|
339
|
+
`# --- PATCH START MARKER ---\n`+
|
|
340
|
+
`${adjustedDiffPatch}\n`+
|
|
341
|
+
`# --- PATCH END MARKER ---`;
|
|
342
|
+
return {
|
|
343
|
+
formatted,
|
|
344
|
+
formattedMetadata,
|
|
345
|
+
unadjustedDiffPatch,
|
|
346
|
+
adjustedDiffPatch
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Formats patch metadata as string
|
|
352
|
+
* @param {Object} metadata - Patch metadata
|
|
353
|
+
* @returns {string} Formatted metadata string
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
function _formatPatchMetadata(metadata) {
|
|
357
|
+
// --- Function body from original PatchUtils._formatPatchMetadata ---
|
|
358
|
+
// --- (Copied Verbatim) ---
|
|
359
|
+
const lines = ['# Patch Metadata'];
|
|
360
|
+
const metadataOrder = [
|
|
361
|
+
'Source-Block-UUID', 'Target-Block-UUID', 'Source-Version', 'Target-Version',
|
|
362
|
+
'Description', 'Authors'
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
// Add ordered metadata
|
|
366
|
+
metadataOrder.forEach(key => {
|
|
367
|
+
if (metadata[key]) {
|
|
368
|
+
lines.push(`# ${key}: ${metadata[key]}`);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Add any other metadata not in the preferred order
|
|
373
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
374
|
+
if (!metadataOrder.includes(key)) {
|
|
375
|
+
lines.push(`# ${key}: ${value}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return lines.join('\n');
|
|
380
|
+
// --- End of Function Body ---
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
applyPatch,
|
|
385
|
+
createPatch,
|
|
386
|
+
createPatchFromCodeBlocks,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component: PatchUtils Constants
|
|
3
|
+
* Block-UUID: 32adc00e-7509-4219-8e40-6c1319371db9
|
|
4
|
+
* Parent-UUID: N/A
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Contains shared constants and regular expressions used by the patch verification utilities.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-05-14T03:45:32.087Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash Thinking (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// Regex to parse context/deletion/addition lines with line numbers
|
|
14
|
+
// Captures: 1: diff prefix (' ', '-', or '+'), 2: line number, 3: content after 'NNN: '
|
|
15
|
+
const CONTENT_LINE_REGEX = /^([ +-])\s*[-+]*(\d+):\s?(.*)$/;
|
|
16
|
+
// Regex to parse hunk headers
|
|
17
|
+
// Captures: 1: old_start, 2: old_count (optional), 3: new_start, 4: new_count (optional)
|
|
18
|
+
const HUNK_HEADER_REGEX = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
CONTENT_LINE_REGEX,
|
|
22
|
+
HUNK_HEADER_REGEX,
|
|
23
|
+
};
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component: PatchUtils Detect and Fix Overlapping Hunks
|
|
3
|
+
* Block-UUID: 0e1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
|
|
4
|
+
* Parent-UUID: 2308ed72-91ff-48ba-bc80-310c23c01ff1
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
* Description: Detects and optionally fixes overlapping hunks in a patch file by merging them.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-05-14T03:48:50.244Z
|
|
9
|
+
* Authors: Gemini 2.5 Flash Thinking (v1.0.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const { CONTENT_LINE_REGEX, HUNK_HEADER_REGEX } = require('./constants');
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents an error found during patch verification.
|
|
18
|
+
* @typedef {object} VerificationError
|
|
19
|
+
* @property {number} [originalLineNumber] - The line number reported in the patch file (for line errors).
|
|
20
|
+
* @property {number} [hunkIndex] - The index (0-based) of the hunk where the error occurred (for header errors).
|
|
21
|
+
* @property {string} lineContent - The full content of the problematic line or header in the patch.
|
|
22
|
+
* @property {string} message - Description of the error.
|
|
23
|
+
* @property {number} [searchStart] - The starting line number (1-based) of the search window in the source.
|
|
24
|
+
* @property {number} [searchEnd] - The ending line number (1-based) of the search window in the source.
|
|
25
|
+
* @property {string} [errorType] - Type of error (e.g., 'redundant-change', 'line-mismatch', 'header-mismatch').
|
|
26
|
+
* @property {string[]} [suggestedFix] - Suggested lines to replace the problematic section.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detects and fixes overlapping hunks in a patch file. Overlapping hunks occur when
|
|
32
|
+
* multiple hunks try to modify the same section of code, which can cause patch application
|
|
33
|
+
* failures. This function identifies such overlaps and merges the affected hunks.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} patchText - The full text of the patch (including metadata and markers).
|
|
36
|
+
* @param {boolean} [autoFix=false] - Whether to automatically fix detected overlapping hunks.
|
|
37
|
+
* @returns {{
|
|
38
|
+
* correctedPatchText: string,
|
|
39
|
+
* correctionsMade: boolean,
|
|
40
|
+
* errors: VerificationError[]
|
|
41
|
+
* }} - An object containing the potentially corrected patch text, a flag indicating if corrections were made, and an array of errors encountered.
|
|
42
|
+
* @throws {Error} If patchText is not a string.
|
|
43
|
+
*/
|
|
44
|
+
function detectAndFixOverlappingHunks(patchText, autoFix = false) {
|
|
45
|
+
if (typeof patchText !== 'string') {
|
|
46
|
+
throw new Error("Invalid input: patchText must be a string.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = patchText.split('\n');
|
|
50
|
+
const errors = [];
|
|
51
|
+
let correctionsMade = false;
|
|
52
|
+
|
|
53
|
+
// Extract all hunks from the patch
|
|
54
|
+
const hunks = [];
|
|
55
|
+
let currentHunk = null;
|
|
56
|
+
let inMetadata = true;
|
|
57
|
+
let metadataLines = [];
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const line = lines[i];
|
|
61
|
+
|
|
62
|
+
// Collect metadata until we hit the patch start marker
|
|
63
|
+
if (inMetadata) {
|
|
64
|
+
metadataLines.push(line);
|
|
65
|
+
if (line === '# --- PATCH START MARKER ---') {
|
|
66
|
+
inMetadata = false;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// End of patch
|
|
72
|
+
if (line === '# --- PATCH END MARKER ---') {
|
|
73
|
+
if (currentHunk) {
|
|
74
|
+
hunks.push(currentHunk);
|
|
75
|
+
currentHunk = null;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Start of a new hunk
|
|
81
|
+
if (line.startsWith('@@ ')) {
|
|
82
|
+
if (currentHunk) {
|
|
83
|
+
hunks.push(currentHunk);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const headerMatch = line.match(HUNK_HEADER_REGEX);
|
|
87
|
+
if (!headerMatch) {
|
|
88
|
+
errors.push({
|
|
89
|
+
lineContent: line,
|
|
90
|
+
message: "Failed to parse hunk header.",
|
|
91
|
+
errorType: 'header-parse-error'
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
currentHunk = {
|
|
97
|
+
header: line,
|
|
98
|
+
headerMatch,
|
|
99
|
+
oldStart: parseInt(headerMatch[1], 10),
|
|
100
|
+
oldCount: headerMatch[2] ? parseInt(headerMatch[2], 10) : 1,
|
|
101
|
+
newStart: parseInt(headerMatch[3], 10),
|
|
102
|
+
newCount: headerMatch[4] ? parseInt(headerMatch[4], 10) : 1,
|
|
103
|
+
lines: [line],
|
|
104
|
+
contentLines: []
|
|
105
|
+
};
|
|
106
|
+
} else if (currentHunk) {
|
|
107
|
+
// Add line to current hunk
|
|
108
|
+
currentHunk.lines.push(line);
|
|
109
|
+
currentHunk.contentLines.push(line);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add the last hunk if there is one
|
|
114
|
+
if (currentHunk) {
|
|
115
|
+
hunks.push(currentHunk);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check for overlapping hunks
|
|
119
|
+
const overlappingPairs = [];
|
|
120
|
+
for (let i = 0; i < hunks.length; i++) {
|
|
121
|
+
for (let j = i + 1; j < hunks.length; j++) {
|
|
122
|
+
const hunk1 = hunks[i];
|
|
123
|
+
const hunk2 = hunks[j];
|
|
124
|
+
|
|
125
|
+
// Check if hunks overlap in the original file
|
|
126
|
+
const hunk1OldEnd = hunk1.oldStart + hunk1.oldCount - 1;
|
|
127
|
+
const hunk2OldEnd = hunk2.oldStart + hunk2.oldCount - 1;
|
|
128
|
+
|
|
129
|
+
// Check if hunks overlap in the new file
|
|
130
|
+
const hunk1NewEnd = hunk1.newStart + hunk1.newCount - 1;
|
|
131
|
+
const hunk2NewEnd = hunk2.newStart + hunk2.newCount - 1;
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if ((hunk1.oldStart <= hunk2OldEnd && hunk2.oldStart <= hunk1OldEnd) ||
|
|
135
|
+
(hunk1.newStart <= hunk2NewEnd && hunk2.newStart <= hunk1NewEnd)) {
|
|
136
|
+
|
|
137
|
+
overlappingPairs.push({ hunk1Index: i, hunk2Index: j });
|
|
138
|
+
|
|
139
|
+
errors.push({
|
|
140
|
+
lineContent: hunk1.header,
|
|
141
|
+
message: `Overlapping hunks detected: Hunk starting at old line ${hunk1.oldStart} overlaps with hunk starting at old line ${hunk2.oldStart}.`,
|
|
142
|
+
errorType: 'overlapping-hunks',
|
|
143
|
+
hunkIndex: i
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// If no overlaps or autoFix is disabled, return original
|
|
150
|
+
if (overlappingPairs.length === 0 || !autoFix) {
|
|
151
|
+
return {
|
|
152
|
+
correctedPatchText: patchText,
|
|
153
|
+
correctionsMade: false,
|
|
154
|
+
errors
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fix overlapping hunks by merging them
|
|
159
|
+
const mergedHunks = [...hunks];
|
|
160
|
+
const removedIndices = new Set();
|
|
161
|
+
|
|
162
|
+
for (const { hunk1Index, hunk2Index } of overlappingPairs) {
|
|
163
|
+
// Skip if either hunk has already been merged
|
|
164
|
+
if (removedIndices.has(hunk1Index) || removedIndices.has(hunk2Index)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const hunk1 = mergedHunks[hunk1Index];
|
|
169
|
+
const hunk2 = mergedHunks[hunk2Index];
|
|
170
|
+
|
|
171
|
+
// Determine the range of the merged hunk
|
|
172
|
+
const oldStart = Math.min(hunk1.oldStart, hunk2.oldStart);
|
|
173
|
+
const oldEnd = Math.max(hunk1.oldStart + hunk1.oldCount - 1, hunk2.oldStart + hunk2.oldCount - 1);
|
|
174
|
+
const oldCount = oldEnd - oldStart + 1;
|
|
175
|
+
|
|
176
|
+
const newStart = Math.min(hunk1.newStart, hunk2.newStart);
|
|
177
|
+
const newEnd = Math.max(hunk1.newStart + hunk1.newCount - 1, hunk2.newStart + hunk2.newCount - 1);
|
|
178
|
+
const newCount = newEnd - newStart + 1;
|
|
179
|
+
|
|
180
|
+
// Create a new merged hunk header
|
|
181
|
+
const oldPart = oldCount === 1 ? `${oldStart}` : `${oldStart},${oldCount}`;
|
|
182
|
+
const newPart = newCount === 1 ? `${newStart}` : `${newStart},${newCount}`;
|
|
183
|
+
const mergedHeader = `@@ -${oldPart} +${newPart} @@`;
|
|
184
|
+
|
|
185
|
+
// Merge the content lines (this is a simplified approach)
|
|
186
|
+
// A more sophisticated implementation would need to handle line conflicts
|
|
187
|
+
const mergedContentLines = [];
|
|
188
|
+
const processedLines = new Set();
|
|
189
|
+
|
|
190
|
+
// Process hunk1 content
|
|
191
|
+
for (const line of hunk1.contentLines) {
|
|
192
|
+
const match = line.match(CONTENT_LINE_REGEX);
|
|
193
|
+
if (match) {
|
|
194
|
+
const lineNum = parseInt(match[2], 10);
|
|
195
|
+
const key = `${match[1]}-${lineNum}-${match[3]}`;
|
|
196
|
+
if (!processedLines.has(key)) {
|
|
197
|
+
mergedContentLines.push(line);
|
|
198
|
+
processedLines.add(key);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
// Handle lines that don't match the content regex (e.g., comments within hunk)
|
|
202
|
+
if (!processedLines.has(line)) {
|
|
203
|
+
mergedContentLines.push(line);
|
|
204
|
+
processedLines.add(line);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Process hunk2 content
|
|
210
|
+
for (const line of hunk2.contentLines) {
|
|
211
|
+
const match = line.match(CONTENT_LINE_REGEX);
|
|
212
|
+
if (match) {
|
|
213
|
+
const lineNum = parseInt(match[2], 10);
|
|
214
|
+
const key = `${match[1]}-${lineNum}-${match[3]}`;
|
|
215
|
+
if (!processedLines.has(key)) {
|
|
216
|
+
mergedContentLines.push(line);
|
|
217
|
+
processedLines.add(key);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Handle lines that don't match the content regex (e.g., comments within hunk)
|
|
221
|
+
if (!processedLines.has(line)) {
|
|
222
|
+
mergedContentLines.push(line);
|
|
223
|
+
processedLines.add(line);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Sort merged content lines by line number for better readability (optional but good practice)
|
|
229
|
+
mergedContentLines.sort((a, b) => {
|
|
230
|
+
const matchA = a.match(CONTENT_LINE_REGEX);
|
|
231
|
+
const matchB = b.match(CONTENT_LINE_REGEX);
|
|
232
|
+
|
|
233
|
+
if (!matchA || !matchB) {
|
|
234
|
+
// If either doesn't match, keep original order relative to each other
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const lineNumA = parseInt(matchA[2], 10);
|
|
239
|
+
const lineNumB = parseInt(matchB[2], 10);
|
|
240
|
+
|
|
241
|
+
return lineNumA - lineNumB;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
// Create the merged hunk
|
|
246
|
+
const mergedHunk = {
|
|
247
|
+
header: mergedHeader,
|
|
248
|
+
oldStart,
|
|
249
|
+
oldCount,
|
|
250
|
+
newStart,
|
|
251
|
+
newCount,
|
|
252
|
+
lines: [mergedHeader, ...mergedContentLines],
|
|
253
|
+
contentLines: mergedContentLines
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Replace hunk1 with the merged hunk and mark hunk2 for removal
|
|
257
|
+
mergedHunks[hunk1Index] = mergedHunk;
|
|
258
|
+
removedIndices.add(hunk2Index);
|
|
259
|
+
|
|
260
|
+
correctionsMade = true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Rebuild the patch text with merged hunks
|
|
264
|
+
const outputLines = [...metadataLines];
|
|
265
|
+
for (let i = 0; i < mergedHunks.length; i++) {
|
|
266
|
+
if (!removedIndices.has(i)) {
|
|
267
|
+
outputLines.push(...mergedHunks[i].lines);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
outputLines.push('# --- PATCH END MARKER ---');
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
correctedPatchText: outputLines.join('\n'),
|
|
274
|
+
correctionsMade,
|
|
275
|
+
errors
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
detectAndFixOverlappingHunks,
|
|
281
|
+
};
|