Merge branch 'mm/line-log-limited-ops' into seen

The 'git log -L<range>:<path>' command has been taught to limit
various 'diff' operations, such as '--stat', '--check', and '-G', to
the specified range and path.

* mm/line-log-limited-ops:
  diffcore-pickaxe: limit -G to the -L tracked range
  diff: support --check with -L line ranges
  diff: support stat formats with -L
  diff: extract a line-range diff helper for reuse
  diff: emit -L hunk headers via xdiff's formatter
  diff: simplify the line-range filter by classifying removals immediately
  diff: rename line-range filter struct and clarify fields
Junio C Hamano 2026-09-17 12:54:36 -07:00
commit 16112e2792
11 changed files with 588 additions and 268 deletions

View File

@ -9,13 +9,18 @@
_<start>_ and _<end>_ (or _<funcname>_) must exist in the starting revision.
You can specify this option more than once. Implies `--patch`.
Patch output can be suppressed using `--no-patch`.
Non-patch diff formats `--raw`, `--name-only`, `--name-status`,
and `--summary` are supported. Diff stat formats
(`--stat`, `--numstat`, `--shortstat`, `--dirstat`) are not
currently implemented.
The following non-patch diff formats are supported: `--raw`,
`--name-only`, `--name-status`, `--summary`, `--check`, `--stat`,
`--numstat`, and `--shortstat`. The stat formats count only lines
within the tracked range. `--dirstat` is not supported with `-L`: it
reports how change is distributed across directories over whole files,
which is not meaningful for line ranges within a file. Use `--numstat`
for exact per-file counts within the range.
+
Patch formatting options such as `--word-diff`, `--color-moved`,
`--no-prefix`, and whitespace options (`-w`, `-b`) are supported,
as are pickaxe options (`-S`, `-G`) and `--diff-filter`.
as are pickaxe options (`-S`, `-G`) and `--diff-filter`. `-G` is
limited to the tracked range. In contrast, `-S` is evaluated over the whole
file and may select a commit with a change outside the tracked range.
+
include::line-range-format.adoc[]

422
diff.c
View File

@ -610,49 +610,27 @@ struct emit_callback {
};

