Merge branch 'en/merge-recursive'
authorJunio C Hamano <gitster@pobox.com>
Tue, 30 Nov 2010 01:52:35 +0000 (17:52 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 30 Nov 2010 01:52:35 +0000 (17:52 -0800)
* en/merge-recursive: (41 commits)
t6022: Use -eq not = to test output of wc -l
merge-recursive:make_room_for_directories - work around dumb compilers
merge-recursive: Remove redundant path clearing for D/F conflicts
merge-recursive: Make room for directories in D/F conflicts
handle_delete_modify(): Check whether D/F conflicts are still present
merge_content(): Check whether D/F conflicts are still present
conflict_rename_rename_1to2(): Fix checks for presence of D/F conflicts
conflict_rename_delete(): Check whether D/F conflicts are still present
merge-recursive: Delay modify/delete conflicts if D/F conflict present
merge-recursive: Delay content merging for renames
merge-recursive: Delay handling of rename/delete conflicts
merge-recursive: Move handling of double rename of one file to other file
merge-recursive: Move handling of double rename of one file to two
merge-recursive: Avoid doubly merging rename/add conflict contents
merge-recursive: Update merge_content() call signature
merge-recursive: Update conflict_rename_rename_1to2() call signature
merge-recursive: Structure process_df_entry() to handle more cases
merge-recursive: Have process_entry() skip D/F or rename entries
merge-recursive: New function to assist resolving renames in-core only
merge-recursive: New data structures for deferring of D/F conflicts
...

Conflicts:
t/t6020-merge-df.sh
t/t6036-recursive-corner-cases.sh

1  2 
merge-recursive.c
t/t3030-merge-recursive.sh
t/t6020-merge-df.sh
t/t6022-merge-rename.sh
t/t6036-recursive-corner-cases.sh
diff --combined merge-recursive.c
index 875859f68efaab5113785896326140c8d0543f45,231e5cbd7f87c95f909709cc649bfb202a4ece87..16c2dbeab95a0f4099e3de8badf688bb4dee8189
@@@ -63,6 -63,22 +63,22 @@@ static int sha_eq(const unsigned char *
        return a && b && hashcmp(a, b) == 0;
  }
  
+ enum rename_type {
+       RENAME_NORMAL = 0,
+       RENAME_DELETE,
+       RENAME_ONE_FILE_TO_TWO
+ };
+ struct rename_df_conflict_info {
+       enum rename_type rename_type;
+       struct diff_filepair *pair1;
+       struct diff_filepair *pair2;
+       const char *branch1;
+       const char *branch2;
+       struct stage_data *dst_entry1;
+       struct stage_data *dst_entry2;
+ };
  /*
   * Since we want to write the index eventually, we cannot reuse the index
   * for these (temporary) data.
@@@ -74,9 -90,37 +90,37 @@@ struct stage_dat
                unsigned mode;
                unsigned char sha[20];
        } stages[4];
+       struct rename_df_conflict_info *rename_df_conflict_info;
        unsigned processed:1;
  };
  
+ static inline void setup_rename_df_conflict_info(enum rename_type rename_type,
+                                                struct diff_filepair *pair1,
+                                                struct diff_filepair *pair2,
+                                                const char *branch1,
+                                                const char *branch2,
+                                                struct stage_data *dst_entry1,
+                                                struct stage_data *dst_entry2)
+ {
+       struct rename_df_conflict_info *ci = xcalloc(1, sizeof(struct rename_df_conflict_info));
+       ci->rename_type = rename_type;
+       ci->pair1 = pair1;
+       ci->branch1 = branch1;
+       ci->branch2 = branch2;
+       ci->dst_entry1 = dst_entry1;
+       dst_entry1->rename_df_conflict_info = ci;
+       dst_entry1->processed = 0;
+       assert(!pair2 == !dst_entry2);
+       if (dst_entry2) {
+               ci->dst_entry2 = dst_entry2;
+               ci->pair2 = pair2;
+               dst_entry2->rename_df_conflict_info = ci;
+               dst_entry2->processed = 0;
+       }
+ }
  static int show(struct merge_options *o, int v)
  {
        return (!o->call_depth && o->verbosity >= v) || o->verbosity >= 5;
@@@ -302,6 -346,63 +346,63 @@@ static struct string_list *get_unmerged
        return unmerged;
  }
  
+ static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
+                                                     struct string_list *entries)
+ {
+       /* If there are D/F conflicts, and the paths currently exist
+        * in the working copy as a file, we want to remove them to
+        * make room for the corresponding directory.  Such paths will
+        * later be processed in process_df_entry() at the end.  If
+        * the corresponding directory ends up being removed by the
+        * merge, then the file will be reinstated at that time;
+        * otherwise, if the file is not supposed to be removed by the
+        * merge, the contents of the file will be placed in another
+        * unique filename.
+        *
+        * NOTE: This function relies on the fact that entries for a
+        * D/F conflict will appear adjacent in the index, with the
+        * entries for the file appearing before entries for paths
+        * below the corresponding directory.
+        */
+       const char *last_file = NULL;
+       int last_len = 0;
+       struct stage_data *last_e;
+       int i;
+       for (i = 0; i < entries->nr; i++) {
+               const char *path = entries->items[i].string;
+               int len = strlen(path);
+               struct stage_data *e = entries->items[i].util;
+               /*
+                * Check if last_file & path correspond to a D/F conflict;
+                * i.e. whether path is last_file+'/'+<something>.
+                * If so, remove last_file to make room for path and friends.
+                */
+               if (last_file &&
+                   len > last_len &&
+                   memcmp(path, last_file, last_len) == 0 &&
+                   path[last_len] == '/') {
+                       output(o, 3, "Removing %s to make room for subdirectory; may re-add later.", last_file);
+                       unlink(last_file);
+               }
+               /*
+                * Determine whether path could exist as a file in the
+                * working directory as a possible D/F conflict.  This
+                * will only occur when it exists in stage 2 as a
+                * file.
+                */
+               if (S_ISREG(e->stages[2].mode) || S_ISLNK(e->stages[2].mode)) {
+                       last_file = path;
+                       last_len = len;
+                       last_e = e;
+               } else {
+                       last_file = NULL;
+               }
+       }
+ }
  struct rename
  {
        struct diff_filepair *pair;
@@@ -334,7 -435,6 +435,7 @@@ static struct string_list *get_renames(
        opts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :
                            o->diff_rename_limit >= 0 ? o->diff_rename_limit :
                            500;
 +      opts.rename_score = o->rename_score;
        opts.warn_on_too_large_rename = 1;
        opts.output_format = DIFF_FORMAT_NO_OUTPUT;
        if (diff_setup_done(&opts) < 0)
        return renames;
  }
  
- static int update_stages(const char *path, struct diff_filespec *o,
+ static int update_stages_options(const char *path, struct diff_filespec *o,
                         struct diff_filespec *a, struct diff_filespec *b,
-                        int clear)
+                        int clear, int options)
  {
-       int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
        if (clear)
                if (remove_file_from_cache(path))
                        return -1;
        return 0;
  }
  
+ static int update_stages(const char *path, struct diff_filespec *o,
+                        struct diff_filespec *a, struct diff_filespec *b,
+                        int clear)
+ {
+       int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
+       return update_stages_options(path, o, a, b, clear, options);
+ }
+ static int update_stages_and_entry(const char *path,
+                                  struct stage_data *entry,
+                                  struct diff_filespec *o,
+                                  struct diff_filespec *a,
+                                  struct diff_filespec *b,
+                                  int clear)
+ {
+       int options;
+       entry->processed = 0;
+       entry->stages[1].mode = o->mode;
+       entry->stages[2].mode = a->mode;
+       entry->stages[3].mode = b->mode;
+       hashcpy(entry->stages[1].sha, o->sha1);
+       hashcpy(entry->stages[2].sha, a->sha1);
+       hashcpy(entry->stages[3].sha, b->sha1);
+       options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_SKIP_DFCHECK;
+       return update_stages_options(path, o, a, b, clear, options);
+ }
  static int remove_file(struct merge_options *o, int clean,
                       const char *path, int no_wd)
  {
@@@ -606,26 -733,22 +734,26 @@@ static int merge_3way(struct merge_opti
                      const char *branch2)
  {
        mmfile_t orig, src1, src2;
 +      struct ll_merge_options ll_opts = {0};
        char *base_name, *name1, *name2;
        int merge_status;
 -      int favor;
  
 -      if (o->call_depth)
 -              favor = 0;
 -      else {
 +      ll_opts.renormalize = o->renormalize;
 +      ll_opts.xdl_opts = o->xdl_opts;
 +
 +      if (o->call_depth) {
 +              ll_opts.virtual_ancestor = 1;
 +              ll_opts.variant = 0;
 +      } else {
                switch (o->recursive_variant) {
                case MERGE_RECURSIVE_OURS:
 -                      favor = XDL_MERGE_FAVOR_OURS;
 +                      ll_opts.variant = XDL_MERGE_FAVOR_OURS;
                        break;
                case MERGE_RECURSIVE_THEIRS:
 -                      favor = XDL_MERGE_FAVOR_THEIRS;
 +                      ll_opts.variant = XDL_MERGE_FAVOR_THEIRS;
                        break;
                default:
 -                      favor = 0;
 +                      ll_opts.variant = 0;
                        break;
                }
        }
        read_mmblob(&src2, b->sha1);
  
        merge_status = ll_merge(result_buf, a->path, &orig, base_name,
 -                              &src1, name1, &src2, name2,
 -                              ((o->call_depth ? LL_OPT_VIRTUAL_ANCESTOR : 0) |
 -                               (o->renormalize ? LL_OPT_RENORMALIZE : 0) |
 -                               create_ll_flag(favor)));
 +                              &src1, name1, &src2, name2, &ll_opts);
  
        free(name1);
        free(name2);
@@@ -732,29 -858,56 +860,56 @@@ static struct merge_file_info merge_fil
        return result;
  }
  
- static void conflict_rename_rename(struct merge_options *o,
-                                  struct rename *ren1,
-                                  const char *branch1,
-                                  struct rename *ren2,
-                                  const char *branch2)
+ static void conflict_rename_delete(struct merge_options *o,
+                                  struct diff_filepair *pair,
+                                  const char *rename_branch,
+                                  const char *other_branch)
  {
+       char *dest_name = pair->two->path;
+       int df_conflict = 0;
+       struct stat st;
+       output(o, 1, "CONFLICT (rename/delete): Rename %s->%s in %s "
+              "and deleted in %s",
+              pair->one->path, pair->two->path, rename_branch,
+              other_branch);
+       if (!o->call_depth)
+               update_stages(dest_name, NULL,
+                             rename_branch == o->branch1 ? pair->two : NULL,
+                             rename_branch == o->branch1 ? NULL : pair->two,
+                             1);
+       if (lstat(dest_name, &st) == 0 && S_ISDIR(st.st_mode)) {
+               dest_name = unique_path(o, dest_name, rename_branch);
+               df_conflict = 1;
+       }
+       update_file(o, 0, pair->two->sha1, pair->two->mode, dest_name);
+       if (df_conflict)
+               free(dest_name);
+ }
+ static void conflict_rename_rename_1to2(struct merge_options *o,
+                                       struct diff_filepair *pair1,
+                                       const char *branch1,
+                                       struct diff_filepair *pair2,
+                                       const char *branch2)
+ {
+       /* One file was renamed in both branches, but to different names. */
        char *del[2];
        int delp = 0;
-       const char *ren1_dst = ren1->pair->two->path;
-       const char *ren2_dst = ren2->pair->two->path;
+       const char *ren1_dst = pair1->two->path;
+       const char *ren2_dst = pair2->two->path;
        const char *dst_name1 = ren1_dst;
        const char *dst_name2 = ren2_dst;
-       if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
+       struct stat st;
+       if (lstat(ren1_dst, &st) == 0 && S_ISDIR(st.st_mode)) {
                dst_name1 = del[delp++] = unique_path(o, ren1_dst, branch1);
                output(o, 1, "%s is a directory in %s adding as %s instead",
                       ren1_dst, branch2, dst_name1);
-               remove_file(o, 0, ren1_dst, 0);
        }
-       if (string_list_has_string(&o->current_directory_set, ren2_dst)) {
+       if (lstat(ren2_dst, &st) == 0 && S_ISDIR(st.st_mode)) {
                dst_name2 = del[delp++] = unique_path(o, ren2_dst, branch2);
                output(o, 1, "%s is a directory in %s adding as %s instead",
                       ren2_dst, branch1, dst_name2);
-               remove_file(o, 0, ren2_dst, 0);
        }
        if (o->call_depth) {
                remove_file_from_cache(dst_name1);
                /*
                 * Uncomment to leave the conflicting names in the resulting tree
                 *
-                * update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, dst_name1);
-                * update_file(o, 0, ren2->pair->two->sha1, ren2->pair->two->mode, dst_name2);
+                * update_file(o, 0, pair1->two->sha1, pair1->two->mode, dst_name1);
+                * update_file(o, 0, pair2->two->sha1, pair2->two->mode, dst_name2);
                 */
        } else {
-               update_stages(dst_name1, NULL, ren1->pair->two, NULL, 1);
-               update_stages(dst_name2, NULL, NULL, ren2->pair->two, 1);
+               update_stages(ren1_dst, NULL, pair1->two, NULL, 1);
+               update_stages(ren2_dst, NULL, NULL, pair2->two, 1);
+               update_file(o, 0, pair1->two->sha1, pair1->two->mode, dst_name1);
+               update_file(o, 0, pair2->two->sha1, pair2->two->mode, dst_name2);
        }
        while (delp--)
                free(del[delp]);
  }
  
- static void conflict_rename_dir(struct merge_options *o,
-                               struct rename *ren1,
-                               const char *branch1)
- {
-       char *new_path = unique_path(o, ren1->pair->two->path, branch1);
-       output(o, 1, "Renaming %s to %s instead", ren1->pair->one->path, new_path);
-       remove_file(o, 0, ren1->pair->two->path, 0);
-       update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path);
-       free(new_path);
- }
- static void conflict_rename_rename_2(struct merge_options *o,
-                                    struct rename *ren1,
-                                    const char *branch1,
-                                    struct rename *ren2,
-                                    const char *branch2)
+ static void conflict_rename_rename_2to1(struct merge_options *o,
+                                       struct rename *ren1,
+                                       const char *branch1,
+                                       struct rename *ren2,
+                                       const char *branch2)
  {
+       /* Two files were renamed to the same thing. */
        char *new_path1 = unique_path(o, ren1->pair->two->path, branch1);
        char *new_path2 = unique_path(o, ren2->pair->two->path, branch2);
        output(o, 1, "Renaming %s to %s and %s to %s instead",
@@@ -879,84 -1025,60 +1027,60 @@@ static int process_renames(struct merge
                        ren2->dst_entry->processed = 1;
                        ren2->processed = 1;
                        if (strcmp(ren1_dst, ren2_dst) != 0) {
-                               clean_merge = 0;
-                               output(o, 1, "CONFLICT (rename/rename): "
-                                      "Rename \"%s\"->\"%s\" in branch \"%s\" "
-                                      "rename \"%s\"->\"%s\" in \"%s\"%s",
-                                      src, ren1_dst, branch1,
-                                      src, ren2_dst, branch2,
-                                      o->call_depth ? " (left unresolved)": "");
-                               if (o->call_depth) {
-                                       remove_file_from_cache(src);
-                                       update_file(o, 0, ren1->pair->one->sha1,
-                                                   ren1->pair->one->mode, src);
-                               }
-                               conflict_rename_rename(o, ren1, branch1, ren2, branch2);
+                               setup_rename_df_conflict_info(RENAME_ONE_FILE_TO_TWO,
+                                                             ren1->pair,
+                                                             ren2->pair,
+                                                             branch1,
+                                                             branch2,
+                                                             ren1->dst_entry,
+                                                             ren2->dst_entry);
                        } else {
-                               struct merge_file_info mfi;
                                remove_file(o, 1, ren1_src, 1);
-                               mfi = merge_file(o,
-                                                ren1->pair->one,
-                                                ren1->pair->two,
-                                                ren2->pair->two,
-                                                branch1,
-                                                branch2);
-                               if (mfi.merge || !mfi.clean)
-                                       output(o, 1, "Renaming %s->%s", src, ren1_dst);
-                               if (mfi.merge)
-                                       output(o, 2, "Auto-merging %s", ren1_dst);
-                               if (!mfi.clean) {
-                                       output(o, 1, "CONFLICT (content): merge conflict in %s",
-                                              ren1_dst);
-                                       clean_merge = 0;
-                                       if (!o->call_depth)
-                                               update_stages(ren1_dst,
-                                                             ren1->pair->one,
-                                                             ren1->pair->two,
-                                                             ren2->pair->two,
-                                                             1 /* clear */);
-                               }
-                               update_file(o, mfi.clean, mfi.sha, mfi.mode, ren1_dst);
+                               update_stages_and_entry(ren1_dst,
+                                                       ren1->dst_entry,
+                                                       ren1->pair->one,
+                                                       ren1->pair->two,
+                                                       ren2->pair->two,
+                                                       1 /* clear */);
                        }
                } else {
                        /* Renamed in 1, maybe changed in 2 */
                        struct string_list_item *item;
                        /* we only use sha1 and mode of these */
                        struct diff_filespec src_other, dst_other;
-                       int try_merge, stage = a_renames == renames1 ? 3: 2;
+                       int try_merge;
  
-                       remove_file(o, 1, ren1_src, o->call_depth || stage == 3);
+                       /*
+                        * unpack_trees loads entries from common-commit
+                        * into stage 1, from head-commit into stage 2, and
+                        * from merge-commit into stage 3.  We keep track
+                        * of which side corresponds to the rename.
+                        */
+                       int renamed_stage = a_renames == renames1 ? 2 : 3;
+                       int other_stage =   a_renames == renames1 ? 3 : 2;
  
-                       hashcpy(src_other.sha1, ren1->src_entry->stages[stage].sha);
-                       src_other.mode = ren1->src_entry->stages[stage].mode;
-                       hashcpy(dst_other.sha1, ren1->dst_entry->stages[stage].sha);
-                       dst_other.mode = ren1->dst_entry->stages[stage].mode;
+                       remove_file(o, 1, ren1_src, o->call_depth || renamed_stage == 2);
  
+                       hashcpy(src_other.sha1, ren1->src_entry->stages[other_stage].sha);
+                       src_other.mode = ren1->src_entry->stages[other_stage].mode;
+                       hashcpy(dst_other.sha1, ren1->dst_entry->stages[other_stage].sha);
+                       dst_other.mode = ren1->dst_entry->stages[other_stage].mode;
                        try_merge = 0;
  
-                       if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
-                               clean_merge = 0;
-                               output(o, 1, "CONFLICT (rename/directory): Rename %s->%s in %s "
-                                      " directory %s added in %s",
-                                      ren1_src, ren1_dst, branch1,
-                                      ren1_dst, branch2);
-                               conflict_rename_dir(o, ren1, branch1);
-                       } else if (sha_eq(src_other.sha1, null_sha1)) {
-                               clean_merge = 0;
-                               output(o, 1, "CONFLICT (rename/delete): Rename %s->%s in %s "
-                                      "and deleted in %s",
-                                      ren1_src, ren1_dst, branch1,
-                                      branch2);
-                               update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, ren1_dst);
-                               if (!o->call_depth)
-                                       update_stages(ren1_dst, NULL,
-                                                       branch1 == o->branch1 ?
-                                                       ren1->pair->two : NULL,
-                                                       branch1 == o->branch1 ?
-                                                       NULL : ren1->pair->two, 1);
+                       if (sha_eq(src_other.sha1, null_sha1)) {
+                               if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
+                                       ren1->dst_entry->processed = 0;
+                                       setup_rename_df_conflict_info(RENAME_DELETE,
+                                                                     ren1->pair,
+                                                                     NULL,
+                                                                     branch1,
+                                                                     branch2,
+                                                                     ren1->dst_entry,
+                                                                     NULL);
+                               } else {
+                                       clean_merge = 0;
+                                       conflict_rename_delete(o, ren1->pair, branch1, branch2);
+                               }
                        } else if ((dst_other.mode == ren1->pair->two->mode) &&
                                   sha_eq(dst_other.sha1, ren1->pair->two->sha1)) {
                                /* Added file on the other side
                                                    mfi.sha,
                                                    mfi.mode,
                                                    ren1_dst);
+                                       try_merge = 0;
                                } else {
                                        new_path = unique_path(o, ren1_dst, branch2);
                                        output(o, 1, "Adding as %s instead", new_path);
                                       "Rename %s->%s in %s",
                                       ren1_src, ren1_dst, branch1,
                                       ren2->pair->one->path, ren2->pair->two->path, branch2);
-                               conflict_rename_rename_2(o, ren1, branch1, ren2, branch2);
+                               conflict_rename_rename_2to1(o, ren1, branch1, ren2, branch2);
                        } else
                                try_merge = 1;
  
                        if (try_merge) {
                                struct diff_filespec *one, *a, *b;
-                               struct merge_file_info mfi;
                                src_other.path = (char *)ren1_src;
  
                                one = ren1->pair->one;
                                        b = ren1->pair->two;
                                        a = &src_other;
                                }
-                               mfi = merge_file(o, one, a, b,
-                                               o->branch1, o->branch2);
-                               if (mfi.clean &&
-                                   sha_eq(mfi.sha, ren1->pair->two->sha1) &&
-                                   mfi.mode == ren1->pair->two->mode) {
-                                       /*
-                                        * This message is part of
-                                        * t6022 test. If you change
-                                        * it update the test too.
-                                        */
-                                       output(o, 3, "Skipped %s (merged same as existing)", ren1_dst);
-                                       /* There may be higher stage entries left
-                                        * in the index (e.g. due to a D/F
-                                        * conflict) that need to be resolved.
-                                        */
-                                       if (!ren1->dst_entry->stages[2].mode !=
-                                           !ren1->dst_entry->stages[3].mode)
-                                               ren1->dst_entry->processed = 0;
-                               } else {
-                                       if (mfi.merge || !mfi.clean)
-                                               output(o, 1, "Renaming %s => %s", ren1_src, ren1_dst);
-                                       if (mfi.merge)
-                                               output(o, 2, "Auto-merging %s", ren1_dst);
-                                       if (!mfi.clean) {
-                                               output(o, 1, "CONFLICT (rename/modify): Merge conflict in %s",
-                                                      ren1_dst);
-                                               clean_merge = 0;
-                                               if (!o->call_depth)
-                                                       update_stages(ren1_dst,
-                                                                     one, a, b, 1);
-                                       }
-                                       update_file(o, mfi.clean, mfi.sha, mfi.mode, ren1_dst);
+                               update_stages_and_entry(ren1_dst, ren1->dst_entry, one, a, b, 1);
+                               if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
+                                       setup_rename_df_conflict_info(RENAME_NORMAL,
+                                                                     ren1->pair,
+                                                                     NULL,
+                                                                     branch1,
+                                                                     NULL,
+                                                                     ren1->dst_entry,
+                                                                     NULL);
                                }
                        }
                }
@@@ -1119,6 -1215,90 +1217,90 @@@ error_return
        return ret;
  }
  
