{"thread":{"id":"47340","subject":"[PATCH v4 15/34] merge-recursive: move the get_renames() function","startedAt":"2017-11-29T01:43:12Z","lastAt":"2017-12-13T19:05:36Z","messageCount":40,"participants":["Elijah Newren","Junio C Hamano","Ramsay Jones"],"isPatch":true,"patchVersion":4,"patchTotal":34},"messages":[{"id":"333721","messageId":"20171129014237.32570-16-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 15/34] merge-recursive: move the get_renames() function","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:18Z","receivedAt":"2017-11-29T01:43:12Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"I want to re-use some other functions in the file without moving those\nother functions or dealing with a handful of annoying split function\ndeclarations and definitions.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 139 +++++++++++++++++++++++++++---------------------------\n 1 file changed, 70 insertions(+), 69 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d78853d5ee..08bf26b9c7 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -537,75 +537,6 @@ struct rename {\n \tunsigned processed:1;\n };\n \n-/*\n- * Get information of all renames which occurred between 'o_tree' and\n- * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and\n- * 'b_tree') to be able to associate the correct cache entries with\n- * the rename information. 'tree' is always equal to either a_tree or b_tree.\n- */\n-static struct string_list *get_renames(struct merge_options *o,\n-\t\t\t\t       struct tree *tree,\n-\t\t\t\t       struct tree *o_tree,\n-\t\t\t\t       struct tree *a_tree,\n-\t\t\t\t       struct tree *b_tree,\n-\t\t\t\t       struct string_list *entries)\n-{\n-\tint i;\n-\tstruct string_list *renames;\n-\tstruct diff_options opts;\n-\n-\trenames = xcalloc(1, sizeof(struct string_list));\n-\tif (!o->detect_rename)\n-\t\treturn renames;\n-\n-\tdiff_setup(&opts);\n-\topts.flags.recursive = 1;\n-\topts.flags.rename_empty = 0;\n-\topts.detect_rename = DIFF_DETECT_RENAME;\n-\topts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :\n-\t\t\t    o->diff_rename_limit >= 0 ? o->diff_rename_limit :\n-\t\t\t    1000;\n-\topts.rename_score = o->rename_score;\n-\topts.show_rename_progress = o->show_rename_progress;\n-\topts.output_format = DIFF_FORMAT_NO_OUTPUT;\n-\tdiff_setup_done(&opts);\n-\tdiff_tree_oid(&o_tree->object.oid, &tree->object.oid, \"\", &opts);\n-\tdiffcore_std(&opts);\n-\tif (opts.needed_rename_limit > o->needed_rename_limit)\n-\t\to->needed_rename_limit = opts.needed_rename_limit;\n-\tfor (i = 0; i < diff_queued_diff.nr; ++i) {\n-\t\tstruct string_list_item *item;\n-\t\tstruct rename *re;\n-\t\tstruct diff_filepair *pair = diff_queued_diff.queue[i];\n-\t\tif (pair->status != 'R') {\n-\t\t\tdiff_free_filepair(pair);\n-\t\t\tcontinue;\n-\t\t}\n-\t\tre = xmalloc(sizeof(*re));\n-\t\tre->processed = 0;\n-\t\tre->pair = pair;\n-\t\titem = string_list_lookup(entries, re->pair->one->path);\n-\t\tif (!item)\n-\t\t\tre->src_entry = insert_stage_data(re->pair->one->path,\n-\t\t\t\t\to_tree, a_tree, b_tree, entries);\n-\t\telse\n-\t\t\tre->src_entry = item->util;\n-\n-\t\titem = string_list_lookup(entries, re->pair->two->path);\n-\t\tif (!item)\n-\t\t\tre->dst_entry = insert_stage_data(re->pair->two->path,\n-\t\t\t\t\to_tree, a_tree, b_tree, entries);\n-\t\telse\n-\t\t\tre->dst_entry = item->util;\n-\t\titem = string_list_insert(renames, pair->one->path);\n-\t\titem->util = re;\n-\t}\n-\topts.output_format = DIFF_FORMAT_NO_OUTPUT;\n-\tdiff_queued_diff.nr = 0;\n-\tdiff_flush(&opts);\n-\treturn renames;\n-}\n-\n static int update_stages(struct merge_options *opt, const char *path,\n \t\t\t const struct diff_filespec *o,\n \t\t\t const struct diff_filespec *a,\n@@ -1380,6 +1311,76 @@ static int conflict_rename_rename_2to1(struct merge_options *o,\n \treturn ret;\n }\n \n+/*\n+ * Get information of all renames which occurred between 'o_tree' and\n+ * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and\n+ * 'b_tree') to be able to associate the correct cache entries with\n+ * the rename information. 'tree' is always equal to either a_tree or b_tree.\n+ */\n+static struct string_list *get_renames(struct merge_options *o,\n+\t\t\t\t       struct tree *tree,\n+\t\t\t\t       struct tree *o_tree,\n+\t\t\t\t       struct tree *a_tree,\n+\t\t\t\t       struct tree *b_tree,\n+\t\t\t\t       struct string_list *entries)\n+{\n+\tint i;\n+\tstruct string_list *renames;\n+\tstruct diff_options opts;\n+\n+\trenames = xcalloc(1, sizeof(struct string_list));\n+\tif (!o->detect_rename)\n+\t\treturn renames;\n+\n+\tdiff_setup(&opts);\n+\topts.flags.recursive = 1;\n+\topts.flags.rename_empty = 0;\n+\topts.detect_rename = DIFF_DETECT_RENAME;\n+\topts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :\n+\t\t\t    o->diff_rename_limit >= 0 ? o->diff_rename_limit :\n+\t\t\t    1000;\n+\topts.rename_score = o->rename_score;\n+\topts.show_rename_progress = o->show_rename_progress;\n+\topts.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\tdiff_setup_done(&opts);\n+\tdiff_tree_oid(&o_tree->object.oid, &tree->object.oid, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tif (opts.needed_rename_limit > o->needed_rename_limit)\n+\t\to->needed_rename_limit = opts.needed_rename_limit;\n+\tfor (i = 0; i < diff_queued_diff.nr; ++i) {\n+\t\tstruct string_list_item *item;\n+\t\tstruct rename *re;\n+\t\tstruct diff_filepair *pair = diff_queued_diff.queue[i];\n+\n+\t\tif (pair->status != 'R') {\n+\t\t\tdiff_free_filepair(pair);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tre = xmalloc(sizeof(*re));\n+\t\tre->processed = 0;\n+\t\tre->pair = pair;\n+\t\titem = string_list_lookup(entries, re->pair->one->path);\n+\t\tif (!item)\n+\t\t\tre->src_entry = insert_stage_data(re->pair->one->path,\n+\t\t\t\t\to_tree, a_tree, b_tree, entries);\n+\t\telse\n+\t\t\tre->src_entry = item->util;\n+\n+\t\titem = string_list_lookup(entries, re->pair->two->path);\n+\t\tif (!item)\n+\t\t\tre->dst_entry = insert_stage_data(re->pair->two->path,\n+\t\t\t\t\to_tree, a_tree, b_tree, entries);\n+\t\telse\n+\t\t\tre->dst_entry = item->util;\n+\t\titem = string_list_insert(renames, pair->one->path);\n+\t\titem->util = re;\n+\t}\n+\topts.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\tdiff_queued_diff.nr = 0;\n+\tdiff_flush(&opts);\n+\treturn renames;\n+}\n+\n static int process_renames(struct merge_options *o,\n \t\t\t   struct string_list *a_renames,\n \t\t\t   struct string_list *b_renames)\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333722","messageId":"20171129014237.32570-25-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 24/34] merge-recursive: add a new hashmap for storing file collisions","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:27Z","receivedAt":"2017-11-29T01:43:15Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Directory renames with the ability to merge directories opens up the\npossibility of add/add/add/.../add conflicts, if each of the N\ndirectories being merged into one target directory all had a file with\nthe same name.  We need a way to check for and report on such\ncollisions; this hashmap will be used for this purpose.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 23 +++++++++++++++++++++++\n merge-recursive.h |  7 +++++++\n 2 files changed, 30 insertions(+)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d92fba2775..6bd4f34d55 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -84,6 +84,29 @@ static void dir_rename_entry_init(struct dir_rename_entry *entry,\n \tstring_list_init(&entry->possible_new_dirs, 0);\n }\n \n+static struct collision_entry *collision_find_entry(struct hashmap *hashmap,\n+\t\t\t\t\t\t    char *target_file)\n+{\n+\tstruct collision_entry key;\n+\n+\thashmap_entry_init(&key, strhash(target_file));\n+\tkey.target_file = target_file;\n+\treturn hashmap_get(hashmap, &key, NULL);\n+}\n+\n+static int collision_cmp(void *unused_cmp_data,\n+\t\t\t const struct collision_entry *e1,\n+\t\t\t const struct collision_entry *e2,\n+\t\t\t const void *unused_keydata)\n+{\n+\treturn strcmp(e1->target_file, e2->target_file);\n+}\n+\n+static void collision_init(struct hashmap *map)\n+{\n+\thashmap_init(map, (hashmap_cmp_fn) collision_cmp, NULL, 0);\n+}\n+\n static void flush_output(struct merge_options *o)\n {\n \tif (o->buffer_output < 2 && o->obuf.len) {\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex d7f4cc80c1..e1be27f57c 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -37,6 +37,13 @@ struct dir_rename_entry {\n \tstruct string_list possible_new_dirs;\n };\n \n+struct collision_entry {\n+\tstruct hashmap_entry ent; /* must be the first member! */\n+\tchar *target_file;\n+\tstruct string_list source_files;\n+\tunsigned reported_already:1;\n+};\n+\n /* merge_trees() but with recursive ancestor consolidation */\n int merge_recursive(struct merge_options *o,\n \t\t    struct commit *h1,\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333723","messageId":"20171129014237.32570-14-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 13/34] directory rename detection: tests for handling overwriting untracked files","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:16Z","receivedAt":"2017-11-29T01:43:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 337 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 337 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 42228a60aa..00b0ee7f08 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -2768,4 +2768,341 @@ test_expect_failure '9g-check: Renamed directory that only contained immediate s\n #   side of history for any implicit directory renames.\n ###########################################################################\n \n+###########################################################################\n+# SECTION 10: Handling untracked files\n+#\n+# unpack_trees(), upon which the recursive merge algorithm is based, aborts\n+# the operation if untracked or dirty files would be deleted or overwritten\n+# by the merge.  Unfortunately, unpack_trees() does not understand renames,\n+# and if it doesn't abort, then it muddies up the working directory before\n+# we even get to the point of detecting renames, so we need some special\n+# handling, at least in the case of directory renames.\n+###########################################################################\n+\n+# Testcase 10a, Overwrite untracked: normal rename/delete\n+#   Commit O: z/{b,c_1}\n+#   Commit A: z/b + untracked z/c + untracked z/d\n+#   Commit B: z/{b,d_1}\n+#   Expected: Aborted Merge +\n+#       ERROR_MSG(untracked working tree files would be overwritten by merge)\n+\n+test_expect_success '10a-setup: Overwrite untracked with normal rename/delete' '\n+\ttest_create_repo 10a &&\n+\t(\n+\t\tcd 10a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit rm z/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z/c z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '10a-check: Overwrite untracked with normal rename/delete' '\n+\t(\n+\t\tcd 10a &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo very >z/c &&\n+\t\techo important >z/d &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"The following untracked working tree files would be overwritten by merge\" err &&\n+\n+\t\ttest 1 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\ttest \"very\" = \"$(cat z/c)\" &&\n+\t\ttest \"important\" = \"$(cat z/d)\" &&\n+\t\ttest $(git rev-parse HEAD:z/b) = $(git rev-parse O:z/b)\n+\t)\n+'\n+\n+# Testcase 10b, Overwrite untracked: dir rename + delete\n+#   Commit O: z/{b,c_1}\n+#   Commit A: y/b + untracked y/{c,d,e}\n+#   Commit B: z/{b,d_1,e}\n+#   Expected: Failed Merge; y/b + untracked y/c + untracked y/d on disk +\n+#             z/c_1 -> z/d_1 rename recorded at stage 3 for y/d +\n+#       ERROR_MSG(refusing to lose untracked file at 'y/d')\n+\n+test_expect_success '10b-setup: Overwrite untracked with dir rename + delete' '\n+\ttest_create_repo 10b &&\n+\t(\n+\t\tcd 10b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit rm z/c &&\n+\t\tgit mv z/ y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z/c z/d &&\n+\t\techo e >z/e &&\n+\t\tgit add z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '10b-check: Overwrite untracked with dir rename + delete' '\n+\t(\n+\t\tcd 10b &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo very >y/c &&\n+\t\techo important >y/d &&\n+\t\techo contents >y/e &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/delete).*Version B^0 of y/d left in tree at y/d~B^0\" out &&\n+\t\ttest_i18ngrep \"Error: Refusing to lose untracked file at y/e; writing to y/e~B^0 instead\" out &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 5 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\ttest $(git rev-parse :0:y/b) = $(git rev-parse O:z/b) &&\n+\t\ttest \"very\" = \"$(cat y/c)\" &&\n+\n+\t\ttest \"important\" = \"$(cat y/d)\" &&\n+\t\ttest \"important\" != \"$(git rev-parse :3:y/d)\" &&\n+\t\ttest $(git rev-parse :3:y/d) = $(git rev-parse O:z/c) &&\n+\n+\t\ttest \"contents\" = \"$(cat y/e)\" &&\n+\t\ttest \"contents\" != \"$(git rev-parse :3:y/e)\" &&\n+\t\ttest $(git rev-parse :3:y/e) = $(git rev-parse B:z/e)\n+\t)\n+'\n+\n+# Testcase 10c, Overwrite untracked: dir rename/rename(1to2)\n+#   Commit O: z/{a,b}, x/{c,d}\n+#   Commit A: y/{a,b}, w/c, x/d + different untracked y/c\n+#   Commit B: z/{a,b,c}, x/d\n+#   Expected: Failed Merge; y/{a,b} + x/d + untracked y/c +\n+#             CONFLICT(rename/rename) x/c -> w/c vs y/c +\n+#             y/c~B^0 +\n+#             ERROR_MSG(Refusing to lose untracked file at y/c)\n+\n+test_expect_success '10c-setup: Overwrite untracked with dir rename/rename(1to2)' '\n+\ttest_create_repo 10c &&\n+\t(\n+\t\tcd 10c &&\n+\n+\t\tmkdir z x &&\n+\t\techo a >z/a &&\n+\t\techo b >z/b &&\n+\t\techo c >x/c &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir w &&\n+\t\tgit mv x/c w/c &&\n+\t\tgit mv z/ y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/c z/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '10c-check: Overwrite untracked with dir rename/rename(1to2)' '\n+\t(\n+\t\tcd 10c &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo important >y/c &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename)\" out &&\n+\t\ttest_i18ngrep \"Refusing to lose untracked file at y/c; adding as y/c~B^0 instead\" out &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/a :0:y/b :0:x/d :1:x/c :2:w/c :3:y/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/a O:z/b O:x/d O:x/c O:x/c O:x/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest \"important\" = \"$(cat y/c)\" &&\n+\t\ttest \"important\" != \"$(git rev-parse :3:y/c)\" &&\n+\t\ttest $(git hash-object y/c~B^0) = $(git rev-parse O:x/c)\n+\t)\n+'\n+\n+# Testcase 10d, Delete untracked w/ dir rename/rename(2to1)\n+#   Commit O: z/{a,b,c_1},        x/{d,e,f_2}\n+#   Commit A: y/{a,b},            x/{d,e,f_2,wham_1} + untracked y/wham\n+#   Commit B: z/{a,b,c_1,wham_2}, y/{d,e}\n+#   Expected: Failed Merge; y/{a,b,d,e} + untracked y/{wham,wham~B^0,wham~HEAD}+\n+#             CONFLICT(rename/rename) z/c_1 vs x/f_2 -> y/wham\n+#             ERROR_MSG(Refusing to lose untracked file at y/wham)\n+\n+test_expect_success '10d-setup: Delete untracked with dir rename/rename(2to1)' '\n+\ttest_create_repo 10d &&\n+\t(\n+\t\tcd 10d &&\n+\n+\t\tmkdir z x &&\n+\t\techo a >z/a &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d >x/d &&\n+\t\techo e >x/e &&\n+\t\techo f >x/f &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z/c x/wham &&\n+\t\tgit mv z/ y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/f z/wham &&\n+\t\tgit mv x/ y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '10d-check: Delete untracked with dir rename/rename(2to1)' '\n+\t(\n+\t\tcd 10d &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo important >y/wham &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename)\" out &&\n+\t\ttest_i18ngrep \"Refusing to lose untracked file at y/wham\" out &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/a :0:y/b :0:y/d :0:y/e :2:y/wham :3:y/wham &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/a O:z/b O:x/d O:x/e O:z/c O:x/f &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_must_fail git rev-parse :1:y/wham &&\n+\n+\t\ttest \"important\" = \"$(cat y/wham)\" &&\n+\t\tgit hash-object >actual \\\n+\t\t\ty/wham~B^0 y/wham~HEAD &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/f O:z/c &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 10e, Does git complain about untracked file that's not in the way?\n+#   Commit O: z/{a,b}\n+#   Commit A: y/{a,b} + untracked z/c\n+#   Commit B: z/{a,b,c}\n+#   Expected: y/{a,b,c} + untracked z/c\n+\n+test_expect_success '10e-setup: Does git complain about untracked file that is not really in the way?' '\n+\ttest_create_repo 10e &&\n+\t(\n+\t\tcd 10e &&\n+\n+\t\tmkdir z &&\n+\t\techo a >z/a &&\n+\t\techo b >z/b &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z/ y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo c >z/c &&\n+\t\tgit add z/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '10e-check: Does git complain about untracked file that is not really in the way?' '\n+\t(\n+\t\tcd 10e &&\n+\n+\t\tgit checkout A^0 &&\n+\t\tmkdir z &&\n+\t\techo random >z/c &&\n+\n+\t\tgit merge -s recursive B^0 >out 2>err &&\n+\t! test_i18ngrep \"following untracked working tree files would be overwritten by merge\" err &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/a :0:y/b :0:y/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/a O:z/b B:z/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest \"random\" = \"$(cat z/c)\"\n+\t)\n+'\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333724","messageId":"20171129014237.32570-6-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 05/34] directory rename detection: directory splitting testcases","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:08Z","receivedAt":"2017-11-29T01:43:21Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 137 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 137 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex d8ead7c56b..335aa1c145 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -427,4 +427,141 @@ test_expect_failure '1f-check: Split a directory into two other directories' '\n #   in section 2, plus testcases 3a and 4a.\n ###########################################################################\n \n+\n+###########################################################################\n+# SECTION 2: Split into multiple directories, with equal number of paths\n+#\n+# Explore the splitting-a-directory rules a bit; what happens in the\n+# edge cases?\n+#\n+# Note that there is a closely related case of a directory not being\n+# split on either side of history, but being renamed differently on\n+# each side.  See testcase 8e for that.\n+###########################################################################\n+\n+# Testcase 2a, Directory split into two on one side, with equal numbers of paths\n+#   Commit O: z/{b,c}\n+#   Commit A: y/b, w/c\n+#   Commit B: z/{b,c,d}\n+#   Expected: y/b, w/c, z/d, with warning about z/ -> (y/ vs. w/) conflict\n+test_expect_success '2a-setup: Directory split into two on one side, with equal numbers of paths' '\n+\ttest_create_repo 2a &&\n+\t(\n+\t\tcd 2a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir y &&\n+\t\tmkdir w &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c w/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo d >z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '2a-check: Directory split into two on one side, with equal numbers of paths' '\n+\t(\n+\t\tcd 2a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:w/c :0:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/d &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_i18ngrep \"CONFLICT.*directory rename split\" out\n+\t)\n+'\n+\n+# Testcase 2b, Directory split into two on one side, with equal numbers of paths\n+#   Commit O: z/{b,c}\n+#   Commit A: y/b, w/c\n+#   Commit B: z/{b,c}, x/d\n+#   Expected: y/b, w/c, x/d; No warning about z/ -> (y/ vs. w/) conflict\n+test_expect_success '2b-setup: Directory split into two on one side, with equal numbers of paths' '\n+\ttest_create_repo 2b &&\n+\t(\n+\t\tcd 2b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir y &&\n+\t\tmkdir w &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c w/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir x &&\n+\t\techo d >x/d &&\n+\t\tgit add x/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '2b-check: Directory split into two on one side, with equal numbers of paths' '\n+\t(\n+\t\tcd 2b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 >out &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:w/c :0:x/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:x/d &&\n+\t\ttest_cmp expect actual &&\n+\t\t! test_i18ngrep \"CONFLICT.*directory rename split\" out\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by section 2:\n+#\n+#   None; the rule was already covered in section 1.  These testcases are\n+#   here just to make sure the conflict resolution and necessary warning\n+#   messages are handled correctly.\n+###########################################################################\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333725","messageId":"20171129014237.32570-13-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 12/34] directory rename detection: miscellaneous testcases to complete coverage","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:15Z","receivedAt":"2017-11-29T01:43:23Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"I came up with the testcases in the first eight sections before coding up\nthe implementation.  The testcases in this section were mostly ones I\nthought of while coding/debugging, and which I was too lazy to insert\ninto the previous sections because I didn't want to re-label with all the\ntestcase references.  :-)\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 536 +++++++++++++++++++++++++++++++++++-\n 1 file changed, 535 insertions(+), 1 deletion(-)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex fc9b13c37d..42228a60aa 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -295,6 +295,7 @@ test_expect_failure '1d-check: Directory renames cause a rename/rename(2to1) con\n '\n \n # Testcase 1e, Renamed directory, with all filenames being renamed too\n+#   (Related to testcases 9f & 9g)\n #   Commit O: z/{oldb,oldc}\n #   Commit A: y/{newb,newc}\n #   Commit B: z/{oldb,oldc,d}\n@@ -575,7 +576,7 @@ test_expect_success '2b-check: Directory split into two on one side, with equal\n ###########################################################################\n \n # Testcase 3a, Avoid implicit rename if involved as source on other side\n-#   (Related to testcases 1c and 1f)\n+#   (Related to testcases 1c, 1f, and 9h)\n #   Commit O: z/{b,c,d}\n #   Commit A: z/{b,c,d} (no change)\n #   Commit B: y/{b,c}, x/d\n@@ -2234,4 +2235,537 @@ test_expect_failure '8e-check: Both sides rename, one side adds to original dire\n \t)\n '\n \n+###########################################################################\n+# SECTION 9: Other testcases\n+#\n+# This section consists of miscellaneous testcases I thought of during\n+# the implementation which round out the testing.\n+###########################################################################\n+\n+# Testcase 9a, Inner renamed directory within outer renamed directory\n+#   (Related to testcase 1f)\n+#   Commit O: z/{b,c,d/{e,f,g}}\n+#   Commit A: y/{b,c}, x/w/{e,f,g}\n+#   Commit B: z/{b,c,d/{e,f,g,h},i}\n+#   Expected: y/{b,c,i}, x/w/{e,f,g,h}\n+#   NOTE: The only reason this one is interesting is because when a directory\n+#         is split into multiple other directories, we determine by the weight\n+#         of which one had the most paths going to it.  A naive implementation\n+#         of that could take the new file in commit B at z/i to x/w/i or x/i.\n+\n+test_expect_success '9a-setup: Inner renamed directory within outer renamed directory' '\n+\ttest_create_repo 9a &&\n+\t(\n+\t\tcd 9a &&\n+\n+\t\tmkdir -p z/d &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo e >z/d/e &&\n+\t\techo f >z/d/f &&\n+\t\techo g >z/d/g &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir x &&\n+\t\tgit mv z/d x/w &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo h >z/d/h &&\n+\t\techo i >z/i &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9a-check: Inner renamed directory within outer renamed directory' '\n+\t(\n+\t\tcd 9a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 7 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/i &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/i &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:x/w/e HEAD:x/w/f HEAD:x/w/g HEAD:x/w/h &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/d/e O:z/d/f O:z/d/g B:z/d/h &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 9b, Transitive rename with content merge\n+#   (Related to testcase 1c)\n+#   Commit O: z/{b,c},   x/d_1\n+#   Commit A: y/{b,c},   x/d_2\n+#   Commit B: z/{b,c,d_3}\n+#   Expected: y/{b,c,d_merged}\n+\n+test_expect_success '9b-setup: Transitive rename with content merge' '\n+\ttest_create_repo 9b &&\n+\t(\n+\t\tcd 9b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\ttest_seq 1 10 >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\ttest_seq 1 11 >x/d &&\n+\t\tgit add x/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\ttest_seq 0 10 >x/d &&\n+\t\tgit mv x/d z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9b-check: Transitive rename with content merge' '\n+\t(\n+\t\tcd 9b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\ttest_seq 0 11 >expected &&\n+\t\tgit add expected &&\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c :0:expected &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git rev-parse HEAD:x/d &&\n+\t\ttest_must_fail git rev-parse HEAD:z/d &&\n+\t\ttest ! -f z/d &&\n+\n+\t\ttest $(git rev-parse HEAD:y/d) != $(git rev-parse O:x/d) &&\n+\t\ttest $(git rev-parse HEAD:y/d) != $(git rev-parse A:x/d) &&\n+\t\ttest $(git rev-parse HEAD:y/d) != $(git rev-parse B:z/d) &&\n+\t\ttest_cmp expected y/d\n+\t)\n+'\n+\n+# Testcase 9c, Doubly transitive rename?\n+#   (Related to testcase 1c, 7e, and 9d)\n+#   Commit O: z/{b,c},     x/{d,e},    w/f\n+#   Commit A: y/{b,c},     x/{d,e,f,g}\n+#   Commit B: z/{b,c,d,e},             w/f\n+#   Expected: y/{b,c,d,e}, x/{f,g}\n+#\n+#   NOTE: x/f and x/g may be slightly confusing here.  The rename from w/f to\n+#         x/f is clear.  Let's look beyond that.  Here's the logic:\n+#            Commit B renamed x/ -> z/\n+#            Commit A renamed z/ -> y/\n+#         So, we could possibly further rename x/f to z/f to y/f, a doubly\n+#         transient rename.  However, where does it end?  We can chain these\n+#         indefinitely (see testcase 9d).  What if there is a D/F conflict\n+#         at z/f/ or y/f/?  Or just another file conflict at one of those\n+#         paths?  In the case of an N-long chain of transient renamings,\n+#         where do we \"abort\" the rename at?  Can the user make sense of\n+#         the resulting conflict and resolve it?\n+#\n+#         To avoid this confusion I use the simple rule that if the other side\n+#         of history did a directory rename to a path that your side renamed\n+#         away, then ignore that particular rename from the other side of\n+#         history for any implicit directory renames.\n+\n+test_expect_success '9c-setup: Doubly transitive rename?' '\n+\ttest_create_repo 9c &&\n+\t(\n+\t\tcd 9c &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d >x/d &&\n+\t\techo e >x/e &&\n+\t\tmkdir w &&\n+\t\techo f >w/f &&\n+\t\tgit add z x w &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\tgit mv w/f x/ &&\n+\t\techo g >x/g &&\n+\t\tgit add x/g &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/d &&\n+\t\tgit mv x/e z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9c-check: Doubly transitive rename?' '\n+\t(\n+\t\tcd 9c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"WARNING: Avoiding applying x -> z rename to x/f\" out &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e HEAD:x/f HEAD:x/g &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:x/d O:x/e O:w/f A:x/g &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 9d, N-fold transitive rename?\n+#   (Related to testcase 9c...and 1c and 7e)\n+#   Commit O: z/a, y/b, x/c, w/d, v/e, u/f\n+#   Commit A:  y/{a,b},  w/{c,d},  u/{e,f}\n+#   Commit B: z/{a,t}, x/{b,c}, v/{d,e}, u/f\n+#   Expected: <see NOTE first>\n+#\n+#   NOTE: z/ -> y/ (in commit A)\n+#         y/ -> x/ (in commit B)\n+#         x/ -> w/ (in commit A)\n+#         w/ -> v/ (in commit B)\n+#         v/ -> u/ (in commit A)\n+#         So, if we add a file to z, say z/t, where should it end up?  In u?\n+#         What if there's another file or directory named 't' in one of the\n+#         intervening directories and/or in u itself?  Also, shouldn't the\n+#         same logic that places 't' in u/ also move ALL other files to u/?\n+#         What if there are file or directory conflicts in any of them?  If\n+#         we attempted to do N-way (N-fold? N-ary? N-uple?) transitive renames\n+#         like this, would the user have any hope of understanding any\n+#         conflicts or how their working tree ended up?  I think not, so I'm\n+#         ruling out N-ary transitive renames for N>1.\n+#\n+#   Therefore our expected result is:\n+#     z/t, y/a, x/b, w/c, u/d, u/e, u/f\n+#   The reason that v/d DOES get transitively renamed to u/d is that u/ isn't\n+#   renamed somewhere.  A slightly sub-optimal result, but it uses fairly\n+#   simple rules that are consistent with what we need for all the other\n+#   testcases and simplifies things for the user.\n+\n+test_expect_success '9d-setup: N-way transitive rename?' '\n+\ttest_create_repo 9d &&\n+\t(\n+\t\tcd 9d &&\n+\n+\t\tmkdir z y x w v u &&\n+\t\techo a >z/a &&\n+\t\techo b >y/b &&\n+\t\techo c >x/c &&\n+\t\techo d >w/d &&\n+\t\techo e >v/e &&\n+\t\techo f >u/f &&\n+\t\tgit add z y x w v u &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z/a y/ &&\n+\t\tgit mv x/c w/ &&\n+\t\tgit mv v/e u/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo t >z/t &&\n+\t\tgit mv y/b x/ &&\n+\t\tgit mv w/d v/ &&\n+\t\tgit add z/t &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9d-check: N-way transitive rename?' '\n+\t(\n+\t\tcd 9d &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"WARNING: Avoiding applying z -> y rename to z/t\" out &&\n+\t\ttest_i18ngrep \"WARNING: Avoiding applying y -> x rename to y/a\" out &&\n+\t\ttest_i18ngrep \"WARNING: Avoiding applying x -> w rename to x/b\" out &&\n+\t\ttest_i18ngrep \"WARNING: Avoiding applying w -> v rename to w/c\" out &&\n+\n+\t\ttest 7 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:z/t HEAD:y/a HEAD:x/b HEAD:w/c HEAD:u/d HEAD:u/e HEAD:u/f &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tB:z/t O:z/a O:y/b O:x/c O:w/d O:v/e A:u/f &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 9e, N-to-1 whammo\n+#   (Related to testcase 9c...and 1c and 7e)\n+#   Commit O: dir1/{a,b}, dir2/{d,e}, dir3/{g,h}, dirN/{j,k}\n+#   Commit A: dir1/{a,b,c,yo}, dir2/{d,e,f,yo}, dir3/{g,h,i,yo}, dirN/{j,k,l,yo}\n+#   Commit B: combined/{a,b,d,e,g,h,j,k}\n+#   Expected: combined/{a,b,c,d,e,f,g,h,i,j,k,l}, CONFLICT(Nto1) warnings,\n+#             dir1/yo, dir2/yo, dir3/yo, dirN/yo\n+\n+test_expect_success '9e-setup: N-to-1 whammo' '\n+\ttest_create_repo 9e &&\n+\t(\n+\t\tcd 9e &&\n+\n+\t\tmkdir dir1 dir2 dir3 dirN &&\n+\t\techo a >dir1/a &&\n+\t\techo b >dir1/b &&\n+\t\techo d >dir2/d &&\n+\t\techo e >dir2/e &&\n+\t\techo g >dir3/g &&\n+\t\techo h >dir3/h &&\n+\t\techo j >dirN/j &&\n+\t\techo k >dirN/k &&\n+\t\tgit add dir* &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\techo c  >dir1/c &&\n+\t\techo yo >dir1/yo &&\n+\t\techo f  >dir2/f &&\n+\t\techo yo >dir2/yo &&\n+\t\techo i  >dir3/i &&\n+\t\techo yo >dir3/yo &&\n+\t\techo l  >dirN/l &&\n+\t\techo yo >dirN/yo &&\n+\t\tgit add dir* &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv dir1 combined &&\n+\t\tgit mv dir2/* combined/ &&\n+\t\tgit mv dir3/* combined/ &&\n+\t\tgit mv dirN/* combined/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9e-check: N-to-1 whammo' '\n+\t(\n+\t\tcd 9e &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (implicit dir rename): Cannot map more than one path to combined/yo\" out >error_line &&\n+\t\tgrep -q dir1/yo error_line &&\n+\t\tgrep -q dir2/yo error_line &&\n+\t\tgrep -q dir3/yo error_line &&\n+\t\tgrep -q dirN/yo error_line &&\n+\n+\t\ttest 16 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:combined/a :0:combined/b :0:combined/c \\\n+\t\t\t:0:combined/d :0:combined/e :0:combined/f \\\n+\t\t\t:0:combined/g :0:combined/h :0:combined/i \\\n+\t\t\t:0:combined/j :0:combined/k :0:combined/l &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:dir1/a O:dir1/b A:dir1/c \\\n+\t\t\tO:dir2/d O:dir2/e A:dir2/f \\\n+\t\t\tO:dir3/g O:dir3/h A:dir3/i \\\n+\t\t\tO:dirN/j O:dirN/k A:dirN/l &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:dir1/yo :0:dir2/yo :0:dir3/yo :0:dirN/yo &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tA:dir1/yo A:dir2/yo A:dir3/yo A:dirN/yo &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 9f, Renamed directory that only contained immediate subdirs\n+#   (Related to testcases 1e & 9g)\n+#   Commit O: goal/{a,b}/$more_files\n+#   Commit A: priority/{a,b}/$more_files\n+#   Commit B: goal/{a,b}/$more_files, goal/c\n+#   Expected: priority/{a,b}/$more_files, priority/c\n+\n+test_expect_success '9f-setup: Renamed directory that only contained immediate subdirs' '\n+\ttest_create_repo 9f &&\n+\t(\n+\t\tcd 9f &&\n+\n+\t\tmkdir -p goal/a &&\n+\t\tmkdir -p goal/b &&\n+\t\techo foo >goal/a/foo &&\n+\t\techo bar >goal/b/bar &&\n+\t\techo baz >goal/b/baz &&\n+\t\tgit add goal &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv goal/ priority &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo c >goal/c &&\n+\t\tgit add goal/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9f-check: Renamed directory that only contained immediate subdirs' '\n+\t(\n+\t\tcd 9f &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:priority/a/foo HEAD:priority/b/bar HEAD:priority/b/baz HEAD:priority/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:goal/a/foo O:goal/b/bar O:goal/b/baz B:goal/c &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git rev-parse HEAD:goal/c\n+\t)\n+'\n+\n+# Testcase 9g, Renamed directory that only contained immediate subdirs, immediate subdirs renamed\n+#   (Related to testcases 1e & 9f)\n+#   Commit O: goal/{a,b}/$more_files\n+#   Commit A: priority/{alpha,bravo}/$more_files\n+#   Commit B: goal/{a,b}/$more_files, goal/c\n+#   Expected: priority/{alpha,bravo}/$more_files, priority/c\n+\n+test_expect_success '9g-setup: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '\n+\ttest_create_repo 9g &&\n+\t(\n+\t\tcd 9g &&\n+\n+\t\tmkdir -p goal/a &&\n+\t\tmkdir -p goal/b &&\n+\t\techo foo >goal/a/foo &&\n+\t\techo bar >goal/b/bar &&\n+\t\techo baz >goal/b/baz &&\n+\t\tgit add goal &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir priority &&\n+\t\tgit mv goal/a/ priority/alpha &&\n+\t\tgit mv goal/b/ priority/beta &&\n+\t\trmdir goal/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo c >goal/c &&\n+\t\tgit add goal/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9g-check: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '\n+\t(\n+\t\tcd 9g &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:priority/alpha/foo HEAD:priority/beta/bar HEAD:priority/beta/baz HEAD:priority/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:goal/a/foo O:goal/b/bar O:goal/b/baz B:goal/c &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git rev-parse HEAD:goal/c\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by section 9:\n+#\n+#   If the other side of history did a directory rename to a path that your\n+#   side renamed away, then ignore that particular rename from the other\n+#   side of history for any implicit directory renames.\n+###########################################################################\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333726","messageId":"20171129014237.32570-20-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 19/34] merge-recursive: split out code for determining diff_filepairs","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:22Z","receivedAt":"2017-11-29T01:43:26Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Create a new function, get_diffpairs() to compute the diff_filepairs\nbetween two trees.  While these are currently only used in\nget_renames(), I want them to be available to some new functions.  No\nactual logic changes yet.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 86 +++++++++++++++++++++++++++++++++++++++++--------------\n 1 file changed, 64 insertions(+), 22 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex da7c67eb82..4adff2d538 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1312,24 +1312,15 @@ static int conflict_rename_rename_2to1(struct merge_options *o,\n }\n \n /*\n- * Get information of all renames which occurred between 'o_tree' and\n- * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and\n- * 'b_tree') to be able to associate the correct cache entries with\n- * the rename information. 'tree' is always equal to either a_tree or b_tree.\n+ * Get the diff_filepairs changed between o_tree and tree.\n  */\n-static struct string_list *get_renames(struct merge_options *o,\n-\t\t\t\t       struct tree *tree,\n-\t\t\t\t       struct tree *o_tree,\n-\t\t\t\t       struct tree *a_tree,\n-\t\t\t\t       struct tree *b_tree,\n-\t\t\t\t       struct string_list *entries)\n+static struct diff_queue_struct *get_diffpairs(struct merge_options *o,\n+\t\t\t\t\t       struct tree *o_tree,\n+\t\t\t\t\t       struct tree *tree)\n {\n-\tint i;\n-\tstruct string_list *renames;\n+\tstruct diff_queue_struct *ret;\n \tstruct diff_options opts;\n \n-\trenames = xcalloc(1, sizeof(struct string_list));\n-\n \tdiff_setup(&opts);\n \topts.flags.recursive = 1;\n \topts.flags.rename_empty = 0;\n@@ -1345,10 +1336,43 @@ static struct string_list *get_renames(struct merge_options *o,\n \tdiffcore_std(&opts);\n \tif (opts.needed_rename_limit > o->needed_rename_limit)\n \t\to->needed_rename_limit = opts.needed_rename_limit;\n-\tfor (i = 0; i < diff_queued_diff.nr; ++i) {\n+\n+\tret = malloc(sizeof(struct diff_queue_struct));\n+\tret->queue = diff_queued_diff.queue;\n+\tret->nr = diff_queued_diff.nr;\n+\t/* Ignore diff_queued_diff.alloc; we won't be changing size at all */\n+\n+\topts.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\tdiff_queued_diff.nr = 0;\n+\tdiff_queued_diff.queue = NULL;\n+\tdiff_flush(&opts);\n+\treturn ret;\n+}\n+\n+/*\n+ * Get information of all renames which occurred in 'pairs', making use of\n+ * any implicit directory renames inferred from the other side of history.\n+ * We need the three trees in the merge ('o_tree', 'a_tree' and 'b_tree')\n+ * to be able to associate the correct cache entries with the rename\n+ * information; tree is always equal to either a_tree or b_tree.\n+ */\n+static struct string_list *get_renames(struct merge_options *o,\n+\t\t\t\t       struct diff_queue_struct *pairs,\n+\t\t\t\t       struct tree *tree,\n+\t\t\t\t       struct tree *o_tree,\n+\t\t\t\t       struct tree *a_tree,\n+\t\t\t\t       struct tree *b_tree,\n+\t\t\t\t       struct string_list *entries)\n+{\n+\tint i;\n+\tstruct string_list *renames;\n+\n+\trenames = xcalloc(1, sizeof(struct string_list));\n+\n+\tfor (i = 0; i < pairs->nr; ++i) {\n \t\tstruct string_list_item *item;\n \t\tstruct rename *re;\n-\t\tstruct diff_filepair *pair = diff_queued_diff.queue[i];\n+\t\tstruct diff_filepair *pair = pairs->queue[i];\n \n \t\tif (pair->status != 'R') {\n \t\t\tdiff_free_filepair(pair);\n@@ -1373,9 +1397,6 @@ static struct string_list *get_renames(struct merge_options *o,\n \t\titem = string_list_insert(renames, pair->one->path);\n \t\titem->util = re;\n \t}\n-\topts.output_format = DIFF_FORMAT_NO_OUTPUT;\n-\tdiff_queued_diff.nr = 0;\n-\tdiff_flush(&opts);\n \treturn renames;\n }\n \n@@ -1646,15 +1667,36 @@ static int handle_renames(struct merge_options *o,\n \t\t\t  struct string_list *entries,\n \t\t\t  struct rename_info *ri)\n {\n+\tstruct diff_queue_struct *head_pairs, *merge_pairs;\n+\tint clean;\n+\n \tri->head_renames = NULL;\n \tri->merge_renames = NULL;\n \n \tif (!o->detect_rename)\n \t\treturn 1;\n \n-\tri->head_renames  = get_renames(o, head, common, head, merge, entries);\n-\tri->merge_renames = get_renames(o, merge, common, head, merge, entries);\n-\treturn process_renames(o, ri->head_renames, ri->merge_renames);\n+\thead_pairs = get_diffpairs(o, common, head);\n+\tmerge_pairs = get_diffpairs(o, common, merge);\n+\n+\tri->head_renames  = get_renames(o, head_pairs, head,\n+\t\t\t\t\t common, head, merge, entries);\n+\tri->merge_renames = get_renames(o, merge_pairs, merge,\n+\t\t\t\t\t common, head, merge, entries);\n+\tclean = process_renames(o, ri->head_renames, ri->merge_renames);\n+\n+\t/*\n+\t * Some cleanup is deferred until cleanup_renames() because the\n+\t * data structures are still needed and referenced in\n+\t * process_entry().  But there are a few things we can free now.\n+\t */\n+\n+\tfree(head_pairs->queue);\n+\tfree(head_pairs);\n+\tfree(merge_pairs->queue);\n+\tfree(merge_pairs);\n+\n+\treturn clean;\n }\n \n static void cleanup_rename(struct string_list *rename)\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333727","messageId":"20171129014237.32570-23-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 22/34] merge-recursive: add get_directory_renames()","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:25Z","receivedAt":"2017-11-29T01:43:28Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"This populates a list of directory renames for us.  The list of\ndirectory renames is not yet used, but will be in subsequent commits.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 152 insertions(+), 3 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex c5932d5c57..6aef357e78 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1384,6 +1384,138 @@ static struct diff_queue_struct *get_diffpairs(struct merge_options *o,\n \treturn ret;\n }\n \n+static void get_renamed_dir_portion(const char *old_path, const char *new_path,\n+\t\t\t\t    char **old_dir, char **new_dir)\n+{\n+\tchar *end_of_old, *end_of_new;\n+\tint old_len, new_len;\n+\n+\t*old_dir = NULL;\n+\t*new_dir = NULL;\n+\n+\t/* For\n+\t *    \"a/b/c/d/foo.c\" -> \"a/b/something-else/d/foo.c\"\n+\t * the \"d/foo.c\" part is the same, we just want to know that\n+\t *    \"a/b/c\" was renamed to \"a/b/something-else\"\n+\t * so, for this example, this function returns \"a/b/c\" in\n+\t * *old_dir and \"a/b/something-else\" in *new_dir.\n+\t *\n+\t * Also, if the basename of the file changed, we don't care.  We\n+\t * want to know which portion of the directory, if any, changed.\n+\t */\n+\tend_of_old = strrchr(old_path, '/');\n+\tend_of_new = strrchr(new_path, '/');\n+\n+\tif (end_of_old == NULL || end_of_new == NULL)\n+\t\treturn;\n+\twhile (*--end_of_new == *--end_of_old &&\n+\t       end_of_old != old_path &&\n+\t       end_of_new != new_path)\n+\t\t; /* Do nothing; all in the while loop */\n+\t/*\n+\t * We've found the first non-matching character in the directory\n+\t * paths.  That means the current directory we were comparing\n+\t * represents the rename.  Move end_of_old and end_of_new back\n+\t * to the full directory name.\n+\t */\n+\tif (*end_of_old == '/')\n+\t\tend_of_old++;\n+\tif (*end_of_old != '/')\n+\t\tend_of_new++;\n+\tend_of_old = strchr(end_of_old, '/');\n+\tend_of_new = strchr(end_of_new, '/');\n+\n+\t/*\n+\t * It may have been the case that old_path and new_path were the same\n+\t * directory all along.  Don't claim a rename if they're the same.\n+\t */\n+\told_len = end_of_old - old_path;\n+\tnew_len = end_of_new - new_path;\n+\n+\tif (old_len != new_len || strncmp(old_path, new_path, old_len)) {\n+\t\t*old_dir = xstrndup(old_path, old_len);\n+\t\t*new_dir = xstrndup(new_path, new_len);\n+\t}\n+}\n+\n+static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,\n+\t\t\t\t\t     struct tree *tree)\n+{\n+\tstruct hashmap *dir_renames;\n+\tstruct hashmap_iter iter;\n+\tstruct dir_rename_entry *entry;\n+\tint i;\n+\n+\tdir_renames = malloc(sizeof(struct hashmap));\n+\tdir_rename_init(dir_renames);\n+\tfor (i = 0; i < pairs->nr; ++i) {\n+\t\tstruct string_list_item *item;\n+\t\tint *count;\n+\t\tstruct diff_filepair *pair = pairs->queue[i];\n+\t\tchar *old_dir, *new_dir;\n+\n+\t\t/* File not part of directory rename if it wasn't renamed */\n+\t\tif (pair->status != 'R')\n+\t\t\tcontinue;\n+\n+\t\tget_renamed_dir_portion(pair->one->path, pair->two->path,\n+\t\t\t\t\t&old_dir,        &new_dir);\n+\t\tif (!old_dir)\n+\t\t\t/* Directory didn't change at all; ignore this one. */\n+\t\t\tcontinue;\n+\n+\t\tentry = dir_rename_find_entry(dir_renames, old_dir);\n+\t\tif (!entry) {\n+\t\t\tentry = xmalloc(sizeof(struct dir_rename_entry));\n+\t\t\tdir_rename_entry_init(entry, old_dir);\n+\t\t\thashmap_put(dir_renames, entry);\n+\t\t} else {\n+\t\t\tfree(old_dir);\n+\t\t}\n+\t\titem = string_list_lookup(&entry->possible_new_dirs, new_dir);\n+\t\tif (!item) {\n+\t\t\titem = string_list_insert(&entry->possible_new_dirs,\n+\t\t\t\t\t\t  new_dir);\n+\t\t\titem->util = xcalloc(1, sizeof(int));\n+\t\t} else {\n+\t\t\tfree(new_dir);\n+\t\t}\n+\t\tcount = item->util;\n+\t\t*count += 1;\n+\t}\n+\n+\thashmap_iter_init(dir_renames, &iter);\n+\twhile ((entry = hashmap_iter_next(&iter))) {\n+\t\tint max = 0;\n+\t\tint bad_max = 0;\n+\t\tchar *best = NULL;\n+\n+\t\tfor (i = 0; i < entry->possible_new_dirs.nr; i++) {\n+\t\t\tint *count = entry->possible_new_dirs.items[i].util;\n+\n+\t\t\tif (*count == max)\n+\t\t\t\tbad_max = max;\n+\t\t\telse if (*count > max) {\n+\t\t\t\tmax = *count;\n+\t\t\t\tbest = entry->possible_new_dirs.items[i].string;\n+\t\t\t}\n+\t\t}\n+\t\tif (bad_max == max)\n+\t\t\tentry->non_unique_new_dir = 1;\n+\t\telse {\n+\t\t\tassert(entry->new_dir.len == 0);\n+\t\t\tstrbuf_addstr(&entry->new_dir, best);\n+\t\t}\n+\t\t/* Strings were xstrndup'ed before inserting into string-list,\n+\t\t * so ask string_list to remove the entries for us.\n+\t\t */\n+\t\tentry->possible_new_dirs.strdup_strings = 1;\n+\t\tstring_list_clear(&entry->possible_new_dirs, 1);\n+\t}\n+\n+\treturn dir_renames;\n+}\n+\n /*\n  * Get information of all renames which occurred in 'pairs', making use of\n  * any implicit directory renames inferred from the other side of history.\n@@ -1695,8 +1827,21 @@ struct rename_info {\n \tstruct string_list *merge_renames;\n };\n \n-static void initial_cleanup_rename(struct diff_queue_struct *pairs)\n+static void initial_cleanup_rename(struct diff_queue_struct *pairs,\n+\t\t\t\t   struct hashmap *dir_renames)\n {\n+\tstruct hashmap_iter iter;\n+\tstruct dir_rename_entry *e;\n+\n+\thashmap_iter_init(dir_renames, &iter);\n+\twhile ((e = hashmap_iter_next(&iter))) {\n+\t\tfree(e->dir);\n+\t\tstrbuf_release(&e->new_dir);\n+\t\t/* possible_new_dirs already cleared in get_directory_renames */\n+\t}\n+\thashmap_free(dir_renames, 1);\n+\tfree(dir_renames);\n+\n \tfree(pairs->queue);\n \tfree(pairs);\n }\n@@ -1709,6 +1854,7 @@ static int handle_renames(struct merge_options *o,\n \t\t\t  struct rename_info *ri)\n {\n \tstruct diff_queue_struct *head_pairs, *merge_pairs;\n+\tstruct hashmap *dir_re_head, *dir_re_merge;\n \tint clean;\n \n \tri->head_renames = NULL;\n@@ -1720,6 +1866,9 @@ static int handle_renames(struct merge_options *o,\n \thead_pairs = get_diffpairs(o, common, head);\n \tmerge_pairs = get_diffpairs(o, common, merge);\n \n+\tdir_re_head = get_directory_renames(head_pairs, head);\n+\tdir_re_merge = get_directory_renames(merge_pairs, merge);\n+\n \tri->head_renames  = get_renames(o, head_pairs, head,\n \t\t\t\t\t common, head, merge, entries);\n \tri->merge_renames = get_renames(o, merge_pairs, merge,\n@@ -1731,8 +1880,8 @@ static int handle_renames(struct merge_options *o,\n \t * data structures are still needed and referenced in\n \t * process_entry().  But there are a few things we can free now.\n \t */\n-\tinitial_cleanup_rename(head_pairs);\n-\tinitial_cleanup_rename(merge_pairs);\n+\tinitial_cleanup_rename(head_pairs, dir_re_head);\n+\tinitial_cleanup_rename(merge_pairs, dir_re_merge);\n \n \treturn clean;\n }\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333728","messageId":"20171129014237.32570-3-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 02/34] merge-recursive: fix logic ordering issue","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:05Z","receivedAt":"2017-11-29T01:43:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"merge_trees() did a variety of work, including:\n  * Calling get_unmerged() to get unmerged entries\n  * Calling record_df_conflict_files() with all unmerged entries to\n    do some work to ensure we could handle D/F conflicts correctly\n  * Calling get_renames() to check for renames.\n\nAn easily overlooked issue is that get_renames() can create more\nunmerged entries and add them to the list, which have the possibility of\nbeing involved in D/F conflicts.  So the call to\nrecord_df_conflict_files() should really be moved after all the rename\ndetection.  I didn't come up with any testcases demonstrating any bugs\nwith the old ordering, but I suspect there were some for both normal\nrenames and for directory renames.  Fix the ordering.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d00b274381..98c84e73d3 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1982,10 +1982,10 @@ int merge_trees(struct merge_options *o,\n \t\tget_files_dirs(o, merge);\n \n \t\tentries = get_unmerged();\n-\t\trecord_df_conflict_files(o, entries);\n \t\tre_head  = get_renames(o, head, common, head, merge, entries);\n \t\tre_merge = get_renames(o, merge, common, head, merge, entries);\n \t\tclean = process_renames(o, re_head, re_merge);\n+\t\trecord_df_conflict_files(o, entries);\n \t\tif (clean < 0)\n \t\t\tgoto cleanup;\n \t\tfor (i = entries->nr-1; 0 <= i; i--) {\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333729","messageId":"20171129014237.32570-19-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 18/34] merge-recursive: make !o->detect_rename codepath more obvious","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:21Z","receivedAt":"2017-11-29T01:43:36Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Previously, if !o->detect_rename then get_renames() would return an\nempty string_list, and then process_renames() would have nothing to\niterate over.  It seems more straightforward to simply avoid calling\neither function in that case.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 11 +++++++++--\n 1 file changed, 9 insertions(+), 2 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex cdd0afa047..da7c67eb82 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1329,8 +1329,6 @@ static struct string_list *get_renames(struct merge_options *o,\n \tstruct diff_options opts;\n \n \trenames = xcalloc(1, sizeof(struct string_list));\n-\tif (!o->detect_rename)\n-\t\treturn renames;\n \n \tdiff_setup(&opts);\n \topts.flags.recursive = 1;\n@@ -1648,6 +1646,12 @@ static int handle_renames(struct merge_options *o,\n \t\t\t  struct string_list *entries,\n \t\t\t  struct rename_info *ri)\n {\n+\tri->head_renames = NULL;\n+\tri->merge_renames = NULL;\n+\n+\tif (!o->detect_rename)\n+\t\treturn 1;\n+\n \tri->head_renames  = get_renames(o, head, common, head, merge, entries);\n \tri->merge_renames = get_renames(o, merge, common, head, merge, entries);\n \treturn process_renames(o, ri->head_renames, ri->merge_renames);\n@@ -1658,6 +1662,9 @@ static void cleanup_rename(struct string_list *rename)\n \tconst struct rename *re;\n \tint i;\n \n+\tif (rename == NULL)\n+\t\treturn;\n+\n \tfor (i = 0; i < rename->nr; i++) {\n \t\tre = rename->items[i].util;\n \t\tdiff_free_filepair(re->pair);\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333730","messageId":"20171129014237.32570-18-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 17/34] merge-recursive: fix leaks of allocated renames and diff_filepairs","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:20Z","receivedAt":"2017-11-29T01:43:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"get_renames() has always zero'ed out diff_queued_diff.nr while only\nmanually free'ing diff_filepairs that did not correspond to renames.\nFurther, it allocated struct renames that were tucked away in the\nreturn string_list.  Make sure all of these are deallocated when we\nare done with them.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 20 +++++++++++++++-----\n 1 file changed, 15 insertions(+), 5 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex e95eac2c70..cdd0afa047 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1653,13 +1653,23 @@ static int handle_renames(struct merge_options *o,\n \treturn process_renames(o, ri->head_renames, ri->merge_renames);\n }\n \n-static void cleanup_renames(struct rename_info *re_info)\n+static void cleanup_rename(struct string_list *rename)\n {\n-\tstring_list_clear(re_info->head_renames, 0);\n-\tstring_list_clear(re_info->merge_renames, 0);\n+\tconst struct rename *re;\n+\tint i;\n \n-\tfree(re_info->head_renames);\n-\tfree(re_info->merge_renames);\n+\tfor (i = 0; i < rename->nr; i++) {\n+\t\tre = rename->items[i].util;\n+\t\tdiff_free_filepair(re->pair);\n+\t}\n+\tstring_list_clear(rename, 1);\n+\tfree(rename);\n+}\n+\n+static void cleanup_renames(struct rename_info *re_info)\n+{\n+\tcleanup_rename(re_info->head_renames);\n+\tcleanup_rename(re_info->merge_renames);\n }\n \n static struct object_id *stage_oid(const struct object_id *oid, unsigned mode)\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333731","messageId":"20171129014237.32570-32-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 31/34] merge-recursive: fix remaining directory rename + dirty overwrite cases","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:34Z","receivedAt":"2017-11-29T01:43:40Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   | 26 +++++++++++++++++++++++---\n t/t6043-merge-rename-directories.sh |  8 ++++----\n 2 files changed, 27 insertions(+), 7 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 2b8a5ca03b..fe42cabad0 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1311,11 +1311,23 @@ static int handle_file(struct merge_options *o,\n \n \tadd = filespec_from_entry(&other, dst_entry, stage ^ 1);\n \tif (add) {\n+\t\tint ren_src_was_dirty = was_dirty(o, rename->path);\n \t\tchar *add_name = unique_path(o, rename->path, other_branch);\n \t\tif (update_file(o, 0, &add->oid, add->mode, add_name))\n \t\t\treturn -1;\n \n-\t\tremove_file(o, 0, rename->path, 0);\n+\t\tif (ren_src_was_dirty) {\n+\t\t\toutput(o, 1, _(\"Refusing to lose dirty file at %s\"),\n+\t\t\t       rename->path);\n+\t\t}\n+\t\t/*\n+\t\t * Stupid double negatives in remove_file; it somehow manages\n+\t\t * to repeatedly mess me up.  So, just for myself:\n+\t\t *    1) update_wd iff !ren_src_was_dirty.\n+\t\t *    2) no_wd iff !update_wd\n+\t\t *    3) so, no_wd == !!ren_src_was_dirty == ren_src_was_dirty\n+\t\t */\n+\t\tremove_file(o, 0, rename->path, ren_src_was_dirty);\n \t\tdst_name = unique_path(o, rename->path, cur_branch);\n \t} else {\n \t\tif (dir_in_way(rename->path, !o->call_depth, 0)) {\n@@ -1453,7 +1465,10 @@ static int conflict_rename_rename_2to1(struct merge_options *o,\n \t\tchar *new_path2 = unique_path(o, path, ci->branch2);\n \t\toutput(o, 1, _(\"Renaming %s to %s and %s to %s instead\"),\n \t\t       a->path, new_path1, b->path, new_path2);\n-\t\tif (would_lose_untracked(path))\n+\t\tif (was_dirty(o, path))\n+\t\t\toutput(o, 1, _(\"Refusing to lose dirty file at %s\"),\n+\t\t\t       path);\n+\t\telse if (would_lose_untracked(path))\n \t\t\t/*\n \t\t\t * Only way we get here is if both renames were from\n \t\t\t * a directory rename AND user had an untracked file\n@@ -2033,6 +2048,7 @@ static void apply_directory_rename_modifications(struct merge_options *o,\n {\n \tstruct string_list_item *item;\n \tint stage = (tree == a_tree ? 2 : 3);\n+\tint update_wd;\n \n \t/*\n \t * In all cases where we can do directory rename detection,\n@@ -2043,7 +2059,11 @@ static void apply_directory_rename_modifications(struct merge_options *o,\n \t * saying the file would have been overwritten), but it might\n \t * be dirty, though.\n \t */\n-\tremove_file(o, 1, pair->two->path, 0 /* no_wd */);\n+\tupdate_wd = !was_dirty(o, pair->two->path);\n+\tif (!update_wd)\n+\t\toutput(o, 1, _(\"Refusing to lose dirty file at %s\"),\n+\t\t       pair->two->path);\n+\tremove_file(o, 1, pair->two->path, !update_wd);\n \n \t/* Find or create a new re->dst_entry */\n \titem = string_list_lookup(entries, new_path);\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 9b16280d5e..f89cce4377 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -3224,7 +3224,7 @@ test_expect_success '11b-setup: Avoid losing dirty file involved in directory re\n \t)\n '\n \n-test_expect_failure '11b-check: Avoid losing dirty file involved in directory rename' '\n+test_expect_success '11b-check: Avoid losing dirty file involved in directory rename' '\n \t(\n \t\tcd 11b &&\n \n@@ -3358,7 +3358,7 @@ test_expect_success '11d-setup: Avoid losing not-uptodate with rename + D/F conf\n \t)\n '\n \n-test_expect_failure '11d-check: Avoid losing not-uptodate with rename + D/F conflict' '\n+test_expect_success '11d-check: Avoid losing not-uptodate with rename + D/F conflict' '\n \t(\n \t\tcd 11d &&\n \n@@ -3432,7 +3432,7 @@ test_expect_success '11e-setup: Avoid deleting not-uptodate with dir rename/rena\n \t)\n '\n \n-test_expect_failure '11e-check: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '\n+test_expect_success '11e-check: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '\n \t(\n \t\tcd 11e &&\n \n@@ -3506,7 +3506,7 @@ test_expect_success '11f-setup: Avoid deleting not-uptodate with dir rename/rena\n \t)\n '\n \n-test_expect_failure '11f-check: Avoid deleting not-uptodate with dir rename/rename(2to1)' '\n+test_expect_success '11f-check: Avoid deleting not-uptodate with dir rename/rename(2to1)' '\n \t(\n \t\tcd 11f &&\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333732","messageId":"20171129014237.32570-5-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 04/34] directory rename detection: basic testcases","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:07Z","receivedAt":"2017-11-29T01:43:44Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 430 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 430 insertions(+)\n create mode 100755 t/t6043-merge-rename-directories.sh\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nnew file mode 100755\nindex 0000000000..d8ead7c56b\n--- /dev/null\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -0,0 +1,430 @@\n+#!/bin/sh\n+\n+test_description=\"recursive merge with directory renames\"\n+# includes checking of many corner cases, with a similar methodology to:\n+#   t6042: corner cases with renames but not criss-cross merges\n+#   t6036: corner cases with both renames and criss-cross merges\n+#\n+# The setup for all of them, pictorially, is:\n+#\n+#      A\n+#      o\n+#     / \\\n+#  O o   ?\n+#     \\ /\n+#      o\n+#      B\n+#\n+# To help make it easier to follow the flow of tests, they have been\n+# divided into sections and each test will start with a quick explanation\n+# of what commits O, A, and B contain.\n+#\n+# Notation:\n+#    z/{b,c}   means  files z/b and z/c both exist\n+#    x/d_1     means  file x/d exists with content d1.  (Purpose of the\n+#                     underscore notation is to differentiate different\n+#                     files that might be renamed into each other's paths.)\n+\n+. ./test-lib.sh\n+\n+\n+###########################################################################\n+# SECTION 1: Basic cases we should be able to handle\n+###########################################################################\n+\n+# Testcase 1a, Basic directory rename.\n+#   Commit O: z/{b,c}\n+#   Commit A: y/{b,c}\n+#   Commit B: z/{b,c,d,e/f}\n+#   Expected: y/{b,c,d,e/f}\n+\n+test_expect_success '1a-setup: Simple directory rename detection' '\n+\ttest_create_repo 1a &&\n+\t(\n+\t\tcd 1a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo d >z/d &&\n+\t\tmkdir z/e &&\n+\t\techo f >z/e/f &&\n+\t\tgit add z/d z/e/f &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '1a-check: Simple directory rename detection' '\n+\t(\n+\t\tcd 1a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e/f &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/d B:z/e/f &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest \"$(git hash-object y/d)\" = $(git rev-parse B:z/d) &&\n+\t\ttest_must_fail git rev-parse HEAD:z/d &&\n+\t\ttest_must_fail git rev-parse HEAD:z/e/f &&\n+\t\ttest ! -d z/d &&\n+\t\ttest ! -d z/e/f\n+\t)\n+'\n+\n+# Testcase 1b, Merge a directory with another\n+#   Commit O: z/{b,c},   y/d\n+#   Commit A: z/{b,c,e}, y/d\n+#   Commit B: y/{b,c,d}\n+#   Expected: y/{b,c,d,e}\n+\n+test_expect_success '1b-setup: Merge a directory with another' '\n+\ttest_create_repo 1b &&\n+\t(\n+\t\tcd 1b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir y &&\n+\t\techo d >y/d &&\n+\t\tgit add z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\techo e >z/e &&\n+\t\tgit add z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z/b y &&\n+\t\tgit mv z/c y &&\n+\t\trmdir z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '1b-check: Merge a directory with another' '\n+\t(\n+\t\tcd 1b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:y/d A:z/e &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git rev-parse HEAD:z/e\n+\t)\n+'\n+\n+# Testcase 1c, Transitive renaming\n+#   (Related to testcases 3a and 6d -- when should a transitive rename apply?)\n+#   (Related to testcases 9c and 9d -- can transitivity repeat?)\n+#   Commit O: z/{b,c},   x/d\n+#   Commit A: y/{b,c},   x/d\n+#   Commit B: z/{b,c,d}\n+#   Expected: y/{b,c,d}  (because x/d -> z/d -> y/d)\n+\n+test_expect_success '1c-setup: Transitive renaming' '\n+\ttest_create_repo 1c &&\n+\t(\n+\t\tcd 1c &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '1c-check: Transitive renaming' '\n+\t(\n+\t\tcd 1c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:x/d &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git rev-parse HEAD:x/d &&\n+\t\ttest_must_fail git rev-parse HEAD:z/d &&\n+\t\ttest ! -f z/d\n+\t)\n+'\n+\n+# Testcase 1d, Directory renames (merging two directories into one new one)\n+#              cause a rename/rename(2to1) conflict\n+#   (Related to testcases 1c and 7b)\n+#   Commit O. z/{b,c},        y/{d,e}\n+#   Commit A. x/{b,c},        y/{d,e,m,wham_1}\n+#   Commit B. z/{b,c,n,wham_2}, x/{d,e}\n+#   Expected: x/{b,c,d,e,m,n}, CONFLICT:(y/wham_1 & z/wham_2 -> x/wham)\n+#   Note: y/m & z/n should definitely move into x.  By the same token, both\n+#         y/wham_1 & z/wham_2 should too...giving us a conflict.\n+\n+test_expect_success '1d-setup: Directory renames cause a rename/rename(2to1) conflict' '\n+\ttest_create_repo 1d &&\n+\t(\n+\t\tcd 1d &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir y &&\n+\t\techo d >y/d &&\n+\t\techo e >y/e &&\n+\t\tgit add z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z x &&\n+\t\techo m >y/m &&\n+\t\techo wham1 >y/wham &&\n+\t\tgit add y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv y x &&\n+\t\techo n >z/n &&\n+\t\techo wham2 >z/wham &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '1d-check: Directory renames cause a rename/rename(2to1) conflict' '\n+\t(\n+\t\tcd 1d &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename)\" out &&\n+\n+\t\ttest 8 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:x/b :0:x/c :0:x/d :0:x/e :0:x/m :0:x/n &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:y/d O:y/e A:y/m B:z/n &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_must_fail git rev-parse :0:x/wham &&\n+\t\tgit rev-parse >actual \\\n+\t\t\t:2:x/wham :3:x/wham &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tA:y/wham B:z/wham &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest ! -f x/wham &&\n+\t\ttest -f x/wham~HEAD &&\n+\t\ttest -f x/wham~B^0 &&\n+\n+\t\tgit hash-object >actual \\\n+\t\t\tx/wham~HEAD x/wham~B^0 &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tA:y/wham B:z/wham &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 1e, Renamed directory, with all filenames being renamed too\n+#   Commit O: z/{oldb,oldc}\n+#   Commit A: y/{newb,newc}\n+#   Commit B: z/{oldb,oldc,d}\n+#   Expected: y/{newb,newc,d}\n+\n+test_expect_success '1e-setup: Renamed directory, with all files being renamed too' '\n+\ttest_create_repo 1e &&\n+\t(\n+\t\tcd 1e &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/oldb &&\n+\t\techo c >z/oldc &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir y &&\n+\t\tgit mv z/oldb y/newb &&\n+\t\tgit mv z/oldc y/newc &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo d >z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '1e-check: Renamed directory, with all files being renamed too' '\n+\t(\n+\t\tcd 1e &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/newb HEAD:y/newc HEAD:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/oldb O:z/oldc B:z/d &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git rev-parse HEAD:z/d\n+\t)\n+'\n+\n+# Testcase 1f, Split a directory into two other directories\n+#   (Related to testcases 3a, all of section 2, and all of section 4)\n+#   Commit O: z/{b,c,d,e,f}\n+#   Commit A: z/{b,c,d,e,f,g}\n+#   Commit B: y/{b,c}, x/{d,e,f}\n+#   Expected: y/{b,c}, x/{d,e,f,g}\n+\n+test_expect_success '1f-setup: Split a directory into two other directories' '\n+\ttest_create_repo 1f &&\n+\t(\n+\t\tcd 1f &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d >z/d &&\n+\t\techo e >z/e &&\n+\t\techo f >z/f &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\techo g >z/g &&\n+\t\tgit add z/g &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir y &&\n+\t\tmkdir x &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\tgit mv z/d x/ &&\n+\t\tgit mv z/e x/ &&\n+\t\tgit mv z/f x/ &&\n+\t\trmdir z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '1f-check: Split a directory into two other directories' '\n+\t(\n+\t\tcd 1f &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:x/d HEAD:x/e HEAD:x/f HEAD:x/g &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:z/d O:z/e O:z/f A:z/g &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest ! -f z/g &&\n+\t\ttest_must_fail git rev-parse HEAD:z/g\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by testcases in section 1:\n+#\n+#   We should still detect the directory rename even if it wasn't just\n+#   the directory renamed, but the files within it. (see 1b)\n+#\n+#   If renames split a directory into two or more others, the directory\n+#   with the most renames, \"wins\" (see 1c).  However, see the testcases\n+#   in section 2, plus testcases 3a and 4a.\n+###########################################################################\n+\n+test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333733","messageId":"20171129014237.32570-27-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 26/34] merge-recursive: check for file level conflicts then get new name","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:29Z","receivedAt":"2017-11-29T01:43:47Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Before trying to apply directory renames to paths within the given\ndirectories, we want to make sure that there aren't conflicts at the\nfile level either.  If there aren't any, then get the new name from\nany directory renames.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   | 174 ++++++++++++++++++++++++++++++++++--\n strbuf.c                            |  16 ++++\n strbuf.h                            |  16 ++++\n t/t6043-merge-rename-directories.sh |   2 +-\n 4 files changed, 199 insertions(+), 9 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 9e31baaf33..78f707d0d7 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1508,6 +1508,91 @@ static void remove_hashmap_entries(struct hashmap *dir_renames,\n \tstring_list_clear(items_to_remove, 0);\n }\n \n+/*\n+ * See if there is a directory rename for path, and if there are any file\n+ * level conflicts for the renamed location.  If there is a rename and\n+ * there are no conflicts, return the new name.  Otherwise, return NULL.\n+ */\n+static char *handle_path_level_conflicts(struct merge_options *o,\n+\t\t\t\t\t const char *path,\n+\t\t\t\t\t struct dir_rename_entry *entry,\n+\t\t\t\t\t struct hashmap *collisions,\n+\t\t\t\t\t struct tree *tree)\n+{\n+\tchar *new_path = NULL;\n+\tstruct collision_entry *collision_ent;\n+\tint clean = 1;\n+\tstruct strbuf collision_paths = STRBUF_INIT;\n+\n+\t/*\n+\t * entry has the mapping of old directory name to new directory name\n+\t * that we want to apply to path.\n+\t */\n+\tnew_path = apply_dir_rename(entry, path);\n+\n+\tif (!new_path) {\n+\t\t/* This should only happen when entry->non_unique_new_dir set */\n+\t\tif (!entry->non_unique_new_dir)\n+\t\t\tBUG(\"entry->non_unqiue_dir not set and !new_path\");\n+\t\toutput(o, 1, _(\"CONFLICT (directory rename split): \"\n+\t\t\t       \"Unclear where to place %s because directory \"\n+\t\t\t       \"%s was renamed to multiple other directories, \"\n+\t\t\t       \"with no destination getting a majority of the \"\n+\t\t\t       \"files.\"),\n+\t\t       path, entry->dir);\n+\t\tclean = 0;\n+\t\treturn NULL;\n+\t}\n+\n+\t/*\n+\t * The caller needs to have ensured that it has pre-populated\n+\t * collisions with all paths that map to new_path.  Do a quick check\n+\t * to ensure that's the case.\n+\t */\n+\tcollision_ent = collision_find_entry(collisions, new_path);\n+\tif (collision_ent == NULL)\n+\t\tBUG(\"collision_ent is NULL\");\n+\n+\t/*\n+\t * Check for one-sided add/add/.../add conflicts, i.e.\n+\t * where implicit renames from the other side doing\n+\t * directory rename(s) can affect this side of history\n+\t * to put multiple paths into the same location.  Warn\n+\t * and bail on directory renames for such paths.\n+\t */\n+\tif (collision_ent->reported_already) {\n+\t\tclean = 0;\n+\t} else if (tree_has_path(tree, new_path)) {\n+\t\tcollision_ent->reported_already = 1;\n+\t\tstrbuf_add_separated_string_list(&collision_paths, \", \",\n+\t\t\t\t\t\t &collision_ent->source_files);\n+\t\toutput(o, 1, _(\"CONFLICT (implicit dir rename): Existing \"\n+\t\t\t       \"file/dir at %s in the way of implicit \"\n+\t\t\t       \"directory rename(s) putting the following \"\n+\t\t\t       \"path(s) there: %s.\"),\n+\t\t       new_path, collision_paths.buf);\n+\t\tclean = 0;\n+\t} else if (collision_ent->source_files.nr > 1) {\n+\t\tcollision_ent->reported_already = 1;\n+\t\tstrbuf_add_separated_string_list(&collision_paths, \", \",\n+\t\t\t\t\t\t &collision_ent->source_files);\n+\t\toutput(o, 1, _(\"CONFLICT (implicit dir rename): Cannot map \"\n+\t\t\t       \"more than one path to %s; implicit directory \"\n+\t\t\t       \"renames tried to put these paths there: %s\"),\n+\t\t       new_path, collision_paths.buf);\n+\t\tclean = 0;\n+\t}\n+\n+\t/* Free memory we no longer need */\n+\tstrbuf_release(&collision_paths);\n+\tif (!clean && new_path) {\n+\t\tfree(new_path);\n+\t\treturn NULL;\n+\t}\n+\n+\treturn new_path;\n+}\n+\n /*\n  * There are a couple things we want to do at the directory level:\n  *   1. Check for both sides renaming to the same thing, in order to avoid\n@@ -1757,6 +1842,59 @@ static void compute_collisions(struct hashmap *collisions,\n \t}\n }\n \n+static char *check_for_directory_rename(struct merge_options *o,\n+\t\t\t\t\tconst char *path,\n+\t\t\t\t\tstruct tree *tree,\n+\t\t\t\t\tstruct hashmap *dir_renames,\n+\t\t\t\t\tstruct hashmap *dir_rename_exclusions,\n+\t\t\t\t\tstruct hashmap *collisions,\n+\t\t\t\t\tint *clean_merge)\n+{\n+\tchar *new_path = NULL;\n+\tstruct dir_rename_entry *entry = check_dir_renamed(path, dir_renames);\n+\tstruct dir_rename_entry *oentry = NULL;\n+\n+\tif (!entry)\n+\t\treturn new_path;\n+\n+\t/*\n+\t * This next part is a little weird.  We do not want to do an\n+\t * implicit rename into a directory we renamed on our side, because\n+\t * that will result in a spurious rename/rename(1to2) conflict.  An\n+\t * example:\n+\t *   Base commit: dumbdir/afile, otherdir/bfile\n+\t *   Side 1:      smrtdir/afile, otherdir/bfile\n+\t *   Side 2:      dumbdir/afile, dumbdir/bfile\n+\t * Here, while working on Side 1, we could notice that otherdir was\n+\t * renamed/merged to dumbdir, and change the diff_filepair for\n+\t * otherdir/bfile into a rename into dumbdir/bfile.  However, Side\n+\t * 2 will notice the rename from dumbdir to smrtdir, and do the\n+\t * transitive rename to move it from dumbdir/bfile to\n+\t * smrtdir/bfile.  That gives us bfile in dumbdir vs being in\n+\t * smrtdir, a rename/rename(1to2) conflict.  We really just want\n+\t * the file to end up in smrtdir.  And the way to achieve that is\n+\t * to not let Side1 do the rename to dumbdir, since we know that is\n+\t * the source of one of our directory renames.\n+\t *\n+\t * That's why oentry and dir_rename_exclusions is here.\n+\t *\n+\t * As it turns out, this also prevents N-way transient rename\n+\t * confusion; See testcases 9c and 9d of t6043.\n+\t */\n+\toentry = dir_rename_find_entry(dir_rename_exclusions, entry->new_dir.buf);\n+\tif (oentry) {\n+\t\toutput(o, 1, _(\"WARNING: Avoiding applying %s -> %s rename \"\n+\t\t\t       \"to %s, because %s itself was renamed.\"),\n+\t\t       entry->dir, entry->new_dir.buf, path, entry->new_dir.buf);\n+\t} else {\n+\t\tnew_path = handle_path_level_conflicts(o, path, entry,\n+\t\t\t\t\t\t       collisions, tree);\n+\t\t*clean_merge &= (new_path != NULL);\n+\t}\n+\n+\treturn new_path;\n+}\n+\n /*\n  * Get information of all renames which occurred in 'pairs', making use of\n  * any implicit directory renames inferred from the other side of history.\n@@ -1767,11 +1905,13 @@ static void compute_collisions(struct hashmap *collisions,\n static struct string_list *get_renames(struct merge_options *o,\n \t\t\t\t       struct diff_queue_struct *pairs,\n \t\t\t\t       struct hashmap *dir_renames,\n+\t\t\t\t       struct hashmap *dir_rename_exclusions,\n \t\t\t\t       struct tree *tree,\n \t\t\t\t       struct tree *o_tree,\n \t\t\t\t       struct tree *a_tree,\n \t\t\t\t       struct tree *b_tree,\n-\t\t\t\t       struct string_list *entries)\n+\t\t\t\t       struct string_list *entries,\n+\t\t\t\t       int *clean_merge)\n {\n \tint i;\n \tstruct hashmap collisions;\n@@ -1786,11 +1926,22 @@ static struct string_list *get_renames(struct merge_options *o,\n \t\tstruct string_list_item *item;\n \t\tstruct rename *re;\n \t\tstruct diff_filepair *pair = pairs->queue[i];\n+\t\tchar *new_path; /* non-NULL only with directory renames */\n \n-\t\tif (pair->status != 'R') {\n+\t\tif (pair->status == 'D') {\n+\t\t\tdiff_free_filepair(pair);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tnew_path = check_for_directory_rename(o, pair->two->path, tree,\n+\t\t\t\t\t\t      dir_renames,\n+\t\t\t\t\t\t      dir_rename_exclusions,\n+\t\t\t\t\t\t      &collisions,\n+\t\t\t\t\t\t      clean_merge);\n+\t\tif (pair->status != 'R' && !new_path) {\n \t\t\tdiff_free_filepair(pair);\n \t\t\tcontinue;\n \t\t}\n+\n \t\tre = xmalloc(sizeof(*re));\n \t\tre->processed = 0;\n \t\tre->pair = pair;\n@@ -2108,7 +2259,7 @@ static int handle_renames(struct merge_options *o,\n {\n \tstruct diff_queue_struct *head_pairs, *merge_pairs;\n \tstruct hashmap *dir_re_head, *dir_re_merge;\n-\tint clean;\n+\tint clean = 1;\n \n \tri->head_renames = NULL;\n \tri->merge_renames = NULL;\n@@ -2127,13 +2278,20 @@ static int handle_renames(struct merge_options *o,\n \t\t\t\t\t dir_re_merge, merge);\n \n \tri->head_renames  = get_renames(o, head_pairs,\n-\t\t\t\t\tdir_re_merge, head,\n-\t\t\t\t\tcommon, head, merge, entries);\n+\t\t\t\t\tdir_re_merge, dir_re_head, head,\n+\t\t\t\t\tcommon, head, merge, entries,\n+\t\t\t\t\t&clean);\n+\tif (clean < 0)\n+\t\tgoto cleanup;\n \tri->merge_renames = get_renames(o, merge_pairs,\n-\t\t\t\t\tdir_re_head, merge,\n-\t\t\t\t\t common, head, merge, entries);\n-\tclean = process_renames(o, ri->head_renames, ri->merge_renames);\n+\t\t\t\t\tdir_re_head, dir_re_merge, merge,\n+\t\t\t\t\tcommon, head, merge, entries,\n+\t\t\t\t\t&clean);\n+\tif (clean < 0)\n+\t\tgoto cleanup;\n+\tclean &= process_renames(o, ri->head_renames, ri->merge_renames);\n \n+cleanup:\n \t/*\n \t * Some cleanup is deferred until cleanup_renames() because the\n \t * data structures are still needed and referenced in\ndiff --git a/strbuf.c b/strbuf.c\nindex 323c49ceb3..6ae384f9cd 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -1,5 +1,6 @@\n #include \"cache.h\"\n #include \"refs.h\"\n+#include \"string-list.h\"\n #include \"utf8.h\"\n \n int starts_with(const char *str, const char *prefix)\n@@ -141,6 +142,21 @@ struct strbuf **strbuf_split_buf(const char *str, size_t slen,\n \treturn ret;\n }\n \n+void strbuf_add_separated_string_list(struct strbuf *str,\n+\t\t\t\t      const char *sep,\n+\t\t\t\t      struct string_list *slist)\n+{\n+\tstruct string_list_item *item;\n+\tint sep_needed = 0;\n+\n+\tfor_each_string_list_item(item, slist) {\n+\t\tif (sep_needed)\n+\t\t\tstrbuf_addstr(str, sep);\n+\t\tstrbuf_addstr(str, item->string);\n+\t\tsep_needed = 1;\n+\t}\n+}\n+\n void strbuf_list_free(struct strbuf **sbs)\n {\n \tstruct strbuf **s = sbs;\ndiff --git a/strbuf.h b/strbuf.h\nindex 0a74acb236..fd6d787a7b 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -1,6 +1,8 @@\n #ifndef STRBUF_H\n #define STRBUF_H\n \n+struct string_list;\n+\n /**\n  * strbuf's are meant to be used with all the usual C string and memory\n  * APIs. Given that the length of the buffer is known, it's often better to\n@@ -537,6 +539,20 @@ static inline struct strbuf **strbuf_split(const struct strbuf *sb,\n \treturn strbuf_split_max(sb, terminator, 0);\n }\n \n+/*\n+ * Adds all strings of a string list to the strbuf, separated by the given\n+ * separator.  For example, if sep is\n+ *   ', '\n+ * and slist contains\n+ *   ['element1', 'element2', ..., 'elementN'],\n+ * then write:\n+ *   'element1, element2, ..., elementN'\n+ * to str.  If only one element, just write \"element1\" to str.\n+ */\n+extern void strbuf_add_separated_string_list(struct strbuf *str,\n+\t\t\t\t\t     const char *sep,\n+\t\t\t\t\t     struct string_list *slist);\n+\n /**\n  * Free a NULL-terminated list of strbufs (for example, the return\n  * values of the strbuf_split*() functions).\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 0644b95ec5..836e57c0a3 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -477,7 +477,7 @@ test_expect_success '2a-setup: Directory split into two on one side, with equal\n \t)\n '\n \n-test_expect_failure '2a-check: Directory split into two on one side, with equal numbers of paths' '\n+test_expect_success '2a-check: Directory split into two on one side, with equal numbers of paths' '\n \t(\n \t\tcd 2a &&\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333734","messageId":"20171129014237.32570-17-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 16/34] merge-recursive: introduce new functions to handle rename logic","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:19Z","receivedAt":"2017-11-29T01:43:50Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"The amount of logic in merge_trees() relative to renames was just a few\nlines, but split it out into new handle_renames() and cleanup_renames()\nfunctions to prepare for additional logic to be added to each.  No code or\nlogic changes, just a new place to put stuff for when the rename detection\ngains additional checks.\n\nNote that process_renames() records pointers to various information (such\nas diff_filepairs) into rename_conflict_info structs.  Even though the\nrename string_lists are not directly used once handle_renames() completes,\nwe should not immediately free the lists at the end of that function\nbecause they store the information referenced in the rename_conflict_info,\nwhich is used later in process_entry().  Thus the reason for a separate\ncleanup_renames().\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 43 +++++++++++++++++++++++++++++++++----------\n 1 file changed, 33 insertions(+), 10 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 08bf26b9c7..e95eac2c70 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1636,6 +1636,32 @@ static int process_renames(struct merge_options *o,\n \treturn clean_merge;\n }\n \n+struct rename_info {\n+\tstruct string_list *head_renames;\n+\tstruct string_list *merge_renames;\n+};\n+\n+static int handle_renames(struct merge_options *o,\n+\t\t\t  struct tree *common,\n+\t\t\t  struct tree *head,\n+\t\t\t  struct tree *merge,\n+\t\t\t  struct string_list *entries,\n+\t\t\t  struct rename_info *ri)\n+{\n+\tri->head_renames  = get_renames(o, head, common, head, merge, entries);\n+\tri->merge_renames = get_renames(o, merge, common, head, merge, entries);\n+\treturn process_renames(o, ri->head_renames, ri->merge_renames);\n+}\n+\n+static void cleanup_renames(struct rename_info *re_info)\n+{\n+\tstring_list_clear(re_info->head_renames, 0);\n+\tstring_list_clear(re_info->merge_renames, 0);\n+\n+\tfree(re_info->head_renames);\n+\tfree(re_info->merge_renames);\n+}\n+\n static struct object_id *stage_oid(const struct object_id *oid, unsigned mode)\n {\n \treturn (is_null_oid(oid) || mode == 0) ? NULL: (struct object_id *)oid;\n@@ -1988,7 +2014,8 @@ int merge_trees(struct merge_options *o,\n \t}\n \n \tif (unmerged_cache()) {\n-\t\tstruct string_list *entries, *re_head, *re_merge;\n+\t\tstruct string_list *entries;\n+\t\tstruct rename_info re_info;\n \t\tint i;\n \t\t/*\n \t\t * Only need the hashmap while processing entries, so\n@@ -2002,9 +2029,8 @@ int merge_trees(struct merge_options *o,\n \t\tget_files_dirs(o, merge);\n \n \t\tentries = get_unmerged();\n-\t\tre_head  = get_renames(o, head, common, head, merge, entries);\n-\t\tre_merge = get_renames(o, merge, common, head, merge, entries);\n-\t\tclean = process_renames(o, re_head, re_merge);\n+\t\tclean = handle_renames(o, common, head, merge, entries,\n+\t\t\t\t       &re_info);\n \t\trecord_df_conflict_files(o, entries);\n \t\tif (clean < 0)\n \t\t\tgoto cleanup;\n@@ -2029,16 +2055,13 @@ int merge_trees(struct merge_options *o,\n \t\t}\n \n cleanup:\n-\t\tstring_list_clear(re_merge, 0);\n-\t\tstring_list_clear(re_head, 0);\n+\t\tcleanup_renames(&re_info);\n+\n \t\tstring_list_clear(entries, 1);\n+\t\tfree(entries);\n \n \t\thashmap_free(&o->current_file_dir_set, 1);\n \n-\t\tfree(re_merge);\n-\t\tfree(re_head);\n-\t\tfree(entries);\n-\n \t\tif (clean < 0)\n \t\t\treturn clean;\n \t}\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333735","messageId":"20171129014237.32570-29-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 28/34] merge-recursive: apply necessary modifications for directory renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:31Z","receivedAt":"2017-11-29T01:43:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"This commit hooks together all the directory rename logic by making the\nnecessary changes to the rename struct, it's dst_entry, and the\ndiff_filepair under consideration.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   | 187 +++++++++++++++++++++++++++++++++++-\n t/t6043-merge-rename-directories.sh |  50 +++++-----\n 2 files changed, 211 insertions(+), 26 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 01934bc1e2..cdf8588c75 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -177,6 +177,7 @@ static int oid_eq(const struct object_id *a, const struct object_id *b)\n \n enum rename_type {\n \tRENAME_NORMAL = 0,\n+\tRENAME_DIR,\n \tRENAME_DELETE,\n \tRENAME_ONE_FILE_TO_ONE,\n \tRENAME_ONE_FILE_TO_TWO,\n@@ -607,6 +608,7 @@ struct rename {\n \t */\n \tstruct stage_data *src_entry;\n \tstruct stage_data *dst_entry;\n+\tunsigned add_turned_into_rename:1;\n \tunsigned processed:1;\n };\n \n@@ -641,6 +643,27 @@ static int update_stages(struct merge_options *opt, const char *path,\n \treturn 0;\n }\n \n+static int update_stages_for_stage_data(struct merge_options *opt,\n+\t\t\t\t\tconst char *path,\n+\t\t\t\t\tconst struct stage_data *stage_data)\n+{\n+\tstruct diff_filespec o, a, b;\n+\n+\to.mode = stage_data->stages[1].mode;\n+\toidcpy(&o.oid, &stage_data->stages[1].oid);\n+\n+\ta.mode = stage_data->stages[2].mode;\n+\toidcpy(&a.oid, &stage_data->stages[2].oid);\n+\n+\tb.mode = stage_data->stages[3].mode;\n+\toidcpy(&b.oid, &stage_data->stages[3].oid);\n+\n+\treturn update_stages(opt, path,\n+\t\t\t     is_null_sha1(o.oid.hash) ? NULL : &o,\n+\t\t\t     is_null_sha1(a.oid.hash) ? NULL : &a,\n+\t\t\t     is_null_sha1(b.oid.hash) ? NULL : &b);\n+}\n+\n static void update_entry(struct stage_data *entry,\n \t\t\t struct diff_filespec *o,\n \t\t\t struct diff_filespec *a,\n@@ -1108,6 +1131,18 @@ static int merge_file_one(struct merge_options *o,\n \treturn merge_file_1(o, &one, &a, &b, branch1, branch2, mfi);\n }\n \n+static int conflict_rename_dir(struct merge_options *o,\n+\t\t\t       struct diff_filepair *pair,\n+\t\t\t       const char *rename_branch,\n+\t\t\t       const char *other_branch)\n+{\n+\tconst struct diff_filespec *dest = pair->two;\n+\n+\tif (update_file(o, 1, &dest->oid, dest->mode, dest->path))\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n static int handle_change_delete(struct merge_options *o,\n \t\t\t\t const char *path, const char *old_path,\n \t\t\t\t const struct object_id *o_oid, int o_mode,\n@@ -1377,6 +1412,24 @@ static int conflict_rename_rename_2to1(struct merge_options *o,\n \t\tif (!ret)\n \t\t\tret = update_file(o, 0, &mfi_c2.oid, mfi_c2.mode,\n \t\t\t\t\t  new_path2);\n+\t\t/*\n+\t\t * unpack_trees() actually populates the index for us for\n+\t\t * \"normal\" rename/rename(2to1) situtations so that the\n+\t\t * correct entries are at the higher stages, which would\n+\t\t * make the call below to update_stages_for_stage_data\n+\t\t * unnecessary.  However, if either of the renames came\n+\t\t * from a directory rename, then unpack_trees() will not\n+\t\t * have gotten the right data loaded into the index, so we\n+\t\t * need to do so now.  (While it'd be tempting to move this\n+\t\t * call to update_stages_for_stage_data() to\n+\t\t * apply_directory_rename_modifications(), that would break\n+\t\t * our intermediate calls to would_lose_untracked() since\n+\t\t * those rely on the current in-memory index.  See also the\n+\t\t * big \"NOTE\" in update_stages()).\n+\t\t */\n+\t\tif (update_stages_for_stage_data(o, path, ci->dst_entry1))\n+\t\t\tret = -1;\n+\n \t\tfree(new_path2);\n \t\tfree(new_path1);\n \t}\n@@ -1910,6 +1963,111 @@ static char *check_for_directory_rename(struct merge_options *o,\n \treturn new_path;\n }\n \n+static void apply_directory_rename_modifications(struct merge_options *o,\n+\t\t\t\t\t\t struct diff_filepair *pair,\n+\t\t\t\t\t\t char *new_path,\n+\t\t\t\t\t\t struct rename *re,\n+\t\t\t\t\t\t struct tree *tree,\n+\t\t\t\t\t\t struct tree *o_tree,\n+\t\t\t\t\t\t struct tree *a_tree,\n+\t\t\t\t\t\t struct tree *b_tree,\n+\t\t\t\t\t\t struct string_list *entries,\n+\t\t\t\t\t\t int *clean)\n+{\n+\tstruct string_list_item *item;\n+\tint stage = (tree == a_tree ? 2 : 3);\n+\n+\t/*\n+\t * In all cases where we can do directory rename detection,\n+\t * unpack_trees() will have read pair->two->path into the\n+\t * index and the working copy.  We need to remove it so that\n+\t * we can instead place it at new_path.  It is guaranteed to\n+\t * not be untracked (unpack_trees() would have errored out\n+\t * saying the file would have been overwritten), but it might\n+\t * be dirty, though.\n+\t */\n+\tremove_file(o, 1, pair->two->path, 0 /* no_wd */);\n+\n+\t/* Find or create a new re->dst_entry */\n+\titem = string_list_lookup(entries, new_path);\n+\tif (item) {\n+\t\t/*\n+\t\t * Since we're renaming on this side of history, and it's\n+\t\t * due to a directory rename on the other side of history\n+\t\t * (which we only allow when the directory in question no\n+\t\t * longer exists on the other side of history), the\n+\t\t * original entry for re->dst_entry is no longer\n+\t\t * necessary...\n+\t\t */\n+\t\tre->dst_entry->processed = 1;\n+\n+\t\t/*\n+\t\t * ...because we'll be using this new one.\n+\t\t */\n+\t\tre->dst_entry = item->util;\n+\t} else {\n+\t\t/*\n+\t\t * re->dst_entry is for the before-dir-rename path, and we\n+\t\t * need it to hold information for the after-dir-rename\n+\t\t * path.  Before creating a new entry, we need to mark the\n+\t\t * old one as unnecessary (...unless it is shared by\n+\t\t * src_entry, i.e. this didn't use to be a rename, in which\n+\t\t * case we can just allow the normal processing to happen\n+\t\t * for it).\n+\t\t */\n+\t\tif (pair->status == 'R')\n+\t\t\tre->dst_entry->processed = 1;\n+\n+\t\tre->dst_entry = insert_stage_data(new_path,\n+\t\t\t\t\t\t  o_tree, a_tree, b_tree,\n+\t\t\t\t\t\t  entries);\n+\t\titem = string_list_insert(entries, new_path);\n+\t\titem->util = re->dst_entry;\n+\t}\n+\n+\t/*\n+\t * Update the stage_data with the information about the path we are\n+\t * moving into place.  That slot will be empty and available for us\n+\t * to write to because of the collision checks in\n+\t * handle_path_level_conflicts().  In other words,\n+\t * re->dst_entry->stages[stage].oid will be the null_oid, so it's\n+\t * open for us to write to.\n+\t *\n+\t * It may be tempting to actually update the index at this point as\n+\t * well, using update_stages_for_stage_data(), but as per the big\n+\t * \"NOTE\" in update_stages(), doing so will modify the current\n+\t * in-memory index which will break calls to would_lose_untracked()\n+\t * that we need to make.  Instead, we need to just make sure that\n+\t * the various conflict_rename_*() functions update the index\n+\t * explicitly rather than relying on unpack_trees() to have done it.\n+\t */\n+\tget_tree_entry(tree->object.oid.hash,\n+\t\t       pair->two->path,\n+\t\t       re->dst_entry->stages[stage].oid.hash,\n+\t\t       &re->dst_entry->stages[stage].mode);\n+\n+\t/* Update pair status */\n+\tif (pair->status == 'A') {\n+\t\t/*\n+\t\t * Recording rename information for this add makes it look\n+\t\t * like a rename/delete conflict.  Make sure we can\n+\t\t * correctly handle this as an add that was moved to a new\n+\t\t * directory instead of reporting a rename/delete conflict.\n+\t\t */\n+\t\tre->add_turned_into_rename = 1;\n+\t}\n+\t/*\n+\t * We don't actually look at pair->status again, but it seems\n+\t * pedagogically correct to adjust it.\n+\t */\n+\tpair->status = 'R';\n+\n+\t/*\n+\t * Finally, record the new location.\n+\t */\n+\tpair->two->path = new_path;\n+}\n+\n /*\n  * Get information of all renames which occurred in 'pairs', making use of\n  * any implicit directory renames inferred from the other side of history.\n@@ -1959,6 +2117,7 @@ static struct string_list *get_renames(struct merge_options *o,\n \n \t\tre = xmalloc(sizeof(*re));\n \t\tre->processed = 0;\n+\t\tre->add_turned_into_rename = 0;\n \t\tre->pair = pair;\n \t\titem = string_list_lookup(entries, re->pair->one->path);\n \t\tif (!item)\n@@ -1975,6 +2134,12 @@ static struct string_list *get_renames(struct merge_options *o,\n \t\t\tre->dst_entry = item->util;\n \t\titem = string_list_insert(renames, pair->one->path);\n \t\titem->util = re;\n+\t\tif (new_path)\n+\t\t\tapply_directory_rename_modifications(o, pair, new_path,\n+\t\t\t\t\t\t\t     re, tree, o_tree,\n+\t\t\t\t\t\t\t     a_tree, b_tree,\n+\t\t\t\t\t\t\t     entries,\n+\t\t\t\t\t\t\t     clean_merge);\n \t}\n \n \thashmap_iter_init(&collisions, &iter);\n@@ -2144,7 +2309,19 @@ static int process_renames(struct merge_options *o,\n \t\t\tdst_other.mode = ren1->dst_entry->stages[other_stage].mode;\n \t\t\ttry_merge = 0;\n \n-\t\t\tif (oid_eq(&src_other.oid, &null_oid)) {\n+\t\t\tif (oid_eq(&src_other.oid, &null_oid) &&\n+\t\t\t    ren1->add_turned_into_rename) {\n+\t\t\t\tsetup_rename_conflict_info(RENAME_DIR,\n+\t\t\t\t\t\t\t   ren1->pair,\n+\t\t\t\t\t\t\t   NULL,\n+\t\t\t\t\t\t\t   branch1,\n+\t\t\t\t\t\t\t   branch2,\n+\t\t\t\t\t\t\t   ren1->dst_entry,\n+\t\t\t\t\t\t\t   NULL,\n+\t\t\t\t\t\t\t   o,\n+\t\t\t\t\t\t\t   NULL,\n+\t\t\t\t\t\t\t   NULL);\n+\t\t\t} else if (oid_eq(&src_other.oid, &null_oid)) {\n \t\t\t\tsetup_rename_conflict_info(RENAME_DELETE,\n \t\t\t\t\t\t\t   ren1->pair,\n \t\t\t\t\t\t\t   NULL,\n@@ -2561,6 +2738,14 @@ static int process_entry(struct merge_options *o,\n \t\t\t\t\t\t    o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,\n \t\t\t\t\t\t    conflict_info);\n \t\t\tbreak;\n+\t\tcase RENAME_DIR:\n+\t\t\tclean_merge = 1;\n+\t\t\tif (conflict_rename_dir(o,\n+\t\t\t\t\t\tconflict_info->pair1,\n+\t\t\t\t\t\tconflict_info->branch1,\n+\t\t\t\t\t\tconflict_info->branch2))\n+\t\t\t\tclean_merge = -1;\n+\t\t\tbreak;\n \t\tcase RENAME_DELETE:\n \t\t\tclean_merge = 0;\n \t\t\tif (conflict_rename_delete(o,\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 836e57c0a3..261fb4917d 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -69,7 +69,7 @@ test_expect_success '1a-setup: Simple directory rename detection' '\n \t)\n '\n \n-test_expect_failure '1a-check: Simple directory rename detection' '\n+test_expect_success '1a-check: Simple directory rename detection' '\n \t(\n \t\tcd 1a &&\n \n@@ -131,7 +131,7 @@ test_expect_success '1b-setup: Merge a directory with another' '\n \t)\n '\n \n-test_expect_failure '1b-check: Merge a directory with another' '\n+test_expect_success '1b-check: Merge a directory with another' '\n \t(\n \t\tcd 1b &&\n \n@@ -188,7 +188,7 @@ test_expect_success '1c-setup: Transitive renaming' '\n \t)\n '\n \n-test_expect_failure '1c-check: Transitive renaming' '\n+test_expect_success '1c-check: Transitive renaming' '\n \t(\n \t\tcd 1c &&\n \n@@ -256,7 +256,7 @@ test_expect_success '1d-setup: Directory renames cause a rename/rename(2to1) con\n \t)\n '\n \n-test_expect_failure '1d-check: Directory renames cause a rename/rename(2to1) conflict' '\n+test_expect_success '1d-check: Directory renames cause a rename/rename(2to1) conflict' '\n \t(\n \t\tcd 1d &&\n \n@@ -332,7 +332,7 @@ test_expect_success '1e-setup: Renamed directory, with all files being renamed t\n \t)\n '\n \n-test_expect_failure '1e-check: Renamed directory, with all files being renamed too' '\n+test_expect_success '1e-check: Renamed directory, with all files being renamed too' '\n \t(\n \t\tcd 1e &&\n \n@@ -397,7 +397,7 @@ test_expect_success '1f-setup: Split a directory into two other directories' '\n \t)\n '\n \n-test_expect_failure '1f-check: Split a directory into two other directories' '\n+test_expect_success '1f-check: Split a directory into two other directories' '\n \t(\n \t\tcd 1f &&\n \n@@ -875,7 +875,7 @@ test_expect_success '5a-setup: Merge directories, other side adds files to origi\n \t)\n '\n \n-test_expect_failure '5a-check: Merge directories, other side adds files to original and target' '\n+test_expect_success '5a-check: Merge directories, other side adds files to original and target' '\n \t(\n \t\tcd 5a &&\n \n@@ -949,7 +949,7 @@ test_expect_success '5b-setup: Rename/delete in order to get add/add/add conflic\n \t)\n '\n \n-test_expect_failure '5b-check: Rename/delete in order to get add/add/add conflict' '\n+test_expect_success '5b-check: Rename/delete in order to get add/add/add conflict' '\n \t(\n \t\tcd 5b &&\n \n@@ -1026,7 +1026,7 @@ test_expect_success '5c-setup: Transitive rename would cause rename/rename/renam\n \t)\n '\n \n-test_expect_failure '5c-check: Transitive rename would cause rename/rename/rename/add/add/add' '\n+test_expect_success '5c-check: Transitive rename would cause rename/rename/rename/add/add/add' '\n \t(\n \t\tcd 5c &&\n \n@@ -1108,7 +1108,7 @@ test_expect_success '5d-setup: Directory/file/file conflict due to directory ren\n \t)\n '\n \n-test_expect_failure '5d-check: Directory/file/file conflict due to directory rename' '\n+test_expect_success '5d-check: Directory/file/file conflict due to directory rename' '\n \t(\n \t\tcd 5d &&\n \n@@ -1526,7 +1526,7 @@ test_expect_success '7a-setup: rename-dir vs. rename-dir (NOT split evenly) PLUS\n \t)\n '\n \n-test_expect_failure '7a-check: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '\n+test_expect_success '7a-check: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '\n \t(\n \t\tcd 7a &&\n \n@@ -1597,7 +1597,7 @@ test_expect_success '7b-setup: rename/rename(2to1), but only due to transitive r\n \t)\n '\n \n-test_expect_failure '7b-check: rename/rename(2to1), but only due to transitive rename' '\n+test_expect_success '7b-check: rename/rename(2to1), but only due to transitive rename' '\n \t(\n \t\tcd 7b &&\n \n@@ -1670,7 +1670,7 @@ test_expect_success '7c-setup: rename/rename(1to...2or3); transitive rename may\n \t)\n '\n \n-test_expect_failure '7c-check: rename/rename(1to...2or3); transitive rename may add complexity' '\n+test_expect_success '7c-check: rename/rename(1to...2or3); transitive rename may add complexity' '\n \t(\n \t\tcd 7c &&\n \n@@ -1731,7 +1731,7 @@ test_expect_success '7d-setup: transitive rename involved in rename/delete; how\n \t)\n '\n \n-test_expect_failure '7d-check: transitive rename involved in rename/delete; how is it reported?' '\n+test_expect_success '7d-check: transitive rename involved in rename/delete; how is it reported?' '\n \t(\n \t\tcd 7d &&\n \n@@ -1818,7 +1818,7 @@ test_expect_success '7e-setup: transitive rename in rename/delete AND dirs in th\n \t)\n '\n \n-test_expect_failure '7e-check: transitive rename in rename/delete AND dirs in the way' '\n+test_expect_success '7e-check: transitive rename in rename/delete AND dirs in the way' '\n \t(\n \t\tcd 7e &&\n \n@@ -1904,7 +1904,7 @@ test_expect_success '8a-setup: Dual-directory rename, one into the others way' '\n \t)\n '\n \n-test_expect_failure '8a-check: Dual-directory rename, one into the others way' '\n+test_expect_success '8a-check: Dual-directory rename, one into the others way' '\n \t(\n \t\tcd 8a &&\n \n@@ -2043,7 +2043,7 @@ test_expect_success '8c-setup: rename+modify/delete' '\n \t)\n '\n \n-test_expect_failure '8c-check: rename+modify/delete' '\n+test_expect_success '8c-check: rename+modify/delete' '\n \t(\n \t\tcd 8c &&\n \n@@ -2127,7 +2127,7 @@ test_expect_success '8d-setup: rename/delete...or not?' '\n \t)\n '\n \n-test_expect_failure '8d-check: rename/delete...or not?' '\n+test_expect_success '8d-check: rename/delete...or not?' '\n \t(\n \t\tcd 8d &&\n \n@@ -2201,7 +2201,7 @@ test_expect_success '8e-setup: Both sides rename, one side adds to original dire\n \t)\n '\n \n-test_expect_failure '8e-check: Both sides rename, one side adds to original directory' '\n+test_expect_success '8e-check: Both sides rename, one side adds to original directory' '\n \t(\n \t\tcd 8e &&\n \n@@ -2288,7 +2288,7 @@ test_expect_success '9a-setup: Inner renamed directory within outer renamed dire\n \t)\n '\n \n-test_expect_failure '9a-check: Inner renamed directory within outer renamed directory' '\n+test_expect_success '9a-check: Inner renamed directory within outer renamed directory' '\n \t(\n \t\tcd 9a &&\n \n@@ -2355,7 +2355,7 @@ test_expect_success '9b-setup: Transitive rename with content merge' '\n \t)\n '\n \n-test_expect_failure '9b-check: Transitive rename with content merge' '\n+test_expect_success '9b-check: Transitive rename with content merge' '\n \t(\n \t\tcd 9b &&\n \n@@ -2444,7 +2444,7 @@ test_expect_success '9c-setup: Doubly transitive rename?' '\n \t)\n '\n \n-test_expect_failure '9c-check: Doubly transitive rename?' '\n+test_expect_success '9c-check: Doubly transitive rename?' '\n \t(\n \t\tcd 9c &&\n \n@@ -2530,7 +2530,7 @@ test_expect_success '9d-setup: N-way transitive rename?' '\n \t)\n '\n \n-test_expect_failure '9d-check: N-way transitive rename?' '\n+test_expect_success '9d-check: N-way transitive rename?' '\n \t(\n \t\tcd 9d &&\n \n@@ -2606,7 +2606,7 @@ test_expect_success '9e-setup: N-to-1 whammo' '\n \t)\n '\n \n-test_expect_failure '9e-check: N-to-1 whammo' '\n+test_expect_success '9e-check: N-to-1 whammo' '\n \t(\n \t\tcd 9e &&\n \n@@ -2681,7 +2681,7 @@ test_expect_success '9f-setup: Renamed directory that only contained immediate s\n \t)\n '\n \n-test_expect_failure '9f-check: Renamed directory that only contained immediate subdirs' '\n+test_expect_success '9f-check: Renamed directory that only contained immediate subdirs' '\n \t(\n \t\tcd 9f &&\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333736","messageId":"20171129014237.32570-24-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 23/34] merge-recursive: check for directory level conflicts","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:26Z","receivedAt":"2017-11-29T01:43:55Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Before trying to apply directory renames to paths within the given\ndirectories, we want to make sure that there aren't conflicts at the\ndirectory level.  There will be additional checks at the individual\nfile level too, which will be added later.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 119 insertions(+)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 6aef357e78..d92fba2775 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1384,6 +1384,15 @@ static struct diff_queue_struct *get_diffpairs(struct merge_options *o,\n \treturn ret;\n }\n \n+static int tree_has_path(struct tree *tree, const char *path)\n+{\n+\tunsigned char hashy[20];\n+\tunsigned int mode_o;\n+\n+\treturn !get_tree_entry(tree->object.oid.hash, path,\n+\t\t\t       hashy, &mode_o);\n+}\n+\n static void get_renamed_dir_portion(const char *old_path, const char *new_path,\n \t\t\t\t    char **old_dir, char **new_dir)\n {\n@@ -1438,6 +1447,112 @@ static void get_renamed_dir_portion(const char *old_path, const char *new_path,\n \t}\n }\n \n+static void remove_hashmap_entries(struct hashmap *dir_renames,\n+\t\t\t\t   struct string_list *items_to_remove)\n+{\n+\tint i;\n+\tstruct dir_rename_entry *entry;\n+\n+\tfor (i = 0; i < items_to_remove->nr; i++) {\n+\t\tentry = items_to_remove->items[i].util;\n+\t\thashmap_remove(dir_renames, entry, NULL);\n+\t}\n+\tstring_list_clear(items_to_remove, 0);\n+}\n+\n+/*\n+ * There are a couple things we want to do at the directory level:\n+ *   1. Check for both sides renaming to the same thing, in order to avoid\n+ *      implicit renaming of files that should be left in place.  (See\n+ *      testcase 6b in t6043 for details.)\n+ *   2. Prune directory renames if there are still files left in the\n+ *      the original directory.  These represent a partial directory rename,\n+ *      i.e. a rename where only some of the files within the directory\n+ *      were renamed elsewhere.  (Technically, this could be done earlier\n+ *      in get_directory_renames(), except that would prevent us from\n+ *      doing the previous check and thus failing testcase 6b.)\n+ *   3. Check for rename/rename(1to2) conflicts (at the directory level).\n+ *      In the future, we could potentially record this info as well and\n+ *      omit reporting rename/rename(1to2) conflicts for each path within\n+ *      the affected directories, thus cleaning up the merge output.\n+ *   NOTE: We do NOT check for rename/rename(2to1) conflicts at the\n+ *         directory level, because merging directories is fine.  If it\n+ *         causes conflicts for files within those merged directories, then\n+ *         that should be detected at the individual path level.\n+ */\n+static void handle_directory_level_conflicts(struct merge_options *o,\n+\t\t\t\t\t     struct hashmap *dir_re_head,\n+\t\t\t\t\t     struct tree *head,\n+\t\t\t\t\t     struct hashmap *dir_re_merge,\n+\t\t\t\t\t     struct tree *merge)\n+{\n+\tstruct hashmap_iter iter;\n+\tstruct dir_rename_entry *head_ent;\n+\tstruct dir_rename_entry *merge_ent;\n+\n+\tstruct string_list remove_from_head = STRING_LIST_INIT_NODUP;\n+\tstruct string_list remove_from_merge = STRING_LIST_INIT_NODUP;\n+\n+\thashmap_iter_init(dir_re_head, &iter);\n+\twhile ((head_ent = hashmap_iter_next(&iter))) {\n+\t\tmerge_ent = dir_rename_find_entry(dir_re_merge, head_ent->dir);\n+\t\tif (merge_ent &&\n+\t\t    !head_ent->non_unique_new_dir &&\n+\t\t    !merge_ent->non_unique_new_dir &&\n+\t\t    !strbuf_cmp(&head_ent->new_dir, &merge_ent->new_dir)) {\n+\t\t\t/* 1. Renamed identically; remove it from both sides */\n+\t\t\tstring_list_append(&remove_from_head,\n+\t\t\t\t\t   head_ent->dir)->util = head_ent;\n+\t\t\tstrbuf_release(&head_ent->new_dir);\n+\t\t\tstring_list_append(&remove_from_merge,\n+\t\t\t\t\t   merge_ent->dir)->util = merge_ent;\n+\t\t\tstrbuf_release(&merge_ent->new_dir);\n+\t\t} else if (tree_has_path(head, head_ent->dir)) {\n+\t\t\t/* 2. This wasn't a directory rename after all */\n+\t\t\tstring_list_append(&remove_from_head,\n+\t\t\t\t\t   head_ent->dir)->util = head_ent;\n+\t\t\tstrbuf_release(&head_ent->new_dir);\n+\t\t}\n+\t}\n+\n+\tremove_hashmap_entries(dir_re_head, &remove_from_head);\n+\tremove_hashmap_entries(dir_re_merge, &remove_from_merge);\n+\n+\thashmap_iter_init(dir_re_merge, &iter);\n+\twhile ((merge_ent = hashmap_iter_next(&iter))) {\n+\t\thead_ent = dir_rename_find_entry(dir_re_head, merge_ent->dir);\n+\t\tif (tree_has_path(merge, merge_ent->dir)) {\n+\t\t\t/* 2. This wasn't a directory rename after all */\n+\t\t\tstring_list_append(&remove_from_merge,\n+\t\t\t\t\t   merge_ent->dir)->util = merge_ent;\n+\t\t} else if (head_ent &&\n+\t\t\t   !head_ent->non_unique_new_dir &&\n+\t\t\t   !merge_ent->non_unique_new_dir) {\n+\t\t\t/* 3. rename/rename(1to2) */\n+\t\t\t/*\n+\t\t\t * We can assume it's not rename/rename(1to1) because\n+\t\t\t * that was case (1), already checked above.  So we\n+\t\t\t * know that head_ent->new_dir and merge_ent->new_dir\n+\t\t\t * are different strings.\n+\t\t\t */\n+\t\t\toutput(o, 1, _(\"CONFLICT (rename/rename): \"\n+\t\t\t\t       \"Rename directory %s->%s in %s. \"\n+\t\t\t\t       \"Rename directory %s->%s in %s\"),\n+\t\t\t       head_ent->dir, head_ent->new_dir.buf, o->branch1,\n+\t\t\t       head_ent->dir, merge_ent->new_dir.buf, o->branch2);\n+\t\t\tstring_list_append(&remove_from_head,\n+\t\t\t\t\t   head_ent->dir)->util = head_ent;\n+\t\t\tstrbuf_release(&head_ent->new_dir);\n+\t\t\tstring_list_append(&remove_from_merge,\n+\t\t\t\t\t   merge_ent->dir)->util = merge_ent;\n+\t\t\tstrbuf_release(&merge_ent->new_dir);\n+\t\t}\n+\t}\n+\n+\tremove_hashmap_entries(dir_re_head, &remove_from_head);\n+\tremove_hashmap_entries(dir_re_merge, &remove_from_merge);\n+}\n+\n static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,\n \t\t\t\t\t     struct tree *tree)\n {\n@@ -1869,6 +1984,10 @@ static int handle_renames(struct merge_options *o,\n \tdir_re_head = get_directory_renames(head_pairs, head);\n \tdir_re_merge = get_directory_renames(merge_pairs, merge);\n \n+\thandle_directory_level_conflicts(o,\n+\t\t\t\t\t dir_re_head, head,\n+\t\t\t\t\t dir_re_merge, merge);\n+\n \tri->head_renames  = get_renames(o, head_pairs, head,\n \t\t\t\t\t common, head, merge, entries);\n \tri->merge_renames = get_renames(o, merge_pairs, merge,\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333737","messageId":"20171129014237.32570-1-newren@gmail.com","threadId":"47340","inReplyTo":null,"subject":"[PATCH v4 00/34] Add directory rename detection to git","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:03Z","receivedAt":"2017-11-29T01:43:58Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"This patchset introduces directory rename detection to merge-recursive.  See\n  https://public-inbox.org/git/20171110190550.27059-1-newren@gmail.com/\nfor the first series (including design considerations, etc.), and follow-up\nseries can be found at\n  https://public-inbox.org/git/20171120220209.15111-1-newren@gmail.com/\n  https://public-inbox.org/git/20171121080059.32304-1-newren@gmail.com/\n\nChanges since V3:\n  * Rebased on latest master (no conflicts, but figured I might as well)\n  * Addressed issues mentioned in reviews of V3:\n    * Fixed style issues\n    * Made use of strbuf (note: new function added to strbuf.[ch] that\n      takes a string_list*; is that okay?)\n    * Switched strndup to xstrndup for windows\n  * Miscellaneous cleanups, perf, etc.:\n    * Skip pair early in get_directory_renames() if pair is not a rename\n    * Add helper cleanup functions to consolidate some code\n    * Make it clear that directory rename detection only operates on\n      add ('A') and rename ('R') filepairs\n\nFull tbdiff against V3:\n 1: a0abcb1378 =  1: d95d5fffea Tighten and correct a few testcases for merging and cherry-picking\n 2: e8745c4f1b =  2: b177dccc28 merge-recursive: fix logic ordering issue\n 3: 0ae4156994 =  3: fe3de710e3 merge-recursive: add explanation for src_entry and dst_entry\n 4: f13686ea6a =  4: 19b82b795a directory rename detection: basic testcases\n 5: 16e7dea569 =  5: 3d23af19e9 directory rename detection: directory splitting testcases\n 6: 51d9a5b059 =  6: d997681a4d directory rename detection: testcases to avoid taking detection too far\n 7: 4b39f6bea3 =  7: 152e3d5d81 directory rename detection: partially renamed directory testcase/discussion\n 8: 0668e65864 =  8: 441211c842 directory rename detection: files/directories in the way of some renames\n 9: fcce655243 =  9: c66d3fb8b4 directory rename detection: testcases checking which side did the rename\n10: 04213cc213 = 10: 4542c5fee4 directory rename detection: more involved edge/corner testcases\n11: 21992300aa ! 11: 771b551982 directory rename detection: testcases exploring possibly suboptimal merges\n    @@ -36,7 +36,7 @@\n     +#   Optimal:                  y/{a,b,e},   z/{c,d,f}\n     +#\n     +# Note: Both x and y got renamed and it'd be nice to detect both, and we do\n    -+# better with directory rename detection than git did previously, but the\n    ++# better with directory rename detection than git did without, but the\n     +# simple rule from section 5 prevents me from handling this as optimally as\n     +# we potentially could.\n     +\n    @@ -321,9 +321,11 @@\n     +#   Commit B: w/{b,c}, z/d\n     +#\n     +# Possible Resolutions:\n    -+#   Previous git: z/d, CONFLICT(z/b -> y/b vs. w/b), CONFLICT(z/c -> y/c vs. w/c)\n    -+#   Expected:     y/d, CONFLICT(z/b -> y/b vs. w/b), CONFLICT(z/c -> y/c vs. w/c)\n    -+#   Preferred:    ??\n    ++#   w/o dir-rename detection: z/d, CONFLICT(z/b -> y/b vs. w/b),\n    ++#                                  CONFLICT(z/c -> y/c vs. w/c)\n    ++#   Currently expected:       y/d, CONFLICT(z/b -> y/b vs. w/b),\n    ++#                                  CONFLICT(z/c -> y/c vs. w/c)\n    ++#   Optimal:                  ??\n     +#\n     +# Notes: In commit A, directory z got renamed to y.  In commit B, directory z\n     +#        did NOT get renamed; the directory is still present; instead it is\n12: 727238bf64 = 12: f1f8b2e0bc directory rename detection: miscellaneous testcases to complete coverage\n13: 44b25c31e2 = 13: 595905c828 directory rename detection: tests for handling overwriting untracked files\n14: fa05a0240c = 14: 71ae85d203 directory rename detection: tests for handling overwriting dirty files\n15: 131af0e3f2 = 15: e9ca40cf6a merge-recursive: move the get_renames() function\n16: c7d1d15bec = 16: f4ff2a3c8b merge-recursive: introduce new functions to handle rename logic\n17: 1ca5c22551 = 17: 84b57b2495 merge-recursive: fix leaks of allocated renames and diff_filepairs\n18: 57288cf7b1 = 18: 03c1d06592 merge-recursive: make !o->detect_rename codepath more obvious\n19: fd6602a327 = 19: cf9d708f1d merge-recursive: split out code for determining diff_filepairs\n20: 8828dd4093 ! 20: ed33e1221f merge-recursive: add a new hashmap for storing directory renames\n    @@ -45,7 +45,7 @@\n     +\thashmap_entry_init(entry, strhash(directory));\n     +\tentry->dir = directory;\n     +\tentry->non_unique_new_dir = 0;\n    -+\tentry->new_dir = NULL;\n    ++\tstrbuf_init(&entry->new_dir, 0);\n     +\tstring_list_init(&entry->possible_new_dirs, 0);\n     +}\n     +\n    @@ -64,7 +64,7 @@\n     +\tstruct hashmap_entry ent; /* must be the first member! */\n     +\tchar *dir;\n     +\tunsigned non_unique_new_dir:1;\n    -+\tchar *new_dir;\n    ++\tstruct strbuf new_dir;\n     +\tstruct string_list possible_new_dirs;\n     +};\n     +\n--:  ------- > 21: 9b88f735c2 merge-recursive: make a helper function for cleanup for handle_renames\n21: f1d3b1f7ab ! 22: 20c94136af merge-recursive: add get_directory_renames()\n    @@ -63,8 +63,8 @@\n     +\tnew_len = end_of_new - new_path;\n     +\n     +\tif (old_len != new_len || strncmp(old_path, new_path, old_len)) {\n    -+\t\t*old_dir = strndup(old_path, old_len);\n    -+\t\t*new_dir = strndup(new_path, new_len);\n    ++\t\t*old_dir = xstrndup(old_path, old_len);\n    ++\t\t*new_dir = xstrndup(new_path, new_len);\n     +\t}\n     +}\n     +\n    @@ -84,6 +84,10 @@\n     +\t\tstruct diff_filepair *pair = pairs->queue[i];\n     +\t\tchar *old_dir, *new_dir;\n     +\n    ++\t\t/* File not part of directory rename if it wasn't renamed */\n    ++\t\tif (pair->status != 'R')\n    ++\t\t\tcontinue;\n    ++\n     +\t\tget_renamed_dir_portion(pair->one->path, pair->two->path,\n     +\t\t\t\t\t&old_dir,        &new_dir);\n     +\t\tif (!old_dir)\n    @@ -128,9 +132,11 @@\n     +\t\t}\n     +\t\tif (bad_max == max)\n     +\t\t\tentry->non_unique_new_dir = 1;\n    -+\t\telse\n    -+\t\t\tentry->new_dir = strdup(best);\n    -+\t\t/* Strings were strndup'ed before inserting into string-list,\n    ++\t\telse {\n    ++\t\t\tassert(entry->new_dir.len == 0);\n    ++\t\t\tstrbuf_addstr(&entry->new_dir, best);\n    ++\t\t}\n    ++\t\t/* Strings were xstrndup'ed before inserting into string-list,\n     +\t\t * so ask string_list to remove the entries for us.\n     +\t\t */\n     +\t\tentry->possible_new_dirs.strdup_strings = 1;\n    @@ -144,12 +150,33 @@\n       * Get information of all renames which occurred in 'pairs', making use of\n       * any implicit directory renames inferred from the other side of history.\n     @@\n    + \tstruct string_list *merge_renames;\n    + };\n    + \n    +-static void initial_cleanup_rename(struct diff_queue_struct *pairs)\n    ++static void initial_cleanup_rename(struct diff_queue_struct *pairs,\n    ++\t\t\t\t   struct hashmap *dir_renames)\n    + {\n    ++\tstruct hashmap_iter iter;\n    ++\tstruct dir_rename_entry *e;\n    ++\n    ++\thashmap_iter_init(dir_renames, &iter);\n    ++\twhile ((e = hashmap_iter_next(&iter))) {\n    ++\t\tfree(e->dir);\n    ++\t\tstrbuf_release(&e->new_dir);\n    ++\t\t/* possible_new_dirs already cleared in get_directory_renames */\n    ++\t}\n    ++\thashmap_free(dir_renames, 1);\n    ++\tfree(dir_renames);\n    ++\n    + \tfree(pairs->queue);\n    + \tfree(pairs);\n    + }\n    +@@\n      \t\t\t  struct rename_info *ri)\n      {\n      \tstruct diff_queue_struct *head_pairs, *merge_pairs;\n     +\tstruct hashmap *dir_re_head, *dir_re_merge;\n    -+\tstruct hashmap_iter iter;\n    -+\tstruct dir_rename_entry *e;\n      \tint clean;\n      \n      \tri->head_renames = NULL;\n    @@ -164,29 +191,13 @@\n      \t\t\t\t\t common, head, merge, entries);\n      \tri->merge_renames = get_renames(o, merge_pairs, merge,\n     @@\n    + \t * data structures are still needed and referenced in\n      \t * process_entry().  But there are a few things we can free now.\n      \t */\n    - \n    -+\thashmap_iter_init(dir_re_head, &iter);\n    -+\twhile ((e = hashmap_iter_next(&iter))) {\n    -+\t\tfree(e->dir);\n    -+\t\tif (e->new_dir)\n    -+\t\t\tfree(e->new_dir);\n    -+\t\t/* possible_new_dirs already cleared in get_directory_renames */\n    -+\t}\n    -+\thashmap_free(dir_re_head, 1);\n    -+\tfree(dir_re_head);\n    -+\n    -+\thashmap_iter_init(dir_re_merge, &iter);\n    -+\twhile ((e = hashmap_iter_next(&iter))) {\n    -+\t\tfree(e->dir);\n    -+\t\tif (e->new_dir)\n    -+\t\t\tfree(e->new_dir);\n    -+\t\t/* possible_new_dirs already cleared in get_directory_renames */\n    -+\t}\n    -+\thashmap_free(dir_re_merge, 1);\n    -+\tfree(dir_re_merge);\n    -+\n    - \tfree(head_pairs->queue);\n    - \tfree(head_pairs);\n    - \tfree(merge_pairs->queue);\n    +-\tinitial_cleanup_rename(head_pairs);\n    +-\tinitial_cleanup_rename(merge_pairs);\n    ++\tinitial_cleanup_rename(head_pairs, dir_re_head);\n    ++\tinitial_cleanup_rename(merge_pairs, dir_re_merge);\n    + \n    + \treturn clean;\n    + }\n22: ba1aa81d38 ! 23: da6b0646d1 merge-recursive: check for directory level conflicts\n    @@ -32,6 +32,19 @@\n      \t}\n      }\n      \n    ++static void remove_hashmap_entries(struct hashmap *dir_renames,\n    ++\t\t\t\t   struct string_list *items_to_remove)\n    ++{\n    ++\tint i;\n    ++\tstruct dir_rename_entry *entry;\n    ++\n    ++\tfor (i = 0; i < items_to_remove->nr; i++) {\n    ++\t\tentry = items_to_remove->items[i].util;\n    ++\t\thashmap_remove(dir_renames, entry, NULL);\n    ++\t}\n    ++\tstring_list_clear(items_to_remove, 0);\n    ++}\n    ++\n     +/*\n     + * There are a couple things we want to do at the directory level:\n     + *   1. Check for both sides renaming to the same thing, in order to avoid\n    @@ -61,7 +74,6 @@\n     +\tstruct hashmap_iter iter;\n     +\tstruct dir_rename_entry *head_ent;\n     +\tstruct dir_rename_entry *merge_ent;\n    -+\tint i;\n     +\n     +\tstruct string_list remove_from_head = STRING_LIST_INIT_NODUP;\n     +\tstruct string_list remove_from_merge = STRING_LIST_INIT_NODUP;\n    @@ -72,21 +84,24 @@\n     +\t\tif (merge_ent &&\n     +\t\t    !head_ent->non_unique_new_dir &&\n     +\t\t    !merge_ent->non_unique_new_dir &&\n    -+\t\t    !strcmp(head_ent->new_dir, merge_ent->new_dir)) {\n    ++\t\t    !strbuf_cmp(&head_ent->new_dir, &merge_ent->new_dir)) {\n     +\t\t\t/* 1. Renamed identically; remove it from both sides */\n     +\t\t\tstring_list_append(&remove_from_head,\n     +\t\t\t\t\t   head_ent->dir)->util = head_ent;\n    -+\t\t\tfree(head_ent->new_dir);\n    ++\t\t\tstrbuf_release(&head_ent->new_dir);\n     +\t\t\tstring_list_append(&remove_from_merge,\n     +\t\t\t\t\t   merge_ent->dir)->util = merge_ent;\n    -+\t\t\tfree(merge_ent->new_dir);\n    ++\t\t\tstrbuf_release(&merge_ent->new_dir);\n     +\t\t} else if (tree_has_path(head, head_ent->dir)) {\n     +\t\t\t/* 2. This wasn't a directory rename after all */\n     +\t\t\tstring_list_append(&remove_from_head,\n     +\t\t\t\t\t   head_ent->dir)->util = head_ent;\n    -+\t\t\tfree(head_ent->new_dir);\n    ++\t\t\tstrbuf_release(&head_ent->new_dir);\n     +\t\t}\n     +\t}\n    ++\n    ++\tremove_hashmap_entries(dir_re_head, &remove_from_head);\n    ++\tremove_hashmap_entries(dir_re_merge, &remove_from_merge);\n     +\n     +\thashmap_iter_init(dir_re_merge, &iter);\n     +\twhile ((merge_ent = hashmap_iter_next(&iter))) {\n    @@ -99,7 +114,8 @@\n     +\t\t\t   !head_ent->non_unique_new_dir &&\n     +\t\t\t   !merge_ent->non_unique_new_dir) {\n     +\t\t\t/* 3. rename/rename(1to2) */\n    -+\t\t\t/* We can assume it's not rename/rename(1to1) because\n    ++\t\t\t/*\n    ++\t\t\t * We can assume it's not rename/rename(1to1) because\n     +\t\t\t * that was case (1), already checked above.  So we\n     +\t\t\t * know that head_ent->new_dir and merge_ent->new_dir\n     +\t\t\t * are different strings.\n    @@ -107,28 +123,19 @@\n     +\t\t\toutput(o, 1, _(\"CONFLICT (rename/rename): \"\n     +\t\t\t\t       \"Rename directory %s->%s in %s. \"\n     +\t\t\t\t       \"Rename directory %s->%s in %s\"),\n    -+\t\t\t       head_ent->dir, head_ent->new_dir, o->branch1,\n    -+\t\t\t       head_ent->dir, merge_ent->new_dir, o->branch2);\n    ++\t\t\t       head_ent->dir, head_ent->new_dir.buf, o->branch1,\n    ++\t\t\t       head_ent->dir, merge_ent->new_dir.buf, o->branch2);\n     +\t\t\tstring_list_append(&remove_from_head,\n     +\t\t\t\t\t   head_ent->dir)->util = head_ent;\n    -+\t\t\tfree(head_ent->new_dir);\n    ++\t\t\tstrbuf_release(&head_ent->new_dir);\n     +\t\t\tstring_list_append(&remove_from_merge,\n     +\t\t\t\t\t   merge_ent->dir)->util = merge_ent;\n    -+\t\t\tfree(merge_ent->new_dir);\n    ++\t\t\tstrbuf_release(&merge_ent->new_dir);\n     +\t\t}\n     +\t}\n     +\n    -+\tfor (i = 0; i < remove_from_head.nr; i++) {\n    -+\t\thead_ent = remove_from_head.items[i].util;\n    -+\t\thashmap_remove(dir_re_head, head_ent, NULL);\n    -+\t}\n    -+\tfor (i = 0; i < remove_from_merge.nr; i++) {\n    -+\t\tmerge_ent = remove_from_merge.items[i].util;\n    -+\t\thashmap_remove(dir_re_merge, merge_ent, NULL);\n    -+\t}\n    -+\n    -+\tstring_list_clear(&remove_from_head, 0);\n    -+\tstring_list_clear(&remove_from_merge, 0);\n    ++\tremove_hashmap_entries(dir_re_head, &remove_from_head);\n    ++\tremove_hashmap_entries(dir_re_merge, &remove_from_merge);\n     +}\n     +\n      static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,\n23: 96534fe595 = 24: 93162a9b56 merge-recursive: add a new hashmap for storing file collisions\n24: 66ada0e7c0 ! 25: 53b26a0d61 merge-recursive: add computation of collisions due to dir rename & merging\n    @@ -26,20 +26,19 @@\n     +static char *apply_dir_rename(struct dir_rename_entry *entry,\n     +\t\t\t      const char *old_path)\n     +{\n    -+\tchar *new_path;\n    -+\tint entrylen, oldlen, newlen;\n    ++\tstruct strbuf new_path = STRBUF_INIT;\n    ++\tint oldlen, newlen;\n     +\n     +\tif (entry->non_unique_new_dir)\n     +\t\treturn NULL;\n     +\n    -+\tentrylen = strlen(entry->new_dir);\n     +\toldlen = strlen(entry->dir);\n    -+\tnewlen = entrylen + (strlen(old_path) - oldlen) + 1;\n    -+\tnew_path = malloc(newlen);\n    -+\tstrcpy(new_path, entry->new_dir);\n    -+\tstrcpy(&new_path[entrylen], &old_path[oldlen]);\n    ++\tnewlen = entry->new_dir.len + (strlen(old_path) - oldlen) + 1;\n    ++\tstrbuf_grow(&new_path, newlen);\n    ++\tstrbuf_addbuf(&new_path, &entry->new_dir);\n    ++\tstrbuf_addstr(&new_path, &old_path[oldlen]);\n     +\n    -+\treturn new_path;\n    ++\treturn strbuf_detach(&new_path, NULL);\n     +}\n     +\n      static void get_renamed_dir_portion(const char *old_path, const char *new_path,\n25: 0f49ea9449 ! 26: 26d4c78d67 merge-recursive: check for file level conflicts then get new name\n    @@ -13,25 +13,9 @@\n     --- a/merge-recursive.c\n     +++ b/merge-recursive.c\n     @@\n    - \t}\n    + \tstring_list_clear(items_to_remove, 0);\n      }\n      \n    -+/*\n    -+ * Write:\n    -+ *   element1, element2, element3, ..., elementN\n    -+ * to str.  If only one element, just write \"element1\" to str.\n    -+ */\n    -+static void comma_separated_list(char *str, struct string_list *slist)\n    -+{\n    -+\tint i;\n    -+\n    -+\tfor (i = 0; i < slist->nr; i++) {\n    -+\t\tstr += sprintf(str, \"%s\", slist->items[i].string);\n    -+\t\tif (i < slist->nr-1)\n    -+\t\t\tstr += sprintf(str, \", \");\n    -+\t}\n    -+}\n    -+\n     +/*\n     + * See if there is a directory rename for path, and if there are any file\n     + * level conflicts for the renamed location.  If there is a rename and\n    @@ -46,7 +30,7 @@\n     +\tchar *new_path = NULL;\n     +\tstruct collision_entry *collision_ent;\n     +\tint clean = 1;\n    -+\tchar *collision_paths;\n    ++\tstruct strbuf collision_paths = STRBUF_INIT;\n     +\n     +\t/*\n     +\t * entry has the mapping of old directory name to new directory name\n    @@ -84,33 +68,31 @@\n     +\t * to put multiple paths into the same location.  Warn\n     +\t * and bail on directory renames for such paths.\n     +\t */\n    -+\tcollision_paths = malloc((PATH_MAX+2) * collision_ent->source_files.nr);\n    -+\n     +\tif (collision_ent->reported_already) {\n     +\t\tclean = 0;\n     +\t} else if (tree_has_path(tree, new_path)) {\n     +\t\tcollision_ent->reported_already = 1;\n    -+\t\tcomma_separated_list(collision_paths,\n    -+\t\t\t\t     &collision_ent->source_files);\n    ++\t\tstrbuf_add_separated_string_list(&collision_paths, \", \",\n    ++\t\t\t\t\t\t &collision_ent->source_files);\n     +\t\toutput(o, 1, _(\"CONFLICT (implicit dir rename): Existing \"\n     +\t\t\t       \"file/dir at %s in the way of implicit \"\n     +\t\t\t       \"directory rename(s) putting the following \"\n     +\t\t\t       \"path(s) there: %s.\"),\n    -+\t\t       new_path, collision_paths);\n    ++\t\t       new_path, collision_paths.buf);\n     +\t\tclean = 0;\n     +\t} else if (collision_ent->source_files.nr > 1) {\n     +\t\tcollision_ent->reported_already = 1;\n    -+\t\tcomma_separated_list(collision_paths,\n    -+\t\t\t\t     &collision_ent->source_files);\n    ++\t\tstrbuf_add_separated_string_list(&collision_paths, \", \",\n    ++\t\t\t\t\t\t &collision_ent->source_files);\n     +\t\toutput(o, 1, _(\"CONFLICT (implicit dir rename): Cannot map \"\n     +\t\t\t       \"more than one path to %s; implicit directory \"\n     +\t\t\t       \"renames tried to put these paths there: %s\"),\n    -+\t\t       new_path, collision_paths);\n    ++\t\t       new_path, collision_paths.buf);\n     +\t\tclean = 0;\n     +\t}\n     +\n     +\t/* Free memory we no longer need */\n    -+\tfree(collision_paths);\n    ++\tstrbuf_release(&collision_paths);\n     +\tif (!clean && new_path) {\n     +\t\tfree(new_path);\n     +\t\treturn NULL;\n    @@ -165,11 +147,11 @@\n     +\t * As it turns out, this also prevents N-way transient rename\n     +\t * confusion; See testcases 9c and 9d of t6043.\n     +\t */\n    -+\toentry = dir_rename_find_entry(dir_rename_exclusions, entry->new_dir);\n    ++\toentry = dir_rename_find_entry(dir_rename_exclusions, entry->new_dir.buf);\n     +\tif (oentry) {\n     +\t\toutput(o, 1, _(\"WARNING: Avoiding applying %s -> %s rename \"\n     +\t\t\t       \"to %s, because %s itself was renamed.\"),\n    -+\t\t       entry->dir, entry->new_dir, path, entry->new_dir);\n    ++\t\t       entry->dir, entry->new_dir.buf, path, entry->new_dir.buf);\n     +\t} else {\n     +\t\tnew_path = handle_path_level_conflicts(o, path, entry,\n     +\t\t\t\t\t\t       collisions, tree);\n    @@ -222,9 +204,9 @@\n      \t\tre->processed = 0;\n      \t\tre->pair = pair;\n     @@\n    + {\n    + \tstruct diff_queue_struct *head_pairs, *merge_pairs;\n      \tstruct hashmap *dir_re_head, *dir_re_merge;\n    - \tstruct hashmap_iter iter;\n    - \tstruct dir_rename_entry *e;\n     -\tint clean;\n     +\tint clean = 1;\n      \n    @@ -257,6 +239,73 @@\n      \t * Some cleanup is deferred until cleanup_renames() because the\n      \t * data structures are still needed and referenced in\n     \n    +diff --git a/strbuf.c b/strbuf.c\n    +--- a/strbuf.c\n    ++++ b/strbuf.c\n    +@@\n    + #include \"cache.h\"\n    + #include \"refs.h\"\n    ++#include \"string-list.h\"\n    + #include \"utf8.h\"\n    + \n    + int starts_with(const char *str, const char *prefix)\n    +@@\n    + \treturn ret;\n    + }\n    + \n    ++void strbuf_add_separated_string_list(struct strbuf *str,\n    ++\t\t\t\t      const char *sep,\n    ++\t\t\t\t      struct string_list *slist)\n    ++{\n    ++\tstruct string_list_item *item;\n    ++\tint sep_needed = 0;\n    ++\n    ++\tfor_each_string_list_item(item, slist) {\n    ++\t\tif (sep_needed)\n    ++\t\t\tstrbuf_addstr(str, sep);\n    ++\t\tstrbuf_addstr(str, item->string);\n    ++\t\tsep_needed = 1;\n    ++\t}\n    ++}\n    ++\n    + void strbuf_list_free(struct strbuf **sbs)\n    + {\n    + \tstruct strbuf **s = sbs;\n    +\n    +diff --git a/strbuf.h b/strbuf.h\n    +--- a/strbuf.h\n    ++++ b/strbuf.h\n    +@@\n    + #ifndef STRBUF_H\n    + #define STRBUF_H\n    + \n    ++struct string_list;\n    ++\n    + /**\n    +  * strbuf's are meant to be used with all the usual C string and memory\n    +  * APIs. Given that the length of the buffer is known, it's often better to\n    +@@\n    + \treturn strbuf_split_max(sb, terminator, 0);\n    + }\n    + \n    ++/*\n    ++ * Adds all strings of a string list to the strbuf, separated by the given\n    ++ * separator.  For example, if sep is\n    ++ *   ', '\n    ++ * and slist contains\n    ++ *   ['element1', 'element2', ..., 'elementN'],\n    ++ * then write:\n    ++ *   'element1, element2, ..., elementN'\n    ++ * to str.  If only one element, just write \"element1\" to str.\n    ++ */\n    ++extern void strbuf_add_separated_string_list(struct strbuf *str,\n    ++\t\t\t\t\t     const char *sep,\n    ++\t\t\t\t\t     struct string_list *slist);\n    ++\n    + /**\n    +  * Free a NULL-terminated list of strbufs (for example, the return\n    +  * values of the strbuf_split*() functions).\n    +\n     diff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\n     --- a/t/t6043-merge-rename-directories.sh\n     +++ b/t/t6043-merge-rename-directories.sh\n26: 996de01e67 = 27: 0bbb741e1d merge-recursive: when comparing files, don't include trees\n27: 07401ad0ff = 28: 6c398f0278 merge-recursive: apply necessary modifications for directory renames\n28: fafa66de08 = 29: 4de6b08124 merge-recursive: avoid clobbering untracked files with directory renames\n29: 9ab0339ebb = 30: b710627ca6 merge-recursive: fix overwriting dirty files involved in renames\n30: feb4781c7a = 31: 7f56715998 merge-recursive: fix remaining directory rename + dirty overwrite cases\n31: 29975384e0 = 32: a51698e630 directory rename detection: new testcases showcasing a pair of bugs\n32: b084ea16ac ! 33: c27b89922e merge-recursive: avoid spurious rename/rename conflict from dir renames\n    @@ -21,7 +21,7 @@\n      \t\tstruct diff_filepair *pair = pairs->queue[i];\n      \n     -\t\tif (pair->status == 'D')\n    -+\t\tif (pair->status == 'D' || pair->status == 'M')\n    ++\t\tif (pair->status != 'A' && pair->status != 'R')\n      \t\t\tcontinue;\n      \t\tdir_rename_ent = check_dir_renamed(pair->two->path,\n      \t\t\t\t\t\t   dir_renames);\n    @@ -30,7 +30,7 @@\n      \t\tchar *new_path; /* non-NULL only with directory renames */\n      \n     -\t\tif (pair->status == 'D') {\n    -+\t\tif (pair->status == 'D' || pair->status == 'M') {\n    ++\t\tif (pair->status != 'A' && pair->status != 'R') {\n      \t\t\tdiff_free_filepair(pair);\n      \t\t\tcontinue;\n      \t\t}\n33: 62ce55426d = 34: 850bc54b15 merge-recursive: ensure we write updates for directory-renamed file\n\nElijah Newren (34):\n  Tighten and correct a few testcases for merging and cherry-picking\n  merge-recursive: fix logic ordering issue\n  merge-recursive: add explanation for src_entry and dst_entry\n  directory rename detection: basic testcases\n  directory rename detection: directory splitting testcases\n  directory rename detection: testcases to avoid taking detection too\n    far\n  directory rename detection: partially renamed directory\n    testcase/discussion\n  directory rename detection: files/directories in the way of some\n    renames\n  directory rename detection: testcases checking which side did the\n    rename\n  directory rename detection: more involved edge/corner testcases\n  directory rename detection: testcases exploring possibly suboptimal\n    merges\n  directory rename detection: miscellaneous testcases to complete\n    coverage\n  directory rename detection: tests for handling overwriting untracked\n    files\n  directory rename detection: tests for handling overwriting dirty files\n  merge-recursive: move the get_renames() function\n  merge-recursive: introduce new functions to handle rename logic\n  merge-recursive: fix leaks of allocated renames and diff_filepairs\n  merge-recursive: make !o->detect_rename codepath more obvious\n  merge-recursive: split out code for determining diff_filepairs\n  merge-recursive: add a new hashmap for storing directory renames\n  merge-recursive: make a helper function for cleanup for handle_renames\n  merge-recursive: add get_directory_renames()\n  merge-recursive: check for directory level conflicts\n  merge-recursive: add a new hashmap for storing file collisions\n  merge-recursive: add computation of collisions due to dir rename &\n    merging\n  merge-recursive: check for file level conflicts then get new name\n  merge-recursive: when comparing files, don't include trees\n  merge-recursive: apply necessary modifications for directory renames\n  merge-recursive: avoid clobbering untracked files with directory\n    renames\n  merge-recursive: fix overwriting dirty files involved in renames\n  merge-recursive: fix remaining directory rename + dirty overwrite\n    cases\n  directory rename detection: new testcases showcasing a pair of bugs\n  merge-recursive: avoid spurious rename/rename conflict from dir\n    renames\n  merge-recursive: ensure we write updates for directory-renamed file\n\n merge-recursive.c                   | 1231 ++++++++++-\n merge-recursive.h                   |   17 +\n strbuf.c                            |   16 +\n strbuf.h                            |   16 +\n t/t3501-revert-cherry-pick.sh       |    5 +-\n t/t6043-merge-rename-directories.sh | 3823 +++++++++++++++++++++++++++++++++++\n t/t7607-merge-overwrite.sh          |    7 +-\n unpack-trees.c                      |    4 +-\n unpack-trees.h                      |    4 +\n 9 files changed, 5007 insertions(+), 116 deletions(-)\n create mode 100755 t/t6043-merge-rename-directories.sh\n\n-- \n2.15.0.408.g850bc54b15\n"},{"id":"333738","messageId":"20171129014237.32570-7-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 06/34] directory rename detection: testcases to avoid taking detection too far","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:09Z","receivedAt":"2017-11-29T01:44:03Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 150 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 150 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 335aa1c145..0ccabed4a2 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -564,4 +564,154 @@ test_expect_success '2b-check: Directory split into two on one side, with equal\n #   messages are handled correctly.\n ###########################################################################\n \n+\n+###########################################################################\n+# SECTION 3: Path in question is the source path for some rename already\n+#\n+# Combining cases from Section 1 and trying to handle them could lead to\n+# directory renaming detection being over-applied.  So, this section\n+# provides some good testcases to check that the implementation doesn't go\n+# too far.\n+###########################################################################\n+\n+# Testcase 3a, Avoid implicit rename if involved as source on other side\n+#   (Related to testcases 1c and 1f)\n+#   Commit O: z/{b,c,d}\n+#   Commit A: z/{b,c,d} (no change)\n+#   Commit B: y/{b,c}, x/d\n+#   Expected: y/{b,c}, x/d\n+test_expect_success '3a-setup: Avoid implicit rename if involved as source on other side' '\n+\ttest_create_repo 3a &&\n+\t(\n+\t\tcd 3a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\ttest_tick &&\n+\t\tgit commit --allow-empty -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir y &&\n+\t\tmkdir x &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\tgit mv z/d x/ &&\n+\t\trmdir z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '3a-check: Avoid implicit rename if involved as source on other side' '\n+\t(\n+\t\tcd 3a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:x/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:z/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 3b, Avoid implicit rename if involved as source on other side\n+#   (Related to testcases 5c and 7c, also kind of 1e and 1f)\n+#   Commit O: z/{b,c,d}\n+#   Commit A: y/{b,c}, x/d\n+#   Commit B: z/{b,c}, w/d\n+#   Expected: y/{b,c}, CONFLICT:(z/d -> x/d vs. w/d)\n+#   NOTE: We're particularly checking that since z/d is already involved as\n+#         a source in a file rename on the same side of history, that we don't\n+#         get it involved in directory rename detection.  If it were, we might\n+#         end up with CONFLICT:(z/d -> y/d vs. x/d vs. w/d), i.e. a\n+#         rename/rename/rename(1to3) conflict, which is just weird.\n+test_expect_success '3b-setup: Avoid implicit rename if involved as source on current side' '\n+\ttest_create_repo 3b &&\n+\t(\n+\t\tcd 3b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir y &&\n+\t\tmkdir x &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\tgit mv z/d x/ &&\n+\t\trmdir z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir w &&\n+\t\tgit mv z/d w/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '3b-check: Avoid implicit rename if involved as source on current side' '\n+\t(\n+\t\tcd 3b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\n+\t\ttest 5 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :1:z/d :2:x/d :3:w/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:z/d O:z/d O:z/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest ! -f z/d &&\n+\t\tgit hash-object >actual \\\n+\t\t\tx/d w/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/d O:z/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_i18ngrep CONFLICT.*rename/rename.*z/d.*x/d.*w/d out &&\n+\t! test_i18ngrep CONFLICT.*rename/rename.*y/d\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by section 3:\n+#\n+#   Avoid directory-rename-detection for a path, if that path is the source\n+#   of a rename on either side of a merge.\n+###########################################################################\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333739","messageId":"20171129014237.32570-12-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 11/34] directory rename detection: testcases exploring possibly suboptimal merges","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:14Z","receivedAt":"2017-11-29T01:44:05Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 394 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 394 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 2c57a02c6d..fc9b13c37d 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -1840,4 +1840,398 @@ test_expect_failure '7e-check: transitive rename in rename/delete AND dirs in th\n \t)\n '\n \n+###########################################################################\n+# SECTION 8: Suboptimal merges\n+#\n+# As alluded to in the last section, the ruleset we have built up for\n+# detecting directory renames unfortunately has some special cases where it\n+# results in slightly suboptimal or non-intuitive behavior.  This section\n+# explores these cases.\n+#\n+# To be fair, we already had non-intuitive or suboptimal behavior for most\n+# of these cases in git before introducing implicit directory rename\n+# detection, but it'd be nice if there was a modified ruleset out there\n+# that handled these cases a bit better.\n+###########################################################################\n+\n+# Testcase 8a, Dual-directory rename, one into the others' way\n+#   Commit O. x/{a,b},   y/{c,d}\n+#   Commit A. x/{a,b,e}, y/{c,d,f}\n+#   Commit B. y/{a,b},   z/{c,d}\n+#\n+# Possible Resolutions:\n+#   w/o dir-rename detection: y/{a,b,f},   z/{c,d},   x/e\n+#   Currently expected:       y/{a,b,e,f}, z/{c,d}\n+#   Optimal:                  y/{a,b,e},   z/{c,d,f}\n+#\n+# Note: Both x and y got renamed and it'd be nice to detect both, and we do\n+# better with directory rename detection than git did without, but the\n+# simple rule from section 5 prevents me from handling this as optimally as\n+# we potentially could.\n+\n+test_expect_success '8a-setup: Dual-directory rename, one into the others way' '\n+\ttest_create_repo 8a &&\n+\t(\n+\t\tcd 8a &&\n+\n+\t\tmkdir x &&\n+\t\tmkdir y &&\n+\t\techo a >x/a &&\n+\t\techo b >x/b &&\n+\t\techo c >y/c &&\n+\t\techo d >y/d &&\n+\t\tgit add x y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\techo e >x/e &&\n+\t\techo f >y/f &&\n+\t\tgit add x/e y/f &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv y z &&\n+\t\tgit mv x y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '8a-check: Dual-directory rename, one into the others way' '\n+\t(\n+\t\tcd 8a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/a HEAD:y/b HEAD:y/e HEAD:y/f HEAD:z/c HEAD:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/a O:x/b A:x/e A:y/f O:y/c O:y/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 8b, Dual-directory rename, one into the others' way, with conflicting filenames\n+#   Commit O. x/{a_1,b_1},     y/{a_2,b_2}\n+#   Commit A. x/{a_1,b_1,e_1}, y/{a_2,b_2,e_2}\n+#   Commit B. y/{a_1,b_1},     z/{a_2,b_2}\n+#\n+#   w/o dir-rename detection: y/{a_1,b_1,e_2}, z/{a_2,b_2}, x/e_1\n+#   Currently expected:       <same>\n+#   Scary:                    y/{a_1,b_1},     z/{a_2,b_2}, CONFLICT(add/add, e_1 vs. e_2)\n+#   Optimal:                  y/{a_1,b_1,e_1}, z/{a_2,b_2,e_2}\n+#\n+# Note: Very similar to 8a, except instead of 'e' and 'f' in directories x and\n+# y, both are named 'e'.  Without directory rename detection, neither file\n+# moves directories.  Implement directory rename detection suboptimally, and\n+# you get an add/add conflict, but both files were added in commit A, so this\n+# is an add/add conflict where one side of history added both files --\n+# something we can't represent in the index.  Obviously, we'd prefer the last\n+# resolution, but our previous rules are too coarse to allow it.  Using both\n+# the rules from section 4 and section 5 save us from the Scary resolution,\n+# making us fall back to pre-directory-rename-detection behavior for both\n+# e_1 and e_2.\n+\n+test_expect_success '8b-setup: Dual-directory rename, one into the others way, with conflicting filenames' '\n+\ttest_create_repo 8b &&\n+\t(\n+\t\tcd 8b &&\n+\n+\t\tmkdir x &&\n+\t\tmkdir y &&\n+\t\techo a1 >x/a &&\n+\t\techo b1 >x/b &&\n+\t\techo a2 >y/a &&\n+\t\techo b2 >y/b &&\n+\t\tgit add x y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\techo e1 >x/e &&\n+\t\techo e2 >y/e &&\n+\t\tgit add x/e y/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv y z &&\n+\t\tgit mv x y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '8b-check: Dual-directory rename, one into the others way, with conflicting filenames' '\n+\t(\n+\t\tcd 8b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/a HEAD:y/b HEAD:z/a HEAD:z/b HEAD:x/e HEAD:y/e &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/a O:x/b O:y/a O:y/b A:x/e A:y/e &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 8c, rename+modify/delete\n+#   (Related to testcases 5b and 8d)\n+#   Commit O: z/{b,c,d}\n+#   Commit A: y/{b,c}\n+#   Commit B: z/{b,c,d_modified,e}\n+#   Expected: y/{b,c,e}, CONFLICT(rename+modify/delete: x/d -> y/d or deleted)\n+#\n+#   Note: This testcase doesn't present any concerns for me...until you\n+#         compare it with testcases 5b and 8d.  See notes in 8d for more\n+#         details.\n+\n+test_expect_success '8c-setup: rename+modify/delete' '\n+\ttest_create_repo 8c &&\n+\t(\n+\t\tcd 8c &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\ttest_seq 1 10 >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit rm z/d &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo 11 >z/d &&\n+\t\ttest_chmod +x z/d &&\n+\t\techo e >z/e &&\n+\t\tgit add z/d z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '8c-check: rename+modify/delete' '\n+\t(\n+\t\tcd 8c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/delete).* z/d.*y/d\" out &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :0:y/e :3:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/e B:z/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_must_fail git rev-parse :1:y/d &&\n+\t\ttest_must_fail git rev-parse :2:y/d &&\n+\t\tgit ls-files -s y/d | grep ^100755 &&\n+\t\ttest -f y/d\n+\t)\n+'\n+\n+# Testcase 8d, rename/delete...or not?\n+#   (Related to testcase 5b; these may appear slightly inconsistent to users;\n+#    Also related to testcases 7d and 7e)\n+#   Commit O: z/{b,c,d}\n+#   Commit A: y/{b,c}\n+#   Commit B: z/{b,c,d,e}\n+#   Expected: y/{b,c,e}\n+#\n+#   Note: It would also be somewhat reasonable to resolve this as\n+#             y/{b,c,e}, CONFLICT(rename/delete: x/d -> y/d or deleted)\n+#   The logic being that the only difference between this testcase and 8c\n+#   is that there is no modification to d.  That suggests that instead of a\n+#   rename/modify vs. delete conflict, we should just have a rename/delete\n+#   conflict, otherwise we are being inconsistent.\n+#\n+#   However...as far as consistency goes, we didn't report a conflict for\n+#   path d_1 in testcase 5b due to a different file being in the way.  So,\n+#   we seem to be forced to have cases where users can change things\n+#   slightly and get what they may perceive as inconsistent results.  It\n+#   would be nice to avoid that, but I'm not sure I see how.\n+#\n+#   In this case, I'm leaning towards: commit A was the one that deleted z/d\n+#   and it did the rename of z to y, so the two \"conflicts\" (rename vs.\n+#   delete) are both coming from commit A, which is illogical.  Conflicts\n+#   during merging are supposed to be about opposite sides doing things\n+#   differently.\n+\n+test_expect_success '8d-setup: rename/delete...or not?' '\n+\ttest_create_repo 8d &&\n+\t(\n+\t\tcd 8d &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\ttest_seq 1 10 >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit rm z/d &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo e >z/e &&\n+\t\tgit add z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '8d-check: rename/delete...or not?' '\n+\t(\n+\t\tcd 8d &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/e &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/e &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 8e, Both sides rename, one side adds to original directory\n+#   Commit O: z/{b,c}\n+#   Commit A: y/{b,c}\n+#   Commit B: w/{b,c}, z/d\n+#\n+# Possible Resolutions:\n+#   w/o dir-rename detection: z/d, CONFLICT(z/b -> y/b vs. w/b),\n+#                                  CONFLICT(z/c -> y/c vs. w/c)\n+#   Currently expected:       y/d, CONFLICT(z/b -> y/b vs. w/b),\n+#                                  CONFLICT(z/c -> y/c vs. w/c)\n+#   Optimal:                  ??\n+#\n+# Notes: In commit A, directory z got renamed to y.  In commit B, directory z\n+#        did NOT get renamed; the directory is still present; instead it is\n+#        considered to have just renamed a subset of paths in directory z\n+#        elsewhere.  Therefore, the directory rename done in commit A to z/\n+#        applies to z/d and maps it to y/d.\n+#\n+#        It's possible that users would get confused about this, but what\n+#        should we do instead?  Silently leaving at z/d seems just as bad or\n+#        maybe even worse.  Perhaps we could print a big warning about z/d\n+#        and how we're moving to y/d in this case, but when I started thinking\n+#        about the ramifications of doing that, I didn't know how to rule out\n+#        that opening other weird edge and corner cases so I just punted.\n+\n+test_expect_success '8e-setup: Both sides rename, one side adds to original directory' '\n+\ttest_create_repo 8e &&\n+\t(\n+\t\tcd 8e &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z w &&\n+\t\tmkdir z &&\n+\t\techo d >z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '8e-check: Both sides rename, one side adds to original directory' '\n+\t(\n+\t\tcd 8e &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\n+\t\ttest 7 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 6 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\ttest $(git rev-parse :0:y/d) = $(git rev-parse B:z/d) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:w/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/b O:z/b O:z/c O:z/c O:z/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit hash-object >actual \\\n+\t\t\ty/b w/b y/c w/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/b O:z/c O:z/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest ! -f z/b &&\n+\t\ttest ! -f z/c &&\n+\n+\t\ttest_i18ngrep CONFLICT.*rename/rename.*z/c.*y/c.*w/c out &&\n+\t\ttest_i18ngrep CONFLICT.*rename/rename.*z/b.*y/b.*w/b out\n+\t)\n+'\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333740","messageId":"20171129014237.32570-4-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 03/34] merge-recursive: add explanation for src_entry and dst_entry","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:06Z","receivedAt":"2017-11-29T01:44:07Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"If I have to walk through the debugger and inspect the values found in\nhere in order to figure out their meaning, despite having known these\nthings inside and out some years back, then they probably need a comment\nfor the casual reader to explain their purpose.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 19 +++++++++++++++++++\n 1 file changed, 19 insertions(+)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 98c84e73d3..d78853d5ee 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -513,6 +513,25 @@ static void record_df_conflict_files(struct merge_options *o,\n \n struct rename {\n \tstruct diff_filepair *pair;\n+\t/*\n+\t * Purpose of src_entry and dst_entry:\n+\t *\n+\t * If 'before' is renamed to 'after' then src_entry will contain\n+\t * the versions of 'before' from the merge_base, HEAD, and MERGE in\n+\t * stages 1, 2, and 3; dst_entry will contain the respective\n+\t * versions of 'after' in corresponding locations.  Thus, we have a\n+\t * total of six modes and oids, though some will be null.  (Stage 0\n+\t * is ignored; we're interested in handling conflicts.)\n+\t *\n+\t * Since we don't turn on break-rewrites by default, neither\n+\t * src_entry nor dst_entry can have all three of their stages have\n+\t * non-null oids, meaning at most four of the six will be non-null.\n+\t * Also, since this is a rename, both src_entry and dst_entry will\n+\t * have at least one non-null oid, meaning at least two will be\n+\t * non-null.  Of the six oids, a typical rename will have three be\n+\t * non-null.  Only two implies a rename/delete, and four implies a\n+\t * rename/add.\n+\t */\n \tstruct stage_data *src_entry;\n \tstruct stage_data *dst_entry;\n \tunsigned processed:1;\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333741","messageId":"20171129014237.32570-15-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 14/34] directory rename detection: tests for handling overwriting dirty files","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:17Z","receivedAt":"2017-11-29T01:44:11Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 436 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 436 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 00b0ee7f08..0644b95ec5 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -3105,4 +3105,440 @@ test_expect_failure '10e-check: Does git complain about untracked file that is n\n \t)\n '\n \n+###########################################################################\n+# SECTION 11: Handling dirty (not up-to-date) files\n+#\n+# unpack_trees(), upon which the recursive merge algorithm is based, aborts\n+# the operation if untracked or dirty files would be deleted or overwritten\n+# by the merge.  Unfortunately, unpack_trees() does not understand renames,\n+# and if it doesn't abort, then it muddies up the working directory before\n+# we even get to the point of detecting renames, so we need some special\n+# handling.  This was true even of normal renames, but there are additional\n+# codepaths that need special handling with directory renames.  Add\n+# testcases for both renamed-by-directory-rename-detection and standard\n+# rename cases.\n+###########################################################################\n+\n+# Testcase 11a, Avoid losing dirty contents with simple rename\n+#   Commit O: z/{a,b_v1},\n+#   Commit A: z/{a,c_v1}, and z/c_v1 has uncommitted mods\n+#   Commit B: z/{a,b_v2}\n+#   Expected: ERROR_MSG(Refusing to lose dirty file at z/c) +\n+#             z/a, staged version of z/c has sha1sum matching B:z/b_v2,\n+#             z/c~HEAD with contents of B:z/b_v2,\n+#             z/c with uncommitted mods on top of A:z/c_v1\n+\n+test_expect_success '11a-setup: Avoid losing dirty contents with simple rename' '\n+\ttest_create_repo 11a &&\n+\t(\n+\t\tcd 11a &&\n+\n+\t\tmkdir z &&\n+\t\techo a >z/a &&\n+\t\ttest_seq 1 10 >z/b &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z/b z/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo 11 >>z/b &&\n+\t\tgit add z/b &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '11a-check: Avoid losing dirty contents with simple rename' '\n+\t(\n+\t\tcd 11a &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo stuff >>z/c &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"Refusing to lose dirty file at z/c\" out &&\n+\n+\t\ttest_seq 1 10 >expected &&\n+\t\techo stuff >>expected &&\n+\n+\t\ttest 2 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:z/a :2:z/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/a B:z/b &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest \"$(git hash-object z/c~HEAD)\" = $(git rev-parse B:z/b) &&\n+\t\ttest_cmp expected z/c\n+\t)\n+'\n+\n+# Testcase 11b, Avoid losing dirty file involved in directory rename\n+#   Commit O: z/a,         x/{b,c_v1}\n+#   Commit A: z/{a,c_v1},  x/b,       and z/c_v1 has uncommitted mods\n+#   Commit B: y/a,         x/{b,c_v2}\n+#   Expected: y/{a,c_v2}, x/b, z/c_v1 with uncommitted mods untracked,\n+#             ERROR_MSG(Refusing to lose dirty file at z/c)\n+\n+\n+test_expect_success '11b-setup: Avoid losing dirty file involved in directory rename' '\n+\ttest_create_repo 11b &&\n+\t(\n+\t\tcd 11b &&\n+\n+\t\tmkdir z x &&\n+\t\techo a >z/a &&\n+\t\techo b >x/b &&\n+\t\ttest_seq 1 10 >x/c &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv x/c z/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z y &&\n+\t\techo 11 >>x/c &&\n+\t\tgit add x/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '11b-check: Avoid losing dirty file involved in directory rename' '\n+\t(\n+\t\tcd 11b &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo stuff >>z/c &&\n+\n+\t\tgit merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"Refusing to lose dirty file at z/c\" out &&\n+\n+\t\tgrep -q stuff */* &&\n+\t\ttest_seq 1 10 >expected &&\n+\t\techo stuff >>expected &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -m | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:x/b :0:y/a :0:y/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/b O:z/a B:x/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest \"$(git hash-object y/c)\" = $(git rev-parse B:x/c) &&\n+\t\ttest_cmp expected z/c\n+\t)\n+'\n+\n+# Testcase 11c, Avoid losing not-up-to-date with rename + D/F conflict\n+#   Commit O: y/a,         x/{b,c_v1}\n+#   Commit A: y/{a,c_v1},  x/b,       and y/c_v1 has uncommitted mods\n+#   Commit B: y/{a,c/d},   x/{b,c_v2}\n+#   Expected: Abort_msg(\"following files would be overwritten by merge\") +\n+#             y/c left untouched (still has uncommitted mods)\n+\n+test_expect_success '11c-setup: Avoid losing not-uptodate with rename + D/F conflict' '\n+\ttest_create_repo 11c &&\n+\t(\n+\t\tcd 11c &&\n+\n+\t\tmkdir y x &&\n+\t\techo a >y/a &&\n+\t\techo b >x/b &&\n+\t\ttest_seq 1 10 >x/c &&\n+\t\tgit add y x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv x/c y/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir y/c &&\n+\t\techo d >y/c/d &&\n+\t\techo 11 >>x/c &&\n+\t\tgit add x/c y/c/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '11c-check: Avoid losing not-uptodate with rename + D/F conflict' '\n+\t(\n+\t\tcd 11c &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo stuff >>y/c &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"following files would be overwritten by merge\" err &&\n+\n+\t\tgrep -q stuff */* &&\n+\t\ttest_seq 1 10 >expected &&\n+\t\techo stuff >>expected &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -m | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\ttest $(git rev-parse HEAD) = $(git rev-parse A) &&\n+\t\ttest_cmp expected y/c\n+\t)\n+'\n+\n+# Testcase 11d, Avoid losing not-up-to-date with rename + D/F conflict\n+#   Commit O: z/a,         x/{b,c_v1}\n+#   Commit A: z/{a,c_v1},  x/b,       and z/c_v1 has uncommitted mods\n+#   Commit B: y/{a,c/d},   x/{b,c_v2}\n+#   Expected: D/F: y/c_v2 vs y/c/d) +\n+#             Warning_Msg(\"Refusing to lose dirty file at z/c) +\n+#             y/{a,c~HEAD,c/d}, x/b, now-untracked z/c_v1 with uncommitted mods\n+\n+test_expect_success '11d-setup: Avoid losing not-uptodate with rename + D/F conflict' '\n+\ttest_create_repo 11d &&\n+\t(\n+\t\tcd 11d &&\n+\n+\t\tmkdir z x &&\n+\t\techo a >z/a &&\n+\t\techo b >x/b &&\n+\t\ttest_seq 1 10 >x/c &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv x/c z/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z y &&\n+\t\tmkdir y/c &&\n+\t\techo d >y/c/d &&\n+\t\techo 11 >>x/c &&\n+\t\tgit add x/c y/c/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '11d-check: Avoid losing not-uptodate with rename + D/F conflict' '\n+\t(\n+\t\tcd 11d &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo stuff >>z/c &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"Refusing to lose dirty file at z/c\" out &&\n+\n+\t\tgrep -q stuff */* &&\n+\t\ttest_seq 1 10 >expected &&\n+\t\techo stuff >>expected &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 5 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:x/b :0:y/a :0:y/c/d :3:y/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/b O:z/a B:y/c/d B:x/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest \"$(git hash-object y/c~HEAD)\" = $(git rev-parse B:x/c) &&\n+\t\ttest_cmp expected z/c\n+\t)\n+'\n+\n+# Testcase 11e, Avoid deleting not-up-to-date with dir rename/rename(1to2)/add\n+#   Commit O: z/{a,b},      x/{c_1,d}\n+#   Commit A: y/{a,b,c_2},  x/d, w/c_1, and y/c_2 has uncommitted mods\n+#   Commit B: z/{a,b,c_1},  x/d\n+#   Expected: Failed Merge; y/{a,b} + x/d +\n+#             CONFLICT(rename/rename) x/c_1 -> w/c_1 vs y/c_1 +\n+#             ERROR_MSG(Refusing to lose dirty file at y/c)\n+#             y/c~B^0 has O:x/c_1 contents\n+#             y/c~HEAD has A:y/c_2 contents\n+#             y/c has dirty file from before merge\n+\n+test_expect_success '11e-setup: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '\n+\ttest_create_repo 11e &&\n+\t(\n+\t\tcd 11e &&\n+\n+\t\tmkdir z x &&\n+\t\techo a >z/a &&\n+\t\techo b >z/b &&\n+\t\techo c >x/c &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z/ y/ &&\n+\t\techo different >y/c &&\n+\t\tmkdir w &&\n+\t\tgit mv x/c w/ &&\n+\t\tgit add y/c &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/c z/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '11e-check: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '\n+\t(\n+\t\tcd 11e &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo mods >>y/c &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename)\" out &&\n+\t\ttest_i18ngrep \"Refusing to lose dirty file at y/c\" out &&\n+\n+\t\ttest 7 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\techo different >expected &&\n+\t\techo mods >>expected &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/a :0:y/b :0:x/d :1:x/c :2:w/c :2:y/c :3:y/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/a O:z/b O:x/d O:x/c O:x/c A:y/c O:x/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit hash-object >actual \\\n+\t\t\ty/c~B^0 y/c~HEAD &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/c A:y/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_cmp expected y/c\n+\t)\n+'\n+\n+# Testcase 11f, Avoid deleting not-up-to-date w/ dir rename/rename(2to1)\n+#   Commit O: z/{a,b},        x/{c_1,d_2}\n+#   Commit A: y/{a,b,wham_1}, x/d_2, except y/wham has uncommitted mods\n+#   Commit B: z/{a,b,wham_2}, x/c_1\n+#   Expected: Failed Merge; y/{a,b} + untracked y/{wham~B^0,wham~B^HEAD} +\n+#             y/wham with dirty changes from before merge +\n+#             CONFLICT(rename/rename) x/c vs x/d -> y/wham\n+#             ERROR_MSG(Refusing to lose dirty file at y/wham)\n+\n+test_expect_success '11f-setup: Avoid deleting not-uptodate with dir rename/rename(2to1)' '\n+\ttest_create_repo 11f &&\n+\t(\n+\t\tcd 11f &&\n+\n+\t\tmkdir z x &&\n+\t\techo a >z/a &&\n+\t\techo b >z/b &&\n+\t\ttest_seq 1 10 >x/c &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z/ y/ &&\n+\t\tgit mv x/c y/wham &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/wham &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '11f-check: Avoid deleting not-uptodate with dir rename/rename(2to1)' '\n+\t(\n+\t\tcd 11f &&\n+\n+\t\tgit checkout A^0 &&\n+\t\techo important >>y/wham &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out 2>err &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename)\" out &&\n+\t\ttest_i18ngrep \"Refusing to lose dirty file at y/wham\" out &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 4 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\ttest_seq 1 10 >expected &&\n+\t\techo important >>expected &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/a :0:y/b :2:y/wham :3:y/wham &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/a O:z/b O:x/c O:x/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_must_fail git rev-parse :1:y/wham &&\n+\n+\t\ttest_cmp expected y/wham &&\n+\t\tgit hash-object >actual \\\n+\t\t\ty/wham~B^0 y/wham~HEAD &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/d O:x/c &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333742","messageId":"20171129014237.32570-26-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 25/34] merge-recursive: add computation of collisions due to dir rename & merging","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:28Z","receivedAt":"2017-11-29T01:44:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"directory renaming and merging can cause one or more files to be moved to\nwhere an existing file is, or to cause several files to all be moved to\nthe same (otherwise vacant) location.  Add checking and reporting for such\ncases, falling back to no-directory-rename handling for such paths.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 120 insertions(+), 3 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 6bd4f34d55..9e31baaf33 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1416,6 +1416,31 @@ static int tree_has_path(struct tree *tree, const char *path)\n \t\t\t       hashy, &mode_o);\n }\n \n+/*\n+ * Return a new string that replaces the beginning portion (which matches\n+ * entry->dir), with entry->new_dir.  In perl-speak:\n+ *   new_path_name = (old_path =~ s/entry->dir/entry->new_dir/);\n+ * NOTE:\n+ *   Caller must ensure that old_path starts with entry->dir + '/'.\n+ */\n+static char *apply_dir_rename(struct dir_rename_entry *entry,\n+\t\t\t      const char *old_path)\n+{\n+\tstruct strbuf new_path = STRBUF_INIT;\n+\tint oldlen, newlen;\n+\n+\tif (entry->non_unique_new_dir)\n+\t\treturn NULL;\n+\n+\toldlen = strlen(entry->dir);\n+\tnewlen = entry->new_dir.len + (strlen(old_path) - oldlen) + 1;\n+\tstrbuf_grow(&new_path, newlen);\n+\tstrbuf_addbuf(&new_path, &entry->new_dir);\n+\tstrbuf_addstr(&new_path, &old_path[oldlen]);\n+\n+\treturn strbuf_detach(&new_path, NULL);\n+}\n+\n static void get_renamed_dir_portion(const char *old_path, const char *new_path,\n \t\t\t\t    char **old_dir, char **new_dir)\n {\n@@ -1654,6 +1679,84 @@ static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,\n \treturn dir_renames;\n }\n \n+static struct dir_rename_entry *check_dir_renamed(const char *path,\n+\t\t\t\t\t\t  struct hashmap *dir_renames)\n+{\n+\tchar temp[PATH_MAX];\n+\tchar *end;\n+\tstruct dir_rename_entry *entry;\n+\n+\tstrcpy(temp, path);\n+\twhile ((end = strrchr(temp, '/'))) {\n+\t\t*end = '\\0';\n+\t\tentry = dir_rename_find_entry(dir_renames, temp);\n+\t\tif (entry)\n+\t\t\treturn entry;\n+\t}\n+\treturn NULL;\n+}\n+\n+static void compute_collisions(struct hashmap *collisions,\n+\t\t\t       struct hashmap *dir_renames,\n+\t\t\t       struct diff_queue_struct *pairs)\n+{\n+\tint i;\n+\n+\t/*\n+\t * Multiple files can be mapped to the same path due to directory\n+\t * renames done by the other side of history.  Since that other\n+\t * side of history could have merged multiple directories into one,\n+\t * if our side of history added the same file basename to each of\n+\t * those directories, then all N of them would get implicitly\n+\t * renamed by the directory rename detection into the same path,\n+\t * and we'd get an add/add/.../add conflict, and all those adds\n+\t * from *this* side of history.  This is not representable in the\n+\t * index, and users aren't going to easily be able to make sense of\n+\t * it.  So we need to provide a good warning about what's\n+\t * happening, and fall back to no-directory-rename detection\n+\t * behavior for those paths.\n+\t *\n+\t * See testcases 9e and all of section 5 from t6043 for examples.\n+\t */\n+\tcollision_init(collisions);\n+\n+\tfor (i = 0; i < pairs->nr; ++i) {\n+\t\tstruct dir_rename_entry *dir_rename_ent;\n+\t\tstruct collision_entry *collision_ent;\n+\t\tchar *new_path;\n+\t\tstruct diff_filepair *pair = pairs->queue[i];\n+\n+\t\tif (pair->status == 'D')\n+\t\t\tcontinue;\n+\t\tdir_rename_ent = check_dir_renamed(pair->two->path,\n+\t\t\t\t\t\t   dir_renames);\n+\t\tif (!dir_rename_ent)\n+\t\t\tcontinue;\n+\n+\t\tnew_path = apply_dir_rename(dir_rename_ent, pair->two->path);\n+\t\tif (!new_path)\n+\t\t\t/*\n+\t\t\t * dir_rename_ent->non_unique_new_path is true, which\n+\t\t\t * means there is no directory rename for us to use,\n+\t\t\t * which means it won't cause us any additional\n+\t\t\t * collisions.\n+\t\t\t */\n+\t\t\tcontinue;\n+\t\tcollision_ent = collision_find_entry(collisions, new_path);\n+\t\tif (!collision_ent) {\n+\t\t\tcollision_ent = xcalloc(1,\n+\t\t\t\t\t\tsizeof(struct collision_entry));\n+\t\t\thashmap_entry_init(collision_ent, strhash(new_path));\n+\t\t\thashmap_put(collisions, collision_ent);\n+\t\t\tcollision_ent->target_file = new_path;\n+\t\t} else {\n+\t\t\tfree(new_path);\n+\t\t}\n+\t\tstring_list_insert(&collision_ent->source_files,\n+\t\t\t\t   pair->two->path);\n+\t}\n+}\n+\n /*\n  * Get information of all renames which occurred in 'pairs', making use of\n  * any implicit directory renames inferred from the other side of history.\n@@ -1663,6 +1766,7 @@ static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,\n  */\n static struct string_list *get_renames(struct merge_options *o,\n \t\t\t\t       struct diff_queue_struct *pairs,\n+\t\t\t\t       struct hashmap *dir_renames,\n \t\t\t\t       struct tree *tree,\n \t\t\t\t       struct tree *o_tree,\n \t\t\t\t       struct tree *a_tree,\n@@ -1670,8 +1774,12 @@ static struct string_list *get_renames(struct merge_options *o,\n \t\t\t\t       struct string_list *entries)\n {\n \tint i;\n+\tstruct hashmap collisions;\n+\tstruct hashmap_iter iter;\n+\tstruct collision_entry *e;\n \tstruct string_list *renames;\n \n+\tcompute_collisions(&collisions, dir_renames, pairs);\n \trenames = xcalloc(1, sizeof(struct string_list));\n \n \tfor (i = 0; i < pairs->nr; ++i) {\n@@ -1702,6 +1810,13 @@ static struct string_list *get_renames(struct merge_options *o,\n \t\titem = string_list_insert(renames, pair->one->path);\n \t\titem->util = re;\n \t}\n+\n+\thashmap_iter_init(&collisions, &iter);\n+\twhile ((e = hashmap_iter_next(&iter))) {\n+\t\tfree(e->target_file);\n+\t\tstring_list_clear(&e->source_files, 0);\n+\t}\n+\thashmap_free(&collisions, 1);\n \treturn renames;\n }\n \n@@ -2011,9 +2126,11 @@ static int handle_renames(struct merge_options *o,\n \t\t\t\t\t dir_re_head, head,\n \t\t\t\t\t dir_re_merge, merge);\n \n-\tri->head_renames  = get_renames(o, head_pairs, head,\n-\t\t\t\t\t common, head, merge, entries);\n-\tri->merge_renames = get_renames(o, merge_pairs, merge,\n+\tri->head_renames  = get_renames(o, head_pairs,\n+\t\t\t\t\tdir_re_merge, head,\n+\t\t\t\t\tcommon, head, merge, entries);\n+\tri->merge_renames = get_renames(o, merge_pairs,\n+\t\t\t\t\tdir_re_head, merge,\n \t\t\t\t\t common, head, merge, entries);\n \tclean = process_renames(o, ri->head_renames, ri->merge_renames);\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333743","messageId":"20171129014237.32570-34-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 33/34] merge-recursive: avoid spurious rename/rename conflict from dir renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:36Z","receivedAt":"2017-11-29T01:44:15Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"If a file on one side of history was renamed, and merely modified on the\nother side, then applying a directory rename to the modified side gives us\na rename/rename(1to2) conflict.  We should only apply directory renames to\npairs representing either adds or renames.\n\nMaking this change means that a directory rename testcase that was\npreviously reported as a rename/delete conflict will now be reported as a\nmodify/delete conflict.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   |  4 +--\n t/t6043-merge-rename-directories.sh | 55 +++++++++++++++++--------------------\n 2 files changed, 27 insertions(+), 32 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex fe42cabad0..d00786f719 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1951,7 +1951,7 @@ static void compute_collisions(struct hashmap *collisions,\n \t\tchar *new_path;\n \t\tstruct diff_filepair *pair = pairs->queue[i];\n \n-\t\tif (pair->status == 'D')\n+\t\tif (pair->status != 'A' && pair->status != 'R')\n \t\t\tcontinue;\n \t\tdir_rename_ent = check_dir_renamed(pair->two->path,\n \t\t\t\t\t\t   dir_renames);\n@@ -2178,7 +2178,7 @@ static struct string_list *get_renames(struct merge_options *o,\n \t\tstruct diff_filepair *pair = pairs->queue[i];\n \t\tchar *new_path; /* non-NULL only with directory renames */\n \n-\t\tif (pair->status == 'D') {\n+\t\tif (pair->status != 'A' && pair->status != 'R') {\n \t\t\tdiff_free_filepair(pair);\n \t\t\tcontinue;\n \t\t}\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 5a750d5511..8ed85c79de 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -2000,18 +2000,23 @@ test_expect_success '8b-check: Dual-directory rename, one into the others way, w\n \t)\n '\n \n-# Testcase 8c, rename+modify/delete\n-#   (Related to testcases 5b and 8d)\n+# Testcase 8c, modify/delete or rename+modify/delete?\n+#   (Related to testcases 5b, 8d, and 9h)\n #   Commit O: z/{b,c,d}\n #   Commit A: y/{b,c}\n #   Commit B: z/{b,c,d_modified,e}\n-#   Expected: y/{b,c,e}, CONFLICT(rename+modify/delete: x/d -> y/d or deleted)\n+#   Expected: y/{b,c,e}, CONFLICT(modify/delete: on z/d)\n #\n-#   Note: This testcase doesn't present any concerns for me...until you\n-#         compare it with testcases 5b and 8d.  See notes in 8d for more\n-#         details.\n-\n-test_expect_success '8c-setup: rename+modify/delete' '\n+#   Note: It could easily be argued that the correct resolution here is\n+#         y/{b,c,e}, CONFLICT(rename/delete: z/d -> y/d vs deleted)\n+#         and that the modifed version of d should be present in y/ after\n+#         the merge, just marked as conflicted.  Indeed, I previously did\n+#         argue that.  But applying directory renames to the side of\n+#         history where a file is merely modified results in spurious\n+#         rename/rename(1to2) conflicts -- see testcase 9h.  See also\n+#         notes in 8d.\n+\n+test_expect_success '8c-setup: modify/delete or rename+modify/delete?' '\n \ttest_create_repo 8c &&\n \t(\n \t\tcd 8c &&\n@@ -2044,29 +2049,29 @@ test_expect_success '8c-setup: rename+modify/delete' '\n \t)\n '\n \n-test_expect_success '8c-check: rename+modify/delete' '\n+test_expect_success '8c-check: modify/delete or rename+modify/delete' '\n \t(\n \t\tcd 8c &&\n \n \t\tgit checkout A^0 &&\n \n \t\ttest_must_fail git merge -s recursive B^0 >out &&\n-\t\ttest_i18ngrep \"CONFLICT (rename/delete).* z/d.*y/d\" out &&\n+\t\ttest_i18ngrep \"CONFLICT (modify/delete).* z/d\" out &&\n \n-\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n-\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 5 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n \t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n \n \t\tgit rev-parse >actual \\\n-\t\t\t:0:y/b :0:y/c :0:y/e :3:y/d &&\n+\t\t\t:0:y/b :0:y/c :0:y/e :1:z/d :3:z/d &&\n \t\tgit rev-parse >expect \\\n-\t\t\tO:z/b O:z/c B:z/e B:z/d &&\n+\t\t\tO:z/b O:z/c B:z/e O:z/d B:z/d &&\n \t\ttest_cmp expect actual &&\n \n-\t\ttest_must_fail git rev-parse :1:y/d &&\n-\t\ttest_must_fail git rev-parse :2:y/d &&\n-\t\tgit ls-files -s y/d | grep ^100755 &&\n-\t\ttest -f y/d\n+\t\ttest_must_fail git rev-parse :2:z/d &&\n+\t\tgit ls-files -s z/d | grep ^100755 &&\n+\t\ttest -f z/d &&\n+\t\t! test -f y/d\n \t)\n '\n \n@@ -2080,16 +2085,6 @@ test_expect_success '8c-check: rename+modify/delete' '\n #\n #   Note: It would also be somewhat reasonable to resolve this as\n #             y/{b,c,e}, CONFLICT(rename/delete: x/d -> y/d or deleted)\n-#   The logic being that the only difference between this testcase and 8c\n-#   is that there is no modification to d.  That suggests that instead of a\n-#   rename/modify vs. delete conflict, we should just have a rename/delete\n-#   conflict, otherwise we are being inconsistent.\n-#\n-#   However...as far as consistency goes, we didn't report a conflict for\n-#   path d_1 in testcase 5b due to a different file being in the way.  So,\n-#   we seem to be forced to have cases where users can change things\n-#   slightly and get what they may perceive as inconsistent results.  It\n-#   would be nice to avoid that, but I'm not sure I see how.\n #\n #   In this case, I'm leaning towards: commit A was the one that deleted z/d\n #   and it did the rename of z to y, so the two \"conflicts\" (rename vs.\n@@ -2804,7 +2799,7 @@ test_expect_success '9h-setup: Avoid dir rename on merely modified path' '\n \t)\n '\n \n-test_expect_failure '9h-check: Avoid dir rename on merely modified path' '\n+test_expect_success '9h-check: Avoid dir rename on merely modified path' '\n \t(\n \t\tcd 9h &&\n \n@@ -3785,7 +3780,7 @@ test_expect_success '12c-setup: Moving one directory hierarchy into another w/ c\n \t)\n '\n \n-test_expect_failure '12c-check: Moving one directory hierarchy into another w/ content merge' '\n+test_expect_success '12c-check: Moving one directory hierarchy into another w/ content merge' '\n \t(\n \t\tcd 12c &&\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333744","messageId":"20171129014237.32570-28-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 27/34] merge-recursive: when comparing files, don't include trees","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:30Z","receivedAt":"2017-11-29T01:44:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"get_renames() would look up stage data that already existed (populated\nin get_unmerged(), taken from whatever unpack_trees() created), and if\nit didn't exist, would call insert_stage_data() to create the necessary\nentry for the given file.  The insert_stage_data() fallback becomes\nmuch more important for directory rename detection, because that creates\na mechanism to have a file in the resulting merge that didn't exist on\neither side of history.  However, insert_stage_data(), due to calling\nget_tree_entry() loaded up trees as readily as files.  We aren't\ninterested in comparing trees to files; the D/F conflict handling is\ndone elsewhere.  This code is just concerned with what entries existed\nfor a given path on the different sides of the merge, so create a\nget_tree_entry_if_blob() helper function and use it.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 27 +++++++++++++++++++++------\n 1 file changed, 21 insertions(+), 6 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 78f707d0d7..01934bc1e2 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -418,6 +418,21 @@ static void get_files_dirs(struct merge_options *o, struct tree *tree)\n \tread_tree_recursive(tree, \"\", 0, 0, &match_all, save_files_dirs, o);\n }\n \n+static int get_tree_entry_if_blob(const unsigned char *tree,\n+\t\t\t\t  const char *path,\n+\t\t\t\t  unsigned char *hashy,\n+\t\t\t\t  unsigned int *mode_o)\n+{\n+\tint ret;\n+\n+\tret = get_tree_entry(tree, path, hashy, mode_o);\n+\tif (S_ISDIR(*mode_o)) {\n+\t\thashcpy(hashy, null_sha1);\n+\t\t*mode_o = 0;\n+\t}\n+\treturn ret;\n+}\n+\n /*\n  * Returns an index_entry instance which doesn't have to correspond to\n  * a real cache entry in Git's index.\n@@ -428,12 +443,12 @@ static struct stage_data *insert_stage_data(const char *path,\n {\n \tstruct string_list_item *item;\n \tstruct stage_data *e = xcalloc(1, sizeof(struct stage_data));\n-\tget_tree_entry(o->object.oid.hash, path,\n-\t\t\te->stages[1].oid.hash, &e->stages[1].mode);\n-\tget_tree_entry(a->object.oid.hash, path,\n-\t\t\te->stages[2].oid.hash, &e->stages[2].mode);\n-\tget_tree_entry(b->object.oid.hash, path,\n-\t\t\te->stages[3].oid.hash, &e->stages[3].mode);\n+\tget_tree_entry_if_blob(o->object.oid.hash, path,\n+\t\t\t       e->stages[1].oid.hash, &e->stages[1].mode);\n+\tget_tree_entry_if_blob(a->object.oid.hash, path,\n+\t\t\t       e->stages[2].oid.hash, &e->stages[2].mode);\n+\tget_tree_entry_if_blob(b->object.oid.hash, path,\n+\t\t\t       e->stages[3].oid.hash, &e->stages[3].mode);\n \titem = string_list_insert(entries, path);\n \titem->util = e;\n \treturn e;\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333745","messageId":"20171129014237.32570-33-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 32/34] directory rename detection: new testcases showcasing a pair of bugs","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:35Z","receivedAt":"2017-11-29T01:44:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Add a testcase showing spurious rename/rename(1to2) conflicts occurring\ndue to directory rename detection.\n\nAlso add a pair of testcases dealing with moving directory hierarchies\naround that were suggested by Stefan Beller as \"food for thought\" during\nhis review of an earlier patch series, but which actually uncovered a\nbug.  Round things out with a test that is a cross between the two\ntestcases that showed existing bugs in order to make sure we aren't\nmerely addressing problems in isolation but in general.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 284 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 284 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex f89cce4377..5a750d5511 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -153,6 +153,7 @@ test_expect_success '1b-check: Merge a directory with another' '\n # Testcase 1c, Transitive renaming\n #   (Related to testcases 3a and 6d -- when should a transitive rename apply?)\n #   (Related to testcases 9c and 9d -- can transitivity repeat?)\n+#   (Related to testcase 12b -- joint-transitivity?)\n #   Commit O: z/{b,c},   x/d\n #   Commit A: y/{b,c},   x/d\n #   Commit B: z/{b,c,d}\n@@ -2760,6 +2761,67 @@ test_expect_failure '9g-check: Renamed directory that only contained immediate s\n \t)\n '\n \n+# Testcase 9h, Avoid implicit rename if involved as source on other side\n+#   (Extremely closely related to testcase 3a)\n+#   Commit O: z/{b,c,d_1}\n+#   Commit A: z/{b,c,d_2}\n+#   Commit B: y/{b,c}, x/d_1\n+#   Expected: y/{b,c}, x/d_2\n+#   NOTE: If we applied the z/ -> y/ rename to z/d, then we'd end up with\n+#         a rename/rename(1to2) conflict (z/d -> y/d vs. x/d)\n+test_expect_success '9h-setup: Avoid dir rename on merely modified path' '\n+\ttest_create_repo 9h &&\n+\t(\n+\t\tcd 9h &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tprintf \"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\nd\\n\" >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\ttest_tick &&\n+\t\techo more >>z/d &&\n+\t\tgit add z/d &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir y &&\n+\t\tmkdir x &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\tgit mv z/d x/ &&\n+\t\trmdir z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '9h-check: Avoid dir rename on merely modified path' '\n+\t(\n+\t\tcd 9h &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:x/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c A:z/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n ###########################################################################\n # Rules suggested by section 9:\n #\n@@ -3541,4 +3603,226 @@ test_expect_success '11f-check: Avoid deleting not-uptodate with dir rename/rena\n \t)\n '\n \n+###########################################################################\n+# SECTION 12: Everything else\n+#\n+# Tests suggested by others.  Tests added after implementation completed\n+# and submitted.  Grab bag.\n+###########################################################################\n+\n+# Testcase 12a, Moving one directory hierarchy into another\n+#   (Related to testcase 9a)\n+#   Commit O: node1/{leaf1,leaf2}, node2/{leaf3,leaf4}\n+#   Commit A: node1/{leaf1,leaf2,node2/{leaf3,leaf4}}\n+#   Commit B: node1/{leaf1,leaf2,leaf5}, node2/{leaf3,leaf4,leaf6}\n+#   Expected: node1/{leaf1,leaf2,leaf5,node2/{leaf3,leaf4,leaf6}}\n+\n+test_expect_success '12a-setup: Moving one directory hierarchy into another' '\n+\ttest_create_repo 12a &&\n+\t(\n+\t\tcd 12a &&\n+\n+\t\tmkdir -p node1 node2 &&\n+\t\techo leaf1 >node1/leaf1 &&\n+\t\techo leaf2 >node1/leaf2 &&\n+\t\techo leaf3 >node2/leaf3 &&\n+\t\techo leaf4 >node2/leaf4 &&\n+\t\tgit add node1 node2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv node2/ node1/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo leaf5 >node1/leaf5 &&\n+\t\techo leaf6 >node2/leaf6 &&\n+\t\tgit add node1 node2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '12a-check: Moving one directory hierarchy into another' '\n+\t(\n+\t\tcd 12a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:node1/leaf1 HEAD:node1/leaf2 HEAD:node1/leaf5 HEAD:node1/node2/leaf3 HEAD:node1/node2/leaf4 HEAD:node1/node2/leaf6 &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:node1/leaf1 O:node1/leaf2 B:node1/leaf5 O:node2/leaf3 O:node2/leaf4 B:node2/leaf6 &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 12b, Moving two directory hierarchies into each other\n+#   (Related to testcases 1c and 12c)\n+#   Commit O: node1/{leaf1, leaf2}, node2/{leaf3, leaf4}\n+#   Commit A: node1/{leaf1, leaf2, node2/{leaf3, leaf4}}\n+#   Commit B: node2/{leaf3, leaf4, node1/{leaf1, leaf2}}\n+#   Expected: node1/node2/node1/{leaf1, leaf2},\n+#             node2/node1/node2/{leaf3, leaf4}\n+#   NOTE: Without directory renames, we would expect\n+#                   node2/node1/{leaf1, leaf2},\n+#                   node1/node2/{leaf3, leaf4}\n+#         with directory rename detection, we note that\n+#             commit A renames node2/ -> node1/node2/\n+#             commit B renames node1/ -> node2/node1/\n+#         therefore, applying those directory renames to the initial result\n+#         (making all four paths experience a transitive renaming), yields\n+#         the expected result.\n+#\n+#         You may ask, is it weird to have two directories rename each other?\n+#         To which, I can do no more than shrug my shoulders and say that\n+#         even simple rules give weird results when given weird inputs.\n+\n+test_expect_success '12b-setup: Moving one directory hierarchy into another' '\n+\ttest_create_repo 12b &&\n+\t(\n+\t\tcd 12b &&\n+\n+\t\tmkdir -p node1 node2 &&\n+\t\techo leaf1 >node1/leaf1 &&\n+\t\techo leaf2 >node1/leaf2 &&\n+\t\techo leaf3 >node2/leaf3 &&\n+\t\techo leaf4 >node2/leaf4 &&\n+\t\tgit add node1 node2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv node2/ node1/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv node1/ node2/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '12b-check: Moving one directory hierarchy into another' '\n+\t(\n+\t\tcd 12b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:node1/node2/node1/leaf1 \\\n+\t\t\tHEAD:node1/node2/node1/leaf2 \\\n+\t\t\tHEAD:node2/node1/node2/leaf3 \\\n+\t\t\tHEAD:node2/node1/node2/leaf4 &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:node1/leaf1 O:node1/leaf2 \\\n+\t\t\tO:node2/leaf3 O:node2/leaf4 &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 12c, Moving two directory hierarchies into each other w/ content merge\n+#   (Related to testcase 12b)\n+#   Commit O: node1/{       leaf1_1, leaf2_1}, node2/{leaf3_1, leaf4_1}\n+#   Commit A: node1/{       leaf1_2, leaf2_2,  node2/{leaf3_2, leaf4_2}}\n+#   Commit B: node2/{node1/{leaf1_3, leaf2_3},        leaf3_3, leaf4_3}\n+#   Expected: Content merge conflicts for each of:\n+#               node1/node2/node1/{leaf1, leaf2},\n+#               node2/node1/node2/{leaf3, leaf4}\n+#   NOTE: This is *exactly* like 12c, except that every path is modified on\n+#         each side of the merge.\n+\n+test_expect_success '12c-setup: Moving one directory hierarchy into another w/ content merge' '\n+\ttest_create_repo 12c &&\n+\t(\n+\t\tcd 12c &&\n+\n+\t\tmkdir -p node1 node2 &&\n+\t\tprintf \"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\nleaf1\\n\" >node1/leaf1 &&\n+\t\tprintf \"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\nleaf2\\n\" >node1/leaf2 &&\n+\t\tprintf \"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\nleaf3\\n\" >node2/leaf3 &&\n+\t\tprintf \"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\nleaf4\\n\" >node2/leaf4 &&\n+\t\tgit add node1 node2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv node2/ node1/ &&\n+\t\tfor i in `git ls-files`; do echo side A >>$i; done &&\n+\t\tgit add -u &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv node1/ node2/ &&\n+\t\tfor i in `git ls-files`; do echo side B >>$i; done &&\n+\t\tgit add -u &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '12c-check: Moving one directory hierarchy into another w/ content merge' '\n+\t(\n+\t\tcd 12c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 &&\n+\n+\t\ttest 12 -eq $(git ls-files -u | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:1:node1/node2/node1/leaf1 \\\n+\t\t\t:1:node1/node2/node1/leaf2 \\\n+\t\t\t:1:node2/node1/node2/leaf3 \\\n+\t\t\t:1:node2/node1/node2/leaf4 \\\n+\t\t\t:2:node1/node2/node1/leaf1 \\\n+\t\t\t:2:node1/node2/node1/leaf2 \\\n+\t\t\t:2:node2/node1/node2/leaf3 \\\n+\t\t\t:2:node2/node1/node2/leaf4 \\\n+\t\t\t:3:node1/node2/node1/leaf1 \\\n+\t\t\t:3:node1/node2/node1/leaf2 \\\n+\t\t\t:3:node2/node1/node2/leaf3 \\\n+\t\t\t:3:node2/node1/node2/leaf4 &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:node1/leaf1 \\\n+\t\t\tO:node1/leaf2 \\\n+\t\t\tO:node2/leaf3 \\\n+\t\t\tO:node2/leaf4 \\\n+\t\t\tA:node1/leaf1 \\\n+\t\t\tA:node1/leaf2 \\\n+\t\t\tA:node1/node2/leaf3 \\\n+\t\t\tA:node1/node2/leaf4 \\\n+\t\t\tB:node2/node1/leaf1 \\\n+\t\t\tB:node2/node1/leaf2 \\\n+\t\t\tB:node2/leaf3 \\\n+\t\t\tB:node2/leaf4 &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333746","messageId":"20171129014237.32570-22-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 21/34] merge-recursive: make a helper function for cleanup for handle_renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:24Z","receivedAt":"2017-11-29T01:44:27Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"In anticipation of more involved cleanup to come, make a helper function\nfor doing the cleanup at the end of handle_renames.  Rename the already\nexisting cleanup_rename[s]() to final_cleanup_rename[s](), name the new\nhelper initial_cleanup_rename(), and leave the big comment in the code\nabout why we can't do all the cleanup at once.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 23 +++++++++++++----------\n 1 file changed, 13 insertions(+), 10 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 0cb27c66e2..c5932d5c57 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1695,6 +1695,12 @@ struct rename_info {\n \tstruct string_list *merge_renames;\n };\n \n+static void initial_cleanup_rename(struct diff_queue_struct *pairs)\n+{\n+\tfree(pairs->queue);\n+\tfree(pairs);\n+}\n+\n static int handle_renames(struct merge_options *o,\n \t\t\t  struct tree *common,\n \t\t\t  struct tree *head,\n@@ -1725,16 +1731,13 @@ static int handle_renames(struct merge_options *o,\n \t * data structures are still needed and referenced in\n \t * process_entry().  But there are a few things we can free now.\n \t */\n-\n-\tfree(head_pairs->queue);\n-\tfree(head_pairs);\n-\tfree(merge_pairs->queue);\n-\tfree(merge_pairs);\n+\tinitial_cleanup_rename(head_pairs);\n+\tinitial_cleanup_rename(merge_pairs);\n \n \treturn clean;\n }\n \n-static void cleanup_rename(struct string_list *rename)\n+static void final_cleanup_rename(struct string_list *rename)\n {\n \tconst struct rename *re;\n \tint i;\n@@ -1750,10 +1753,10 @@ static void cleanup_rename(struct string_list *rename)\n \tfree(rename);\n }\n \n-static void cleanup_renames(struct rename_info *re_info)\n+static void final_cleanup_renames(struct rename_info *re_info)\n {\n-\tcleanup_rename(re_info->head_renames);\n-\tcleanup_rename(re_info->merge_renames);\n+\tfinal_cleanup_rename(re_info->head_renames);\n+\tfinal_cleanup_rename(re_info->merge_renames);\n }\n \n static struct object_id *stage_oid(const struct object_id *oid, unsigned mode)\n@@ -2149,7 +2152,7 @@ int merge_trees(struct merge_options *o,\n \t\t}\n \n cleanup:\n-\t\tcleanup_renames(&re_info);\n+\t\tfinal_cleanup_renames(&re_info);\n \n \t\tstring_list_clear(entries, 1);\n \t\tfree(entries);\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333747","messageId":"20171129014237.32570-11-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 10/34] directory rename detection: more involved edge/corner testcases","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:13Z","receivedAt":"2017-11-29T01:44:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 381 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 381 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 5db2986de8..2c57a02c6d 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -1459,4 +1459,385 @@ test_expect_success '6e-check: Add/add from one side' '\n #   side of history is the one doing the renaming.\n ###########################################################################\n \n+\n+###########################################################################\n+# SECTION 7: More involved Edge/Corner cases\n+#\n+# The ruleset we have generated in the above sections seems to provide\n+# well-defined merges.  But can we find edge/corner cases that either (a)\n+# are harder for users to understand, or (b) have a resolution that is\n+# non-intuitive or suboptimal?\n+#\n+# The testcases in this section dive into cases that I've tried to craft in\n+# a way to find some that might be surprising to users or difficult for\n+# them to understand (the next section will look at non-intuitive or\n+# suboptimal merge results).  Some of the testcases are similar to ones\n+# from past sections, but have been simplified to try to highlight error\n+# messages using a \"modified\" path (due to the directory rename).  Are\n+# users okay with these?\n+#\n+# In my opinion, testcases that are difficult to understand from this\n+# section is due to difficulty in the testcase rather than the directory\n+# renaming (similar to how t6042 and t6036 have difficult resolutions due\n+# to the problem setup itself being complex).  And I don't think the\n+# error messages are a problem.\n+#\n+# On the other hand, the testcases in section 8 worry me slightly more...\n+###########################################################################\n+\n+# Testcase 7a, rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file\n+#   Commit O: z/{b,c}\n+#   Commit A: y/{b,c}\n+#   Commit B: w/b, x/c, z/d\n+#   Expected: y/d, CONFLICT(rename/rename for both z/b and z/c)\n+#   NOTE: There's a rename of z/ here, y/ has more renames, so z/d -> y/d.\n+\n+test_expect_success '7a-setup: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '\n+\ttest_create_repo 7a &&\n+\t(\n+\t\tcd 7a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir w &&\n+\t\tmkdir x &&\n+\t\tgit mv z/b w/ &&\n+\t\tgit mv z/c x/ &&\n+\t\techo d > z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '7a-check: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '\n+\t(\n+\t\tcd 7a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename).*z/b.*y/b.*w/b\" out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename).*z/c.*y/c.*x/c\" out &&\n+\n+\t\ttest 7 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 6 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\ttest $(git rev-parse :0:y/d) = $(git rev-parse B:z/d) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:x/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/b O:z/b O:z/c O:z/c O:z/c &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit hash-object >actual \\\n+\t\t\ty/b w/b y/c x/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/b O:z/c O:z/c &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 7b, rename/rename(2to1), but only due to transitive rename\n+#   (Related to testcase 1d)\n+#   Commit O: z/{b,c},     x/d_1, w/d_2\n+#   Commit A: y/{b,c,d_2}, x/d_1\n+#   Commit B: z/{b,c,d_1},        w/d_2\n+#   Expected: y/{b,c}, CONFLICT(rename/rename(2to1): x/d_1, w/d_2 -> y_d)\n+\n+test_expect_success '7b-setup: rename/rename(2to1), but only due to transitive rename' '\n+\ttest_create_repo 7b &&\n+\t(\n+\t\tcd 7b &&\n+\n+\t\tmkdir z &&\n+\t\tmkdir x &&\n+\t\tmkdir w &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d1 > x/d &&\n+\t\techo d2 > w/d &&\n+\t\tgit add z x w &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\tgit mv w/d y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/ &&\n+\t\trmdir x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '7b-check: rename/rename(2to1), but only due to transitive rename' '\n+\t(\n+\t\tcd 7b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename)\" out &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :2:y/d :3:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:w/d O:x/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest ! -f y/d &&\n+\t\ttest -f y/d~HEAD &&\n+\t\ttest -f y/d~B^0 &&\n+\n+\t\tgit hash-object >actual \\\n+\t\t\ty/d~HEAD y/d~B^0 &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:w/d O:x/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 7c, rename/rename(1to...2or3); transitive rename may add complexity\n+#   (Related to testcases 3b and 5c)\n+#   Commit O: z/{b,c}, x/d\n+#   Commit A: y/{b,c}, w/d\n+#   Commit B: z/{b,c,d}\n+#   Expected: y/{b,c}, CONFLICT(x/d -> w/d vs. y/d)\n+#   NOTE: z/ was renamed to y/ so we do want to report\n+#         neither CONFLICT(x/d -> w/d vs. z/d)\n+#         nor CONFLiCT x/d -> w/d vs. y/d vs. z/d)\n+\n+test_expect_success '7c-setup: rename/rename(1to...2or3); transitive rename may add complexity' '\n+\ttest_create_repo 7c &&\n+\t(\n+\t\tcd 7c &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\tgit mv x w &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/ &&\n+\t\trmdir x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '7c-check: rename/rename(1to...2or3); transitive rename may add complexity' '\n+\t(\n+\t\tcd 7c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename).*x/d.*w/d.*y/d\" out &&\n+\n+\t\ttest 5 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :1:x/d :2:w/d :3:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:x/d O:x/d O:x/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 7d, transitive rename involved in rename/delete; how is it reported?\n+#   (Related somewhat to testcases 5b and 8d)\n+#   Commit O: z/{b,c}, x/d\n+#   Commit A: y/{b,c}\n+#   Commit B: z/{b,c,d}\n+#   Expected: y/{b,c}, CONFLICT(delete x/d vs rename to y/d)\n+#   NOTE: z->y so NOT CONFLICT(delete x/d vs rename to z/d)\n+\n+test_expect_success '7d-setup: transitive rename involved in rename/delete; how is it reported?' '\n+\ttest_create_repo 7d &&\n+\t(\n+\t\tcd 7d &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\tgit rm -rf x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/ &&\n+\t\trmdir x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '7d-check: transitive rename involved in rename/delete; how is it reported?' '\n+\t(\n+\t\tcd 7d &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/delete).*x/d.*y/d\" out &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :3:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:x/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 7e, transitive rename in rename/delete AND dirs in the way\n+#   (Very similar to 'both rename source and destination involved in D/F conflict' from t6022-merge-rename.sh)\n+#   (Also related to testcases 9c and 9d)\n+#   Commit O: z/{b,c},     x/d_1\n+#   Commit A: y/{b,c,d/g}, x/d/f\n+#   Commit B: z/{b,c,d_1}\n+#   Expected: rename/delete(x/d_1->y/d_1 vs. None) + D/F conflict on y/d\n+#             y/{b,c,d/g}, y/d_1~B^0, x/d/f\n+\n+#   NOTE: The main path of interest here is d_1 and where it ends up, but\n+#         this is actually a case that has two potential directory renames\n+#         involved and D/F conflict(s), so it makes sense to walk through\n+#         each step.\n+#\n+#         Commit A renames z/ -> y/.  Thus everything that B adds to z/\n+#         should be instead moved to y/.  This gives us the D/F conflict on\n+#         y/d because x/d_1 -> z/d_1 -> y/d_1 conflicts with y/d/g.\n+#\n+#         Further, commit B renames x/ -> z/, thus everything A adds to x/\n+#         should instead be moved to z/...BUT we removed z/ and renamed it\n+#         to y/, so maybe everything should move not from x/ to z/, but\n+#         from x/ to z/ to y/.  Doing so might make sense from the logic so\n+#         far, but note that commit A had both an x/ and a y/; it did the\n+#         renaming of z/ to y/ and created x/d/f and it clearly made these\n+#         things separate, so it doesn't make much sense to push these\n+#         together.  Doing so is what I'd call a doubly transitive rename;\n+#         see testcases 9c and 9d for further discussion of this issue and\n+#         how it's resolved.\n+\n+test_expect_success '7e-setup: transitive rename in rename/delete AND dirs in the way' '\n+\ttest_create_repo 7e &&\n+\t(\n+\t\tcd 7e &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d1 >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\tgit rm x/d &&\n+\t\tmkdir -p x/d &&\n+\t\tmkdir -p y/d &&\n+\t\techo f >x/d/f &&\n+\t\techo g >y/d/g &&\n+\t\tgit add x/d/f y/d/g &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/ &&\n+\t\trmdir x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '7e-check: transitive rename in rename/delete AND dirs in the way' '\n+\t(\n+\t\tcd 7e &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/delete).*x/d.*y/d\" out &&\n+\n+\t\ttest 5 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:x/d/f :0:y/d/g :0:y/b :0:y/c :3:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tA:x/d/f A:y/d/g O:z/b O:z/c O:x/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest $(git hash-object y/d~B^0) = $(git rev-parse O:x/d)\n+\t)\n+'\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333748","messageId":"20171129014237.32570-21-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 20/34] merge-recursive: add a new hashmap for storing directory renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:23Z","receivedAt":"2017-11-29T01:44:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"This just adds dir_rename_entry and the associated functions; code using\nthese will be added in subsequent commits.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c | 35 +++++++++++++++++++++++++++++++++++\n merge-recursive.h |  8 ++++++++\n 2 files changed, 43 insertions(+)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex 4adff2d538..0cb27c66e2 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -49,6 +49,41 @@ static unsigned int path_hash(const char *path)\n \treturn ignore_case ? strihash(path) : strhash(path);\n }\n \n+static struct dir_rename_entry *dir_rename_find_entry(struct hashmap *hashmap,\n+\t\t\t\t\t\t      char *dir)\n+{\n+\tstruct dir_rename_entry key;\n+\n+\tif (dir == NULL)\n+\t\treturn NULL;\n+\thashmap_entry_init(&key, strhash(dir));\n+\tkey.dir = dir;\n+\treturn hashmap_get(hashmap, &key, NULL);\n+}\n+\n+static int dir_rename_cmp(void *unused_cmp_data,\n+\t\t\t  const struct dir_rename_entry *e1,\n+\t\t\t  const struct dir_rename_entry *e2,\n+\t\t\t  const void *unused_keydata)\n+{\n+\treturn strcmp(e1->dir, e2->dir);\n+}\n+\n+static void dir_rename_init(struct hashmap *map)\n+{\n+\thashmap_init(map, (hashmap_cmp_fn) dir_rename_cmp, NULL, 0);\n+}\n+\n+static void dir_rename_entry_init(struct dir_rename_entry *entry,\n+\t\t\t\t  char *directory)\n+{\n+\thashmap_entry_init(entry, strhash(directory));\n+\tentry->dir = directory;\n+\tentry->non_unique_new_dir = 0;\n+\tstrbuf_init(&entry->new_dir, 0);\n+\tstring_list_init(&entry->possible_new_dirs, 0);\n+}\n+\n static void flush_output(struct merge_options *o)\n {\n \tif (o->buffer_output < 2 && o->obuf.len) {\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex 80d69d1401..d7f4cc80c1 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -29,6 +29,14 @@ struct merge_options {\n \tstruct string_list df_conflict_file_set;\n };\n \n+struct dir_rename_entry {\n+\tstruct hashmap_entry ent; /* must be the first member! */\n+\tchar *dir;\n+\tunsigned non_unique_new_dir:1;\n+\tstruct strbuf new_dir;\n+\tstruct string_list possible_new_dirs;\n+};\n+\n /* merge_trees() but with recursive ancestor consolidation */\n int merge_recursive(struct merge_options *o,\n \t\t    struct commit *h1,\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333749","messageId":"20171129014237.32570-10-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 09/34] directory rename detection: testcases checking which side did the rename","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:12Z","receivedAt":"2017-11-29T01:44:34Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 321 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 321 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 29b2af7f19..5db2986de8 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -1138,4 +1138,325 @@ test_expect_failure '5d-check: Directory/file/file conflict due to directory ren\n #   back to old handling.  But, sadly, see testcases 8a and 8b.\n ###########################################################################\n \n+\n+###########################################################################\n+# SECTION 6: Same side of the merge was the one that did the rename\n+#\n+# It may sound obvious that you only want to apply implicit directory\n+# renames to directories if the _other_ side of history did the renaming.\n+# If you did make an implementation that didn't explicitly enforce this\n+# rule, the majority of cases that would fall under this section would\n+# also be solved by following the rules from the above sections.  But\n+# there are still a few that stick out, so this section covers them just\n+# to make sure we also get them right.\n+###########################################################################\n+\n+# Testcase 6a, Tricky rename/delete\n+#   Commit O: z/{b,c,d}\n+#   Commit A: z/b\n+#   Commit B: y/{b,c}, z/d\n+#   Expected: y/b, CONFLICT(rename/delete, z/c -> y/c vs. NULL)\n+#   Note: We're just checking here that the rename of z/b and z/c to put\n+#         them under y/ doesn't accidentally catch z/d and make it look like\n+#         it is also involved in a rename/delete conflict.\n+\n+test_expect_success '6a-setup: Tricky rename/delete' '\n+\ttest_create_repo 6a &&\n+\t(\n+\t\tcd 6a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit rm z/c &&\n+\t\tgit rm z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir y &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '6a-check: Tricky rename/delete' '\n+\t(\n+\t\tcd 6a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/delete).*z/c.*y/c\" out &&\n+\n+\t\ttest 2 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :3:y/c &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 6b, Same rename done on both sides\n+#   (Related to testcases 6c and 8e)\n+#   Commit O: z/{b,c}\n+#   Commit A: y/{b,c}\n+#   Commit B: y/{b,c}, z/d\n+#   Expected: y/{b,c}, z/d\n+#   Note: If we did directory rename detection here, we'd move z/d into y/,\n+#         but B did that rename and still decided to put the file into z/,\n+#         so we probably shouldn't apply directory rename detection for it.\n+\n+test_expect_success '6b-setup: Same rename done on both sides' '\n+\ttest_create_repo 6b &&\n+\t(\n+\t\tcd 6b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z y &&\n+\t\tmkdir z &&\n+\t\techo d >z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '6b-check: Same rename done on both sides' '\n+\t(\n+\t\tcd 6b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 6c, Rename only done on same side\n+#   (Related to testcases 6b and 8e)\n+#   Commit O: z/{b,c}\n+#   Commit A: z/{b,c} (no change)\n+#   Commit B: y/{b,c}, z/d\n+#   Expected: y/{b,c}, z/d\n+#   NOTE: Seems obvious, but just checking that the implementation doesn't\n+#         \"accidentally detect a rename\" and give us y/{b,c,d}.\n+\n+test_expect_success '6c-setup: Rename only done on same side' '\n+\ttest_create_repo 6c &&\n+\t(\n+\t\tcd 6c &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\ttest_tick &&\n+\t\tgit commit --allow-empty -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z y &&\n+\t\tmkdir z &&\n+\t\techo d >z/d &&\n+\t\tgit add z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '6c-check: Rename only done on same side' '\n+\t(\n+\t\tcd 6c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 6d, We don't always want transitive renaming\n+#   (Related to testcase 1c)\n+#   Commit O: z/{b,c}, x/d\n+#   Commit A: z/{b,c}, x/d (no change)\n+#   Commit B: y/{b,c}, z/d\n+#   Expected: y/{b,c}, z/d\n+#   NOTE: Again, this seems obvious but just checking that the implementation\n+#         doesn't \"accidentally detect a rename\" and give us y/{b,c,d}.\n+\n+test_expect_success '6d-setup: We do not always want transitive renaming' '\n+\ttest_create_repo 6d &&\n+\t(\n+\t\tcd 6d &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\ttest_tick &&\n+\t\tgit commit --allow-empty -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z y &&\n+\t\tgit mv x z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '6d-check: We do not always want transitive renaming' '\n+\t(\n+\t\tcd 6d &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:x/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# Testcase 6e, Add/add from one-side\n+#   Commit O: z/{b,c}\n+#   Commit A: z/{b,c} (no change)\n+#   Commit B: y/{b,c,d_1}, z/d_2\n+#   Expected: y/{b,c,d_1}, z/d_2\n+#   NOTE: Again, this seems obvious but just checking that the implementation\n+#         doesn't \"accidentally detect a rename\" and give us y/{b,c} +\n+#         add/add conflict on y/d_1 vs y/d_2.\n+\n+test_expect_success '6e-setup: Add/add from one side' '\n+\ttest_create_repo 6e &&\n+\t(\n+\t\tcd 6e &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\ttest_tick &&\n+\t\tgit commit --allow-empty -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z y &&\n+\t\techo d1 > y/d &&\n+\t\tmkdir z &&\n+\t\techo d2 > z/d &&\n+\t\tgit add y/d z/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '6e-check: Add/add from one side' '\n+\t(\n+\t\tcd 6e &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 4 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d HEAD:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:y/d B:z/d &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by section 6:\n+#\n+#   Only apply implicit directory renames to directories if the other\n+#   side of history is the one doing the renaming.\n+###########################################################################\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333750","messageId":"20171129014237.32570-31-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 30/34] merge-recursive: fix overwriting dirty files involved in renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:33Z","receivedAt":"2017-11-29T01:44:38Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"This fixes an issue that existed before my directory rename detection\npatches that affects both normal renames and renames implied by\ndirectory rename detection.  Additional codepaths that only affect\noverwriting of directy files that are involved in directory rename\ndetection will be added in a subsequent commit.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   | 85 ++++++++++++++++++++++++++++---------\n merge-recursive.h                   |  2 +\n t/t3501-revert-cherry-pick.sh       |  2 +-\n t/t6043-merge-rename-directories.sh |  2 +-\n t/t7607-merge-overwrite.sh          |  2 +-\n unpack-trees.c                      |  4 +-\n unpack-trees.h                      |  4 ++\n 7 files changed, 77 insertions(+), 24 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex e77d2b043d..2b8a5ca03b 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -334,32 +334,37 @@ static void init_tree_desc_from_tree(struct tree_desc *desc, struct tree *tree)\n \tinit_tree_desc(desc, tree->buffer, tree->size);\n }\n \n-static int git_merge_trees(int index_only,\n+static int git_merge_trees(struct merge_options *o,\n \t\t\t   struct tree *common,\n \t\t\t   struct tree *head,\n \t\t\t   struct tree *merge)\n {\n \tint rc;\n \tstruct tree_desc t[3];\n-\tstruct unpack_trees_options opts;\n \n-\tmemset(&opts, 0, sizeof(opts));\n-\tif (index_only)\n-\t\topts.index_only = 1;\n+\tmemset(&o->unpack_opts, 0, sizeof(o->unpack_opts));\n+\tif (o->call_depth)\n+\t\to->unpack_opts.index_only = 1;\n \telse\n-\t\topts.update = 1;\n-\topts.merge = 1;\n-\topts.head_idx = 2;\n-\topts.fn = threeway_merge;\n-\topts.src_index = &the_index;\n-\topts.dst_index = &the_index;\n-\tsetup_unpack_trees_porcelain(&opts, \"merge\");\n+\t\to->unpack_opts.update = 1;\n+\to->unpack_opts.merge = 1;\n+\to->unpack_opts.head_idx = 2;\n+\to->unpack_opts.fn = threeway_merge;\n+\to->unpack_opts.src_index = &the_index;\n+\to->unpack_opts.dst_index = &the_index;\n+\tsetup_unpack_trees_porcelain(&o->unpack_opts, \"merge\");\n \n \tinit_tree_desc_from_tree(t+0, common);\n \tinit_tree_desc_from_tree(t+1, head);\n \tinit_tree_desc_from_tree(t+2, merge);\n \n-\trc = unpack_trees(3, t, &opts);\n+\trc = unpack_trees(3, t, &o->unpack_opts);\n+\t/*\n+\t * unpack_trees NULLifies src_index, but it's used in verify_uptodate,\n+\t * so set to the new index which will usually have modification\n+\t * timestamp info copied over.\n+\t */\n+\to->unpack_opts.src_index = &the_index;\n \tcache_tree_free(&active_cache_tree);\n \treturn rc;\n }\n@@ -792,6 +797,20 @@ static int would_lose_untracked(const char *path)\n \treturn !was_tracked(path) && file_exists(path);\n }\n \n+static int was_dirty(struct merge_options *o, const char *path)\n+{\n+\tstruct cache_entry *ce;\n+\tint dirty = 1;\n+\n+\tif (o->call_depth || !was_tracked(path))\n+\t\treturn !dirty;\n+\n+\tce = cache_file_exists(path, strlen(path), ignore_case);\n+\tdirty = (ce->ce_stat_data.sd_mtime.sec > 0 &&\n+\t\t verify_uptodate(ce, &o->unpack_opts) != 0);\n+\treturn dirty;\n+}\n+\n static int make_room_for_path(struct merge_options *o, const char *path)\n {\n \tint status, i;\n@@ -2645,6 +2664,7 @@ static int handle_modify_delete(struct merge_options *o,\n \n static int merge_content(struct merge_options *o,\n \t\t\t const char *path,\n+\t\t\t int file_in_way,\n \t\t\t struct object_id *o_oid, int o_mode,\n \t\t\t struct object_id *a_oid, int a_mode,\n \t\t\t struct object_id *b_oid, int b_mode,\n@@ -2719,7 +2739,7 @@ static int merge_content(struct merge_options *o,\n \t\t\t\treturn -1;\n \t}\n \n-\tif (df_conflict_remains) {\n+\tif (df_conflict_remains || file_in_way) {\n \t\tchar *new_path;\n \t\tif (o->call_depth) {\n \t\t\tremove_file_from_cache(path);\n@@ -2753,6 +2773,30 @@ static int merge_content(struct merge_options *o,\n \treturn mfi.clean;\n }\n \n+static int conflict_rename_normal(struct merge_options *o,\n+\t\t\t\t  const char *path,\n+\t\t\t\t  struct object_id *o_oid, unsigned int o_mode,\n+\t\t\t\t  struct object_id *a_oid, unsigned int a_mode,\n+\t\t\t\t  struct object_id *b_oid, unsigned int b_mode,\n+\t\t\t\t  struct rename_conflict_info *ci)\n+{\n+\tint clean_merge;\n+\tint file_in_the_way = 0;\n+\n+\tif (was_dirty(o, path)) {\n+\t\tfile_in_the_way = 1;\n+\t\toutput(o, 1, _(\"Refusing to lose dirty file at %s\"), path);\n+\t}\n+\n+\t/* Merge the content and write it out */\n+\tclean_merge = merge_content(o, path, file_in_the_way,\n+\t\t\t\t    o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,\n+\t\t\t\t    ci);\n+\tif (clean_merge > 0 && file_in_the_way)\n+\t\tclean_merge = 0;\n+\treturn clean_merge;\n+}\n+\n /* Per entry merge function */\n static int process_entry(struct merge_options *o,\n \t\t\t const char *path, struct stage_data *entry)\n@@ -2772,9 +2816,12 @@ static int process_entry(struct merge_options *o,\n \t\tswitch (conflict_info->rename_type) {\n \t\tcase RENAME_NORMAL:\n \t\tcase RENAME_ONE_FILE_TO_ONE:\n-\t\t\tclean_merge = merge_content(o, path,\n-\t\t\t\t\t\t    o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,\n-\t\t\t\t\t\t    conflict_info);\n+\t\t\tclean_merge = conflict_rename_normal(o,\n+\t\t\t\t\t\t\t     path,\n+\t\t\t\t\t\t\t     o_oid, o_mode,\n+\t\t\t\t\t\t\t     a_oid, a_mode,\n+\t\t\t\t\t\t\t     b_oid, b_mode,\n+\t\t\t\t\t\t\t     conflict_info);\n \t\t\tbreak;\n \t\tcase RENAME_DIR:\n \t\t\tclean_merge = 1;\n@@ -2870,7 +2917,7 @@ static int process_entry(struct merge_options *o,\n \t} else if (a_oid && b_oid) {\n \t\t/* Case C: Added in both (check for same permissions) and */\n \t\t/* case D: Modified in both, but differently. */\n-\t\tclean_merge = merge_content(o, path,\n+\t\tclean_merge = merge_content(o, path, 0 /* file_in_way */,\n \t\t\t\t\t    o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,\n \t\t\t\t\t    NULL);\n \t} else if (!o_oid && !a_oid && !b_oid) {\n@@ -2904,7 +2951,7 @@ int merge_trees(struct merge_options *o,\n \t\treturn 1;\n \t}\n \n-\tcode = git_merge_trees(o->call_depth, common, head, merge);\n+\tcode = git_merge_trees(o, common, head, merge);\n \n \tif (code != 0) {\n \t\tif (show(o, 4) || o->call_depth)\ndiff --git a/merge-recursive.h b/merge-recursive.h\nindex e1be27f57c..a557201a50 100644\n--- a/merge-recursive.h\n+++ b/merge-recursive.h\n@@ -1,6 +1,7 @@\n #ifndef MERGE_RECURSIVE_H\n #define MERGE_RECURSIVE_H\n \n+#include \"unpack-trees.h\"\n #include \"string-list.h\"\n \n struct merge_options {\n@@ -27,6 +28,7 @@ struct merge_options {\n \tstruct strbuf obuf;\n \tstruct hashmap current_file_dir_set;\n \tstruct string_list df_conflict_file_set;\n+\tstruct unpack_trees_options unpack_opts;\n };\n \n struct dir_rename_entry {\ndiff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh\nindex 783bdbf59d..0d89f6d0f6 100755\n--- a/t/t3501-revert-cherry-pick.sh\n+++ b/t/t3501-revert-cherry-pick.sh\n@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick \"-\" works with arguments' '\n \ttest_cmp expect actual\n '\n \n-test_expect_failure 'cherry-pick works with dirty renamed file' '\n+test_expect_success 'cherry-pick works with dirty renamed file' '\n \ttest_commit to-rename &&\n \tgit checkout -b unrelated &&\n \ttest_commit unrelated &&\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex b1ca3d18e5..9b16280d5e 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -3157,7 +3157,7 @@ test_expect_success '11a-setup: Avoid losing dirty contents with simple rename'\n \t)\n '\n \n-test_expect_failure '11a-check: Avoid losing dirty contents with simple rename' '\n+test_expect_success '11a-check: Avoid losing dirty contents with simple rename' '\n \t(\n \t\tcd 11a &&\n \ndiff --git a/t/t7607-merge-overwrite.sh b/t/t7607-merge-overwrite.sh\nindex 00617dadf8..e44fb50173 100755\n--- a/t/t7607-merge-overwrite.sh\n+++ b/t/t7607-merge-overwrite.sh\n@@ -92,7 +92,7 @@ test_expect_success 'will not overwrite removed file with staged changes' '\n \ttest_cmp important c1.c\n '\n \n-test_expect_failure 'will not overwrite unstaged changes in renamed file' '\n+test_expect_success 'will not overwrite unstaged changes in renamed file' '\n \tgit reset --hard c1 &&\n \tgit mv c1.c other.c &&\n \tgit commit -m rename &&\ndiff --git a/unpack-trees.c b/unpack-trees.c\nindex bf8b602901..5b922c1939 100644\n--- a/unpack-trees.c\n+++ b/unpack-trees.c\n@@ -1486,8 +1486,8 @@ static int verify_uptodate_1(const struct cache_entry *ce,\n \t\tadd_rejected_path(o, error_type, ce->name);\n }\n \n-static int verify_uptodate(const struct cache_entry *ce,\n-\t\t\t   struct unpack_trees_options *o)\n+int verify_uptodate(const struct cache_entry *ce,\n+\t\t    struct unpack_trees_options *o)\n {\n \tif (!o->skip_sparse_checkout && (ce->ce_flags & CE_NEW_SKIP_WORKTREE))\n \t\treturn 0;\ndiff --git a/unpack-trees.h b/unpack-trees.h\nindex 6c48117b84..41178ada94 100644\n--- a/unpack-trees.h\n+++ b/unpack-trees.h\n@@ -1,6 +1,7 @@\n #ifndef UNPACK_TREES_H\n #define UNPACK_TREES_H\n \n+#include \"tree-walk.h\"\n #include \"string-list.h\"\n \n #define MAX_UNPACK_TREES 8\n@@ -78,6 +79,9 @@ struct unpack_trees_options {\n extern int unpack_trees(unsigned n, struct tree_desc *t,\n \t\tstruct unpack_trees_options *options);\n \n+int verify_uptodate(const struct cache_entry *ce,\n+\t\t    struct unpack_trees_options *o);\n+\n int threeway_merge(const struct cache_entry * const *stages,\n \t\t   struct unpack_trees_options *o);\n int twoway_merge(const struct cache_entry * const *src,\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333751","messageId":"20171129014237.32570-30-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 29/34] merge-recursive: avoid clobbering untracked files with directory renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:32Z","receivedAt":"2017-11-29T01:44:41Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   | 42 +++++++++++++++++++++++++++++++++++--\n t/t6043-merge-rename-directories.sh |  6 +++---\n 2 files changed, 43 insertions(+), 5 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex cdf8588c75..e77d2b043d 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -1138,6 +1138,26 @@ static int conflict_rename_dir(struct merge_options *o,\n {\n \tconst struct diff_filespec *dest = pair->two;\n \n+\tif (!o->call_depth && would_lose_untracked(dest->path)) {\n+\t\tchar *alt_path = unique_path(o, dest->path, rename_branch);\n+\n+\t\toutput(o, 1, _(\"Error: Refusing to lose untracked file at %s; \"\n+\t\t\t       \"writing to %s instead.\"),\n+\t\t       dest->path, alt_path);\n+\t\t/*\n+\t\t * Write the file in worktree at alt_path, but not in the\n+\t\t * index.  Instead, write to dest->path for the index but\n+\t\t * only at the higher appropriate stage.\n+\t\t */\n+\t\tif (update_file(o, 0, &dest->oid, dest->mode, alt_path))\n+\t\t\treturn -1;\n+\t\tfree(alt_path);\n+\t\treturn update_stages(o, dest->path, NULL,\n+\t\t\t\t     rename_branch == o->branch1 ? dest : NULL,\n+\t\t\t\t     rename_branch == o->branch1 ? NULL : dest);\n+\t}\n+\n+\t/* Update dest->path both in index and in worktree */\n \tif (update_file(o, 1, &dest->oid, dest->mode, dest->path))\n \t\treturn -1;\n \treturn 0;\n@@ -1156,7 +1176,8 @@ static int handle_change_delete(struct merge_options *o,\n \tconst char *update_path = path;\n \tint ret = 0;\n \n-\tif (dir_in_way(path, !o->call_depth, 0)) {\n+\tif (dir_in_way(path, !o->call_depth, 0) ||\n+\t    (!o->call_depth && would_lose_untracked(path))) {\n \t\tupdate_path = alt_path = unique_path(o, path, change_branch);\n \t}\n \n@@ -1282,6 +1303,12 @@ static int handle_file(struct merge_options *o,\n \t\t\tdst_name = unique_path(o, rename->path, cur_branch);\n \t\t\toutput(o, 1, _(\"%s is a directory in %s adding as %s instead\"),\n \t\t\t       rename->path, other_branch, dst_name);\n+\t\t} else if (!o->call_depth &&\n+\t\t\t   would_lose_untracked(rename->path)) {\n+\t\t\tdst_name = unique_path(o, rename->path, cur_branch);\n+\t\t\toutput(o, 1, _(\"Refusing to lose untracked file at %s; \"\n+\t\t\t\t       \"adding as %s instead\"),\n+\t\t\t       rename->path, dst_name);\n \t\t}\n \t}\n \tif ((ret = update_file(o, 0, &rename->oid, rename->mode, dst_name)))\n@@ -1407,7 +1434,18 @@ static int conflict_rename_rename_2to1(struct merge_options *o,\n \t\tchar *new_path2 = unique_path(o, path, ci->branch2);\n \t\toutput(o, 1, _(\"Renaming %s to %s and %s to %s instead\"),\n \t\t       a->path, new_path1, b->path, new_path2);\n-\t\tremove_file(o, 0, path, 0);\n+\t\tif (would_lose_untracked(path))\n+\t\t\t/*\n+\t\t\t * Only way we get here is if both renames were from\n+\t\t\t * a directory rename AND user had an untracked file\n+\t\t\t * at the location where both files end up after the\n+\t\t\t * two directory renames.  See testcase 10d of t6043.\n+\t\t\t */\n+\t\t\toutput(o, 1, _(\"Refusing to lose untracked file at \"\n+\t\t\t\t       \"%s, even though it's in the way.\"),\n+\t\t\t       path);\n+\t\telse\n+\t\t\tremove_file(o, 0, path, 0);\n \t\tret = update_file(o, 0, &mfi_c1.oid, mfi_c1.mode, new_path1);\n \t\tif (!ret)\n \t\t\tret = update_file(o, 0, &mfi_c2.oid, mfi_c2.mode,\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 261fb4917d..b1ca3d18e5 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -2873,7 +2873,7 @@ test_expect_success '10b-setup: Overwrite untracked with dir rename + delete' '\n \t)\n '\n \n-test_expect_failure '10b-check: Overwrite untracked with dir rename + delete' '\n+test_expect_success '10b-check: Overwrite untracked with dir rename + delete' '\n \t(\n \t\tcd 10b &&\n \n@@ -2944,7 +2944,7 @@ test_expect_success '10c-setup: Overwrite untracked with dir rename/rename(1to2)\n \t)\n '\n \n-test_expect_failure '10c-check: Overwrite untracked with dir rename/rename(1to2)' '\n+test_expect_success '10c-check: Overwrite untracked with dir rename/rename(1to2)' '\n \t(\n \t\tcd 10c &&\n \n@@ -3013,7 +3013,7 @@ test_expect_success '10d-setup: Delete untracked with dir rename/rename(2to1)' '\n \t)\n '\n \n-test_expect_failure '10d-check: Delete untracked with dir rename/rename(2to1)' '\n+test_expect_success '10d-check: Delete untracked with dir rename/rename(2to1)' '\n \t(\n \t\tcd 10d &&\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333752","messageId":"20171129014237.32570-9-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 08/34] directory rename detection: files/directories in the way of some renames","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:11Z","receivedAt":"2017-11-29T01:44:43Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 320 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 320 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 1dcf010aa6..29b2af7f19 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -818,4 +818,324 @@ test_expect_success '4a-check: Directory split, with original directory still pr\n #   detection.)  But, sadly, see testcase 8b.\n ###########################################################################\n \n+\n+###########################################################################\n+# SECTION 5: Files/directories in the way of subset of to-be-renamed paths\n+#\n+# Implicitly renaming files due to a detected directory rename could run\n+# into problems if there are files or directories in the way of the paths\n+# we want to rename.  Explore such cases in this section.\n+###########################################################################\n+\n+# Testcase 5a, Merge directories, other side adds files to original and target\n+#   Commit O: z/{b,c},       y/d\n+#   Commit A: z/{b,c,e_1,f}, y/{d,e_2}\n+#   Commit B: y/{b,c,d}\n+#   Expected: z/e_1, y/{b,c,d,e_2,f} + CONFLICT warning\n+#   NOTE: While directory rename detection is active here causing z/f to\n+#         become y/f, we did not apply this for z/e_1 because that would\n+#         give us an add/add conflict for y/e_1 vs y/e_2.  This problem with\n+#         this add/add, is that both versions of y/e are from the same side\n+#         of history, giving us no way to represent this conflict in the\n+#         index.\n+\n+test_expect_success '5a-setup: Merge directories, other side adds files to original and target' '\n+\ttest_create_repo 5a &&\n+\t(\n+\t\tcd 5a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir y &&\n+\t\techo d >y/d &&\n+\t\tgit add z y &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\techo e1 >z/e &&\n+\t\techo f >z/f &&\n+\t\techo e2 >y/e &&\n+\t\tgit add z/e z/f y/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\trmdir z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '5a-check: Merge directories, other side adds files to original and target' '\n+\t(\n+\t\tcd 5a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :0:y/d :0:y/e :0:z/e &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:y/d A:y/e A:z/e &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest $(git rev-parse :0:y/f) = $(git rev-parse A:z/f) &&\n+\n+\t\ttest_i18ngrep \"CONFLICT.*implicit dir rename\" out\n+\t)\n+'\n+\n+# Testcase 5b, Rename/delete in order to get add/add/add conflict\n+#   (Related to testcase 8d; these may appear slightly inconsistent to users;\n+#    Also related to testcases 7d and 7e)\n+#   Commit O: z/{b,c,d_1}\n+#   Commit A: y/{b,c,d_2}\n+#   Commit B: z/{b,c,d_1,e}, y/d_3\n+#   Expected: y/{b,c,e}, CONFLICT(add/add: y/d_2 vs. y/d_3)\n+#   NOTE: If z/d_1 in commit B were to be involved in dir rename detection, as\n+#         we normaly would since z/ is being renamed to y/, then this would be\n+#         a rename/delete (z/d_1 -> y/d_1 vs. deleted) AND an add/add/add\n+#         conflict of y/d_1 vs. y/d_2 vs. y/d_3.  Add/add/add is not\n+#         representable in the index, so the existence of y/d_3 needs to\n+#         cause us to bail on directory rename detection for that path, falling\n+#         back to git behavior without the directory rename detection.\n+\n+test_expect_success '5b-setup: Rename/delete in order to get add/add/add conflict' '\n+\ttest_create_repo 5b &&\n+\t(\n+\t\tcd 5b &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d1 >z/d &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit rm z/d &&\n+\t\tgit mv z y &&\n+\t\techo d2 >y/d &&\n+\t\tgit add y/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir y &&\n+\t\techo d3 >y/d &&\n+\t\techo e >z/e &&\n+\t\tgit add y/d z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '5b-check: Rename/delete in order to get add/add/add conflict' '\n+\t(\n+\t\tcd 5b &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (add/add).* y/d\" out &&\n+\n+\t\ttest 5 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :0:y/e :2:y/d :3:y/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/e A:y/d B:y/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_must_fail git rev-parse :1:y/d &&\n+\t\ttest -f y/d\n+\t)\n+'\n+\n+# Testcase 5c, Transitive rename would cause rename/rename/rename/add/add/add\n+#   (Directory rename detection would result in transitive rename vs.\n+#    rename/rename(1to2) and turn it into a rename/rename(1to3).  Further,\n+#    rename paths conflict with separate adds on the other side)\n+#   (Related to testcases 3b and 7c)\n+#   Commit O: z/{b,c}, x/d_1\n+#   Commit A: y/{b,c,d_2}, w/d_1\n+#   Commit B: z/{b,c,d_1,e}, w/d_3, y/d_4\n+#   Expected: A mess, but only a rename/rename(1to2)/add/add mess.  Use the\n+#             presence of y/d_4 in B to avoid doing transitive rename of\n+#             x/d_1 -> z/d_1 -> y/d_1, so that the only paths we have at\n+#             y/d are y/d_2 and y/d_4.  We still do the move from z/e to y/e,\n+#             though, because it doesn't have anything in the way.\n+\n+test_expect_success '5c-setup: Transitive rename would cause rename/rename/rename/add/add/add' '\n+\ttest_create_repo 5c &&\n+\t(\n+\t\tcd 5c &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tmkdir x &&\n+\t\techo d1 >x/d &&\n+\t\tgit add z x &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\techo d2 >y/d &&\n+\t\tgit add y/d &&\n+\t\tgit mv x w &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tgit mv x/d z/ &&\n+\t\tmkdir w &&\n+\t\tmkdir y &&\n+\t\techo d3 >w/d &&\n+\t\techo d4 >y/d &&\n+\t\techo e >z/e &&\n+\t\tgit add w/ y/ z/e &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '5c-check: Transitive rename would cause rename/rename/rename/add/add/add' '\n+\t(\n+\t\tcd 5c &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (rename/rename).*x/d.*w/d.*z/d\" out &&\n+\t\ttest_i18ngrep \"CONFLICT (add/add).* y/d\" out &&\n+\n+\t\ttest 9 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 6 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 3 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :0:y/e &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/e &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_must_fail git rev-parse :1:y/d &&\n+\t\tgit rev-parse >actual \\\n+\t\t\t:2:w/d :3:w/d :1:x/d :2:y/d :3:y/d :3:z/d &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/d B:w/d O:x/d A:y/d B:y/d O:x/d &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit hash-object >actual \\\n+\t\t\tw/d~HEAD w/d~B^0 &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:x/d B:w/d &&\n+\t\ttest_cmp expect actual &&\n+\t\ttest ! -f x/d &&\n+\t\ttest -f y/d &&\n+\t\tgrep -q \"<<<<\" y/d &&  # conflict markers should be present\n+\t\ttest $(git hash-object z/d) = $(git rev-parse O:x/d)\n+\t)\n+'\n+\n+# Testcase 5d, Directory/file/file conflict due to directory rename\n+#   Commit O: z/{b,c}\n+#   Commit A: y/{b,c,d_1}\n+#   Commit B: z/{b,c,d_2,f}, y/d/e\n+#   Expected: y/{b,c,d/e,f}, z/d_2, CONFLICT(file/directory), y/d_1~HEAD\n+#   Note: The fact that y/d/ exists in B makes us bail on directory rename\n+#         detection for z/d_2, but that doesn't prevent us from applying the\n+#         directory rename detection for z/f -> y/f.\n+\n+test_expect_success '5d-setup: Directory/file/file conflict due to directory rename' '\n+\ttest_create_repo 5d &&\n+\t(\n+\t\tcd 5d &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tgit mv z y &&\n+\t\techo d1 >y/d &&\n+\t\tgit add y/d &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\tmkdir -p y/d &&\n+\t\techo e >y/d/e &&\n+\t\techo d2 >z/d &&\n+\t\techo f >z/f &&\n+\t\tgit add y/d/e z/d z/f &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_failure '5d-check: Directory/file/file conflict due to directory rename' '\n+\t(\n+\t\tcd 5d &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\ttest_must_fail git merge -s recursive B^0 >out &&\n+\t\ttest_i18ngrep \"CONFLICT (file/directory).*y/d\" out &&\n+\n+\t\ttest 6 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 1 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 2 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\t:0:y/b :0:y/c :0:z/d :0:y/f :2:y/d :0:y/d/e &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c B:z/d B:z/f A:y/d B:y/d/e &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest $(git hash-object y/d~HEAD) = $(git rev-parse A:y/d)\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by section 5:\n+#\n+#   If a subset of to-be-renamed files have a file or directory in the way,\n+#   \"turn off\" the directory rename for those specific sub-paths, falling\n+#   back to old handling.  But, sadly, see testcases 8a and 8b.\n+###########################################################################\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333753","messageId":"20171129014237.32570-35-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 34/34] merge-recursive: ensure we write updates for directory-renamed file","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:37Z","receivedAt":"2017-11-29T01:44:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"When a file is present in HEAD before the merge and the other side of the\nmerge does not modify that file, we try to avoid re-writing the file and\nmaking it stat-dirty.  However, when a file is present in HEAD before the\nmerge and was in a directory that was renamed by the other side of the\nmerge, we have to move the file to a new location and re-write it.\nUpdate the code that checks whether we can skip the update to also work in\nthe presence of directory renames.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n merge-recursive.c                   | 4 +---\n t/t6043-merge-rename-directories.sh | 2 +-\n 2 files changed, 2 insertions(+), 4 deletions(-)\n\ndiff --git a/merge-recursive.c b/merge-recursive.c\nindex d00786f719..16c52a4347 100644\n--- a/merge-recursive.c\n+++ b/merge-recursive.c\n@@ -2732,7 +2732,6 @@ static int merge_content(struct merge_options *o,\n \n \tif (mfi.clean && !df_conflict_remains &&\n \t    oid_eq(&mfi.oid, a_oid) && mfi.mode == a_mode) {\n-\t\tint path_renamed_outside_HEAD;\n \t\toutput(o, 3, _(\"Skipped %s (merged same as existing)\"), path);\n \t\t/*\n \t\t * The content merge resulted in the same file contents we\n@@ -2740,8 +2739,7 @@ static int merge_content(struct merge_options *o,\n \t\t * are recorded at the correct path (which may not be true\n \t\t * if the merge involves a rename).\n \t\t */\n-\t\tpath_renamed_outside_HEAD = !path2 || !strcmp(path, path2);\n-\t\tif (!path_renamed_outside_HEAD) {\n+\t\tif (was_tracked(path)) {\n \t\t\tadd_cacheinfo(o, mfi.mode, &mfi.oid, path,\n \t\t\t\t      0, (!o->call_depth), 0);\n \t\t\treturn mfi.clean;\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 8ed85c79de..f64c7d273b 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -3713,7 +3713,7 @@ test_expect_success '12b-setup: Moving one directory hierarchy into another' '\n \t)\n '\n \n-test_expect_failure '12b-check: Moving one directory hierarchy into another' '\n+test_expect_success '12b-check: Moving one directory hierarchy into another' '\n \t(\n \t\tcd 12b &&\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333754","messageId":"20171129014237.32570-8-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 07/34] directory rename detection: partially renamed directory testcase/discussion","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:10Z","receivedAt":"2017-11-29T01:44:50Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Signed-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t6043-merge-rename-directories.sh | 104 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 104 insertions(+)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex 0ccabed4a2..1dcf010aa6 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -714,4 +714,108 @@ test_expect_success '3b-check: Avoid implicit rename if involved as source on cu\n #   of a rename on either side of a merge.\n ###########################################################################\n \n+\n+###########################################################################\n+# SECTION 4: Partially renamed directory; still exists on both sides of merge\n+#\n+# What if we were to attempt to do directory rename detection when someone\n+# \"mostly\" moved a directory but still left some files around, or,\n+# equivalently, fully renamed a directory in one commmit and then recreated\n+# that directory in a later commit adding some new files and then tried to\n+# merge?\n+#\n+# It's hard to divine user intent in these cases, because you can make an\n+# argument that, depending on the intermediate history of the side being\n+# merged, that some users will want files in that directory to\n+# automatically be detected and renamed, while users with a different\n+# intermediate history wouldn't want that rename to happen.\n+#\n+# I think that it is best to simply not have directory rename detection\n+# apply to such cases.  My reasoning for this is four-fold: (1) it's\n+# easiest for users in general to figure out what happened if we don't\n+# apply directory rename detection in any such case, (2) it's an easy rule\n+# to explain [\"We don't do directory rename detection if the directory\n+# still exists on both sides of the merge\"], (3) we can get some hairy\n+# edge/corner cases that would be really confusing and possibly not even\n+# representable in the index if we were to even try, and [related to 3] (4)\n+# attempting to resolve this issue of divining user intent by examining\n+# intermediate history goes against the spirit of three-way merges and is a\n+# path towards crazy corner cases that are far more complex than what we're\n+# already dealing with.\n+#\n+# This section contains a test for this partially-renamed-directory case.\n+###########################################################################\n+\n+# Testcase 4a, Directory split, with original directory still present\n+#   (Related to testcase 1f)\n+#   Commit O: z/{b,c,d,e}\n+#   Commit A: y/{b,c,d}, z/e\n+#   Commit B: z/{b,c,d,e,f}\n+#   Expected: y/{b,c,d}, z/{e,f}\n+#   NOTE: Even though most files from z moved to y, we don't want f to follow.\n+\n+test_expect_success '4a-setup: Directory split, with original directory still present' '\n+\ttest_create_repo 4a &&\n+\t(\n+\t\tcd 4a &&\n+\n+\t\tmkdir z &&\n+\t\techo b >z/b &&\n+\t\techo c >z/c &&\n+\t\techo d >z/d &&\n+\t\techo e >z/e &&\n+\t\tgit add z &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"O\" &&\n+\n+\t\tgit branch O &&\n+\t\tgit branch A &&\n+\t\tgit branch B &&\n+\n+\t\tgit checkout A &&\n+\t\tmkdir y &&\n+\t\tgit mv z/b y/ &&\n+\t\tgit mv z/c y/ &&\n+\t\tgit mv z/d y/ &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"A\" &&\n+\n+\t\tgit checkout B &&\n+\t\techo f >z/f &&\n+\t\tgit add z/f &&\n+\t\ttest_tick &&\n+\t\tgit commit -m \"B\"\n+\t)\n+'\n+\n+test_expect_success '4a-check: Directory split, with original directory still present' '\n+\t(\n+\t\tcd 4a &&\n+\n+\t\tgit checkout A^0 &&\n+\n+\t\tgit merge -s recursive B^0 &&\n+\n+\t\ttest 5 -eq $(git ls-files -s | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n+\t\ttest 0 -eq $(git ls-files -o | wc -l) &&\n+\n+\t\tgit rev-parse >actual \\\n+\t\t\tHEAD:y/b HEAD:y/c HEAD:y/d HEAD:z/e HEAD:z/f &&\n+\t\tgit rev-parse >expect \\\n+\t\t\tO:z/b O:z/c O:z/d O:z/e B:z/f &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+###########################################################################\n+# Rules suggested by section 4:\n+#\n+#   Directory-rename-detection should be turned off for any directories (as\n+#   a source for renames) that exist on both sides of the merge.  (The \"as\n+#   a source for renames\" clarification is due to cases like 1c where\n+#   the target directory exists on both sides and we do want the rename\n+#   detection.)  But, sadly, see testcase 8b.\n+###########################################################################\n+\n test_done\n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"333755","messageId":"20171129014237.32570-2-newren@gmail.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"[PATCH v4 01/34] Tighten and correct a few testcases for merging and cherry-picking","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-11-29T01:42:04Z","receivedAt":"2017-11-29T01:44:52Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"t3501 had a testcase originally added in 05f2dfb965 (cherry-pick:\ndemonstrate a segmentation fault, 2016-11-26) to ensure cherry-pick\nwouldn't segfault when working with a dirty file involved in a rename.\nWhile the segfault was fixed, there was another problem this test\ndemonstrated: namely, that git would overwrite a dirty file involved in a\nrename.  Further, the test encoded a \"successful merge\" and overwriting of\nthis file as correct behavior.  Modify the test so that it would still\ncatch the segfault, but to require the correct behavior.  Mark it as\ntest_expect_failure for now too, since this second bug is not yet fixed.\n\nt7607 had a test added in 30fd3a5425 (merge overwrites unstaged changes in\nrenamed file, 2012-04-15) specific to looking for a merge overwriting a\ndirty file involved in a rename, but it too actually encoded what I would\nterm incorrect behavior: it expected the merge to succeed.  Fix that, and\nadd a few more checks to make sure that the merge really does produce the\nexpected results.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n t/t3501-revert-cherry-pick.sh | 7 +++++--\n t/t7607-merge-overwrite.sh    | 5 ++++-\n 2 files changed, 9 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh\nindex 4f2a263b63..783bdbf59d 100755\n--- a/t/t3501-revert-cherry-pick.sh\n+++ b/t/t3501-revert-cherry-pick.sh\n@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick \"-\" works with arguments' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'cherry-pick works with dirty renamed file' '\n+test_expect_failure 'cherry-pick works with dirty renamed file' '\n \ttest_commit to-rename &&\n \tgit checkout -b unrelated &&\n \ttest_commit unrelated &&\n@@ -150,7 +150,10 @@ test_expect_success 'cherry-pick works with dirty renamed file' '\n \ttest_tick &&\n \tgit commit -m renamed &&\n \techo modified >renamed &&\n-\tgit cherry-pick refs/heads/unrelated\n+\ttest_must_fail git cherry-pick refs/heads/unrelated >out &&\n+\ttest_i18ngrep \"Refusing to lose dirty file at renamed\" out &&\n+\ttest $(git rev-parse :0:renamed) = $(git rev-parse HEAD^:to-rename.t) &&\n+\tgrep -q \"^modified$\" renamed\n '\n \n test_done\ndiff --git a/t/t7607-merge-overwrite.sh b/t/t7607-merge-overwrite.sh\nindex 9444d6a9b9..00617dadf8 100755\n--- a/t/t7607-merge-overwrite.sh\n+++ b/t/t7607-merge-overwrite.sh\n@@ -97,7 +97,10 @@ test_expect_failure 'will not overwrite unstaged changes in renamed file' '\n \tgit mv c1.c other.c &&\n \tgit commit -m rename &&\n \tcp important other.c &&\n-\tgit merge c1a &&\n+\ttest_must_fail git merge c1a >out &&\n+\ttest_i18ngrep \"Refusing to lose dirty file at other.c\" out &&\n+\ttest -f other.c~HEAD &&\n+\ttest $(git hash-object other.c~HEAD) = $(git rev-parse c1a:c1.c) &&\n \ttest_cmp important other.c\n '\n \n-- \n2.15.0.408.g850bc54b15\n\n"},{"id":"334732","messageId":"xmqqh8svxwpm.fsf@gitster.mtv.corp.google.com","threadId":"47340","inReplyTo":"20171129014237.32570-1-newren@gmail.com","subject":"Re: [PATCH v4 00/34] Add directory rename detection to git","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-12-13T01:06:45Z","receivedAt":"2017-12-13T01:06:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> This patchset introduces directory rename detection to merge-recursive.\n\nThe use of negated form of test_i18ngrep in these patches are all\nwrong.  Because the helper must say \"even though the string does not\nmatch (does match), the test expects it to match (does not match),\nand we know that expectation won't hold simply because we are under\npoison build\", so negating the result of test_i18ngrep won't work.\nInstead, you would tell test_i18ngrep that we do not expect it to\nfind matching lines.\n\nEven with the attached, test #70 will still fail because you have a\nconstruct that greps in output of test_i18ngrep.  That won't work\nunder poison build, because the output of test_i18ngrep won't have\nthe string you are looking for under poison build.\n\nWe may probably want to redirect the output of underlying grep to\n/dev/null in test_i18ngrep to make this kind of misuse easier to\nspot.\n\n t/t6043-merge-rename-directories.sh | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh\nindex f64c7d273b..8f58f08ed2 100755\n--- a/t/t6043-merge-rename-directories.sh\n+++ b/t/t6043-merge-rename-directories.sh\n@@ -554,7 +554,7 @@ test_expect_success '2b-check: Directory split into two on one side, with equal\n \t\tgit rev-parse >expect \\\n \t\t\tO:z/b O:z/c B:x/d &&\n \t\ttest_cmp expect actual &&\n-\t\t! test_i18ngrep \"CONFLICT.*directory rename split\" out\n+\t\ttest_i18ngrep ! \"CONFLICT.*directory rename split\" out\n \t)\n '\n \n@@ -705,7 +705,7 @@ test_expect_success '3b-check: Avoid implicit rename if involved as source on cu\n \t\ttest_cmp expect actual &&\n \n \t\ttest_i18ngrep CONFLICT.*rename/rename.*z/d.*x/d.*w/d out &&\n-\t! test_i18ngrep CONFLICT.*rename/rename.*y/d\n+\t\ttest_i18ngrep ! CONFLICT.*rename/rename.*y/d\n \t)\n '\n \n@@ -3146,7 +3146,7 @@ test_expect_failure '10e-check: Does git complain about untracked file that is n\n \t\techo random >z/c &&\n \n \t\tgit merge -s recursive B^0 >out 2>err &&\n-\t! test_i18ngrep \"following untracked working tree files would be overwritten by merge\" err &&\n+\t\ttest_i18ngrep ! \"following untracked working tree files would be overwritten by merge\" err &&\n \n \t\ttest 3 -eq $(git ls-files -s | wc -l) &&\n \t\ttest 0 -eq $(git ls-files -u | wc -l) &&\n"},{"id":"334733","messageId":"CAPc5daU1b1sJPK37sjMKdr=xY5yg3qaHk9PPdgJ1JH9KvJxatQ@mail.gmail.com","threadId":"47340","inReplyTo":"xmqqh8svxwpm.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH v4 00/34] Add directory rename detection to git","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-12-13T02:01:04Z","receivedAt":"2017-12-13T02:12:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"OK, it seems that I managed to make this test pass under poison build\n(see https://travis-ci.org/git/git/jobs/315658242)\n\nPlease check https://github.com/git/git/commit/e5c5e24ad91a75b5a70c056fe6c6e3bfb55b56fc\nand sprinkle its fix to whichever original commits in the series that\nneed fixing.\n\nThanks.\n"},{"id":"334754","messageId":"CABPp-BG8rZtVarrfapWOzOhsS0-WnUS5g8cDZW0hLnUgQhNJ4A@mail.gmail.com","threadId":"47340","inReplyTo":"CAPc5daU1b1sJPK37sjMKdr=xY5yg3qaHk9PPdgJ1JH9KvJxatQ@mail.gmail.com","subject":"Re: [PATCH v4 00/34] Add directory rename detection to git","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2017-12-13T15:38:11Z","receivedAt":"2017-12-13T15:38:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 12, 2017 at 6:01 PM, Junio C Hamano <gitster@pobox.com> wrote:\n> OK, it seems that I managed to make this test pass under poison build\n> (see https://travis-ci.org/git/git/jobs/315658242)\n>\n> Please check https://github.com/git/git/commit/e5c5e24ad91a75b5a70c056fe6c6e3bfb55b56fc\n> and sprinkle its fix to whichever original commits in the series that\n> need fixing.\n\nWill do; sorry for the problems.  Thanks for finding and fixing.\n"},{"id":"334768","messageId":"14bf2760-2787-5dd3-e5cc-a2ec0e0d8950@ramsayjones.plus.com","threadId":"47340","inReplyTo":"xmqqh8svxwpm.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH v4 00/34] Add directory rename detection to git","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2017-12-13T18:15:49Z","receivedAt":"2017-12-13T18:15:57Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 13/12/17 01:06, Junio C Hamano wrote:\n> Elijah Newren <newren@gmail.com> writes:\n> \n>> This patchset introduces directory rename detection to merge-recursive.\n> \n> The use of negated form of test_i18ngrep in these patches are all\n> wrong.  Because the helper must say \"even though the string does not\n> match (does match), the test expects it to match (does not match),\n> and we know that expectation won't hold simply because we are under\n> poison build\", so negating the result of test_i18ngrep won't work.\n> Instead, you would tell test_i18ngrep that we do not expect it to\n> find matching lines.\n> \n> Even with the attached, test #70 will still fail because you have a\n> construct that greps in output of test_i18ngrep.  That won't work\n> under poison build, because the output of test_i18ngrep won't have\n> the string you are looking for under poison build.\n> \n> We may probably want to redirect the output of underlying grep to\n> /dev/null in test_i18ngrep to make this kind of misuse easier to\n> spot.\n\nI have test-suite failures on the 'pu' branch for t4151-am-abort.sh\n(#3 and #6) and t5536-fetch-conflicts.sh (#3 and #6-7), which on a\nvery quick inspection seem to be due to this (ie your SQUASH commit\ne5c5e24ad9).\n\nFor t4151 the test (both 3 & 6) is using test_i18ngrep on the output\nof a previous test_i18ngrep, and t5536 is doing likewise in the\nverify_stderr() helper function.\n\nATB,\nRamsay Jones\n\n\n"},{"id":"334773","messageId":"xmqqvahawirr.fsf@gitster.mtv.corp.google.com","threadId":"47340","inReplyTo":"14bf2760-2787-5dd3-e5cc-a2ec0e0d8950@ramsayjones.plus.com","subject":"Re: [PATCH v4 00/34] Add directory rename detection to git","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-12-13T19:05:28Z","receivedAt":"2017-12-13T19:05:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ramsay Jones <ramsay@ramsayjones.plus.com> writes:\n\n> On 13/12/17 01:06, Junio C Hamano wrote:\n>\n>> We may probably want to redirect the output of underlying grep to\n>> /dev/null in test_i18ngrep to make this kind of misuse easier to\n>> spot.\n>\n> I have test-suite failures on the 'pu' branch for t4151-am-abort.sh\n> (#3 and #6) and t5536-fetch-conflicts.sh (#3 and #6-7), which on a\n> very quick inspection seem to be due to this (ie your SQUASH commit\n> e5c5e24ad9).\n\nYup, sorry for leaving it broken this morning.  I re-pushed after\ndropping the /dev/null experiment to fix it.  In the longer term, I\nthink we should make sure that output from test_i18ngrep is never\nused (i.e. t4151 etc. must be fixed), though.\n\n"}]}