Hello,
Consider the following ...
`git merge --no-overwrite-ignore` is documented to abort rather than overwrite ignored files. When directory rename detection moves a file that was added on the current branch into a path that holds an ignored, untracked file, the merge replaces that file's contents without any warning.
With merge.directoryRenames=true the merge reports success (exit 0). With the default, merge.directoryRenames=conflict, it stops with a "file location" conflict, but the ignored file has already been overwritten. A direct collision with the same ignored path is correctly refused, so the protection is bypassed only for the destination path that rename detection generates.
What did you do before the bug happened? (Steps to reproduce your issue)
Save the script below as repro.sh and run it as "sh repro.sh true", "sh repro.sh conflict", or "sh repro.sh default" (no -c override). It works in a throwaway repository with empty global and system configuration.
#!/bin/sh
set -eu
mode=${1:-true}
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR
GIT_CONFIG_PARAMETERS
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
tmp=$(mktemp -d)
cd "$tmp"
git init -q -b main repo
cd repo
git config user.name Example
git config user.email example@example.invalid
echo new/private.dat >.gitignore
mkdir old
echo seed >old/seed
git add .gitignore old/seed
git commit -qm base
git checkout -qb incoming
git mv old new
git commit -qm 'rename old/ to new/'
git checkout -q main
echo 'committed local bytes' >old/private.dat
git add old/private.dat
git commit -qm 'add old/private.dat'
mkdir new
echo 'unique ignored bytes' >new/private.dat
git status --porcelain --ignored
git version
status=0
if [ "$mode" = default ]; then set -- ; else set -- -c
merge.directoryRenames="$mode"; fi
git "$@" merge --no-edit --no-overwrite-ignore incoming || status=$?
echo "merge exit status: $status"
echo "new/private.dat now contains: $(cat new/private.dat)"Before the merge, `git status --porcelain --ignored` reports only "!! new/": the worktree is clean, and new/private.dat is ignored and untracked.
What did you expect to happen? (Expected behavior)
As for any other merge result that would overwrite an ignored file under --no-overwrite-ignore, Git refuses before touching the worktree, names new/private.dat, and leaves its contents as "unique ignored bytes".
What happened instead? (Actual behavior)
With merge.directoryRenames=true:
Path updated: old/private.dat added in HEAD inside a directory
that was renamed in incoming; moving it to new/private.dat.
Merge made by the 'ort' strategy.
{old => new}/private.dat | 0
{old => new}/seed | 0
2 files changed, 0 insertions(+), 0 deletions(-)
rename {old => new}/private.dat (100%)
rename {old => new}/seed (100%)
merge exit status: 0
new/private.dat now contains: committed local bytesWith the default configuration, or merge.directoryRenames=conflict:
CONFLICT (file location): old/private.dat added in HEAD inside a
directory that was renamed in incoming, suggesting it should perhaps
be moved to new/private.dat.
Automatic merge failed; fix conflicts and then commit the result.
merge exit status: 1
new/private.dat now contains: committed local bytesIn both cases the ignored bytes are lost. They are not in any commit, in the index, or in a stash, so `git merge --abort` cannot restore them.
What's different between what you expected and what actually happened?
Control case: if "incoming" instead adds new/private.dat directly (with `git add -f`, no rename involved), the same `git merge --no-edit --no-overwrite-ignore incoming` aborts with exit 1 and leaves "unique ignored bytes" intact:
error: The following untracked working tree files would be
overwritten by merge:
new/private.dat
Please move or remove them before you merge.
AbortingSo --no-overwrite-ignore works for a direct collision and fails only when the destination is produced by directory rename detection in the ort strategy.
This matters because --no-overwrite-ignore is the only merge-level guard for ignored content such as local configuration, credential files, or build inputs that users deliberately keep out of history. A user or tool relying on it can lose unique, unversioned data, with no error at all or with only an unrelated rename conflict message.
Anything else you want to add:
Reproduced with identical results, in both modes, on:
git version 2.56.0.138.gc61827130 (built from next)
git version 2.56.0.50.gc46c1e3772 (built from master)
git version 2.56.0 (Homebrew)
git version 2.55.0 (Homebrew)
git version 2.54.0 (Apple Git-157)all on macOS 27 (arm64, APFS), using the empty-configuration environment shown in the script. I am happy to test a patch.
Regards, CoyGeek