/*
* State for the line-range callback wrappers that sit between
* xdi_diff_outf() and fn_out_consume(). xdiff produces a normal,
* unfiltered diff; the wrappers intercept each hunk header and line,
* track post-image position, and forward only lines that fall within
* the requested ranges. Contiguous in-range lines are collected into
* range hunks and flushed with a synthetic @@ header so that
* fn_out_consume() sees well-formed unified-diff fragments.
*
* Removal lines ('-') cannot be classified by post-image position, so
* they are buffered in pending_rm until the next '+' or ' ' line
* reveals whether they precede an in-range line (flush into range hunk) or
* an out-of-range line (discard).
* Filter the line ranges that are emitted by diff.
*/
struct line_range_callback {
struct line_range_filter {
xdiff_emit_line_fn orig_line_fn;
xdiff_emit_hunk_fn orig_hunk_fn;
void *orig_cb_data;
const struct range_set *ranges; /* 0-based [start, end) */
unsigned int cur_range; /* index into the range_set */
const struct range_set *range_sets_to_filter_by;
unsigned int range_set_idx;

/* Post/pre-image line counters (1-based, set from hunk headers) */
long lno_post;
long lno_pre;
struct {
char func_name[80];
long func_name_len;
long old_begin;
long new_begin;
long lno_in_preimage;
long lno_in_postimage;
struct strbuf lines;
int active;
} accumulating_hunk;

/*
* Function name from most recent xdiff hunk header;
* size matches struct func_line.buf in xdiff/xemit.c.
*/
char func[80];
long funclen;

/* Range hunk being accumulated for the current range */
struct strbuf rhunk;
long rhunk_old_begin, rhunk_old_count;
long rhunk_new_begin, rhunk_new_count;
int rhunk_active;
int rhunk_has_changes; /* any '+' or '-' lines? */

/* Removal lines not yet known to be in-range */
struct strbuf pending_rm;
int pending_rm_count;
long pending_rm_pre_begin; /* pre-image line of first pending */

int ret; /* latched error from orig_line_fn */
int ret;
};

static int count_lines(const char *data, int size)
@ -2540,186 +2518,198 @@ static int quick_consume(void *priv, char *line UNUSED, unsigned long len UNUSED
return 1;
}

static void discard_pending_rm(struct line_range_callback *s)
static void line_range_filter_init(struct line_range_filter *filter,
const struct range_set *ranges,
xdiff_emit_line_fn line_fn,
void *cb_data)
{
strbuf_reset(&s->pending_rm);
s->pending_rm_count = 0;
memset(filter, 0, sizeof(*filter));
filter->orig_line_fn = line_fn;
filter->orig_cb_data = cb_data;
filter->range_sets_to_filter_by = ranges;
strbuf_init(&filter->accumulating_hunk.lines, 0);
}

static void flush_rhunk(struct line_range_callback *s)
static void begin_range_hunk(struct line_range_filter *filter)
{
filter->accumulating_hunk.active = 1;
filter->accumulating_hunk.new_begin = filter->accumulating_hunk.lno_in_postimage;
filter->accumulating_hunk.old_begin = filter->accumulating_hunk.lno_in_preimage;
strbuf_reset(&filter->accumulating_hunk.lines);
}

static void flush_range_hunk(struct line_range_filter *filter)
{
struct strbuf hdr = STRBUF_INIT;
const char *p, *end;
const char *line_buf, *line_buf_end;
long old_count = 0, new_count = 0;
int has_changes = 0;

if (!s->rhunk_active || s->ret)
if (!filter->accumulating_hunk.active || filter->ret)
return;

/* Drain any pending removal lines into the range hunk */
if (s->pending_rm_count) {
strbuf_addbuf(&s->rhunk, &s->pending_rm);
s->rhunk_old_count += s->pending_rm_count;
s->rhunk_has_changes = 1;
discard_pending_rm(s);
line_buf = filter->accumulating_hunk.lines.buf;
line_buf_end = line_buf + filter->accumulating_hunk.lines.len;
while (line_buf < line_buf_end) {
const char *eol = memchr(line_buf, '\n', line_buf_end - line_buf);
if (*line_buf == ' ') {
old_count++;
new_count++;
}
else if (*line_buf == '-') {
old_count++;
has_changes = 1;
}
else if (*line_buf == '+') {
new_count++;
has_changes = 1;
}
line_buf = eol ? eol + 1 : line_buf_end;
}

/*
* Suppress context-only hunks: they contain no actual changes
* and would just be noise. This can happen when the inflated
* ctxlen causes xdiff to emit context covering a range that
* has no changes in this commit.
*/
if (!s->rhunk_has_changes) {
s->rhunk_active = 0;
strbuf_reset(&s->rhunk);
if (!has_changes) {
filter->accumulating_hunk.active = 0;
strbuf_reset(&filter->accumulating_hunk.lines);
return;
}

strbuf_addf(&hdr, "@@ -%ld,%ld +%ld,%ld @@",
s->rhunk_old_begin, s->rhunk_old_count,
s->rhunk_new_begin, s->rhunk_new_count);
if (s->funclen > 0) {
strbuf_addch(&hdr, ' ');
strbuf_add(&hdr, s->func, s->funclen);
}
strbuf_addch(&hdr, '\n');
xdiff_emit_hunk_header(&hdr, filter->accumulating_hunk.old_begin, old_count,
filter->accumulating_hunk.new_begin, new_count,
filter->accumulating_hunk.func_name,
filter->accumulating_hunk.func_name_len);

s->ret = s->orig_line_fn(s->orig_cb_data, hdr.buf, hdr.len);
if (filter->orig_hunk_fn)
filter->orig_hunk_fn(filter->orig_cb_data,
filter->accumulating_hunk.old_begin, old_count,
filter->accumulating_hunk.new_begin, new_count,
filter->accumulating_hunk.func_name,
filter->accumulating_hunk.func_name_len);

filter->ret = filter->orig_line_fn(filter->orig_cb_data, hdr.buf, hdr.len);
strbuf_release(&hdr);

/*
* Replay buffered lines one at a time through fn_out_consume.
* The cast discards const because xdiff_emit_line_fn takes
* char *, though fn_out_consume does not modify the buffer.
*/
p = s->rhunk.buf;
end = p + s->rhunk.len;
while (!s->ret && p < end) {
const char *eol = memchr(p, '\n', end - p);
unsigned long line_len = eol ? (unsigned long)(eol - p + 1)
: (unsigned long)(end - p);
s->ret = s->orig_line_fn(s->orig_cb_data, (char *)p, line_len);
p += line_len;
line_buf = filter->accumulating_hunk.lines.buf;
line_buf_end = line_buf + filter->accumulating_hunk.lines.len;
while (!filter->ret && line_buf < line_buf_end) {
const char *eol = memchr(line_buf, '\n', line_buf_end - line_buf);
unsigned long line_len = eol ? (unsigned long)(eol - line_buf + 1)
: (unsigned long)(line_buf_end - line_buf);
filter->ret = filter->orig_line_fn(filter->orig_cb_data,
(char *)line_buf, line_len);
line_buf += line_len;
}

s->rhunk_active = 0;
strbuf_reset(&s->rhunk);
filter->accumulating_hunk.active = 0;
strbuf_reset(&filter->accumulating_hunk.lines);
}

static void line_range_hunk_fn(void *data,
long old_begin, long old_nr UNUSED,
long new_begin, long new_nr UNUSED,
long old_begin, long old_nr,
long new_begin, long new_nr,
const char *func, long funclen)
{
struct line_range_callback *s = data;
struct line_range_filter *filter = data;

/*
* When count > 0, begin is 1-based. When count == 0, begin is
* adjusted down by 1 by xdl_emit_hunk_hdr(), but no lines of
* that type will arrive, so the value is unused.
*
* Any pending removal lines from the previous xdiff hunk are
* intentionally left in pending_rm: the line callback will
* flush or discard them when the next content line reveals
* whether the removals precede in-range content.
*/
s->lno_post = new_begin;
s->lno_pre = old_begin;
filter->accumulating_hunk.lno_in_postimage = new_nr ? new_begin : new_begin + 1;
filter->accumulating_hunk.lno_in_preimage = old_nr ? old_begin : old_begin + 1;

if (funclen > 0) {
if (funclen > (long)sizeof(s->func))
funclen = sizeof(s->func);
memcpy(s->func, func, funclen);
if (funclen > (long)sizeof(filter->accumulating_hunk.func_name))
funclen = sizeof(filter->accumulating_hunk.func_name);
memcpy(filter->accumulating_hunk.func_name, func, funclen);
}
s->funclen = funclen;
filter->accumulating_hunk.func_name_len = funclen;
}

static int line_range_line_fn(void *priv, char *line, unsigned long len)
{
struct line_range_callback *s = priv;
const struct range *cur;
long lno_0, cur_pre;
struct line_range_filter *filter = priv;
long idx_in_postimage;
int in_range;

if (s->ret)
return s->ret;

if (line[0] == '-') {
if (!s->pending_rm_count)
s->pending_rm_pre_begin = s->lno_pre;
s->lno_pre++;
strbuf_add(&s->pending_rm, line, len);
s->pending_rm_count++;
return s->ret;
}
if (filter->ret)
return filter->ret;

if (line[0] == '\\') {
if (s->pending_rm_count)
strbuf_add(&s->pending_rm, line, len);
else if (s->rhunk_active)
strbuf_add(&s->rhunk, line, len);
/* otherwise outside tracked range; drop silently */
return s->ret;
if (filter->accumulating_hunk.active)
strbuf_add(&filter->accumulating_hunk.lines, line, len);
return filter->ret;
}

if (line[0] != '+' && line[0] != ' ')
if (line[0] != '+' && line[0] != ' ' && line[0] != '-')
BUG("unexpected diff line type '%c'", line[0]);

lno_0 = s->lno_post - 1;
cur_pre = s->lno_pre; /* save before advancing for context lines */
s->lno_post++;
if (line[0] == ' ')
s->lno_pre++;
idx_in_postimage = filter->accumulating_hunk.lno_in_postimage - 1;

/* Advance past ranges we've passed */
while (s->cur_range < s->ranges->nr &&
lno_0 >= s->ranges->ranges[s->cur_range].end) {
if (s->rhunk_active)
flush_rhunk(s);
discard_pending_rm(s);
s->cur_range++;
while (filter->range_set_idx < filter->range_sets_to_filter_by->nr &&
idx_in_postimage >=
filter->range_sets_to_filter_by->ranges[filter->range_set_idx].end) {
if (filter->accumulating_hunk.active)
flush_range_hunk(filter);
filter->range_set_idx++;
}

/* Past all ranges */
if (s->cur_range >= s->ranges->nr) {
discard_pending_rm(s);
return s->ret;
in_range = filter->range_set_idx < filter->range_sets_to_filter_by->nr &&
idx_in_postimage >=
filter->range_sets_to_filter_by->ranges[filter->range_set_idx].start &&
idx_in_postimage <
filter->range_sets_to_filter_by->ranges[filter->range_set_idx].end;

if (in_range) {
if (!filter->accumulating_hunk.active)
begin_range_hunk(filter);

strbuf_add(&filter->accumulating_hunk.lines, line, len);
}

cur = &s->ranges->ranges[s->cur_range];
if (line[0] == ' ' || line[0] == '+')
filter->accumulating_hunk.lno_in_postimage++;
if (line[0] == ' ' || line[0] == '-')
filter->accumulating_hunk.lno_in_preimage++;

/* Before current range */
if (lno_0 < cur->start) {
discard_pending_rm(s);
return s->ret;
return filter->ret;
}


static int line_range_filter_diff(struct line_range_filter *filter,
mmfile_t *mf1, mmfile_t *mf2,
xpparam_t *xpp, xdemitconf_t *xecfg)
{
const struct range_set *ranges = filter->range_sets_to_filter_by;
long max_span = 0;
unsigned int i;
int ret;

for (i = 0; i < ranges->nr; i++) {
long span = ranges->ranges[i].end - ranges->ranges[i].start;
if (span > max_span)
max_span = span;
}
if (max_span > xecfg->ctxlen)
xecfg->ctxlen = max_span;

/* In range so start a new range hunk if needed */
if (!s->rhunk_active) {
s->rhunk_active = 1;
s->rhunk_has_changes = 0;
s->rhunk_new_begin = lno_0 + 1;
s->rhunk_old_begin = s->pending_rm_count
? s->pending_rm_pre_begin : cur_pre;
s->rhunk_old_count = 0;
s->rhunk_new_count = 0;
strbuf_reset(&s->rhunk);
/* the filter seeds its per-image position from hunk headers */
xecfg->flags &= ~XDL_EMIT_NO_HUNK_HDR;

ret = xdi_diff_outf(mf1, mf2, line_range_hunk_fn,
line_range_line_fn, filter, xpp, xecfg);
if (!ret) {
flush_range_hunk(filter);
ret = filter->ret;
}
strbuf_release(&filter->accumulating_hunk.lines);
return ret;
}

/* Flush pending removals into range hunk */
if (s->pending_rm_count) {
strbuf_addbuf(&s->rhunk, &s->pending_rm);
s->rhunk_old_count += s->pending_rm_count;
s->rhunk_has_changes = 1;
discard_pending_rm(s);
}
int diff_emit_line_ranges(mmfile_t *one, mmfile_t *two,
const struct range_set *ranges,
xdiff_emit_line_fn line_fn, void *cb_data,
xpparam_t *xpp, xdemitconf_t *xecfg)
{
struct line_range_filter filter;

strbuf_add(&s->rhunk, line, len);
s->rhunk_new_count++;
if (line[0] == '+')
s->rhunk_has_changes = 1;
else
s->rhunk_old_count++;

return s->ret;
line_range_filter_init(&filter, ranges, line_fn, cb_data);
return line_range_filter_diff(&filter, one, two, xpp, xecfg);
}

static void pprint_rename(struct strbuf *name, const char *a, const char *b)
@ -4066,51 +4056,15 @@ static void builtin_diff(const char *name_a,
xdi_diff_outf(&mf1, &mf2, NULL, quick_consume,
&ecbdata, &xpp, &xecfg);
} else if (line_ranges) {
struct line_range_callback lr_state;
unsigned int i;
long max_span = 0;
struct line_range_filter lr_filter;

memset(&lr_state, 0, sizeof(lr_state));
lr_state.orig_line_fn = fn_out_consume;
lr_state.orig_cb_data = &ecbdata;
lr_state.ranges = line_ranges;
strbuf_init(&lr_state.rhunk, 0);
strbuf_init(&lr_state.pending_rm, 0);
line_range_filter_init(&lr_filter, line_ranges,
fn_out_consume, &ecbdata);

/*
* Inflate ctxlen so that all changes within
* any single range are merged into one xdiff
* hunk and the inter-change context is emitted.
* The callback clips back to range boundaries.
*
* The optimal ctxlen depends on where changes
* fall within the range, which is only known
* after xdiff runs; the max range span is the
* upper bound that guarantees correctness in a
* single pass.
*/
for (i = 0; i < line_ranges->nr; i++) {
long span = line_ranges->ranges[i].end -
line_ranges->ranges[i].start;
if (span > max_span)
max_span = span;
}
if (max_span > xecfg.ctxlen)
xecfg.ctxlen = max_span;

if (xdi_diff_outf(&mf1, &mf2,
line_range_hunk_fn,
line_range_line_fn,
&lr_state, &xpp, &xecfg))
if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
&xpp, &xecfg))
die("unable to generate diff for %s",
one->path);

flush_rhunk(&lr_state);
if (lr_state.ret)
die("unable to generate diff for %s",
one->path);
strbuf_release(&lr_state.rhunk);
strbuf_release(&lr_state.pending_rm);
} else if (xdi_diff_outf(&mf1, &mf2, NULL, fn_out_consume,
&ecbdata, &xpp, &xecfg))
die("unable to generate diff for %s", one->path);
@ -4227,7 +4181,18 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
xecfg.ctxlen = o->context;
xecfg.interhunkctxlen = o->interhunkcontext;
xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
if (xdi_diff_outf(&mf1, &mf2, NULL,

if (p->line_ranges) {
struct line_range_filter lr_filter;

line_range_filter_init(&lr_filter, p->line_ranges,
diffstat_consume, diffstat);

if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
&xpp, &xecfg))
die("unable to generate diffstat for %s",
one->path);
} else if (xdi_diff_outf(&mf1, &mf2, NULL,
diffstat_consume, diffstat, &xpp, &xecfg))
die("unable to generate diffstat for %s", one->path);

@ -4257,11 +4222,23 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
diff_free_filespec_data(two);
}

static int idx_in_ranges(const struct range_set *ranges, long idx)
{
unsigned int i;

for (i = 0; i < ranges->nr; i++)
if (idx >= ranges->ranges[i].start &&
idx < ranges->ranges[i].end)
return 1;
return 0;
}

static void builtin_checkdiff(const char *name_a, const char *name_b,
const char *attr_path,
struct diff_filespec *one,
struct diff_filespec *two,
struct diff_options *o)
struct diff_options *o,
const struct range_set *line_ranges)
{
mmfile_t mf1, mf2;
struct checkdiff_t data;
@ -4301,7 +4278,19 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
memset(&xecfg, 0, sizeof(xecfg));
xecfg.ctxlen = 1; /* at least one context line */
xpp.flags = 0;
if (xdi_diff_outf(&mf1, &mf2, checkdiff_consume_hunk,

if (line_ranges) {
struct line_range_filter lr_filter;

line_range_filter_init(&lr_filter, line_ranges,
checkdiff_consume, &data);
lr_filter.orig_hunk_fn = checkdiff_consume_hunk;

if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
&xpp, &xecfg))
die("unable to generate checkdiff for %s",
one->path);
} else if (xdi_diff_outf(&mf1, &mf2, checkdiff_consume_hunk,
checkdiff_consume, &data,
&xpp, &xecfg))
die("unable to generate checkdiff for %s", one->path);
@ -4314,6 +4303,10 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
check_blank_at_eof(&mf1, &mf2, &ecbdata);
blank_at_eof = ecbdata.blank_at_eof_in_postimage;

if (blank_at_eof && line_ranges &&
!idx_in_ranges(line_ranges, blank_at_eof - 1))
blank_at_eof = 0;

if (blank_at_eof) {
static char *err;
if (!err)
@ -5109,7 +5102,8 @@ static void run_checkdiff(struct diff_filepair *p, struct diff_options *o)
diff_fill_oid_info(p->one, o->repo->index);
diff_fill_oid_info(p->two, o->repo->index);

builtin_checkdiff(name, other, attr_path, p->one, p->two, o);
builtin_checkdiff(name, other, attr_path, p->one, p->two, o,
p->line_ranges);
}

void repo_diff_setup(struct repository *r, struct diff_options *options)

View File

@ -16,7 +16,8 @@

typedef int (*pickaxe_fn)(mmfile_t *one, mmfile_t *two,
struct diff_options *o,
regex_t *regexp, kwset_t kws);
regex_t *regexp, kwset_t kws,
const struct range_set *ranges);

struct diffgrep_cb {
regex_t *regexp;
@ -42,7 +43,8 @@ static int diffgrep_consume(void *priv, char *line, unsigned long len)

static int diff_grep(mmfile_t *one, mmfile_t *two,
struct diff_options *o,
regex_t *regexp, kwset_t kws UNUSED)
regex_t *regexp, kwset_t kws UNUSED,
const struct range_set *ranges)
{
struct diffgrep_cb ecbdata;
xpparam_t xpp;
@ -65,8 +67,12 @@ static int diff_grep(mmfile_t *one, mmfile_t *two,
* An xdiff error might be our "data->hit" from above. See the
* comment for xdiff_emit_line_fn in xdiff-interface.h
*/
ret = xdi_diff_outf(one, two, NULL, diffgrep_consume,
&ecbdata, &xpp, &xecfg);
if (ranges)
ret = diff_emit_line_ranges(one, two, ranges, diffgrep_consume,
&ecbdata, &xpp, &xecfg);
else
ret = xdi_diff_outf(one, two, NULL, diffgrep_consume,
&ecbdata, &xpp, &xecfg);
if (ecbdata.hit)
return 1;
if (ret)
@ -119,8 +125,13 @@ static unsigned int contains(mmfile_t *mf, regex_t *regexp, kwset_t kws,

static int has_changes(mmfile_t *one, mmfile_t *two,
struct diff_options *o UNUSED,
regex_t *regexp, kwset_t kws)
regex_t *regexp, kwset_t kws,
const struct range_set *ranges UNUSED)
{
/*
* -S counts needle occurrences in each whole blob. Limiting this to
* an -L range is left as a follow-up; for now -S ignores the range.
*/
unsigned int c1 = one ? contains(one, regexp, kws, 0) : 0;
unsigned int c2 = two ? contains(two, regexp, kws, c1 + 1) : 0;
return c1 != c2;
@ -132,6 +143,7 @@ static int pickaxe_match(struct diff_filepair *p, struct diff_options *o,
struct userdiff_driver *textconv_one = NULL;
struct userdiff_driver *textconv_two = NULL;
mmfile_t mf1, mf2;
const struct range_set *ranges;
int ret;

/* ignore unmerged */
@ -169,7 +181,13 @@ static int pickaxe_match(struct diff_filepair *p, struct diff_options *o,
mf1.size = fill_textconv(o->repo, textconv_one, p->one, &mf1.ptr);
mf2.size = fill_textconv(o->repo, textconv_two, p->two, &mf2.ptr);

ret = fn(&mf1, &mf2, o, regexp, kws);
/*
* -L limits the search to the tracked range, but the range is in
* pre-textconv line coordinates that do not map onto textconv
* output, so search the whole file when textconv is enabled.
*/
ranges = (textconv_one || textconv_two) ? NULL : p->line_ranges;
ret = fn(&mf1, &mf2, o, regexp, kws, ranges);

if (textconv_one)
free(mf1.ptr);

View File

@ -3249,8 +3249,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
(revs->diffopt.output_format &
~(DIFF_FORMAT_PATCH | DIFF_FORMAT_NO_OUTPUT |
DIFF_FORMAT_RAW | DIFF_FORMAT_NAME |
DIFF_FORMAT_NAME_STATUS | DIFF_FORMAT_SUMMARY))))
die(_("-L does not yet support the requested diff format"));
DIFF_FORMAT_NAME_STATUS | DIFF_FORMAT_SUMMARY |
DIFF_FORMAT_NUMSTAT | DIFF_FORMAT_DIFFSTAT |
DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_CHECKDIFF))))
die(_("-L does not support the requested diff format"));

if (revs->expand_tabs_in_log < 0)
revs->expand_tabs_in_log = revs->expand_tabs_in_log_default;

View File

@ -176,24 +176,9 @@ test_expect_success '--name-status shows status and path' '
test_grep ! "^@@" actual
'

test_expect_success '--stat is not yet supported with -L' '
test_must_fail git log -L1,24:b.c --stat 2>err &&
test_grep "does not yet support" err
'

test_expect_success '--numstat is not yet supported with -L' '
test_must_fail git log -L1,24:b.c --numstat 2>err &&
test_grep "does not yet support" err
'

test_expect_success '--shortstat is not yet supported with -L' '
test_must_fail git log -L1,24:b.c --shortstat 2>err &&
test_grep "does not yet support" err
'

test_expect_success '--dirstat is not yet supported with -L' '
test_expect_success '--dirstat is not supported with -L' '
test_must_fail git log -L1,24:b.c --dirstat 2>err &&
test_grep "does not yet support" err
test_grep "does not support" err
'

test_expect_success 'setup for checking fancy rename following' '
@ -718,24 +703,49 @@ test_expect_success '-L suppresses deletions outside tracked range' '
test $(grep -c "^diff --git" actual) = 1
'

test_expect_success '-L with -S filters to string-count changes' '
test_expect_success '-L with -S selects only the matching commit' '
git checkout parent-oids &&
git log -L:func2:file.c -S "F2 + 2" --format= >actual &&
# -S searches the whole file, not just the tracked range;
# combined with the -L range walk, this selects commits that
# both touch func2 and change the count of "F2 + 2" in the file.
test $(grep -c "^diff --git" actual) = 1 &&
test_grep "F2 + 2" actual
git log -L:func2:file.c -S "F2 + 2" --format=%s --no-patch >actual &&
echo "Modify func2() in file.c" >expect &&
test_cmp expect actual
'

test_expect_success '-L with -G filters to diff-text matches' '
test_expect_success '-L with -G selects only the matching commit' '
git checkout parent-oids &&
git log -L:func2:file.c -G "F2 [+] 2" --format= >actual &&
# -G greps the whole-file diff text, not just the tracked range;
# combined with -L, this selects commits that both touch func2
# and have "F2 + 2" in their diff.
test $(grep -c "^diff --git" actual) = 1 &&
test_grep "F2 + 2" actual
git log -L:func2:file.c -G "F2 [+] 2" --format=%s --no-patch >actual &&
echo "Modify func2() in file.c" >expect &&
test_cmp expect actual
'

test_expect_success 'setup for trailing deletion test' '
git checkout --orphan trailing-del &&
git reset --hard &&
cat >file.c <<-\EOF &&
void tracked()
{
return 1;
}
// trailing comment outside tracked range
EOF
git add file.c &&
test_tick &&
git commit -m "add file with trailing comment" &&
# Remove the trailing comment AND modify tracked() so there
# is a modification to the line range we track and a
# modification to the following line, which we do not track.
cat >file.c <<-\EOF &&
void tracked()
{
return 2;
}
EOF
git commit -a -m "modify tracked and delete trailing comment"
'

test_expect_success '-L does not include deletions past end of tracked range' '
git log -L:tracked:file.c --format= -1 -p >actual &&
test_grep "return 2" actual &&
test_grep ! "trailing comment" actual
'

test_expect_success '-L with --diff-filter=M excludes root commit' '
@ -762,9 +772,9 @@ test_expect_success '-L with -S suppresses non-matching commits' '
test_cmp expect actual
'

test_expect_success '--full-diff is not yet supported with -L' '
test_expect_success '--full-diff is not supported with -L' '
test_must_fail git log -L1,24:b.c --full-diff 2>err &&
test_grep "does not yet support" err
test_grep "does not support" err
'

test_expect_success '-L --oneline has no extra blank line before diff' '
@ -775,6 +785,113 @@ test_expect_success '-L --oneline has no extra blank line before diff' '
test_grep "^diff --git" line2
'

test_expect_success 'setup for -L stat tests' '
git checkout --orphan stat-range &&
git reset --hard &&
cat >file.c <<-\EOF &&
int func1()
{
return F1;
}

int tracked_fn()
{
return F2;
}
EOF
git add file.c &&
test_tick &&
git commit -m "Add func1() and tracked_fn()" &&

# Modify both functions so whole-file stats (2 added, 2 deleted)
# differ from the tracked range of tracked_fn (1 and 1).
sed -e "s/F1/F1 + 1/" -e "s/F2/F2 + 2/" file.c >tmp &&
mv tmp file.c &&
git commit -a -m "Modify both functions"
'

test_expect_success '-L --numstat limits counts to the tracked range' '
git log -L:tracked_fn:file.c --numstat --format=%s >actual &&
cat >expect <<-\EOF &&
Modify both functions

1 1 file.c
Add func1() and tracked_fn()

4 0 file.c
EOF
test_cmp expect actual
'

test_expect_success '-L --stat and --shortstat limit counts to the tracked range' '
git log -L:tracked_fn:file.c --stat --format=%s -1 >actual &&
cat >expect <<-\EOF &&
Modify both functions

file.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
EOF
test_cmp expect actual &&

git log -L:tracked_fn:file.c --shortstat --format=%s -1 >actual &&
cat >expect <<-\EOF &&
Modify both functions

1 file changed, 1 insertion(+), 1 deletion(-)
EOF
test_cmp expect actual
'

test_expect_success '--numstat across renames and multiple commits' '
# parallel-change carries the tracked function f across an a.c -> b.c
# rename and a merge of two parallel histories.
git checkout parallel-change &&
git log -M -L ":f:b.c" --format= --numstat >actual &&
cat >expect <<-\EOF &&
1 1 b.c
1 1 a.c
1 1 a.c
1 1 a.c
1 0 a.c
13 0 a.c
EOF
test_cmp expect actual
'

test_expect_success '-L multiple ranges with --numstat excludes untracked change' '
git checkout --orphan multi-range &&
git reset --hard &&
cat >m.c <<-\EOF &&
int tracked_func1()
{
return F1;
}

int tracked_func2()
{
return F2;
}

int func3()
{
return F3;
}
EOF
git add m.c &&
test_tick &&
git commit -m "add m.c" &&
sed -e "s/F1/F1 + 1/" -e "s/F2/F2 + 2/" -e "s/F3/F3 + 3/" m.c >tmp &&
mv tmp m.c &&
git commit -a -m "Modify all three functions" &&
git log -L:tracked_func1:m.c -L:tracked_func2:m.c --numstat --format=%s -1 >actual &&
cat >expect <<-\EOF &&
Modify all three functions

2 2 m.c
EOF
test_cmp expect actual
'

test_expect_success '--summary shows new file on root commit' '
git checkout parent-oids &&
git log -L:func2:file.c --summary --format= >actual &&
@ -801,4 +918,146 @@ test_expect_success 'get_commit_action() does not mutate a not-yet-walked commit
)
'

test_expect_success 'setup for --check test' '
git checkout --orphan check-test &&
git reset --hard &&
cat >check.c <<-\EOF &&
void tracked()
{
return;
}

void other()
{
return;
}
EOF
git add check.c &&
test_tick &&
git commit -m "add check.c" &&
sed "s/return;/return; /" check.c >check.c.tmp &&
mv check.c.tmp check.c &&
git commit -a -m "introduce trailing whitespace"
'

test_expect_success '--check is limited to tracked ranges and reports real file line numbers' '
test_must_fail git log -L:tracked:check.c --check --format= >raw &&
grep -E ":[0-9]+:" raw >actual &&
echo "check.c:3: trailing whitespace." >expect &&
test_cmp expect actual &&

test_must_fail git log -L:tracked:check.c -L:other:check.c \
--check --format= >raw &&
grep -E ":[0-9]+:" raw >actual &&
cat >expect <<-\EOF &&
check.c:3: trailing whitespace.
check.c:8: trailing whitespace.
EOF
test_cmp expect actual
'

test_expect_success '--check reports each error at its real line across a gap in one range' '
git checkout --orphan check-gap &&
git reset --hard &&
cat >gap.c <<-\EOF &&
void tracked()
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
int g = 7;
return;
}
EOF
git add gap.c &&
test_tick &&
git commit -m "add gap.c" &&
sed -e "s/int a = 1;/int a = 1; /" -e "s/int g = 7;/int g = 7; /" gap.c >tmp &&
mv tmp gap.c &&
git commit -a -m "ws errors with a gap" &&
test_must_fail git log -L:tracked:gap.c --check --format= >raw &&
grep -E ":[0-9]+:" raw >actual &&
cat >expect <<-\EOF &&
gap.c:3: trailing whitespace.
gap.c:8: trailing whitespace.
EOF
test_cmp expect actual
'

test_expect_success '--check does not report blank-at-eof outside the range' '
git checkout --orphan check-eof &&
git reset --hard &&
printf "void tracked()\n{\n return;\n}\n\nint tail = 1;\n" >eof.c &&
git add eof.c &&
test_tick &&
git commit -m "add eof.c" &&
printf "void tracked()\n{\n return; \n}\n\nint tail = 1;\n\n" >eof.c &&
git commit -a -m "ws in range, blank at eof out of range" &&
test_must_fail git log -L:tracked:eof.c --check --format= >raw &&
grep -E ":[0-9]+:" raw >actual &&
echo "eof.c:3: trailing whitespace." >expect &&
test_cmp expect actual
'

test_expect_success '-L -G is limited to the tracked range' '
git checkout --orphan grep-range &&
git reset --hard &&
cat >gp.c <<-\EOF &&
int func1()
{
return ALPHA;
}

int func2()
{
return BETA;
}
EOF
git add gp.c &&
test_tick &&
git commit -m "add gp.c" &&
sed -e "s/ALPHA/ALPHA2/" -e "s/BETA/BETA2/" gp.c >tmp &&
mv tmp gp.c &&
git commit -a -m "touch both functions" &&
git log -L:func2:gp.c -G BETA --format=%s --no-patch >actual &&
cat >expect <<-\EOF &&
touch both functions
add gp.c
EOF
test_cmp expect actual &&
git log -L:func2:gp.c -G ALPHA --format=%s --no-patch >actual &&
test_must_be_empty actual
'

test_expect_success '-L -G searches the whole file under textconv' '
git checkout --orphan grep-textconv &&
git reset --hard &&
cat >tc.c <<-\EOF &&
int func1()
{
return F1;
}

int func2()
{
return F2;
}
EOF
git add tc.c &&
test_tick &&
git commit -m "add tc.c" &&
sed -e "s/F1/F1 + 1/" -e "s/return F2/return FINDME/" tc.c >tmp &&
mv tmp tc.c &&
git commit -a -m "change both funcs" &&
echo "tc.c diff=tc" >.gitattributes &&
git log -L:func1:tc.c -G FINDME --format=%s --no-patch >actual &&
test_must_be_empty actual &&
git config diff.tc.textconv cat &&
git log -L:func1:tc.c -G FINDME --format=%s --no-patch >actual &&
echo "change both funcs" >expect &&
test_cmp expect actual
'

test_done

View File

@ -8,7 +8,7 @@ diff --git a/b.c b/b.c
index bf79c2f..27c829c 100644
--- a/b.c
+++ b/b.c
@@ -25,0 +18,9 @@
@@ -24,0 +18,9 @@
+long f(long x)
+{
+ int s = 0;

View File

@ -8,7 +8,7 @@ diff --git a/a.c b/a.c
index 0b9cae5..5de3ea4 100644
--- a/a.c
+++ b/a.c
@@ -23,0 +24,1 @@ int main ()
@@ -22,0 +24 @@ int main ()
+/* incomplete lines are bad! */

commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
@ -21,7 +21,7 @@ diff --git a/a.c b/a.c
index 5e709a1..0b9cae5 100644
--- a/a.c
+++ b/a.c
@@ -22,1 +22,1 @@ int main ()
@@ -22 +22 @@ int main ()
-}
+}
\ No newline at end of file
@ -37,5 +37,5 @@ new file mode 100644
index 0000000..444e415
--- /dev/null
+++ b/a.c
@@ -0,0 +20,1 @@
@@ -0,0 +20 @@
+}

