Skip to content

Commit f0fd4d0

Browse files
newrengitster
authored andcommitted
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>
1 parent 7b1c610 commit f0fd4d0

File tree

2 files changed

+53
-13
lines changed

2 files changed

+53
-13
lines changed

merge-recursive.c

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,37 @@ static struct string_list *get_unmerged(void)
331331
return unmerged;
332332
}
333333

334+
static int string_list_df_name_compare(const void *a, const void *b)
335+
{
336+
const struct string_list_item *one = a;
337+
const struct string_list_item *two = b;
338+
int onelen = strlen(one->string);
339+
int twolen = strlen(two->string);
340+
/*
341+
* Here we only care that entries for D/F conflicts are
342+
* adjacent, in particular with the file of the D/F conflict
343+
* appearing before files below the corresponding directory.
344+
* The order of the rest of the list is irrelevant for us.
345+
*
346+
* To achieve this, we sort with df_name_compare and provide
347+
* the mode S_IFDIR so that D/F conflicts will sort correctly.
348+
* We use the mode S_IFDIR for everything else for simplicity,
349+
* since in other cases any changes in their order due to
350+
* sorting cause no problems for us.
351+
*/
352+
int cmp = df_name_compare(one->string, onelen, S_IFDIR,
353+
two->string, twolen, S_IFDIR);
354+
/*
355+
* Now that 'foo' and 'foo/bar' compare equal, we have to make sure
356+
* that 'foo' comes before 'foo/bar'.
357+
*/
358+
if (cmp)
359+
return cmp;
360+
return onelen - twolen;
361+
}
362+
363+
364+
334365
static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
335366
struct string_list *entries)
336367
{
@@ -343,11 +374,6 @@ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
343374
* otherwise, if the file is not supposed to be removed by the
344375
* merge, the contents of the file will be placed in another
345376
* unique filename.
346-
*
347-
* NOTE: This function relies on the fact that entries for a
348-
* D/F conflict will appear adjacent in the index, with the
349-
* entries for the file appearing before entries for paths
350-
* below the corresponding directory.
351377
*/
352378
const char *last_file = NULL;
353379
int last_len = 0;
@@ -360,6 +386,10 @@ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
360386
if (o->call_depth)
361387
return;
362388

389+
/* Ensure D/F conflicts are adjacent in the entries list. */
390+
qsort(entries->items, entries->nr, sizeof(*entries->items),
391+
string_list_df_name_compare);
392+
363393
for (i = 0; i < entries->nr; i++) {
364394
const char *path = entries->items[i].string;
365395
int len = strlen(path);

t/t6020-merge-df.sh

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,41 +59,51 @@ test_expect_success 'setup modify/delete + directory/file conflict' '
5959
git add letters &&
6060
git commit -m initial &&
6161
62+
# Throw in letters.txt for sorting order fun
63+
# ("letters.txt" sorts between "letters" and "letters/file")
6264
echo i >>letters &&
63-
git add letters &&
65+
echo "version 2" >letters.txt &&
66+
git add letters letters.txt &&
6467
git commit -m modified &&
6568
6669
git checkout -b delete HEAD^ &&
6770
git rm letters &&
6871
mkdir letters &&
6972
>letters/file &&
70-
git add letters &&
73+
echo "version 1" >letters.txt &&
74+
git add letters letters.txt &&
7175
git commit -m deleted
7276
'
7377

7478
test_expect_success 'modify/delete + directory/file conflict' '
7579
git checkout delete^0 &&
7680
test_must_fail git merge modify &&
7781
78-
test 3 = $(git ls-files -s | wc -l) &&
79-
test 2 = $(git ls-files -u | wc -l) &&
80-
test 1 = $(git ls-files -o | wc -l) &&
82+
test 5 -eq $(git ls-files -s | wc -l) &&
83+
test 4 -eq $(git ls-files -u | wc -l) &&
84+
test 1 -eq $(git ls-files -o | wc -l) &&
8185
8286
test -f letters/file &&
87+
test -f letters.txt &&
8388
test -f letters~modify
8489
'
8590

8691
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 &&
8795
git reset --hard &&
8896
git clean -f &&
8997
git checkout modify^0 &&
98+
9099
test_must_fail git merge delete &&
91100
92-
test 3 = $(git ls-files -s | wc -l) &&
93-
test 2 = $(git ls-files -u | wc -l) &&
94-
test 1 = $(git ls-files -o | wc -l) &&
101+
test 5 -eq $(git ls-files -s | wc -l) &&
102+
test 4 -eq $(git ls-files -u | wc -l) &&
103+
test 1 -eq $(git ls-files -o | wc -l) &&
95104
96105
test -f letters/file &&
106+
test -f letters.txt &&
97107
test -f letters~HEAD
98108
'
99109

0 commit comments

Comments
 (0)