]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'en/rename-directory-detection'
authorJunio C Hamano <gitster@pobox.com>
Mon, 9 Apr 2018 23:25:43 +0000 (08:25 +0900)
committerJunio C Hamano <gitster@pobox.com>
Mon, 9 Apr 2018 23:25:43 +0000 (08:25 +0900)
Rename detection logic in "diff" family that is used in "merge" has
learned to guess when all of x/a, x/b and x/c have moved to z/a,
z/b and z/c, it is likely that x/d added in the meantime would also
want to move to z/d by taking the hint that the entire directory
'x' moved to 'z'.  A bug causing dirty files involved in a rename
to be overwritten during merge has also been fixed as part of this
work.

* en/rename-directory-detection: (29 commits)
  merge-recursive: ensure we write updates for directory-renamed file
  merge-recursive: avoid spurious rename/rename conflict from dir renames
  directory rename detection: new testcases showcasing a pair of bugs
  merge-recursive: fix remaining directory rename + dirty overwrite cases
  merge-recursive: fix overwriting dirty files involved in renames
  merge-recursive: avoid clobbering untracked files with directory renames
  merge-recursive: apply necessary modifications for directory renames
  merge-recursive: when comparing files, don't include trees
  merge-recursive: check for file level conflicts then get new name
  merge-recursive: add computation of collisions due to dir rename & merging
  merge-recursive: check for directory level conflicts
  merge-recursive: add get_directory_renames()
  merge-recursive: make a helper function for cleanup for handle_renames
  merge-recursive: split out code for determining diff_filepairs
  merge-recursive: make !o->detect_rename codepath more obvious
  merge-recursive: fix leaks of allocated renames and diff_filepairs
  merge-recursive: introduce new functions to handle rename logic
  merge-recursive: move the get_renames() function
  directory rename detection: tests for handling overwriting dirty files
  directory rename detection: tests for handling overwriting untracked files
  ...

merge-recursive.c
merge-recursive.h
strbuf.c
strbuf.h
t/t3501-revert-cherry-pick.sh
t/t6043-merge-rename-directories.sh [new file with mode: 0755]
t/t7607-merge-overwrite.sh
unpack-trees.c
unpack-trees.h

index 869092f7b9bc0ce1104a1c43c7201acdee2eb4f8..55d1e779a0e8faa2a31e699ad001e5f799fee55e 100644 (file)
@@ -49,6 +49,67 @@ static unsigned int path_hash(const char *path)
        return ignore_case ? strihash(path) : strhash(path);
 }
 