View File

@ -8,7 +8,7 @@ diff --git a/b.c b/b.c
index 69cb69c..a0d566e 100644
--- a/b.c
+++ b/b.c
@@ -25,0 +18,9 @@
@@ -24,0 +18,9 @@
+long f(long x)
+{
+ int s = 0;

View File

@ -8,7 +8,7 @@ diff --git a/a.c b/a.c
index e4fa1d8..62c1fc2 100644
--- a/a.c
+++ b/a.c
@@ -23,0 +24,1 @@ int main ()
@@ -22,0 +24 @@ int main ()
+/* incomplete lines are bad! */

commit 29f32ac3141c48b22803e5c4127b719917b67d0f8ca8c5248bebfa2a19f7da10
@ -21,7 +21,7 @@ diff --git a/a.c b/a.c
index d325124..e4fa1d8 100644
--- a/a.c
+++ b/a.c
@@ -22,1 +22,1 @@ int main ()
@@ -22 +22 @@ int main ()
-}
+}
\ No newline at end of file
@ -37,5 +37,5 @@ new file mode 100644
index 0000000..9f550c3
--- /dev/null
+++ b/a.c
@@ -0,0 +20,1 @@
@@ -0,0 +20 @@
+}

View File

@ -91,6 +91,25 @@ static int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf)
return 0;
}

