@@ -608,6 +608,52 @@ struct emit_callback {
608608 struct strbuf * header ;
609609};
610610
611+ /*
612+ * State for the line-range callback wrappers that sit between
613+ * xdi_diff_outf() and fn_out_consume(). xdiff produces a normal,
614+ * unfiltered diff; the wrappers intercept each hunk header and line,
615+ * track post-image position, and forward only lines that fall within
616+ * the requested ranges. Contiguous in-range lines are collected into
617+ * range hunks and flushed with a synthetic @@ header so that
618+ * fn_out_consume() sees well-formed unified-diff fragments.
619+ *
620+ * Removal lines ('-') cannot be classified by post-image position, so
621+ * they are buffered in pending_rm until the next '+' or ' ' line
622+ * reveals whether they precede an in-range line (flush into range hunk) or
623+ * an out-of-range line (discard).
624+ */
625+ struct line_range_callback {
626+ xdiff_emit_line_fn orig_line_fn ;
627+ void * orig_cb_data ;
628+ const struct range_set * ranges ; /* 0-based [start, end) */
629+ unsigned int cur_range ; /* index into the range_set */
630+
631+ /* Post/pre-image line counters (1-based, set from hunk headers) */
632+ long lno_post ;
633+ long lno_pre ;
634+
635+ /*
636+ * Function name from most recent xdiff hunk header;
637+ * size matches struct func_line.buf in xdiff/xemit.c.
638+ */
639+ char func [80 ];
640+ long funclen ;
641+
642+ /* Range hunk being accumulated for the current range */
643+ struct strbuf rhunk ;
644+ long rhunk_old_begin , rhunk_old_count ;
645+ long rhunk_new_begin , rhunk_new_count ;
646+ int rhunk_active ;
647+ int rhunk_has_changes ; /* any '+' or '-' lines? */
648+
649+ /* Removal lines not yet known to be in-range */
650+ struct strbuf pending_rm ;
651+ int pending_rm_count ;
652+ long pending_rm_pre_begin ; /* pre-image line of first pending */
653+
654+ int ret ; /* latched error from orig_line_fn */
655+ };
656+
611657static int count_lines (const char * data , int size )
612658{
613659 int count , ch , completely_empty = 1 , nl_just_seen = 0 ;
@@ -2493,6 +2539,188 @@ static int quick_consume(void *priv, char *line UNUSED, unsigned long len UNUSED
24932539 return 1 ;
24942540}
24952541
2542+ static void discard_pending_rm (struct line_range_callback * s )
2543+ {
2544+ strbuf_reset (& s -> pending_rm );
2545+ s -> pending_rm_count = 0 ;
2546+ }
2547+
2548+ static void flush_rhunk (struct line_range_callback * s )
2549+ {
2550+ struct strbuf hdr = STRBUF_INIT ;
2551+ const char * p , * end ;
2552+
2553+ if (!s -> rhunk_active || s -> ret )
2554+ return ;
2555+
2556+ /* Drain any pending removal lines into the range hunk */
2557+ if (s -> pending_rm_count ) {
2558+ strbuf_addbuf (& s -> rhunk , & s -> pending_rm );
2559+ s -> rhunk_old_count += s -> pending_rm_count ;
2560+ s -> rhunk_has_changes = 1 ;
2561+ discard_pending_rm (s );
2562+ }
2563+
2564+ /*
2565+ * Suppress context-only hunks: they contain no actual changes
2566+ * and would just be noise. This can happen when the inflated
2567+ * ctxlen causes xdiff to emit context covering a range that
2568+ * has no changes in this commit.
2569+ */
2570+ if (!s -> rhunk_has_changes ) {
2571+ s -> rhunk_active = 0 ;
2572+ strbuf_reset (& s -> rhunk );
2573+ return ;
2574+ }
2575+
2576+ strbuf_addf (& hdr , "@@ -%ld,%ld +%ld,%ld @@" ,
2577+ s -> rhunk_old_begin , s -> rhunk_old_count ,
2578+ s -> rhunk_new_begin , s -> rhunk_new_count );
2579+ if (s -> funclen > 0 ) {
2580+ strbuf_addch (& hdr , ' ' );
2581+ strbuf_add (& hdr , s -> func , s -> funclen );
2582+ }
2583+ strbuf_addch (& hdr , '\n' );
2584+
2585+ s -> ret = s -> orig_line_fn (s -> orig_cb_data , hdr .buf , hdr .len );
2586+ strbuf_release (& hdr );
2587+
2588+ /*
2589+ * Replay buffered lines one at a time through fn_out_consume.
2590+ * The cast discards const because xdiff_emit_line_fn takes
2591+ * char *, though fn_out_consume does not modify the buffer.
2592+ */
2593+ p = s -> rhunk .buf ;
2594+ end = p + s -> rhunk .len ;
2595+ while (!s -> ret && p < end ) {
2596+ const char * eol = memchr (p , '\n' , end - p );
2597+ unsigned long line_len = eol ? (unsigned long )(eol - p + 1 )
2598+ : (unsigned long )(end - p );
2599+ s -> ret = s -> orig_line_fn (s -> orig_cb_data , (char * )p , line_len );
2600+ p += line_len ;
2601+ }
2602+
2603+ s -> rhunk_active = 0 ;
2604+ strbuf_reset (& s -> rhunk );
2605+ }
2606+
2607+ static void line_range_hunk_fn (void * data ,
2608+ long old_begin , long old_nr UNUSED ,
2609+ long new_begin , long new_nr UNUSED ,
2610+ const char * func , long funclen )
2611+ {
2612+ struct line_range_callback * s = data ;
2613+
2614+ /*
2615+ * When count > 0, begin is 1-based. When count == 0, begin is
2616+ * adjusted down by 1 by xdl_emit_hunk_hdr(), but no lines of
2617+ * that type will arrive, so the value is unused.
2618+ *
2619+ * Any pending removal lines from the previous xdiff hunk are
2620+ * intentionally left in pending_rm: the line callback will
2621+ * flush or discard them when the next content line reveals
2622+ * whether the removals precede in-range content.
2623+ */
2624+ s -> lno_post = new_begin ;
2625+ s -> lno_pre = old_begin ;
2626+
2627+ if (funclen > 0 ) {
2628+ if (funclen > (long )sizeof (s -> func ))
2629+ funclen = sizeof (s -> func );
2630+ memcpy (s -> func , func , funclen );
2631+ }
2632+ s -> funclen = funclen ;
2633+ }
2634+
2635+ static int line_range_line_fn (void * priv , char * line , unsigned long len )
2636+ {
2637+ struct line_range_callback * s = priv ;
2638+ const struct range * cur ;
2639+ long lno_0 , cur_pre ;
2640+
2641+ if (s -> ret )
2642+ return s -> ret ;
2643+
2644+ if (line [0 ] == '-' ) {
2645+ if (!s -> pending_rm_count )
2646+ s -> pending_rm_pre_begin = s -> lno_pre ;
2647+ s -> lno_pre ++ ;
2648+ strbuf_add (& s -> pending_rm , line , len );
2649+ s -> pending_rm_count ++ ;
2650+ return s -> ret ;
2651+ }
2652+
2653+ if (line [0 ] == '\\' ) {
2654+ if (s -> pending_rm_count )
2655+ strbuf_add (& s -> pending_rm , line , len );
2656+ else if (s -> rhunk_active )
2657+ strbuf_add (& s -> rhunk , line , len );
2658+ /* otherwise outside tracked range; drop silently */
2659+ return s -> ret ;
2660+ }
2661+
2662+ if (line [0 ] != '+' && line [0 ] != ' ' )
2663+ BUG ("unexpected diff line type '%c'" , line [0 ]);
2664+
2665+ lno_0 = s -> lno_post - 1 ;
2666+ cur_pre = s -> lno_pre ; /* save before advancing for context lines */
2667+ s -> lno_post ++ ;
2668+ if (line [0 ] == ' ' )
2669+ s -> lno_pre ++ ;
2670+
2671+ /* Advance past ranges we've passed */
2672+ while (s -> cur_range < s -> ranges -> nr &&
2673+ lno_0 >= s -> ranges -> ranges [s -> cur_range ].end ) {
2674+ if (s -> rhunk_active )
2675+ flush_rhunk (s );
2676+ discard_pending_rm (s );
2677+ s -> cur_range ++ ;
2678+ }
2679+
2680+ /* Past all ranges */
2681+ if (s -> cur_range >= s -> ranges -> nr ) {
2682+ discard_pending_rm (s );
2683+ return s -> ret ;
2684+ }
2685+
2686+ cur = & s -> ranges -> ranges [s -> cur_range ];
2687+
2688+ /* Before current range */
2689+ if (lno_0 < cur -> start ) {
2690+ discard_pending_rm (s );
2691+ return s -> ret ;
2692+ }
2693+
2694+ /* In range so start a new range hunk if needed */
2695+ if (!s -> rhunk_active ) {
2696+ s -> rhunk_active = 1 ;
2697+ s -> rhunk_has_changes = 0 ;
2698+ s -> rhunk_new_begin = lno_0 + 1 ;
2699+ s -> rhunk_old_begin = s -> pending_rm_count
2700+ ? s -> pending_rm_pre_begin : cur_pre ;
2701+ s -> rhunk_old_count = 0 ;
2702+ s -> rhunk_new_count = 0 ;
2703+ strbuf_reset (& s -> rhunk );
2704+ }
2705+
2706+ /* Flush pending removals into range hunk */
2707+ if (s -> pending_rm_count ) {
2708+ strbuf_addbuf (& s -> rhunk , & s -> pending_rm );
2709+ s -> rhunk_old_count += s -> pending_rm_count ;
2710+ s -> rhunk_has_changes = 1 ;
2711+ discard_pending_rm (s );
2712+ }
2713+
2714+ strbuf_add (& s -> rhunk , line , len );
2715+ s -> rhunk_new_count ++ ;
2716+ if (line [0 ] == '+' )
2717+ s -> rhunk_has_changes = 1 ;
2718+ else
2719+ s -> rhunk_old_count ++ ;
2720+
2721+ return s -> ret ;
2722+ }
2723+
24962724static void pprint_rename (struct strbuf * name , const char * a , const char * b )
24972725{
24982726 const char * old_name = a ;
@@ -3592,7 +3820,8 @@ static void builtin_diff(const char *name_a,
35923820 const char * xfrm_msg ,
35933821 int must_show_header ,
35943822 struct diff_options * o ,
3595- int complete_rewrite )
3823+ int complete_rewrite ,
3824+ const struct range_set * line_ranges )
35963825{
35973826 mmfile_t mf1 , mf2 ;
35983827 const char * lbl [2 ];
@@ -3833,6 +4062,52 @@ static void builtin_diff(const char *name_a,
38334062 */
38344063 xdi_diff_outf (& mf1 , & mf2 , NULL , quick_consume ,
38354064 & ecbdata , & xpp , & xecfg );
4065+ } else if (line_ranges ) {
4066+ struct line_range_callback lr_state ;
4067+ unsigned int i ;
4068+ long max_span = 0 ;
4069+
4070+ memset (& lr_state , 0 , sizeof (lr_state ));
4071+ lr_state .orig_line_fn = fn_out_consume ;
4072+ lr_state .orig_cb_data = & ecbdata ;
4073+ lr_state .ranges = line_ranges ;
4074+ strbuf_init (& lr_state .rhunk , 0 );
4075+ strbuf_init (& lr_state .pending_rm , 0 );
4076+
4077+ /*
4078+ * Inflate ctxlen so that all changes within
4079+ * any single range are merged into one xdiff
4080+ * hunk and the inter-change context is emitted.
4081+ * The callback clips back to range boundaries.
4082+ *
4083+ * The optimal ctxlen depends on where changes
4084+ * fall within the range, which is only known
4085+ * after xdiff runs; the max range span is the
4086+ * upper bound that guarantees correctness in a
4087+ * single pass.
4088+ */
4089+ for (i = 0 ; i < line_ranges -> nr ; i ++ ) {
4090+ long span = line_ranges -> ranges [i ].end -
4091+ line_ranges -> ranges [i ].start ;
4092+ if (span > max_span )
4093+ max_span = span ;
4094+ }
4095+ if (max_span > xecfg .ctxlen )
4096+ xecfg .ctxlen = max_span ;
4097+
4098+ if (xdi_diff_outf (& mf1 , & mf2 ,
4099+ line_range_hunk_fn ,
4100+ line_range_line_fn ,
4101+ & lr_state , & xpp , & xecfg ))
4102+ die ("unable to generate diff for %s" ,
4103+ one -> path );
4104+
4105+ flush_rhunk (& lr_state );
4106+ if (lr_state .ret )
4107+ die ("unable to generate diff for %s" ,
4108+ one -> path );
4109+ strbuf_release (& lr_state .rhunk );
4110+ strbuf_release (& lr_state .pending_rm );
38364111 } else if (xdi_diff_outf (& mf1 , & mf2 , NULL , fn_out_consume ,
38374112 & ecbdata , & xpp , & xecfg ))
38384113 die ("unable to generate diff for %s" , one -> path );
@@ -4674,7 +4949,7 @@ static void run_diff_cmd(const struct external_diff *pgm,
46744949
46754950 builtin_diff (name , other ? other : name ,
46764951 one , two , xfrm_msg , must_show_header ,
4677- o , complete_rewrite );
4952+ o , complete_rewrite , p -> line_ranges );
46784953 if (p -> status == DIFF_STATUS_COPIED ||
46794954 p -> status == DIFF_STATUS_RENAMED )
46804955 o -> found_changes = 1 ;
0 commit comments