{"thread":{"id":"66440","subject":"[BUG] merge-ort: --no-overwrite-ignore overwrites an ignored file at a directory-rename destination","startedAt":"2026-10-01T23:54:24Z","lastAt":"2026-10-01T23:54:24Z","messageCount":1,"participants":["Coy Geek"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"553878","messageId":"CACgTecNrMKHGexWTkm1bqpVzL7SVWD5Hwo17wVokVGe3_Rnx0A@mail.gmail.com","threadId":"66440","inReplyTo":null,"subject":"[BUG] merge-ort: --no-overwrite-ignore overwrites an ignored file at a directory-rename destination","fromName":"Coy Geek","fromEmail":"coygeek@gmail.com","sentAt":"2026-10-01T23:54:12Z","receivedAt":"2026-10-01T23:54:24Z","isPatch":false,"body":"Hello,\n\nConsider the following ...\n\n`git merge --no-overwrite-ignore` is documented to abort rather than\noverwrite ignored files. When directory rename detection moves a file\nthat was added on the current branch into a path that holds an ignored,\nuntracked file, the merge replaces that file's contents without any\nwarning.\n\nWith merge.directoryRenames=true the merge reports success (exit 0).\nWith the default, merge.directoryRenames=conflict, it stops with a \"file\nlocation\" conflict, but the ignored file has already been overwritten. A\ndirect collision with the same ignored path is correctly refused, so the\nprotection is bypassed only for the destination path that rename\ndetection generates.\n\nWhat did you do before the bug happened? (Steps to reproduce your issue)\n\nSave the script below as repro.sh and run it as \"sh repro.sh true\", \"sh\nrepro.sh conflict\", or \"sh repro.sh default\" (no -c override). It works\nin a throwaway repository with empty global and system configuration.\n\n    #!/bin/sh\n    set -eu\n    mode=${1:-true}\n    unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR\nGIT_CONFIG_PARAMETERS\n    export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null\n    tmp=$(mktemp -d)\n    cd \"$tmp\"\n    git init -q -b main repo\n    cd repo\n    git config user.name Example\n    git config user.email example@example.invalid\n    echo new/private.dat >.gitignore\n    mkdir old\n    echo seed >old/seed\n    git add .gitignore old/seed\n    git commit -qm base\n    git checkout -qb incoming\n    git mv old new\n    git commit -qm 'rename old/ to new/'\n    git checkout -q main\n    echo 'committed local bytes' >old/private.dat\n    git add old/private.dat\n    git commit -qm 'add old/private.dat'\n    mkdir new\n    echo 'unique ignored bytes' >new/private.dat\n    git status --porcelain --ignored\n    git version\n    status=0\n    if [ \"$mode\" = default ]; then set -- ; else set -- -c\nmerge.directoryRenames=\"$mode\"; fi\n    git \"$@\" merge --no-edit --no-overwrite-ignore incoming || status=$?\n    echo \"merge exit status: $status\"\n    echo \"new/private.dat now contains: $(cat new/private.dat)\"\n\nBefore the merge, `git status --porcelain --ignored` reports only \"!!\nnew/\": the worktree is clean, and new/private.dat is ignored and\nuntracked.\n\nWhat did you expect to happen? (Expected behavior)\n\nAs for any other merge result that would overwrite an ignored file under\n--no-overwrite-ignore, Git refuses before touching the worktree, names\nnew/private.dat, and leaves its contents as \"unique ignored bytes\".\n\nWhat happened instead? (Actual behavior)\n\nWith merge.directoryRenames=true:\n\n    Path updated: old/private.dat added in HEAD inside a directory\nthat was renamed in incoming; moving it to new/private.dat.\n    Merge made by the 'ort' strategy.\n     {old => new}/private.dat | 0\n     {old => new}/seed        | 0\n     2 files changed, 0 insertions(+), 0 deletions(-)\n     rename {old => new}/private.dat (100%)\n     rename {old => new}/seed (100%)\n    merge exit status: 0\n    new/private.dat now contains: committed local bytes\n\nWith the default configuration, or merge.directoryRenames=conflict:\n\n    CONFLICT (file location): old/private.dat added in HEAD inside a\ndirectory that was renamed in incoming, suggesting it should perhaps\nbe moved to new/private.dat.\n    Automatic merge failed; fix conflicts and then commit the result.\n    merge exit status: 1\n    new/private.dat now contains: committed local bytes\n\nIn both cases the ignored bytes are lost. They are not in any commit, in\nthe index, or in a stash, so `git merge --abort` cannot restore them.\n\nWhat's different between what you expected and what actually happened?\n\nControl case: if \"incoming\" instead adds new/private.dat directly (with\n`git add -f`, no rename involved), the same `git merge --no-edit\n--no-overwrite-ignore incoming` aborts with exit 1 and leaves \"unique\nignored bytes\" intact:\n\n    error: The following untracked working tree files would be\noverwritten by merge:\n            new/private.dat\n    Please move or remove them before you merge.\n    Aborting\n\nSo --no-overwrite-ignore works for a direct collision and fails only\nwhen the destination is produced by directory rename detection in the\nort strategy.\n\nThis matters because --no-overwrite-ignore is the only merge-level guard\nfor ignored content such as local configuration, credential files, or\nbuild inputs that users deliberately keep out of history. A user or tool\nrelying on it can lose unique, unversioned data, with no error at all or\nwith only an unrelated rename conflict message.\n\nAnything else you want to add:\n\nReproduced with identical results, in both modes, on:\n\n    git version 2.56.0.138.gc61827130   (built from next)\n    git version 2.56.0.50.gc46c1e3772   (built from master)\n    git version 2.56.0                  (Homebrew)\n    git version 2.55.0                  (Homebrew)\n    git version 2.54.0 (Apple Git-157)\n\nall on macOS 27 (arm64, APFS), using the empty-configuration environment\nshown in the script. I am happy to test a patch.\n\nRegards, CoyGeek\n"}]}