static int strbuf_out_line(void *priv, mmbuffer_t *mb, int nbuf)
{
struct strbuf *out = priv;
int i;
for (i = 0; i < nbuf; i++)
strbuf_add(out, mb[i].ptr, mb[i].size);
return 0;
}

void xdiff_emit_hunk_header(struct strbuf *out,
long old_begin, long old_count,
long new_begin, long new_count,
const char *func, long funclen)
{
xdemitcb_t ecb = { .priv = out, .out_line = strbuf_out_line };
xdl_emit_hunk_hdr(old_begin, old_count, new_begin, new_count,
func, funclen, &ecb);
}

/*
* Trim down common substring at the end of the buffers,
* but end on a complete line.

View File

@ -46,6 +46,16 @@ int xdi_diff_outf(mmfile_t *mf1, mmfile_t *mf2,
xdiff_emit_line_fn line_fn,
void *consume_callback_data,
xpparam_t const *xpp, xdemitconf_t const *xecfg);

struct range_set;
/*
* Like xdi_diff_outf(), but forwards only the lines within the given
* postimage line ranges to line_fn.
*/
int diff_emit_line_ranges(mmfile_t *mf1, mmfile_t *mf2,
const struct range_set *ranges,
xdiff_emit_line_fn line_fn, void *cb_data,
xpparam_t *xpp, xdemitconf_t *xecfg);
int read_mmfile(mmfile_t *ptr, const char *filename);
void read_mmblob(mmfile_t *ptr, struct object_database *odb,
const struct object_id *oid);
@ -76,4 +86,17 @@ int xdiff_compare_lines(const char *l1, long s1,
*/
unsigned long xdiff_hash_string(const char *s, size_t len, long flags);

struct strbuf;

/*
* Append a unified-diff hunk header to `out`, e.g.
* "@@ -<old> +<new> @@ func\n". The header comes from wrapping xdiff's
* own hunk-header emitter, so it matches what a normal diff would
* produce for the given line number begins and line counts.
*/
void xdiff_emit_hunk_header(struct strbuf *out,
long old_begin, long old_nr,
long new_begin, long new_nr,
const char *func, long funclen);

#endif