+ static void handle_delete_modify(struct merge_options *o,
+                                const char *path,
+                                const char *new_path,
+                                unsigned char *a_sha, int a_mode,
+                                unsigned char *b_sha, int b_mode)
+ {
+       if (!a_sha) {
+               output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
+                      "and modified in %s. Version %s of %s left in tree%s%s.",
+                      path, o->branch1,
+                      o->branch2, o->branch2, path,
+                      path == new_path ? "" : " at ",
+                      path == new_path ? "" : new_path);
+               update_file(o, 0, b_sha, b_mode, new_path);
+       } else {
+               output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
+                      "and modified in %s. Version %s of %s left in tree%s%s.",
+                      path, o->branch2,
+                      o->branch1, o->branch1, path,
+                      path == new_path ? "" : " at ",
+                      path == new_path ? "" : new_path);
+               update_file(o, 0, a_sha, a_mode, new_path);
+       }
+ }
+ static int merge_content(struct merge_options *o,
+                        const char *path,
+                        unsigned char *o_sha, int o_mode,
+                        unsigned char *a_sha, int a_mode,
+                        unsigned char *b_sha, int b_mode,
+                        const char *df_rename_conflict_branch)
+ {
+       const char *reason = "content";
+       struct merge_file_info mfi;
+       struct diff_filespec one, a, b;
+       struct stat st;
+       unsigned df_conflict_remains = 0;
+       if (!o_sha) {
+               reason = "add/add";
+               o_sha = (unsigned char *)null_sha1;
+       }
+       one.path = a.path = b.path = (char *)path;
+       hashcpy(one.sha1, o_sha);
+       one.mode = o_mode;
+       hashcpy(a.sha1, a_sha);
+       a.mode = a_mode;
+       hashcpy(b.sha1, b_sha);
+       b.mode = b_mode;
+       mfi = merge_file(o, &one, &a, &b, o->branch1, o->branch2);
+       if (df_rename_conflict_branch &&
+           lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+               df_conflict_remains = 1;
+       }
+       if (mfi.clean && !df_conflict_remains &&
+           sha_eq(mfi.sha, a_sha) && mfi.mode == a.mode)
+               output(o, 3, "Skipped %s (merged same as existing)", path);
+       else
+               output(o, 2, "Auto-merging %s", path);
+       if (!mfi.clean) {
+               if (S_ISGITLINK(mfi.mode))
+                       reason = "submodule";
+               output(o, 1, "CONFLICT (%s): Merge conflict in %s",
+                               reason, path);
+       }
+       if (df_conflict_remains) {
+               const char *new_path;
+               update_file_flags(o, mfi.sha, mfi.mode, path,
+                                 o->call_depth || mfi.clean, 0);
+               new_path = unique_path(o, path, df_rename_conflict_branch);
+               mfi.clean = 0;
+               output(o, 1, "Adding as %s instead", new_path);
+               update_file_flags(o, mfi.sha, mfi.mode, new_path, 0, 1);
+       } else {
+               update_file(o, mfi.clean, mfi.sha, mfi.mode, path);
+       }
+       return mfi.clean;
+ }
  /* Per entry merge function */
  static int process_entry(struct merge_options *o,
                         const char *path, struct stage_data *entry)
        unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode);
        unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode);
  
