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>

Elijah Newren committed Aug 11, 2011 at 23:19 UTC f0fd4d05e8a17fe5ccdd4d3edd686bc6702b8144
2 files changed +53 -13
merge-recursive.c
+35 -5
@@ -331,6 +331,37 @@ static struct string_list *get_unmerged(void)
331 return unmerged;
332 }
333
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 +
365 static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
366 struct string_list *entries)
367 {
@@ -343,11 +374,6 @@ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
374 * otherwise, if the file is not supposed to be removed by the
375 * merge, the contents of the file will be placed in another
376 * 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.
377 */
378 const char *last_file = NULL;
379 int last_len = 0;
@@ -360,6 +386,10 @@ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
386 if (o->call_depth)
387 return;
388
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 +
393 for (i = 0; i < entries->nr; i++) {
394 const char *path = entries->items[i].string;
395 int len = strlen(path);
t/t6020-merge-df.sh
+18 -8
@@ -59,15 +59,19 @@ test_expect_success 'setup modify/delete + directory/file conflict' '
59 git add letters &&
60 git commit -m initial &&
61
62 + # Throw in letters.txt for sorting order fun
63 + # ("letters.txt" sorts between "letters" and "letters/file")
64 echo i >>letters &&
63 - git add letters &&
65 + echo "version 2" >letters.txt &&
66 + git add letters letters.txt &&
67 git commit -m modified &&
68
69 git checkout -b delete HEAD^ &&
70 git rm letters &&
71 mkdir letters &&
72 >letters/file &&
70 - git add letters &&
73 + echo "version 1" >letters.txt &&
74 + git add letters letters.txt &&
75 git commit -m deleted
76 '
77
@@ -75,25 +79,31 @@ test_expect_success 'modify/delete + directory/file conflict' '
79 git checkout delete^0 &&
80 test_must_fail git merge modify &&
81
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) &&
85
86 test -f letters/file &&
87 + test -f letters.txt &&
88 test -f letters~modify
89 '
90
91 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 &&
95 git reset --hard &&
96 git clean -f &&
97 git checkout modify^0 &&
98 +
99 test_must_fail git merge delete &&
100
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) &&
104
105 test -f letters/file &&
106 + test -f letters.txt &&
107 test -f letters~HEAD
108 '
109