test-lib: introduce test_commit_bulk
[gitweb.git] / merge-recursive.c
index ada1c19ed2d1625479e95ac0bf651e8da4bd82d9..d2e380b7ed845e6fb2cd5616754a3f12a11c7b18 100644 (file)
@@ -163,6 +163,11 @@ static struct tree *shift_tree_object(struct repository *repo,
        return lookup_tree(repo, &shifted);
 }
 
+static inline void set_commit_tree(struct commit *c, struct tree *t)
+{
+       c->maybe_tree = t;
+}
+
 static struct commit *make_virtual_commit(struct repository *repo,
                                          struct tree *tree,
                                          const char *comment)
@@ -170,7 +175,7 @@ static struct commit *make_virtual_commit(struct repository *repo,
        struct commit *commit = alloc_commit_node(repo);
 
        set_merge_remote_desc(commit, comment, (struct object *)commit);
-       commit->maybe_tree = tree;
+       set_commit_tree(commit, tree);
        commit->object.parsed = 1;
        return commit;
 }
@@ -201,17 +206,22 @@ enum rename_type {
  * for these (temporary) data.
  */
 struct stage_data {
-       struct {
-               unsigned short mode;
-               struct object_id oid;
-       } stages[4];
+       struct diff_filespec stages[4]; /* mostly for oid & mode; maybe path */
        struct rename_conflict_info *rename_conflict_info;
        unsigned processed:1;
 };
 
 struct rename {
+       unsigned processed:1;
        struct diff_filepair *pair;
        const char *branch; /* branch that the rename occurred on */
+       /*
+        * If directory rename detection affected this rename, what was its
+        * original type ('A' or 'R') and it's original destination before
+        * the directory rename (otherwise, '\0' and NULL for these two vars).
+        */
+       char dir_rename_original_type;
+       char *dir_rename_original_dest;
        /*
         * Purpose of src_entry and dst_entry:
         *
@@ -233,8 +243,6 @@ struct rename {
         */
        struct stage_data *src_entry;
        struct stage_data *dst_entry;
-       unsigned add_turned_into_rename:1;
-       unsigned processed:1;
 };
 
 struct rename_conflict_info {
@@ -269,7 +277,6 @@ static inline void setup_rename_conflict_info(enum rename_type rename_type,
 
        ci->ren1->dst_entry->processed = 0;
        ci->ren1->dst_entry->rename_conflict_info = ci;
-
        if (ren2) {
                ci->ren2->dst_entry->rename_conflict_info = ci;
        }
@@ -326,14 +333,14 @@ static void output_commit_title(struct merge_options *opt, struct commit *commit
 }
 
 static int add_cacheinfo(struct merge_options *opt,
-                        unsigned int mode, const struct object_id *oid,
+                        const struct diff_filespec *blob,
                         const char *path, int stage, int refresh, int options)
 {
        struct index_state *istate = opt->repo->index;
        struct cache_entry *ce;
        int ret;
 
-       ce = make_cache_entry(istate, mode, oid ? oid : &null_oid, path, stage, 0);
+       ce = make_cache_entry(istate, blob->mode, &blob->oid, path, stage, 0);
        if (!ce)
                return err(opt, _("add_cacheinfo failed for path '%s'; merge aborting."), path);
 
@@ -464,15 +471,14 @@ static void get_files_dirs(struct merge_options *opt, struct tree *tree)
 
 static int get_tree_entry_if_blob(const struct object_id *tree,
                                  const char *path,
-                                 struct object_id *hashy,
-                                 unsigned short *mode_o)
+                                 struct diff_filespec *dfs)
 {
        int ret;
 
-       ret = get_tree_entry(tree, path, hashy, mode_o);
-       if (S_ISDIR(*mode_o)) {
-               oidcpy(hashy, &null_oid);
-               *mode_o = 0;
+       ret = get_tree_entry(tree, path, &dfs->oid, &dfs->mode);
+       if (S_ISDIR(dfs->mode)) {
+               oidcpy(&dfs->oid, &null_oid);
+               dfs->mode = 0;
        }
        return ret;
 }
@@ -487,12 +493,9 @@ 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_if_blob(&o->object.oid, path,
-                              &e->stages[1].oid, &e->stages[1].mode);
-       get_tree_entry_if_blob(&a->object.oid, path,
-                              &e->stages[2].oid, &e->stages[2].mode);
-       get_tree_entry_if_blob(&b->object.oid, path,
-                              &e->stages[3].oid, &e->stages[3].mode);
+       get_tree_entry_if_blob(&o->object.oid, path, &e->stages[1]);
+       get_tree_entry_if_blob(&a->object.oid, path, &e->stages[2]);
+       get_tree_entry_if_blob(&b->object.oid, path, &e->stages[3]);
        item = string_list_insert(entries, path);
        item->util = e;
        return e;
@@ -648,13 +651,13 @@ static int update_stages(struct merge_options *opt, const char *path,
                if (remove_file_from_index(opt->repo->index, path))
                        return -1;
        if (o)
-               if (add_cacheinfo(opt, o->mode, &o->oid, path, 1, 0, options))
+               if (add_cacheinfo(opt, o, path, 1, 0, options))
                        return -1;
        if (a)
-               if (add_cacheinfo(opt, a->mode, &a->oid, path, 2, 0, options))
+               if (add_cacheinfo(opt, a, path, 2, 0, options))
                        return -1;
        if (b)
-               if (add_cacheinfo(opt, b->mode, &b->oid, path, 3, 0, options))
+               if (add_cacheinfo(opt, b, path, 3, 0, options))
                        return -1;
        return 0;
 }
@@ -767,7 +770,7 @@ static int dir_in_way(struct index_state *istate, const char *path,
  * and its oid and mode match the specified values
  */
 static int was_tracked_and_matches(struct merge_options *opt, const char *path,
-                                  const struct object_id *oid, unsigned mode)
+                                  const struct diff_filespec *blob)
 {
        int pos = index_name_pos(&opt->orig_index, path, strlen(path));
        struct cache_entry *ce;
@@ -778,7 +781,7 @@ static int was_tracked_and_matches(struct merge_options *opt, const char *path,
 
        /* See if the file we were tracking before matches */
        ce = opt->orig_index.cache[pos];
-       return (oid_eq(&ce->oid, oid) && ce->ce_mode == mode);
+       return (oid_eq(&ce->oid, &blob->oid) && ce->ce_mode == blob->mode);
 }
 
 /*
@@ -903,8 +906,7 @@ static int make_room_for_path(struct merge_options *opt, const char *path)
 }
 
 static int update_file_flags(struct merge_options *opt,
-                            const struct object_id *oid,
-                            unsigned mode,
+                            const struct diff_filespec *contents,
                             const char *path,
                             int update_cache,
                             int update_wd)
@@ -919,7 +921,7 @@ static int update_file_flags(struct merge_options *opt,
                void *buf;
                unsigned long size;
 
-               if (S_ISGITLINK(mode)) {
+               if (S_ISGITLINK(contents->mode)) {
                        /*
                         * We may later decide to recursively descend into
                         * the submodule directory and update its index
@@ -929,14 +931,16 @@ static int update_file_flags(struct merge_options *opt,
                        goto update_index;
                }
 
-               buf = read_object_file(oid, &type, &size);
+               buf = read_object_file(&contents->oid, &type, &size);
                if (!buf)
-                       return err(opt, _("cannot read object %s '%s'"), oid_to_hex(oid), path);
+                       return err(opt, _("cannot read object %s '%s'"),
+                                  oid_to_hex(&contents->oid), path);
                if (type != OBJ_BLOB) {
-                       ret = err(opt, _("blob expected for %s '%s'"), oid_to_hex(oid), path);
+                       ret = err(opt, _("blob expected for %s '%s'"),
+                                 oid_to_hex(&contents->oid), path);
                        goto free_buf;
                }
-               if (S_ISREG(mode)) {
+               if (S_ISREG(contents->mode)) {
                        struct strbuf strbuf = STRBUF_INIT;
                        if (convert_to_working_tree(opt->repo->index, path, buf, size, &strbuf)) {
                                free(buf);
@@ -949,12 +953,11 @@ static int update_file_flags(struct merge_options *opt,
                        update_wd = 0;
                        goto free_buf;
                }
-               if (S_ISREG(mode) || (!has_symlinks && S_ISLNK(mode))) {
+               if (S_ISREG(contents->mode) ||
+                   (!has_symlinks && S_ISLNK(contents->mode))) {
                        int fd;
-                       if (mode & 0100)
-                               mode = 0777;
-                       else
-                               mode = 0666;
+                       int mode = (contents->mode & 0100 ? 0777 : 0666);
+
                        fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, mode);
                        if (fd < 0) {
                                ret = err(opt, _("failed to open '%s': %s"),
@@ -963,7 +966,7 @@ static int update_file_flags(struct merge_options *opt,
                        }
                        write_in_full(fd, buf, size);
                        close(fd);
-               } else if (S_ISLNK(mode)) {
+               } else if (S_ISLNK(contents->mode)) {
                        char *lnk = xmemdupz(buf, size);
                        safe_create_leading_directories_const(path);
                        unlink(path);
@@ -974,13 +977,13 @@ static int update_file_flags(struct merge_options *opt,
                } else
                        ret = err(opt,
                                  _("do not know what to do with %06o %s '%s'"),
-                                 mode, oid_to_hex(oid), path);
+                                 contents->mode, oid_to_hex(&contents->oid), path);
        free_buf:
                free(buf);
        }
 update_index:
        if (!ret && update_cache)
-               if (add_cacheinfo(opt, mode, oid, path, 0, update_wd,
+               if (add_cacheinfo(opt, contents, path, 0, update_wd,
                                  ADD_CACHE_OK_TO_ADD))
                        return -1;
        return ret;
@@ -988,18 +991,17 @@ static int update_file_flags(struct merge_options *opt,
 
 static int update_file(struct merge_options *opt,
                       int clean,
-                      const struct object_id *oid,
-                      unsigned mode,
+                      const struct diff_filespec *contents,
                       const char *path)
 {
-       return update_file_flags(opt, oid, mode, path, opt->call_depth || clean, !opt->call_depth);
+       return update_file_flags(opt, contents, path,
+                                opt->call_depth || clean, !opt->call_depth);
 }
 
 /* Low level file merging, update and removal */
 
 struct merge_file_info {
-       struct object_id oid;
-       unsigned mode;
+       struct diff_filespec blob; /* mostly use oid & mode; sometimes path */
        unsigned clean:1,
                 merge:1;
 };
@@ -1039,6 +1041,7 @@ static int merge_3way(struct merge_options *opt,
                }
        }
 
+       assert(a->path && b->path);
        if (strcmp(a->path, b->path) ||
            (opt->ancestor != NULL && strcmp(a->path, o->path) != 0)) {
                base_name = opt->ancestor == NULL ? NULL :
@@ -1078,7 +1081,7 @@ static int find_first_merges(struct repository *repo,
        struct commit *commit;
        int contains_another;
 
-       char merged_revision[42];
+       char merged_revision[GIT_MAX_HEXSZ + 2];
        const char *rev_args[] = { "rev-list", "--merges", "--ancestry-path",
                                   "--all", merged_revision, NULL };
        struct rev_info revs;
@@ -1140,6 +1143,11 @@ static void print_commit(struct commit *commit)
        strbuf_release(&sb);
 }
 
+static int is_valid(const struct diff_filespec *dfs)
+{
+       return dfs->mode != 0 && !is_null_oid(&dfs->oid);
+}
+
 static int merge_submodule(struct merge_options *opt,
                           struct object_id *result, const char *path,
                           const struct object_id *base, const struct object_id *a,
@@ -1278,11 +1286,11 @@ static int merge_mode_and_contents(struct merge_options *opt,
        if ((S_IFMT & a->mode) != (S_IFMT & b->mode)) {
                result->clean = 0;
                if (S_ISREG(a->mode)) {
-                       result->mode = a->mode;
-                       oidcpy(&result->oid, &a->oid);
+                       result->blob.mode = a->mode;
+                       oidcpy(&result->blob.oid, &a->oid);
                } else {
-                       result->mode = b->mode;
-                       oidcpy(&result->oid, &b->oid);
+                       result->blob.mode = b->mode;
+                       oidcpy(&result->blob.oid, &b->oid);
                }
        } else {
                if (!oid_eq(&a->oid, &o->oid) && !oid_eq(&b->oid, &o->oid))
@@ -1292,9 +1300,9 @@ static int merge_mode_and_contents(struct merge_options *opt,
                 * Merge modes
                 */
                if (a->mode == b->mode || a->mode == o->mode)
-                       result->mode = b->mode;
+                       result->blob.mode = b->mode;
                else {
-                       result->mode = a->mode;
+                       result->blob.mode = a->mode;
                        if (b->mode != o->mode) {
                                result->clean = 0;
                                result->merge = 1;
@@ -1302,9 +1310,9 @@ static int merge_mode_and_contents(struct merge_options *opt,
                }
 
                if (oid_eq(&a->oid, &b->oid) || oid_eq(&a->oid, &o->oid))
-                       oidcpy(&result->oid, &b->oid);
+                       oidcpy(&result->blob.oid, &b->oid);
                else if (oid_eq(&b->oid, &o->oid))
-                       oidcpy(&result->oid, &a->oid);
+                       oidcpy(&result->blob.oid, &a->oid);
                else if (S_ISREG(a->mode)) {
                        mmbuffer_t result_buf;
                        int ret = 0, merge_status;
@@ -1318,7 +1326,7 @@ static int merge_mode_and_contents(struct merge_options *opt,
 
                        if (!ret &&
                            write_object_file(result_buf.ptr, result_buf.size,
-                                             blob_type, &result->oid))
+                                             blob_type, &result->blob.oid))
                                ret = err(opt, _("Unable to add %s to database"),
                                          a->path);
 
@@ -1327,7 +1335,7 @@ static int merge_mode_and_contents(struct merge_options *opt,
                                return ret;
                        result->clean = (merge_status == 0);
                } else if (S_ISGITLINK(a->mode)) {
-                       result->clean = merge_submodule(opt, &result->oid,
+                       result->clean = merge_submodule(opt, &result->blob.oid,
                                                        o->path,
                                                        &o->oid,
                                                        &a->oid,
@@ -1335,15 +1343,15 @@ static int merge_mode_and_contents(struct merge_options *opt,
                } else if (S_ISLNK(a->mode)) {
                        switch (opt->recursive_variant) {
                        case MERGE_RECURSIVE_NORMAL:
-                               oidcpy(&result->oid, &a->oid);
+                               oidcpy(&result->blob.oid, &a->oid);
                                if (!oid_eq(&a->oid, &b->oid))
                                        result->clean = 0;
                                break;
                        case MERGE_RECURSIVE_OURS:
-                               oidcpy(&result->oid, &a->oid);
+                               oidcpy(&result->blob.oid, &a->oid);
                                break;
                        case MERGE_RECURSIVE_THEIRS:
-                               oidcpy(&result->oid, &b->oid);
+                               oidcpy(&result->blob.oid, &b->oid);
                                break;
                        }
                } else
@@ -1367,37 +1375,45 @@ static int handle_rename_via_dir(struct merge_options *opt,
         */
        const struct rename *ren = ci->ren1;
        const struct diff_filespec *dest = ren->pair->two;
+       char *file_path = dest->path;
+       int mark_conflicted = (opt->detect_directory_renames == 1);
+       assert(ren->dir_rename_original_dest);
 
        if (!opt->call_depth && would_lose_untracked(opt, dest->path)) {
-               char *alt_path = unique_path(opt, dest->path, ren->branch);
-
+               mark_conflicted = 1;
+               file_path = unique_path(opt, dest->path, ren->branch);
                output(opt, 1, _("Error: Refusing to lose untracked file at %s; "
-                              "writing to %s instead."),
-                      dest->path, alt_path);
+                                "writing to %s instead."),
+                      dest->path, file_path);
+       }
+
+       if (mark_conflicted) {
                /*
-                * 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.
+                * Write the file in worktree at file_path.  In the index,
+                * only record the file at dest->path in the appropriate
+                * higher stage.
                 */
-               if (update_file(opt, 0, &dest->oid, dest->mode, alt_path))
+               if (update_file(opt, 0, dest, file_path))
                        return -1;
-               free(alt_path);
-               return update_stages(opt, dest->path, NULL,
-                                    ren->branch == opt->branch1 ? dest : NULL,
-                                    ren->branch == opt->branch1 ? NULL : dest);
+               if (file_path != dest->path)
+                       free(file_path);
+               if (update_stages(opt, dest->path, NULL,
+                                 ren->branch == opt->branch1 ? dest : NULL,
+                                 ren->branch == opt->branch1 ? NULL : dest))
+                       return -1;
+               return 0; /* not clean, but conflicted */
+       } else {
+               /* Update dest->path both in index and in worktree */
+               if (update_file(opt, 1, dest, dest->path))
+                       return -1;
+               return 1; /* clean */
        }
-
-       /* Update dest->path both in index and in worktree */
-       if (update_file(opt, 1, &dest->oid, dest->mode, dest->path))
-               return -1;
-       return 0;
 }
 
 static int handle_change_delete(struct merge_options *opt,
                                const char *path, const char *old_path,
-                               const struct object_id *o_oid, int o_mode,
-                               const struct object_id *changed_oid,
-                               int changed_mode,
+                               const struct diff_filespec *o,
+                               const struct diff_filespec *changed,
                                const char *change_branch,
                                const char *delete_branch,
                                const char *change, const char *change_past)
@@ -1419,7 +1435,7 @@ static int handle_change_delete(struct merge_options *opt,
                 */
                ret = remove_file_from_index(opt->repo->index, path);
                if (!ret)
-                       ret = update_file(opt, 0, o_oid, o_mode, update_path);
+                       ret = update_file(opt, 0, o, update_path);
        } else {
                /*
                 * Despite the four nearly duplicate messages and argument
@@ -1468,7 +1484,7 @@ static int handle_change_delete(struct merge_options *opt,
                 * and update_wd=0, but that's a no-op.
                 */
                if (change_branch != opt->branch1 || alt_path)
-                       ret = update_file(opt, 0, changed_oid, changed_mode, update_path);
+                       ret = update_file(opt, 0, changed, update_path);
        }
        free(alt_path);
 
@@ -1488,8 +1504,7 @@ static int handle_rename_delete(struct merge_options *opt,
        if (handle_change_delete(opt,
                                 opt->call_depth ? orig->path : dest->path,
                                 opt->call_depth ? NULL : orig->path,
-                                &orig->oid, orig->mode,
-                                &dest->oid, dest->mode,
+                                orig, dest,
                                 rename_branch, delete_branch,
                                 _("rename"), _("renamed")))
                return -1;
@@ -1502,31 +1517,16 @@ static int handle_rename_delete(struct merge_options *opt,
                                     rename_branch == opt->branch1 ? NULL : dest);
 }
 
-static struct diff_filespec *filespec_from_entry(struct diff_filespec *target,
-                                                struct stage_data *entry,
-                                                int stage)
-{
-       struct object_id *oid = &entry->stages[stage].oid;
-       unsigned mode = entry->stages[stage].mode;
-       if (mode == 0 || is_null_oid(oid))
-               return NULL;
-       oidcpy(&target->oid, oid);
-       target->mode = mode;
-       return target;
-}
-
 static int handle_file_collision(struct merge_options *opt,
                                 const char *collide_path,
                                 const char *prev_path1,
                                 const char *prev_path2,
                                 const char *branch1, const char *branch2,
-                                const struct object_id *a_oid,
-                                unsigned int a_mode,
-                                const struct object_id *b_oid,
-                                unsigned int b_mode)
+                                struct diff_filespec *a,
+                                struct diff_filespec *b)
 {
        struct merge_file_info mfi;
-       struct diff_filespec null, a, b;
+       struct diff_filespec null;
        char *alt_path = NULL;
        const char *update_path = collide_path;
 
@@ -1540,29 +1540,28 @@ static int handle_file_collision(struct merge_options *opt,
                return handle_file_collision(opt, collide_path,
                                             prev_path2, prev_path1,
                                             branch2, branch1,
-                                            b_oid, b_mode,
-                                            a_oid, a_mode);
+                                            b, a);
        }
 
        /*
         * In the recursive case, we just opt to undo renames
         */
        if (opt->call_depth && (prev_path1 || prev_path2)) {
-               /* Put first file (a_oid, a_mode) in its original spot */
+               /* Put first file (a->oid, a->mode) in its original spot */
                if (prev_path1) {
-                       if (update_file(opt, 1, a_oid, a_mode, prev_path1))
+                       if (update_file(opt, 1, a, prev_path1))
                                return -1;
                } else {
-                       if (update_file(opt, 1, a_oid, a_mode, collide_path))
+                       if (update_file(opt, 1, a, collide_path))
                                return -1;
                }
 
-               /* Put second file (b_oid, b_mode) in its original spot */
+               /* Put second file (b->oid, b->mode) in its original spot */
                if (prev_path2) {
-                       if (update_file(opt, 1, b_oid, b_mode, prev_path2))
+                       if (update_file(opt, 1, b, prev_path2))
                                return -1;
                } else {
-                       if (update_file(opt, 1, b_oid, b_mode, collide_path))
+                       if (update_file(opt, 1, b, collide_path))
                                return -1;
                }
 
@@ -1616,26 +1615,18 @@ static int handle_file_collision(struct merge_options *opt,
        }
 
        /* Store things in diff_filespecs for functions that need it */
-       memset(&a, 0, sizeof(struct diff_filespec));
-       memset(&b, 0, sizeof(struct diff_filespec));
-       null.path = a.path = b.path = (char *)collide_path;
+       null.path = (char *)collide_path;
        oidcpy(&null.oid, &null_oid);
        null.mode = 0;
-       oidcpy(&a.oid, a_oid);
-       a.mode = a_mode;
-       a.oid_valid = 1;
-       oidcpy(&b.oid, b_oid);
-       b.mode = b_mode;
-       b.oid_valid = 1;
-
-       if (merge_mode_and_contents(opt, &null, &a, &b, collide_path,
+
+       if (merge_mode_and_contents(opt, &null, a, b, collide_path,
                                    branch1, branch2, opt->call_depth * 2, &mfi))
                return -1;
        mfi.clean &= !alt_path;
-       if (update_file(opt, mfi.clean, &mfi.oid, mfi.mode, update_path))
+       if (update_file(opt, mfi.clean, &mfi.blob, update_path))
                return -1;
        if (!mfi.clean && !opt->call_depth &&
-           update_stages(opt, collide_path, NULL, &a, &b))
+           update_stages(opt, collide_path, NULL, a, b))
                return -1;
        free(alt_path);
        /*
@@ -1654,7 +1645,6 @@ static int handle_rename_add(struct merge_options *opt,
        /* a was renamed to c, and a separate c was added. */
        struct diff_filespec *a = ci->ren1->pair->one;
        struct diff_filespec *c = ci->ren1->pair->two;
-       struct diff_filespec tmp;
        char *path = c->path;
        char *prev_path_desc;
        struct merge_file_info mfi;
@@ -1669,23 +1659,22 @@ static int handle_rename_add(struct merge_options *opt,
               a->path, c->path, rename_branch,
               c->path, add_branch);
 
-       filespec_from_entry(&tmp, ci->ren1->src_entry, other_stage);
-       tmp.path = a->path;
-
        prev_path_desc = xstrfmt("version of %s from %s", path, a->path);
-       if (merge_mode_and_contents(opt, a, c, &tmp,
+       ci->ren1->src_entry->stages[other_stage].path = a->path;
+       if (merge_mode_and_contents(opt, a, c,
+                                   &ci->ren1->src_entry->stages[other_stage],
                                    prev_path_desc,
                                    opt->branch1, opt->branch2,
                                    1 + opt->call_depth * 2, &mfi))
                return -1;
        free(prev_path_desc);
 
+       ci->ren1->dst_entry->stages[other_stage].path = mfi.blob.path = c->path;
        return handle_file_collision(opt,
                                     c->path, a->path, NULL,
                                     rename_branch, add_branch,
-                                    &mfi.oid, mfi.mode,
-                                    &ci->ren1->dst_entry->stages[other_stage].oid,
-                                    ci->ren1->dst_entry->stages[other_stage].mode);
+                                    &mfi.blob,
+                                    &ci->ren1->dst_entry->stages[other_stage]);
 }
 
 static char *find_path_for_conflict(struct merge_options *opt,
@@ -1714,7 +1703,6 @@ static int handle_rename_rename_1to2(struct merge_options *opt,
 {
        /* One file was renamed in both branches, but to different names. */
        struct merge_file_info mfi;
-       struct diff_filespec other;
        struct diff_filespec *add;
        struct diff_filespec *o = ci->ren1->pair->one;
        struct diff_filespec *a = ci->ren1->pair->two;
@@ -1743,7 +1731,7 @@ static int handle_rename_rename_1to2(struct merge_options *opt,
                 * pathname and then either rename the add-source file to that
                 * unique path, or use that unique path instead of src here.
                 */
-               if (update_file(opt, 0, &mfi.oid, mfi.mode, o->path))
+               if (update_file(opt, 0, &mfi.blob, o->path))
                        return -1;
 
                /*
@@ -1754,16 +1742,16 @@ static int handle_rename_rename_1to2(struct merge_options *opt,
                 * such cases, we should keep the added file around,
                 * resolving the conflict at that path in its favor.
                 */
-               add = filespec_from_entry(&other, ci->ren1->dst_entry, 2 ^ 1);
-               if (add) {
-                       if (update_file(opt, 0, &add->oid, add->mode, a->path))
+               add = &ci->ren1->dst_entry->stages[2 ^ 1];
+               if (is_valid(add)) {
+                       if (update_file(opt, 0, add, a->path))
                                return -1;
                }
                else
                        remove_file_from_index(opt->repo->index, a->path);
-               add = filespec_from_entry(&other, ci->ren2->dst_entry, 3 ^ 1);
-               if (add) {
-                       if (update_file(opt, 0, &add->oid, add->mode, b->path))
+               add = &ci->ren2->dst_entry->stages[3 ^ 1];
+               if (is_valid(add)) {
+                       if (update_file(opt, 0, add, b->path))
                                return -1;
                }
                else
@@ -1774,40 +1762,42 @@ static int handle_rename_rename_1to2(struct merge_options *opt,
                 * rename/add collision.  If not, we can write the file out
                 * to the specified location.
                 */
-               add = filespec_from_entry(&other, ci->ren1->dst_entry, 2 ^ 1);
-               if (add) {
+               add = &ci->ren1->dst_entry->stages[2 ^ 1];
+               if (is_valid(add)) {
+                       add->path = mfi.blob.path = a->path;
                        if (handle_file_collision(opt, a->path,
                                                  NULL, NULL,
                                                  ci->ren1->branch,
                                                  ci->ren2->branch,
-                                                 &mfi.oid, mfi.mode,
-                                                 &add->oid, add->mode) < 0)
+                                                 &mfi.blob, add) < 0)
                                return -1;
                } else {
                        char *new_path = find_path_for_conflict(opt, a->path,
                                                                ci->ren1->branch,
                                                                ci->ren2->branch);
-                       if (update_file(opt, 0, &mfi.oid, mfi.mode, new_path ? new_path : a->path))
+                       if (update_file(opt, 0, &mfi.blob,
+                                       new_path ? new_path : a->path))
                                return -1;
                        free(new_path);
                        if (update_stages(opt, a->path, NULL, a, NULL))
                                return -1;
                }
 
-               add = filespec_from_entry(&other, ci->ren2->dst_entry, 3 ^ 1);
-               if (add) {
+               add = &ci->ren2->dst_entry->stages[3 ^ 1];
+               if (is_valid(add)) {
+                       add->path = mfi.blob.path = b->path;
                        if (handle_file_collision(opt, b->path,
                                                  NULL, NULL,
                                                  ci->ren1->branch,
                                                  ci->ren2->branch,
-                                                 &add->oid, add->mode,
-                                                 &mfi.oid, mfi.mode) < 0)
+                                                 add, &mfi.blob) < 0)
                                return -1;
                } else {
                        char *new_path = find_path_for_conflict(opt, b->path,
                                                                ci->ren2->branch,
                                                                ci->ren1->branch);
-                       if (update_file(opt, 0, &mfi.oid, mfi.mode, new_path ? new_path : b->path))
+                       if (update_file(opt, 0, &mfi.blob,
+                                       new_path ? new_path : b->path))
                                return -1;
                        free(new_path);
                        if (update_stages(opt, b->path, NULL, NULL, b))
@@ -1826,12 +1816,12 @@ static int handle_rename_rename_2to1(struct merge_options *opt,
        struct diff_filespec *b = ci->ren2->pair->one;
        struct diff_filespec *c1 = ci->ren1->pair->two;
        struct diff_filespec *c2 = ci->ren2->pair->two;
-       struct diff_filespec tmp1, tmp2;
        char *path = c1->path; /* == c2->path */
        char *path_side_1_desc;
        char *path_side_2_desc;
        struct merge_file_info mfi_c1;
        struct merge_file_info mfi_c2;
+       int ostage1, ostage2;
 
        output(opt, 1, _("CONFLICT (rename/rename): "
               "Rename %s->%s in %s. "
@@ -1839,27 +1829,31 @@ static int handle_rename_rename_2to1(struct merge_options *opt,
               a->path, c1->path, ci->ren1->branch,
               b->path, c2->path, ci->ren2->branch);
 
-       filespec_from_entry(&tmp1, ci->ren1->src_entry, 3);
-       tmp1.path = a->path;
-       filespec_from_entry(&tmp2, ci->ren2->src_entry, 2);
-       tmp2.path = b->path;
-
        path_side_1_desc = xstrfmt("version of %s from %s", path, a->path);
        path_side_2_desc = xstrfmt("version of %s from %s", path, b->path);
-       if (merge_mode_and_contents(opt, a, c1, &tmp1, path_side_1_desc,
+       ostage1 = ci->ren1->branch == opt->branch1 ? 3 : 2;
+       ostage2 = ostage1 ^ 1;
+       ci->ren1->src_entry->stages[ostage1].path = a->path;
+       ci->ren2->src_entry->stages[ostage2].path = b->path;
+       if (merge_mode_and_contents(opt, a, c1,
+                                   &ci->ren1->src_entry->stages[ostage1],
+                                   path_side_1_desc,
                                    opt->branch1, opt->branch2,
                                    1 + opt->call_depth * 2, &mfi_c1) ||
-           merge_mode_and_contents(opt, b, &tmp2, c2, path_side_2_desc,
+           merge_mode_and_contents(opt, b,
+                                   &ci->ren2->src_entry->stages[ostage2],
+                                   c2, path_side_2_desc,
                                    opt->branch1, opt->branch2,
                                    1 + opt->call_depth * 2, &mfi_c2))
                return -1;
        free(path_side_1_desc);
        free(path_side_2_desc);
+       mfi_c1.blob.path = path;
+       mfi_c2.blob.path = path;
 
        return handle_file_collision(opt, path, a->path, b->path,
                                     ci->ren1->branch, ci->ren2->branch,
-                                    &mfi_c1.oid, mfi_c1.mode,
-                                    &mfi_c2.oid, mfi_c2.mode);
+                                    &mfi_c1.blob, &mfi_c2.blob);
 }
 
 /*
@@ -2511,16 +2505,18 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
                       &re->dst_entry->stages[stage].oid,
                       &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;
-       }
+       /*
+        * Record the original change status (or 'type' of change).  If it
+        * was originally an add ('A'), this lets us differentiate later
+        * between a RENAME_DELETE conflict and RENAME_VIA_DIR (they
+        * otherwise look the same).  If it was originally a rename ('R'),
+        * this lets us remember and report accurately about the transitive
+        * renaming that occurred via the directory rename detection.  Also,
+        * record the original destination name.
+        */
+       re->dir_rename_original_type = pair->status;
+       re->dir_rename_original_dest = pair->two->path;
+
        /*
         * We don't actually look at pair->status again, but it seems
         * pedagogically correct to adjust it.
@@ -2583,9 +2579,10 @@ static struct string_list *get_renames(struct merge_options *opt,
 
                re = xmalloc(sizeof(*re));
                re->processed = 0;
-               re->add_turned_into_rename = 0;
                re->pair = pair;
                re->branch = branch;
+               re->dir_rename_original_type = '\0';
+               re->dir_rename_original_dest = NULL;
                item = string_list_lookup(entries, re->pair->one->path);
                if (!item)
                        re->src_entry = insert_stage_data(re->pair->one->path,
@@ -2722,7 +2719,6 @@ static int process_renames(struct merge_options *opt,
 
                        setup_rename_conflict_info(RENAME_TWO_FILES_TO_ONE,
                                                   opt, ren1, ren2);
-
                } else {
                        /* Renamed in 1, maybe changed in 2 */
                        /* we only use sha1 and mode of these */
@@ -2754,7 +2750,7 @@ static int process_renames(struct merge_options *opt,
                        try_merge = 0;
 
                        if (oid_eq(&src_other.oid, &null_oid) &&
-                           ren1->add_turned_into_rename) {
+                           ren1->dir_rename_original_type == 'A') {
                                setup_rename_conflict_info(RENAME_VIA_DIR,
                                                           opt, ren1, NULL);
                        } else if (oid_eq(&src_other.oid, &null_oid)) {
@@ -2771,8 +2767,7 @@ static int process_renames(struct merge_options *opt,
                                 * update_file().
                                 */
                                if (update_file_flags(opt,
-                                                     &ren1->pair->two->oid,
-                                                     ren1->pair->two->mode,
+                                                     ren1->pair->two,
                                                      ren1_dst,
                                                      1, /* update_cache */
                                                      0  /* update_wd    */))
@@ -2923,11 +2918,6 @@ static void final_cleanup_renames(struct rename_info *re_info)
        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;
-}
-
 static int read_oid_strbuf(struct merge_options *opt,
                           const struct object_id *oid,
                           struct strbuf *dst)
@@ -2947,10 +2937,8 @@ static int read_oid_strbuf(struct merge_options *opt,
 }
 
 static int blob_unchanged(struct merge_options *opt,
-                         const struct object_id *o_oid,
-                         unsigned o_mode,
-                         const struct object_id *a_oid,
-                         unsigned a_mode,
+                         const struct diff_filespec *o,
+                         const struct diff_filespec *a,
                          int renormalize, const char *path)
 {
        struct strbuf obuf = STRBUF_INIT;
@@ -2958,16 +2946,15 @@ static int blob_unchanged(struct merge_options *opt,
        int ret = 0; /* assume changed for safety */
        const struct index_state *idx = opt->repo->index;
 
-       if (a_mode != o_mode)
+       if (a->mode != o->mode)
                return 0;
-       if (oid_eq(o_oid, a_oid))
+       if (oid_eq(&o->oid, &a->oid))
                return 1;
        if (!renormalize)
                return 0;
 
-       assert(o_oid && a_oid);
-       if (read_oid_strbuf(opt, o_oid, &obuf) ||
-           read_oid_strbuf(opt, a_oid, &abuf))
+       if (read_oid_strbuf(opt, &o->oid, &obuf) ||
+           read_oid_strbuf(opt, &a->oid, &abuf))
                goto error_return;
        /*
         * Note: binary | is used so that both renormalizations are
@@ -2986,83 +2973,53 @@ static int blob_unchanged(struct merge_options *opt,
 
 static int handle_modify_delete(struct merge_options *opt,
                                const char *path,
-                               struct object_id *o_oid, int o_mode,
-                               struct object_id *a_oid, int a_mode,
-                               struct object_id *b_oid, int b_mode)
+                               const struct diff_filespec *o,
+                               const struct diff_filespec *a,
+                               const struct diff_filespec *b)
 {
        const char *modify_branch, *delete_branch;
-       struct object_id *changed_oid;
-       int changed_mode;
+       const struct diff_filespec *changed;
 
-       if (a_oid) {
+       if (is_valid(a)) {
                modify_branch = opt->branch1;
                delete_branch = opt->branch2;
-               changed_oid = a_oid;
-               changed_mode = a_mode;
+               changed = a;
        } else {
                modify_branch = opt->branch2;
                delete_branch = opt->branch1;
-               changed_oid = b_oid;
-               changed_mode = b_mode;
+               changed = b;
        }
 
        return handle_change_delete(opt,
                                    path, NULL,
-                                   o_oid, o_mode,
-                                   changed_oid, changed_mode,
+                                   o, changed,
                                    modify_branch, delete_branch,
                                    _("modify"), _("modified"));
 }
 
-static int handle_content_merge(struct merge_options *opt,
+static int handle_content_merge(struct merge_file_info *mfi,
+                               struct merge_options *opt,
                                const char *path,
                                int is_dirty,
-                               struct object_id *o_oid, int o_mode,
-                               struct object_id *a_oid, int a_mode,
-                               struct object_id *b_oid, int b_mode,
+                               const struct diff_filespec *o,
+                               const struct diff_filespec *a,
+                               const struct diff_filespec *b,
                                struct rename_conflict_info *ci)
 {
        const char *reason = _("content");
-       const char *path1 = NULL, *path2 = NULL;
-       struct merge_file_info mfi;
-       struct diff_filespec one, a, b;
        unsigned df_conflict_remains = 0;
 
-       if (!o_oid) {
+       if (!is_valid(o))
                reason = _("add/add");
-               o_oid = (struct object_id *)&null_oid;
-       }
-       one.path = a.path = b.path = (char *)path;
-       oidcpy(&one.oid, o_oid);
-       one.mode = o_mode;
-       oidcpy(&a.oid, a_oid);
-       a.mode = a_mode;
-       oidcpy(&b.oid, b_oid);
-       b.mode = b_mode;
-
-       if (ci) {
-               struct diff_filepair *pair1 = ci->ren1->pair;
-
-               path1 = (opt->branch1 == ci->ren1->branch) ?
-                       pair1->two->path : pair1->one->path;
-               /* If ci->ren2->pair != NULL, we are in
-                * RENAME_ONE_FILE_TO_ONE case.  Otherwise, we have a
-                * normal rename.
-                */
-               path2 = ((ci->ren2 && ci->ren2->pair) ||
-                        opt->branch2 == ci->ren1->branch) ?
-                       pair1->two->path : pair1->one->path;
-               one.path = pair1->one->path;
-               a.path = (char *)path1;
-               b.path = (char *)path2;
-
-               if (dir_in_way(opt->repo->index, path, !opt->call_depth,
-                              S_ISGITLINK(pair1->two->mode)))
-                       df_conflict_remains = 1;
-       }
-       if (merge_mode_and_contents(opt, &one, &a, &b, path,
+
+       assert(o->path && a->path && b->path);
+       if (ci && dir_in_way(opt->repo->index, path, !opt->call_depth,
+                            S_ISGITLINK(ci->ren1->pair->two->mode)))
+               df_conflict_remains = 1;
+
+       if (merge_mode_and_contents(opt, o, a, b, path,
                                    opt->branch1, opt->branch2,
-                                   opt->call_depth * 2, &mfi))
+                                   opt->call_depth * 2, mfi))
                return -1;
 
        /*
@@ -3071,14 +3028,13 @@ static int handle_content_merge(struct merge_options *opt,
         *   b) The merge matches what was in HEAD (content, mode, pathname)
         *   c) The target path is usable (i.e. not involved in D/F conflict)
         */
-       if (mfi.clean &&
-           was_tracked_and_matches(opt, path, &mfi.oid, mfi.mode) &&
+       if (mfi->clean && was_tracked_and_matches(opt, path, &mfi->blob) &&
            !df_conflict_remains) {
                int pos;
                struct cache_entry *ce;
 
                output(opt, 3, _("Skipped %s (merged same as existing)"), path);
-               if (add_cacheinfo(opt, mfi.mode, &mfi.oid, path,
+               if (add_cacheinfo(opt, &mfi->blob, path,
                                  0, (!opt->call_depth && !is_dirty), 0))
                        return -1;
                /*
@@ -3094,16 +3050,16 @@ static int handle_content_merge(struct merge_options *opt,
                        ce = opt->repo->index->cache[pos];
                        ce->ce_flags |= CE_SKIP_WORKTREE;
                }
-               return mfi.clean;
+               return mfi->clean;
        }
 
-       if (!mfi.clean) {
-               if (S_ISGITLINK(mfi.mode))
+       if (!mfi->clean) {
+               if (S_ISGITLINK(mfi->blob.mode))
                        reason = _("submodule");
                output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
                                reason, path);
                if (ci && !df_conflict_remains)
-                       if (update_stages(opt, path, &one, &a, &b))
+                       if (update_stages(opt, path, o, a, b))
                                return -1;
        }
 
@@ -3112,18 +3068,15 @@ static int handle_content_merge(struct merge_options *opt,
                if (opt->call_depth) {
                        remove_file_from_index(opt->repo->index, path);
                } else {
-                       if (!mfi.clean) {
-                               if (update_stages(opt, path, &one, &a, &b))
+                       if (!mfi->clean) {
+                               if (update_stages(opt, path, o, a, b))
                                        return -1;
                        } else {
                                int file_from_stage2 = was_tracked(opt, path);
-                               struct diff_filespec merged;
-                               oidcpy(&merged.oid, &mfi.oid);
-                               merged.mode = mfi.mode;
 
                                if (update_stages(opt, path, NULL,
-                                                 file_from_stage2 ? &merged : NULL,
-                                                 file_from_stage2 ? NULL : &merged))
+                                                 file_from_stage2 ? &mfi->blob : NULL,
+                                                 file_from_stage2 ? NULL : &mfi->blob))
                                        return -1;
                        }
 
@@ -3134,28 +3087,106 @@ static int handle_content_merge(struct merge_options *opt,
                               path);
                }
                output(opt, 1, _("Adding as %s instead"), new_path);
-               if (update_file(opt, 0, &mfi.oid, mfi.mode, new_path)) {
+               if (update_file(opt, 0, &mfi->blob, new_path)) {
                        free(new_path);
                        return -1;
                }
                free(new_path);
-               mfi.clean = 0;
-       } else if (update_file(opt, mfi.clean, &mfi.oid, mfi.mode, path))
+               mfi->clean = 0;
+       } else if (update_file(opt, mfi->clean, &mfi->blob, path))
                return -1;
-       return !is_dirty && mfi.clean;
+       return !is_dirty && mfi->clean;
 }
 
 static int handle_rename_normal(struct merge_options *opt,
                                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,
+                               const struct diff_filespec *o,
+                               const struct diff_filespec *a,
+                               const struct diff_filespec *b,
                                struct rename_conflict_info *ci)
 {
+       struct rename *ren = ci->ren1;
+       struct merge_file_info mfi;
+       int clean;
+       int side = (ren->branch == opt->branch1 ? 2 : 3);
+
        /* Merge the content and write it out */
-       return handle_content_merge(opt, path, was_dirty(opt, path),
-                                   o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
-                                   ci);
+       clean = handle_content_merge(&mfi, opt, path, was_dirty(opt, path),
+                                    o, a, b, ci);
+
+       if (clean && opt->detect_directory_renames == 1 &&
+           ren->dir_rename_original_dest) {
+               if (update_stages(opt, path,
+                                 NULL,
+                                 side == 2 ? &mfi.blob : NULL,
+                                 side == 2 ? NULL : &mfi.blob))
+                       return -1;
+               clean = 0; /* not clean, but conflicted */
+       }
+       return clean;
+}
+
+static void dir_rename_warning(const char *msg,
+                              int is_add,
+                              int clean,
+                              struct merge_options *opt,
+                              struct rename *ren)
+{
+       const char *other_branch;
+       other_branch = (ren->branch == opt->branch1 ?
+                       opt->branch2 : opt->branch1);
+       if (is_add) {
+               output(opt, clean ? 2 : 1, msg,
+                      ren->pair->one->path, ren->branch,
+                      other_branch, ren->pair->two->path);
+               return;
+       }
+       output(opt, clean ? 2 : 1, msg,
+              ren->pair->one->path, ren->dir_rename_original_dest, ren->branch,
+              other_branch, ren->pair->two->path);
+}
+static int warn_about_dir_renamed_entries(struct merge_options *opt,
+                                         struct rename *ren)
+{
+       const char *msg;
+       int clean = 1, is_add;
+
+       if (!ren)
+               return clean;
+
+       /* Return early if ren was not affected/created by a directory rename */
+       if (!ren->dir_rename_original_dest)
+               return clean;
+
+       /* Sanity checks */
+       assert(opt->detect_directory_renames > 0);
+       assert(ren->dir_rename_original_type == 'A' ||
+              ren->dir_rename_original_type == 'R');
+
+       /* Check whether to treat directory renames as a conflict */
+       clean = (opt->detect_directory_renames == 2);
+
+       is_add = (ren->dir_rename_original_type == 'A');
+       if (ren->dir_rename_original_type == 'A' && clean) {
+               msg = _("Path updated: %s added in %s inside a "
+                       "directory that was renamed in %s; moving it to %s.");
+       } else if (ren->dir_rename_original_type == 'A' && !clean) {
+               msg = _("CONFLICT (file location): %s added in %s "
+                       "inside a directory that was renamed in %s, "
+                       "suggesting it should perhaps be moved to %s.");
+       } else if (ren->dir_rename_original_type == 'R' && clean) {
+               msg = _("Path updated: %s renamed to %s in %s, inside a "
+                       "directory that was renamed in %s; moving it to %s.");
+       } else if (ren->dir_rename_original_type == 'R' && !clean) {
+               msg = _("CONFLICT (file location): %s renamed to %s in %s, "
+                       "inside a directory that was renamed in %s, "
+                       "suggesting it should perhaps be moved to %s.");
+       } else {
+               BUG("Impossible dir_rename_original_type/clean combination");
+       }
+       dir_rename_warning(msg, is_add, clean, opt, ren);
+
+       return clean;
 }
 
 /* Per entry merge function */
@@ -3164,30 +3195,44 @@ static int process_entry(struct merge_options *opt,
 {
        int clean_merge = 1;
        int normalize = opt->renormalize;
-       unsigned o_mode = entry->stages[1].mode;
-       unsigned a_mode = entry->stages[2].mode;
-       unsigned b_mode = entry->stages[3].mode;
-       struct object_id *o_oid = stage_oid(&entry->stages[1].oid, o_mode);
-       struct object_id *a_oid = stage_oid(&entry->stages[2].oid, a_mode);
-       struct object_id *b_oid = stage_oid(&entry->stages[3].oid, b_mode);
+
+       struct diff_filespec *o = &entry->stages[1];
+       struct diff_filespec *a = &entry->stages[2];
+       struct diff_filespec *b = &entry->stages[3];
+       int o_valid = is_valid(o);
+       int a_valid = is_valid(a);
+       int b_valid = is_valid(b);
+       o->path = a->path = b->path = (char*)path;
 
        entry->processed = 1;
        if (entry->rename_conflict_info) {
                struct rename_conflict_info *ci = entry->rename_conflict_info;
+               struct diff_filespec *temp;
+               int path_clean;
+
+               path_clean = warn_about_dir_renamed_entries(opt, ci->ren1);
+               path_clean &= warn_about_dir_renamed_entries(opt, ci->ren2);
+
+               /*
+                * For cases with a single rename, {o,a,b}->path have all been
+                * set to the rename target path; we need to set two of these
+                * back to the rename source.
+                * For rename/rename conflicts, we'll manually fix paths below.
+                */
+               temp = (opt->branch1 == ci->ren1->branch) ? b : a;
+               o->path = temp->path = ci->ren1->pair->one->path;
+               if (ci->ren2) {
+                       assert(opt->branch1 == ci->ren1->branch);
+               }
+
                switch (ci->rename_type) {
                case RENAME_NORMAL:
                case RENAME_ONE_FILE_TO_ONE:
-                       clean_merge = handle_rename_normal(opt,
-                                                          path,
-                                                          o_oid, o_mode,
-                                                          a_oid, a_mode,
-                                                          b_oid, b_mode,
+                       clean_merge = handle_rename_normal(opt, path, o, a, b,
                                                           ci);
                        break;
                case RENAME_VIA_DIR:
-                       clean_merge = 1;
-                       if (handle_rename_via_dir(opt, ci))
-                               clean_merge = -1;
+                       clean_merge = handle_rename_via_dir(opt, ci);
                        break;
                case RENAME_ADD:
                        /*
@@ -3204,11 +3249,27 @@ static int process_entry(struct merge_options *opt,
                                clean_merge = -1;
                        break;
                case RENAME_ONE_FILE_TO_TWO:
+                       /*
+                        * Manually fix up paths; note:
+                        * ren[12]->pair->one->path are equal.
+                        */
+                       o->path = ci->ren1->pair->one->path;
+                       a->path = ci->ren1->pair->two->path;
+                       b->path = ci->ren2->pair->two->path;
+
                        clean_merge = 0;
                        if (handle_rename_rename_1to2(opt, ci))
                                clean_merge = -1;
                        break;
                case RENAME_TWO_FILES_TO_ONE:
+                       /*
+                        * Manually fix up paths; note,
+                        * ren[12]->pair->two->path are actually equal.
+                        */
+                       o->path = NULL;
+                       a->path = ci->ren1->pair->two->path;
+                       b->path = ci->ren2->pair->two->path;
+
                        /*
                         * Probably unclean merge, but if the two renamed
                         * files merge cleanly and the two resulting files
@@ -3221,57 +3282,55 @@ static int process_entry(struct merge_options *opt,
                        entry->processed = 0;
                        break;
                }
-       } else if (o_oid && (!a_oid || !b_oid)) {
+               if (path_clean < clean_merge)
+                       clean_merge = path_clean;
+       } else if (o_valid && (!a_valid || !b_valid)) {
                /* Case A: Deleted in one */
-               if ((!a_oid && !b_oid) ||
-                   (!b_oid && blob_unchanged(opt, o_oid, o_mode, a_oid, a_mode, normalize, path)) ||
-                   (!a_oid && blob_unchanged(opt, o_oid, o_mode, b_oid, b_mode, normalize, path))) {
+               if ((!a_valid && !b_valid) ||
+                   (!b_valid && blob_unchanged(opt, o, a, normalize, path)) ||
+                   (!a_valid && blob_unchanged(opt, o, b, normalize, path))) {
                        /* Deleted in both or deleted in one and
                         * unchanged in the other */
-                       if (a_oid)
+                       if (a_valid)
                                output(opt, 2, _("Removing %s"), path);
                        /* do not touch working file if it did not exist */
-                       remove_file(opt, 1, path, !a_oid);
+                       remove_file(opt, 1, path, !a_valid);
                } else {
                        /* Modify/delete; deleted side may have put a directory in the way */
                        clean_merge = 0;
-                       if (handle_modify_delete(opt, path, o_oid, o_mode,
-                                                a_oid, a_mode, b_oid, b_mode))
+                       if (handle_modify_delete(opt, path, o, a, b))
                                clean_merge = -1;
                }
-       } else if ((!o_oid && a_oid && !b_oid) ||
-                  (!o_oid && !a_oid && b_oid)) {
+       } else if ((!o_valid && a_valid && !b_valid) ||
+                  (!o_valid && !a_valid && b_valid)) {
                /* Case B: Added in one. */
                /* [nothing|directory] -> ([nothing|directory], file) */
 
                const char *add_branch;
                const char *other_branch;
-               unsigned mode;
-               const struct object_id *oid;
                const char *conf;
+               const struct diff_filespec *contents;
 
-               if (a_oid) {
+               if (a_valid) {
                        add_branch = opt->branch1;
                        other_branch = opt->branch2;
-                       mode = a_mode;
-                       oid = a_oid;
+                       contents = a;
                        conf = _("file/directory");
                } else {
                        add_branch = opt->branch2;
                        other_branch = opt->branch1;
-                       mode = b_mode;
-                       oid = b_oid;
+                       contents = b;
                        conf = _("directory/file");
                }
                if (dir_in_way(opt->repo->index, path,
-                              !opt->call_depth && !S_ISGITLINK(a_mode),
+                              !opt->call_depth && !S_ISGITLINK(a->mode),
                               0)) {
                        char *new_path = unique_path(opt, path, add_branch);
                        clean_merge = 0;
                        output(opt, 1, _("CONFLICT (%s): There is a directory with name %s in %s. "
                               "Adding %s as %s"),
                               conf, path, other_branch, path, new_path);
-                       if (update_file(opt, 0, oid, mode, new_path))
+                       if (update_file(opt, 0, contents, new_path))
                                clean_merge = -1;
                        else if (opt->call_depth)
                                remove_file_from_index(opt->repo->index, path);
@@ -3279,11 +3338,11 @@ static int process_entry(struct merge_options *opt,
                } else {
                        output(opt, 2, _("Adding %s"), path);
                        /* do not overwrite file if already present */
-                       if (update_file_flags(opt, oid, mode, path, 1, !a_oid))
+                       if (update_file_flags(opt, contents, path, 1, !a_valid))
                                clean_merge = -1;
                }
-       } else if (a_oid && b_oid) {
-               if (!o_oid) {
+       } else if (a_valid && b_valid) {
+               if (!o_valid) {
                        /* Case C: Added in both (check for same permissions) */
                        output(opt, 1,
                               _("CONFLICT (add/add): Merge conflict in %s"),
@@ -3292,24 +3351,21 @@ static int process_entry(struct merge_options *opt,
                                                            path, NULL, NULL,
                                                            opt->branch1,
                                                            opt->branch2,
-                                                           a_oid, a_mode,
-                                                           b_oid, b_mode);
+                                                           a, b);
                } else {
                        /* case D: Modified in both, but differently. */
+                       struct merge_file_info mfi;
                        int is_dirty = 0; /* unpack_trees would have bailed if dirty */
-                       clean_merge = handle_content_merge(opt, path,
+                       clean_merge = handle_content_merge(&mfi, opt, path,
                                                           is_dirty,
-                                                          o_oid, o_mode,
-                                                          a_oid, a_mode,
-                                                          b_oid, b_mode,
-                                                          NULL);
+                                                          o, a, b, NULL);
                }
-       } else if (!o_oid && !a_oid && !b_oid) {
+       } else if (!o_valid && !a_valid && !b_valid) {
                /*
                 * this entry was deleted altogether. a_mode == 0 means
                 * we had that path and want to actively remove it.
                 */
-               remove_file(opt, 1, path, !a_mode);
+               remove_file(opt, 1, path, !a->mode);
        } else
                BUG("fatal merge failure, shouldn't happen.");
 
@@ -3599,6 +3655,15 @@ static void merge_recursive_config(struct merge_options *opt)
                opt->merge_detect_rename = git_config_rename("merge.renames", value);
                free(value);
        }
+       if (!git_config_get_string("merge.directoryrenames", &value)) {
+               int boolval = git_parse_maybe_bool(value);
+               if (0 <= boolval) {
+                       opt->detect_directory_renames = boolval ? 2 : 0;
+               } else if (!strcasecmp(value, "conflict")) {
+                       opt->detect_directory_renames = 1;
+               } /* avoid erroring on values from future versions of git */
+               free(value);
+       }
        git_config(git_xmerge_config, NULL);
 }