+static struct dir_rename_entry *dir_rename_find_entry(struct hashmap *hashmap,
+                                                     char *dir)
+{
+       struct dir_rename_entry key;
+
+       if (dir == NULL)
+               return NULL;
+       hashmap_entry_init(&key, strhash(dir));
+       key.dir = dir;
+       return hashmap_get(hashmap, &key, NULL);
+}
+
+static int dir_rename_cmp(const void *unused_cmp_data,
+                         const void *entry,
+                         const void *entry_or_key,
+                         const void *unused_keydata)
+{
+       const struct dir_rename_entry *e1 = entry;
+       const struct dir_rename_entry *e2 = entry_or_key;
+
+       return strcmp(e1->dir, e2->dir);
+}
+
+static void dir_rename_init(struct hashmap *map)
+{
+       hashmap_init(map, dir_rename_cmp, NULL, 0);
+}
+
+static void dir_rename_entry_init(struct dir_rename_entry *entry,
+                                 char *directory)
+{
+       hashmap_entry_init(entry, strhash(directory));
+       entry->dir = directory;
+       entry->non_unique_new_dir = 0;
+       strbuf_init(&entry->new_dir, 0);
+       string_list_init(&entry->possible_new_dirs, 0);
+}
+
+static struct collision_entry *collision_find_entry(struct hashmap *hashmap,
+                                                   char *target_file)
+{
+       struct collision_entry key;
+
+       hashmap_entry_init(&key, strhash(target_file));
+       key.target_file = target_file;
+       return hashmap_get(hashmap, &key, NULL);
+}
+
+static int collision_cmp(void *unused_cmp_data,
+                        const struct collision_entry *e1,
+                        const struct collision_entry *e2,
+                        const void *unused_keydata)
+{
+       return strcmp(e1->target_file, e2->target_file);
+}
+
+static void collision_init(struct hashmap *map)
+{
+       hashmap_init(map, (hashmap_cmp_fn) collision_cmp, NULL, 0);
+}
+
 static void flush_output(struct merge_options *o)
 {
        if (o->buffer_output < 2 && o->obuf.len) {
@@ -119,6 +180,7 @@ static int oid_eq(const struct object_id *a, const struct object_id *b)
 
 enum rename_type {
        RENAME_NORMAL = 0,
+       RENAME_DIR,
        RENAME_DELETE,
        RENAME_ONE_FILE_TO_ONE,
        RENAME_ONE_FILE_TO_TWO,
@@ -275,32 +337,37 @@ static void init_tree_desc_from_tree(struct tree_desc *desc, struct tree *tree)
        init_tree_desc(desc, tree->buffer, tree->size);
 }
 
-static int git_merge_trees(int index_only,
+static int git_merge_trees(struct merge_options *o,
                           struct tree *common,
                           struct tree *head,
                           struct tree *merge)
 {
        int rc;
        struct tree_desc t[3];
-       struct unpack_trees_options opts;
 
-       memset(&opts, 0, sizeof(opts));
-       if (index_only)
-               opts.index_only = 1;
+       memset(&o->unpack_opts, 0, sizeof(o->unpack_opts));
+       if (o->call_depth)
+               o->unpack_opts.index_only = 1;
        else
-               opts.update = 1;
-       opts.merge = 1;
-       opts.head_idx = 2;
-       opts.fn = threeway_merge;
-       opts.src_index = &the_index;
-       opts.dst_index = &the_index;
-       setup_unpack_trees_porcelain(&opts, "merge");
+               o->unpack_opts.update = 1;
+       o->unpack_opts.merge = 1;
+       o->unpack_opts.head_idx = 2;
+       o->unpack_opts.fn = threeway_merge;
+       o->unpack_opts.src_index = &the_index;
+       o->unpack_opts.dst_index = &the_index;
+       setup_unpack_trees_porcelain(&o->unpack_opts, "merge");
 
        init_tree_desc_from_tree(t+0, common);
        init_tree_desc_from_tree(t+1, head);
        init_tree_desc_from_tree(t+2, merge);
 
-       rc = unpack_trees(3, t, &opts);
+       rc = unpack_trees(3, t, &o->unpack_opts);
+       /*
+        * unpack_trees NULLifies src_index, but it's used in verify_uptodate,
+        * so set to the new index which will usually have modification
+        * timestamp info copied over.
+        */
+       o->unpack_opts.src_index = &the_index;
        cache_tree_free(&active_cache_tree);
        return rc;
 }
@@ -360,6 +427,21 @@ static void get_files_dirs(struct merge_options *o, struct tree *tree)
        read_tree_recursive(tree, "", 0, 0, &match_all, save_files_dirs, o);
 }
 
+static int get_tree_entry_if_blob(const unsigned char *tree,
+                                 const char *path,
+                                 unsigned char *hashy,
+                                 unsigned int *mode_o)
+{
+       int ret;
+
+       ret = get_tree_entry(tree, path, hashy, mode_o);
+       if (S_ISDIR(*mode_o)) {
+               hashcpy(hashy, null_sha1);
+               *mode_o = 0;
+       }
+       return ret;
+}
+
 /*
  * Returns an index_entry instance which doesn't have to correspond to
  * a real cache entry in Git's index.
@@ -370,12 +452,12 @@ static struct stage_data *insert_stage_data(const char *path,
 {
        struct string_list_item *item;
        struct stage_data *e = xcalloc(1, sizeof(struct stage_data));
-       get_tree_entry(o->object.oid.hash, path,
-                       e->stages[1].oid.hash, &e->stages[1].mode);
-       get_tree_entry(a->object.oid.hash, path,
-                       e->stages[2].oid.hash, &e->stages[2].mode);
-       get_tree_entry(b->object.oid.hash, path,
-                       e->stages[3].oid.hash, &e->stages[3].mode);
+       get_tree_entry_if_blob(o->object.oid.hash, path,
+                              e->stages[1].oid.hash, &e->stages[1].mode);
+       get_tree_entry_if_blob(a->object.oid.hash, path,
+                              e->stages[2].oid.hash, &e->stages[2].mode);
+       get_tree_entry_if_blob(b->object.oid.hash, path,
+                              e->stages[3].oid.hash, &e->stages[3].mode);
        item = string_list_insert(entries, path);
        item->util = e;
        return e;
@@ -534,78 +616,10 @@ struct rename {
         */
        struct stage_data *src_entry;
        struct stage_data *dst_entry;
+       unsigned add_turned_into_rename:1;
        unsigned processed:1;
 };
 
-/*
- * Get information of all renames which occurred between 'o_tree' and
- * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and
- * 'b_tree') to be able to associate the correct cache entries with
- * the rename information. 'tree' is always equal to either a_tree or b_tree.
- */
-static struct string_list *get_renames(struct merge_options *o,
-                                      struct tree *tree,
-                                      struct tree *o_tree,
-                                      struct tree *a_tree,
-                                      struct tree *b_tree,
-                                      struct string_list *entries)
-{
-       int i;
-       struct string_list *renames;
-       struct diff_options opts;
-
-       renames = xcalloc(1, sizeof(struct string_list));
-       if (!o->detect_rename)
-               return renames;
-
-       diff_setup(&opts);
-       opts.flags.recursive = 1;
-       opts.flags.rename_empty = 0;
-       opts.detect_rename = DIFF_DETECT_RENAME;
-       opts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :
-                           o->diff_rename_limit >= 0 ? o->diff_rename_limit :
-                           1000;
-       opts.rename_score = o->rename_score;
-       opts.show_rename_progress = o->show_rename_progress;
-       opts.output_format = DIFF_FORMAT_NO_OUTPUT;
-       diff_setup_done(&opts);
-       diff_tree_oid(&o_tree->object.oid, &tree->object.oid, "", &opts);
-       diffcore_std(&opts);
-       if (opts.needed_rename_limit > o->needed_rename_limit)
-               o->needed_rename_limit = opts.needed_rename_limit;
-       for (i = 0; i < diff_queued_diff.nr; ++i) {
-               struct string_list_item *item;
-               struct rename *re;
-               struct diff_filepair *pair = diff_queued_diff.queue[i];
-               if (pair->status != 'R') {
-                       diff_free_filepair(pair);
-                       continue;
-               }
-               re = xmalloc(sizeof(*re));
-               re->processed = 0;
-               re->pair = pair;
-               item = string_list_lookup(entries, re->pair->one->path);
-               if (!item)
-                       re->src_entry = insert_stage_data(re->pair->one->path,
-                                       o_tree, a_tree, b_tree, entries);
-               else
-                       re->src_entry = item->util;
-
-               item = string_list_lookup(entries, re->pair->two->path);
-               if (!item)
-                       re->dst_entry = insert_stage_data(re->pair->two->path,
-                                       o_tree, a_tree, b_tree, entries);
-               else
-                       re->dst_entry = item->util;
-               item = string_list_insert(renames, pair->one->path);
-               item->util = re;
-       }
-       opts.output_format = DIFF_FORMAT_NO_OUTPUT;
-       diff_queued_diff.nr = 0;
-       diff_flush(&opts);
-       return renames;
-}
-
 static int update_stages(struct merge_options *opt, const char *path,
                         const struct diff_filespec *o,
                         const struct diff_filespec *a,
@@ -637,6 +651,27 @@ static int update_stages(struct merge_options *opt, const char *path,
        return 0;
 }
 
+static int update_stages_for_stage_data(struct merge_options *opt,
+                                       const char *path,
+                                       const struct stage_data *stage_data)
+{
+       struct diff_filespec o, a, b;
+
+       o.mode = stage_data->stages[1].mode;
+       oidcpy(&o.oid, &stage_data->stages[1].oid);
+
+       a.mode = stage_data->stages[2].mode;
+       oidcpy(&a.oid, &stage_data->stages[2].oid);
+
+       b.mode = stage_data->stages[3].mode;
+       oidcpy(&b.oid, &stage_data->stages[3].oid);
+
+       return update_stages(opt, path,
+                            is_null_oid(&o.oid) ? NULL : &o,
+                            is_null_oid(&a.oid) ? NULL : &a,
+                            is_null_oid(&b.oid) ? NULL : &b);
+}
+
 static void update_entry(struct stage_data *entry,
                         struct diff_filespec *o,
                         struct diff_filespec *a,
@@ -765,6 +800,20 @@ static int would_lose_untracked(const char *path)
        return !was_tracked(path) && file_exists(path);
 }
 
+static int was_dirty(struct merge_options *o, const char *path)
+{
+       struct cache_entry *ce;
+       int dirty = 1;
+
+       if (o->call_depth || !was_tracked(path))
+               return !dirty;
+
+       ce = cache_file_exists(path, strlen(path), ignore_case);
+       dirty = (ce->ce_stat_data.sd_mtime.sec > 0 &&
+                verify_uptodate(ce, &o->unpack_opts) != 0);
+       return dirty;
+}
+
 static int make_room_for_path(struct merge_options *o, const char *path)
 {
        int status, i;
@@ -1114,6 +1163,38 @@ static int merge_file_one(struct merge_options *o,
        return merge_file_1(o, &one, &a, &b, branch1, branch2, mfi);
 }
 
+static int conflict_rename_dir(struct merge_options *o,
+                              struct diff_filepair *pair,
+                              const char *rename_branch,
+                              const char *other_branch)
+{
+       const struct diff_filespec *dest = pair->two;
+
+       if (!o->call_depth && would_lose_untracked(dest->path)) {
+               char *alt_path = unique_path(o, dest->path, rename_branch);
+
+               output(o, 1, _("Error: Refusing to lose untracked file at %s; "
+                              "writing to %s instead."),
+                      dest->path, alt_path);
+               /*
+                * Write the file in worktree at alt_path, but not in the
+                * index.  Instead, write to dest->path for the index but
+                * only at the higher appropriate stage.
+                */
+               if (update_file(o, 0, &dest->oid, dest->mode, alt_path))
+                       return -1;
+               free(alt_path);
+               return update_stages(o, dest->path, NULL,
+                                    rename_branch == o->branch1 ? dest : NULL,
+                                    rename_branch == o->branch1 ? NULL : dest);
+       }
+
+       /* Update dest->path both in index and in worktree */
+       if (update_file(o, 1, &dest->oid, dest->mode, dest->path))
+               return -1;
+       return 0;
+}
+
 static int handle_change_delete(struct merge_options *o,
                                 const char *path, const char *old_path,
                                 const struct object_id *o_oid, int o_mode,
@@ -1127,7 +1208,8 @@ static int handle_change_delete(struct merge_options *o,
        const char *update_path = path;
        int ret = 0;
 
-       if (dir_in_way(path, !o->call_depth, 0)) {
+       if (dir_in_way(path, !o->call_depth, 0) ||
+           (!o->call_depth && would_lose_untracked(path))) {
                update_path = alt_path = unique_path(o, path, change_branch);
        }
 
@@ -1242,17 +1324,34 @@ static int handle_file(struct merge_options *o,
 
        add = filespec_from_entry(&other, dst_entry, stage ^ 1);
        if (add) {
+               int ren_src_was_dirty = was_dirty(o, rename->path);
                char *add_name = unique_path(o, rename->path, other_branch);
                if (update_file(o, 0, &add->oid, add->mode, add_name))
                        return -1;
 
-               remove_file(o, 0, rename->path, 0);
+               if (ren_src_was_dirty) {
+                       output(o, 1, _("Refusing to lose dirty file at %s"),
+                              rename->path);
+               }
+               /*
+                * Because the double negatives somehow keep confusing me...
+                *    1) update_wd iff !ren_src_was_dirty.
+                *    2) no_wd iff !update_wd
+                *    3) so, no_wd == !!ren_src_was_dirty == ren_src_was_dirty
+                */
+               remove_file(o, 0, rename->path, ren_src_was_dirty);
                dst_name = unique_path(o, rename->path, cur_branch);
        } else {
                if (dir_in_way(rename->path, !o->call_depth, 0)) {
                        dst_name = unique_path(o, rename->path, cur_branch);
                        output(o, 1, _("%s is a directory in %s adding as %s instead"),
                               rename->path, other_branch, dst_name);
+               } else if (!o->call_depth &&
+                          would_lose_untracked(rename->path)) {
+                       dst_name = unique_path(o, rename->path, cur_branch);
+                       output(o, 1, _("Refusing to lose untracked file at %s; "
+                                      "adding as %s instead"),
+                              rename->path, dst_name);
                }
        }
        if ((ret = update_file(o, 0, &rename->oid, rename->mode, dst_name)))
@@ -1378,11 +1477,43 @@ static int conflict_rename_rename_2to1(struct merge_options *o,
                char *new_path2 = unique_path(o, path, ci->branch2);
                output(o, 1, _("Renaming %s to %s and %s to %s instead"),
                       a->path, new_path1, b->path, new_path2);
-               remove_file(o, 0, path, 0);
+               if (was_dirty(o, path))
+                       output(o, 1, _("Refusing to lose dirty file at %s"),
+                              path);
+               else if (would_lose_untracked(path))
+                       /*
+                        * Only way we get here is if both renames were from
+                        * a directory rename AND user had an untracked file
+                        * at the location where both files end up after the
+                        * two directory renames.  See testcase 10d of t6043.
+                        */
+                       output(o, 1, _("Refusing to lose untracked file at "
+                                      "%s, even though it's in the way."),
+                              path);
+               else
+                       remove_file(o, 0, path, 0);
                ret = update_file(o, 0, &mfi_c1.oid, mfi_c1.mode, new_path1);
                if (!ret)
                        ret = update_file(o, 0, &mfi_c2.oid, mfi_c2.mode,
                                          new_path2);
+               /*
+                * unpack_trees() actually populates the index for us for
+                * "normal" rename/rename(2to1) situtations so that the
+                * correct entries are at the higher stages, which would
+                * make the call below to update_stages_for_stage_data
+                * unnecessary.  However, if either of the renames came
+                * from a directory rename, then unpack_trees() will not
+                * have gotten the right data loaded into the index, so we
+                * need to do so now.  (While it'd be tempting to move this
+                * call to update_stages_for_stage_data() to
+                * apply_directory_rename_modifications(), that would break
+                * our intermediate calls to would_lose_untracked() since
+                * those rely on the current in-memory index.  See also the
+                * big "NOTE" in update_stages()).
+                */
+               if (update_stages_for_stage_data(o, path, ci->dst_entry1))
+                       ret = -1;
+
                free(new_path2);
                free(new_path1);
        }
@@ -1390,6 +1521,754 @@ static int conflict_rename_rename_2to1(struct merge_options *o,
        return ret;
 }
 
+/*
+ * Get the diff_filepairs changed between o_tree and tree.
+ */
+static struct diff_queue_struct *get_diffpairs(struct merge_options *o,
+                                              struct tree *o_tree,
+                                              struct tree *tree)
+{
+       struct diff_queue_struct *ret;
+       struct diff_options opts;
+
+       diff_setup(&opts);
+       opts.flags.recursive = 1;
+       opts.flags.rename_empty = 0;
+       opts.detect_rename = DIFF_DETECT_RENAME;
+       opts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :
+                           o->diff_rename_limit >= 0 ? o->diff_rename_limit :
+                           1000;
+       opts.rename_score = o->rename_score;
+       opts.show_rename_progress = o->show_rename_progress;
+       opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       diff_setup_done(&opts);
+       diff_tree_oid(&o_tree->object.oid, &tree->object.oid, "", &opts);
+       diffcore_std(&opts);
+       if (opts.needed_rename_limit > o->needed_rename_limit)
+               o->needed_rename_limit = opts.needed_rename_limit;
+
+       ret = xmalloc(sizeof(*ret));
+       *ret = diff_queued_diff;
+
+       opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+       diff_queued_diff.nr = 0;
+       diff_queued_diff.queue = NULL;
+       diff_flush(&opts);
+       return ret;
+}
+
+static int tree_has_path(struct tree *tree, const char *path)
+{
+       unsigned char hashy[GIT_MAX_RAWSZ];
+       unsigned int mode_o;
+
+       return !get_tree_entry(tree->object.oid.hash, path,
+                              hashy, &mode_o);
+}
+
+/*
+ * Return a new string that replaces the beginning portion (which matches
+ * entry->dir), with entry->new_dir.  In perl-speak:
+ *   new_path_name = (old_path =~ s/entry->dir/entry->new_dir/);
+ * NOTE:
+ *   Caller must ensure that old_path starts with entry->dir + '/'.
+ */
+static char *apply_dir_rename(struct dir_rename_entry *entry,
+                             const char *old_path)
+{
+       struct strbuf new_path = STRBUF_INIT;
+       int oldlen, newlen;
+
+       if (entry->non_unique_new_dir)
+               return NULL;
+
+       oldlen = strlen(entry->dir);
+       newlen = entry->new_dir.len + (strlen(old_path) - oldlen) + 1;
+       strbuf_grow(&new_path, newlen);
+       strbuf_addbuf(&new_path, &entry->new_dir);
+       strbuf_addstr(&new_path, &old_path[oldlen]);
+
+       return strbuf_detach(&new_path, NULL);
+}
+
+static void get_renamed_dir_portion(const char *old_path, const char *new_path,
+                                   char **old_dir, char **new_dir)
+{
+       char *end_of_old, *end_of_new;
+       int old_len, new_len;
+
+       *old_dir = NULL;
+       *new_dir = NULL;
+
+       /*
+        * For
+        *    "a/b/c/d/e/foo.c" -> "a/b/some/thing/else/e/foo.c"
+        * the "e/foo.c" part is the same, we just want to know that
+        *    "a/b/c/d" was renamed to "a/b/some/thing/else"
+        * so, for this example, this function returns "a/b/c/d" in
+        * *old_dir and "a/b/some/thing/else" in *new_dir.
+        *
+        * Also, if the basename of the file changed, we don't care.  We
+        * want to know which portion of the directory, if any, changed.
+        */
+       end_of_old = strrchr(old_path, '/');
+       end_of_new = strrchr(new_path, '/');
+
+       if (end_of_old == NULL || end_of_new == NULL)
+               return;
+       while (*--end_of_new == *--end_of_old &&
+              end_of_old != old_path &&
+              end_of_new != new_path)
+               ; /* Do nothing; all in the while loop */
+       /*
+        * We've found the first non-matching character in the directory
+        * paths.  That means the current directory we were comparing
+        * represents the rename.  Move end_of_old and end_of_new back
+        * to the full directory name.
+        */
+       if (*end_of_old == '/')
+               end_of_old++;
+       if (*end_of_old != '/')
+               end_of_new++;
+       end_of_old = strchr(end_of_old, '/');
+       end_of_new = strchr(end_of_new, '/');
+
+       /*
+        * It may have been the case that old_path and new_path were the same
+        * directory all along.  Don't claim a rename if they're the same.
+        */
+       old_len = end_of_old - old_path;
+       new_len = end_of_new - new_path;
+
+       if (old_len != new_len || strncmp(old_path, new_path, old_len)) {
+               *old_dir = xstrndup(old_path, old_len);
+               *new_dir = xstrndup(new_path, new_len);
+       }
+}
+
+static void remove_hashmap_entries(struct hashmap *dir_renames,
+                                  struct string_list *items_to_remove)
+{
+       int i;
+       struct dir_rename_entry *entry;
+
+       for (i = 0; i < items_to_remove->nr; i++) {
+               entry = items_to_remove->items[i].util;
+               hashmap_remove(dir_renames, entry, NULL);
+       }
+       string_list_clear(items_to_remove, 0);
+}
+
+/*
+ * See if there is a directory rename for path, and if there are any file
+ * level conflicts for the renamed location.  If there is a rename and
+ * there are no conflicts, return the new name.  Otherwise, return NULL.
+ */
+static char *handle_path_level_conflicts(struct merge_options *o,
+                                        const char *path,
+                                        struct dir_rename_entry *entry,
+                                        struct hashmap *collisions,
+                                        struct tree *tree)
+{
+       char *new_path = NULL;
+       struct collision_entry *collision_ent;
+       int clean = 1;
+       struct strbuf collision_paths = STRBUF_INIT;
+
+       /*
+        * entry has the mapping of old directory name to new directory name
+        * that we want to apply to path.
+        */
+       new_path = apply_dir_rename(entry, path);
+
+       if (!new_path) {
+               /* This should only happen when entry->non_unique_new_dir set */
+               if (!entry->non_unique_new_dir)
+                       BUG("entry->non_unqiue_dir not set and !new_path");
+               output(o, 1, _("CONFLICT (directory rename split): "
+                              "Unclear where to place %s because directory "
+                              "%s was renamed to multiple other directories, "
+                              "with no destination getting a majority of the "
+                              "files."),
+                      path, entry->dir);
+               clean = 0;
+               return NULL;
+       }
+
+       /*
+        * The caller needs to have ensured that it has pre-populated
+        * collisions with all paths that map to new_path.  Do a quick check
+        * to ensure that's the case.
+        */
+       collision_ent = collision_find_entry(collisions, new_path);
+       if (collision_ent == NULL)
+               BUG("collision_ent is NULL");
+
+       /*
+        * Check for one-sided add/add/.../add conflicts, i.e.
+        * where implicit renames from the other side doing
+        * directory rename(s) can affect this side of history
+        * to put multiple paths into the same location.  Warn
+        * and bail on directory renames for such paths.
+        */
+       if (collision_ent->reported_already) {
+               clean = 0;
+       } else if (tree_has_path(tree, new_path)) {
+               collision_ent->reported_already = 1;
+               strbuf_add_separated_string_list(&collision_paths, ", ",
+                                                &collision_ent->source_files);
+               output(o, 1, _("CONFLICT (implicit dir rename): Existing "
+                              "file/dir at %s in the way of implicit "
+                              "directory rename(s) putting the following "
+                              "path(s) there: %s."),
+                      new_path, collision_paths.buf);
+               clean = 0;
+       } else if (collision_ent->source_files.nr > 1) {
+               collision_ent->reported_already = 1;
+               strbuf_add_separated_string_list(&collision_paths, ", ",
+                                                &collision_ent->source_files);
+               output(o, 1, _("CONFLICT (implicit dir rename): Cannot map "
+                              "more than one path to %s; implicit directory "
+                              "renames tried to put these paths there: %s"),
+                      new_path, collision_paths.buf);
+               clean = 0;
+       }
+
+       /* Free memory we no longer need */
+       strbuf_release(&collision_paths);
+       if (!clean && new_path) {
+               free(new_path);
+               return NULL;
+       }
+
+       return new_path;
+}
+
+/*
+ * There are a couple things we want to do at the directory level:
+ *   1. Check for both sides renaming to the same thing, in order to avoid
+ *      implicit renaming of files that should be left in place.  (See
+ *      testcase 6b in t6043 for details.)
+ *   2. Prune directory renames if there are still files left in the
+ *      the original directory.  These represent a partial directory rename,
+ *      i.e. a rename where only some of the files within the directory
+ *      were renamed elsewhere.  (Technically, this could be done earlier
+ *      in get_directory_renames(), except that would prevent us from
+ *      doing the previous check and thus failing testcase 6b.)
+ *   3. Check for rename/rename(1to2) conflicts (at the directory level).
+ *      In the future, we could potentially record this info as well and
+ *      omit reporting rename/rename(1to2) conflicts for each path within
+ *      the affected directories, thus cleaning up the merge output.
+ *   NOTE: We do NOT check for rename/rename(2to1) conflicts at the
+ *         directory level, because merging directories is fine.  If it
+ *         causes conflicts for files within those merged directories, then
+ *         that should be detected at the individual path level.
+ */
+static void handle_directory_level_conflicts(struct merge_options *o,
+                                            struct hashmap *dir_re_head,
+                                            struct tree *head,
+                                            struct hashmap *dir_re_merge,
+                                            struct tree *merge)
+{
+       struct hashmap_iter iter;
+       struct dir_rename_entry *head_ent;
+       struct dir_rename_entry *merge_ent;
+
+       struct string_list remove_from_head = STRING_LIST_INIT_NODUP;
+       struct string_list remove_from_merge = STRING_LIST_INIT_NODUP;
+
+       hashmap_iter_init(dir_re_head, &iter);
+       while ((head_ent = hashmap_iter_next(&iter))) {
+               merge_ent = dir_rename_find_entry(dir_re_merge, head_ent->dir);
+               if (merge_ent &&
+                   !head_ent->non_unique_new_dir &&
+                   !merge_ent->non_unique_new_dir &&
+                   !strbuf_cmp(&head_ent->new_dir, &merge_ent->new_dir)) {
+                       /* 1. Renamed identically; remove it from both sides */
+                       string_list_append(&remove_from_head,
+                                          head_ent->dir)->util = head_ent;
+                       strbuf_release(&head_ent->new_dir);
+                       string_list_append(&remove_from_merge,
+                                          merge_ent->dir)->util = merge_ent;
+                       strbuf_release(&merge_ent->new_dir);
+               } else if (tree_has_path(head, head_ent->dir)) {
+                       /* 2. This wasn't a directory rename after all */
+                       string_list_append(&remove_from_head,
+                                          head_ent->dir)->util = head_ent;
+                       strbuf_release(&head_ent->new_dir);
+               }
+       }
+
+       remove_hashmap_entries(dir_re_head, &remove_from_head);
+       remove_hashmap_entries(dir_re_merge, &remove_from_merge);
+
+       hashmap_iter_init(dir_re_merge, &iter);
+       while ((merge_ent = hashmap_iter_next(&iter))) {
+               head_ent = dir_rename_find_entry(dir_re_head, merge_ent->dir);
+               if (tree_has_path(merge, merge_ent->dir)) {
+                       /* 2. This wasn't a directory rename after all */
+                       string_list_append(&remove_from_merge,
+                                          merge_ent->dir)->util = merge_ent;
+               } else if (head_ent &&
+                          !head_ent->non_unique_new_dir &&
+                          !merge_ent->non_unique_new_dir) {
+                       /* 3. rename/rename(1to2) */
+                       /*
+                        * We can assume it's not rename/rename(1to1) because
+                        * that was case (1), already checked above.  So we
+                        * know that head_ent->new_dir and merge_ent->new_dir
+                        * are different strings.
+                        */
+                       output(o, 1, _("CONFLICT (rename/rename): "
+                                      "Rename directory %s->%s in %s. "
+                                      "Rename directory %s->%s in %s"),
+                              head_ent->dir, head_ent->new_dir.buf, o->branch1,
+                              head_ent->dir, merge_ent->new_dir.buf, o->branch2);
+                       string_list_append(&remove_from_head,
+                                          head_ent->dir)->util = head_ent;
+                       strbuf_release(&head_ent->new_dir);
+                       string_list_append(&remove_from_merge,
+                                          merge_ent->dir)->util = merge_ent;
+                       strbuf_release(&merge_ent->new_dir);
+               }
+       }
+
+       remove_hashmap_entries(dir_re_head, &remove_from_head);
+       remove_hashmap_entries(dir_re_merge, &remove_from_merge);
+}
+
+static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,
+                                            struct tree *tree)
+{
+       struct hashmap *dir_renames;
+       struct hashmap_iter iter;
+       struct dir_rename_entry *entry;
+       int i;
+
+       /*
+        * Typically, we think of a directory rename as all files from a
+        * certain directory being moved to a target directory.  However,
+        * what if someone first moved two files from the original
+        * directory in one commit, and then renamed the directory
+        * somewhere else in a later commit?  At merge time, we just know
+        * that files from the original directory went to two different
+        * places, and that the bulk of them ended up in the same place.
+        * We want each directory rename to represent where the bulk of the
+        * files from that directory end up; this function exists to find
+        * where the bulk of the files went.
+        *
+        * The first loop below simply iterates through the list of file
+        * renames, finding out how often each directory rename pair
+        * possibility occurs.
+        */
+       dir_renames = xmalloc(sizeof(struct hashmap));
+       dir_rename_init(dir_renames);
+       for (i = 0; i < pairs->nr; ++i) {
+               struct string_list_item *item;
+               int *count;
+               struct diff_filepair *pair = pairs->queue[i];
+               char *old_dir, *new_dir;
+
+               /* File not part of directory rename if it wasn't renamed */
+               if (pair->status != 'R')
+                       continue;
+
+               get_renamed_dir_portion(pair->one->path, pair->two->path,
+                                       &old_dir,        &new_dir);
+               if (!old_dir)
+                       /* Directory didn't change at all; ignore this one. */
+                       continue;
+
+               entry = dir_rename_find_entry(dir_renames, old_dir);
+               if (!entry) {
+                       entry = xmalloc(sizeof(struct dir_rename_entry));
+                       dir_rename_entry_init(entry, old_dir);
+                       hashmap_put(dir_renames, entry);
+               } else {
+                       free(old_dir);
+               }
+               item = string_list_lookup(&entry->possible_new_dirs, new_dir);
+               if (!item) {
+                       item = string_list_insert(&entry->possible_new_dirs,
+                                                 new_dir);
+                       item->util = xcalloc(1, sizeof(int));
+               } else {
+                       free(new_dir);
+               }
+               count = item->util;
+               *count += 1;
+       }
+
+       /*
+        * For each directory with files moved out of it, we find out which
+        * target directory received the most files so we can declare it to
+        * be the "winning" target location for the directory rename.  This
+        * winner gets recorded in new_dir.  If there is no winner
+        * (multiple target directories received the same number of files),
+        * we set non_unique_new_dir.  Once we've determined the winner (or
+        * that there is no winner), we no longer need possible_new_dirs.
+        */
+       hashmap_iter_init(dir_renames, &iter);
+       while ((entry = hashmap_iter_next(&iter))) {
+               int max = 0;
+               int bad_max = 0;
+               char *best = NULL;
+
+               for (i = 0; i < entry->possible_new_dirs.nr; i++) {
+                       int *count = entry->possible_new_dirs.items[i].util;
+
+                       if (*count == max)
+                               bad_max = max;
+                       else if (*count > max) {
+                               max = *count;
+                               best = entry->possible_new_dirs.items[i].string;
+                       }
+               }
+               if (bad_max == max)
+                       entry->non_unique_new_dir = 1;
+               else {
+                       assert(entry->new_dir.len == 0);
+                       strbuf_addstr(&entry->new_dir, best);
+               }
+               /*
+                * The relevant directory sub-portion of the original full
+                * filepaths were xstrndup'ed before inserting into
+                * possible_new_dirs, and instead of manually iterating the
+                * list and free'ing each, just lie and tell
+                * possible_new_dirs that it did the strdup'ing so that it
+                * will free them for us.
+                */
+               entry->possible_new_dirs.strdup_strings = 1;
+               string_list_clear(&entry->possible_new_dirs, 1);
+       }
+
+       return dir_renames;
+}
+
+static struct dir_rename_entry *check_dir_renamed(const char *path,
+                                                 struct hashmap *dir_renames)
+{
+       char temp[PATH_MAX];
+       char *end;
+       struct dir_rename_entry *entry;
+
+       strcpy(temp, path);
+       while ((end = strrchr(temp, '/'))) {
+               *end = '\0';
+               entry = dir_rename_find_entry(dir_renames, temp);
+               if (entry)
+                       return entry;
+       }
+       return NULL;
+}
+
+static void compute_collisions(struct hashmap *collisions,
+                              struct hashmap *dir_renames,
+                              struct diff_queue_struct *pairs)
+{
+       int i;
+
+       /*
+        * Multiple files can be mapped to the same path due to directory
+        * renames done by the other side of history.  Since that other
+        * side of history could have merged multiple directories into one,
+        * if our side of history added the same file basename to each of
+        * those directories, then all N of them would get implicitly
+        * renamed by the directory rename detection into the same path,
+        * and we'd get an add/add/.../add conflict, and all those adds
+        * from *this* side of history.  This is not representable in the
+        * index, and users aren't going to easily be able to make sense of
+        * it.  So we need to provide a good warning about what's
+        * happening, and fall back to no-directory-rename detection
+        * behavior for those paths.
+        *
+        * See testcases 9e and all of section 5 from t6043 for examples.
+        */
+       collision_init(collisions);
+
+       for (i = 0; i < pairs->nr; ++i) {
+               struct dir_rename_entry *dir_rename_ent;
+               struct collision_entry *collision_ent;
+               char *new_path;
+               struct diff_filepair *pair = pairs->queue[i];
+
+               if (pair->status != 'A' && pair->status != 'R')
+                       continue;
+               dir_rename_ent = check_dir_renamed(pair->two->path,
+                                                  dir_renames);
+               if (!dir_rename_ent)
+                       continue;
+
+               new_path = apply_dir_rename(dir_rename_ent, pair->two->path);
+               if (!new_path)
+                       /*
+                        * dir_rename_ent->non_unique_new_path is true, which
+                        * means there is no directory rename for us to use,
+                        * which means it won't cause us any additional
+                        * collisions.
+                        */
+                       continue;
+               collision_ent = collision_find_entry(collisions, new_path);
+               if (!collision_ent) {
+                       collision_ent = xcalloc(1,
+                                               sizeof(struct collision_entry));
+                       hashmap_entry_init(collision_ent, strhash(new_path));
+                       hashmap_put(collisions, collision_ent);
+                       collision_ent->target_file = new_path;
+               } else {
+                       free(new_path);
+               }
+               string_list_insert(&collision_ent->source_files,
+                                  pair->two->path);
+       }
+}
+
+static char *check_for_directory_rename(struct merge_options *o,
+                                       const char *path,
+                                       struct tree *tree,
+                                       struct hashmap *dir_renames,
+                                       struct hashmap *dir_rename_exclusions,
+                                       struct hashmap *collisions,
+                                       int *clean_merge)
+{
+       char *new_path = NULL;
+       struct dir_rename_entry *entry = check_dir_renamed(path, dir_renames);
+       struct dir_rename_entry *oentry = NULL;
+
+       if (!entry)
+               return new_path;
+
+       /*
+        * This next part is a little weird.  We do not want to do an
+        * implicit rename into a directory we renamed on our side, because
+        * that will result in a spurious rename/rename(1to2) conflict.  An
+        * example:
+        *   Base commit: dumbdir/afile, otherdir/bfile
+        *   Side 1:      smrtdir/afile, otherdir/bfile
+        *   Side 2:      dumbdir/afile, dumbdir/bfile
+        * Here, while working on Side 1, we could notice that otherdir was
+        * renamed/merged to dumbdir, and change the diff_filepair for
+        * otherdir/bfile into a rename into dumbdir/bfile.  However, Side
+        * 2 will notice the rename from dumbdir to smrtdir, and do the
+        * transitive rename to move it from dumbdir/bfile to
+        * smrtdir/bfile.  That gives us bfile in dumbdir vs being in
+        * smrtdir, a rename/rename(1to2) conflict.  We really just want
+        * the file to end up in smrtdir.  And the way to achieve that is
+        * to not let Side1 do the rename to dumbdir, since we know that is
+        * the source of one of our directory renames.
+        *
+        * That's why oentry and dir_rename_exclusions is here.
+        *
+        * As it turns out, this also prevents N-way transient rename
+        * confusion; See testcases 9c and 9d of t6043.
+        */
+       oentry = dir_rename_find_entry(dir_rename_exclusions, entry->new_dir.buf);
+       if (oentry) {
+               output(o, 1, _("WARNING: Avoiding applying %s -> %s rename "
+                              "to %s, because %s itself was renamed."),
+                      entry->dir, entry->new_dir.buf, path, entry->new_dir.buf);
+       } else {
+               new_path = handle_path_level_conflicts(o, path, entry,
+                                                      collisions, tree);
+               *clean_merge &= (new_path != NULL);
+       }
+
+       return new_path;
+}
+
+static void apply_directory_rename_modifications(struct merge_options *o,
+                                                struct diff_filepair *pair,
+                                                char *new_path,
+                                                struct rename *re,
+                                                struct tree *tree,
+                                                struct tree *o_tree,
+                                                struct tree *a_tree,
+                                                struct tree *b_tree,
+                                                struct string_list *entries,
+                                                int *clean)
+{
+       struct string_list_item *item;
+       int stage = (tree == a_tree ? 2 : 3);
+       int update_wd;
+
+       /*
+        * In all cases where we can do directory rename detection,
+        * unpack_trees() will have read pair->two->path into the
+        * index and the working copy.  We need to remove it so that
+        * we can instead place it at new_path.  It is guaranteed to
+        * not be untracked (unpack_trees() would have errored out
+        * saying the file would have been overwritten), but it might
+        * be dirty, though.
+        */
+       update_wd = !was_dirty(o, pair->two->path);
+       if (!update_wd)
+               output(o, 1, _("Refusing to lose dirty file at %s"),
+                      pair->two->path);
+       remove_file(o, 1, pair->two->path, !update_wd);
+
+       /* Find or create a new re->dst_entry */
+       item = string_list_lookup(entries, new_path);
+       if (item) {
+               /*
+                * Since we're renaming on this side of history, and it's
+                * due to a directory rename on the other side of history
+                * (which we only allow when the directory in question no
+                * longer exists on the other side of history), the
+                * original entry for re->dst_entry is no longer
+                * necessary...
+                */
+               re->dst_entry->processed = 1;
+
+               /*
+                * ...because we'll be using this new one.
+                */
+               re->dst_entry = item->util;
+       } else {
+               /*
+                * re->dst_entry is for the before-dir-rename path, and we
+                * need it to hold information for the after-dir-rename
+                * path.  Before creating a new entry, we need to mark the
+                * old one as unnecessary (...unless it is shared by
+                * src_entry, i.e. this didn't use to be a rename, in which
+                * case we can just allow the normal processing to happen
+                * for it).
+                */
+               if (pair->status == 'R')
+                       re->dst_entry->processed = 1;
+
+               re->dst_entry = insert_stage_data(new_path,
+                                                 o_tree, a_tree, b_tree,
+                                                 entries);
+               item = string_list_insert(entries, new_path);
+               item->util = re->dst_entry;
+       }
+
+       /*
+        * Update the stage_data with the information about the path we are
+        * moving into place.  That slot will be empty and available for us
+        * to write to because of the collision checks in
+        * handle_path_level_conflicts().  In other words,
+        * re->dst_entry->stages[stage].oid will be the null_oid, so it's
+        * open for us to write to.
+        *
+        * It may be tempting to actually update the index at this point as
+        * well, using update_stages_for_stage_data(), but as per the big
+        * "NOTE" in update_stages(), doing so will modify the current
+        * in-memory index which will break calls to would_lose_untracked()
+        * that we need to make.  Instead, we need to just make sure that
+        * the various conflict_rename_*() functions update the index
+        * explicitly rather than relying on unpack_trees() to have done it.
+        */
+       get_tree_entry(tree->object.oid.hash,
+                      pair->two->path,
+                      re->dst_entry->stages[stage].oid.hash,
+                      &re->dst_entry->stages[stage].mode);
+
+       /* Update pair status */
+       if (pair->status == 'A') {
+               /*
+                * Recording rename information for this add makes it look
+                * like a rename/delete conflict.  Make sure we can
+                * correctly handle this as an add that was moved to a new
+                * directory instead of reporting a rename/delete conflict.
+                */
+               re->add_turned_into_rename = 1;
+       }
+       /*
+        * We don't actually look at pair->status again, but it seems
+        * pedagogically correct to adjust it.
+        */
+       pair->status = 'R';
+
+       /*
+        * Finally, record the new location.
+        */
+       pair->two->path = new_path;
+}
+
+/*
+ * Get information of all renames which occurred in 'pairs', making use of
+ * any implicit directory renames inferred from the other side of history.
+ * We need the three trees in the merge ('o_tree', 'a_tree' and 'b_tree')
+ * to be able to associate the correct cache entries with the rename
+ * information; tree is always equal to either a_tree or b_tree.
+ */
+static struct string_list *get_renames(struct merge_options *o,
+                                      struct diff_queue_struct *pairs,
+                                      struct hashmap *dir_renames,
+                                      struct hashmap *dir_rename_exclusions,
+                                      struct tree *tree,
+                                      struct tree *o_tree,
+                                      struct tree *a_tree,
+                                      struct tree *b_tree,
+                                      struct string_list *entries,
+                                      int *clean_merge)
+{
+       int i;
+       struct hashmap collisions;
+       struct hashmap_iter iter;
+       struct collision_entry *e;
+       struct string_list *renames;
+
+       compute_collisions(&collisions, dir_renames, pairs);
+       renames = xcalloc(1, sizeof(struct string_list));
+
+       for (i = 0; i < pairs->nr; ++i) {
+               struct string_list_item *item;
+               struct rename *re;
+               struct diff_filepair *pair = pairs->queue[i];
+               char *new_path; /* non-NULL only with directory renames */
+
+               if (pair->status != 'A' && pair->status != 'R') {
+                       diff_free_filepair(pair);
+                       continue;
+               }
+               new_path = check_for_directory_rename(o, pair->two->path, tree,
+                                                     dir_renames,
+                                                     dir_rename_exclusions,
+                                                     &collisions,
+                                                     clean_merge);
+               if (pair->status != 'R' && !new_path) {
+                       diff_free_filepair(pair);
+                       continue;
+               }
+
+               re = xmalloc(sizeof(*re));
+               re->processed = 0;
+               re->add_turned_into_rename = 0;
+               re->pair = pair;
+               item = string_list_lookup(entries, re->pair->one->path);
+               if (!item)
+                       re->src_entry = insert_stage_data(re->pair->one->path,
+                                       o_tree, a_tree, b_tree, entries);
+               else
+                       re->src_entry = item->util;
+
+               item = string_list_lookup(entries, re->pair->two->path);
+               if (!item)
+                       re->dst_entry = insert_stage_data(re->pair->two->path,
+                                       o_tree, a_tree, b_tree, entries);
+               else
+                       re->dst_entry = item->util;
+               item = string_list_insert(renames, pair->one->path);
+               item->util = re;
+               if (new_path)
+                       apply_directory_rename_modifications(o, pair, new_path,
+                                                            re, tree, o_tree,
+                                                            a_tree, b_tree,
+                                                            entries,
+                                                            clean_merge);
+       }
+
+       hashmap_iter_init(&collisions, &iter);
+       while ((e = hashmap_iter_next(&iter))) {
+               free(e->target_file);
+               string_list_clear(&e->source_files, 0);
+       }
+       hashmap_free(&collisions, 1);
+       return renames;
+}
+
 static int process_renames(struct merge_options *o,
                           struct string_list *a_renames,
                           struct string_list *b_renames)
@@ -1548,7 +2427,19 @@ static int process_renames(struct merge_options *o,
                        dst_other.mode = ren1->dst_entry->stages[other_stage].mode;
                        try_merge = 0;
 
-                       if (oid_eq(&src_other.oid, &null_oid)) {
+                       if (oid_eq(&src_other.oid, &null_oid) &&
+                           ren1->add_turned_into_rename) {
+                               setup_rename_conflict_info(RENAME_DIR,
+                                                          ren1->pair,
+                                                          NULL,
+                                                          branch1,
+                                                          branch2,
+                                                          ren1->dst_entry,
+                                                          NULL,
+                                                          o,
+                                                          NULL,
+                                                          NULL);
+                       } else if (oid_eq(&src_other.oid, &null_oid)) {
                                setup_rename_conflict_info(RENAME_DELETE,
                                                           ren1->pair,
                                                           NULL,
@@ -1645,6 +2536,105 @@ cleanup_and_return:
        return clean_merge;
 }
 
+struct rename_info {
+       struct string_list *head_renames;
+       struct string_list *merge_renames;
+};
+
+static void initial_cleanup_rename(struct diff_queue_struct *pairs,
+                                  struct hashmap *dir_renames)
+{
+       struct hashmap_iter iter;
+       struct dir_rename_entry *e;
+
+       hashmap_iter_init(dir_renames, &iter);
+       while ((e = hashmap_iter_next(&iter))) {
+               free(e->dir);
+               strbuf_release(&e->new_dir);
+               /* possible_new_dirs already cleared in get_directory_renames */
+       }
+       hashmap_free(dir_renames, 1);
+       free(dir_renames);
+
+       free(pairs->queue);
+       free(pairs);
+}
+
+static int handle_renames(struct merge_options *o,
+                         struct tree *common,
+                         struct tree *head,
+                         struct tree *merge,
+                         struct string_list *entries,
+                         struct rename_info *ri)
+{
+       struct diff_queue_struct *head_pairs, *merge_pairs;
+       struct hashmap *dir_re_head, *dir_re_merge;
+       int clean = 1;
+
+       ri->head_renames = NULL;
+       ri->merge_renames = NULL;
+
+       if (!o->detect_rename)
+               return 1;
+
+       head_pairs = get_diffpairs(o, common, head);
+       merge_pairs = get_diffpairs(o, common, merge);
+
+       dir_re_head = get_directory_renames(head_pairs, head);
+       dir_re_merge = get_directory_renames(merge_pairs, merge);
+
+       handle_directory_level_conflicts(o,
+                                        dir_re_head, head,
+                                        dir_re_merge, merge);
+
+       ri->head_renames  = get_renames(o, head_pairs,
+                                       dir_re_merge, dir_re_head, head,
+                                       common, head, merge, entries,
+                                       &clean);
+       if (clean < 0)
+               goto cleanup;
+       ri->merge_renames = get_renames(o, merge_pairs,
+                                       dir_re_head, dir_re_merge, merge,
+                                       common, head, merge, entries,
+                                       &clean);
+       if (clean < 0)
+               goto cleanup;
+       clean &= process_renames(o, ri->head_renames, ri->merge_renames);
+
+cleanup:
+       /*
+        * Some cleanup is deferred until cleanup_renames() because the
+        * data structures are still needed and referenced in
+        * process_entry().  But there are a few things we can free now.
+        */
+       initial_cleanup_rename(head_pairs, dir_re_head);
+       initial_cleanup_rename(merge_pairs, dir_re_merge);
+
+       return clean;
+}
+
+static void final_cleanup_rename(struct string_list *rename)
+{
+       const struct rename *re;
+       int i;
+
+       if (rename == NULL)
+               return;
+
+       for (i = 0; i < rename->nr; i++) {
+               re = rename->items[i].util;
+               diff_free_filepair(re->pair);
+       }
+       string_list_clear(rename, 1);
+       free(rename);
+}
+
+static void final_cleanup_renames(struct rename_info *re_info)
+{
+       final_cleanup_rename(re_info->head_renames);
+       final_cleanup_rename(re_info->merge_renames);
+}
+
 static struct object_id *stage_oid(const struct object_id *oid, unsigned mode)
 {
        return (is_null_oid(oid) || mode == 0) ? NULL: (struct object_id *)oid;
@@ -1735,6 +2725,7 @@ static int handle_modify_delete(struct merge_options *o,
 
 static int merge_content(struct merge_options *o,
                         const char *path,
+                        int file_in_way,
                         struct object_id *o_oid, int o_mode,
                         struct object_id *a_oid, int a_mode,
                         struct object_id *b_oid, int b_mode,
@@ -1782,7 +2773,6 @@ static int merge_content(struct merge_options *o,
 
        if (mfi.clean && !df_conflict_remains &&
            oid_eq(&mfi.oid, a_oid) && mfi.mode == a_mode) {
-               int path_renamed_outside_HEAD;
                output(o, 3, _("Skipped %s (merged same as existing)"), path);
                /*
                 * The content merge resulted in the same file contents we
@@ -1790,8 +2780,7 @@ static int merge_content(struct merge_options *o,
                 * are recorded at the correct path (which may not be true
                 * if the merge involves a rename).
                 */
-               path_renamed_outside_HEAD = !path2 || !strcmp(path, path2);
-               if (!path_renamed_outside_HEAD) {
+               if (was_tracked(path)) {
                        add_cacheinfo(o, mfi.mode, &mfi.oid, path,
                                      0, (!o->call_depth), 0);
                        return mfi.clean;
@@ -1809,7 +2798,7 @@ static int merge_content(struct merge_options *o,
                                return -1;
        }
 
-       if (df_conflict_remains) {
+       if (df_conflict_remains || file_in_way) {
                char *new_path;
                if (o->call_depth) {
                        remove_file_from_cache(path);
@@ -1843,6 +2832,30 @@ static int merge_content(struct merge_options *o,
        return mfi.clean;
 }
 
+static int conflict_rename_normal(struct merge_options *o,
+                                 const char *path,
+                                 struct object_id *o_oid, unsigned int o_mode,
+                                 struct object_id *a_oid, unsigned int a_mode,
+                                 struct object_id *b_oid, unsigned int b_mode,
+                                 struct rename_conflict_info *ci)
+{
+       int clean_merge;
+       int file_in_the_way = 0;
+
+       if (was_dirty(o, path)) {
+               file_in_the_way = 1;
+               output(o, 1, _("Refusing to lose dirty file at %s"), path);
+       }
+
+       /* Merge the content and write it out */
+       clean_merge = merge_content(o, path, file_in_the_way,
+                                   o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
+                                   ci);
+       if (clean_merge > 0 && file_in_the_way)
+               clean_merge = 0;
+       return clean_merge;
+}
+
 /* Per entry merge function */
 static int process_entry(struct merge_options *o,
                         const char *path, struct stage_data *entry)
@@ -1862,9 +2875,20 @@ static int process_entry(struct merge_options *o,
                switch (conflict_info->rename_type) {
                case RENAME_NORMAL:
                case RENAME_ONE_FILE_TO_ONE:
-                       clean_merge = merge_content(o, path,
-                                                   o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
-                                                   conflict_info);
+                       clean_merge = conflict_rename_normal(o,
+                                                            path,
+                                                            o_oid, o_mode,
+                                                            a_oid, a_mode,
+                                                            b_oid, b_mode,
+                                                            conflict_info);
+                       break;
+               case RENAME_DIR:
+                       clean_merge = 1;
+                       if (conflict_rename_dir(o,
+                                               conflict_info->pair1,
+                                               conflict_info->branch1,
+                                               conflict_info->branch2))
+                               clean_merge = -1;
                        break;
                case RENAME_DELETE:
                        clean_merge = 0;
@@ -1952,7 +2976,7 @@ static int process_entry(struct merge_options *o,
        } else if (a_oid && b_oid) {
                /* Case C: Added in both (check for same permissions) and */
                /* case D: Modified in both, but differently. */
-               clean_merge = merge_content(o, path,
+               clean_merge = merge_content(o, path, 0 /* file_in_way */,
                                            o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
                                            NULL);
        } else if (!o_oid && !a_oid && !b_oid) {
@@ -1993,7 +3017,7 @@ int merge_trees(struct merge_options *o,
                return 1;
        }
 
-       code = git_merge_trees(o->call_depth, common, head, merge);
+       code = git_merge_trees(o, common, head, merge);
 
        if (code != 0) {
                if (show(o, 4) || o->call_depth)
@@ -2004,7 +3028,8 @@ int merge_trees(struct merge_options *o,
        }
 
        if (unmerged_cache()) {
-               struct string_list *entries, *re_head, *re_merge;
+               struct string_list *entries;
+               struct rename_info re_info;
                int i;
                /*
                 * Only need the hashmap while processing entries, so
@@ -2018,9 +3043,8 @@ int merge_trees(struct merge_options *o,
                get_files_dirs(o, merge);
 
                entries = get_unmerged();
-               re_head  = get_renames(o, head, common, head, merge, entries);
-               re_merge = get_renames(o, merge, common, head, merge, entries);
-               clean = process_renames(o, re_head, re_merge);
+               clean = handle_renames(o, common, head, merge, entries,
+                                      &re_info);
                record_df_conflict_files(o, entries);
                if (clean < 0)
                        goto cleanup;
@@ -2045,16 +3069,13 @@ int merge_trees(struct merge_options *o,
                }
 
 cleanup:
-               string_list_clear(re_merge, 0);
-               string_list_clear(re_head, 0);
+               final_cleanup_renames(&re_info);
+
                string_list_clear(entries, 1);
+               free(entries);
 
                hashmap_free(&o->current_file_dir_set, 1);
 
-               free(re_merge);
-               free(re_head);
-               free(entries);
-
                if (clean < 0)
                        return clean;
        }
index 80d69d140195cc3ba1054050569e56bfc0277b56..d863cf88676ef321aad85d395f964efe5673a805 100644 (file)
@@ -1,6 +1,7 @@
 #ifndef MERGE_RECURSIVE_H
 #define MERGE_RECURSIVE_H
 
+#include "unpack-trees.h"
 #include "string-list.h"
 
 struct merge_options {
@@ -27,6 +28,32 @@ struct merge_options {
        struct strbuf obuf;
        struct hashmap current_file_dir_set;
        struct string_list df_conflict_file_set;
+       struct unpack_trees_options unpack_opts;
+};
+
+/*
+ * For dir_rename_entry, directory names are stored as a full path from the
+ * toplevel of the repository and do not include a trailing '/'.  Also:
+ *
+ *   dir:                original name of directory being renamed
+ *   non_unique_new_dir: if true, could not determine new_dir
+ *   new_dir:            final name of directory being renamed
+ *   possible_new_dirs:  temporary used to help determine new_dir; see comments
+ *                       in get_directory_renames() for details
+ */
+struct dir_rename_entry {
+       struct hashmap_entry ent; /* must be the first member! */
+       char *dir;
+       unsigned non_unique_new_dir:1;
+       struct strbuf new_dir;
+       struct string_list possible_new_dirs;
+};
+
+struct collision_entry {
+       struct hashmap_entry ent; /* must be the first member! */
+       char *target_file;
+       struct string_list source_files;
+       unsigned reported_already:1;
 };
 
 /* merge_trees() but with recursive ancestor consolidation */
index 0759590b3e56de0fab6ff34f25206fb4cdeca4c5..013fc673fa421d6c5abdc40d3ce3c48528464724 100644 (file)
--- a/strbuf.c
+++ b/strbuf.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "refs.h"
+#include "string-list.h"
 #include "utf8.h"
 
 int starts_with(const char *str, const char *prefix)
@@ -171,6 +172,21 @@ struct strbuf **strbuf_split_buf(const char *str, size_t slen,
        return ret;
 }
 
+void strbuf_add_separated_string_list(struct strbuf *str,
+                                     const char *sep,
+                                     struct string_list *slist)
+{
+       struct string_list_item *item;
+       int sep_needed = 0;
+
+       for_each_string_list_item(item, slist) {
+               if (sep_needed)
+                       strbuf_addstr(str, sep);
+               strbuf_addstr(str, item->string);
+               sep_needed = 1;
+       }
+}
+
 void strbuf_list_free(struct strbuf **sbs)
 {
        struct strbuf **s = sbs;
index e6cae5f4398c8eb4c4a53c7db226a920e32bfde9..652f17392c15993272db5891227ec25b3c78d8bd 100644 (file)
--- a/strbuf.h
+++ b/strbuf.h
@@ -1,6 +1,8 @@
 #ifndef STRBUF_H
 #define STRBUF_H
 
+struct string_list;
+
 /**
  * strbuf's are meant to be used with all the usual C string and memory
  * APIs. Given that the length of the buffer is known, it's often better to
@@ -531,6 +533,20 @@ static inline struct strbuf **strbuf_split(const struct strbuf *sb,
        return strbuf_split_max(sb, terminator, 0);
 }
 
+/*
+ * Adds all strings of a string list to the strbuf, separated by the given
+ * separator.  For example, if sep is
+ *   ', '
+ * and slist contains
+ *   ['element1', 'element2', ..., 'elementN'],
+ * then write:
+ *   'element1, element2, ..., elementN'
+ * to str.  If only one element, just write "element1" to str.
+ */
+extern void strbuf_add_separated_string_list(struct strbuf *str,
+                                            const char *sep,
+                                            struct string_list *slist);
+
 /**
  * Free a NULL-terminated list of strbufs (for example, the return
  * values of the strbuf_split*() functions).
index 783bdbf59db07ebf2afac83165d31b86fd6b95db..0d89f6d0f651e46251c9c3c610f711353c701893 100755 (executable)
@@ -141,7 +141,7 @@ test_expect_success 'cherry-pick "-" works with arguments' '
        test_cmp expect actual
 '
 
-test_expect_failure 'cherry-pick works with dirty renamed file' '
+test_expect_success 'cherry-pick works with dirty renamed file' '
        test_commit to-rename &&
        git checkout -b unrelated &&
        test_commit unrelated &&
diff --git a/t/t6043-merge-rename-directories.sh b/t/t6043-merge-rename-directories.sh
new file mode 100755 (executable)
index 0000000..2e28f29
--- /dev/null
@@ -0,0 +1,3998 @@
+#!/bin/sh
+
+test_description="recursive merge with directory renames"
+# includes checking of many corner cases, with a similar methodology to:
+#   t6042: corner cases with renames but not criss-cross merges
+#   t6036: corner cases with both renames and criss-cross merges
+#
+# The setup for all of them, pictorially, is:
+#
+#      A
+#      o
+#     / \
+#  O o   ?
+#     \ /
+#      o
+#      B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+#    z/{b,c}   means  files z/b and z/c both exist
+#    x/d_1     means  file x/d exists with content d1.  (Purpose of the
+#                     underscore notation is to differentiate different
+#                     files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+
+
+###########################################################################
+# SECTION 1: Basic cases we should be able to handle
+###########################################################################
+
+# Testcase 1a, Basic directory rename.
+#   Commit O: z/{b,c}
+#   Commit A: y/{b,c}
+#   Commit B: z/{b,c,d,e/f}
+#   Expected: y/{b,c,d,e/f}
+
+test_expect_success '1a-setup: Simple directory rename detection' '
+       test_create_repo 1a &&
+       (
+               cd 1a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo d >z/d &&
+               mkdir z/e &&
+               echo f >z/e/f &&
+               git add z/d z/e/f &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '1a-check: Simple directory rename detection' '
+       (
+               cd 1a &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e/f &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    B:z/d    B:z/e/f &&
+               test_cmp expect actual &&
+
+               git hash-object y/d >actual &&
+               git rev-parse B:z/d >expect &&
+               test_cmp expect actual &&
+
+               test_must_fail git rev-parse HEAD:z/d &&
+               test_must_fail git rev-parse HEAD:z/e/f &&
+               test_path_is_missing z/d &&
+               test_path_is_missing z/e/f
+       )
+'
+
+# Testcase 1b, Merge a directory with another
+#   Commit O: z/{b,c},   y/d
+#   Commit A: z/{b,c,e}, y/d
+#   Commit B: y/{b,c,d}
+#   Expected: y/{b,c,d,e}
+
+test_expect_success '1b-setup: Merge a directory with another' '
+       test_create_repo 1b &&
+       (
+               cd 1b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir y &&
+               echo d >y/d &&
+               git add z y &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               echo e >z/e &&
+               git add z/e &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z/b y &&
+               git mv z/c y &&
+               rmdir z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '1b-check: Merge a directory with another' '
+       (
+               cd 1b &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:y/d    A:z/e &&
+               test_cmp expect actual &&
+               test_must_fail git rev-parse HEAD:z/e
+       )
+'
+
+# Testcase 1c, Transitive renaming
+#   (Related to testcases 3a and 6d -- when should a transitive rename apply?)
+#   (Related to testcases 9c and 9d -- can transitivity repeat?)
+#   (Related to testcase 12b -- joint-transitivity?)
+#   Commit O: z/{b,c},   x/d
+#   Commit A: y/{b,c},   x/d
+#   Commit B: z/{b,c,d}
+#   Expected: y/{b,c,d}  (because x/d -> z/d -> y/d)
+
+test_expect_success '1c-setup: Transitive renaming' '
+       test_create_repo 1c &&
+       (
+               cd 1c &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '1c-check: Transitive renaming' '
+       (
+               cd 1c &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:x/d &&
+               test_cmp expect actual &&
+               test_must_fail git rev-parse HEAD:x/d &&
+               test_must_fail git rev-parse HEAD:z/d &&
+               test_path_is_missing z/d
+       )
+'
+
+# Testcase 1d, Directory renames (merging two directories into one new one)
+#              cause a rename/rename(2to1) conflict
+#   (Related to testcases 1c and 7b)
+#   Commit O. z/{b,c},        y/{d,e}
+#   Commit A. x/{b,c},        y/{d,e,m,wham_1}
+#   Commit B. z/{b,c,n,wham_2}, x/{d,e}
+#   Expected: x/{b,c,d,e,m,n}, CONFLICT:(y/wham_1 & z/wham_2 -> x/wham)
+#   Note: y/m & z/n should definitely move into x.  By the same token, both
+#         y/wham_1 & z/wham_2 should too...giving us a conflict.
+
+test_expect_success '1d-setup: Directory renames cause a rename/rename(2to1) conflict' '
+       test_create_repo 1d &&
+       (
+               cd 1d &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir y &&
+               echo d >y/d &&
+               echo e >y/e &&
+               git add z y &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z x &&
+               echo m >y/m &&
+               echo wham1 >y/wham &&
+               git add y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv y x &&
+               echo n >z/n &&
+               echo wham2 >z/wham &&
+               git add z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '1d-check: Directory renames cause a rename/rename(2to1) conflict' '
+       (
+               cd 1d &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/rename)" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 8 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       :0:x/b :0:x/c :0:x/d :0:x/e :0:x/m :0:x/n &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  O:y/d  O:y/e  A:y/m  B:z/n &&
+               test_cmp expect actual &&
+
+               test_must_fail git rev-parse :0:x/wham &&
+               git rev-parse >actual \
+                       :2:x/wham :3:x/wham &&
+               git rev-parse >expect \
+                        A:y/wham  B:z/wham &&
+               test_cmp expect actual &&
+
+               test_path_is_missing x/wham &&
+               test_path_is_file x/wham~HEAD &&
+               test_path_is_file x/wham~B^0 &&
+
+               git hash-object >actual \
+                       x/wham~HEAD x/wham~B^0 &&
+               git rev-parse >expect \
+                       A:y/wham    B:z/wham &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 1e, Renamed directory, with all filenames being renamed too
+#   (Related to testcases 9f & 9g)
+#   Commit O: z/{oldb,oldc}
+#   Commit A: y/{newb,newc}
+#   Commit B: z/{oldb,oldc,d}
+#   Expected: y/{newb,newc,d}
+
+test_expect_success '1e-setup: Renamed directory, with all files being renamed too' '
+       test_create_repo 1e &&
+       (
+               cd 1e &&
+
+               mkdir z &&
+               echo b >z/oldb &&
+               echo c >z/oldc &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir y &&
+               git mv z/oldb y/newb &&
+               git mv z/oldc y/newc &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo d >z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '1e-check: Renamed directory, with all files being renamed too' '
+       (
+               cd 1e &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/newb HEAD:y/newc HEAD:y/d &&
+               git rev-parse >expect \
+                       O:z/oldb    O:z/oldc    B:z/d &&
+               test_cmp expect actual &&
+               test_must_fail git rev-parse HEAD:z/d
+       )
+'
+
+# Testcase 1f, Split a directory into two other directories
+#   (Related to testcases 3a, all of section 2, and all of section 4)
+#   Commit O: z/{b,c,d,e,f}
+#   Commit A: z/{b,c,d,e,f,g}
+#   Commit B: y/{b,c}, x/{d,e,f}
+#   Expected: y/{b,c}, x/{d,e,f,g}
+
+test_expect_success '1f-setup: Split a directory into two other directories' '
+       test_create_repo 1f &&
+       (
+               cd 1f &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d >z/d &&
+               echo e >z/e &&
+               echo f >z/f &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               echo g >z/g &&
+               git add z/g &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir y &&
+               mkdir x &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               git mv z/d x/ &&
+               git mv z/e x/ &&
+               git mv z/f x/ &&
+               rmdir z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '1f-check: Split a directory into two other directories' '
+       (
+               cd 1f &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:x/d HEAD:x/e HEAD:x/f HEAD:x/g &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:z/d    O:z/e    O:z/f    A:z/g &&
+               test_cmp expect actual &&
+               test_path_is_missing z/g &&
+               test_must_fail git rev-parse HEAD:z/g
+       )
+'
+
+###########################################################################
+# Rules suggested by testcases in section 1:
+#
+#   We should still detect the directory rename even if it wasn't just
+#   the directory renamed, but the files within it. (see 1b)
+#
+#   If renames split a directory into two or more others, the directory
+#   with the most renames, "wins" (see 1c).  However, see the testcases
+#   in section 2, plus testcases 3a and 4a.
+###########################################################################
+
+
+###########################################################################
+# SECTION 2: Split into multiple directories, with equal number of paths
+#
+# Explore the splitting-a-directory rules a bit; what happens in the
+# edge cases?
+#
+# Note that there is a closely related case of a directory not being
+# split on either side of history, but being renamed differently on
+# each side.  See testcase 8e for that.
+###########################################################################
+
+# Testcase 2a, Directory split into two on one side, with equal numbers of paths
+#   Commit O: z/{b,c}
+#   Commit A: y/b, w/c
+#   Commit B: z/{b,c,d}
+#   Expected: y/b, w/c, z/d, with warning about z/ -> (y/ vs. w/) conflict
+test_expect_success '2a-setup: Directory split into two on one side, with equal numbers of paths' '
+       test_create_repo 2a &&
+       (
+               cd 2a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir y &&
+               mkdir w &&
+               git mv z/b y/ &&
+               git mv z/c w/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo d >z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '2a-check: Directory split into two on one side, with equal numbers of paths' '
+       (
+               cd 2a &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT.*directory rename split" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:w/c :0:z/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  B:z/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 2b, Directory split into two on one side, with equal numbers of paths
+#   Commit O: z/{b,c}
+#   Commit A: y/b, w/c
+#   Commit B: z/{b,c}, x/d
+#   Expected: y/b, w/c, x/d; No warning about z/ -> (y/ vs. w/) conflict
+test_expect_success '2b-setup: Directory split into two on one side, with equal numbers of paths' '
+       test_create_repo 2b &&
+       (
+               cd 2b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir y &&
+               mkdir w &&
+               git mv z/b y/ &&
+               git mv z/c w/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir x &&
+               echo d >x/d &&
+               git add x/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '2b-check: Directory split into two on one side, with equal numbers of paths' '
+       (
+               cd 2b &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 >out &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:w/c :0:x/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  B:x/d &&
+               test_cmp expect actual &&
+               test_i18ngrep ! "CONFLICT.*directory rename split" out
+       )
+'
+
+###########################################################################
+# Rules suggested by section 2:
+#
+#   None; the rule was already covered in section 1.  These testcases are
+#   here just to make sure the conflict resolution and necessary warning
+#   messages are handled correctly.
+###########################################################################
+
+
+###########################################################################
+# SECTION 3: Path in question is the source path for some rename already
+#
+# Combining cases from Section 1 and trying to handle them could lead to
+# directory renaming detection being over-applied.  So, this section
+# provides some good testcases to check that the implementation doesn't go
+# too far.
+###########################################################################
+
+# Testcase 3a, Avoid implicit rename if involved as source on other side
+#   (Related to testcases 1c, 1f, and 9h)
+#   Commit O: z/{b,c,d}
+#   Commit A: z/{b,c,d} (no change)
+#   Commit B: y/{b,c}, x/d
+#   Expected: y/{b,c}, x/d
+test_expect_success '3a-setup: Avoid implicit rename if involved as source on other side' '
+       test_create_repo 3a &&
+       (
+               cd 3a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               test_tick &&
+               git commit --allow-empty -m "A" &&
+
+               git checkout B &&
+               mkdir y &&
+               mkdir x &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               git mv z/d x/ &&
+               rmdir z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '3a-check: Avoid implicit rename if involved as source on other side' '
+       (
+               cd 3a &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:x/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:z/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 3b, Avoid implicit rename if involved as source on other side
+#   (Related to testcases 5c and 7c, also kind of 1e and 1f)
+#   Commit O: z/{b,c,d}
+#   Commit A: y/{b,c}, x/d
+#   Commit B: z/{b,c}, w/d
+#   Expected: y/{b,c}, CONFLICT:(z/d -> x/d vs. w/d)
+#   NOTE: We're particularly checking that since z/d is already involved as
+#         a source in a file rename on the same side of history, that we don't
+#         get it involved in directory rename detection.  If it were, we might
+#         end up with CONFLICT:(z/d -> y/d vs. x/d vs. w/d), i.e. a
+#         rename/rename/rename(1to3) conflict, which is just weird.
+test_expect_success '3b-setup: Avoid implicit rename if involved as source on current side' '
+       test_create_repo 3b &&
+       (
+               cd 3b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir y &&
+               mkdir x &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               git mv z/d x/ &&
+               rmdir z &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir w &&
+               git mv z/d w/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '3b-check: Avoid implicit rename if involved as source on current side' '
+       (
+               cd 3b &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep CONFLICT.*rename/rename.*z/d.*x/d.*w/d out &&
+               test_i18ngrep ! CONFLICT.*rename/rename.*y/d out &&
+
+               git ls-files -s >out &&
+               test_line_count = 5 out &&
+               git ls-files -u >out &&
+               test_line_count = 3 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :1:z/d :2:x/d :3:w/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  O:z/d  O:z/d  O:z/d &&
+               test_cmp expect actual &&
+
+               test_path_is_missing z/d &&
+               git hash-object >actual \
+                       x/d   w/d &&
+               git rev-parse >expect \
+                       O:z/d O:z/d &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# Rules suggested by section 3:
+#
+#   Avoid directory-rename-detection for a path, if that path is the source
+#   of a rename on either side of a merge.
+###########################################################################
+
+
+###########################################################################
+# SECTION 4: Partially renamed directory; still exists on both sides of merge
+#
+# What if we were to attempt to do directory rename detection when someone
+# "mostly" moved a directory but still left some files around, or,
+# equivalently, fully renamed a directory in one commmit and then recreated
+# that directory in a later commit adding some new files and then tried to
+# merge?
+#
+# It's hard to divine user intent in these cases, because you can make an
+# argument that, depending on the intermediate history of the side being
+# merged, that some users will want files in that directory to
+# automatically be detected and renamed, while users with a different
+# intermediate history wouldn't want that rename to happen.
+#
+# I think that it is best to simply not have directory rename detection
+# apply to such cases.  My reasoning for this is four-fold: (1) it's
+# easiest for users in general to figure out what happened if we don't
+# apply directory rename detection in any such case, (2) it's an easy rule
+# to explain ["We don't do directory rename detection if the directory
+# still exists on both sides of the merge"], (3) we can get some hairy
+# edge/corner cases that would be really confusing and possibly not even
+# representable in the index if we were to even try, and [related to 3] (4)
+# attempting to resolve this issue of divining user intent by examining
+# intermediate history goes against the spirit of three-way merges and is a
+# path towards crazy corner cases that are far more complex than what we're
+# already dealing with.
+#
+# Note that the wording of the rule ("We don't do directory rename
+# detection if the directory still exists on both sides of the merge.")
+# also excludes "renaming" of a directory into a subdirectory of itself
+# (e.g. /some/dir/* -> /some/dir/subdir/*).  It may be possible to carve
+# out an exception for "renaming"-beneath-itself cases without opening
+# weird edge/corner cases for other partial directory renames, but for now
+# we are keeping the rule simple.
+#
+# This section contains a test for a partially-renamed-directory case.
+###########################################################################
+
+# Testcase 4a, Directory split, with original directory still present
+#   (Related to testcase 1f)
+#   Commit O: z/{b,c,d,e}
+#   Commit A: y/{b,c,d}, z/e
+#   Commit B: z/{b,c,d,e,f}
+#   Expected: y/{b,c,d}, z/{e,f}
+#   NOTE: Even though most files from z moved to y, we don't want f to follow.
+
+test_expect_success '4a-setup: Directory split, with original directory still present' '
+       test_create_repo 4a &&
+       (
+               cd 4a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d >z/d &&
+               echo e >z/e &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir y &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               git mv z/d y/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo f >z/f &&
+               git add z/f &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '4a-check: Directory split, with original directory still present' '
+       (
+               cd 4a &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 5 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d HEAD:z/e HEAD:z/f &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:z/d    O:z/e    B:z/f &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# Rules suggested by section 4:
+#
+#   Directory-rename-detection should be turned off for any directories (as
+#   a source for renames) that exist on both sides of the merge.  (The "as
+#   a source for renames" clarification is due to cases like 1c where
+#   the target directory exists on both sides and we do want the rename
+#   detection.)  But, sadly, see testcase 8b.
+###########################################################################
+
+
+###########################################################################
+# SECTION 5: Files/directories in the way of subset of to-be-renamed paths
+#
+# Implicitly renaming files due to a detected directory rename could run
+# into problems if there are files or directories in the way of the paths
+# we want to rename.  Explore such cases in this section.
+###########################################################################
+
+# Testcase 5a, Merge directories, other side adds files to original and target
+#   Commit O: z/{b,c},       y/d
+#   Commit A: z/{b,c,e_1,f}, y/{d,e_2}
+#   Commit B: y/{b,c,d}
+#   Expected: z/e_1, y/{b,c,d,e_2,f} + CONFLICT warning
+#   NOTE: While directory rename detection is active here causing z/f to
+#         become y/f, we did not apply this for z/e_1 because that would
+#         give us an add/add conflict for y/e_1 vs y/e_2.  This problem with
+#         this add/add, is that both versions of y/e are from the same side
+#         of history, giving us no way to represent this conflict in the
+#         index.
+
+test_expect_success '5a-setup: Merge directories, other side adds files to original and target' '
+       test_create_repo 5a &&
+       (
+               cd 5a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir y &&
+               echo d >y/d &&
+               git add z y &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               echo e1 >z/e &&
+               echo f >z/f &&
+               echo e2 >y/e &&
+               git add z/e z/f y/e &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               rmdir z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '5a-check: Merge directories, other side adds files to original and target' '
+       (
+               cd 5a &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT.*implicit dir rename" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :0:y/d :0:y/e :0:z/e :0:y/f &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  O:y/d  A:y/e  A:z/e  A:z/f &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 5b, Rename/delete in order to get add/add/add conflict
+#   (Related to testcase 8d; these may appear slightly inconsistent to users;
+#    Also related to testcases 7d and 7e)
+#   Commit O: z/{b,c,d_1}
+#   Commit A: y/{b,c,d_2}
+#   Commit B: z/{b,c,d_1,e}, y/d_3
+#   Expected: y/{b,c,e}, CONFLICT(add/add: y/d_2 vs. y/d_3)
+#   NOTE: If z/d_1 in commit B were to be involved in dir rename detection, as
+#         we normaly would since z/ is being renamed to y/, then this would be
+#         a rename/delete (z/d_1 -> y/d_1 vs. deleted) AND an add/add/add
+#         conflict of y/d_1 vs. y/d_2 vs. y/d_3.  Add/add/add is not
+#         representable in the index, so the existence of y/d_3 needs to
+#         cause us to bail on directory rename detection for that path, falling
+#         back to git behavior without the directory rename detection.
+
+test_expect_success '5b-setup: Rename/delete in order to get add/add/add conflict' '
+       test_create_repo 5b &&
+       (
+               cd 5b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d1 >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git rm z/d &&
+               git mv z y &&
+               echo d2 >y/d &&
+               git add y/d &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir y &&
+               echo d3 >y/d &&
+               echo e >z/e &&
+               git add y/d z/e &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '5b-check: Rename/delete in order to get add/add/add conflict' '
+       (
+               cd 5b &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (add/add).* y/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 5 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :0:y/e :2:y/d :3:y/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  B:z/e  A:y/d  B:y/d &&
+               test_cmp expect actual &&
+
+               test_must_fail git rev-parse :1:y/d &&
+               test_path_is_file y/d
+       )
+'
+
+# Testcase 5c, Transitive rename would cause rename/rename/rename/add/add/add
+#   (Directory rename detection would result in transitive rename vs.
+#    rename/rename(1to2) and turn it into a rename/rename(1to3).  Further,
+#    rename paths conflict with separate adds on the other side)
+#   (Related to testcases 3b and 7c)
+#   Commit O: z/{b,c}, x/d_1
+#   Commit A: y/{b,c,d_2}, w/d_1
+#   Commit B: z/{b,c,d_1,e}, w/d_3, y/d_4
+#   Expected: A mess, but only a rename/rename(1to2)/add/add mess.  Use the
+#             presence of y/d_4 in B to avoid doing transitive rename of
+#             x/d_1 -> z/d_1 -> y/d_1, so that the only paths we have at
+#             y/d are y/d_2 and y/d_4.  We still do the move from z/e to y/e,
+#             though, because it doesn't have anything in the way.
+
+test_expect_success '5c-setup: Transitive rename would cause rename/rename/rename/add/add/add' '
+       test_create_repo 5c &&
+       (
+               cd 5c &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d1 >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               echo d2 >y/d &&
+               git add y/d &&
+               git mv x w &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/ &&
+               mkdir w &&
+               mkdir y &&
+               echo d3 >w/d &&
+               echo d4 >y/d &&
+               echo e >z/e &&
+               git add w/ y/ z/e &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '5c-check: Transitive rename would cause rename/rename/rename/add/add/add' '
+       (
+               cd 5c &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/rename).*x/d.*w/d.*z/d" out &&
+               test_i18ngrep "CONFLICT (add/add).* y/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 9 out &&
+               git ls-files -u >out &&
+               test_line_count = 6 out &&
+               git ls-files -o >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :0:y/e &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  B:z/e &&
+               test_cmp expect actual &&
+
+               test_must_fail git rev-parse :1:y/d &&
+               git rev-parse >actual \
+                       :2:w/d :3:w/d :1:x/d :2:y/d :3:y/d :3:z/d &&
+               git rev-parse >expect \
+                        O:x/d  B:w/d  O:x/d  A:y/d  B:y/d  O:x/d &&
+               test_cmp expect actual &&
+
+               git hash-object >actual \
+                       w/d~HEAD w/d~B^0 z/d &&
+               git rev-parse >expect \
+                       O:x/d    B:w/d   O:x/d &&
+               test_cmp expect actual &&
+               test_path_is_missing x/d &&
+               test_path_is_file y/d &&
+               grep -q "<<<<" y/d  # conflict markers should be present
+       )
+'
+
+# Testcase 5d, Directory/file/file conflict due to directory rename
+#   Commit O: z/{b,c}
+#   Commit A: y/{b,c,d_1}
+#   Commit B: z/{b,c,d_2,f}, y/d/e
+#   Expected: y/{b,c,d/e,f}, z/d_2, CONFLICT(file/directory), y/d_1~HEAD
+#   Note: The fact that y/d/ exists in B makes us bail on directory rename
+#         detection for z/d_2, but that doesn't prevent us from applying the
+#         directory rename detection for z/f -> y/f.
+
+test_expect_success '5d-setup: Directory/file/file conflict due to directory rename' '
+       test_create_repo 5d &&
+       (
+               cd 5d &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               echo d1 >y/d &&
+               git add y/d &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir -p y/d &&
+               echo e >y/d/e &&
+               echo d2 >z/d &&
+               echo f >z/f &&
+               git add y/d/e z/d z/f &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '5d-check: Directory/file/file conflict due to directory rename' '
+       (
+               cd 5d &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (file/directory).*y/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -u >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 2 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :0:z/d :0:y/f :2:y/d :0:y/d/e &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  B:z/d  B:z/f  A:y/d  B:y/d/e &&
+               test_cmp expect actual &&
+
+               git hash-object y/d~HEAD >actual &&
+               git rev-parse A:y/d >expect &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# Rules suggested by section 5:
+#
+#   If a subset of to-be-renamed files have a file or directory in the way,
+#   "turn off" the directory rename for those specific sub-paths, falling
+#   back to old handling.  But, sadly, see testcases 8a and 8b.
+###########################################################################
+
+
+###########################################################################
+# SECTION 6: Same side of the merge was the one that did the rename
+#
+# It may sound obvious that you only want to apply implicit directory
+# renames to directories if the _other_ side of history did the renaming.
+# If you did make an implementation that didn't explicitly enforce this
+# rule, the majority of cases that would fall under this section would
+# also be solved by following the rules from the above sections.  But
+# there are still a few that stick out, so this section covers them just
+# to make sure we also get them right.
+###########################################################################
+
+# Testcase 6a, Tricky rename/delete
+#   Commit O: z/{b,c,d}
+#   Commit A: z/b
+#   Commit B: y/{b,c}, z/d
+#   Expected: y/b, CONFLICT(rename/delete, z/c -> y/c vs. NULL)
+#   Note: We're just checking here that the rename of z/b and z/c to put
+#         them under y/ doesn't accidentally catch z/d and make it look like
+#         it is also involved in a rename/delete conflict.
+
+test_expect_success '6a-setup: Tricky rename/delete' '
+       test_create_repo 6a &&
+       (
+               cd 6a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git rm z/c &&
+               git rm z/d &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir y &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '6a-check: Tricky rename/delete' '
+       (
+               cd 6a &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/delete).*z/c.*y/c" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 2 out &&
+               git ls-files -u >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :3:y/c &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 6b, Same rename done on both sides
+#   (Related to testcases 6c and 8e)
+#   Commit O: z/{b,c}
+#   Commit A: y/{b,c}
+#   Commit B: y/{b,c}, z/d
+#   Expected: y/{b,c}, z/d
+#   Note: If we did directory rename detection here, we'd move z/d into y/,
+#         but B did that rename and still decided to put the file into z/,
+#         so we probably shouldn't apply directory rename detection for it.
+
+test_expect_success '6b-setup: Same rename done on both sides' '
+       test_create_repo 6b &&
+       (
+               cd 6b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z y &&
+               mkdir z &&
+               echo d >z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '6b-check: Same rename done on both sides' '
+       (
+               cd 6b &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:z/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    B:z/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 6c, Rename only done on same side
+#   (Related to testcases 6b and 8e)
+#   Commit O: z/{b,c}
+#   Commit A: z/{b,c} (no change)
+#   Commit B: y/{b,c}, z/d
+#   Expected: y/{b,c}, z/d
+#   NOTE: Seems obvious, but just checking that the implementation doesn't
+#         "accidentally detect a rename" and give us y/{b,c,d}.
+
+test_expect_success '6c-setup: Rename only done on same side' '
+       test_create_repo 6c &&
+       (
+               cd 6c &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               test_tick &&
+               git commit --allow-empty -m "A" &&
+
+               git checkout B &&
+               git mv z y &&
+               mkdir z &&
+               echo d >z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '6c-check: Rename only done on same side' '
+       (
+               cd 6c &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:z/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    B:z/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 6d, We don't always want transitive renaming
+#   (Related to testcase 1c)
+#   Commit O: z/{b,c}, x/d
+#   Commit A: z/{b,c}, x/d (no change)
+#   Commit B: y/{b,c}, z/d
+#   Expected: y/{b,c}, z/d
+#   NOTE: Again, this seems obvious but just checking that the implementation
+#         doesn't "accidentally detect a rename" and give us y/{b,c,d}.
+
+test_expect_success '6d-setup: We do not always want transitive renaming' '
+       test_create_repo 6d &&
+       (
+               cd 6d &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               test_tick &&
+               git commit --allow-empty -m "A" &&
+
+               git checkout B &&
+               git mv z y &&
+               git mv x z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '6d-check: We do not always want transitive renaming' '
+       (
+               cd 6d &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:z/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:x/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 6e, Add/add from one-side
+#   Commit O: z/{b,c}
+#   Commit A: z/{b,c} (no change)
+#   Commit B: y/{b,c,d_1}, z/d_2
+#   Expected: y/{b,c,d_1}, z/d_2
+#   NOTE: Again, this seems obvious but just checking that the implementation
+#         doesn't "accidentally detect a rename" and give us y/{b,c} +
+#         add/add conflict on y/d_1 vs y/d_2.
+
+test_expect_success '6e-setup: Add/add from one side' '
+       test_create_repo 6e &&
+       (
+               cd 6e &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               test_tick &&
+               git commit --allow-empty -m "A" &&
+
+               git checkout B &&
+               git mv z y &&
+               echo d1 > y/d &&
+               mkdir z &&
+               echo d2 > z/d &&
+               git add y/d z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '6e-check: Add/add from one side' '
+       (
+               cd 6e &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d HEAD:z/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    B:y/d    B:z/d &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# Rules suggested by section 6:
+#
+#   Only apply implicit directory renames to directories if the other
+#   side of history is the one doing the renaming.
+###########################################################################
+
+
+###########################################################################
+# SECTION 7: More involved Edge/Corner cases
+#
+# The ruleset we have generated in the above sections seems to provide
+# well-defined merges.  But can we find edge/corner cases that either (a)
+# are harder for users to understand, or (b) have a resolution that is
+# non-intuitive or suboptimal?
+#
+# The testcases in this section dive into cases that I've tried to craft in
+# a way to find some that might be surprising to users or difficult for
+# them to understand (the next section will look at non-intuitive or
+# suboptimal merge results).  Some of the testcases are similar to ones
+# from past sections, but have been simplified to try to highlight error
+# messages using a "modified" path (due to the directory rename).  Are
+# users okay with these?
+#
+# In my opinion, testcases that are difficult to understand from this
+# section is due to difficulty in the testcase rather than the directory
+# renaming (similar to how t6042 and t6036 have difficult resolutions due
+# to the problem setup itself being complex).  And I don't think the
+# error messages are a problem.
+#
+# On the other hand, the testcases in section 8 worry me slightly more...
+###########################################################################
+
+# Testcase 7a, rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file
+#   Commit O: z/{b,c}
+#   Commit A: y/{b,c}
+#   Commit B: w/b, x/c, z/d
+#   Expected: y/d, CONFLICT(rename/rename for both z/b and z/c)
+#   NOTE: There's a rename of z/ here, y/ has more renames, so z/d -> y/d.
+
+test_expect_success '7a-setup: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '
+       test_create_repo 7a &&
+       (
+               cd 7a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir w &&
+               mkdir x &&
+               git mv z/b w/ &&
+               git mv z/c x/ &&
+               echo d > z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '7a-check: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '
+       (
+               cd 7a &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/rename).*z/b.*y/b.*w/b" out &&
+               test_i18ngrep "CONFLICT (rename/rename).*z/c.*y/c.*x/c" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 7 out &&
+               git ls-files -u >out &&
+               test_line_count = 6 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:x/c :0:y/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/b  O:z/b  O:z/c  O:z/c  O:z/c  B:z/d &&
+               test_cmp expect actual &&
+
+               git hash-object >actual \
+                       y/b   w/b   y/c   x/c &&
+               git rev-parse >expect \
+                       O:z/b O:z/b O:z/c O:z/c &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 7b, rename/rename(2to1), but only due to transitive rename
+#   (Related to testcase 1d)
+#   Commit O: z/{b,c},     x/d_1, w/d_2
+#   Commit A: y/{b,c,d_2}, x/d_1
+#   Commit B: z/{b,c,d_1},        w/d_2
+#   Expected: y/{b,c}, CONFLICT(rename/rename(2to1): x/d_1, w/d_2 -> y_d)
+
+test_expect_success '7b-setup: rename/rename(2to1), but only due to transitive rename' '
+       test_create_repo 7b &&
+       (
+               cd 7b &&
+
+               mkdir z &&
+               mkdir x &&
+               mkdir w &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d1 > x/d &&
+               echo d2 > w/d &&
+               git add z x w &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               git mv w/d y/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/ &&
+               rmdir x &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '7b-check: rename/rename(2to1), but only due to transitive rename' '
+       (
+               cd 7b &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/rename)" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :2:y/d :3:y/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  O:w/d  O:x/d &&
+               test_cmp expect actual &&
+
+               test_path_is_missing y/d &&
+               test_path_is_file y/d~HEAD &&
+               test_path_is_file y/d~B^0 &&
+
+               git hash-object >actual \
+                       y/d~HEAD y/d~B^0 &&
+               git rev-parse >expect \
+                       O:w/d    O:x/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 7c, rename/rename(1to...2or3); transitive rename may add complexity
+#   (Related to testcases 3b and 5c)
+#   Commit O: z/{b,c}, x/d
+#   Commit A: y/{b,c}, w/d
+#   Commit B: z/{b,c,d}
+#   Expected: y/{b,c}, CONFLICT(x/d -> w/d vs. y/d)
+#   NOTE: z/ was renamed to y/ so we do want to report
+#         neither CONFLICT(x/d -> w/d vs. z/d)
+#         nor CONFLiCT x/d -> w/d vs. y/d vs. z/d)
+
+test_expect_success '7c-setup: rename/rename(1to...2or3); transitive rename may add complexity' '
+       test_create_repo 7c &&
+       (
+               cd 7c &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               git mv x w &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/ &&
+               rmdir x &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '7c-check: rename/rename(1to...2or3); transitive rename may add complexity' '
+       (
+               cd 7c &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/rename).*x/d.*w/d.*y/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 5 out &&
+               git ls-files -u >out &&
+               test_line_count = 3 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :1:x/d :2:w/d :3:y/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  O:x/d  O:x/d  O:x/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 7d, transitive rename involved in rename/delete; how is it reported?
+#   (Related somewhat to testcases 5b and 8d)
+#   Commit O: z/{b,c}, x/d
+#   Commit A: y/{b,c}
+#   Commit B: z/{b,c,d}
+#   Expected: y/{b,c}, CONFLICT(delete x/d vs rename to y/d)
+#   NOTE: z->y so NOT CONFLICT(delete x/d vs rename to z/d)
+
+test_expect_success '7d-setup: transitive rename involved in rename/delete; how is it reported?' '
+       test_create_repo 7d &&
+       (
+               cd 7d &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               git rm -rf x &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/ &&
+               rmdir x &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '7d-check: transitive rename involved in rename/delete; how is it reported?' '
+       (
+               cd 7d &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/delete).*x/d.*y/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :3:y/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  O:x/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 7e, transitive rename in rename/delete AND dirs in the way
+#   (Very similar to 'both rename source and destination involved in D/F conflict' from t6022-merge-rename.sh)
+#   (Also related to testcases 9c and 9d)
+#   Commit O: z/{b,c},     x/d_1
+#   Commit A: y/{b,c,d/g}, x/d/f
+#   Commit B: z/{b,c,d_1}
+#   Expected: rename/delete(x/d_1->y/d_1 vs. None) + D/F conflict on y/d
+#             y/{b,c,d/g}, y/d_1~B^0, x/d/f
+
+#   NOTE: The main path of interest here is d_1 and where it ends up, but
+#         this is actually a case that has two potential directory renames
+#         involved and D/F conflict(s), so it makes sense to walk through
+#         each step.
+#
+#         Commit A renames z/ -> y/.  Thus everything that B adds to z/
+#         should be instead moved to y/.  This gives us the D/F conflict on
+#         y/d because x/d_1 -> z/d_1 -> y/d_1 conflicts with y/d/g.
+#
+#         Further, commit B renames x/ -> z/, thus everything A adds to x/
+#         should instead be moved to z/...BUT we removed z/ and renamed it
+#         to y/, so maybe everything should move not from x/ to z/, but
+#         from x/ to z/ to y/.  Doing so might make sense from the logic so
+#         far, but note that commit A had both an x/ and a y/; it did the
+#         renaming of z/ to y/ and created x/d/f and it clearly made these
+#         things separate, so it doesn't make much sense to push these
+#         together.  Doing so is what I'd call a doubly transitive rename;
+#         see testcases 9c and 9d for further discussion of this issue and
+#         how it's resolved.
+
+test_expect_success '7e-setup: transitive rename in rename/delete AND dirs in the way' '
+       test_create_repo 7e &&
+       (
+               cd 7e &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d1 >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               git rm x/d &&
+               mkdir -p x/d &&
+               mkdir -p y/d &&
+               echo f >x/d/f &&
+               echo g >y/d/g &&
+               git add x/d/f y/d/g &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/ &&
+               rmdir x &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '7e-check: transitive rename in rename/delete AND dirs in the way' '
+       (
+               cd 7e &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (rename/delete).*x/d.*y/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 5 out &&
+               git ls-files -u >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 2 out &&
+
+               git rev-parse >actual \
+                       :0:x/d/f :0:y/d/g :0:y/b :0:y/c :3:y/d &&
+               git rev-parse >expect \
+                        A:x/d/f  A:y/d/g  O:z/b  O:z/c  O:x/d &&
+               test_cmp expect actual &&
+
+               git hash-object y/d~B^0 >actual &&
+               git rev-parse O:x/d >expect &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# SECTION 8: Suboptimal merges
+#
+# As alluded to in the last section, the ruleset we have built up for
+# detecting directory renames unfortunately has some special cases where it
+# results in slightly suboptimal or non-intuitive behavior.  This section
+# explores these cases.
+#
+# To be fair, we already had non-intuitive or suboptimal behavior for most
+# of these cases in git before introducing implicit directory rename
+# detection, but it'd be nice if there was a modified ruleset out there
+# that handled these cases a bit better.
+###########################################################################
+
+# Testcase 8a, Dual-directory rename, one into the others' way
+#   Commit O. x/{a,b},   y/{c,d}
+#   Commit A. x/{a,b,e}, y/{c,d,f}
+#   Commit B. y/{a,b},   z/{c,d}
+#
+# Possible Resolutions:
+#   w/o dir-rename detection: y/{a,b,f},   z/{c,d},   x/e
+#   Currently expected:       y/{a,b,e,f}, z/{c,d}
+#   Optimal:                  y/{a,b,e},   z/{c,d,f}
+#
+# Note: Both x and y got renamed and it'd be nice to detect both, and we do
+# better with directory rename detection than git did without, but the
+# simple rule from section 5 prevents me from handling this as optimally as
+# we potentially could.
+
+test_expect_success '8a-setup: Dual-directory rename, one into the others way' '
+       test_create_repo 8a &&
+       (
+               cd 8a &&
+
+               mkdir x &&
+               mkdir y &&
+               echo a >x/a &&
+               echo b >x/b &&
+               echo c >y/c &&
+               echo d >y/d &&
+               git add x y &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               echo e >x/e &&
+               echo f >y/f &&
+               git add x/e y/f &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv y z &&
+               git mv x y &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '8a-check: Dual-directory rename, one into the others way' '
+       (
+               cd 8a &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/a HEAD:y/b HEAD:y/e HEAD:y/f HEAD:z/c HEAD:z/d &&
+               git rev-parse >expect \
+                       O:x/a    O:x/b    A:x/e    A:y/f    O:y/c    O:y/d &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 8b, Dual-directory rename, one into the others' way, with conflicting filenames
+#   Commit O. x/{a_1,b_1},     y/{a_2,b_2}
+#   Commit A. x/{a_1,b_1,e_1}, y/{a_2,b_2,e_2}
+#   Commit B. y/{a_1,b_1},     z/{a_2,b_2}
+#
+#   w/o dir-rename detection: y/{a_1,b_1,e_2}, z/{a_2,b_2}, x/e_1
+#   Currently expected:       <same>
+#   Scary:                    y/{a_1,b_1},     z/{a_2,b_2}, CONFLICT(add/add, e_1 vs. e_2)
+#   Optimal:                  y/{a_1,b_1,e_1}, z/{a_2,b_2,e_2}
+#
+# Note: Very similar to 8a, except instead of 'e' and 'f' in directories x and
+# y, both are named 'e'.  Without directory rename detection, neither file
+# moves directories.  Implement directory rename detection suboptimally, and
+# you get an add/add conflict, but both files were added in commit A, so this
+# is an add/add conflict where one side of history added both files --
+# something we can't represent in the index.  Obviously, we'd prefer the last
+# resolution, but our previous rules are too coarse to allow it.  Using both
+# the rules from section 4 and section 5 save us from the Scary resolution,
+# making us fall back to pre-directory-rename-detection behavior for both
+# e_1 and e_2.
+
+test_expect_success '8b-setup: Dual-directory rename, one into the others way, with conflicting filenames' '
+       test_create_repo 8b &&
+       (
+               cd 8b &&
+
+               mkdir x &&
+               mkdir y &&
+               echo a1 >x/a &&
+               echo b1 >x/b &&
+               echo a2 >y/a &&
+               echo b2 >y/b &&
+               git add x y &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               echo e1 >x/e &&
+               echo e2 >y/e &&
+               git add x/e y/e &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv y z &&
+               git mv x y &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '8b-check: Dual-directory rename, one into the others way, with conflicting filenames' '
+       (
+               cd 8b &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/a HEAD:y/b HEAD:z/a HEAD:z/b HEAD:x/e HEAD:y/e &&
+               git rev-parse >expect \
+                       O:x/a    O:x/b    O:y/a    O:y/b    A:x/e    A:y/e &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 8c, modify/delete or rename+modify/delete?
+#   (Related to testcases 5b, 8d, and 9h)
+#   Commit O: z/{b,c,d}
+#   Commit A: y/{b,c}
+#   Commit B: z/{b,c,d_modified,e}
+#   Expected: y/{b,c,e}, CONFLICT(modify/delete: on z/d)
+#
+#   Note: It could easily be argued that the correct resolution here is
+#         y/{b,c,e}, CONFLICT(rename/delete: z/d -> y/d vs deleted)
+#         and that the modifed version of d should be present in y/ after
+#         the merge, just marked as conflicted.  Indeed, I previously did
+#         argue that.  But applying directory renames to the side of
+#         history where a file is merely modified results in spurious
+#         rename/rename(1to2) conflicts -- see testcase 9h.  See also
+#         notes in 8d.
+
+test_expect_success '8c-setup: modify/delete or rename+modify/delete?' '
+       test_create_repo 8c &&
+       (
+               cd 8c &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               test_seq 1 10 >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git rm z/d &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo 11 >z/d &&
+               test_chmod +x z/d &&
+               echo e >z/e &&
+               git add z/d z/e &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '8c-check: modify/delete or rename+modify/delete' '
+       (
+               cd 8c &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               test_i18ngrep "CONFLICT (modify/delete).* z/d" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 5 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :0:y/c :0:y/e :1:z/d :3:z/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/c  B:z/e  O:z/d  B:z/d &&
+               test_cmp expect actual &&
+
+               test_must_fail git rev-parse :2:z/d &&
+               git ls-files -s z/d | grep ^100755 &&
+               test_path_is_file z/d &&
+               test_path_is_missing y/d
+       )
+'
+
+# Testcase 8d, rename/delete...or not?
+#   (Related to testcase 5b; these may appear slightly inconsistent to users;
+#    Also related to testcases 7d and 7e)
+#   Commit O: z/{b,c,d}
+#   Commit A: y/{b,c}
+#   Commit B: z/{b,c,d,e}
+#   Expected: y/{b,c,e}
+#
+#   Note: It would also be somewhat reasonable to resolve this as
+#             y/{b,c,e}, CONFLICT(rename/delete: x/d -> y/d or deleted)
+#
+#   In this case, I'm leaning towards: commit A was the one that deleted z/d
+#   and it did the rename of z to y, so the two "conflicts" (rename vs.
+#   delete) are both coming from commit A, which is illogical.  Conflicts
+#   during merging are supposed to be about opposite sides doing things
+#   differently.
+
+test_expect_success '8d-setup: rename/delete...or not?' '
+       test_create_repo 8d &&
+       (
+               cd 8d &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               test_seq 1 10 >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git rm z/d &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo e >z/e &&
+               git add z/e &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '8d-check: rename/delete...or not?' '
+       (
+               cd 8d &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/e &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    B:z/e &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 8e, Both sides rename, one side adds to original directory
+#   Commit O: z/{b,c}
+#   Commit A: y/{b,c}
+#   Commit B: w/{b,c}, z/d
+#
+# Possible Resolutions:
+#   w/o dir-rename detection: z/d, CONFLICT(z/b -> y/b vs. w/b),
+#                                  CONFLICT(z/c -> y/c vs. w/c)
+#   Currently expected:       y/d, CONFLICT(z/b -> y/b vs. w/b),
+#                                  CONFLICT(z/c -> y/c vs. w/c)
+#   Optimal:                  ??
+#
+# Notes: In commit A, directory z got renamed to y.  In commit B, directory z
+#        did NOT get renamed; the directory is still present; instead it is
+#        considered to have just renamed a subset of paths in directory z
+#        elsewhere.  Therefore, the directory rename done in commit A to z/
+#        applies to z/d and maps it to y/d.
+#
+#        It's possible that users would get confused about this, but what
+#        should we do instead?  Silently leaving at z/d seems just as bad or
+#        maybe even worse.  Perhaps we could print a big warning about z/d
+#        and how we're moving to y/d in this case, but when I started thinking
+#        about the ramifications of doing that, I didn't know how to rule out
+#        that opening other weird edge and corner cases so I just punted.
+
+test_expect_success '8e-setup: Both sides rename, one side adds to original directory' '
+       test_create_repo 8e &&
+       (
+               cd 8e &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z w &&
+               mkdir z &&
+               echo d >z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '8e-check: Both sides rename, one side adds to original directory' '
+       (
+               cd 8e &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep CONFLICT.*rename/rename.*z/c.*y/c.*w/c out &&
+               test_i18ngrep CONFLICT.*rename/rename.*z/b.*y/b.*w/b out &&
+
+               git ls-files -s >out &&
+               test_line_count = 7 out &&
+               git ls-files -u >out &&
+               test_line_count = 6 out &&
+               git ls-files -o >out &&
+               test_line_count = 2 out &&
+
+               git rev-parse >actual \
+                       :1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:w/c :0:y/d &&
+               git rev-parse >expect \
+                        O:z/b  O:z/b  O:z/b  O:z/c  O:z/c  O:z/c  B:z/d &&
+               test_cmp expect actual &&
+
+               git hash-object >actual \
+                       y/b   w/b   y/c   w/c &&
+               git rev-parse >expect \
+                       O:z/b O:z/b O:z/c O:z/c &&
+               test_cmp expect actual &&
+
+               test_path_is_missing z/b &&
+               test_path_is_missing z/c
+       )
+'
+
+###########################################################################
+# SECTION 9: Other testcases
+#
+# This section consists of miscellaneous testcases I thought of during
+# the implementation which round out the testing.
+###########################################################################
+
+# Testcase 9a, Inner renamed directory within outer renamed directory
+#   (Related to testcase 1f)
+#   Commit O: z/{b,c,d/{e,f,g}}
+#   Commit A: y/{b,c}, x/w/{e,f,g}
+#   Commit B: z/{b,c,d/{e,f,g,h},i}
+#   Expected: y/{b,c,i}, x/w/{e,f,g,h}
+#   NOTE: The only reason this one is interesting is because when a directory
+#         is split into multiple other directories, we determine by the weight
+#         of which one had the most paths going to it.  A naive implementation
+#         of that could take the new file in commit B at z/i to x/w/i or x/i.
+
+test_expect_success '9a-setup: Inner renamed directory within outer renamed directory' '
+       test_create_repo 9a &&
+       (
+               cd 9a &&
+
+               mkdir -p z/d &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo e >z/d/e &&
+               echo f >z/d/f &&
+               echo g >z/d/g &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir x &&
+               git mv z/d x/w &&
+               git mv z y &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo h >z/d/h &&
+               echo i >z/i &&
+               git add z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '9a-check: Inner renamed directory within outer renamed directory' '
+       (
+               cd 9a &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 7 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/i &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    B:z/i &&
+               test_cmp expect actual &&
+
+               git rev-parse >actual \
+                       HEAD:x/w/e HEAD:x/w/f HEAD:x/w/g HEAD:x/w/h &&
+               git rev-parse >expect \
+                       O:z/d/e    O:z/d/f    O:z/d/g    B:z/d/h &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 9b, Transitive rename with content merge
+#   (Related to testcase 1c)
+#   Commit O: z/{b,c},   x/d_1
+#   Commit A: y/{b,c},   x/d_2
+#   Commit B: z/{b,c,d_3}
+#   Expected: y/{b,c,d_merged}
+
+test_expect_success '9b-setup: Transitive rename with content merge' '
+       test_create_repo 9b &&
+       (
+               cd 9b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               test_seq 1 10 >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               test_seq 1 11 >x/d &&
+               git add x/d &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               test_seq 0 10 >x/d &&
+               git mv x/d z/d &&
+               git add z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '9b-check: Transitive rename with content merge' '
+       (
+               cd 9b &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+
+               test_seq 0 11 >expected &&
+               test_cmp expected y/d &&
+               git add expected &&
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    :0:expected &&
+               test_cmp expect actual &&
+               test_must_fail git rev-parse HEAD:x/d &&
+               test_must_fail git rev-parse HEAD:z/d &&
+               test_path_is_missing z/d &&
+
+               test $(git rev-parse HEAD:y/d) != $(git rev-parse O:x/d) &&
+               test $(git rev-parse HEAD:y/d) != $(git rev-parse A:x/d) &&
+               test $(git rev-parse HEAD:y/d) != $(git rev-parse B:z/d)
+       )
+'
+
+# Testcase 9c, Doubly transitive rename?
+#   (Related to testcase 1c, 7e, and 9d)
+#   Commit O: z/{b,c},     x/{d,e},    w/f
+#   Commit A: y/{b,c},     x/{d,e,f,g}
+#   Commit B: z/{b,c,d,e},             w/f
+#   Expected: y/{b,c,d,e}, x/{f,g}
+#
+#   NOTE: x/f and x/g may be slightly confusing here.  The rename from w/f to
+#         x/f is clear.  Let's look beyond that.  Here's the logic:
+#            Commit B renamed x/ -> z/
+#            Commit A renamed z/ -> y/
+#         So, we could possibly further rename x/f to z/f to y/f, a doubly
+#         transient rename.  However, where does it end?  We can chain these
+#         indefinitely (see testcase 9d).  What if there is a D/F conflict
+#         at z/f/ or y/f/?  Or just another file conflict at one of those
+#         paths?  In the case of an N-long chain of transient renamings,
+#         where do we "abort" the rename at?  Can the user make sense of
+#         the resulting conflict and resolve it?
+#
+#         To avoid this confusion I use the simple rule that if the other side
+#         of history did a directory rename to a path that your side renamed
+#         away, then ignore that particular rename from the other side of
+#         history for any implicit directory renames.
+
+test_expect_success '9c-setup: Doubly transitive rename?' '
+       test_create_repo 9c &&
+       (
+               cd 9c &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               mkdir x &&
+               echo d >x/d &&
+               echo e >x/e &&
+               mkdir w &&
+               echo f >w/f &&
+               git add z x w &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z y &&
+               git mv w/f x/ &&
+               echo g >x/g &&
+               git add x/g &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/d &&
+               git mv x/e z/e &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '9c-check: Doubly transitive rename?' '
+       (
+               cd 9c &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 >out &&
+               test_i18ngrep "WARNING: Avoiding applying x -> z rename to x/f" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e HEAD:x/f HEAD:x/g &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    O:x/d    O:x/e    O:w/f    A:x/g &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 9d, N-fold transitive rename?
+#   (Related to testcase 9c...and 1c and 7e)
+#   Commit O: z/a, y/b, x/c, w/d, v/e, u/f
+#   Commit A:  y/{a,b},  w/{c,d},  u/{e,f}
+#   Commit B: z/{a,t}, x/{b,c}, v/{d,e}, u/f
+#   Expected: <see NOTE first>
+#
+#   NOTE: z/ -> y/ (in commit A)
+#         y/ -> x/ (in commit B)
+#         x/ -> w/ (in commit A)
+#         w/ -> v/ (in commit B)
+#         v/ -> u/ (in commit A)
+#         So, if we add a file to z, say z/t, where should it end up?  In u?
+#         What if there's another file or directory named 't' in one of the
+#         intervening directories and/or in u itself?  Also, shouldn't the
+#         same logic that places 't' in u/ also move ALL other files to u/?
+#         What if there are file or directory conflicts in any of them?  If
+#         we attempted to do N-way (N-fold? N-ary? N-uple?) transitive renames
+#         like this, would the user have any hope of understanding any
+#         conflicts or how their working tree ended up?  I think not, so I'm
+#         ruling out N-ary transitive renames for N>1.
+#
+#   Therefore our expected result is:
+#     z/t, y/a, x/b, w/c, u/d, u/e, u/f
+#   The reason that v/d DOES get transitively renamed to u/d is that u/ isn't
+#   renamed somewhere.  A slightly sub-optimal result, but it uses fairly
+#   simple rules that are consistent with what we need for all the other
+#   testcases and simplifies things for the user.
+
+test_expect_success '9d-setup: N-way transitive rename?' '
+       test_create_repo 9d &&
+       (
+               cd 9d &&
+
+               mkdir z y x w v u &&
+               echo a >z/a &&
+               echo b >y/b &&
+               echo c >x/c &&
+               echo d >w/d &&
+               echo e >v/e &&
+               echo f >u/f &&
+               git add z y x w v u &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z/a y/ &&
+               git mv x/c w/ &&
+               git mv v/e u/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo t >z/t &&
+               git mv y/b x/ &&
+               git mv w/d v/ &&
+               git add z/t &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '9d-check: N-way transitive rename?' '
+       (
+               cd 9d &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 >out &&
+               test_i18ngrep "WARNING: Avoiding applying z -> y rename to z/t" out &&
+               test_i18ngrep "WARNING: Avoiding applying y -> x rename to y/a" out &&
+               test_i18ngrep "WARNING: Avoiding applying x -> w rename to x/b" out &&
+               test_i18ngrep "WARNING: Avoiding applying w -> v rename to w/c" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 7 out &&
+               git ls-files -o >out &&
+               test_line_count = 1 out &&
+
+               git rev-parse >actual \
+                       HEAD:z/t \
+                       HEAD:y/a HEAD:x/b HEAD:w/c \
+                       HEAD:u/d HEAD:u/e HEAD:u/f &&
+               git rev-parse >expect \
+                       B:z/t    \
+                       O:z/a    O:y/b    O:x/c    \
+                       O:w/d    O:v/e    A:u/f &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 9e, N-to-1 whammo
+#   (Related to testcase 9c...and 1c and 7e)
+#   Commit O: dir1/{a,b}, dir2/{d,e}, dir3/{g,h}, dirN/{j,k}
+#   Commit A: dir1/{a,b,c,yo}, dir2/{d,e,f,yo}, dir3/{g,h,i,yo}, dirN/{j,k,l,yo}
+#   Commit B: combined/{a,b,d,e,g,h,j,k}
+#   Expected: combined/{a,b,c,d,e,f,g,h,i,j,k,l}, CONFLICT(Nto1) warnings,
+#             dir1/yo, dir2/yo, dir3/yo, dirN/yo
+
+test_expect_success '9e-setup: N-to-1 whammo' '
+       test_create_repo 9e &&
+       (
+               cd 9e &&
+
+               mkdir dir1 dir2 dir3 dirN &&
+               echo a >dir1/a &&
+               echo b >dir1/b &&
+               echo d >dir2/d &&
+               echo e >dir2/e &&
+               echo g >dir3/g &&
+               echo h >dir3/h &&
+               echo j >dirN/j &&
+               echo k >dirN/k &&
+               git add dir* &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               echo c  >dir1/c &&
+               echo yo >dir1/yo &&
+               echo f  >dir2/f &&
+               echo yo >dir2/yo &&
+               echo i  >dir3/i &&
+               echo yo >dir3/yo &&
+               echo l  >dirN/l &&
+               echo yo >dirN/yo &&
+               git add dir* &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv dir1 combined &&
+               git mv dir2/* combined/ &&
+               git mv dir3/* combined/ &&
+               git mv dirN/* combined/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success C_LOCALE_OUTPUT '9e-check: N-to-1 whammo' '
+       (
+               cd 9e &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 >out &&
+               grep "CONFLICT (implicit dir rename): Cannot map more than one path to combined/yo" out >error_line &&
+               grep -q dir1/yo error_line &&
+               grep -q dir2/yo error_line &&
+               grep -q dir3/yo error_line &&
+               grep -q dirN/yo error_line &&
+
+               git ls-files -s >out &&
+               test_line_count = 16 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 2 out &&
+
+               git rev-parse >actual \
+                       :0:combined/a :0:combined/b :0:combined/c \
+                       :0:combined/d :0:combined/e :0:combined/f \
+                       :0:combined/g :0:combined/h :0:combined/i \
+                       :0:combined/j :0:combined/k :0:combined/l &&
+               git rev-parse >expect \
+                        O:dir1/a      O:dir1/b      A:dir1/c \
+                        O:dir2/d      O:dir2/e      A:dir2/f \
+                        O:dir3/g      O:dir3/h      A:dir3/i \
+                        O:dirN/j      O:dirN/k      A:dirN/l &&
+               test_cmp expect actual &&
+
+               git rev-parse >actual \
+                       :0:dir1/yo :0:dir2/yo :0:dir3/yo :0:dirN/yo &&
+               git rev-parse >expect \
+                        A:dir1/yo  A:dir2/yo  A:dir3/yo  A:dirN/yo &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 9f, Renamed directory that only contained immediate subdirs
+#   (Related to testcases 1e & 9g)
+#   Commit O: goal/{a,b}/$more_files
+#   Commit A: priority/{a,b}/$more_files
+#   Commit B: goal/{a,b}/$more_files, goal/c
+#   Expected: priority/{a,b}/$more_files, priority/c
+
+test_expect_success '9f-setup: Renamed directory that only contained immediate subdirs' '
+       test_create_repo 9f &&
+       (
+               cd 9f &&
+
+               mkdir -p goal/a &&
+               mkdir -p goal/b &&
+               echo foo >goal/a/foo &&
+               echo bar >goal/b/bar &&
+               echo baz >goal/b/baz &&
+               git add goal &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv goal/ priority &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo c >goal/c &&
+               git add goal/c &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '9f-check: Renamed directory that only contained immediate subdirs' '
+       (
+               cd 9f &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       HEAD:priority/a/foo \
+                       HEAD:priority/b/bar \
+                       HEAD:priority/b/baz \
+                       HEAD:priority/c &&
+               git rev-parse >expect \
+                       O:goal/a/foo \
+                       O:goal/b/bar \
+                       O:goal/b/baz \
+                       B:goal/c &&
+               test_cmp expect actual &&
+               test_must_fail git rev-parse HEAD:goal/c
+       )
+'
+
+# Testcase 9g, Renamed directory that only contained immediate subdirs, immediate subdirs renamed
+#   (Related to testcases 1e & 9f)
+#   Commit O: goal/{a,b}/$more_files
+#   Commit A: priority/{alpha,bravo}/$more_files
+#   Commit B: goal/{a,b}/$more_files, goal/c
+#   Expected: priority/{alpha,bravo}/$more_files, priority/c
+
+test_expect_success '9g-setup: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
+       test_create_repo 9g &&
+       (
+               cd 9g &&
+
+               mkdir -p goal/a &&
+               mkdir -p goal/b &&
+               echo foo >goal/a/foo &&
+               echo bar >goal/b/bar &&
+               echo baz >goal/b/baz &&
+               git add goal &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir priority &&
+               git mv goal/a/ priority/alpha &&
+               git mv goal/b/ priority/beta &&
+               rmdir goal/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo c >goal/c &&
+               git add goal/c &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_failure '9g-check: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
+       (
+               cd 9g &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       HEAD:priority/alpha/foo \
+                       HEAD:priority/beta/bar  \
+                       HEAD:priority/beta/baz  \
+                       HEAD:priority/c &&
+               git rev-parse >expect \
+                       O:goal/a/foo \
+                       O:goal/b/bar \
+                       O:goal/b/baz \
+                       B:goal/c &&
+               test_cmp expect actual &&
+               test_must_fail git rev-parse HEAD:goal/c
+       )
+'
+
+# Testcase 9h, Avoid implicit rename if involved as source on other side
+#   (Extremely closely related to testcase 3a)
+#   Commit O: z/{b,c,d_1}
+#   Commit A: z/{b,c,d_2}
+#   Commit B: y/{b,c}, x/d_1
+#   Expected: y/{b,c}, x/d_2
+#   NOTE: If we applied the z/ -> y/ rename to z/d, then we'd end up with
+#         a rename/rename(1to2) conflict (z/d -> y/d vs. x/d)
+test_expect_success '9h-setup: Avoid dir rename on merely modified path' '
+       test_create_repo 9h &&
+       (
+               cd 9h &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               printf "1\n2\n3\n4\n5\n6\n7\n8\nd\n" >z/d &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               test_tick &&
+               echo more >>z/d &&
+               git add z/d &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir y &&
+               mkdir x &&
+               git mv z/b y/ &&
+               git mv z/c y/ &&
+               git mv z/d x/ &&
+               rmdir z &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '9h-check: Avoid dir rename on merely modified path' '
+       (
+               cd 9h &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       HEAD:y/b HEAD:y/c HEAD:x/d &&
+               git rev-parse >expect \
+                       O:z/b    O:z/c    A:z/d &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# Rules suggested by section 9:
+#
+#   If the other side of history did a directory rename to a path that your
+#   side renamed away, then ignore that particular rename from the other
+#   side of history for any implicit directory renames.
+###########################################################################
+
+###########################################################################
+# SECTION 10: Handling untracked files
+#
+# unpack_trees(), upon which the recursive merge algorithm is based, aborts
+# the operation if untracked or dirty files would be deleted or overwritten
+# by the merge.  Unfortunately, unpack_trees() does not understand renames,
+# and if it doesn't abort, then it muddies up the working directory before
+# we even get to the point of detecting renames, so we need some special
+# handling, at least in the case of directory renames.
+###########################################################################
+
+# Testcase 10a, Overwrite untracked: normal rename/delete
+#   Commit O: z/{b,c_1}
+#   Commit A: z/b + untracked z/c + untracked z/d
+#   Commit B: z/{b,d_1}
+#   Expected: Aborted Merge +
+#       ERROR_MSG(untracked working tree files would be overwritten by merge)
+
+test_expect_success '10a-setup: Overwrite untracked with normal rename/delete' '
+       test_create_repo 10a &&
+       (
+               cd 10a &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git rm z/c &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z/c z/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '10a-check: Overwrite untracked with normal rename/delete' '
+       (
+               cd 10a &&
+
+               git checkout A^0 &&
+               echo very >z/c &&
+               echo important >z/d &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "The following untracked working tree files would be overwritten by merge" err &&
+
+               git ls-files -s >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 4 out &&
+
+               echo very >expect &&
+               test_cmp expect z/c &&
+
+               echo important >expect &&
+               test_cmp expect z/d &&
+
+               git rev-parse HEAD:z/b >actual &&
+               git rev-parse O:z/b >expect &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 10b, Overwrite untracked: dir rename + delete
+#   Commit O: z/{b,c_1}
+#   Commit A: y/b + untracked y/{c,d,e}
+#   Commit B: z/{b,d_1,e}
+#   Expected: Failed Merge; y/b + untracked y/c + untracked y/d on disk +
+#             z/c_1 -> z/d_1 rename recorded at stage 3 for y/d +
+#       ERROR_MSG(refusing to lose untracked file at 'y/d')
+
+test_expect_success '10b-setup: Overwrite untracked with dir rename + delete' '
+       test_create_repo 10b &&
+       (
+               cd 10b &&
+
+               mkdir z &&
+               echo b >z/b &&
+               echo c >z/c &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git rm z/c &&
+               git mv z/ y/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z/c z/d &&
+               echo e >z/e &&
+               git add z/e &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '10b-check: Overwrite untracked with dir rename + delete' '
+       (
+               cd 10b &&
+
+               git checkout A^0 &&
+               echo very >y/c &&
+               echo important >y/d &&
+               echo contents >y/e &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "CONFLICT (rename/delete).*Version B\^0 of y/d left in tree at y/d~B\^0" out &&
+               test_i18ngrep "Error: Refusing to lose untracked file at y/e; writing to y/e~B\^0 instead" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 5 out &&
+
+               git rev-parse >actual \
+                       :0:y/b :3:y/d :3:y/e &&
+               git rev-parse >expect \
+                       O:z/b  O:z/c  B:z/e &&
+               test_cmp expect actual &&
+
+               echo very >expect &&
+               test_cmp expect y/c &&
+
+               echo important >expect &&
+               test_cmp expect y/d &&
+
+               echo contents >expect &&
+               test_cmp expect y/e
+       )
+'
+
+# Testcase 10c, Overwrite untracked: dir rename/rename(1to2)
+#   Commit O: z/{a,b}, x/{c,d}
+#   Commit A: y/{a,b}, w/c, x/d + different untracked y/c
+#   Commit B: z/{a,b,c}, x/d
+#   Expected: Failed Merge; y/{a,b} + x/d + untracked y/c +
+#             CONFLICT(rename/rename) x/c -> w/c vs y/c +
+#             y/c~B^0 +
+#             ERROR_MSG(Refusing to lose untracked file at y/c)
+
+test_expect_success '10c-setup: Overwrite untracked with dir rename/rename(1to2)' '
+       test_create_repo 10c &&
+       (
+               cd 10c &&
+
+               mkdir z x &&
+               echo a >z/a &&
+               echo b >z/b &&
+               echo c >x/c &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               mkdir w &&
+               git mv x/c w/c &&
+               git mv z/ y/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/c z/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '10c-check: Overwrite untracked with dir rename/rename(1to2)' '
+       (
+               cd 10c &&
+
+               git checkout A^0 &&
+               echo important >y/c &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "CONFLICT (rename/rename)" out &&
+               test_i18ngrep "Refusing to lose untracked file at y/c; adding as y/c~B\^0 instead" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -u >out &&
+               test_line_count = 3 out &&
+               git ls-files -o >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       :0:y/a :0:y/b :0:x/d :1:x/c :2:w/c :3:y/c &&
+               git rev-parse >expect \
+                        O:z/a  O:z/b  O:x/d  O:x/c  O:x/c  O:x/c &&
+               test_cmp expect actual &&
+
+               git hash-object y/c~B^0 >actual &&
+               git rev-parse O:x/c >expect &&
+               test_cmp expect actual &&
+
+               echo important >expect &&
+               test_cmp expect y/c
+       )
+'
+
+# Testcase 10d, Delete untracked w/ dir rename/rename(2to1)
+#   Commit O: z/{a,b,c_1},        x/{d,e,f_2}
+#   Commit A: y/{a,b},            x/{d,e,f_2,wham_1} + untracked y/wham
+#   Commit B: z/{a,b,c_1,wham_2}, y/{d,e}
+#   Expected: Failed Merge; y/{a,b,d,e} + untracked y/{wham,wham~B^0,wham~HEAD}+
+#             CONFLICT(rename/rename) z/c_1 vs x/f_2 -> y/wham
+#             ERROR_MSG(Refusing to lose untracked file at y/wham)
+
+test_expect_success '10d-setup: Delete untracked with dir rename/rename(2to1)' '
+       test_create_repo 10d &&
+       (
+               cd 10d &&
+
+               mkdir z x &&
+               echo a >z/a &&
+               echo b >z/b &&
+               echo c >z/c &&
+               echo d >x/d &&
+               echo e >x/e &&
+               echo f >x/f &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z/c x/wham &&
+               git mv z/ y/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/f z/wham &&
+               git mv x/ y/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '10d-check: Delete untracked with dir rename/rename(2to1)' '
+       (
+               cd 10d &&
+
+               git checkout A^0 &&
+               echo important >y/wham &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "CONFLICT (rename/rename)" out &&
+               test_i18ngrep "Refusing to lose untracked file at y/wham" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       :0:y/a :0:y/b :0:y/d :0:y/e :2:y/wham :3:y/wham &&
+               git rev-parse >expect \
+                        O:z/a  O:z/b  O:x/d  O:x/e  O:z/c     O:x/f &&
+               test_cmp expect actual &&
+
+               test_must_fail git rev-parse :1:y/wham &&
+
+               echo important >expect &&
+               test_cmp expect y/wham &&
+
+               git hash-object >actual \
+                       y/wham~B^0 y/wham~HEAD &&
+               git rev-parse >expect \
+                       O:x/f      O:z/c &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 10e, Does git complain about untracked file that's not in the way?
+#   Commit O: z/{a,b}
+#   Commit A: y/{a,b} + untracked z/c
+#   Commit B: z/{a,b,c}
+#   Expected: y/{a,b,c} + untracked z/c
+
+test_expect_success '10e-setup: Does git complain about untracked file that is not really in the way?' '
+       test_create_repo 10e &&
+       (
+               cd 10e &&
+
+               mkdir z &&
+               echo a >z/a &&
+               echo b >z/b &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z/ y/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo c >z/c &&
+               git add z/c &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_failure '10e-check: Does git complain about untracked file that is not really in the way?' '
+       (
+               cd 10e &&
+
+               git checkout A^0 &&
+               mkdir z &&
+               echo random >z/c &&
+
+               git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep ! "following untracked working tree files would be overwritten by merge" err &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 3 out &&
+
+               git rev-parse >actual \
+                       :0:y/a :0:y/b :0:y/c &&
+               git rev-parse >expect \
+                        O:z/a  O:z/b  B:z/c &&
+               test_cmp expect actual &&
+
+               echo random >expect &&
+               test_cmp expect z/c
+       )
+'
+
+###########################################################################
+# SECTION 11: Handling dirty (not up-to-date) files
+#
+# unpack_trees(), upon which the recursive merge algorithm is based, aborts
+# the operation if untracked or dirty files would be deleted or overwritten
+# by the merge.  Unfortunately, unpack_trees() does not understand renames,
+# and if it doesn't abort, then it muddies up the working directory before
+# we even get to the point of detecting renames, so we need some special
+# handling.  This was true even of normal renames, but there are additional
+# codepaths that need special handling with directory renames.  Add
+# testcases for both renamed-by-directory-rename-detection and standard
+# rename cases.
+###########################################################################
+
+# Testcase 11a, Avoid losing dirty contents with simple rename
+#   Commit O: z/{a,b_v1},
+#   Commit A: z/{a,c_v1}, and z/c_v1 has uncommitted mods
+#   Commit B: z/{a,b_v2}
+#   Expected: ERROR_MSG(Refusing to lose dirty file at z/c) +
+#             z/a, staged version of z/c has sha1sum matching B:z/b_v2,
+#             z/c~HEAD with contents of B:z/b_v2,
+#             z/c with uncommitted mods on top of A:z/c_v1
+
+test_expect_success '11a-setup: Avoid losing dirty contents with simple rename' '
+       test_create_repo 11a &&
+       (
+               cd 11a &&
+
+               mkdir z &&
+               echo a >z/a &&
+               test_seq 1 10 >z/b &&
+               git add z &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z/b z/c &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo 11 >>z/b &&
+               git add z/b &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '11a-check: Avoid losing dirty contents with simple rename' '
+       (
+               cd 11a &&
+
+               git checkout A^0 &&
+               echo stuff >>z/c &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "Refusing to lose dirty file at z/c" out &&
+
+               test_seq 1 10 >expected &&
+               echo stuff >>expected &&
+               test_cmp expected z/c &&
+
+               git ls-files -s >out &&
+               test_line_count = 2 out &&
+               git ls-files -u >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       :0:z/a :2:z/c &&
+               git rev-parse >expect \
+                        O:z/a  B:z/b &&
+               test_cmp expect actual &&
+
+               git hash-object z/c~HEAD >actual &&
+               git rev-parse B:z/b >expect &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 11b, Avoid losing dirty file involved in directory rename
+#   Commit O: z/a,         x/{b,c_v1}
+#   Commit A: z/{a,c_v1},  x/b,       and z/c_v1 has uncommitted mods
+#   Commit B: y/a,         x/{b,c_v2}
+#   Expected: y/{a,c_v2}, x/b, z/c_v1 with uncommitted mods untracked,
+#             ERROR_MSG(Refusing to lose dirty file at z/c)
+
+
+test_expect_success '11b-setup: Avoid losing dirty file involved in directory rename' '
+       test_create_repo 11b &&
+       (
+               cd 11b &&
+
+               mkdir z x &&
+               echo a >z/a &&
+               echo b >x/b &&
+               test_seq 1 10 >x/c &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv x/c z/c &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z y &&
+               echo 11 >>x/c &&
+               git add x/c &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '11b-check: Avoid losing dirty file involved in directory rename' '
+       (
+               cd 11b &&
+
+               git checkout A^0 &&
+               echo stuff >>z/c &&
+
+               git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "Refusing to lose dirty file at z/c" out &&
+
+               grep -q stuff z/c &&
+               test_seq 1 10 >expected &&
+               echo stuff >>expected &&
+               test_cmp expected z/c &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -m >out &&
+               test_line_count = 0 out &&
+               git ls-files -o >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       :0:x/b :0:y/a :0:y/c &&
+               git rev-parse >expect \
+                        O:x/b  O:z/a  B:x/c &&
+               test_cmp expect actual &&
+
+               git hash-object y/c >actual &&
+               git rev-parse B:x/c >expect &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 11c, Avoid losing not-up-to-date with rename + D/F conflict
+#   Commit O: y/a,         x/{b,c_v1}
+#   Commit A: y/{a,c_v1},  x/b,       and y/c_v1 has uncommitted mods
+#   Commit B: y/{a,c/d},   x/{b,c_v2}
+#   Expected: Abort_msg("following files would be overwritten by merge") +
+#             y/c left untouched (still has uncommitted mods)
+
+test_expect_success '11c-setup: Avoid losing not-uptodate with rename + D/F conflict' '
+       test_create_repo 11c &&
+       (
+               cd 11c &&
+
+               mkdir y x &&
+               echo a >y/a &&
+               echo b >x/b &&
+               test_seq 1 10 >x/c &&
+               git add y x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv x/c y/c &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               mkdir y/c &&
+               echo d >y/c/d &&
+               echo 11 >>x/c &&
+               git add x/c y/c/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '11c-check: Avoid losing not-uptodate with rename + D/F conflict' '
+       (
+               cd 11c &&
+
+               git checkout A^0 &&
+               echo stuff >>y/c &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "following files would be overwritten by merge" err &&
+
+               grep -q stuff y/c &&
+               test_seq 1 10 >expected &&
+               echo stuff >>expected &&
+               test_cmp expected y/c &&
+
+               git ls-files -s >out &&
+               test_line_count = 3 out &&
+               git ls-files -u >out &&
+               test_line_count = 0 out &&
+               git ls-files -m >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 3 out
+       )
+'
+
+# Testcase 11d, Avoid losing not-up-to-date with rename + D/F conflict
+#   Commit O: z/a,         x/{b,c_v1}
+#   Commit A: z/{a,c_v1},  x/b,       and z/c_v1 has uncommitted mods
+#   Commit B: y/{a,c/d},   x/{b,c_v2}
+#   Expected: D/F: y/c_v2 vs y/c/d) +
+#             Warning_Msg("Refusing to lose dirty file at z/c) +
+#             y/{a,c~HEAD,c/d}, x/b, now-untracked z/c_v1 with uncommitted mods
+
+test_expect_success '11d-setup: Avoid losing not-uptodate with rename + D/F conflict' '
+       test_create_repo 11d &&
+       (
+               cd 11d &&
+
+               mkdir z x &&
+               echo a >z/a &&
+               echo b >x/b &&
+               test_seq 1 10 >x/c &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv x/c z/c &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv z y &&
+               mkdir y/c &&
+               echo d >y/c/d &&
+               echo 11 >>x/c &&
+               git add x/c y/c/d &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '11d-check: Avoid losing not-uptodate with rename + D/F conflict' '
+       (
+               cd 11d &&
+
+               git checkout A^0 &&
+               echo stuff >>z/c &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "Refusing to lose dirty file at z/c" out &&
+
+               grep -q stuff z/c &&
+               test_seq 1 10 >expected &&
+               echo stuff >>expected &&
+               test_cmp expected z/c
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+               git ls-files -u >out &&
+               test_line_count = 1 out &&
+               git ls-files -o >out &&
+               test_line_count = 5 out &&
+
+               git rev-parse >actual \
+                       :0:x/b :0:y/a :0:y/c/d :3:y/c &&
+               git rev-parse >expect \
+                        O:x/b  O:z/a  B:y/c/d  B:x/c &&
+               test_cmp expect actual &&
+
+               git hash-object y/c~HEAD >actual &&
+               git rev-parse B:x/c >expect &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 11e, Avoid deleting not-up-to-date with dir rename/rename(1to2)/add
+#   Commit O: z/{a,b},      x/{c_1,d}
+#   Commit A: y/{a,b,c_2},  x/d, w/c_1, and y/c_2 has uncommitted mods
+#   Commit B: z/{a,b,c_1},  x/d
+#   Expected: Failed Merge; y/{a,b} + x/d +
+#             CONFLICT(rename/rename) x/c_1 -> w/c_1 vs y/c_1 +
+#             ERROR_MSG(Refusing to lose dirty file at y/c)
+#             y/c~B^0 has O:x/c_1 contents
+#             y/c~HEAD has A:y/c_2 contents
+#             y/c has dirty file from before merge
+
+test_expect_success '11e-setup: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '
+       test_create_repo 11e &&
+       (
+               cd 11e &&
+
+               mkdir z x &&
+               echo a >z/a &&
+               echo b >z/b &&
+               echo c >x/c &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z/ y/ &&
+               echo different >y/c &&
+               mkdir w &&
+               git mv x/c w/ &&
+               git add y/c &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/c z/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '11e-check: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '
+       (
+               cd 11e &&
+
+               git checkout A^0 &&
+               echo mods >>y/c &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "CONFLICT (rename/rename)" out &&
+               test_i18ngrep "Refusing to lose dirty file at y/c" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 7 out &&
+               git ls-files -u >out &&
+               test_line_count = 4 out &&
+               git ls-files -o >out &&
+               test_line_count = 4 out &&
+
+               echo different >expected &&
+               echo mods >>expected &&
+               test_cmp expected y/c &&
+
+               git rev-parse >actual \
+                       :0:y/a :0:y/b :0:x/d :1:x/c :2:w/c :2:y/c :3:y/c &&
+               git rev-parse >expect \
+                        O:z/a  O:z/b  O:x/d  O:x/c  O:x/c  A:y/c  O:x/c &&
+               test_cmp expect actual &&
+
+               git hash-object >actual \
+                       y/c~B^0 y/c~HEAD &&
+               git rev-parse >expect \
+                       O:x/c   A:y/c &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 11f, Avoid deleting not-up-to-date w/ dir rename/rename(2to1)
+#   Commit O: z/{a,b},        x/{c_1,d_2}
+#   Commit A: y/{a,b,wham_1}, x/d_2, except y/wham has uncommitted mods
+#   Commit B: z/{a,b,wham_2}, x/c_1
+#   Expected: Failed Merge; y/{a,b} + untracked y/{wham~B^0,wham~B^HEAD} +
+#             y/wham with dirty changes from before merge +
+#             CONFLICT(rename/rename) x/c vs x/d -> y/wham
+#             ERROR_MSG(Refusing to lose dirty file at y/wham)
+
+test_expect_success '11f-setup: Avoid deleting not-uptodate with dir rename/rename(2to1)' '
+       test_create_repo 11f &&
+       (
+               cd 11f &&
+
+               mkdir z x &&
+               echo a >z/a &&
+               echo b >z/b &&
+               test_seq 1 10 >x/c &&
+               echo d >x/d &&
+               git add z x &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv z/ y/ &&
+               git mv x/c y/wham &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv x/d z/wham &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '11f-check: Avoid deleting not-uptodate with dir rename/rename(2to1)' '
+       (
+               cd 11f &&
+
+               git checkout A^0 &&
+               echo important >>y/wham &&
+
+               test_must_fail git merge -s recursive B^0 >out 2>err &&
+               test_i18ngrep "CONFLICT (rename/rename)" out &&
+               test_i18ngrep "Refusing to lose dirty file at y/wham" out &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+               git ls-files -u >out &&
+               test_line_count = 2 out &&
+               git ls-files -o >out &&
+               test_line_count = 4 out &&
+
+               test_seq 1 10 >expected &&
+               echo important >>expected &&
+               test_cmp expected y/wham &&
+
+               test_must_fail git rev-parse :1:y/wham &&
+               git hash-object >actual \
+                       y/wham~B^0 y/wham~HEAD &&
+               git rev-parse >expect \
+                       O:x/d      O:x/c &&
+               test_cmp expect actual &&
+
+               git rev-parse >actual \
+                       :0:y/a :0:y/b :2:y/wham :3:y/wham &&
+               git rev-parse >expect \
+                        O:z/a  O:z/b  O:x/c     O:x/d &&
+               test_cmp expect actual
+       )
+'
+
+###########################################################################
+# SECTION 12: Everything else
+#
+# Tests suggested by others.  Tests added after implementation completed
+# and submitted.  Grab bag.
+###########################################################################
+
+# Testcase 12a, Moving one directory hierarchy into another
+#   (Related to testcase 9a)
+#   Commit O: node1/{leaf1,leaf2}, node2/{leaf3,leaf4}
+#   Commit A: node1/{leaf1,leaf2,node2/{leaf3,leaf4}}
+#   Commit B: node1/{leaf1,leaf2,leaf5}, node2/{leaf3,leaf4,leaf6}
+#   Expected: node1/{leaf1,leaf2,leaf5,node2/{leaf3,leaf4,leaf6}}
+
+test_expect_success '12a-setup: Moving one directory hierarchy into another' '
+       test_create_repo 12a &&
+       (
+               cd 12a &&
+
+               mkdir -p node1 node2 &&
+               echo leaf1 >node1/leaf1 &&
+               echo leaf2 >node1/leaf2 &&
+               echo leaf3 >node2/leaf3 &&
+               echo leaf4 >node2/leaf4 &&
+               git add node1 node2 &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv node2/ node1/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               echo leaf5 >node1/leaf5 &&
+               echo leaf6 >node2/leaf6 &&
+               git add node1 node2 &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '12a-check: Moving one directory hierarchy into another' '
+       (
+               cd 12a &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 6 out &&
+
+               git rev-parse >actual \
+                       HEAD:node1/leaf1 HEAD:node1/leaf2 HEAD:node1/leaf5 \
+                       HEAD:node1/node2/leaf3 \
+                       HEAD:node1/node2/leaf4 \
+                       HEAD:node1/node2/leaf6 &&
+               git rev-parse >expect \
+                       O:node1/leaf1    O:node1/leaf2    B:node1/leaf5 \
+                       O:node2/leaf3 \
+                       O:node2/leaf4 \
+                       B:node2/leaf6 &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 12b, Moving two directory hierarchies into each other
+#   (Related to testcases 1c and 12c)
+#   Commit O: node1/{leaf1, leaf2}, node2/{leaf3, leaf4}
+#   Commit A: node1/{leaf1, leaf2, node2/{leaf3, leaf4}}
+#   Commit B: node2/{leaf3, leaf4, node1/{leaf1, leaf2}}
+#   Expected: node1/node2/node1/{leaf1, leaf2},
+#             node2/node1/node2/{leaf3, leaf4}
+#   NOTE: Without directory renames, we would expect
+#                   node2/node1/{leaf1, leaf2},
+#                   node1/node2/{leaf3, leaf4}
+#         with directory rename detection, we note that
+#             commit A renames node2/ -> node1/node2/
+#             commit B renames node1/ -> node2/node1/
+#         therefore, applying those directory renames to the initial result
+#         (making all four paths experience a transitive renaming), yields
+#         the expected result.
+#
+#         You may ask, is it weird to have two directories rename each other?
+#         To which, I can do no more than shrug my shoulders and say that
+#         even simple rules give weird results when given weird inputs.
+
+test_expect_success '12b-setup: Moving one directory hierarchy into another' '
+       test_create_repo 12b &&
+       (
+               cd 12b &&
+
+               mkdir -p node1 node2 &&
+               echo leaf1 >node1/leaf1 &&
+               echo leaf2 >node1/leaf2 &&
+               echo leaf3 >node2/leaf3 &&
+               echo leaf4 >node2/leaf4 &&
+               git add node1 node2 &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv node2/ node1/ &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv node1/ node2/ &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '12b-check: Moving one directory hierarchy into another' '
+       (
+               cd 12b &&
+
+               git checkout A^0 &&
+
+               git merge -s recursive B^0 &&
+
+               git ls-files -s >out &&
+               test_line_count = 4 out &&
+
+               git rev-parse >actual \
+                       HEAD:node1/node2/node1/leaf1 \
+                       HEAD:node1/node2/node1/leaf2 \
+                       HEAD:node2/node1/node2/leaf3 \
+                       HEAD:node2/node1/node2/leaf4 &&
+               git rev-parse >expect \
+                       O:node1/leaf1 \
+                       O:node1/leaf2 \
+                       O:node2/leaf3 \
+                       O:node2/leaf4 &&
+               test_cmp expect actual
+       )
+'
+
+# Testcase 12c, Moving two directory hierarchies into each other w/ content merge
+#   (Related to testcase 12b)
+#   Commit O: node1/{       leaf1_1, leaf2_1}, node2/{leaf3_1, leaf4_1}
+#   Commit A: node1/{       leaf1_2, leaf2_2,  node2/{leaf3_2, leaf4_2}}
+#   Commit B: node2/{node1/{leaf1_3, leaf2_3},        leaf3_3, leaf4_3}
+#   Expected: Content merge conflicts for each of:
+#               node1/node2/node1/{leaf1, leaf2},
+#               node2/node1/node2/{leaf3, leaf4}
+#   NOTE: This is *exactly* like 12c, except that every path is modified on
+#         each side of the merge.
+
+test_expect_success '12c-setup: Moving one directory hierarchy into another w/ content merge' '
+       test_create_repo 12c &&
+       (
+               cd 12c &&
+
+               mkdir -p node1 node2 &&
+               printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf1\n" >node1/leaf1 &&
+               printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf2\n" >node1/leaf2 &&
+               printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf3\n" >node2/leaf3 &&
+               printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf4\n" >node2/leaf4 &&
+               git add node1 node2 &&
+               test_tick &&
+               git commit -m "O" &&
+
+               git branch O &&
+               git branch A &&
+               git branch B &&
+
+               git checkout A &&
+               git mv node2/ node1/ &&
+               for i in `git ls-files`; do echo side A >>$i; done &&
+               git add -u &&
+               test_tick &&
+               git commit -m "A" &&
+
+               git checkout B &&
+               git mv node1/ node2/ &&
+               for i in `git ls-files`; do echo side B >>$i; done &&
+               git add -u &&
+               test_tick &&
+               git commit -m "B"
+       )
+'
+
+test_expect_success '12c-check: Moving one directory hierarchy into another w/ content merge' '
+       (
+               cd 12c &&
+
+               git checkout A^0 &&
+
+               test_must_fail git merge -s recursive B^0 &&
+
+               git ls-files -u >out &&
+               test_line_count = 12 out &&
+
+               git rev-parse >actual \
+                       :1:node1/node2/node1/leaf1 \
+                       :1:node1/node2/node1/leaf2 \
+                       :1:node2/node1/node2/leaf3 \
+                       :1:node2/node1/node2/leaf4 \
+                       :2:node1/node2/node1/leaf1 \
+                       :2:node1/node2/node1/leaf2 \
+                       :2:node2/node1/node2/leaf3 \
+                       :2:node2/node1/node2/leaf4 \
+                       :3:node1/node2/node1/leaf1 \
+                       :3:node1/node2/node1/leaf2 \
+                       :3:node2/node1/node2/leaf3 \
+                       :3:node2/node1/node2/leaf4 &&
+               git rev-parse >expect \
+                       O:node1/leaf1 \
+                       O:node1/leaf2 \
+                       O:node2/leaf3 \
+                       O:node2/leaf4 \
+                       A:node1/leaf1 \
+                       A:node1/leaf2 \
+                       A:node1/node2/leaf3 \
+                       A:node1/node2/leaf4 \
+                       B:node2/node1/leaf1 \
+                       B:node2/node1/leaf2 \
+                       B:node2/leaf3 \
+                       B:node2/leaf4 &&
+               test_cmp expect actual
+       )
+'
+
+test_done
index 9c422bcd7cc8c1b2525579ca3a7ffa6ee412ba42..dd8ab7ede182fc3c3da840fee083bf23a94ce13c 100755 (executable)
@@ -92,7 +92,7 @@ test_expect_success 'will not overwrite removed file with staged changes' '
        test_cmp important c1.c
 '
 
-test_expect_failure 'will not overwrite unstaged changes in renamed file' '
+test_expect_success 'will not overwrite unstaged changes in renamed file' '
        git reset --hard c1 &&
        git mv c1.c other.c &&
        git commit -m rename &&
index e73745051e505934b44be216b1d80a6c053c96de..79fd97074ec238714634de1c9e6144accdc710f3 100644 (file)
@@ -1509,8 +1509,8 @@ static int verify_uptodate_1(const struct cache_entry *ce,
                add_rejected_path(o, error_type, ce->name);
 }
 
-static int verify_uptodate(const struct cache_entry *ce,
-                          struct unpack_trees_options *o)
+int verify_uptodate(const struct cache_entry *ce,
+                   struct unpack_trees_options *o)
 {
        if (!o->skip_sparse_checkout && (ce->ce_flags & CE_NEW_SKIP_WORKTREE))
                return 0;
index 6c48117b845fbf7b983852be302e4472c5e6d651..41178ada94a4b7c5222cab7dd17d9eeb7a1956e4 100644 (file)
@@ -1,6 +1,7 @@
 #ifndef UNPACK_TREES_H
 #define UNPACK_TREES_H
 
+#include "tree-walk.h"
 #include "string-list.h"
 
 #define MAX_UNPACK_TREES 8
@@ -78,6 +79,9 @@ struct unpack_trees_options {
 extern int unpack_trees(unsigned n, struct tree_desc *t,
                struct unpack_trees_options *options);
 
+int verify_uptodate(const struct cache_entry *ce,
+                   struct unpack_trees_options *o);
+
 int threeway_merge(const struct cache_entry * const *stages,
                   struct unpack_trees_options *o);
 int twoway_merge(const struct cache_entry * const *src,