{"thread":{"id":"65399","subject":"BUG: git commit -a crashes with \"unable to stat\" during unresolved merge","startedAt":"2026-03-31T21:53:28Z","lastAt":"2026-04-01T19:08:53Z","messageCount":5,"participants":["Nick Golden","blindmansion","Junio C Hamano","Blind Mansion"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"540567","messageId":"CAF6hZH5TyFBm5H_fcVyaf1aw-mPsVbAmNvkUGCMoQYYCX1+HRQ@mail.gmail.com","threadId":"65399","inReplyTo":null,"subject":"BUG: git commit -a crashes with \"unable to stat\" during unresolved merge","fromName":"Nick Golden","fromEmail":"nreesegolden@gmail.com","sentAt":"2026-03-31T21:53:16Z","receivedAt":"2026-03-31T21:53:28Z","isPatch":false,"body":"Hello,\n\nI found a reproducible bug in `git commit -a`.\n\nWith an unresolved merge conflict present, if a different tracked file\nhas been deleted from the working tree, `git commit -a` can crash\nwith:\n\n    fatal: unable to stat 'bystander.txt': No such file or directory\n\nI can reproduce this reliably with Git 2.53.0 on macOS in a fresh repository.\n\nTo reproduce:\n\n1. Create two tracked files with identical content.\n2. Create a merge conflict on one of them.\n3. Delete the other tracked file from the working tree.\n4. Run `git commit -a -m test`.\n\nExpected:\n\nGit should either stage the deletion and then stop because of the unresolved\nmerge, or refuse the commit because of the unresolved merge, but not\ncrash trying to stat the deleted path.\n\nActual:\n\nGit aborts with:\n\n    fatal: unable to stat 'bystander.txt': No such file or directory\n\nReproducer script follows.\n\nPossible cause:\n\nThis appears to involve rename detection during `add_files_to_cache()`: the\ndeleted file gets paired with the unmerged path, and a later index update tries\nto `stat()` the deleted path.\n\nThanks,\n\nNick Golden\nnreesegolden@gmail.com\n\n---8<---\n#!/bin/sh\nset -eu\n\nrepro_dir=\"$(mktemp -d \"${TMPDIR:-/tmp}/git-commit-a-rename-crash.XXXXXX\")\"\necho \"Working in: $repro_dir\"\ncd \"$repro_dir\"\n\ngit init -b main\ngit config user.name \"Test User\"\ngit config user.email \"test@example.com\"\n\ni=1\nwhile [ \"$i\" -le 100 ]; do\n    printf 'line %s: shared content that is identical across both files\\n' \"$i\"\n    i=$((i + 1))\ndone > conflict.txt\n\ncp conflict.txt bystander.txt\n\ngit add conflict.txt bystander.txt\ngit commit -m \"initial\"\n\ngit checkout -b feature\nperl -0pi -e 's/line 50:.*$/line 50: FEATURE BRANCH CHANGE/m' conflict.txt\ngit add conflict.txt\ngit commit -m \"feature\"\n\ngit checkout main\nperl -0pi -e 's/line 50:.*$/line 50: MAIN BRANCH CHANGE/m' conflict.txt\ngit add conflict.txt\ngit commit -m \"main\"\n\ngit merge feature || true\nrm bystander.txt\n\nset +e\noutput=\"$(git commit -a -m test 2>&1)\"\nstatus=$?\nset -e\n\nprintf '%s\\n' \"$output\"\nprintf 'exit status: %s\\n' \"$status\"\n\ncase \"$output\" in\n    *\"fatal: unable to stat 'bystander.txt': No such file or directory\"*)\n        echo \"Bug reproduced.\"\n        exit 0\n        ;;\n    *)\n        echo \"Did not reproduce the expected failure.\"\n        exit 1\n        ;;\nesac\n"},{"id":"540656","messageId":"20260401170502.35877-1-blindmansion@gmail.com","threadId":"65399","inReplyTo":"CAF6hZH5TyFBm5H_fcVyaf1aw-mPsVbAmNvkUGCMoQYYCX1+HRQ@mail.gmail.com","subject":"[PATCH] read-cache: disable renames in add_files_to_cache","fromName":"blindmansion","fromEmail":"blindmansion@gmail.com","sentAt":"2026-04-01T17:05:02Z","receivedAt":"2026-04-01T17:12:48Z","isPatch":true,"body":"add_files_to_cache() refreshes the index from worktree changes and does\nnot need rename detection. When unmerged entries and a deleted stage-0\npath are present together, rename detection can pair them and rewrite an\nunmerged diff pair to point at the deleted path.\n\nThat later makes \"git commit -a\" and \"git add -u\" try to stat the\ndeleted path and die with \"unable to stat\". Disable rename detection in\nthis callback-driven staging path and add a regression test covering the\ncrash.\n---\n read-cache.c          |  1 +\n t/t2200-add-update.sh | 39 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 40 insertions(+)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 5049f9b..d938abc 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4049,6 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \trev.diffopt.format_callback = update_callback;\n \trev.diffopt.format_callback_data = &data;\n \trev.diffopt.flags.override_submodule_config = 1;\n+\trev.diffopt.detect_rename = 0; /* staging worktree changes does not need renames */\n \trev.max_count = 0; /* do not compare unmerged paths with stage #2 */\n \n \t/*\ndiff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\nindex 06e83d3..56c7e55 100755\n--- a/t/t2200-add-update.sh\n+++ b/t/t2200-add-update.sh\n@@ -200,6 +200,45 @@ test_expect_success 'add -u resolves unmerged paths' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n+\ttest_create_repo rename-crash &&\n+\t(\n+\t\tcd rename-crash &&\n+\t\ttest_seq 1 100 |\n+\t\tsed \"s/.*/line &: shared content that is identical across both files/\" >conflict.txt &&\n+\t\tcp conflict.txt bystander.txt &&\n+\t\tgit add conflict.txt bystander.txt &&\n+\t\tgit commit -m \"initial: two files with identical content\" &&\n+\t\tmain_branch=$(git symbolic-ref --short HEAD) &&\n+\t\tgit checkout -b feature &&\n+\t\tperl -pe '\\''s/^line 50:.*/line 50: FEATURE BRANCH CHANGE/'\\'' \\\n+\t\t\tconflict.txt >conflict.txt.tmp &&\n+\t\tmv conflict.txt.tmp conflict.txt &&\n+\t\tgit add conflict.txt &&\n+\t\tgit commit -m \"feature: modify line 50\" &&\n+\t\tgit checkout \"$main_branch\" &&\n+\t\tperl -pe '\\''s/^line 50:.*/line 50: MAIN BRANCH CHANGE/'\\'' \\\n+\t\t\tconflict.txt >conflict.txt.tmp &&\n+\t\tmv conflict.txt.tmp conflict.txt &&\n+\t\tgit add conflict.txt &&\n+\t\tgit commit -m \"main: modify line 50 differently\" &&\n+\t\ttest_must_fail git merge feature &&\n+\t\trm bystander.txt &&\n+\t\tgit add -u >out 2>err &&\n+\t\ttest_must_be_empty out &&\n+\t\ttest_must_be_empty err &&\n+\t\tgit ls-files -u >actual &&\n+\t\ttest_must_be_empty actual &&\n+\t\tgit ls-files bystander.txt >actual &&\n+\t\ttest_must_be_empty actual &&\n+\t\techo conflict.txt >expect &&\n+\t\tgit ls-files conflict.txt >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit diff-files --name-only >actual &&\n+\t\ttest_must_be_empty actual\n+\t)\n+'\n+\n test_expect_success '\"add -u non-existent\" should fail' '\n \ttest_must_fail git add -u non-existent &&\n \tgit ls-files >actual &&\n-- \n2.53.0\n\n"},{"id":"540659","messageId":"xmqqy0j6v94q.fsf@gitster.g","threadId":"65399","inReplyTo":"20260401170502.35877-1-blindmansion@gmail.com","subject":"Re: [PATCH] read-cache: disable renames in add_files_to_cache","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-01T18:16:37Z","receivedAt":"2026-04-01T18:16:40Z","isPatch":true,"body":"blindmansion <blindmansion@gmail.com> writes:\n\n> add_files_to_cache() refreshes the index from worktree changes and does\n> not need rename detection. When unmerged entries and a deleted stage-0\n> path are present together, rename detection can pair them and rewrite an\n> unmerged diff pair to point at the deleted path.\n>\n> That later makes \"git commit -a\" and \"git add -u\" try to stat the\n> deleted path and die with \"unable to stat\". Disable rename detection in\n> this callback-driven staging path and add a regression test covering the\n> crash.\n> ---\n\nThanks for trying to help.\n\n * Documentation/SubmittingPatches::[[sign-off]]\n * Documentation/SubmittingPatches::[[real-name]]\n\nOther than that, the contents of the proposed log message looks\nperfect, which is rarely seen in a patch from somebody whose patch\nwe haven't seen around here yet.\n\n> diff --git a/read-cache.c b/read-cache.c\n> index 5049f9b..d938abc 100644\n> --- a/read-cache.c\n> +++ b/read-cache.c\n> @@ -4049,6 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n>  \trev.diffopt.format_callback = update_callback;\n>  \trev.diffopt.format_callback_data = &data;\n>  \trev.diffopt.flags.override_submodule_config = 1;\n> +\trev.diffopt.detect_rename = 0; /* staging worktree changes does not need renames */\n>  \trev.max_count = 0; /* do not compare unmerged paths with stage #2 */\n>  \n\nThe change itself looks sensible, but it makes me wonder if there\nare other very similar glitches lying around.\n\n> diff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\n> index 06e83d3..56c7e55 100755\n> --- a/t/t2200-add-update.sh\n> +++ b/t/t2200-add-update.sh\n> @@ -200,6 +200,45 @@ test_expect_success 'add -u resolves unmerged paths' '\n>  \ttest_cmp expect actual\n>  '\n\nI didn't see too many questionable steps in the new test, other than\nthe mixed use of \"perl\" plus \"sed\".\n\n - Can't we do this without relying on \"perl\"?  Also, why does those\n   invocations of Perl insist on using single quotes, which is more\n   awkward to write in our test framework, where double quotes should\n   work equally well?\n\n - Can't the sample line be shortened so that patch fits comfortably\n   on 80-column terminals?\n\n - Insisting on \"add -u\" showing nothing on its standard error\n   stream makes the test more fragile than it is necessary.  We\n   cannot dictate that nobody in the future will add progress\n   eye-candy to the codepath and some folks run the test on overly\n   loaded machine.  Checking that the command exits with status 0 is\n   a good thing to do (which is already done here) and should be\n   sufficient in this case.\n\n - Wouldn't running a single \"git ls-files\" with pathspec and\n   comparing it with expected list of contents in the index make it\n   more clear what is being tested and expected in the last part of\n   the test where we see no bystander.txt is mentioned and\n   conflict.txt is mentioned there?  I.e., something like\n\n\t\tgit ls-files bystander.txt conflict.txt >actual &&\n\t\tcat >expect <<\\-EOF &&\n\t\tconflict.txt\n\t\tEOF\n\t\ttest_cmp expect actual\n\n> +test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n> +\ttest_create_repo rename-crash &&\n> +\t(\n> +\t\tcd rename-crash &&\n> +\t\ttest_seq 1 100 |\n> +\t\tsed \"s/.*/line &: shared content that is identical across both files/\" >conflict.txt &&\n> +\t\tcp conflict.txt bystander.txt &&\n> +\t\tgit add conflict.txt bystander.txt &&\n> +\t\tgit commit -m \"initial: two files with identical content\" &&\n> +\t\tmain_branch=$(git symbolic-ref --short HEAD) &&\n> +\t\tgit checkout -b feature &&\n> +\t\tperl -pe '\\''s/^line 50:.*/line 50: FEATURE BRANCH CHANGE/'\\'' \\\n> +\t\t\tconflict.txt >conflict.txt.tmp &&\n> +\t\tmv conflict.txt.tmp conflict.txt &&\n> +\t\tgit add conflict.txt &&\n> +\t\tgit commit -m \"feature: modify line 50\" &&\n> +\t\tgit checkout \"$main_branch\" &&\n> +\t\tperl -pe '\\''s/^line 50:.*/line 50: MAIN BRANCH CHANGE/'\\'' \\\n> +\t\t\tconflict.txt >conflict.txt.tmp &&\n> +\t\tmv conflict.txt.tmp conflict.txt &&\n> +\t\tgit add conflict.txt &&\n> +\t\tgit commit -m \"main: modify line 50 differently\" &&\n> +\t\ttest_must_fail git merge feature &&\n> +\t\trm bystander.txt &&\n> +\t\tgit add -u >out 2>err &&\n> +\t\ttest_must_be_empty out &&\n> +\t\ttest_must_be_empty err &&\n> +\t\tgit ls-files -u >actual &&\n> +\t\ttest_must_be_empty actual &&\n> +\t\tgit ls-files bystander.txt >actual &&\n> +\t\ttest_must_be_empty actual &&\n> +\t\techo conflict.txt >expect &&\n> +\t\tgit ls-files conflict.txt >actual &&\n> +\t\ttest_cmp expect actual &&\n> +\t\tgit diff-files --name-only >actual &&\n> +\t\ttest_must_be_empty actual\n> +\t)\n> +'\n> +\n>  test_expect_success '\"add -u non-existent\" should fail' '\n>  \ttest_must_fail git add -u non-existent &&\n>  \tgit ls-files >actual &&\n"},{"id":"540662","messageId":"20260401190033.11325-1-blindmansion@gmail.com","threadId":"65399","inReplyTo":"CAF6hZH5TyFBm5H_fcVyaf1aw-mPsVbAmNvkUGCMoQYYCX1+HRQ@mail.gmail.com","subject":"[PATCH v2] read-cache: disable renames in add_files_to_cache","fromName":"Nick Golden","fromEmail":"blindmansion@gmail.com","sentAt":"2026-04-01T19:00:33Z","receivedAt":"2026-04-01T19:00:40Z","isPatch":true,"body":"add_files_to_cache() refreshes the index from worktree changes and does\nnot need rename detection. When unmerged entries and a deleted stage-0\npath are present together, rename detection can pair them and rewrite an\nunmerged diff pair to point at the deleted path.\n\nThat later makes \"git commit -a\" and \"git add -u\" try to stat the\ndeleted path and die with \"unable to stat\". Disable rename detection in\nthis callback-driven staging path and add a regression test covering the\ncrash.\n\nSigned-off-by: Nick Golden <blindmansion@gmail.com>\n---\nChanges since v1:\n- use sed instead of perl in the regression test\n- shorten the sample line content\n- stop asserting that stderr is empty\n- simplify the final ls-files check\n- use real name and add Signed-off-by\n\n read-cache.c          |  1 +\n t/t2200-add-update.sh | 38 ++++++++++++++++++++++++++++++++++++++\n 2 files changed, 39 insertions(+)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 5049f9b..d938abc 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4049,6 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \trev.diffopt.format_callback = update_callback;\n \trev.diffopt.format_callback_data = &data;\n \trev.diffopt.flags.override_submodule_config = 1;\n+\trev.diffopt.detect_rename = 0; /* staging worktree changes does not need renames */\n \trev.max_count = 0; /* do not compare unmerged paths with stage #2 */\n \n \t/*\ndiff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\nindex 06e83d3..0a96655 100755\n--- a/t/t2200-add-update.sh\n+++ b/t/t2200-add-update.sh\n@@ -200,6 +200,44 @@ test_expect_success 'add -u resolves unmerged paths' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n+\ttest_create_repo rename-crash &&\n+\t(\n+\t\tcd rename-crash &&\n+\t\ttest_seq 1 100 |\n+\t\tsed \"s/.*/line &: same text/\" >conflict.txt &&\n+\t\tcp conflict.txt bystander.txt &&\n+\t\tgit add conflict.txt bystander.txt &&\n+\t\tgit commit -m \"initial: two files with identical content\" &&\n+\t\tmain_branch=$(git symbolic-ref --short HEAD) &&\n+\t\tgit checkout -b feature &&\n+\t\tsed \"s/^line 50:.*/line 50: FEATURE/\" \\\n+\t\t\tconflict.txt >conflict.txt.tmp &&\n+\t\tmv conflict.txt.tmp conflict.txt &&\n+\t\tgit add conflict.txt &&\n+\t\tgit commit -m \"feature: modify line 50\" &&\n+\t\tgit checkout \"$main_branch\" &&\n+\t\tsed \"s/^line 50:.*/line 50: MAIN/\" \\\n+\t\t\tconflict.txt >conflict.txt.tmp &&\n+\t\tmv conflict.txt.tmp conflict.txt &&\n+\t\tgit add conflict.txt &&\n+\t\tgit commit -m \"main: modify line 50 differently\" &&\n+\t\ttest_must_fail git merge feature &&\n+\t\trm bystander.txt &&\n+\t\tgit add -u >out &&\n+\t\ttest_must_be_empty out &&\n+\t\tgit ls-files -u >actual &&\n+\t\ttest_must_be_empty actual &&\n+\t\tgit ls-files bystander.txt conflict.txt >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\tconflict.txt\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit diff-files --name-only >actual &&\n+\t\ttest_must_be_empty actual\n+\t)\n+'\n+\n test_expect_success '\"add -u non-existent\" should fail' '\n \ttest_must_fail git add -u non-existent &&\n \tgit ls-files >actual &&\n-- \n2.53.0\n\n"},{"id":"540663","messageId":"CAEu6FALj=789M=gASYpWT9CGC4_802-X=9sGoF=YndFRCgcb1w@mail.gmail.com","threadId":"65399","inReplyTo":"20260401190033.11325-1-blindmansion@gmail.com","subject":"Re: [PATCH v2] read-cache: disable renames in add_files_to_cache","fromName":"Blind Mansion","fromEmail":"blindmansion@gmail.com","sentAt":"2026-04-01T19:08:38Z","receivedAt":"2026-04-01T19:08:53Z","isPatch":true,"body":"Thanks for the review. I encountered this bug while doing differential\ntesting against another implementation, and enjoyed learning more about\nthis process. I have sent v2 with the metadata fix and test\ncleanups you noted.\n\nI also audited nearby callback-driven index refresh/staging paths and did\nnot find another obvious instance of this specific pattern outside\nadd_files_to_cache().\n\n\nOn Wed, Apr 1, 2026 at 3:00 PM Nick Golden <blindmansion@gmail.com> wrote:\n>\n> add_files_to_cache() refreshes the index from worktree changes and does\n> not need rename detection. When unmerged entries and a deleted stage-0\n> path are present together, rename detection can pair them and rewrite an\n> unmerged diff pair to point at the deleted path.\n>\n> That later makes \"git commit -a\" and \"git add -u\" try to stat the\n> deleted path and die with \"unable to stat\". Disable rename detection in\n> this callback-driven staging path and add a regression test covering the\n> crash.\n>\n> Signed-off-by: Nick Golden <blindmansion@gmail.com>\n> ---\n> Changes since v1:\n> - use sed instead of perl in the regression test\n> - shorten the sample line content\n> - stop asserting that stderr is empty\n> - simplify the final ls-files check\n> - use real name and add Signed-off-by\n>\n>  read-cache.c          |  1 +\n>  t/t2200-add-update.sh | 38 ++++++++++++++++++++++++++++++++++++++\n>  2 files changed, 39 insertions(+)\n>\n> diff --git a/read-cache.c b/read-cache.c\n> index 5049f9b..d938abc 100644\n> --- a/read-cache.c\n> +++ b/read-cache.c\n> @@ -4049,6 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n>         rev.diffopt.format_callback = update_callback;\n>         rev.diffopt.format_callback_data = &data;\n>         rev.diffopt.flags.override_submodule_config = 1;\n> +       rev.diffopt.detect_rename = 0; /* staging worktree changes does not need renames */\n>         rev.max_count = 0; /* do not compare unmerged paths with stage #2 */\n>\n>         /*\n> diff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\n> index 06e83d3..0a96655 100755\n> --- a/t/t2200-add-update.sh\n> +++ b/t/t2200-add-update.sh\n> @@ -200,6 +200,44 @@ test_expect_success 'add -u resolves unmerged paths' '\n>         test_cmp expect actual\n>  '\n>\n> +test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n> +       test_create_repo rename-crash &&\n> +       (\n> +               cd rename-crash &&\n> +               test_seq 1 100 |\n> +               sed \"s/.*/line &: same text/\" >conflict.txt &&\n> +               cp conflict.txt bystander.txt &&\n> +               git add conflict.txt bystander.txt &&\n> +               git commit -m \"initial: two files with identical content\" &&\n> +               main_branch=$(git symbolic-ref --short HEAD) &&\n> +               git checkout -b feature &&\n> +               sed \"s/^line 50:.*/line 50: FEATURE/\" \\\n> +                       conflict.txt >conflict.txt.tmp &&\n> +               mv conflict.txt.tmp conflict.txt &&\n> +               git add conflict.txt &&\n> +               git commit -m \"feature: modify line 50\" &&\n> +               git checkout \"$main_branch\" &&\n> +               sed \"s/^line 50:.*/line 50: MAIN/\" \\\n> +                       conflict.txt >conflict.txt.tmp &&\n> +               mv conflict.txt.tmp conflict.txt &&\n> +               git add conflict.txt &&\n> +               git commit -m \"main: modify line 50 differently\" &&\n> +               test_must_fail git merge feature &&\n> +               rm bystander.txt &&\n> +               git add -u >out &&\n> +               test_must_be_empty out &&\n> +               git ls-files -u >actual &&\n> +               test_must_be_empty actual &&\n> +               git ls-files bystander.txt conflict.txt >actual &&\n> +               cat >expect <<-\\EOF &&\n> +               conflict.txt\n> +               EOF\n> +               test_cmp expect actual &&\n> +               git diff-files --name-only >actual &&\n> +               test_must_be_empty actual\n> +       )\n> +'\n> +\n>  test_expect_success '\"add -u non-existent\" should fail' '\n>         test_must_fail git add -u non-existent &&\n>         git ls-files >actual &&\n> --\n> 2.53.0\n>\n"}]}