diff: support --check with -L line ranges

builtin_checkdiff() runs its own xdiff pass to detect whitespace errors in newly added lines. When -L is active, the check should be scoped to the tracked line ranges rather than the whole file. Reuse the line_range_filter to wrap checkdiff_consume(), the same pattern already used for patch output and diffstat. The filter forwards only in-range lines for whitespace checking. checkdiff reports the file line number of each error, which it normally learns from the hunk header via checkdiff_consume_hunk(). The filter synthesizes its own hunk headers, so give it an optional hunk callback and route checkdiff_consume_hunk() through it; this sets the post-image position before the in-range lines are replayed. Without it the reported line numbers would count from the start of the range hunk rather than the start of the file. The trailing blank-at-eof check is a second pass that scans the whole file via check_blank_at_eof(), so gate its report on the tracked ranges as well; otherwise a blank line added at end of file is reported even when it lies outside the range. Add DIFF_FORMAT_CHECKDIFF to the -L output format allowlist in setup_revisions() so that -L --check is accepted, and list --check among the supported formats in the documentation. Add tests covering that whitespace errors are reported, scoped to the tracked range, and labeled with the correct file line number, including when two errors in one range are separated by a gap that would otherwise split into multiple xdiff hunks. Signed-off-by: Michael Montalbo <mmontalbo@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael Montalbo committed Jun 27, 2026 at 17:29 UTC 54438a56a4edb4c70459180f37af3e43e5d7e6a3
4 files changed +156 -5
Documentation/line-range-options.adoc
+1 -1
@@ -10,7 +10,7 @@
10 You can specify this option more than once. Implies `--patch`.
11 Patch output can be suppressed using `--no-patch`.
12 The following non-patch diff formats are supported: `--raw`,
13 - `--name-only`, `--name-status`, `--summary`,
13 + `--name-only`, `--name-status`, `--summary`, `--check`,
14 `--stat`, `--numstat`, and `--shortstat`.
15 The stat formats count only lines within the tracked range.
16 `--dirstat` is not supported
diff.c
+62 -3
@@ -665,6 +665,12 @@ struct emit_callback {
665 */
666 struct line_range_filter {
667 xdiff_emit_line_fn orig_line_fn;
668 + /*
669 + * Optional; consumers that report file line numbers (e.g.
670 + * checkdiff) need the synthetic hunk header to set their
671 + * post-image position before in-range lines are replayed.
672 + */
673 + xdiff_emit_hunk_fn orig_hunk_fn;
674 void *orig_cb_data;
675 const struct range_set *ranges; /* 0-based [start, end) */
676 unsigned int cur_range; /* index into the range_set */
@@ -2652,6 +2658,17 @@ static void flush_range_hunk(struct line_range_filter *filter)
2658 filter->hunk.new_begin, new_count,
2659 filter->func, filter->funclen);
2660
2661 + /*
2662 + * Inform a line-numbering consumer of the post-image position
2663 + * before replaying lines, mirroring the hunk callback xdiff
2664 + * would have issued for a non-scoped diff.
2665 + */
2666 + if (filter->orig_hunk_fn)
2667 + filter->orig_hunk_fn(filter->orig_cb_data,
2668 + filter->hunk.old_begin, old_count,
2669 + filter->hunk.new_begin, new_count,
2670 + filter->func, filter->funclen);
2671 +
2672 filter->ret = filter->orig_line_fn(filter->orig_cb_data, hdr.buf, hdr.len);
2673 strbuf_release(&hdr);
2674
@@ -4330,11 +4347,29 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
4347 diff_free_filespec_data(two);
4348 }
4349
4350 +/*
4351 + * Is the 0-based line index within any of the tracked ranges?
4352 + * (range_set ranges are 0-based, half-open [start, end).) This is a
4353 + * one-shot query for a single line and scans; the streaming filter
4354 + * (line_range_line_fn) uses a forward cursor instead.
4355 + */
4356 +static int idx_in_ranges(const struct range_set *ranges, long idx)
4357 +{
4358 + unsigned int i;
4359 +
4360 + for (i = 0; i < ranges->nr; i++)
4361 + if (idx >= ranges->ranges[i].start &&
4362 + idx < ranges->ranges[i].end)
4363 + return 1;
4364 + return 0;
4365 +}
4366 +
4367 static void builtin_checkdiff(const char *name_a, const char *name_b,
4368 const char *attr_path,
4369 struct diff_filespec *one,
4370 struct diff_filespec *two,
4337 - struct diff_options *o)
4371 + struct diff_options *o,
4372 + const struct range_set *line_ranges)
4373 {
4374 mmfile_t mf1, mf2;
4375 struct checkdiff_t data;
@@ -4374,7 +4409,19 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
4409 memset(&xecfg, 0, sizeof(xecfg));
4410 xecfg.ctxlen = 1; /* at least one context line */
4411 xpp.flags = 0;
4377 - if (xdi_diff_outf(&mf1, &mf2, checkdiff_consume_hunk,
4412 +
4413 + if (line_ranges) {
4414 + struct line_range_filter lr_filter;
4415 +
4416 + line_range_filter_init(&lr_filter, line_ranges,
4417 + checkdiff_consume, &data);
4418 + lr_filter.orig_hunk_fn = checkdiff_consume_hunk;
4419 +
4420 + if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
4421 + &xpp, &xecfg))
4422 + die("unable to generate checkdiff for %s",
4423 + one->path);
4424 + } else if (xdi_diff_outf(&mf1, &mf2, checkdiff_consume_hunk,
4425 checkdiff_consume, &data,
4426 &xpp, &xecfg))
4427 die("unable to generate checkdiff for %s", one->path);
@@ -4387,6 +4434,17 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
4434 check_blank_at_eof(&mf1, &mf2, &ecbdata);
4435 blank_at_eof = ecbdata.blank_at_eof_in_postimage;
4436
4437 + /*
4438 + * check_blank_at_eof() scans the whole file; with -L,
4439 + * keep the report only when its line is in a tracked
4440 + * range. The error's location is the first trailing
4441 + * blank line (blank_at_eof, 1-based; ranges 0-based), so
4442 + * we scope by that line.
4443 + */
4444 + if (blank_at_eof && line_ranges &&
4445 + !idx_in_ranges(line_ranges, blank_at_eof - 1))
4446 + blank_at_eof = 0;
4447 +
4448 if (blank_at_eof) {
4449 static char *err;
4450 if (!err)
@@ -5179,7 +5237,8 @@ static void run_checkdiff(struct diff_filepair *p, struct diff_options *o)
5237 diff_fill_oid_info(p->one, o->repo->index);
5238 diff_fill_oid_info(p->two, o->repo->index);
5239
5182 - builtin_checkdiff(name, other, attr_path, p->one, p->two, o);
5240 + builtin_checkdiff(name, other, attr_path, p->one, p->two, o,
5241 + p->line_ranges);
5242 }
5243
5244 void repo_diff_setup(struct repository *r, struct diff_options *options)
revision.c
+1 -1
@@ -3195,7 +3195,7 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
3195 DIFF_FORMAT_RAW | DIFF_FORMAT_NAME |
3196 DIFF_FORMAT_NAME_STATUS | DIFF_FORMAT_SUMMARY |
3197 DIFF_FORMAT_NUMSTAT | DIFF_FORMAT_DIFFSTAT |
3198 - DIFF_FORMAT_SHORTSTAT))))
3198 + DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_CHECKDIFF))))
3199 die(_("-L does not support the requested diff format"));
3200
3201 if (revs->expand_tabs_in_log < 0)
t/t4211-line-log.sh
+92
@@ -1018,4 +1018,96 @@ test_expect_success '--summary shows new file on root commit' '
1018 test_grep "create mode 100644 file.c" actual
1019 '
1020
1021 +test_expect_success 'setup for --check test' '
1022 + git checkout --orphan check-test &&
1023 + git reset --hard &&
1024 + cat >check.c <<-\EOF &&
1025 + void tracked()
1026 + {
1027 + return;
1028 + }
1029 +
1030 + void other()
1031 + {
1032 + return;
1033 + }
1034 + EOF
1035 + git add check.c &&
1036 + test_tick &&
1037 + git commit -m "add check.c" &&
1038 + # Introduce trailing whitespace errors in both functions
1039 + sed "s/return;/return; /" check.c >check.c.tmp &&
1040 + mv check.c.tmp check.c &&
1041 + git commit -a -m "introduce trailing whitespace"
1042 +'
1043 +
1044 +test_expect_success '--check scoped to tracked range with correct file line' '
1045 + # tracked() trailing whitespace is at check.c:3; report it with the
1046 + # real file line number, not a count from the start of the range
1047 + # hunk. other() at check.c:8 is outside the range and is excluded.
1048 + test_must_fail git log -L:tracked:check.c --check --format= >actual &&
1049 + test_grep "check.c:3: trailing whitespace" actual &&
1050 + test_grep ! "check.c:8:" actual
1051 +'
1052 +
1053 +test_expect_success '--check reports each of several tracked ranges' '
1054 + # Track both functions as separate ranges. Each range is flushed
1055 + # as its own hunk, so the second error must report its real file
1056 + # line (check.c:8), not continue the numbering from the first
1057 + # range (check.c:3).
1058 + test_must_fail git log -L:tracked:check.c -L:other:check.c \
1059 + --check --format= >actual &&
1060 + test_grep "check.c:3: trailing whitespace" actual &&
1061 + test_grep "check.c:8: trailing whitespace" actual
1062 +'
1063 +
1064 +test_expect_success '--check line numbers stay correct across a gap in one range' '
1065 + git checkout --orphan check-gap &&
1066 + git reset --hard &&
1067 + cat >gap.c <<-\EOF &&
1068 + void tracked()
1069 + {
1070 + int a = 1;
1071 + int b = 2;
1072 + int c = 3;
1073 + int d = 4;
1074 + int e = 5;
1075 + int g = 7;
1076 + return;
1077 + }
1078 + EOF
1079 + git add gap.c &&
1080 + test_tick &&
1081 + git commit -m "add gap.c" &&
1082 + # Two trailing-whitespace errors within one tracked range,
1083 + # separated by clean lines. ctxlen is inflated to the range span,
1084 + # so they land in a single xdiff hunk with the gap as context;
1085 + # both must report their real file line number, with the context
1086 + # lines between them counted.
1087 + sed -e "s/int a = 1;/int a = 1; /" -e "s/int g = 7;/int g = 7; /" gap.c >tmp &&
1088 + mv tmp gap.c &&
1089 + git commit -a -m "ws errors with a gap" &&
1090 + test_must_fail git log -L:tracked:gap.c --check --format= >actual &&
1091 + test_grep "gap.c:3: trailing whitespace" actual &&
1092 + test_grep "gap.c:8: trailing whitespace" actual
1093 +'
1094 +
1095 +test_expect_success '--check does not report blank-at-eof outside the range' '
1096 + git checkout --orphan check-eof &&
1097 + git reset --hard &&
1098 + printf "void tracked()\n{\n return;\n}\n\nint tail = 1;\n" >eof.c &&
1099 + git add eof.c &&
1100 + test_tick &&
1101 + git commit -m "add eof.c" &&
1102 + # One commit introduces a trailing-whitespace error inside tracked()
1103 + # (line 3) and a blank line at end of file (line 7, outside the
1104 + # range). The blank-at-eof check scans the whole file, so it must be
1105 + # scoped: report the in-range error, not the out-of-range EOF blank.
1106 + printf "void tracked()\n{\n return; \n}\n\nint tail = 1;\n\n" >eof.c &&
1107 + git commit -a -m "ws in range, blank at eof out of range" &&
1108 + test_must_fail git log -L:tracked:eof.c --check --format= >actual &&
1109 + test_grep "eof.c:3: trailing whitespace" actual &&
1110 + test_grep ! "blank line at EOF" actual
1111 +'
1112 +
1113 test_done