+       if (entry->rename_df_conflict_info)
+               return 1; /* Such cases are handled elsewhere. */
        entry->processed = 1;
        if (o_sha && (!a_sha || !b_sha)) {
                /* Case A: Deleted in one */
                                output(o, 2, "Removing %s", path);
                        /* do not touch working file if it did not exist */
                        remove_file(o, 1, path, !a_sha);
+               } else if (string_list_has_string(&o->current_directory_set,
+                                                 path)) {
+                       entry->processed = 0;
+                       return 1; /* Assume clean until processed */
                } else {
                        /* Deleted in one and changed in the other */
                        clean_merge = 0;
-                       if (!a_sha) {
-                               output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
-                                      "and modified in %s. Version %s of %s left in tree.",
-                                      path, o->branch1,
-                                      o->branch2, o->branch2, path);
-                               update_file(o, 0, b_sha, b_mode, path);
-                       } else {
-                               output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
-                                      "and modified in %s. Version %s of %s left in tree.",
-                                      path, o->branch2,
-                                      o->branch1, o->branch1, path);
-                               update_file(o, 0, a_sha, a_mode, path);
-                       }
+                       handle_delete_modify(o, path, path,
+                                            a_sha, a_mode, b_sha, b_mode);
                }
  
        } else if ((!o_sha && a_sha && !b_sha) ||
                if (string_list_has_string(&o->current_directory_set, path)) {
                        /* Handle D->F conflicts after all subfiles */
                        entry->processed = 0;
-                       /* But get any file out of the way now, so conflicted
-                        * entries below the directory of the same name can
-                        * be put in the working directory.
-                        */
-                       if (a_sha)
-                               output(o, 2, "Removing %s", path);
-                       /* do not touch working file if it did not exist */
-                       remove_file(o, 0, path, !a_sha);
-                       return 1; /* Assume clean till processed */
+                       return 1; /* Assume clean until processed */
                } else {
                        output(o, 2, "Adding %s", path);
                        update_file(o, 1, sha, mode, path);
        } else if (a_sha && b_sha) {
                /* Case C: Added in both (check for same permissions) and */
                /* case D: Modified in both, but differently. */
-               const char *reason = "content";
-               struct merge_file_info mfi;
-               struct diff_filespec one, a, b;
-               if (!o_sha) {
-                       reason = "add/add";
-                       o_sha = (unsigned char *)null_sha1;
-               }
-               output(o, 2, "Auto-merging %s", path);
-               one.path = a.path = b.path = (char *)path;
-               hashcpy(one.sha1, o_sha);
-               one.mode = o_mode;
-               hashcpy(a.sha1, a_sha);
-               a.mode = a_mode;
-               hashcpy(b.sha1, b_sha);
-               b.mode = b_mode;
-               mfi = merge_file(o, &one, &a, &b,
-                                o->branch1, o->branch2);
-               clean_merge = mfi.clean;
-               if (!mfi.clean) {
-                       if (S_ISGITLINK(mfi.mode))
-                               reason = "submodule";
-                       output(o, 1, "CONFLICT (%s): Merge conflict in %s",
-                                       reason, path);
-               }
-               update_file(o, mfi.clean, mfi.sha, mfi.mode, path);
+               clean_merge = merge_content(o, path,
+                                           o_sha, o_mode, a_sha, a_mode, b_sha, b_mode,
+                                           NULL);
        } else if (!o_sha && !a_sha && !b_sha) {
                /*
                 * this entry was deleted altogether. a_mode == 0 means
  }
  
  /*
-  * Per entry merge function for D/F conflicts, to be called only after
-  * all files below dir have been processed.  We do this because in the
-  * cases we can cleanly resolve D/F conflicts, process_entry() can clean
-  * out all the files below the directory for us.
+  * Per entry merge function for D/F (and/or rename) conflicts.  In the
+  * cases we can cleanly resolve D/F conflicts, process_entry() can
+  * clean out all the files below the directory for us.  All D/F
+  * conflict cases must be handled here at the end to make sure any
+  * directories that can be cleaned out, are.
+  *
+  * Some rename conflicts may also be handled here that don't necessarily
+  * involve D/F conflicts, since the code to handle them is generic enough
+  * to handle those rename conflicts with or without D/F conflicts also
+  * being involved.
   */
  static int process_df_entry(struct merge_options *o,
-                        const char *path, struct stage_data *entry)
+                           const char *path, struct stage_data *entry)
  {
        int clean_merge = 1;
        unsigned o_mode = entry->stages[1].mode;
        unsigned char *o_sha = stage_sha(entry->stages[1].sha, o_mode);
        unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode);
        unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode);
