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

Re: Regression in `git diff --quiet HEAD` when a new file is staged

From
Jeff King <peff@peff.net>
Date
Oct 23, 2025, 12:15 UTC
Message-ID
<20251023121525.GB1123594@coredump.intra.peff.net>
In-Reply-To
<20251023120101.GA1123594@coredump.intra.peff.net>
On Thu, Oct 23, 2025 at 08:01:01AM -0400, Jeff King wrote:
> You could even imagine diff_flush_patch_quietly() saving the /dev/null
> descriptor in a static variable and effectively leaking it (or if we
> want to be more structured, cached inside the diff_options struct). And
> then the callers do not have to worry about it at all.

Something like this (on top of jk/diff-from-contents-fix, replacing what you have in jc/diff-from-contents/fix):

diff --git a/diff.c b/diff.c
index 9b8d658b9e..c9d3aaeb0f 100644
--- a/diff.c
+++ b/diff.c
@@ -6175,14 +6175,34 @@ static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o)
 /* return 1 if any change is found; otherwise, return 0 */
 static int diff_flush_patch_quietly(struct diff_filepair *p, struct diff_options *o)
 {
+	static FILE *dev_null;
+	FILE *saved_file = o->file;
+	int saved_color_moved = o->color_moved;
 	int saved_dry_run = o->dry_run;
 	int saved_found_changes = o->found_changes;
 	int ret;
 
+	/*
+	 * As an extra precaution against code sending output to o->file even
+	 * when o->dry_run is set, redirect to /dev/null.
+	 *
+	 * We cache the /dev/null filehandle forever, effectively leaking it.
+	 * Gross, but it's O(1) gross-ness. A better solution would perhaps be
+	 * stuffing it into o->cached_dev_null or something, and freeing it
+	 * with the rest of the diff options.
+	 */
+	if (!dev_null)
+		dev_null = xfopen("/dev/null", "w");
+
+	o->file = dev_null;
+	/* TODO check if this is actually doing anything! */
+	o->color_moved = 0;
 	o->dry_run = 1;
 	o->found_changes = 0;
 	diff_flush_patch(p, o);
 	ret = o->found_changes;
+	o->file = saved_file;
+	o->color_moved = saved_color_moved;
 	o->dry_run = saved_dry_run;
 	o->found_changes |= saved_found_changes;
 	return ret;
@@ -6876,15 +6896,6 @@ void diff_flush(struct diff_options *options)
 	if (output_format & DIFF_FORMAT_NO_OUTPUT &&
 	    options->flags.exit_with_status &&
 	    options->flags.diff_from_contents) {
-		/*
-		 * run diff_flush_patch for the exit status. setting
-		 * options->file to /dev/null should be safe, because we
-		 * aren't supposed to produce any output anyway.
-		 */
-		diff_free_file(options);
-		options->file = xfopen("/dev/null", "w");
-		options->close_file = 1;
-		options->color_moved = 0;
 		for (i = 0; i < q->nr; i++) {
 			struct diff_filepair *p = q->queue[i];
 			if (check_pair_status(p))

And you can see the difference with the tests Lidong added in t4013, or
just with this simple sequence:

  git init
  echo content >file
  git add file
  git commit -m foo
  git.compile show -w --name-status

Without either the /dev/null redirection above (or the actual dry_run
fixes), you get a bogus "diff --git" header in the output.

So mulling over that for a moment...if we are going to teach all code
paths that look at o->file to check o->dry_run, why do we need a
/dev/null redirection at all? Can't we just set o->file to NULL, and
that is the clue that we do not want output?

I know that is more intricate, and not what we want to do for the
immediate regression fix. But in the long run it makes more sense to me.
We get rid of the extra flag, and any code that does the wrong thing (by
trying to write to o->file) will blow up horribly with a segfault rather
than quietly produce wrong output.

-Peff
Previous: Jeff KingNext: Junio C Hamano
Message 23 of 27 in “Regression in `git diff --quiet HEAD` when a new file is staged”
  1. Jake ZimmermanOct 17, 2025
  2. Jeff KingOct 17, 2025
  3. diff: restore redirection to /dev/null for diff_from_contentsJeff King, Oct 17, 2025
  4. Junio C HamanoOct 17, 2025
  5. Johannes SchindelinOct 19, 2025
  6. Jeff KingOct 21, 2025
  7. Johannes SchindelinOct 17, 2025
  8. Junio C HamanoOct 17, 2025
  9. Lidong YanOct 18, 2025
  10. Jeff KingOct 18, 2025
  11. Jeff KingOct 18, 2025
  12. Junio C HamanoOct 18, 2025
  13. Jeff KingOct 21, 2025
  14. Junio C HamanoOct 21, 2025
  15. Lidong YanOct 22, 2025
  16. Jeff KingOct 22, 2025
  17. Lidong YanOct 22, 2025
  18. Junio C HamanoOct 22, 2025
  19. Junio C HamanoOct 22, 2025
  20. Jeff KingOct 22, 2025
  21. Junio C HamanoOct 22, 2025
  22. Jeff KingOct 23, 2025
  23. Jeff KingOct 23, 2025
  24. Junio C HamanoOct 23, 2025
  25. Junio C HamanoOct 22, 2025
  26. Lidong YanOct 23, 2025
  27. Junio C HamanoOct 23, 2025

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.