11const fs = require ( 'fs' ) . promises ;
22
33// Patch tool to apply custom diffs marked with <<<<<<< ORIGINAL and >>>>>>> UPDATED
4- // Handles patches with context and applies only the segments between markers.
5-
4+ // More tolerant to whitespace differences on each line and reports per-block success.
65module . exports = async function patch ( { text, filename } , ctx ) {
7- // Regex to match each patch block
8- // Be lenient about the number of conflict marker characters because some
9- // environments may trim one or more > or < characters.
10- const patchRegex = / < { 6 , } \s * O R I G I N A L [ ^ \n ] * \n ( [ \s \S ] * ?) = + \n ( [ \s \S ] * ?) > { 6 , } \s * U P D A T E D [ ^ \n ] * (?: \n | $ ) / g;
11- const patches = [ ] ;
12- let match ;
6+ if ( ! text || ! filename ) {
7+ return "No patch text or filename provided" ;
8+ }
139
14- // Extract all old/new segments
15- while ( ( match = patchRegex . exec ( text ) ) !== null ) {
16- const oldPart = match [ 1 ] . replace ( / ^ \n + | \n + $ / g, "" ) ;
17- const newPart = match [ 2 ] . replace ( / ^ \n + | \n + $ / g, "" ) ;
10+ // Match: <<<<<<< ORIGINAL ... ======= ... >>>>>>> UPDATED
11+ // Be tolerant to CRLF/LF and optional trailing text/spaces on the markers.
12+ const patchRegex = / < { 6 , } \s * O R I G I N A L [ ^ \n ] * \r ? \n ( [ \s \S ] * ?) = + [ ^ \n ] * \r ? \n ( [ \s \S ] * ?) > { 6 , } \s * U P D A T E D [ ^ \n ] * (?: \r ? \n | $ ) / g;
13+
14+ const patches = [ ] ;
15+ let m ;
16+ while ( ( m = patchRegex . exec ( text ) ) !== null ) {
17+ const oldPart = String ( m [ 1 ] ) . replace ( / ^ \s * \r ? \n + | \r ? \n + \s * $ / g, "" ) ;
18+ const newPart = String ( m [ 2 ] ) . replace ( / ^ \s * \r ? \n + | \r ? \n + \s * $ / g, "" ) ;
1819 patches . push ( { oldPart, newPart } ) ;
1920 }
2021
@@ -24,18 +25,41 @@ module.exports = async function patch({ text, filename }, ctx) {
2425
2526 let fileContent = await ctx . read ( filename ) ;
2627
27- for ( const { oldPart, newPart } of patches ) {
28- // Escape regex special chars in oldPart.
29- // Do NOT relax all whitespace to \s+; that can swallow preceding newlines.
30- // Only normalize line endings so CRLF in patches can match LF in files.
31- let escaped = oldPart . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
28+ function buildPattern ( oldStr ) {
29+ // Escape special regex chars
30+ let escaped = oldStr . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
31+ // Normalize line endings to \r?\n so CRLF/LF both match
3232 escaped = escaped . replace ( / \r ? \n / g, "\\r?\\n" ) ;
33- const oldRegex = new RegExp ( escaped , "g" ) ;
33+ // Tolerate indentation/space differences (spaces or tabs), zero-or-more
34+ // Keep newlines strict so structure must still match.
35+ escaped = escaped . replace ( / [ \t ] + / g, "[ \\t]*" ) ;
36+ return new RegExp ( escaped , "g" ) ;
37+ }
38+
39+ const totalSegments = patches . length ;
40+ let appliedSegments = 0 ;
41+ let totalReplacements = 0 ;
3442
35- // Perform replacement using a function to avoid replacement string ambiguities
36- fileContent = fileContent . replace ( oldRegex , ( ) => newPart ) ;
43+ for ( const { oldPart, newPart } of patches ) {
44+ const re = buildPattern ( oldPart ) ;
45+ let matches = 0 ;
46+ fileContent = fileContent . replace ( re , ( ) => {
47+ matches += 1 ;
48+ return newPart ;
49+ } ) ;
50+ if ( matches > 0 ) {
51+ appliedSegments += 1 ;
52+ totalReplacements += matches ;
53+ }
3754 }
3855
3956 await ctx . write ( filename , fileContent ) ;
40- return "patched" ;
57+
58+ if ( appliedSegments === 0 ) {
59+ return `no matches applied (0/${ totalSegments } )` ;
60+ }
61+ if ( appliedSegments < totalSegments ) {
62+ return `patched partially (${ appliedSegments } /${ totalSegments } ), replacements: ${ totalReplacements } ` ;
63+ }
64+ return `patched (${ appliedSegments } /${ totalSegments } ), replacements: ${ totalReplacements } ` ;
4165} ;
0 commit comments