-       const char *add_branch;
-       const char *other_branch;
-       unsigned mode;
-       const unsigned char *sha;
-       const char *conf;
        struct stat st;
  
-       /* We currently only handle D->F cases */
-       assert((!o_sha && a_sha && !b_sha) ||
-              (!o_sha && !a_sha && b_sha));
        entry->processed = 1;
-       if (a_sha) {
-               add_branch = o->branch1;
-               other_branch = o->branch2;
-               mode = a_mode;
-               sha = a_sha;
-               conf = "file/directory";
-       } else {
-               add_branch = o->branch2;
-               other_branch = o->branch1;
-               mode = b_mode;
-               sha = b_sha;
-               conf = "directory/file";
-       }
-       if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
-               const char *new_path = unique_path(o, path, add_branch);
+       if (entry->rename_df_conflict_info) {
+               struct rename_df_conflict_info *conflict_info = entry->rename_df_conflict_info;
+               char *src;
+               switch (conflict_info->rename_type) {
+               case RENAME_NORMAL:
+                       clean_merge = merge_content(o, path,
+                                                   o_sha, o_mode, a_sha, a_mode, b_sha, b_mode,
+                                                   conflict_info->branch1);
+                       break;
+               case RENAME_DELETE:
+                       clean_merge = 0;
+                       conflict_rename_delete(o, conflict_info->pair1,
+                                              conflict_info->branch1,
+                                              conflict_info->branch2);
+                       break;
+               case RENAME_ONE_FILE_TO_TWO:
+                       src = conflict_info->pair1->one->path;
+                       clean_merge = 0;
+                       output(o, 1, "CONFLICT (rename/rename): "
+                              "Rename \"%s\"->\"%s\" in branch \"%s\" "
+                              "rename \"%s\"->\"%s\" in \"%s\"%s",
+                              src, conflict_info->pair1->two->path, conflict_info->branch1,
+                              src, conflict_info->pair2->two->path, conflict_info->branch2,
+                              o->call_depth ? " (left unresolved)" : "");
+                       if (o->call_depth) {
+                               remove_file_from_cache(src);
+                               update_file(o, 0, conflict_info->pair1->one->sha1,
+                                           conflict_info->pair1->one->mode, src);
+                       }
+                       conflict_rename_rename_1to2(o, conflict_info->pair1,
+                                                   conflict_info->branch1,
+                                                   conflict_info->pair2,
+                                                   conflict_info->branch2);
+                       conflict_info->dst_entry2->processed = 1;
+                       break;
+               default:
+                       entry->processed = 0;
+                       break;
+               }
+       } else if (o_sha && (!a_sha || !b_sha)) {
+               /* Modify/delete; deleted side may have put a directory in the way */
+               const char *new_path = path;
+               if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode))
+                       new_path = unique_path(o, path, a_sha ? o->branch1 : o->branch2);
                clean_merge = 0;
-               output(o, 1, "CONFLICT (%s): There is a directory with name %s in %s. "
-                      "Adding %s as %s",
-                      conf, path, other_branch, path, new_path);
-               remove_file(o, 0, path, 0);
-               update_file(o, 0, sha, mode, new_path);
+               handle_delete_modify(o, path, new_path,
+                                    a_sha, a_mode, b_sha, b_mode);
+       } else if (!o_sha && !!a_sha != !!b_sha) {
+               /* directory -> (directory, file) */
+               const char *add_branch;
+               const char *other_branch;
+               unsigned mode;
+               const unsigned char *sha;
+               const char *conf;
+               if (a_sha) {
+                       add_branch = o->branch1;
+                       other_branch = o->branch2;
+                       mode = a_mode;
+                       sha = a_sha;
+                       conf = "file/directory";
+               } else {
+                       add_branch = o->branch2;
+                       other_branch = o->branch1;
+                       mode = b_mode;
+                       sha = b_sha;
+                       conf = "directory/file";
+               }
+               if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+                       const char *new_path = unique_path(o, path, add_branch);
+                       clean_merge = 0;
+                       output(o, 1, "CONFLICT (%s): There is a directory with name %s in %s. "
+                              "Adding %s as %s",
+                              conf, path, other_branch, path, new_path);
+                       update_file(o, 0, sha, mode, new_path);
+               } else {
+                       output(o, 2, "Adding %s", path);
+                       update_file(o, 1, sha, mode, path);
+               }
        } else {
-               output(o, 2, "Adding %s", path);
-               update_file(o, 1, sha, mode, path);
+               entry->processed = 0;
+               return 1; /* not handled; assume clean until processed */
        }
  
        return clean_merge;
@@@ -1335,6 -1532,7 +1534,7 @@@ int merge_trees(struct merge_options *o
                get_files_dirs(o, merge);
  
                entries = get_unmerged();
+               make_room_for_directories_of_df_conflicts(o, entries);
                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);
                                && !process_df_entry(o, path, e))
                                clean = 0;
                }
+               for (i = 0; i < entries->nr; i++) {
+                       struct stage_data *e = entries->items[i].util;
+                       if (!e->processed)
+                               die("Unprocessed path??? %s",
+                                   entries->items[i].string);
+               }
  
                string_list_clear(re_merge, 0);
                string_list_clear(re_head, 0);
