* diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code
@ 2025-11-08 19:05 D. Ben Knoble
2025-11-08 19:08 ` D. Ben Knoble
` (2 more replies)
0 siblings, 3 replies; 15+ messages in thread
From: D. Ben Knoble @ 2025-11-08 19:05 UTC (permalink / raw)
To: Git
AFAICT, you need all of the mentioned options to trigger the bug.
Allowing ext-diff works fine, I don't think it's triggered in
non-cached diffs, and I've never seen it without --find-copies-harder.
Notably, s/quiet/exit-code works just fine.
Here's a repro from git.git:
cp git{,1}.c
git add git1.c
git diff --cached --no-ext-diff --quiet --find-copies-harder &&
echo 'this should exit 1!'
(And of course, ^quiet^exit-code if your shell supports it yields a
different outcome)
Context: my distro applies a patch that allows
diff.renames=copies-harder. In a repo with that turned on,
git-prompt.sh stopped showing some staged changes. Turns out it runs
git diff with all these flags (less --find-copies-harder, which is
enabled by the config option). I _have_ confirmed this bug exists in
unpatched Git, however.
Some rough debugging notes: when entering diffcore_std (or
diffcore_rename_extended's cleanup loop):
- for exit-code, diff_queued_diff.nr matches "git ls-files :/ | wc -l"
- for quiet, it's just 1 (the first file listed by git ls-files :/, AFAICT)
The only other obvious difference I spotted is that the "quick" flag
is turned on for quiet, which makes sense.
I tried to figure out who builds the queue, and it looks like it's
diff_cache -> unpack_trees -> ..., where at some point for exit-code
we _keep_ queuing files, but for quiet we don't.
I also saw diff_cache tweaks opts.diff_index_cached based on
--find-copies-harder, but I haven't looked further to see how that
affects things or what the interaction with ext-diff is.
Help welcome, thanks.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code
2025-11-08 19:05 diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code D. Ben Knoble
@ 2025-11-08 19:08 ` D. Ben Knoble
2025-11-08 19:12 ` D. Ben Knoble
2025-11-09 12:11 ` [PATCH] diff: disabled quick optimization with --find-copies-harder René Scharfe
2025-11-09 16:43 ` [PATCH v2] diff: disable rename detection with --quiet René Scharfe
2 siblings, 1 reply; 15+ messages in thread
From: D. Ben Knoble @ 2025-11-08 19:08 UTC (permalink / raw)
To: Git
On Sat, Nov 8, 2025 at 2:05 PM D. Ben Knoble <ben.knoble@gmail.com> wrote:
>
> AFAICT, you need all of the mentioned options to trigger the bug.
> Allowing ext-diff works fine, I don't think it's triggered in
> non-cached diffs, and I've never seen it without --find-copies-harder.
> Notably, s/quiet/exit-code works just fine.
>
> Here's a repro from git.git:
>
> cp git{,1}.c
> git add git1.c
> git diff --cached --no-ext-diff --quiet --find-copies-harder &&
> echo 'this should exit 1!'
>
> (And of course, ^quiet^exit-code if your shell supports it yields a
> different outcome)
>
> Context: my distro applies a patch that allows
> diff.renames=copies-harder. In a repo with that turned on,
> git-prompt.sh stopped showing some staged changes. Turns out it runs
> git diff with all these flags (less --find-copies-harder, which is
> enabled by the config option). I _have_ confirmed this bug exists in
> unpatched Git, however.
>
> Some rough debugging notes: when entering diffcore_std (or
> diffcore_rename_extended's cleanup loop):
> - for exit-code, diff_queued_diff.nr matches "git ls-files :/ | wc -l"
> - for quiet, it's just 1 (the first file listed by git ls-files :/, AFAICT)
> The only other obvious difference I spotted is that the "quick" flag
> is turned on for quiet, which makes sense.
Ah, woops. The reason this matters is that, after diff_rename, in the
correct version the queue is non-empty and has_changes gets set, which
plays into diff_result_code. In the broken version, the resulting
queue ends up empty, so has_changes is _reset_ to 0 (despite
previously being 1?)
I think I also spotted a difference in diff_from_contents, but not
sure if that's relevant.
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code
2025-11-08 19:08 ` D. Ben Knoble
@ 2025-11-08 19:12 ` D. Ben Knoble
0 siblings, 0 replies; 15+ messages in thread
From: D. Ben Knoble @ 2025-11-08 19:12 UTC (permalink / raw)
To: Git
On Sat, Nov 8, 2025 at 2:08 PM D. Ben Knoble <ben.knoble@gmail.com> wrote:
>
> On Sat, Nov 8, 2025 at 2:05 PM D. Ben Knoble <ben.knoble@gmail.com> wrote:
> >
> > AFAICT, you need all of the mentioned options to trigger the bug.
> > Allowing ext-diff works fine, I don't think it's triggered in
> > non-cached diffs, and I've never seen it without --find-copies-harder.
> > Notably, s/quiet/exit-code works just fine.
>
>
> I think I also spotted a difference in diff_from_contents, but not
> sure if that's relevant.
Yeesh. You know how writing for others clarifies thoughts? Well...
I just noticed that diff_setup_done tweaks diff_from_contents based on
whether external diffs are allowed. Possibly relevant? I haven't been
able to easily identify a place where all 3 relevant options come
together, but this would be 2 of them (quiet and ext-diff).
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH] diff: disabled quick optimization with --find-copies-harder
2025-11-08 19:05 diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code D. Ben Knoble
2025-11-08 19:08 ` D. Ben Knoble
@ 2025-11-09 12:11 ` René Scharfe
2025-11-09 14:18 ` Phillip Wood
2025-11-09 16:43 ` [PATCH v2] diff: disable rename detection with --quiet René Scharfe
2 siblings, 1 reply; 15+ messages in thread
From: René Scharfe @ 2025-11-09 12:11 UTC (permalink / raw)
To: D. Ben Knoble, Git; +Cc: Junio C Hamano, Jeff King
If --find-copies-harder is given, diff-lib.c::show_modified() queues
even non-modified entries using diff_change() because we need them for
copy detection. diff_change() sets flags.has_changes, though. If
--quiet is also given this causes diff_can_quit_early() to declare we're
done after seeing only the very first entry, which is way too soon.
Disable this optimization in that case.
This issue is hidden without --no-ext-diff because then we set
flags.diff_from_contents, which disables the optimization in a
different way.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
diff.c | 1 +
t/t4007-rename-3.sh | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/diff.c b/diff.c
index a1961526c0..84ac148c37 100644
--- a/diff.c
+++ b/diff.c
@@ -7188,6 +7188,7 @@ int diff_can_quit_early(struct diff_options *opt)
{
return (opt->flags.quick &&
!opt->filter &&
+ !opt->flags.find_copies_harder &&
opt->flags.has_changes);
}
diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh
index e8faf0dd2e..3fc81bcd76 100755
--- a/t/t4007-rename-3.sh
+++ b/t/t4007-rename-3.sh
@@ -41,6 +41,16 @@ test_expect_success 'copy detection, cached' '
compare_diff_raw current expected
'
+test_expect_success 'exit code of quiet copy detection' '
+ test_expect_code 1 \
+ git diff --quiet --cached --find-copies-harder $tree
+'
+
+test_expect_success 'exit code of quiet copy detection with --no-ext-diff' '
+ test_expect_code 1 \
+ git diff --quiet --cached --find-copies-harder --no-ext-diff $tree
+'
+
# In the tree, there is only path0/COPYING. In the cache, path0 and
# path1 both have COPYING and the latter is a copy of path0/COPYING.
# However when we say we care only about path1, we should just see
--
2.51.2
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH] diff: disabled quick optimization with --find-copies-harder
2025-11-09 12:11 ` [PATCH] diff: disabled quick optimization with --find-copies-harder René Scharfe
@ 2025-11-09 14:18 ` Phillip Wood
2025-11-09 16:43 ` René Scharfe
0 siblings, 1 reply; 15+ messages in thread
From: Phillip Wood @ 2025-11-09 14:18 UTC (permalink / raw)
To: René Scharfe, D. Ben Knoble, Git; +Cc: Junio C Hamano, Jeff King
On 09/11/2025 12:11, René Scharfe wrote:
> If --find-copies-harder is given, diff-lib.c::show_modified() queues
> even non-modified entries using diff_change() because we need them for
> copy detection. diff_change() sets flags.has_changes, though. If
> --quiet is also given this causes diff_can_quit_early() to declare we're
> done after seeing only the very first entry, which is way too soon.
> Disable this optimization in that case.
Stepping back a bit I'm confused as to why we don't disable rename and
copy detection when "--quiet" is given. I can't see why detecting copies
or renames would change the exit code but maybe I'm missing something.
Thanks
Phillip
> This issue is hidden without --no-ext-diff because then we set
> flags.diff_from_contents, which disables the optimization in a
> different way.
>
> Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
> Signed-off-by: René Scharfe <l.s.r@web.de>
> ---
> diff.c | 1 +
> t/t4007-rename-3.sh | 10 ++++++++++
> 2 files changed, 11 insertions(+)
>
> diff --git a/diff.c b/diff.c
> index a1961526c0..84ac148c37 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -7188,6 +7188,7 @@ int diff_can_quit_early(struct diff_options *opt)
> {
> return (opt->flags.quick &&
> !opt->filter &&
> + !opt->flags.find_copies_harder &&
> opt->flags.has_changes);
> }
>
> diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh
> index e8faf0dd2e..3fc81bcd76 100755
> --- a/t/t4007-rename-3.sh
> +++ b/t/t4007-rename-3.sh
> @@ -41,6 +41,16 @@ test_expect_success 'copy detection, cached' '
> compare_diff_raw current expected
> '
>
> +test_expect_success 'exit code of quiet copy detection' '
> + test_expect_code 1 \
> + git diff --quiet --cached --find-copies-harder $tree
> +'
> +
> +test_expect_success 'exit code of quiet copy detection with --no-ext-diff' '
> + test_expect_code 1 \
> + git diff --quiet --cached --find-copies-harder --no-ext-diff $tree
> +'
> +
> # In the tree, there is only path0/COPYING. In the cache, path0 and
> # path1 both have COPYING and the latter is a copy of path0/COPYING.
> # However when we say we care only about path1, we should just see
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH] diff: disabled quick optimization with --find-copies-harder
2025-11-09 14:18 ` Phillip Wood
@ 2025-11-09 16:43 ` René Scharfe
0 siblings, 0 replies; 15+ messages in thread
From: René Scharfe @ 2025-11-09 16:43 UTC (permalink / raw)
To: phillip.wood, D. Ben Knoble, Git; +Cc: Junio C Hamano, Jeff King
On 11/9/25 3:18 PM, Phillip Wood wrote:
>
> Stepping back a bit I'm confused as to why we don't disable rename
> and copy detection when "--quiet" is given. I can't see why
> detecting copies or renames would change the exit code but maybe I'm
> missing something.
Excellent question! I also can't think of a reason.
René
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v2] diff: disable rename detection with --quiet
2025-11-08 19:05 diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code D. Ben Knoble
2025-11-08 19:08 ` D. Ben Knoble
2025-11-09 12:11 ` [PATCH] diff: disabled quick optimization with --find-copies-harder René Scharfe
@ 2025-11-09 16:43 ` René Scharfe
2025-11-09 17:34 ` D. Ben Knoble
2025-11-10 17:54 ` Jeff King
2 siblings, 2 replies; 15+ messages in thread
From: René Scharfe @ 2025-11-09 16:43 UTC (permalink / raw)
To: D. Ben Knoble, Git; +Cc: Phillip Wood, Junio C Hamano, Jeff King
Detecting renames and copies improves diff's output. This effort is
wasted if we don't show any. Disable detection in that case.
This actually fixes the error code when using the options --cached,
--find-copies-harder, --no-ext-diff and --quiet together:
run_diff_index() indirectly calls diff-lib.c::show_modified(), which
queues even non-modified entries using diff_change() because we need
them for copy detection. diff_change() sets flags.has_changes, though,
which causes diff_can_quit_early() to declare we're done after seeing
only the very first entry -- way too soon.
Using --cached, --find-copies-harder and --quiet together without
--no-ext-diff was not affected even before, as it causes the flag
flags.diff_from_contents to be set, which disables the optimization
in a different way.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Suggested-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
diff.c | 2 ++
t/t4007-rename-3.sh | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/diff.c b/diff.c
index a1961526c0..efa8d9773c 100644
--- a/diff.c
+++ b/diff.c
@@ -4987,6 +4987,8 @@ void diff_setup_done(struct diff_options *options)
if (options->flags.quick) {
options->output_format = DIFF_FORMAT_NO_OUTPUT;
options->flags.exit_with_status = 1;
+ options->detect_rename = 0;
+ options->flags.find_copies_harder = 0;
}
/*
diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh
index e8faf0dd2e..3fc81bcd76 100755
--- a/t/t4007-rename-3.sh
+++ b/t/t4007-rename-3.sh
@@ -41,6 +41,16 @@ test_expect_success 'copy detection, cached' '
compare_diff_raw current expected
'
+test_expect_success 'exit code of quiet copy detection' '
+ test_expect_code 1 \
+ git diff --quiet --cached --find-copies-harder $tree
+'
+
+test_expect_success 'exit code of quiet copy detection with --no-ext-diff' '
+ test_expect_code 1 \
+ git diff --quiet --cached --find-copies-harder --no-ext-diff $tree
+'
+
# In the tree, there is only path0/COPYING. In the cache, path0 and
# path1 both have COPYING and the latter is a copy of path0/COPYING.
# However when we say we care only about path1, we should just see
--
2.51.2
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-09 16:43 ` [PATCH v2] diff: disable rename detection with --quiet René Scharfe
@ 2025-11-09 17:34 ` D. Ben Knoble
2025-11-09 18:35 ` René Scharfe
2025-11-10 9:42 ` Phillip Wood
2025-11-10 17:54 ` Jeff King
1 sibling, 2 replies; 15+ messages in thread
From: D. Ben Knoble @ 2025-11-09 17:34 UTC (permalink / raw)
To: René Scharfe; +Cc: Git, Phillip Wood, Junio C Hamano, Jeff King
On Sun, Nov 9, 2025 at 11:43 AM René Scharfe <l.s.r@web.de> wrote:
>
> Detecting renames and copies improves diff's output. This effort is
> wasted if we don't show any. Disable detection in that case.
Indeed. I've confirmed this and v1 both fix the issue, although v2 is
significantly faster (which is great for the intended use in
git-prompt.sh!):
λ hyperfine -NiP v 1 2 ~/code/git/'buildv{v}/git diff --cached --quiet
--no-ext-diff --find-copies-harder'
Benchmark 1: /home/benknoble/code/git/buildv1/git diff --cached
--quiet --no-ext-diff --find-copies-harder
Time (mean ± σ): 72.0 ms ± 3.3 ms [User: 45.2 ms, System: 26.2 ms]
Range (min … max): 67.6 ms … 79.6 ms 42 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: /home/benknoble/code/git/buildv2/git diff --cached
--quiet --no-ext-diff --find-copies-harder
Time (mean ± σ): 19.9 ms ± 1.5 ms [User: 8.9 ms, System: 10.6 ms]
Range (min … max): 16.1 ms … 24.0 ms 151 runs
Warning: Ignoring non-zero exit code.
Summary
/home/benknoble/code/git/buildv2/git diff --cached --quiet
--no-ext-diff --find-copies-harder ran
3.61 ± 0.31 times faster than /home/benknoble/code/git/buildv1/git
diff --cached --quiet --no-ext-diff --find-copies-harder
> This actually fixes the error code when using the options --cached,
> --find-copies-harder, --no-ext-diff and --quiet together:
> run_diff_index() indirectly calls diff-lib.c::show_modified(), which
> queues even non-modified entries using diff_change() because we need
> them for copy detection. diff_change() sets flags.has_changes, though,
> which causes diff_can_quit_early() to declare we're done after seeing
> only the very first entry -- way too soon.
This does describe the behavior I saw, but it seems to me that, if we
have changes, then we ought to be able to quit early for --quiet, no?
So there's some other knock-on effect that causes quitting early to be
wrong here, and I'm not exactly sure what it is (other than the diff
queues being different sizes when we hit relevant parts of
diffcore_std, though it's the working case that has the larger queue).
So I'm having a hard time tying this paragraph to the actual issue
(mostly due to my complete unfamiliarity with the diffing subsystem).
> Using --cached, --find-copies-harder and --quiet together without
> --no-ext-diff was not affected even before, as it causes the flag
> flags.diff_from_contents to be set, which disables the optimization
> in a different way.
>
> Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
> Suggested-by: Phillip Wood <phillip.wood@dunelm.org.uk>
> Signed-off-by: René Scharfe <l.s.r@web.de>
> ---
> diff.c | 2 ++
> t/t4007-rename-3.sh | 10 ++++++++++
> 2 files changed, 12 insertions(+)
>
> diff --git a/diff.c b/diff.c
> index a1961526c0..efa8d9773c 100644
> --- a/diff.c
> +++ b/diff.c
> @@ -4987,6 +4987,8 @@ void diff_setup_done(struct diff_options *options)
> if (options->flags.quick) {
> options->output_format = DIFF_FORMAT_NO_OUTPUT;
> options->flags.exit_with_status = 1;
> + options->detect_rename = 0;
> + options->flags.find_copies_harder = 0;
> }
>
> /*
> diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh
> index e8faf0dd2e..3fc81bcd76 100755
> --- a/t/t4007-rename-3.sh
> +++ b/t/t4007-rename-3.sh
> @@ -41,6 +41,16 @@ test_expect_success 'copy detection, cached' '
> compare_diff_raw current expected
> '
>
> +test_expect_success 'exit code of quiet copy detection' '
> + test_expect_code 1 \
> + git diff --quiet --cached --find-copies-harder $tree
> +'
> +
> +test_expect_success 'exit code of quiet copy detection with --no-ext-diff' '
> + test_expect_code 1 \
> + git diff --quiet --cached --find-copies-harder --no-ext-diff $tree
> +'
> +
> # In the tree, there is only path0/COPYING. In the cache, path0 and
> # path1 both have COPYING and the latter is a copy of path0/COPYING.
> # However when we say we care only about path1, we should just see
> --
> 2.51.2
Covering both seems like the right move to me, thanks!
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-09 17:34 ` D. Ben Knoble
@ 2025-11-09 18:35 ` René Scharfe
2025-11-10 23:58 ` D. Ben Knoble
2025-11-10 9:42 ` Phillip Wood
1 sibling, 1 reply; 15+ messages in thread
From: René Scharfe @ 2025-11-09 18:35 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: Git, Phillip Wood, Junio C Hamano, Jeff King
On 11/9/25 6:34 PM, D. Ben Knoble wrote:
> On Sun, Nov 9, 2025 at 11:43 AM René Scharfe <l.s.r@web.de> wrote:
>>
>> This actually fixes the error code when using the options --cached,
>> --find-copies-harder, --no-ext-diff and --quiet together:
>> run_diff_index() indirectly calls diff-lib.c::show_modified(), which
>> queues even non-modified entries using diff_change() because we need
>> them for copy detection. diff_change() sets flags.has_changes, though,
>> which causes diff_can_quit_early() to declare we're done after seeing
>> only the very first entry -- way too soon.
>
> This does describe the behavior I saw, but it seems to me that, if we
> have changes, then we ought to be able to quit early for --quiet, no?
>
> So there's some other knock-on effect that causes quitting early to be
> wrong here, and I'm not exactly sure what it is (other than the diff
> queues being different sizes when we hit relevant parts of
> diffcore_std, though it's the working case that has the larger queue).
> So I'm having a hard time tying this paragraph to the actual issue
> (mostly due to my complete unfamiliarity with the diffing subsystem).
run_diff_index() calls diff-lib.c::diff_cache() to queue up index
entries. As mentioned above it only queues up the very first one, no
matter if it's a change or not. In Git's repo this would be
.cirrus.yml. That's not the end of it, yet, though. It then calls
diffcore_std(), which calls diffcore_rename() to remove non-changes
from the queue and overwrites flags.has_changes based on whether the
queue is empty now.
René
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-09 17:34 ` D. Ben Knoble
2025-11-09 18:35 ` René Scharfe
@ 2025-11-10 9:42 ` Phillip Wood
1 sibling, 0 replies; 15+ messages in thread
From: Phillip Wood @ 2025-11-10 9:42 UTC (permalink / raw)
To: D. Ben Knoble, René Scharfe
Cc: Git, Phillip Wood, Junio C Hamano, Jeff King
Hi Ben
On 09/11/2025 17:34, D. Ben Knoble wrote:
> On Sun, Nov 9, 2025 at 11:43 AM René Scharfe <l.s.r@web.de> wrote:
>>
> λ hyperfine -NiP v 1 2 ~/code/git/'buildv{v}/git diff --cached --quiet
> --no-ext-diff --find-copies-harder'
> Benchmark 1: /home/benknoble/code/git/buildv1/git diff --cached
> --quiet --no-ext-diff --find-copies-harder
> Time (mean ± σ): 72.0 ms ± 3.3 ms [User: 45.2 ms, System: 26.2 ms]
> Range (min … max): 67.6 ms … 79.6 ms 42 runs
>
> Warning: Ignoring non-zero exit code.
>
> Benchmark 2: /home/benknoble/code/git/buildv2/git diff --cached
> --quiet --no-ext-diff --find-copies-harder
> Time (mean ± σ): 19.9 ms ± 1.5 ms [User: 8.9 ms, System: 10.6 ms]
> Range (min … max): 16.1 ms … 24.0 ms 151 runs
>
> Warning: Ignoring non-zero exit code.
>
> Summary
> /home/benknoble/code/git/buildv2/git diff --cached --quiet
> --no-ext-diff --find-copies-harder ran
> 3.61 ± 0.31 times faster than /home/benknoble/code/git/buildv1/git
> diff --cached --quiet --no-ext-diff --find-copies-harder
That's a nice speedup. Thanks for sharing that - I knew in an abstract
way that "--find-copies-harder" slowed things down but seeing some
concrete numbers really brings it home.
Best Wishes
Phillip
>> This actually fixes the error code when using the options --cached,
>> --find-copies-harder, --no-ext-diff and --quiet together:
>> run_diff_index() indirectly calls diff-lib.c::show_modified(), which
>> queues even non-modified entries using diff_change() because we need
>> them for copy detection. diff_change() sets flags.has_changes, though,
>> which causes diff_can_quit_early() to declare we're done after seeing
>> only the very first entry -- way too soon.
>
> This does describe the behavior I saw, but it seems to me that, if we
> have changes, then we ought to be able to quit early for --quiet, no?
>
> So there's some other knock-on effect that causes quitting early to be
> wrong here, and I'm not exactly sure what it is (other than the diff
> queues being different sizes when we hit relevant parts of
> diffcore_std, though it's the working case that has the larger queue).
> So I'm having a hard time tying this paragraph to the actual issue
> (mostly due to my complete unfamiliarity with the diffing subsystem).
>
>> Using --cached, --find-copies-harder and --quiet together without
>> --no-ext-diff was not affected even before, as it causes the flag
>> flags.diff_from_contents to be set, which disables the optimization
>> in a different way.
>>
>> Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
>> Suggested-by: Phillip Wood <phillip.wood@dunelm.org.uk>
>> Signed-off-by: René Scharfe <l.s.r@web.de>
>> ---
>> diff.c | 2 ++
>> t/t4007-rename-3.sh | 10 ++++++++++
>> 2 files changed, 12 insertions(+)
>>
>> diff --git a/diff.c b/diff.c
>> index a1961526c0..efa8d9773c 100644
>> --- a/diff.c
>> +++ b/diff.c
>> @@ -4987,6 +4987,8 @@ void diff_setup_done(struct diff_options *options)
>> if (options->flags.quick) {
>> options->output_format = DIFF_FORMAT_NO_OUTPUT;
>> options->flags.exit_with_status = 1;
>> + options->detect_rename = 0;
>> + options->flags.find_copies_harder = 0;
>> }
>>
>> /*
>> diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh
>> index e8faf0dd2e..3fc81bcd76 100755
>> --- a/t/t4007-rename-3.sh
>> +++ b/t/t4007-rename-3.sh
>> @@ -41,6 +41,16 @@ test_expect_success 'copy detection, cached' '
>> compare_diff_raw current expected
>> '
>>
>> +test_expect_success 'exit code of quiet copy detection' '
>> + test_expect_code 1 \
>> + git diff --quiet --cached --find-copies-harder $tree
>> +'
>> +
>> +test_expect_success 'exit code of quiet copy detection with --no-ext-diff' '
>> + test_expect_code 1 \
>> + git diff --quiet --cached --find-copies-harder --no-ext-diff $tree
>> +'
>> +
>> # In the tree, there is only path0/COPYING. In the cache, path0 and
>> # path1 both have COPYING and the latter is a copy of path0/COPYING.
>> # However when we say we care only about path1, we should just see
>> --
>> 2.51.2
>
> Covering both seems like the right move to me, thanks!
>
> --
> D. Ben Knoble
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-09 16:43 ` [PATCH v2] diff: disable rename detection with --quiet René Scharfe
2025-11-09 17:34 ` D. Ben Knoble
@ 2025-11-10 17:54 ` Jeff King
2025-11-10 19:13 ` Junio C Hamano
2025-11-22 21:44 ` René Scharfe
1 sibling, 2 replies; 15+ messages in thread
From: Jeff King @ 2025-11-10 17:54 UTC (permalink / raw)
To: René Scharfe; +Cc: D. Ben Knoble, Git, Phillip Wood, Junio C Hamano
On Sun, Nov 09, 2025 at 05:43:36PM +0100, René Scharfe wrote:
> Detecting renames and copies improves diff's output. This effort is
> wasted if we don't show any. Disable detection in that case.
>
> This actually fixes the error code when using the options --cached,
> --find-copies-harder, --no-ext-diff and --quiet together:
> run_diff_index() indirectly calls diff-lib.c::show_modified(), which
> queues even non-modified entries using diff_change() because we need
> them for copy detection. diff_change() sets flags.has_changes, though,
> which causes diff_can_quit_early() to declare we're done after seeing
> only the very first entry -- way too soon.
>
> Using --cached, --find-copies-harder and --quiet together without
> --no-ext-diff was not affected even before, as it causes the flag
> flags.diff_from_contents to be set, which disables the optimization
> in a different way.
This makes sense to me, and I can't think of a reason why you would want
rename detection on if we're not going to show the results (and likewise
I can't think of a way that a rename result would affect has_changes).
I wonder if we should _also_ take the hunk from v1 that teaches
can_quit_early() to avoid triggering when copy detection is on. It's
probably redundant now, but it feels to me like that's the place where
the correctness check should kick in. And the patch here is just
optimizing out the unnecessary work, but also happens to align things
for correctness downstream.
But I dunno. Maybe a check for a condition that we think can never be
triggered becomes too confusing for later maintenance.
You don't say in the commit message when this bug started. I briefly
wondered if it was caused by the recent diff_from_contents stuff we've
been discussing. But it's the opposite here (the bug happens when we
_don't_ set diff_from_contents). And I think it goes all the way back to
b4194828dc (diff-index --quiet: learn the "stop feeding the backend
early" logic, 2011-05-31).
-Peff
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-10 17:54 ` Jeff King
@ 2025-11-10 19:13 ` Junio C Hamano
2025-11-22 21:44 ` René Scharfe
1 sibling, 0 replies; 15+ messages in thread
From: Junio C Hamano @ 2025-11-10 19:13 UTC (permalink / raw)
To: Jeff King; +Cc: René Scharfe, D. Ben Knoble, Git, Phillip Wood
Jeff King <peff@peff.net> writes:
> This makes sense to me, and I can't think of a reason why you would want
> rename detection on if we're not going to show the results (and likewise
> I can't think of a way that a rename result would affect has_changes).
>
> I wonder if we should _also_ take the hunk from v1 that teaches
> can_quit_early() to avoid triggering when copy detection is on. It's
> probably redundant now, but it feels to me like that's the place where
> the correctness check should kick in. And the patch here is just
> optimizing out the unnecessary work, but also happens to align things
> for correctness downstream.
Concurred on both counts.
> You don't say in the commit message when this bug started. I briefly
> wondered if it was caused by the recent diff_from_contents stuff we've
> been discussing. But it's the opposite here (the bug happens when we
> _don't_ set diff_from_contents). And I think it goes all the way back to
> b4194828dc (diff-index --quiet: learn the "stop feeding the backend
> early" logic, 2011-05-31).
Yup, I think so. Back then I think our assumptions are that the
user knows better than giving complex diffcore requests like
find-copies-harder only to discard the results with --quiet, and the
patch started this thread helps other users, which is good ;-).
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-09 18:35 ` René Scharfe
@ 2025-11-10 23:58 ` D. Ben Knoble
0 siblings, 0 replies; 15+ messages in thread
From: D. Ben Knoble @ 2025-11-10 23:58 UTC (permalink / raw)
To: René Scharfe; +Cc: Git, Phillip Wood, Junio C Hamano, Jeff King
On Sun, Nov 9, 2025 at 1:35 PM René Scharfe <l.s.r@web.de> wrote:
>
> On 11/9/25 6:34 PM, D. Ben Knoble wrote:
> > On Sun, Nov 9, 2025 at 11:43 AM René Scharfe <l.s.r@web.de> wrote:
> >>
> >> This actually fixes the error code when using the options --cached,
> >> --find-copies-harder, --no-ext-diff and --quiet together:
> >> run_diff_index() indirectly calls diff-lib.c::show_modified(), which
> >> queues even non-modified entries using diff_change() because we need
> >> them for copy detection. diff_change() sets flags.has_changes, though,
> >> which causes diff_can_quit_early() to declare we're done after seeing
> >> only the very first entry -- way too soon.
> >
> > This does describe the behavior I saw, but it seems to me that, if we
> > have changes, then we ought to be able to quit early for --quiet, no?
> >
> > So there's some other knock-on effect that causes quitting early to be
> > wrong here, and I'm not exactly sure what it is (other than the diff
> > queues being different sizes when we hit relevant parts of
> > diffcore_std, though it's the working case that has the larger queue).
> > So I'm having a hard time tying this paragraph to the actual issue
> > (mostly due to my complete unfamiliarity with the diffing subsystem).
>
> run_diff_index() calls diff-lib.c::diff_cache() to queue up index
> entries. As mentioned above it only queues up the very first one, no
> matter if it's a change or not. In Git's repo this would be
> .cirrus.yml. That's not the end of it, yet, though. It then calls
> diffcore_std(), which calls diffcore_rename() to remove non-changes
> from the queue and overwrites flags.has_changes based on whether the
> queue is empty now.
>
> René
>
Thanks, btw. Still try to absorb this part of the code, but this helps :)
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-10 17:54 ` Jeff King
2025-11-10 19:13 ` Junio C Hamano
@ 2025-11-22 21:44 ` René Scharfe
2025-11-23 7:09 ` Junio C Hamano
1 sibling, 1 reply; 15+ messages in thread
From: René Scharfe @ 2025-11-22 21:44 UTC (permalink / raw)
To: Jeff King; +Cc: D. Ben Knoble, Git, Phillip Wood, Junio C Hamano
On 11/10/25 6:54 PM, Jeff King wrote:
>
> I wonder if we should _also_ take the hunk from v1 that teaches
> can_quit_early() to avoid triggering when copy detection is on. It's
> probably redundant now, but it feels to me like that's the place where
> the correctness check should kick in. And the patch here is just
> optimizing out the unnecessary work, but also happens to align things
> for correctness downstream.
>
> But I dunno. Maybe a check for a condition that we think can never be
> triggered becomes too confusing for later maintenance.
That check was only necessary because we queue unchanged filepairs for
rename detection as if they were changes. With --quiet forcing rename
detection off we won't run into this anymore, but it still feels like a
trip hazard. Adding a check would help, but we could also stop doing
that in the first place. Patch below.
René
--- >8 ---
Subject: [PATCH] diff-index: don't queue unchanged filepairs with diff_change()
diff_cache() queues unchanged filepairs if the flag find_copies_harder
is set, and uses diff_change() for that. This function does a few
things that are unnecessary for unchanged filepairs and always sets the
diff_flag has_changes, which is simply misleading in this case.
Add a new streamlined function for queuing unchanged filepairs and
use it in show_modified(), which is called by diff_cache() via
oneway_diff() and do_oneway_diff(). It allocates only one half of each
filepair, which has a measurable effect if there are a lot of them, like
in the Linux repo:
Benchmark 1: ./git_v2.52.0 -C ../linux diff --cached --find-copies-harder
Time (mean ± σ): 31.8 ms ± 0.2 ms [User: 24.2 ms, System: 6.3 ms]
Range (min … max): 31.5 ms … 32.3 ms 85 runs
Benchmark 2: ./git -C ../linux diff --cached --find-copies-harder
Time (mean ± σ): 23.9 ms ± 0.2 ms [User: 18.1 ms, System: 4.6 ms]
Range (min … max): 23.5 ms … 24.4 ms 111 runs
Summary
./git -C ../linux diff --cached --find-copies-harder ran
1.33 ± 0.01 times faster than ./git_v2.52.0 -C ../linux diff --cached --find-copies-harder
Signed-off-by: René Scharfe <l.s.r@web.de>
---
diff-lib.c | 13 ++++++-------
diff.c | 20 ++++++++++++++++++++
diff.h | 5 +++++
3 files changed, 31 insertions(+), 7 deletions(-)
diff --git a/diff-lib.c b/diff-lib.c
index b8f8f3bc31..8e624f38c6 100644
--- a/diff-lib.c
+++ b/diff-lib.c
@@ -418,13 +418,12 @@ static int show_modified(struct rev_info *revs,
}
oldmode = old_entry->ce_mode;
- if (mode == oldmode && oideq(oid, &old_entry->oid) && !dirty_submodule &&
- !revs->diffopt.flags.find_copies_harder)
- return 0;
-
- diff_change(&revs->diffopt, oldmode, mode,
- &old_entry->oid, oid, 1, !is_null_oid(oid),
- old_entry->name, 0, dirty_submodule);
+ if (mode != oldmode || !oideq(oid, &old_entry->oid) || dirty_submodule)
+ diff_change(&revs->diffopt, oldmode, mode,
+ &old_entry->oid, oid, 1, !is_null_oid(oid),
+ old_entry->name, 0, dirty_submodule);
+ else if (revs->diffopt.flags.find_copies_harder)
+ diff_same(&revs->diffopt, mode, oid, old_entry->name);
return 0;
}
diff --git a/diff.c b/diff.c
index efa8d9773c..e2a2927f8c 100644
--- a/diff.c
+++ b/diff.c
@@ -7349,6 +7349,26 @@ void diff_change(struct diff_options *options,
concatpath, old_dirty_submodule, new_dirty_submodule);
}
+void diff_same(struct diff_options *options,
+ unsigned mode,
+ const struct object_id *oid,
+ const char *concatpath)
+{
+ struct diff_filespec *one;
+
+ if (S_ISGITLINK(mode) && is_submodule_ignored(concatpath, options))
+ return;
+
+ if (options->prefix &&
+ strncmp(concatpath, options->prefix, options->prefix_length))
+ return;
+
+ one = alloc_filespec(concatpath);
+ fill_filespec(one, oid, 1, mode);
+ one->count++;
+ diff_queue(&diff_queued_diff, one, one);
+}
+
struct diff_filepair *diff_unmerge(struct diff_options *options, const char *path)
{
struct diff_filepair *pair;
diff --git a/diff.h b/diff.h
index 31eedd5c0c..e80503aebb 100644
--- a/diff.h
+++ b/diff.h
@@ -572,6 +572,11 @@ void diff_change(struct diff_options *,
const char *fullpath,
unsigned dirty_submodule1, unsigned dirty_submodule2);
+void diff_same(struct diff_options *,
+ unsigned mode,
+ const struct object_id *oid,
+ const char *fullpath);
+
struct diff_filepair *diff_unmerge(struct diff_options *, const char *path);
void compute_diffstat(struct diff_options *options, struct diffstat_t *diffstat,
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v2] diff: disable rename detection with --quiet
2025-11-22 21:44 ` René Scharfe
@ 2025-11-23 7:09 ` Junio C Hamano
0 siblings, 0 replies; 15+ messages in thread
From: Junio C Hamano @ 2025-11-23 7:09 UTC (permalink / raw)
To: René Scharfe; +Cc: Jeff King, D. Ben Knoble, Git, Phillip Wood
René Scharfe <l.s.r@web.de> writes:
> --- >8 ---
> Subject: [PATCH] diff-index: don't queue unchanged filepairs with diff_change()
>
> diff_cache() queues unchanged filepairs if the flag find_copies_harder
> is set, and uses diff_change() for that. This function does a few
> things that are unnecessary for unchanged filepairs and always sets the
> diff_flag has_changes, which is simply misleading in this case.
>
> Add a new streamlined function for queuing unchanged filepairs and
> use it in show_modified(), which is called by diff_cache() via
> oneway_diff() and do_oneway_diff(). It allocates only one half of each
> filepair, ...
It's a misleading thing to say. It allocates a full filepair, but
because a filespec is reference counted, it can reuse the same
filespec to hold both preimage and postimage, halving the memory
requirement without leading to double freeing. And having a
separete helper do so would make it almost trivial to avoid setting
the has_changes bit.
Cleverly done.
Thanks.
> ... which has a measurable effect if there are a lot of them, like
> in the Linux repo:
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2025-11-23 7:09 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-08 19:05 diff --cached --no-ext-diff --find-copies-harder --quiet exits with wrong status code D. Ben Knoble
2025-11-08 19:08 ` D. Ben Knoble
2025-11-08 19:12 ` D. Ben Knoble
2025-11-09 12:11 ` [PATCH] diff: disabled quick optimization with --find-copies-harder René Scharfe
2025-11-09 14:18 ` Phillip Wood
2025-11-09 16:43 ` René Scharfe
2025-11-09 16:43 ` [PATCH v2] diff: disable rename detection with --quiet René Scharfe
2025-11-09 17:34 ` D. Ben Knoble
2025-11-09 18:35 ` René Scharfe
2025-11-10 23:58 ` D. Ben Knoble
2025-11-10 9:42 ` Phillip Wood
2025-11-10 17:54 ` Jeff King
2025-11-10 19:13 ` Junio C Hamano
2025-11-22 21:44 ` René Scharfe
2025-11-23 7:09 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).