diff: emit -L hunk headers via xdiff's formatter

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 committed Jun 27, 2026 at 17:28 UTC 7555510bf32835fae50363b86a8a667c1b681803
7 files changed +54 -23
diff.c
+12 -15
@@ -2636,14 +2636,9 @@ static void flush_range_hunk(struct line_range_filter *filter)
2636 return;
2637 }
2638
2639 - strbuf_addf(&hdr, "@@ -%ld,%ld +%ld,%ld @@",
2640 - filter->hunk.old_begin, old_count,
2641 - filter->hunk.new_begin, new_count);
2642 - if (filter->funclen > 0) {
2643 - strbuf_addch(&hdr, ' ');
2644 - strbuf_add(&hdr, filter->func, filter->funclen);
2645 - }
2646 - strbuf_addch(&hdr, '\n');
2639 + xdiff_emit_hunk_header(&hdr, filter->hunk.old_begin, old_count,
2640 + filter->hunk.new_begin, new_count,
2641 + filter->func, filter->funclen);
2642
2643 filter->ret = filter->orig_line_fn(filter->orig_cb_data, hdr.buf, hdr.len);
2644 strbuf_release(&hdr);
@@ -2668,19 +2663,21 @@ static void flush_range_hunk(struct line_range_filter *filter)
2663 }
2664
2665 static void line_range_hunk_fn(void *data,
2671 - long old_begin, long old_nr UNUSED,
2672 - long new_begin, long new_nr UNUSED,
2666 + long old_begin, long old_nr,
2667 + long new_begin, long new_nr,
2668 const char *func, long funclen)
2669 {
2670 struct line_range_filter *filter = data;
2671
2672 /*
2678 - * When count > 0, begin is 1-based. When count == 0, begin is
2679 - * adjusted down by 1 by xdl_emit_hunk_hdr(), but no lines of
2680 - * that type will arrive, so the value is unused.
2673 + * Seed the per-image line cursors from the hunk header's begins. For
2674 + * a side with no lines (count 0), xdiff's callback has already moved
2675 + * its begin to the line before the change, so add one back to recover
2676 + * the true 1-based start. xdiff_emit_hunk_header() reapplies that -1
2677 + * when the clipped hunk is emitted.
2678 */
2682 - filter->lno_in_postimage = new_begin;
2683 - filter->lno_in_preimage = old_begin;
2679 + filter->lno_in_postimage = new_nr ? new_begin : new_begin + 1;
2680 + filter->lno_in_preimage = old_nr ? old_begin : old_begin + 1;
2681
2682 if (funclen > 0) {
2683 if (funclen > (long)sizeof(filter->func))
t/t4211/sha1/expect.no-assertion-error
+1 -1
@@ -8,7 +8,7 @@ diff --git a/b.c b/b.c
8 index bf79c2f..27c829c 100644
9 --- a/b.c
10 +++ b/b.c
11 -@@ -25,0 +18,9 @@
11 +@@ -24,0 +18,9 @@
12 +long f(long x)
13 +{
14 + int s = 0;
t/t4211/sha1/expect.vanishes-early
+3 -3
@@ -8,7 +8,7 @@ diff --git a/a.c b/a.c
8 index 0b9cae5..5de3ea4 100644
9 --- a/a.c
10 +++ b/a.c
11 -@@ -23,0 +24,1 @@ int main ()
11 +@@ -22,0 +24 @@ int main ()
12 +/* incomplete lines are bad! */
13
14 commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
@@ -21,7 +21,7 @@ diff --git a/a.c b/a.c
21 index 5e709a1..0b9cae5 100644
22 --- a/a.c
23 +++ b/a.c
24 -@@ -22,1 +22,1 @@ int main ()
24 +@@ -22 +22 @@ int main ()
25 -}
26 +}
27 \ No newline at end of file
@@ -37,5 +37,5 @@ new file mode 100644
37 index 0000000..444e415
38 --- /dev/null
39 +++ b/a.c
40 -@@ -0,0 +20,1 @@
40 +@@ -0,0 +20 @@
41 +}
t/t4211/sha256/expect.no-assertion-error
+1 -1
@@ -8,7 +8,7 @@ diff --git a/b.c b/b.c
8 index 69cb69c..a0d566e 100644
9 --- a/b.c
10 +++ b/b.c
11 -@@ -25,0 +18,9 @@
11 +@@ -24,0 +18,9 @@
12 +long f(long x)
13 +{
14 + int s = 0;
t/t4211/sha256/expect.vanishes-early
+3 -3
@@ -8,7 +8,7 @@ diff --git a/a.c b/a.c
8 index e4fa1d8..62c1fc2 100644
9 --- a/a.c
10 +++ b/a.c
11 -@@ -23,0 +24,1 @@ int main ()
11 +@@ -22,0 +24 @@ int main ()
12 +/* incomplete lines are bad! */
13
14 commit 29f32ac3141c48b22803e5c4127b719917b67d0f8ca8c5248bebfa2a19f7da10
@@ -21,7 +21,7 @@ diff --git a/a.c b/a.c
21 index d325124..e4fa1d8 100644
22 --- a/a.c
23 +++ b/a.c
24 -@@ -22,1 +22,1 @@ int main ()
24 +@@ -22 +22 @@ int main ()
25 -}
26 +}
27 \ No newline at end of file
@@ -37,5 +37,5 @@ new file mode 100644
37 index 0000000..9f550c3
38 --- /dev/null
39 +++ b/a.c
40 -@@ -0,0 +20,1 @@
40 +@@ -0,0 +20 @@
41 +}
xdiff-interface.c
+19
@@ -91,6 +91,25 @@ static int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf)
91 return 0;
92 }
93
94 +static int strbuf_out_line(void *priv, mmbuffer_t *mb, int nbuf)
95 +{
96 + struct strbuf *out = priv;
97 + int i;
98 + for (i = 0; i < nbuf; i++)
99 + strbuf_add(out, mb[i].ptr, mb[i].size);
100 + return 0;
101 +}
102 +
103 +void xdiff_emit_hunk_header(struct strbuf *out,
104 + long old_begin, long old_count,
105 + long new_begin, long new_count,
106 + const char *func, long funclen)
107 +{
108 + xdemitcb_t ecb = { .priv = out, .out_line = strbuf_out_line };
109 + xdl_emit_hunk_hdr(old_begin, old_count, new_begin, new_count,
110 + func, funclen, &ecb);
111 +}
112 +
113 /*
114 * Trim down common substring at the end of the buffers,
115 * but end on a complete line.
xdiff-interface.h
+15
@@ -76,4 +76,19 @@ int xdiff_compare_lines(const char *l1, long s1,
76 */
77 unsigned long xdiff_hash_string(const char *s, size_t len, long flags);
78
79 +struct strbuf;
80 +
81 +/*
82 + * Append a unified-diff hunk header to `out`, e.g.
83 + * "@@ -<old> +<new> @@ func\n". The header comes from wrapping xdiff's
84 + * own hunk-header emitter, so it matches what a normal diff would
85 + * produce for these begins and counts. For a side with no lines
86 + * (count 0) the begin is the line before the change, and a count of 1
87 + * is omitted.
88 + */
89 +void xdiff_emit_hunk_header(struct strbuf *out,
90 + long old_begin, long old_count,
91 + long new_begin, long new_count,
92 + const char *func, long funclen);
93 +
94 #endif