@@@ -1552,37 -1756,3 +1758,37 @@@ void init_merge_options(struct merge_op
        memset(&o->current_directory_set, 0, sizeof(struct string_list));
        o->current_directory_set.strdup_strings = 1;
  }
 +
 +int parse_merge_opt(struct merge_options *o, const char *s)
 +{
 +      if (!s || !*s)
 +              return -1;
 +      if (!strcmp(s, "ours"))
 +              o->recursive_variant = MERGE_RECURSIVE_OURS;
 +      else if (!strcmp(s, "theirs"))
 +              o->recursive_variant = MERGE_RECURSIVE_THEIRS;
 +      else if (!strcmp(s, "subtree"))
 +              o->subtree_shift = "";
 +      else if (!prefixcmp(s, "subtree="))
 +              o->subtree_shift = s + strlen("subtree=");
 +      else if (!strcmp(s, "patience"))
 +              o->xdl_opts |= XDF_PATIENCE_DIFF;
 +      else if (!strcmp(s, "ignore-space-change"))
 +              o->xdl_opts |= XDF_IGNORE_WHITESPACE_CHANGE;
 +      else if (!strcmp(s, "ignore-all-space"))
 +              o->xdl_opts |= XDF_IGNORE_WHITESPACE;
 +      else if (!strcmp(s, "ignore-space-at-eol"))
 +              o->xdl_opts |= XDF_IGNORE_WHITESPACE_AT_EOL;
 +      else if (!strcmp(s, "renormalize"))
 +              o->renormalize = 1;
 +      else if (!strcmp(s, "no-renormalize"))
 +              o->renormalize = 0;
 +      else if (!prefixcmp(s, "rename-threshold=")) {
 +              const char *score = s + strlen("rename-threshold=");
 +              if ((o->rename_score = parse_rename_score(&score)) == -1 || *score != 0)
 +                      return -1;
 +      }
 +      else
 +              return -1;
 +      return 0;
 +}
index 20d4f11dbe7edda104364ad282af94b13367224b,3935c4b1c4eab29f794c3ac8ee3b30931e0d2889..34794f8a70e1c9b71384363e022d08da4ac8872e
@@@ -25,6 -25,10 +25,10 @@@ test_expect_success 'setup 1' 
        git branch submod &&
        git branch copy &&
        git branch rename &&
+       if test_have_prereq SYMLINKS
+       then
+               git branch rename-ln
+       fi &&
  
        echo hello >>a &&
        cp a d/e &&
@@@ -255,7 -259,16 +259,16 @@@ test_expect_success 'setup 8' 
        git mv a e &&
        git add e &&
        test_tick &&
-       git commit -m "rename a->e"
+       git commit -m "rename a->e" &&
+       if test_have_prereq SYMLINKS
+       then
+               git checkout rename-ln &&
+               git mv a e &&
+               ln -s e a &&
+               git add a e &&
+               test_tick &&
+               git commit -m "rename a->e, symlink a->e"
+       fi
  '
  
  test_expect_success 'setup 9' '
@@@ -544,7 -557,7 +557,7 @@@ test_expect_success 'reset and bind mer
                echo "100644 $o0 0      c"
                echo "100644 $o1 0      d/e"
        ) >expected &&
 -      test_cmp expected actual
 +      test_cmp expected actual &&
  
        git read-tree --prefix=z/ master &&
        git ls-files -s >actual &&
@@@ -615,4 -628,26 +628,26 @@@ test_expect_success 'merge-recursive co
        test_cmp expected actual
  '
  
