diffcore-pickaxe: scope -G to the -L tracked range

git log -L scopes its diff output to the tracked range, but pickaxe (-S, -G) still runs in diffcore over the whole-file change, so -L -G selects a commit whenever the pattern appears in any added or removed line of the file, even outside the tracked range. Teach -G to honor the range. diff_grep() already runs an xdiff pass and greps the +/- lines; route that pass through the line-range filter so only the tracked range's lines are grepped. Expose the filter as diff_emit_line_ranges(), an xdi_diff_outf() that emits only the tracked range's lines, thread the filepair's line_ranges through the pickaxe callback, and pass it from pickaxe_match(). Skip scoping under textconv, whose output is not in the original file's line coordinates. -G needs only a hit/no-hit answer, so the line-number concerns the filter handles for patch and check output do not apply here. -S is left matching the whole file: it counts needle occurrences per blob rather than grepping the diff, so scoping it needs a different approach, left to a follow-up. has_changes() takes the range parameter but ignores it for now. Document the resulting -L pickaxe scoping: -G is scoped to the tracked range, while -S still matches the whole file. 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 f67c51df064d2b64b257bd8c17d757cc0ce1b7fc
5 files changed +130 -12
Documentation/line-range-options.adoc
+4 -1
@@ -20,6 +20,9 @@
20 +
21 Patch formatting options such as `--word-diff`, `--color-moved`,
22 `--no-prefix`, and whitespace options (`-w`, `-b`) are supported,
23 -as are pickaxe options (`-S`, `-G`) and `--diff-filter`.
23 +as are pickaxe options (`-S`, `-G`) and `--diff-filter`. `-G` is
24 +scoped to the tracked range; `-S` is still evaluated over the whole
25 +file, so an `-S` query may select a commit for a change outside the
26 +range.
27 +
28 include::line-range-format.adoc[]
diff.c
+15
@@ -2817,6 +2817,21 @@ static int line_range_filter_diff(struct line_range_filter *filter,
2817 return ret;
2818 }
2819
2820 +/*
2821 + * Expose the in-file line-range filter to callers outside diff.c (e.g.
2822 + * pickaxe -G); see xdiff-interface.h for the contract.
2823 + */
2824 +int diff_emit_line_ranges(mmfile_t *one, mmfile_t *two,
2825 + const struct range_set *ranges,
2826 + xdiff_emit_line_fn line_fn, void *cb_data,
2827 + xpparam_t *xpp, xdemitconf_t *xecfg)
2828 +{
2829 + struct line_range_filter filter;
2830 +
2831 + line_range_filter_init(&filter, ranges, line_fn, cb_data);
2832 + return line_range_filter_diff(&filter, one, two, xpp, xecfg);
2833 +}
2834 +
2835 static void pprint_rename(struct strbuf *name, const char *a, const char *b)
2836 {
2837 const char *old_name = a;
diffcore-pickaxe.c
+29 -8
@@ -16,7 +16,8 @@
16
17 typedef int (*pickaxe_fn)(mmfile_t *one, mmfile_t *two,
18 struct diff_options *o,
19 - regex_t *regexp, kwset_t kws);
19 + regex_t *regexp, kwset_t kws,
20 + const struct range_set *ranges);
21
22 struct diffgrep_cb {
23 regex_t *regexp;
@@ -42,7 +43,8 @@ static int diffgrep_consume(void *priv, char *line, unsigned long len)
43
44 static int diff_grep(mmfile_t *one, mmfile_t *two,
45 struct diff_options *o,
45 - regex_t *regexp, kwset_t kws UNUSED)
46 + regex_t *regexp, kwset_t kws UNUSED,
47 + const struct range_set *ranges)
48 {
49 struct diffgrep_cb ecbdata;
50 xpparam_t xpp;
@@ -50,8 +52,11 @@ static int diff_grep(mmfile_t *one, mmfile_t *two,
52 int ret;
53
54 /*
53 - * We have both sides; need to run textual diff and see if
54 - * the pattern appears on added/deleted lines.
55 + * We have both sides; need to run textual diff and see if the
56 + * pattern appears on added/deleted lines. Under -L (ranges set),
57 + * forward only the tracked range's lines so the match is scoped.
58 + * -G needs only a hit/no-hit answer, so the line-number bookkeeping
59 + * the filter does for -L patch and check output is irrelevant here.
60 */
61 memset(&xpp, 0, sizeof(xpp));
62 memset(&xecfg, 0, sizeof(xecfg));
@@ -65,8 +70,12 @@ static int diff_grep(mmfile_t *one, mmfile_t *two,
70 * An xdiff error might be our "data->hit" from above. See the
71 * comment for xdiff_emit_line_fn in xdiff-interface.h
72 */
68 - ret = xdi_diff_outf(one, two, NULL, diffgrep_consume,
69 - &ecbdata, &xpp, &xecfg);
73 + if (ranges)
74 + ret = diff_emit_line_ranges(one, two, ranges, diffgrep_consume,
75 + &ecbdata, &xpp, &xecfg);
76 + else
77 + ret = xdi_diff_outf(one, two, NULL, diffgrep_consume,
78 + &ecbdata, &xpp, &xecfg);
79 if (ecbdata.hit)
80 return 1;
81 if (ret)
@@ -119,8 +128,13 @@ static unsigned int contains(mmfile_t *mf, regex_t *regexp, kwset_t kws,
128
129 static int has_changes(mmfile_t *one, mmfile_t *two,
130 struct diff_options *o UNUSED,
122 - regex_t *regexp, kwset_t kws)
131 + regex_t *regexp, kwset_t kws,
132 + const struct range_set *ranges UNUSED)
133 {
134 + /*
135 + * -S counts needle occurrences in each whole blob. Scoping this to
136 + * a -L range is left to a follow-up; for now -S ignores the range.
137 + */
138 unsigned int c1 = one ? contains(one, regexp, kws, 0) : 0;
139 unsigned int c2 = two ? contains(two, regexp, kws, c1 + 1) : 0;
140 return c1 != c2;
@@ -132,6 +146,7 @@ static int pickaxe_match(struct diff_filepair *p, struct diff_options *o,
146 struct userdiff_driver *textconv_one = NULL;
147 struct userdiff_driver *textconv_two = NULL;
148 mmfile_t mf1, mf2;
149 + const struct range_set *ranges;
150 int ret;
151
152 /* ignore unmerged */
@@ -169,7 +184,13 @@ static int pickaxe_match(struct diff_filepair *p, struct diff_options *o,
184 mf1.size = fill_textconv(o->repo, textconv_one, p->one, &mf1.ptr);
185 mf2.size = fill_textconv(o->repo, textconv_two, p->two, &mf2.ptr);
186
172 - ret = fn(&mf1, &mf2, o, regexp, kws);
187 + /*
188 + * -L scopes the search to the tracked range, but the range is in
189 + * original-file line coordinates that do not map onto textconv
190 + * output, so search the whole file when textconv is in play.
191 + */
192 + ranges = (textconv_one || textconv_two) ? NULL : p->line_ranges;
193 + ret = fn(&mf1, &mf2, o, regexp, kws, ranges);
194
195 if (textconv_one)
196 free(mf1.ptr);
t/t4211-line-log.sh
+69 -3
@@ -722,9 +722,9 @@ test_expect_success '-L with -S filters to string-count changes' '
722 test_expect_success '-L with -G filters to diff-text matches' '
723 git checkout parent-oids &&
724 git log -L:func2:file.c -G "F2 [+] 2" --format= >actual &&
725 - # -G greps the whole-file diff text, not just the tracked range;
726 - # combined with -L, this selects commits that both touch func2
727 - # and have "F2 + 2" in their diff.
725 + # -G greps the diff text, and under -L only the lines in the
726 + # tracked range (unlike -S above, which searches the whole file);
727 + # this selects commits whose change to func2 contains "F2 + 2".
728 test $(grep -c "^diff --git" actual) = 1 &&
729 grep "F2 + 2" actual
730 '
@@ -1110,4 +1110,70 @@ test_expect_success '--check does not report blank-at-eof outside the range' '
1110 test_grep ! "blank line at EOF" actual
1111 '
1112
1113 +test_expect_success '-L -G is scoped to the tracked range' '
1114 + git checkout --orphan grep-scope &&
1115 + git reset --hard &&
1116 + cat >gp.c <<-\EOF &&
1117 + int func1()
1118 + {
1119 + return ALPHA;
1120 + }
1121 +
1122 + int func2()
1123 + {
1124 + return BETA;
1125 + }
1126 + EOF
1127 + git add gp.c &&
1128 + test_tick &&
1129 + git commit -m "add gp.c" &&
1130 + sed -e "s/ALPHA/ALPHA2/" -e "s/BETA/BETA2/" gp.c >tmp &&
1131 + mv tmp gp.c &&
1132 + git commit -a -m "touch both functions" &&
1133 + # The commit changes ALPHA (func1) and BETA (func2). Tracking func2,
1134 + # -G BETA matches its in-range change; -G ALPHA must not, since ALPHA
1135 + # changes only outside the tracked range.
1136 + git log -L:func2:gp.c -G BETA --format=%s >actual &&
1137 + test_grep "touch both functions" actual &&
1138 + git log -L:func2:gp.c -G ALPHA --format=%s >actual &&
1139 + test_grep ! "touch both functions" actual
1140 +'
1141 +
1142 +test_expect_success '-L -G searches the whole file under textconv' '
1143 + git checkout --orphan grep-textconv &&
1144 + git reset --hard &&
1145 + cat >tc.c <<-\EOF &&
1146 + int func1()
1147 + {
1148 + return F1;
1149 + }
1150 +
1151 + int func2()
1152 + {
1153 + return F2;
1154 + }
1155 + EOF
1156 + git add tc.c &&
1157 + test_tick &&
1158 + git commit -m "add tc.c" &&
1159 + # One commit changes func1 and func2; MAGIC lands only in the
1160 + # func2 change, outside func1.
1161 + sed -e "s/F1/F1 + 1/" -e "s/return F2/return MAGIC/" tc.c >tmp &&
1162 + mv tmp tc.c &&
1163 + git commit -a -m "change both funcs" &&
1164 + echo "tc.c diff=tc" >.gitattributes &&
1165 +
1166 + # Without a textconv driver, -G is scoped to func1, so MAGIC (only
1167 + # in the func2 change) does not select the commit.
1168 + git log -L:func1:tc.c -G MAGIC --format=%s --no-patch >actual &&
1169 + test_must_be_empty actual &&
1170 +
1171 + # A textconv driver makes the range (original-file line numbers)
1172 + # meaningless against the driver output, so -G falls back to the
1173 + # whole file and MAGIC now selects the commit.
1174 + git config diff.tc.textconv cat &&
1175 + git log -L:func1:tc.c -G MAGIC --format=%s --no-patch >actual &&
1176 + test_grep "change both funcs" actual
1177 +'
1178 +
1179 test_done
xdiff-interface.h
+13
@@ -46,6 +46,19 @@ int xdi_diff_outf(mmfile_t *mf1, mmfile_t *mf2,
46 xdiff_emit_line_fn line_fn,
47 void *consume_callback_data,
48 xpparam_t const *xpp, xdemitconf_t const *xecfg);
49 +
50 +struct range_set;
51 +/*
52 + * Like xdi_diff_outf(), but forwards only the lines within the given
53 + * (post-image) line ranges to line_fn, as "git log -L" scopes its output.
54 + * Returns line_fn's latched return value (so a consumer can signal a hit
55 + * with a non-zero return), or non-zero on xdiff failure. Defined in
56 + * diff.c (it reuses the line-range filter there).
57 + */
58 +int diff_emit_line_ranges(mmfile_t *mf1, mmfile_t *mf2,
59 + const struct range_set *ranges,
60 + xdiff_emit_line_fn line_fn, void *cb_data,
61 + xpparam_t *xpp, xdemitconf_t *xecfg);
62 int read_mmfile(mmfile_t *ptr, const char *filename);
63 void read_mmblob(mmfile_t *ptr, struct object_database *odb,
64 const struct object_id *oid);