@@ -212,3 +212,237 @@ export const format_tag = (tag, options = {}) => {
212
212
}
213
213
return s ;
214
214
}
215
+
216
+ export const diff_formatting_options = {
217
+ 'patch' : {
218
+ description : 'Generate a patch.' ,
219
+ type : 'boolean' ,
220
+ short : 'p' ,
221
+ } ,
222
+ 'no-patch' : {
223
+ description : 'Suppress patch output. Useful for commands that output a patch by default.' ,
224
+ type : 'boolean' ,
225
+ short : 's' ,
226
+ } ,
227
+ 'raw' : {
228
+ description : 'Generate diff in raw format.' ,
229
+ type : 'boolean' ,
230
+ } ,
231
+ 'patch-with-raw' : {
232
+ description : 'Alias for --patch --raw.' ,
233
+ type : 'boolean' ,
234
+ } ,
235
+ 'numstat' : {
236
+ description : 'Generate a diffstat in a machine-friendly format.' ,
237
+ type : 'boolean' ,
238
+ } ,
239
+ 'summary' : {
240
+ description : 'List newly added, deleted, or moved files.' ,
241
+ type : 'boolean' ,
242
+ } ,
243
+ 'unified' : {
244
+ description : 'Generate patches with N lines of context. Implies --patch.' ,
245
+ type : 'string' ,
246
+ short : 'U' ,
247
+ } ,
248
+ 'src-prefix' : {
249
+ description : 'Show the given source prefix instead of "a/".' ,
250
+ type : 'string' ,
251
+ } ,
252
+ 'dst-prefix' : {
253
+ description : 'Show the given destination prefix instead of "b/".' ,
254
+ type : 'string' ,
255
+ } ,
256
+ 'no-prefix' : {
257
+ description : 'Do not show source or destination prefixes.' ,
258
+ type : 'boolean' ,
259
+ } ,
260
+ 'default-prefix' : {
261
+ description : 'Use default "a/" and "b/" source and destination prefixes.' ,
262
+ type : 'boolean' ,
263
+ } ,
264
+ } ;
265
+
266
+ /**
267
+ * Process command-line options related to diff formatting, and return an options object to pass to format_diff().
268
+ * @param options Parsed command-line options.
269
+ * @returns {{raw: boolean, numstat: boolean, summary: boolean, patch: boolean, context_lines: number, no_patch: boolean, source_prefix: string, dest_prefix: string } }
270
+ */
271
+ export const process_diff_formatting_options = ( options ) => {
272
+ const result = {
273
+ raw : false ,
274
+ numstat : false ,
275
+ summary : false ,
276
+ patch : false ,
277
+ context_lines : 3 ,
278
+ no_patch : false ,
279
+ source_prefix : 'a/' ,
280
+ dest_prefix : 'b/' ,
281
+ } ;
282
+
283
+ if ( options [ 'raw' ] )
284
+ result . raw = true ;
285
+ if ( options [ 'numstat' ] )
286
+ result . numstat = true ;
287
+ if ( options [ 'summary' ] )
288
+ result . summary = true ;
289
+ if ( options [ 'patch' ] )
290
+ result . patch = true ;
291
+ if ( options [ 'patch-with-raw' ] ) {
292
+ result . patch = true ;
293
+ result . raw = true ;
294
+ }
295
+ if ( options [ 'unified' ] !== undefined ) {
296
+ result . patch = true ;
297
+ result . context_lines = options [ 'unified' ] ;
298
+ }
299
+
300
+ // Prefixes
301
+ if ( options [ 'src-prefix' ] )
302
+ result . source_prefix = options [ 'src-prefix' ] ;
303
+ if ( options [ 'dst-prefix' ] )
304
+ result . dest_prefix = options [ 'dst-prefix' ] ;
305
+ if ( options [ 'default-prefix' ] ) {
306
+ result . source_prefix = 'a/' ;
307
+ result . dest_prefix = 'b/' ;
308
+ }
309
+ if ( options [ 'no-prefix' ] ) {
310
+ result . source_prefix = '' ;
311
+ result . dest_prefix = '' ;
312
+ }
313
+
314
+ // If nothing is specified, default to --patch
315
+ if ( ! result . raw && ! result . numstat && ! result . summary && ! result . patch )
316
+ result . patch = true ;
317
+
318
+ // --no-patch overrides the others
319
+ if ( options [ 'no-patch' ] )
320
+ result . no_patch = true ;
321
+
322
+ return result ;
323
+ }
324
+
325
+ /**
326
+ * Produce a string representation of the given diffs.
327
+ * @param diffs A single object, or array of them, in the format:
328
+ * {
329
+ * a: { mode, oid },
330
+ * b: { mode, oid },
331
+ * diff: object returned by Diff.structuredPatch() - see https://www.npmjs.com/package/diff
332
+ * }
333
+ * @param options Object returned by process_diff_formatting_options()
334
+ * @returns {string }
335
+ */
336
+ export const format_diffs = ( diffs , options ) => {
337
+ if ( ! ( diffs instanceof Array ) )
338
+ diffs = [ diffs ] ;
339
+
340
+ let s = '' ;
341
+ if ( options . raw ) {
342
+ // https://git-scm.com/docs/diff-format#_raw_output_format
343
+ for ( const { a, b, diff } of diffs ) {
344
+ s += `:${ a . mode } ${ b . mode } ${ shorten_hash ( a . oid ) } ${ shorten_hash ( b . oid ) } ` ;
345
+ // Status. For now, we just support A/D/M
346
+ if ( a . mode === '000000' ) {
347
+ s += 'A' ; // Added
348
+ } else if ( b . mode === '000000' ) {
349
+ s += 'D' ; // Deleted
350
+ } else {
351
+ s += 'M' ; // Modified
352
+ }
353
+ // TODO: -z option
354
+ s += `\t${ diff . oldFileName } \n` ;
355
+ }
356
+ s += '\n' ;
357
+ }
358
+
359
+ if ( options . numstat ) {
360
+ // https://git-scm.com/docs/diff-format#_other_diff_formats
361
+ for ( const { a, b, diff } of diffs ) {
362
+ const { added_lines, deleted_lines } = diff . hunks . reduce ( ( acc , hunk ) => {
363
+ const first_char_counts = hunk . lines . reduce ( ( acc , line ) => {
364
+ acc [ line [ 0 ] ] = ( acc [ line [ 0 ] ] || 0 ) + 1 ;
365
+ return acc ;
366
+ } , { } ) ;
367
+ acc . added_lines += first_char_counts [ '+' ] || 0 ;
368
+ acc . deleted_lines += first_char_counts [ '-' ] || 0 ;
369
+ return acc ;
370
+ } , { added_lines : 0 , deleted_lines : 0 } ) ;
371
+
372
+ // TODO: -z option
373
+ s += `${ added_lines } \t${ deleted_lines } \t` ;
374
+ if ( diff . oldFileName === diff . newFileName ) {
375
+ s += `${ diff . oldFileName } \n` ;
376
+ } else {
377
+ s += `${ diff . oldFileName } => ${ diff . newFileName } \n` ;
378
+ }
379
+ }
380
+ }
381
+
382
+ // TODO: --stat / --compact-summary
383
+
384
+ if ( options . summary ) {
385
+ // https://git-scm.com/docs/diff-format#_other_diff_formats
386
+ for ( const { a, b, diff } of diffs ) {
387
+ if ( diff . oldFileName === diff . newFileName )
388
+ continue ;
389
+
390
+ if ( diff . oldFileName === '/dev/null' ) {
391
+ s += `create mode ${ b . mode } ${ diff . newFileName } \n` ;
392
+ } else if ( diff . newFileName === '/dev/null' ) {
393
+ s += `delete mode ${ a . mode } ${ diff . oldFileName } \n` ;
394
+ } else {
395
+ // TODO: Abbreviate shared parts of path - see git manual link above.
396
+ s += `rename ${ diff . oldFileName } => ${ diff . newFileName } \n` ;
397
+ }
398
+ }
399
+ }
400
+
401
+ if ( options . patch ) {
402
+ for ( const { a, b, diff } of diffs ) {
403
+ const a_path = diff . oldFileName . startsWith ( '/' ) ? diff . oldFileName : `${ options . source_prefix } ${ diff . oldFileName } ` ;
404
+ const b_path = diff . newFileName . startsWith ( '/' ) ? diff . newFileName : `${ options . dest_prefix } ${ diff . newFileName } ` ;
405
+
406
+ // NOTE: This first line shows `a/$newFileName` for files that are new, not `/dev/null`.
407
+ const first_line_a_path = a_path !== '/dev/null' ? a_path : `${ options . source_prefix } ${ diff . newFileName } ` ;
408
+ s += `diff --git ${ first_line_a_path } ${ b_path } \n` ;
409
+ if ( a . mode === b . mode ) {
410
+ s += `index ${ shorten_hash ( a . oid ) } ..${ shorten_hash ( b . oid ) } ${ a . mode } ` ;
411
+ } else {
412
+ if ( a . mode === '000000' ) {
413
+ s += `new file mode ${ b . mode } \n` ;
414
+ } else {
415
+ s += `old mode ${ a . mode } \n` ;
416
+ s += `new mode ${ b . mode } \n` ;
417
+ }
418
+ s += `index ${ shorten_hash ( a . oid ) } ..${ shorten_hash ( b . oid ) } \n` ;
419
+ }
420
+ if ( ! diff . hunks . length )
421
+ continue ;
422
+
423
+ s += `--- ${ a_path } \n` ;
424
+ s += `+++ ${ b_path } \n` ;
425
+
426
+ for ( const hunk of diff . hunks ) {
427
+ s += `\x1b[36;1m@@ -${ hunk . oldStart } ,${ hunk . oldLines } +${ hunk . newStart } ,${ hunk . newLines } @@\x1b[0m\n` ;
428
+
429
+ for ( const line of hunk . lines ) {
430
+ switch ( line [ 0 ] ) {
431
+ case '+' :
432
+ s += `\x1b[32;1m${ line } \x1b[0m\n` ;
433
+ break ;
434
+ case '-' :
435
+ s += `\x1b[31;1m${ line } \x1b[0m\n` ;
436
+ break ;
437
+ default :
438
+ s += `${ line } \n` ;
439
+ break ;
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+
446
+
447
+ return s ;
448
+ }
0 commit comments