Skip to content

Commit e105e8b

Browse files
newrengitster
authored andcommitted
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), 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), 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. Signed-off-by: Elijah Newren <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 4a1c9c3 commit e105e8b

4 files changed

+12
-13
lines changed

read-cache.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,10 +2632,13 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock,
26322632

26332633
/*
26342634
* Read the index file that is potentially unmerged into given
2635-
* index_state, dropping any unmerged entries. Returns true if
2636-
* the index is unmerged. Callers who want to refuse to work
2637-
* from an unmerged state can call this and check its return value,
2638-
* instead of calling read_cache().
2635+
* index_state, dropping any unmerged entries to stage #0 (potentially
2636+
* resulting in a path appearing as both a file and a directory in the
2637+
* index; the caller is responsible to clear out the extra entries
2638+
* before writing the index to a tree). Returns true if the index is
2639+
* unmerged. Callers who want to refuse to work from an unmerged
2640+
* state can call this and check its return value, instead of calling
2641+
* read_cache().
26392642
*/
26402643
int read_index_unmerged(struct index_state *istate)
26412644
{
@@ -2658,7 +2661,7 @@ int read_index_unmerged(struct index_state *istate)
26582661
new_ce->ce_flags = create_ce_flags(0) | CE_CONFLICTED;
26592662
new_ce->ce_namelen = len;
26602663
new_ce->ce_mode = ce->ce_mode;
2661-
if (add_index_entry(istate, new_ce, 0))
2664+
if (add_index_entry(istate, new_ce, ADD_CACHE_SKIP_DFCHECK))
26622665
return error("%s: cannot drop to stage #0",
26632666
new_ce->name);
26642667
}

t/t1015-read-index-unmerged.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test_expect_success 'setup modify/delete + directory/file conflict' '
3030
)
3131
'
3232

33-
test_expect_failure 'read-tree --reset cleans unmerged entries' '
33+
test_expect_success 'read-tree --reset cleans unmerged entries' '
3434
test_when_finished "git -C df_plus_modify_delete clean -f" &&
3535
test_when_finished "git -C df_plus_modify_delete reset --hard" &&
3636
(
@@ -45,7 +45,7 @@ test_expect_failure 'read-tree --reset cleans unmerged entries' '
4545
)
4646
'
4747

48-
test_expect_failure 'One reset --hard cleans unmerged entries' '
48+
test_expect_success 'One reset --hard cleans unmerged entries' '
4949
test_when_finished "git -C df_plus_modify_delete clean -f" &&
5050
test_when_finished "git -C df_plus_modify_delete reset --hard" &&
5151
(
@@ -87,7 +87,7 @@ test_expect_success 'setup directory/file conflict + simple edit/edit' '
8787
)
8888
'
8989

90-
test_expect_failure 'git merge --abort succeeds despite D/F conflict' '
90+
test_expect_success 'git merge --abort succeeds despite D/F conflict' '
9191
test_when_finished "git -C df_plus_edit_edit clean -f" &&
9292
test_when_finished "git -C df_plus_edit_edit reset --hard" &&
9393
(
@@ -103,7 +103,7 @@ test_expect_failure 'git merge --abort succeeds despite D/F conflict' '
103103
)
104104
'
105105

106-
test_expect_failure 'git am --skip succeeds despite D/F conflict' '
106+
test_expect_success 'git am --skip succeeds despite D/F conflict' '
107107
test_when_finished "git -C df_plus_edit_edit clean -f" &&
108108
test_when_finished "git -C df_plus_edit_edit reset --hard" &&
109109
(

t/t6020-merge-df.sh

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ test_expect_success 'modify/delete + directory/file conflict' '
8989
'
9090

9191
test_expect_success 'modify/delete + directory/file conflict; other way' '
92-
# Yes, we really need the double reset since "letters" appears as
93-
# both a file and a directory.
94-
git reset --hard &&
9592
git reset --hard &&
9693
git clean -f &&
9794
git checkout modify^0 &&

t/t6042-merge-rename-corner-cases.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,6 @@ test_expect_success 'rename/directory conflict + content merge conflict' '
323323
(
324324
cd rename-directory-1 &&
325325
326-
git reset --hard &&
327326
git reset --hard &&
328327
git clean -fdqx &&
329328

0 commit comments

Comments
 (0)