git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v3 3/7] diff: emit -L hunk headers via xdiff's formatter

From
Michael Montalbo via GitGitGadget <gitgitgadget@gmail.com>
Date
Sep 3, 2026, 05:05 UTC
Message-ID
<2081d5d257d65b7b823fc46777e952a2f650e6dd.1788411919.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.2152.v3.git.1788411919.gitgitgadget@gmail.com>
From: Michael Montalbo <mmontalbo@gmail.com>

Currently, diff's line-range filter implements its own method for emitting diff hunk headers. This mostly matches what xdiff itself outputs, but there is a discrepancy for postimage or preimage sides with 0 line changes. For a side with no lines (count 0), the begin is the line before the change. The header omits the line count of 1.

Rather than fix this case in the line-range implementation, expose the function xdiff uses to emit its headers. Reusing it keeps the header format consistent with and without -L.

Update test scenarios and fixtures to reflect the now consistent header format.

Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
 diff.c                                   | 21 ++++++++-------------
 t/t4211/sha1/expect.no-assertion-error   |  2 +-
 t/t4211/sha1/expect.vanishes-early       |  6 +++---
 t/t4211/sha256/expect.no-assertion-error |  2 +-
 t/t4211/sha256/expect.vanishes-early     |  6 +++---
 xdiff-interface.c                        | 19 +++++++++++++++++++
 xdiff-interface.h                        | 13 +++++++++++++
 7 files changed, 48 insertions(+), 21 deletions(-)
diff --git a/diff.c b/diff.c
index c94ddbebe5..cb1a85c624 100644
--- a/diff.c
+++ b/diff.c
@@ -2560,15 +2560,10 @@ static void flush_range_hunk(struct line_range_filter *filter)
 		return;
 	}
 
-	strbuf_addf(&hdr, "@@ -%ld,%ld +%ld,%ld @@",
-		    filter->accumulating_hunk.old_begin, old_count,
-		    filter->accumulating_hunk.new_begin, new_count);
-	if (filter->accumulating_hunk.func_name_len > 0) {
-		strbuf_addch(&hdr, ' ');
-		strbuf_add(&hdr, filter->accumulating_hunk.func_name,
-			   filter->accumulating_hunk.func_name_len);
-	}
-	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);
 
 	filter->ret = filter->orig_line_fn(filter->orig_cb_data, hdr.buf, hdr.len);
 	strbuf_release(&hdr);
@@ -2589,14 +2584,14 @@ 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;
 
-	filter->accumulating_hunk.lno_in_postimage = new_begin;
-	filter->accumulating_hunk.lno_in_preimage = 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(filter->accumulating_hunk.func_name))
diff --git a/t/t4211/sha1/expect.no-assertion-error b/t/t4211/sha1/expect.no-assertion-error
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;
diff --git a/t/t4211/sha1/expect.vanishes-early b/t/t4211/sha1/expect.vanishes-early
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 @@
 +}
diff --git a/t/t4211/sha256/expect.no-assertion-error b/t/t4211/sha256/expect.no-assertion-error
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;
diff --git a/t/t4211/sha256/expect.vanishes-early b/t/t4211/sha256/expect.vanishes-early
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 @@
 +}
diff --git a/xdiff-interface.c b/xdiff-interface.c
index db6938689f..686db8bb8b 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.
diff --git a/xdiff-interface.h b/xdiff-interface.h
index ce54e1c0e0..2428456629 100644
--- a/xdiff-interface.h
+++ b/xdiff-interface.h
@@ -76,4 +76,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
-- 
gitgitgadget
Previous: Michael Montalbo via GitGitGadgetNext: Michael Montalbo via GitGitGadget
Message 23 of 27 in “line-log: range-scope stat, check, and -G under -L”
  1. 0/7 line-log: range-scope stat, check, and -G under -LMichael Montalbo via GitGitGadget, Jun 18, 2026
  2. 1/7 diff: rename and group the line-range filter for clarityMichael Montalbo via GitGitGadget, Jun 18, 2026
  3. 2/7 diff: simplify the line-range filter by classifying removals immediatelyMichael Montalbo via GitGitGadget, Jun 18, 2026
  4. 3/7 diff: emit -L hunk headers via xdiff's formatterMichael Montalbo via GitGitGadget, Jun 18, 2026
  5. 4/7 diff: extract a line-range diff helper for reuseMichael Montalbo via GitGitGadget, Jun 18, 2026
  6. 5/7 line-log: support diff stat formats with -LMichael Montalbo via GitGitGadget, Jun 18, 2026
  7. Junio C HamanoJun 18, 2026
  8. Michael MontalboJun 23, 2026
  9. 6/7 diff: support --check with -L line rangesMichael Montalbo via GitGitGadget, Jun 18, 2026
  10. 7/7 diffcore-pickaxe: scope -G to the -L tracked rangeMichael Montalbo via GitGitGadget, Jun 18, 2026
  11. 0/7 line-log: scope stat, check, and -G to -L line rangesMichael Montalbo via GitGitGadget, Jun 27, 2026
  12. 1/7 diff: rename and group the line-range filter for clarityMichael Montalbo via GitGitGadget, Jun 27, 2026
  13. 2/7 diff: simplify the line-range filter by classifying removals immediatelyMichael Montalbo via GitGitGadget, Jun 27, 2026
  14. 3/7 diff: emit -L hunk headers via xdiff's formatterMichael Montalbo via GitGitGadget, Jun 27, 2026
  15. 4/7 diff: extract a line-range diff helper for reuseMichael Montalbo via GitGitGadget, Jun 27, 2026
  16. 5/7 line-log: support diff stat formats with -LMichael Montalbo via GitGitGadget, Jun 27, 2026
  17. 6/7 diff: support --check with -L line rangesMichael Montalbo via GitGitGadget, Jun 27, 2026
  18. 7/7 diffcore-pickaxe: scope -G to the -L tracked rangeMichael Montalbo via GitGitGadget, Jun 27, 2026
  19. Michael MontalboJul 15, 2026
  20. 0/7 line-log: scope stat, check, and -G to -L line rangesMichael Montalbo via GitGitGadget, Sep 3, 2026
  21. 1/7 diff: rename line-range filter struct and clarify fieldsMichael Montalbo via GitGitGadget, Sep 3, 2026
  22. 2/7 diff: simplify the line-range filter by classifying removals immediatelyMichael Montalbo via GitGitGadget, Sep 3, 2026
  23. 3/7 diff: emit -L hunk headers via xdiff's formatterMichael Montalbo via GitGitGadget, Sep 3, 2026
  24. 4/7 diff: extract a line-range diff helper for reuseMichael Montalbo via GitGitGadget, Sep 3, 2026
  25. 5/7 diff: support stat formats with -LMichael Montalbo via GitGitGadget, Sep 3, 2026
  26. 6/7 diff: support --check with -L line rangesMichael Montalbo via GitGitGadget, Sep 3, 2026
  27. 7/7 diffcore-pickaxe: limit -G to the -L tracked rangeMichael Montalbo via GitGitGadget, Sep 3, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.