@@ -1235,25 +1235,59 @@ function applyHunksToContent(content: string, hunks: PatchHunk[], filePath: stri
12351235 return result . join ( "\n" ) ;
12361236}
12371237
1238+ // Reads a file's complete content for use as the base of an edit. readFile()
1239+ // is display-oriented — it caps its result at MAX_READ_CHARS and appends a
1240+ // "[truncated ...]" marker — which makes it unsafe to write back: everything
1241+ // past the cap would be destroyed and the marker itself saved into the file.
1242+ function readFileForEdit ( workspaceRoot : string , relativePath : string ) : string {
1243+ const target = resolveSafePath ( workspaceRoot , relativePath ) ;
1244+ if ( fs . statSync ( target ) . isDirectory ( ) ) throw new Error ( `"${ relativePath } " is a directory, not a file.` ) ;
1245+ return fs . readFileSync ( target , "utf-8" ) ;
1246+ }
1247+
12381248export function applyPatch ( workspaceRoot : string , patchText : string ) : { filesChanged : string [ ] } {
12391249 const files = parseUnifiedDiff ( patchText ) ;
12401250 if ( files . length === 0 ) throw new Error ( "No valid file patches found in the given diff." ) ;
12411251
1242- const filesChanged : string [ ] = [ ] ;
1252+ // Every file's outcome is resolved in memory before anything is written.
1253+ // Writing as we went meant a patch whose later file failed to align left
1254+ // the earlier ones already modified — a half-applied patch, which is the
1255+ // exact outcome this parser's refusal to fuzzy-match exists to avoid.
1256+ //
1257+ // `pending` (rather than a plain list) keeps resolution sequential: a diff
1258+ // with two sections for the same path, or one that creates a file and then
1259+ // patches it, has to see the earlier section's result instead of the stale
1260+ // copy on disk. null means the path is pending deletion.
1261+ const pending = new Map < string , string | null > ( ) ;
1262+ const order : string [ ] = [ ] ;
1263+ const remember = ( relativePath : string , content : string | null ) : void => {
1264+ if ( ! pending . has ( relativePath ) ) order . push ( relativePath ) ;
1265+ pending . set ( relativePath , content ) ;
1266+ } ;
1267+
12431268 for ( const file of files ) {
12441269 if ( file . newPath === null ) {
12451270 if ( ! file . oldPath ) throw new Error ( "Patch deletes a file but its path (/dev/null on both sides) is missing." ) ;
1246- deletePath ( workspaceRoot , file . oldPath , false ) ;
1247- filesChanged . push ( file . oldPath ) ;
1271+ remember ( file . oldPath , null ) ;
12481272 continue ;
12491273 }
1250- const isNewFile = file . oldPath === null ;
1251- const existingContent = isNewFile ? "" : readFile ( workspaceRoot , file . newPath ) ;
1252- const newContent = applyHunksToContent ( existingContent , file . hunks , file . newPath ) ;
1253- writeFile ( workspaceRoot , file . newPath , newContent ) ;
1254- filesChanged . push ( file . newPath ) ;
1274+ let existingContent : string ;
1275+ if ( pending . has ( file . newPath ) ) {
1276+ // Already touched by an earlier section of this same patch — a
1277+ // pending deletion reads back as empty, i.e. as a fresh file.
1278+ existingContent = pending . get ( file . newPath ) ?? "" ;
1279+ } else {
1280+ existingContent = file . oldPath === null ? "" : readFileForEdit ( workspaceRoot , file . newPath ) ;
1281+ }
1282+ remember ( file . newPath , applyHunksToContent ( existingContent , file . hunks , file . newPath ) ) ;
1283+ }
1284+
1285+ for ( const relativePath of order ) {
1286+ const content = pending . get ( relativePath ) ?? null ;
1287+ if ( content === null ) deletePath ( workspaceRoot , relativePath , false ) ;
1288+ else writeFile ( workspaceRoot , relativePath , content ) ;
12551289 }
1256- return { filesChanged } ;
1290+ return { filesChanged : order } ;
12571291}
12581292
12591293export interface WebSearchResult {
0 commit comments