@@ -32,6 +32,12 @@ const { decodeBytes } = require('./text.js');
3232function parseOptions ( args ) {
3333 /** @type {DiffOptions } */
3434 const o = { r : false , N : false , q : false , s : false , i : false , w : false , b : false , B : false , Z : false , stripCr : false , text : false , format : 'normal' , context : 3 , labels : [ ] , exclude : [ ] , excludeFrom : [ ] } ;
35+ // the number of context lines is the greatest of what -U N and -C N said, or the number of -NUM; a bare -u or -c is
36+ // three lines, but only when there is no -NUM (`-u -U1` is three, `-U0 -u` is three, `-1 -u` is one, `-U1` is one)
37+ let contextValue = - 1 ;
38+ let contextDigits = - 1 ;
39+ let bareStyle = false ;
40+ const more = ( n ) => { if ( n > contextValue ) { contextValue = n ; } } ;
3541 /** @type {string[] } */
3642 const operands = [ ] ;
3743 /** @type {string[] } */
@@ -64,13 +70,13 @@ function parseOptions(args) {
6470 case 'unified' : case 'context' : {
6571 const e = style ( name ) ;
6672 if ( e ) { return e ; }
67- if ( value !== null ) { if ( ! / ^ \d + $ / . test ( value ) ) { return { error : `diff: invalid context length '${ value } '\n` } ; } o . context = Number ( value ) ; }
73+ if ( value !== null ) { if ( ! / ^ \d + $ / . test ( value ) ) { return { error : `diff: invalid context length '${ value } '\ndiff: Try 'diff --help' for more information.\ n` } ; } more ( Number ( value ) ) ; } else { bareStyle = true ; }
6874 break ;
6975 }
7076 case 'label' : { const v = need ( ) ; if ( v === undefined ) { return { unknown : true } ; } o . labels . push ( v ) ; break ; }
7177 case 'exclude' : { const v = need ( ) ; if ( v === undefined ) { return { unknown : true } ; } o . exclude . push ( v ) ; break ; }
7278 case 'exclude-from' : { const v = need ( ) ; if ( v === undefined ) { return { unknown : true } ; } o . excludeFrom . push ( v ) ; break ; }
73- case 'no-dereference' : break ;
79+ case 'no-dereference' : case 'minimal' : case 'speed-large-files' : break ;
7480 default : return { unknown : true } ;
7581 }
7682 continue ;
@@ -90,7 +96,7 @@ function parseOptions(args) {
9096 if ( ch >= '0' && ch <= '9' ) {
9197 // -NUM is the number of context lines, and nothing more: the format is what -c or -u says
9298 const m = / ^ \d + / . exec ( a . slice ( c ) ) ;
93- o . context = Number ( m && m [ 0 ] ) ;
99+ contextDigits = Number ( m && m [ 0 ] ) ;
94100 c += ( m ? m [ 0 ] . length : 1 ) - 1 ;
95101 continue ;
96102 }
@@ -106,15 +112,15 @@ function parseOptions(args) {
106112 case 'Z' : o . Z = true ; break ;
107113 case 'a' : o . text = true ; break ;
108114 case 'd' : case 'H' : case 'v' : break ;
109- case 'u' : { const e = style ( 'unified' ) ; if ( e ) { return e ; } break ; }
110- case 'c' : { const e = style ( 'context' ) ; if ( e ) { return e ; } break ; }
115+ case 'u' : { const e = style ( 'unified' ) ; if ( e ) { return e ; } bareStyle = true ; break ; }
116+ case 'c' : { const e = style ( 'context' ) ; if ( e ) { return e ; } bareStyle = true ; break ; }
111117 case 'U' : case 'C' : {
112118 const v = takeValue ( ) ;
113119 if ( v === undefined ) { return { unknown : true } ; }
114- if ( ! / ^ \d + $ / . test ( v ) ) { return { error : `diff: invalid context length '${ v } '\n` } ; }
120+ if ( ! / ^ \d + $ / . test ( v ) ) { return { error : `diff: invalid context length '${ v } '\ndiff: Try 'diff --help' for more information.\ n` } ; }
115121 const e = style ( ch === 'U' ? 'unified' : 'context' ) ;
116122 if ( e ) { return e ; }
117- o . context = Number ( v ) ;
123+ more ( Number ( v ) ) ;
118124 break ;
119125 }
120126 case 'L' : { const v = takeValue ( ) ; if ( v === undefined ) { return { unknown : true } ; } o . labels . push ( v ) ; break ; }
@@ -125,6 +131,8 @@ function parseOptions(args) {
125131 }
126132 }
127133 }
134+ if ( o . labels . length > 2 ) { return { error : 'diff: too many file label options\n' } ; }
135+ o . context = contextDigits >= 0 ? Math . max ( contextValue , contextDigits ) : bareStyle ? Math . max ( contextValue , 3 ) : contextValue >= 0 ? contextValue : 3 ;
128136 return { o, operands, words } ;
129137}
130138
@@ -173,6 +181,88 @@ function parseNormal(text) {
173181 return hunks ;
174182}
175183
184+ /**
185+ * GNU's `shift_boundaries` (analyze.c): where a change could be put in more than one place (the added line is the same as
186+ * the one before or after it, a blank line, a `}`), a run of changed lines is slid back as far as the line before it
187+ * equals its last line, which joins it with a change it meets, and then forward as far as its first line equals the one
188+ * after it, and so on until it does not move; a run that was joined is put back to where it lines up with a change on the
189+ * other side. Two diffs of the same size that differ only in where such a run sits come out as GNU's. The ranges are the
190+ * engine's own (its comparison is what finds them); `same(x, y)` says two lines are equal for the options.
191+ *
192+ * @param {Array<{ a1: number, a2: number, b1: number, b2: number }> } changes
193+ * @param {number } n lines on the left
194+ * @param {number } m lines on the right
195+ * @param {(left: boolean, i: number, j: number) => boolean } same lines i and j (0-based) of one side are equal
196+ * @returns {Array<{ a1: number, a2: number, b1: number, b2: number }> }
197+ */
198+ function shiftBoundaries ( changes , n , m , same ) {
199+ const ca = new Uint8Array ( n + 2 ) ;
200+ const cb = new Uint8Array ( m + 2 ) ;
201+ for ( const c of changes ) {
202+ for ( let k = c . a1 ; k <= c . a2 ; k ++ ) { ca [ k - 1 ] = 1 ; }
203+ for ( let k = c . b1 ; k <= c . b2 ; k ++ ) { cb [ k - 1 ] = 1 ; }
204+ }
205+ // the same pass for each side, with the other side's marks as `other`
206+ const pass = ( changed , other , len , left ) => {
207+ let i = 0 ;
208+ let j = 0 ;
209+ const end = len ;
210+ for ( ; ; ) {
211+ let runlength ;
212+ let start ;
213+ let corresponding ;
214+ // to the beginning of the next run, keeping the place on the other side
215+ while ( i < end && ! changed [ i ] ) {
216+ while ( other [ j ++ ] ) { /* skip */ }
217+ i ++ ;
218+ }
219+ if ( i === end ) { break ; }
220+ start = i ;
221+ while ( changed [ ++ i ] ) { /* the end of the run */ }
222+ while ( other [ j ] ) { j ++ ; }
223+ do {
224+ runlength = i - start ;
225+ // back, while the line before the run is the run's last: merges with what is before
226+ while ( start > 0 && same ( left , start - 1 , i - 1 ) ) {
227+ changed [ -- start ] = 1 ;
228+ changed [ -- i ] = 0 ;
229+ while ( start > 0 && changed [ start - 1 ] ) { start -- ; }
230+ while ( j > 0 && other [ -- j ] ) { /* skip */ }
231+ }
232+ corresponding = j > 0 && other [ j - 1 ] ? i : end ;
233+ // forward, while the run's first line is the one after it: merges with what is after
234+ while ( i !== end && same ( left , start , i ) ) {
235+ changed [ start ++ ] = 0 ;
236+ changed [ i ++ ] = 1 ;
237+ while ( changed [ i ] ) { i ++ ; }
238+ while ( other [ ++ j ] ) { corresponding = i ; }
239+ }
240+ } while ( runlength !== i - start ) ;
241+ // a merged run goes back to where it lines up with a change on the other side
242+ while ( corresponding < i ) {
243+ changed [ -- start ] = 1 ;
244+ changed [ -- i ] = 0 ;
245+ while ( j > 0 && other [ -- j ] ) { /* skip */ }
246+ }
247+ }
248+ } ;
249+ pass ( ca , cb , n , true ) ;
250+ pass ( cb , ca , m , false ) ;
251+ // the script: runs that sit at the same place on both sides are one change
252+ const out = [ ] ;
253+ let i = 0 ;
254+ let j = 0 ;
255+ while ( i < n || j < m ) {
256+ if ( ! ca [ i ] && ! cb [ j ] ) { i ++ ; j ++ ; continue ; }
257+ const i0 = i ;
258+ const j0 = j ;
259+ while ( i < n && ca [ i ] ) { i ++ ; }
260+ while ( j < m && cb [ j ] ) { j ++ ; }
261+ out . push ( { a1 : i0 + 1 , a2 : i , b1 : j0 + 1 , b2 : j } ) ;
262+ }
263+ return out ;
264+ }
265+
176266/**
177267 * The hunks of a unified or context diff: changed ranges that are close are one. GNU puts two changes in one hunk when
178268 * no more than twice the number of context lines separates them.
@@ -222,7 +312,7 @@ function unifiedBody(changes, a, b, eof, context) {
222312 const endB = last . b2 + ( endA - last . a2 ) ;
223313 const countA = endA - startA + 1 ;
224314 const countB = endB - startB + 1 ;
225- out += `@@ -${ range ( countA === 0 ? startA : startA , countA ) } +${ range ( startB , countB ) } @@\n` ;
315+ out += `@@ -${ range ( startA , countA ) } +${ range ( startB , countB ) } @@\n` ;
226316 let at = startA ;
227317 for ( const c of group ) {
228318 for ( ; at < c . a1 ; at ++ ) { out += lineA ( ' ' , at ) ; }
@@ -379,6 +469,9 @@ async function run(args, io) {
379469 * (`/dev/null` for a missing one), `shownA` and `shownB` what is said; `show` puts the `diff ...` line first.
380470 */
381471 const compareFiles = async ( nameA , nameB , shownA , shownB , show ) => {
472+ // what the one-line messages call the files: the label of each, when there is one
473+ const saidA = o . labels [ 0 ] !== undefined ? o . labels [ 0 ] : shownA ;
474+ const saidB = o . labels [ 1 ] !== undefined ? o . labels [ 1 ] : shownB ;
382475 const load = async ( name ) => {
383476 if ( name === '-' ) { return { b : Uint8Array . from ( io . stdin , ( ch ) => ch . charCodeAt ( 0 ) & 0xff ) , t : null } ; }
384477 if ( name === '/dev/null' ) { return { b : new Uint8Array ( 0 ) , t : 0 , missing : true } ; }
@@ -399,8 +492,8 @@ async function run(args, io) {
399492 const identical = same ( A . b , B . b ) ;
400493 const say = ( m ) => { out += m + '\n' ; } ;
401494 if ( ! o . text && ( hasNul ( A . b ) || hasNul ( B . b ) ) ) {
402- if ( identical ) { if ( o . s ) { say ( `Files ${ shownA } and ${ shownB } are identical` ) ; } return ; }
403- say ( o . q ? `Files ${ shownA } and ${ shownB } differ` : `Binary files ${ shownA } and ${ shownB } differ` ) ;
495+ if ( identical ) { if ( o . s ) { say ( `Files ${ saidA } and ${ saidB } are identical` ) ; } return ; }
496+ say ( o . q ? `Files ${ saidA } and ${ saidB } differ` : `Binary files ${ saidA } and ${ saidB } differ` ) ;
404497 status = Math . max ( status , 1 ) ;
405498 return ;
406499 }
@@ -431,12 +524,22 @@ async function run(args, io) {
431524 }
432525 }
433526 }
527+ // where the engine put a change that could be somewhere else is moved to where GNU puts it
528+ if ( changes . length ) {
529+ const key = ( side , k ) => {
530+ const lines = side === sa ? sa : sb ;
531+ return canonical ( lines . lines [ k ] , o ) + ( ! ( o . w || o . b || o . Z ) && lines . noNewline && k === lines . lines . length - 1 ? '\uE000' : '' ) ;
532+ } ;
533+ const keysA = sa . lines . map ( ( _l , k ) => key ( sa , k ) ) ;
534+ const keysB = sb . lines . map ( ( _l , k ) => key ( sb , k ) ) ;
535+ changes = shiftBoundaries ( changes , keysA . length , keysB . length , ( left , x , y ) => ( left ? keysA [ x ] === keysA [ y ] : keysB [ x ] === keysB [ y ] ) ) ;
536+ }
434537 if ( ! changes . length ) {
435- if ( o . s ) { say ( `Files ${ shownA } and ${ shownB } are identical` ) ; }
538+ if ( o . s ) { say ( `Files ${ saidA } and ${ saidB } are identical` ) ; }
436539 return ;
437540 }
438541 status = Math . max ( status , 1 ) ;
439- if ( o . q ) { say ( `Files ${ shownA } and ${ shownB } differ` ) ; return ; }
542+ if ( o . q ) { say ( `Files ${ saidA } and ${ saidB } differ` ) ; return ; }
440543 if ( show ) { say ( `${ header } ${ shownA } ${ shownB } ` ) ; }
441544 const eof = { noNlA : sa . noNewline , noNlB : sb . noNewline } ;
442545 const labelA = o . labels [ 0 ] !== undefined ? o . labels [ 0 ] : `${ shownA } \t${ io . stamp ( A . t ) } ` ;
@@ -465,8 +568,9 @@ async function run(args, io) {
465568 const compareDirs = async ( A , B ) => {
466569 let namesA ;
467570 let namesB ;
468- try { namesA = await io . readdir ( A ) ; } catch { trouble ( `diff: ${ A } : No such file or directory\n` ) ; return ; }
469- try { namesB = await io . readdir ( B ) ; } catch { trouble ( `diff: ${ B } : No such file or directory\n` ) ; return ; }
571+ // with -N a folder that is not there is an empty one
572+ try { namesA = await io . readdir ( A ) ; } catch { if ( o . N && ! ( await io . stat ( A ) ) ) { namesA = [ ] ; } else { trouble ( `diff: ${ A } : No such file or directory\n` ) ; return ; } }
573+ try { namesB = await io . readdir ( B ) ; } catch { if ( o . N && ! ( await io . stat ( B ) ) ) { namesB = [ ] ; } else { trouble ( `diff: ${ B } : No such file or directory\n` ) ; return ; } }
470574 const names = [ ...new Set ( [ ...namesA , ...namesB ] ) ] . filter ( ( n ) => ! isExcluded ( n ) ) . sort ( ( x , y ) => ( x < y ? - 1 : x > y ? 1 : 0 ) ) ;
471575 for ( const name of names ) {
472576 const pa = join ( A , name ) ;
@@ -480,7 +584,8 @@ async function run(args, io) {
480584 out += `Only in ${ sa ? A : B } : ${ name } \n` ;
481585 status = Math . max ( status , 1 ) ;
482586 } else if ( dirA || dirB ) {
483- await compareOneSided ( sa ? pa : pb , sa ? pb : pa , ! sa ) ;
587+ // a folder that one side lacks is, with -N, an empty one: with -r it is walked, and without it is "common"
588+ if ( o . r ) { await compareOneSided ( sa ? pa : pb , sa ? pb : pa , ! sa ) ; } else { out += `Common subdirectories: ${ pa } and ${ pb } \n` ; }
484589 } else {
485590 await compareFiles ( sa ? pa : '/dev/null' , sb ? pb : '/dev/null' , pa , pb , ! o . q ) ;
486591 }
@@ -506,7 +611,11 @@ async function run(args, io) {
506611 await compareFiles ( sa ? A : '/dev/null' , sb ? B : '/dev/null' , A , B , false ) ;
507612 return { stdout : out , stderr : err , exitCode : status } ;
508613 }
509- return { stdout : '' , stderr : `diff: ${ ! sa ? A : B } : No such file or directory\n` , exitCode : 2 } ;
614+ if ( o . N && ( sa ? sa . isDirectory : sb && sb . isDirectory ) ) {
615+ await compareDirs ( A , B ) ;
616+ return { stdout : out , stderr : err , exitCode : status } ;
617+ }
618+ return { stdout : '' , stderr : ( ! sa ? `diff: ${ A } : No such file or directory\n` : '' ) + ( ! sb ? `diff: ${ B } : No such file or directory\n` : '' ) , exitCode : 2 } ;
510619 }
511620 if ( sa . isDirectory && sb . isDirectory ) {
512621 await compareDirs ( A , B ) ;
@@ -527,4 +636,4 @@ async function run(args, io) {
527636 return { stdout : out , stderr : err , exitCode : status } ;
528637}
529638
530- module . exports = { run, parseOptions, splitLines, canonical, parseNormal, groupChanges, unifiedBody, contextBody, normalBody, globToRegExp } ;
639+ module . exports = { run, parseOptions, splitLines, canonical, parseNormal, shiftBoundaries , groupChanges, unifiedBody, contextBody, normalBody, globToRegExp } ;
0 commit comments