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

Re: [PATCH] diff: restore redirection to /dev/null for diff_from_contents

From
Johannes Schindelin <johannes.schindelin@gmx.de>
Date
Oct 19, 2025, 21:09 UTC
Message-ID
<d5895f9c-5b3c-7a69-46e0-cf16cda5bf3a@gmx.de>
In-Reply-To
<20251017083641.GB4073661@coredump.intra.peff.net>
Hi Jeff,
On Fri, 17 Oct 2025, Jeff King wrote:
Show 17 quoted lines
> diff --git a/diff.c b/diff.c
> index 87fa16b730..687206f353 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -6890,6 +6890,15 @@ 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;

I do not see any discussion about the `color_moved` line in https://lore.kernel.org/git/20250808033019.78817-1-yldhome2d2@gmail.com/#r, nor here.

Since you re-add it, I consider at least a little bit of reasonsing in order, e.g. why this is necessary, and if it is necessary, why isn't `options->use_color` forced to 0 also?

Taking a step back to see the 100ft view, I can understand why you want that "extra level of protection" here. An even more important thing, that is missing, is a plan to avoid the need for this protection.

Given that you're still on GitHub's payroll if the hallway rumors are correct, I am quite a bit puzzled that you did not immediately reach for CodeQL (which is a GitHub-sponsored technology, after all) to get clarity on the code paths that would make this exra "layer of protection" still necessary, and thereby provide said plan.

