Skip to content

Commit 1678b7d

Browse files
committed
Merge branch 'mm/line-log-use-standard-diff-output'
The way the "git log -L<range>:<file>" feature is bolted onto the log/diff machinery is being reworked a bit to make the feature compatible with more diff options, like -S/G. * mm/line-log-use-standard-diff-output: doc: note that -L supports patch formatting and pickaxe options t4211: add tests for -L with standard diff options line-log: route -L output through the standard diff pipeline line-log: fix crash when combined with pickaxe options
2 parents fb55169 + 512536a commit 1678b7d

35 files changed

+870
-217
lines changed

Documentation/line-range-options.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@
1212
(namely `--raw`, `--numstat`, `--shortstat`, `--dirstat`, `--summary`,
1313
`--name-only`, `--name-status`, `--check`) are not currently implemented.
1414
+
15+
Patch formatting options such as `--word-diff`, `--color-moved`,
16+
`--no-prefix`, and whitespace options (`-w`, `-b`) are supported,
17+
as are pickaxe options (`-S`, `-G`).
18+
+
1519
include::line-range-format.adoc[]

diff.c

Lines changed: 277 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
611657
static 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+
24962724
static 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;

diffcore.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ struct userdiff_driver;
1919
* in anything else.
2020
*/
2121

22+
/* A range [start, end). Lines are numbered starting at 0. */
23+
struct range {
24+
long start, end;
25+
};
26+
27+
/* A set of ranges. The ranges must always be disjoint and sorted. */
28+
struct range_set {
29+
unsigned int alloc, nr;
30+
struct range *ranges;
31+
};
32+
2233
/* We internally use unsigned short as the score value,
2334
* and rely on an int capable to hold 32-bits. -B can take
2435
* -Bmerge_score/break_score format and the two scores are
@@ -106,6 +117,11 @@ int diff_filespec_is_binary(struct repository *, struct diff_filespec *);
106117
struct diff_filepair {
107118
struct diff_filespec *one;
108119
struct diff_filespec *two;
120+
/*
121+
* Tracked line ranges for -L filtering; borrowed from
122+
* line_log_data and must not be freed.
123+
*/
124+
const struct range_set *line_ranges;
109125
unsigned short int score;
110126
char status; /* M C R A D U etc. (see Documentation/diff-format.adoc or DIFF_STATUS_* in diff.h) */
111127
unsigned broken_pair : 1;

0 commit comments

Comments
 (0)