merge-recursive: Fix sorting order and directory change assumptions
We cannot assume that directory/file conflicts will appear in sorted order; for example, 'letters.txt' comes between 'letters' and 'letters/file'. Thanks to Johannes for a pointer about qsort stability issues with Windows and suggested code change. Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Johannes Sixt <j6t@kdbg.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
parent
7b1c610f84
commit
f0fd4d05e8
@ -331,6 +331,37 @@ static struct string_list *get_unmerged(void)
|
||||
return unmerged;
|
||||
}
|
||||
|
||||
static int string_list_df_name_compare(const void *a, const void *b)
|
||||
{
|
||||
const struct string_list_item *one = a;
|
||||
const struct string_list_item *two = b;
|
||||
int onelen = strlen(one->string);
|
||||
int twolen = strlen(two->string);
|
||||
/*
|
||||
* Here we only care that entries for D/F conflicts are
|
||||
* adjacent, in particular with the file of the D/F conflict
|
||||
* appearing before files below the corresponding directory.
|
||||
* The order of the rest of the list is irrelevant for us.
|
||||
*
|
||||
* To achieve this, we sort with df_name_compare and provide
|
||||
* the mode S_IFDIR so that D/F conflicts will sort correctly.
|
||||
* We use the mode S_IFDIR for everything else for simplicity,
|
||||
* since in other cases any changes in their order due to
|
||||
* sorting cause no problems for us.
|
||||
*/
|
||||
int cmp = df_name_compare(one->string, onelen, S_IFDIR,
|
||||
two->string, twolen, S_IFDIR);
|
||||
/*
|
||||
* Now that 'foo' and 'foo/bar' compare equal, we have to make sure
|
||||
* that 'foo' comes before 'foo/bar'.
|
||||
*/
|
||||
if (cmp)
|
||||
return cmp;
|
||||
return onelen - twolen;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
|
||||
struct string_list *entries)
|
||||
{
|
||||
@ -343,11 +374,6 @@ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
|
||||
* otherwise, if the file is not supposed to be removed by the
|
||||
* merge, the contents of the file will be placed in another
|
||||
* unique filename.
|
||||
*
|
||||
* NOTE: This function relies on the fact that entries for a
|
||||
* D/F conflict will appear adjacent in the index, with the
|
||||
* entries for the file appearing before entries for paths
|
||||
* below the corresponding directory.
|
||||
*/
|
||||
const char *last_file = NULL;
|
||||
int last_len = 0;
|
||||
@ -360,6 +386,10 @@ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
|
||||
if (o->call_depth)
|
||||
return;
|
||||
|
||||
/* Ensure D/F conflicts are adjacent in the entries list. */
|
||||
qsort(entries->items, entries->nr, sizeof(*entries->items),
|
||||
string_list_df_name_compare);
|
||||
|
||||
for (i = 0; i < entries->nr; i++) {
|
||||
const char *path = entries->items[i].string;
|
||||
int len = strlen(path);
|
||||
|
@ -59,15 +59,19 @@ test_expect_success 'setup modify/delete + directory/file conflict' '
|
||||
git add letters &&
|
||||
git commit -m initial &&
|
||||
|
||||
# Throw in letters.txt for sorting order fun
|
||||
# ("letters.txt" sorts between "letters" and "letters/file")
|
||||
echo i >>letters &&
|
||||
git add letters &&
|
||||
echo "version 2" >letters.txt &&
|
||||
git add letters letters.txt &&
|
||||
git commit -m modified &&
|
||||
|
||||
git checkout -b delete HEAD^ &&
|
||||
git rm letters &&
|
||||
mkdir letters &&
|
||||
>letters/file &&
|
||||
git add letters &&
|
||||
echo "version 1" >letters.txt &&
|
||||
git add letters letters.txt &&
|
||||
git commit -m deleted
|
||||
'
|
||||
|
||||
@ -75,25 +79,31 @@ test_expect_success 'modify/delete + directory/file conflict' '
|
||||
git checkout delete^0 &&
|
||||
test_must_fail git merge modify &&
|
||||
|
||||
test 3 = $(git ls-files -s | wc -l) &&
|
||||
test 2 = $(git ls-files -u | wc -l) &&
|
||||
test 1 = $(git ls-files -o | wc -l) &&
|
||||
test 5 -eq $(git ls-files -s | wc -l) &&
|
||||
test 4 -eq $(git ls-files -u | wc -l) &&
|
||||
test 1 -eq $(git ls-files -o | wc -l) &&
|
||||
|
||||
test -f letters/file &&
|
||||
test -f letters.txt &&
|
||||
test -f letters~modify
|
||||
'
|
||||
|
||||
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 &&
|
||||
|
||||
test_must_fail git merge delete &&
|
||||
|
||||
test 3 = $(git ls-files -s | wc -l) &&
|
||||
test 2 = $(git ls-files -u | wc -l) &&
|
||||
test 1 = $(git ls-files -o | wc -l) &&
|
||||
test 5 -eq $(git ls-files -s | wc -l) &&
|
||||
test 4 -eq $(git ls-files -u | wc -l) &&
|
||||
test 1 -eq $(git ls-files -o | wc -l) &&
|
||||
|
||||
test -f letters/file &&
|
||||
test -f letters.txt &&
|
||||
test -f letters~HEAD
|
||||
'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user