+ if test_have_prereq SYMLINKS
+ then
+       test_expect_success 'merge-recursive rename vs. rename/symlink' '
+               git checkout -f rename &&
+               git merge rename-ln &&
+               ( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
+               (
+                       echo "100644 blob $o0   b"
+                       echo "100644 blob $o0   c"
+                       echo "100644 blob $o0   d/e"
+                       echo "100644 blob $o0   e"
+                       echo "100644 $o0 0      b"
+                       echo "100644 $o0 0      c"
+                       echo "100644 $o0 0      d/e"
+                       echo "100644 $o0 0      e"
+               ) >expected &&
+               test_cmp expected actual
+       '
+ fi
  test_done
diff --combined t/t6020-merge-df.sh
index 5d91d056d2ff97bb4253671cde8ac9a207bd6bd9,b129f1dbd5a5ffe4413ea359f3e893e935117115..eec8f4e3edd85e350f5b8d8c86415317cce2aba1
@@@ -6,22 -6,25 +6,25 @@@
  test_description='Test merge with directory/file conflicts'
  . ./test-lib.sh
  
- test_expect_success 'prepare repository' \
- 'echo "Hello" > init &&
- git add init &&
- git commit -m "Initial commit" &&
- git branch B &&
- mkdir dir &&
- echo "foo" > dir/foo &&
- git add dir/foo &&
- git commit -m "File: dir/foo" &&
- git checkout B &&
- echo "file dir" > dir &&
- git add dir &&
- git commit -m "File: dir"'
+ test_expect_success 'prepare repository' '
+       echo Hello >init &&
+       git add init &&
+       git commit -m initial &&
+       git branch B &&
+       mkdir dir &&
+       echo foo >dir/foo &&
+       git add dir/foo &&
+       git commit -m "File: dir/foo" &&
+       git checkout B &&
+       echo file dir >dir &&
+       git add dir &&
+       git commit -m "File: dir"
+ '
  
  test_expect_success 'Merge with d/f conflicts' '
 -      test_must_fail git merge master
 +      test_expect_code 1 git merge "merge msg" B master
  '
  
  test_expect_success 'F/D conflict' '
        git merge master
  '
  
+ test_expect_success 'setup modify/delete + directory/file conflict' '
+       git checkout --orphan modify &&
+       git rm -rf . &&
+       git clean -fdqx &&
+       printf "a\nb\nc\nd\ne\nf\ng\nh\n" >letters &&
+       git add letters &&
+       git commit -m initial &&
+       echo i >>letters &&
+       git add letters &&
+       git commit -m modified &&
+       git checkout -b delete HEAD^ &&
+       git rm letters &&
+       mkdir letters &&
+       >letters/file &&
+       git add letters &&
+       git commit -m deleted
+ '
+ test_expect_success 'modify/delete + directory/file conflict' '
+       git checkout delete^0 &&
+       test_must_fail git merge modify &&
+       test 3 = $(git ls-files -s | wc -l) &&
+       test 2 = $(git ls-files -u | wc -l) &&
+       test 1 = $(git ls-files -o | wc -l) &&
+       test -f letters/file &&
+       test -f letters~modify
+ '
+ test_expect_success 'modify/delete + directory/file conflict; other way' '
+       git reset --hard &&
+       git clean -f &&
+       git checkout modify^0 &&
+       test_must_fail git merge delete &&
+       test 3 = $(git ls-files -s | wc -l) &&
+       test 2 = $(git ls-files -u | wc -l) &&
+       test 1 = $(git ls-files -o | wc -l) &&
+       test -f letters/file &&
+       test -f letters~HEAD
+ '
  test_done
diff --combined t/t6022-merge-rename.sh
index 83efc7abf5f15c9a5603107f2223454cd32f9abc,66473f088e88bd14f79495d3e5f8a19dc1f100da..1ed259d864b4ef4c8c7060fa5f22634a25c8e032
@@@ -3,6 -3,11 +3,11 @@@
  test_description='Merge-recursive merging renames'
  . ./test-lib.sh
  
+ modify () {
+       sed -e "$1" <"$2" >"$2.x" &&
+       mv "$2.x" "$2"
+ }
  test_expect_success setup \
  '
  cat >A <<\EOF &&
@@@ -94,147 -99,245 +99,147 @@@ git checkout master
  
  test_expect_success 'pull renaming branch into unrenaming one' \
  '
 -      git show-branch
 -      git pull . white && {
 -              echo "BAD: should have conflicted"
 -              return 1
 -      }
 -      git ls-files -s
 -      test "$(git ls-files -u B | wc -l)" -eq 3 || {
 -              echo "BAD: should have left stages for B"
 -              return 1
 -      }
 -      test "$(git ls-files -s N | wc -l)" -eq 1 || {
 -              echo "BAD: should have merged N"
 -              return 1
 -      }
 +      git show-branch &&
 +      test_expect_code 1 git pull . white &&
 +      git ls-files -s &&
 +      git ls-files -u B >b.stages &&
 +      test_line_count = 3 b.stages &&
 +      git ls-files -s N >n.stages &&
 +      test_line_count = 1 n.stages &&
        sed -ne "/^g/{
        p
        q
 -      }" B | grep master || {
 -              echo "BAD: should have listed our change first"
 -              return 1
 -      }
 -      test "$(git diff white N | wc -l)" -eq 0 || {
 -              echo "BAD: should have taken colored branch"
 -              return 1
 -      }
 +      }" B | grep master &&
 +      git diff --exit-code white N
  '
  
  test_expect_success 'pull renaming branch into another renaming one' \
  '
 -      rm -f B
 -      git reset --hard
 -      git checkout red
 -      git pull . white && {
 -              echo "BAD: should have conflicted"
 -              return 1
 -      }
 -      test "$(git ls-files -u B | wc -l)" -eq 3 || {
 -              echo "BAD: should have left stages"
 -              return 1
 -      }
 -      test "$(git ls-files -s N | wc -l)" -eq 1 || {
 -              echo "BAD: should have merged N"
 -              return 1
 -      }
 +      rm -f B &&
 +      git reset --hard &&
 +      git checkout red &&
 +      test_expect_code 1 git pull . white &&
 +      git ls-files -u B >b.stages &&
 +      test_line_count = 3 b.stages &&
 +      git ls-files -s N >n.stages &&
 +      test_line_count = 1 n.stages &&
        sed -ne "/^g/{
        p
        q
 -      }" B | grep red || {
 -              echo "BAD: should have listed our change first"
 -              return 1
 -      }
 -      test "$(git diff white N | wc -l)" -eq 0 || {
 -              echo "BAD: should have taken colored branch"
 -              return 1
 -      }
 +      }" B | grep red &&
 +      git diff --exit-code white N
  '
  
  test_expect_success 'pull unrenaming branch into renaming one' \
  '
 -      git reset --hard
 -      git show-branch
 -      git pull . master && {
 -              echo "BAD: should have conflicted"
 -              return 1
 -      }
 -      test "$(git ls-files -u B | wc -l)" -eq 3 || {
 -              echo "BAD: should have left stages"
 -              return 1
 -      }
 -      test "$(git ls-files -s N | wc -l)" -eq 1 || {
 -              echo "BAD: should have merged N"
 -              return 1
 -      }
 +      git reset --hard &&
 +      git show-branch &&
 +      test_expect_code 1 git pull . master &&
 +      git ls-files -u B >b.stages &&
 +      test_line_count = 3 b.stages &&
 +      git ls-files -s N >n.stages &&
 +      test_line_count = 1 n.stages &&
        sed -ne "/^g/{
        p
        q
 -      }" B | grep red || {
 -              echo "BAD: should have listed our change first"
 -              return 1
 -      }
 -      test "$(git diff white N | wc -l)" -eq 0 || {
 -              echo "BAD: should have taken colored branch"
 -              return 1
 -      }
 +      }" B | grep red &&
 +      git diff --exit-code white N
  '
  
  test_expect_success 'pull conflicting renames' \
  '
 -      git reset --hard
 -      git show-branch
 -      git pull . blue && {
 -              echo "BAD: should have conflicted"
 -              return 1
 -      }
 -      test "$(git ls-files -u A | wc -l)" -eq 1 || {
 -              echo "BAD: should have left a stage"
 -              return 1
 -      }
 -      test "$(git ls-files -u B | wc -l)" -eq 1 || {
 -              echo "BAD: should have left a stage"
 -              return 1
 -      }
 -      test "$(git ls-files -u C | wc -l)" -eq 1 || {
 -              echo "BAD: should have left a stage"
 -              return 1
 -      }
 -      test "$(git ls-files -s N | wc -l)" -eq 1 || {
 -              echo "BAD: should have merged N"
 -              return 1
 -      }
 +      git reset --hard &&
 +      git show-branch &&
 +      test_expect_code 1 git pull . blue &&
 +      git ls-files -u A >a.stages &&
 +      test_line_count = 1 a.stages &&
 +      git ls-files -u B >b.stages &&
 +      test_line_count = 1 b.stages &&
 +      git ls-files -u C >c.stages &&
 +      test_line_count = 1 c.stages &&
 +      git ls-files -s N >n.stages &&
 +      test_line_count = 1 n.stages &&
        sed -ne "/^g/{
        p
        q
 -      }" B | grep red || {
 -              echo "BAD: should have listed our change first"
 -              return 1
 -      }
 -      test "$(git diff white N | wc -l)" -eq 0 || {
 -              echo "BAD: should have taken colored branch"
 -              return 1
 -      }
 +      }" B | grep red &&
 +      git diff --exit-code white N
  '
  
  test_expect_success 'interference with untracked working tree file' '
 -
 -      git reset --hard
 -      git show-branch
 -      echo >A this file should not matter
 -      git pull . white && {
 -              echo "BAD: should have conflicted"
 -              return 1
 -      }
 -      test -f A || {
 -              echo "BAD: should have left A intact"
 -              return 1
 -      }
 +      git reset --hard &&
 +      git show-branch &&
 +      echo >A this file should not matter &&
 +      test_expect_code 1 git pull . white &&
 +      test_path_is_file A
  '
  
  test_expect_success 'interference with untracked working tree file' '
 -
 -      git reset --hard
 -      git checkout white
 -      git show-branch
 -      rm -f A
 -      echo >A this file should not matter
 -      git pull . red && {
 -              echo "BAD: should have conflicted"
 -              return 1
 -      }
 -      test -f A || {
 -              echo "BAD: should have left A intact"
 -              return 1
 -      }
 +      git reset --hard &&
 +      git checkout white &&
 +      git show-branch &&
 +      rm -f A &&
 +      echo >A this file should not matter &&
 +      test_expect_code 1 git pull . red &&
 +      test_path_is_file A
  '
  
  test_expect_success 'interference with untracked working tree file' '
 -
 -      git reset --hard
 -      rm -f A M
 -      git checkout -f master
 -      git tag -f anchor
 -      git show-branch
 -      git pull . yellow || {
 -              echo "BAD: should have cleanly merged"
 -              return 1
 -      }
 -      test -f M && {
 -              echo "BAD: should have removed M"
 -              return 1
 -      }
 +      git reset --hard &&
 +      rm -f A M &&
 +      git checkout -f master &&
 +      git tag -f anchor &&
 +      git show-branch &&
 +      git pull . yellow &&
 +      test_path_is_missing M &&
        git reset --hard anchor
  '
  
  test_expect_success 'updated working tree file should prevent the merge' '
 -
 -      git reset --hard
 -      rm -f A M
 -      git checkout -f master
 -      git tag -f anchor
 -      git show-branch
 -      echo >>M one line addition
 -      cat M >M.saved
 -      git pull . yellow && {
 -              echo "BAD: should have complained"
 -              return 1
 -      }
 -      test_cmp M M.saved || {
 -              echo "BAD: should have left M intact"
 -              return 1
 -      }
 +      git reset --hard &&
 +      rm -f A M &&
 +      git checkout -f master &&
 +      git tag -f anchor &&
 +      git show-branch &&
 +      echo >>M one line addition &&
 +      cat M >M.saved &&
 +      test_expect_code 128 git pull . yellow &&
 +      test_cmp M M.saved &&
        rm -f M.saved
  '
  
  test_expect_success 'updated working tree file should prevent the merge' '
 -
 -      git reset --hard
 -      rm -f A M
 -      git checkout -f master
 -      git tag -f anchor
 -      git show-branch
 -      echo >>M one line addition
 -      cat M >M.saved
 -      git update-index M
 -      git pull . yellow && {
 -              echo "BAD: should have complained"
 -              return 1
 -      }
 -      test_cmp M M.saved || {
 -              echo "BAD: should have left M intact"
 -              return 1
 -      }
 +      git reset --hard &&
 +      rm -f A M &&
 +      git checkout -f master &&
 +      git tag -f anchor &&
 +      git show-branch &&
 +      echo >>M one line addition &&
 +      cat M >M.saved &&
 +      git update-index M &&
 +      test_expect_code 128 git pull . yellow &&
 +      test_cmp M M.saved &&
        rm -f M.saved
  '
  
  test_expect_success 'interference with untracked working tree file' '
 -
 -      git reset --hard
 -      rm -f A M
 -      git checkout -f yellow
 -      git tag -f anchor
 -      git show-branch
 -      echo >M this file should not matter
 -      git pull . master || {
 -              echo "BAD: should have cleanly merged"
 -              return 1
 -      }
 -      test -f M || {
 -              echo "BAD: should have left M intact"
 -              return 1
 -      }
 -      git ls-files -s | grep M && {
 -              echo "BAD: M must be untracked in the result"
 -              return 1
 -      }
 +      git reset --hard &&
 +      rm -f A M &&
 +      git checkout -f yellow &&
 +      git tag -f anchor &&
 +      git show-branch &&
 +      echo >M this file should not matter &&
 +      git pull . master &&
 +      test_path_is_file M &&
 +      ! {
 +              git ls-files -s |
 +              grep M
 +      } &&
        git reset --hard anchor
  '
  
  test_expect_success 'merge of identical changes in a renamed file' '
 -      rm -f A M N
 +      rm -f A M N &&
        git reset --hard &&
        git checkout change+rename &&
        GIT_MERGE_VERBOSITY=3 git merge change | grep "^Skipped B" &&
        GIT_MERGE_VERBOSITY=3 git merge change+rename | grep "^Skipped B"
  '
  
