Note: even upgrading Git might not be enough, not until Git 2.19 (Q3 2018), since "git merge --abort
" etc. did not clean things up properly when there were conflicted entries in the index in certain order that are involved in D/F conflicts.
This has been corrected with Git 2.19.
See commit ad37620, commit 25c200a (31 Jul 2018) by Elijah Newren (newren
).
(Merged by Junio C Hamano -- gitster
-- in commit 8ba8642, 17 Aug 2018)
read-cache
: fix directory/file conflict handling in read_index_unmerged()
read_index_unmerged()
has two intended purposes:
- return 1 if there are any unmerged entries, 0 otherwise
- drops any higher-stage entries down to stage #0
There are several callers of read_index_unmerged()
that check the return
value to see if it is non-zero, all of which then die()
if that condition
is met.
For these callers, dropping higher-stage entries down to stage #0
is a waste of resources, and returning immediately on first unmerged entry would be better.
But it's probably only a very minor difference and isn't the focus of this series.
The remaining callers ignore the return value and call this function for the side effect of dropping higher-stage entries down to stage #0.
As mentioned in commit e11d7b5 ("'reset --merge': fix unmerged case", 2009-12-31, Git 1.7.0),
The only reason we want to keep a previously unmerged entry in the index at stage #0 is so that we don't forget the fact that we have corresponding file in the work tree in order to be able to remove it when the tree we are resetting to does not have the path.
In fact, prior to commit d1a43f2 ("reset --hard/read-tree --reset -u:
remove unmerged new paths", 2008-10-15, Git 1.6.0.4), read_index_unmerged()
did just
remove unmerged entries from the cache immediately but that had the unwanted effect of leaving around new untracked files in the tree from aborted merges.
So, that's the intended purpose of this function.
The problem is that when directory/files conflicts are present, trying to add the file to the index at stage 0 fails (because there is still a directory in the way),
and the function returns early with a -1 return code to signify the error.
As noted above, none of the callers who want the drop-to-stage-0 behavior check the return status, though, so this means all remaining unmerged entries remain in the index and the callers proceed assuming otherwise.
Users then see errors of the form:
error: 'DIR-OR-FILE' appears as both a file and as a directory
error: DIR-OR-FILE: cannot drop to stage #0
and potentially also messages about other unmerged entries which came lexicographically later than whatever pathname was both a file and a directory. Google finds a few hits searching for those messages, suggesting there were probably a couple people who hit this besides me.
Luckily, calling git reset --hard
multiple times would workaround this bug.
Since the whole purpose here is to just put the entry temporarily into the index so that any associated file in the working copy can be removed, we can just skip the DFCHECK and allow both the file and directory to appear in the index.
The temporary simultaneous appearance of the directory and file entries in the index will be removed by the callers by calling unpack_trees()
, which excludes these unmerged entries marked with CE_CONFLICTED
flag from the resulting index, before they attempt to write the index anywhere.
In case of problem, git fsck
can help:
Before Git 2.28 (Q3 2020), the check in "git fsck
" to ensure that the tree objects are sorted still had corner cases it missed unsorted entries.
See commit fe74704, commit 3d71b1c, commit fc12aa7, commit 8671559 (21 May 2020) by René Scharfe (rscharfe
).
(Merged by Junio C Hamano -- gitster
-- in commit 7e75aeb, 09 Jun 2020)
fsck
: detect more in-tree d/f conflicts
Signed-off-by: René Scharfe
(D/F: Directories/Files)
If the conflict candidate file name from the top of the stack is not a prefix of the current candiate directory then we can discard it as no matching directory can come up later.
But we are not done checking the candidate directory -- the stack might still hold a matching file name, so stay in the loop and check the next candidate file name.
git version 2.21.0 (Apple Git-122.2)
I had that problem as well, git status also is the workaround