Skip to content

Commit

Permalink
read-cache: fix directory/file conflict handling in read_index_unmerg…
Browse files Browse the repository at this point in the history
…ed()

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 <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
newren authored and gitster committed Jul 31, 2018
1 parent 25c200a commit ad37620
Show file tree
Hide file tree
Showing 4 changed files with 12 additions and 14 deletions.
13 changes: 8 additions & 5 deletions read-cache.c
Expand Up @@ -2632,10 +2632,13 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock,

/*
* Read the index file that is potentially unmerged into given
* index_state, dropping any unmerged entries. Returns true if
* the index is unmerged. Callers who want to refuse to work
* from an unmerged state can call this and check its return value,
* instead of calling read_cache().
* index_state, dropping any unmerged entries to stage #0 (potentially
* resulting in a path appearing as both a file and a directory in the
* index; the caller is responsible to clear out the extra entries
* before writing the index to a tree). Returns true if the index is
* unmerged. Callers who want to refuse to work from an unmerged
* state can call this and check its return value, instead of calling
* read_cache().
*/
int read_index_unmerged(struct index_state *istate)
{
Expand All @@ -2658,7 +2661,7 @@ int read_index_unmerged(struct index_state *istate)
new_ce->ce_flags = create_ce_flags(0) | CE_CONFLICTED;
new_ce->ce_namelen = len;
new_ce->ce_mode = ce->ce_mode;
if (add_index_entry(istate, new_ce, 0))
if (add_index_entry(istate, new_ce, ADD_CACHE_SKIP_DFCHECK))
return error("%s: cannot drop to stage #0",
new_ce->name);
}
Expand Down
8 changes: 4 additions & 4 deletions t/t1015-read-index-unmerged.sh
Expand Up @@ -30,7 +30,7 @@ test_expect_success 'setup modify/delete + directory/file conflict' '
)
'

test_expect_failure 'read-tree --reset cleans unmerged entries' '
test_expect_success 'read-tree --reset cleans unmerged entries' '
test_when_finished "git -C df_plus_modify_delete clean -f" &&
test_when_finished "git -C df_plus_modify_delete reset --hard" &&
(
Expand All @@ -45,7 +45,7 @@ test_expect_failure 'read-tree --reset cleans unmerged entries' '
)
'

test_expect_failure 'One reset --hard cleans unmerged entries' '
test_expect_success 'One reset --hard cleans unmerged entries' '
test_when_finished "git -C df_plus_modify_delete clean -f" &&
test_when_finished "git -C df_plus_modify_delete reset --hard" &&
(
Expand Down Expand Up @@ -87,7 +87,7 @@ test_expect_success 'setup directory/file conflict + simple edit/edit' '
)
'

test_expect_failure 'git merge --abort succeeds despite D/F conflict' '
test_expect_success 'git merge --abort succeeds despite D/F conflict' '
test_when_finished "git -C df_plus_edit_edit clean -f" &&
test_when_finished "git -C df_plus_edit_edit reset --hard" &&
(
Expand All @@ -103,7 +103,7 @@ test_expect_failure 'git merge --abort succeeds despite D/F conflict' '
)
'

test_expect_failure 'git am --skip succeeds despite D/F conflict' '
test_expect_success 'git am --skip succeeds despite D/F conflict' '
test_when_finished "git -C df_plus_edit_edit clean -f" &&
test_when_finished "git -C df_plus_edit_edit reset --hard" &&
(
Expand Down
3 changes: 0 additions & 3 deletions t/t6020-merge-df.sh
Expand Up @@ -89,9 +89,6 @@ test_expect_success 'modify/delete + directory/file conflict' '
'

test_expect_success 'modify/delete + directory/file conflict; other way' '
# Yes, we really need the double reset since "letters" appears as
# both a file and a directory.
git reset --hard &&
git reset --hard &&
git clean -f &&
git checkout modify^0 &&
Expand Down
2 changes: 0 additions & 2 deletions t/t6042-merge-rename-corner-cases.sh
Expand Up @@ -235,7 +235,6 @@ test_expect_success 'setup content merge + rename/directory conflict' '
'

test_expect_success 'rename/directory conflict + clean content merge' '
git reset --hard &&
git reset --hard &&
git clean -fdqx &&
Expand All @@ -259,7 +258,6 @@ test_expect_success 'rename/directory conflict + clean content merge' '
'

test_expect_success 'rename/directory conflict + content merge conflict' '
git reset --hard &&
git reset --hard &&
git clean -fdqx &&
Expand Down

0 comments on commit ad37620

Please sign in to comment.