+ test_expect_success 'setup for rename + d/f conflicts' '
+       git reset --hard &&
+       git checkout --orphan dir-in-way &&
+       git rm -rf . &&
+       mkdir sub &&
+       mkdir dir &&
+       printf "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n" >sub/file &&
+       echo foo >dir/file-in-the-way &&
+       git add -A &&
+       git commit -m "Common commmit" &&
+       echo 11 >>sub/file &&
+       echo more >>dir/file-in-the-way &&
+       git add -u &&
+       git commit -m "Commit to merge, with dir in the way" &&
+       git checkout -b dir-not-in-way &&
+       git reset --soft HEAD^ &&
+       git rm -rf dir &&
+       git commit -m "Commit to merge, with dir removed" -- dir sub/file &&
+       git checkout -b renamed-file-has-no-conflicts dir-in-way~1 &&
+       git rm -rf dir &&
+       git rm sub/file &&
+       printf "1\n2\n3\n4\n5555\n6\n7\n8\n9\n10\n" >dir &&
+       git add dir &&
+       git commit -m "Independent change" &&
+       git checkout -b renamed-file-has-conflicts dir-in-way~1 &&
+       git rm -rf dir &&
+       git mv sub/file dir &&
+       echo 12 >>dir &&
+       git add dir &&
+       git commit -m "Conflicting change"
+ '
+ printf "1\n2\n3\n4\n5555\n6\n7\n8\n9\n10\n11\n" >expected
+ test_expect_success 'Rename+D/F conflict; renamed file merges + dir not in way' '
+       git reset --hard &&
+       git checkout -q renamed-file-has-no-conflicts^0 &&
+       git merge --strategy=recursive dir-not-in-way &&
+       git diff --quiet &&
+       test -f dir &&
+       test_cmp expected dir
+ '
+ test_expect_success 'Rename+D/F conflict; renamed file merges but dir in way' '
+       git reset --hard &&
+       rm -rf dir~* &&
+       git checkout -q renamed-file-has-no-conflicts^0 &&
+       test_must_fail git merge --strategy=recursive dir-in-way >output &&
+       grep "CONFLICT (delete/modify): dir/file-in-the-way" output &&
+       grep "Auto-merging dir" output &&
+       grep "Adding as dir~HEAD instead" output &&
+       test 2 -eq "$(git ls-files -u | wc -l)" &&
+       test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test_must_fail git diff --cached --quiet &&
+       test -f dir/file-in-the-way &&
+       test -f dir~HEAD &&
+       test_cmp expected dir~HEAD
+ '
+ test_expect_success 'Same as previous, but merged other way' '
+       git reset --hard &&
+       rm -rf dir~* &&
+       git checkout -q dir-in-way^0 &&
+       test_must_fail git merge --strategy=recursive renamed-file-has-no-conflicts >output 2>errors &&
+       ! grep "error: refusing to lose untracked file at" errors &&
+       grep "CONFLICT (delete/modify): dir/file-in-the-way" output &&
+       grep "Auto-merging dir" output &&
+       grep "Adding as dir~renamed-file-has-no-conflicts instead" output &&
+       test 2 -eq "$(git ls-files -u | wc -l)" &&
+       test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test_must_fail git diff --cached --quiet &&
+       test -f dir/file-in-the-way &&
+       test -f dir~renamed-file-has-no-conflicts &&
+       test_cmp expected dir~renamed-file-has-no-conflicts
+ '
+ cat >expected <<\EOF &&
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ <<<<<<< HEAD
+ 12
+ =======
+ 11
+ >>>>>>> dir-not-in-way
+ EOF
+ test_expect_success 'Rename+D/F conflict; renamed file cannot merge, dir not in way' '
+       git reset --hard &&
+       rm -rf dir~* &&
+       git checkout -q renamed-file-has-conflicts^0 &&
+       test_must_fail git merge --strategy=recursive dir-not-in-way &&
+       test 3 -eq "$(git ls-files -u | wc -l)" &&
+       test 3 -eq "$(git ls-files -u dir | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test_must_fail git diff --cached --quiet &&
+       test -f dir &&
+       test_cmp expected dir
+ '
+ test_expect_success 'Rename+D/F conflict; renamed file cannot merge and dir in the way' '
+       modify s/dir-not-in-way/dir-in-way/ expected &&
+       git reset --hard &&
+       rm -rf dir~* &&
+       git checkout -q renamed-file-has-conflicts^0 &&
+       test_must_fail git merge --strategy=recursive dir-in-way &&
+       test 5 -eq "$(git ls-files -u | wc -l)" &&
+       test 3 -eq "$(git ls-files -u dir | grep -v file-in-the-way | wc -l)" &&
+       test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test_must_fail git diff --cached --quiet &&
+       test -f dir/file-in-the-way &&
+       test -f dir~HEAD &&
+       test_cmp expected dir~HEAD
+ '
+ cat >expected <<\EOF &&
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ <<<<<<< HEAD
+ 11
+ =======
+ 12
+ >>>>>>> renamed-file-has-conflicts
+ EOF
+ test_expect_success 'Same as previous, but merged other way' '
+       git reset --hard &&
+       rm -rf dir~* &&
+       git checkout -q dir-in-way^0 &&
+       test_must_fail git merge --strategy=recursive renamed-file-has-conflicts &&
+       test 5 -eq "$(git ls-files -u | wc -l)" &&
+       test 3 -eq "$(git ls-files -u dir | grep -v file-in-the-way | wc -l)" &&
+       test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test_must_fail git diff --cached --quiet &&
+       test -f dir/file-in-the-way &&
+       test -f dir~renamed-file-has-conflicts &&
+       test_cmp expected dir~renamed-file-has-conflicts
+ '
+ test_expect_success 'setup both rename source and destination involved in D/F conflict' '
+       git reset --hard &&
+       git checkout --orphan rename-dest &&
+       git rm -rf . &&
+       git clean -fdqx &&
+       mkdir one &&
+       echo stuff >one/file &&
+       git add -A &&
+       git commit -m "Common commmit" &&
+       git mv one/file destdir &&
+       git commit -m "Renamed to destdir" &&
+       git checkout -b source-conflict HEAD~1 &&
+       git rm -rf one &&
+       mkdir destdir &&
+       touch one destdir/foo &&
+       git add -A &&
+       git commit -m "Conflicts in the way"
+ '
+ test_expect_success 'both rename source and destination involved in D/F conflict' '
+       git reset --hard &&
+       rm -rf dir~* &&
+       git checkout -q rename-dest^0 &&
+       test_must_fail git merge --strategy=recursive source-conflict &&
+       test 1 -eq "$(git ls-files -u | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test -f destdir/foo &&
+       test -f one &&
+       test -f destdir~HEAD &&
+       test "stuff" = "$(cat destdir~HEAD)"
+ '
+ test_expect_success 'setup pair rename to parent of other (D/F conflicts)' '
+       git reset --hard &&
+       git checkout --orphan rename-two &&
+       git rm -rf . &&
+       git clean -fdqx &&
+       mkdir one &&
+       mkdir two &&
+       echo stuff >one/file &&
+       echo other >two/file &&
+       git add -A &&
+       git commit -m "Common commmit" &&
+       git rm -rf one &&
+       git mv two/file one &&
+       git commit -m "Rename two/file -> one" &&
+       git checkout -b rename-one HEAD~1 &&
+       git rm -rf two &&
+       git mv one/file two &&
+       rm -r one &&
+       git commit -m "Rename one/file -> two"
+ '
+ test_expect_success 'pair rename to parent of other (D/F conflicts) w/ untracked dir' '
+       git checkout -q rename-one^0 &&
+       mkdir one &&
+       test_must_fail git merge --strategy=recursive rename-two &&
+       test 2 -eq "$(git ls-files -u | wc -l)" &&
+       test 1 -eq "$(git ls-files -u one | wc -l)" &&
+       test 1 -eq "$(git ls-files -u two | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test 4 -eq $(find . | grep -v .git | wc -l) &&
+       test -d one &&
+       test -f one~rename-two &&
+       test -f two &&
+       test "other" = $(cat one~rename-two) &&
+       test "stuff" = $(cat two)
+ '
+ test_expect_success 'pair rename to parent of other (D/F conflicts) w/ clean start' '
+       git reset --hard &&
+       git clean -fdqx &&
+       test_must_fail git merge --strategy=recursive rename-two &&
+       test 2 -eq "$(git ls-files -u | wc -l)" &&
+       test 1 -eq "$(git ls-files -u one | wc -l)" &&
+       test 1 -eq "$(git ls-files -u two | wc -l)" &&
+       test_must_fail git diff --quiet &&
+       test 3 -eq $(find . | grep -v .git | wc -l) &&
+       test -f one &&
+       test -f two &&
+       test "other" = $(cat one) &&
+       test "stuff" = $(cat two)
+ '
+ test_expect_success 'setup rename of one file to two, with directories in the way' '
+       git reset --hard &&
+       git checkout --orphan first-rename &&
+       git rm -rf . &&
+       git clean -fdqx &&
+       echo stuff >original &&
+       git add -A &&
+       git commit -m "Common commmit" &&
+       mkdir two &&
+       >two/file &&
+       git add two/file &&
+       git mv original one &&
+       git commit -m "Put two/file in the way, rename to one" &&
+       git checkout -b second-rename HEAD~1 &&
+       mkdir one &&
+       >one/file &&
+       git add one/file &&
+       git mv original two &&
+       git commit -m "Put one/file in the way, rename to two"
+ '
+ test_expect_success 'check handling of differently renamed file with D/F conflicts' '
+       git checkout -q first-rename^0 &&
+       test_must_fail git merge --strategy=recursive second-rename &&
+       test 5 -eq "$(git ls-files -s | wc -l)" &&
+       test 3 -eq "$(git ls-files -u | wc -l)" &&
+       test 1 -eq "$(git ls-files -u one | wc -l)" &&
+       test 1 -eq "$(git ls-files -u two | wc -l)" &&
+       test 1 -eq "$(git ls-files -u original | wc -l)" &&
+       test 2 -eq "$(git ls-files -o | wc -l)" &&
+       test -f one/file &&
+       test -f two/file &&
+       test -f one~HEAD &&
+       test -f two~second-rename &&
+       ! test -f original
+ '
+ test_expect_success 'setup rename one file to two; directories moving out of the way' '
+       git reset --hard &&
+       git checkout --orphan first-rename-redo &&
+       git rm -rf . &&
+       git clean -fdqx &&
+       echo stuff >original &&
+       mkdir one two &&
+       touch one/file two/file &&
+       git add -A &&
+       git commit -m "Common commmit" &&
+       git rm -rf one &&
+       git mv original one &&
+       git commit -m "Rename to one" &&
+       git checkout -b second-rename-redo HEAD~1 &&
+       git rm -rf two &&
+       git mv original two &&
+       git commit -m "Rename to two"
+ '
+ test_expect_success 'check handling of differently renamed file with D/F conflicts' '
+       git checkout -q first-rename-redo^0 &&
+       test_must_fail git merge --strategy=recursive second-rename-redo &&
+       test 3 -eq "$(git ls-files -u | wc -l)" &&
+       test 1 -eq "$(git ls-files -u one | wc -l)" &&
+       test 1 -eq "$(git ls-files -u two | wc -l)" &&
+       test 1 -eq "$(git ls-files -u original | wc -l)" &&
+       test 0 -eq "$(git ls-files -o | wc -l)" &&
+       test -f one &&
+       test -f two &&
+       ! test -f original
+ '
  test_done