I started an AI-assisted brainstorm session and ended up with this query (which is neither as concise nor as comprehensible as I would have liked, but at least it does the job of finding the `run_diff_cmd()` code path that I also find, and no other code path, and in v4 of Lidong Yan's patch, it finds no remaining code path):

```codeql
/**
 * @name Potential file write during a dry run
 * @description Traces paths where `diff_options->dry_run` is set to non-zero
 * and the corresponding `diff_options->file` is later used.
 * @kind path-problem
 * @problem.severity warning
 * @id cpp/potential-dry-run-file-write
 * @tags correctness
 */

import cpp import semmle.code.cpp.dataflow.new.DataFlow import semmle.code.cpp.controlflow.IRGuards as IRGuards import semmle.code.cpp.exprs.LogicalOperation import semmle.code.cpp.exprs.ComparisonOperation import semmle.code.cpp.exprs.Literal

/** Holds when `assign` sets `dry_run` to a non-zero literal. */
predicate setsDryRunNonZero(AssignExpr assign, FieldAccess dryRunField) {
  dryRunField.getTarget().hasName("dry_run") and
  assign.getLValue() = dryRunField and
  assign.getOperator() = "=" and
  isNonZeroLiteralExpr(assign.getRValue())
}
/** True when `expr` is literally zero (allowing common suffixes). */
predicate isZeroLiteralExpr(Expr expr) {
  exists(Literal lit |
    expr = lit and
    lit.getValueText().regexpMatch("(?i)\\s*0[uUlL]*\\s*")
  )
}
/** True when `expr` is a literal that is definitely non-zero. */
predicate isNonZeroLiteralExpr(Expr expr) {
  exists(Literal lit |
    expr = lit and
    not lit.getValueText().regexpMatch("(?i)\\s*0[uUlL]*\\s*")
  )
}
/** Holds if `access` uses the `file` member of a diff_options instance. */
predicate usesFileField(FieldAccess access) {
  access.getTarget().hasName("file")
}
/** True when an `if (options->dry_run)` immediately returns. */
predicate earlyReturnOnDryRun(FieldAccess fa) {
  fa.getTarget().hasName("dry_run") and
  exists(IfStmt ifStmt, ReturnStmt ret |
    ret = ifStmt.getThen() and
    ifStmt.getCondition() = fa and
    not exists(Stmt elseStmt | elseStmt = ifStmt.getElse())
  )
}
/** Data-flow configuration tracking diff options pointers while dry-run is enabled. */
module DryRunConfig implements DataFlow::ConfigSig {
  /** Sources: the `diff_options *` pointer whose `dry_run` field is set to non-zero. */
  predicate isSource(DataFlow::Node source) {
    exists(AssignExpr assign, FieldAccess dryRunField |
      setsDryRunNonZero(assign, dryRunField) and
      source.asExpr() = dryRunField.getQualifier()
    )
  }
  /** Sinks: any dereference of the `file` field through that diff options pointer. */
  predicate isSink(DataFlow::Node sink) {
    exists(FieldAccess access |
      usesFileField(access) and
      sink.asExpr() = access.getQualifier()
    )
  }
  /** Barriers: proofs that `dry_run` is zero or explicit resets back to zero. */
  predicate isBarrier(DataFlow::Node barrier) {
    exists(IRGuards::GuardCondition guard, FieldAccess fa |
      fa.getTarget().hasName("dry_run") and
      guard.getAChild*() = fa and
      barrier.asExpr() = fa.getQualifier() and
      safeDryRunCheck(guard, fa)
    )
    or
    exists(AssignExpr assign, FieldAccess fa |
      fa.getTarget().hasName("dry_run") and
      assign.getLValue() = fa and
      assign.getOperator() = "=" and
      barrier.asExpr() = fa.getQualifier() and
      isZeroLiteralExpr(assign.getRValue())
    )
    or
    exists(FieldAccess fa |
      earlyReturnOnDryRun(fa) and
      barrier.asExpr() = fa.getQualifier()
    )
  }
  /** Holds if `guard` ensures that `dry_run` evaluates to zero/false. */
  additional predicate safeDryRunCheck(IRGuards::GuardCondition guard, FieldAccess fa) {
    exists(NotExpr notExpr |
      guard.getAChild*() = notExpr and
      notExpr.getOperand() = fa
    )
    or
    exists(EQExpr eqExpr |
      guard.getAChild*() = eqExpr and
      (
        eqExpr.getLeftOperand() = fa and
        isZeroLiteralExpr(eqExpr.getRightOperand())
        or
        eqExpr.getRightOperand() = fa and
        isZeroLiteralExpr(eqExpr.getLeftOperand())
      )
    )
  }
}

/** Execute the configured global data-flow analysis. */ module DryRunFlow = DataFlow::Global<DryRunConfig>;

from DryRunFlow::PathNode source, DryRunFlow::PathNode sink,
  AssignExpr srcAssign, FieldAccess dryRunAccess, FieldAccess fileAccess
where
  DryRunFlow::flowPath(source, sink) and
  setsDryRunNonZero(srcAssign, dryRunAccess) and
  source.getNode().asExpr() = dryRunAccess.getQualifier() and
  usesFileField(fileAccess) and
  sink.getNode().asExpr() = fileAccess.getQualifier()
select fileAccess, source, sink,
  "`diff_options->file` used while `dry_run` forced non-zero at $@ and consumed here at $@.",
  srcAssign, "dry_run assignment",
  fileAccess, "file field use"
query predicate edges(DryRunFlow::PathNode edgeSource, DryRunFlow::PathNode edgeSink,
  string edgeKind, string edgeText) {
  DryRunFlow::PathGraph::edges(edgeSource, edgeSink, edgeKind, edgeText)
}
```
Show 14 quoted lines
>  		for (i = 0; i < q->nr; i++) {
>  			struct diff_filepair *p = q->queue[i];
>  			if (check_pair_status(p))
> diff --git a/t/t4035-diff-quiet.sh b/t/t4035-diff-quiet.sh
> index 0352bf81a9..35eaf0855f 100755
> --- a/t/t4035-diff-quiet.sh
> +++ b/t/t4035-diff-quiet.sh
> @@ -50,6 +50,10 @@ test_expect_success 'git diff-tree HEAD HEAD' '
>  	test_expect_code 0 git diff-tree --quiet HEAD HEAD >cnt &&
>  	test_line_count = 0 cnt
>  '
> +test_expect_success 'git diff-tree -w HEAD^ HEAD' '
> +	test_expect_code 1 git diff-tree --quiet -w HEAD^ HEAD >cnt &&
> +	test_line_count = 0 cnt

I understand that you imitate the surrounding code, but there is `test_must_be_empty` now, which has the huge advantage of documenting intention much better than requiring the line count to be zero (and I wish that there was a comprehensive roadmap and planning in general to avoid, or at least clean up, the vast amount of style inconsistencies in Git, preferably via automation so that no human being is burdened with _that_ cognitive load).

Ciao, Johannes

Show 9 quoted lines
> +'
>  test_expect_success 'git diff-files' '
>  	test_expect_code 0 git diff-files --quiet >cnt &&
>  	test_line_count = 0 cnt
> -- 
> 2.51.1.685.g6bf3278fbc
> 
> 
> 
Previous: Junio C HamanoNext: Jeff King
Message 5 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.