The line-range filter builds its own "@@ -<old> +<new> @@" header for
each range hunk. For a side with no lines (count 0, such as the old
side of a pure insertion), the begin should be the number of the line
before the change, per the convention git diff and xdl_emit_hunk_hdr()
follow. The hand-rolled code's begin was one too high; in t4211 this
produced
@@ -25,0 +18,9 @@
an old begin of 25 in a 24-line file, where git diff would give 24.
Stop hand-rolling the header. flush_range_hunk() now formats it through
xdiff's own emitter: a new xdiff_emit_hunk_header() helper wraps
xdl_emit_hunk_hdr(), the function that produces every other diff's hunk
headers. The count-0 begin is then correct by construction, and as a
side effect -L headers match git diff exactly, including its omission of
a count of 1 ("@@ -22 +22 @@" rather than "@@ -22,1 +22,1 @@").
xdiff's hunk callback already hands line_range_hunk_fn() a count-0 begin
decremented, so undo that when seeding the cursors and let the formatter
re-apply the convention once, at emit time.
The off-by-one predates this series, and the two regenerated fixtures
reach it from different origins: no-assertion-error has carried it since
its test was added in ab60c693a2 (line-log: fix assertion error,
2025-08-18), while vanishes-early acquired it when 86e986f166 (line-log:
route -L output through the standard diff pipeline) reshaped its tracked
line into a pure insertion. vanishes-early also drops its count-1
counts.
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Michael Montalbo committedJun 27, 2026 at 17:28 UTC7555510bf32835fae50363b86a8a667c1b681803
7 files changed+54-23
diff.c
+12-15
index ee765d7ac2..9751bb6798 100644--- a/diff.c+++ b/diff.c@@ -2636,14 +2636,9 @@ static void flush_range_hunk(struct line_range_filter *filter) return; }- strbuf_addf(&hdr, "@@ -%ld,%ld +%ld,%ld @@",- filter->hunk.old_begin, old_count,- filter->hunk.new_begin, new_count);- if (filter->funclen > 0) {- strbuf_addch(&hdr, ' ');- strbuf_add(&hdr, filter->func, filter->funclen);- }- strbuf_addch(&hdr, '\n');+ xdiff_emit_hunk_header(&hdr, filter->hunk.old_begin, old_count,+ filter->hunk.new_begin, new_count,+ filter->func, filter->funclen); filter->ret = filter->orig_line_fn(filter->orig_cb_data, hdr.buf, hdr.len); strbuf_release(&hdr);@@ -2668,19 +2663,21 @@ static void flush_range_hunk(struct line_range_filter *filter) } 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_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.+ * Seed the per-image line cursors from the hunk header's begins. For+ * a side with no lines (count 0), xdiff's callback has already moved+ * its begin to the line before the change, so add one back to recover+ * the true 1-based start. xdiff_emit_hunk_header() reapplies that -1+ * when the clipped hunk is emitted. */- filter->lno_in_postimage = new_begin;- filter->lno_in_preimage = old_begin;+ filter->lno_in_postimage = new_nr ? new_begin : new_begin + 1;+ filter->lno_in_preimage = old_nr ? old_begin : old_begin + 1; if (funclen > 0) { if (funclen > (long)sizeof(filter->func))
t/t4211/sha1/expect.no-assertion-error
+1-1
index 54c568f273..95faf51a7b 100644--- a/t/t4211/sha1/expect.no-assertion-error+++ b/t/t4211/sha1/expect.no-assertion-error@@ -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;
t/t4211/sha1/expect.vanishes-early
+3-3
index a413ad3659..e4b1a201d5 100644--- a/t/t4211/sha1/expect.vanishes-early+++ b/t/t4211/sha1/expect.vanishes-early@@ -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 @@ +}
t/t4211/sha256/expect.no-assertion-error
+1-1
index c25f2ce19c..815d27f7f1 100644--- a/t/t4211/sha256/expect.no-assertion-error+++ b/t/t4211/sha256/expect.no-assertion-error@@ -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;
t/t4211/sha256/expect.vanishes-early
+3-3
index bc33b963dc..263fc9eaac 100644--- a/t/t4211/sha256/expect.vanishes-early+++ b/t/t4211/sha256/expect.vanishes-early@@ -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 @@ +}
xdiff-interface.c
+19
index 5ee2b96d0a..32e04630ee 100644--- a/xdiff-interface.c+++ b/xdiff-interface.c@@ -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.
xdiff-interface.h
+15
index ce54e1c0e0..51c88296ed 100644--- a/xdiff-interface.h+++ b/xdiff-interface.h@@ -76,4 +76,19 @@ 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 these begins and counts. For a side with no lines+ * (count 0) the begin is the line before the change, and a count of 1+ * is omitted.+ */+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);+ #endif