index 004c365ad6b32fff16c3c96ef2b6a173157b02a8,a2e5c5c08fdacbc50e8d9e464665f0e9c1bcfccf..871577d90cc2817ffcc0cfe1ecf76aab6be61723
@@@ -14,8 -14,8 +14,8 @@@ test_description='recursive merge corne
  #  R1  R2
  #
  
- test_expect_success setup '
+ test_expect_success 'setup basic criss-cross + rename with no modifications' '
 -      ten="0 1 2 3 4 5 6 7 8 9"
 +      ten="0 1 2 3 4 5 6 7 8 9" &&
        for i in $ten
        do
                echo line $i in a sample file
        git tag R2
  '
  
- test_expect_success merge '
+ test_expect_success 'merge simple rename+criss-cross with no modifications' '
        git reset --hard &&
        git checkout L2^0 &&
  
-       test_must_fail git merge -s recursive R2^0
+       test_must_fail git merge -s recursive R2^0 &&
+       test 5 = $(git ls-files -s | wc -l) &&
+       test 3 = $(git ls-files -u | wc -l) &&
+       test 0 = $(git ls-files -o | wc -l) &&
+       test $(git rev-parse :0:one) = $(git rev-parse L2:one) &&
+       test $(git rev-parse :0:two) = $(git rev-parse R2:two) &&
+       test $(git rev-parse :2:three) = $(git rev-parse L2:three) &&
+       test $(git rev-parse :3:three) = $(git rev-parse R2:three) &&
+       cp two merged &&
+       >empty &&
+       test_must_fail git merge-file \
+               -L "Temporary merge branch 2" \
+               -L "" \
+               -L "Temporary merge branch 1" \
+               merged empty one &&
+       test $(git rev-parse :1:three) = $(git hash-object merged)
+ '
+ #
+ # Same as before, but modify L1 slightly:
+ #
+ #  L1m L2
+ #   o---o
+ #  / \ / \
+ # o   X   ?
+ #  \ / \ /
+ #   o---o
+ #  R1  R2
+ #
+ test_expect_success 'setup criss-cross + rename merges with basic modification' '
+       git rm -rf . &&
+       git clean -fdqx &&
+       rm -rf .git &&
+       git init &&
+       ten="0 1 2 3 4 5 6 7 8 9"
+       for i in $ten
+       do
+               echo line $i in a sample file
+       done >one &&
+       for i in $ten
+       do
+               echo line $i in another sample file
+       done >two &&
+       git add one two &&
+       test_tick && git commit -m initial &&
+       git branch L1 &&
+       git checkout -b R1 &&
+       git mv one three &&
+       echo more >>two &&
+       git add two &&
+       test_tick && git commit -m R1 &&
+       git checkout L1 &&
+       git mv two three &&
+       test_tick && git commit -m L1 &&
+       git checkout L1^0 &&
+       test_tick && git merge -s ours R1 &&
+       git tag L2 &&
+       git checkout R1^0 &&
+       test_tick && git merge -s ours L1 &&
+       git tag R2
+ '
+ test_expect_success 'merge criss-cross + rename merges with basic modification' '
+       git reset --hard &&
+       git checkout L2^0 &&
+       test_must_fail git merge -s recursive R2^0 &&
+       test 5 = $(git ls-files -s | wc -l) &&
+       test 3 = $(git ls-files -u | wc -l) &&
+       test 0 = $(git ls-files -o | wc -l) &&
+       test $(git rev-parse :0:one) = $(git rev-parse L2:one) &&
+       test $(git rev-parse :0:two) = $(git rev-parse R2:two) &&
+       test $(git rev-parse :2:three) = $(git rev-parse L2:three) &&
+       test $(git rev-parse :3:three) = $(git rev-parse R2:three) &&
+       head -n 10 two >merged &&
+       cp one merge-me &&
+       >empty &&
+       test_must_fail git merge-file \
+               -L "Temporary merge branch 2" \
+               -L "" \
+               -L "Temporary merge branch 1" \
+               merged empty merge-me &&
+       test $(git rev-parse :1:three) = $(git hash-object merged)
+ '
+ #
+ # For the next test, we start with three commits in two lines of development
+ # which setup a rename/add conflict:
+ #   Commit A: File 'a' exists
+ #   Commit B: Rename 'a' -> 'new_a'
+ #   Commit C: Modify 'a', create different 'new_a'
+ # Later, two different people merge and resolve differently:
+ #   Commit D: Merge B & C, ignoring separately created 'new_a'
+ #   Commit E: Merge B & C making use of some piece of secondary 'new_a'
+ # Finally, someone goes to merge D & E.  Does git detect the conflict?
+ #
+ #      B   D
+ #      o---o
+ #     / \ / \
+ #  A o   X   ? F
+ #     \ / \ /
+ #      o---o
+ #      C   E
+ #
+ test_expect_success 'setup differently handled merges of rename/add conflict' '
+       git rm -rf . &&
+       git clean -fdqx &&
+       rm -rf .git &&
+       git init &&
+       printf "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n" >a &&
+       git add a &&
+       test_tick && git commit -m A &&
+       git branch B &&
+       git checkout -b C &&
+       echo 10 >>a &&
+       echo "other content" >>new_a &&
+       git add a new_a &&
+       test_tick && git commit -m C &&
+       git checkout B &&
+       git mv a new_a &&
+       test_tick && git commit -m B &&
+       git checkout B^0 &&
+       test_must_fail git merge C &&
+       git clean -f &&
+       test_tick && git commit -m D &&
+       git tag D &&
+       git checkout C^0 &&
+       test_must_fail git merge B &&
+       rm new_a~HEAD new_a &&
+       printf "Incorrectly merged content" >>new_a &&
+       git add -u &&
+       test_tick && git commit -m E &&
+       git tag E
+ '
+ test_expect_success 'git detects differently handled merges conflict' '
+       git reset --hard &&
+       git checkout D^0 &&
+       git merge -s recursive E^0 && {
+               echo "BAD: should have conflicted"
+               test "Incorrectly merged content" = "$(cat new_a)" &&
+                       echo "BAD: Silently accepted wrong content"
+               return 1
+       }
+       test 3 = $(git ls-files -s | wc -l) &&
+       test 3 = $(git ls-files -u | wc -l) &&
+       test 0 = $(git ls-files -o | wc -l) &&
+       test $(git rev-parse :2:new_a) = $(git rev-parse D:new_a) &&
+       test $(git rev-parse :3:new_a) = $(git rev-parse E:new_a) &&
+       git cat-file -p B:new_a >>merged &&
+       git cat-file -p C:new_a >>merge-me &&
+       >empty &&
+       test_must_fail git merge-file \
+               -L "Temporary merge branch 2" \
+               -L "" \
+               -L "Temporary merge branch 1" \
+               merged empty merge-me &&
+       test $(git rev-parse :1:new_a) = $(git hash-object merged)
  '
  
  test_done