Code

Merge branch 'en/d-f-conflict-fix'
authorJunio C Hamano <gitster@pobox.com>
Tue, 31 Aug 2010 23:23:58 +0000 (16:23 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 31 Aug 2010 23:23:58 +0000 (16:23 -0700)
* en/d-f-conflict-fix:
  merge-recursive: Avoid excessive output for and reprocessing of renames
  merge-recursive: Fix multiple file rename across D/F conflict
  t6031: Add a testcase covering multiple renames across a D/F conflict
  merge-recursive: Fix typo
  Mark tests that use symlinks as needing SYMLINKS prerequisite
  t/t6035-merge-dir-to-symlink.sh: Remove TODO on passing test
  fast-import: Improve robustness when D->F changes provided in wrong order
  fast-export: Fix output order of D/F changes
  merge_recursive: Fix renames across paths below D/F conflicts
  merge-recursive: Fix D/F conflicts
  Add a rename + D/F conflict testcase
  Add additional testcases for D/F conflicts

Conflicts:
merge-recursive.c

1  2 
builtin/fast-export.c
fast-import.c
merge-recursive.c
t/t6035-merge-dir-to-symlink.sh
t/t9350-fast-export.sh

diff --combined builtin/fast-export.c
index 66cafe63301ab242b4e7994ba08d9c75d33eca62,965e90e5e8c78ce6b9587234fa251c509e6fbdd5..a9bbf8653d1a8fa704a38c06102ec3ace4a59a28
@@@ -27,7 -27,6 +27,7 @@@ static enum { ABORT, VERBATIM, WARN, ST
  static enum { ERROR, DROP, REWRITE } tag_of_filtered_mode = ABORT;
  static int fake_missing_tagger;
  static int no_data;
 +static int full_tree;
  
  static int parse_opt_signed_tag_mode(const struct option *opt,
                                     const char *arg, int unset)
@@@ -148,10 -147,39 +148,39 @@@ static void handle_object(const unsigne
        free(buf);
  }
  
+ static int depth_first(const void *a_, const void *b_)
+ {
+       const struct diff_filepair *a = *((const struct diff_filepair **)a_);
+       const struct diff_filepair *b = *((const struct diff_filepair **)b_);
+       const char *name_a, *name_b;
+       int len_a, len_b, len;
+       int cmp;
+       name_a = a->one ? a->one->path : a->two->path;
+       name_b = b->one ? b->one->path : b->two->path;
+       len_a = strlen(name_a);
+       len_b = strlen(name_b);
+       len = (len_a < len_b) ? len_a : len_b;
+       /* strcmp will sort 'd' before 'd/e', we want 'd/e' before 'd' */
+       cmp = memcmp(name_a, name_b, len);
+       if (cmp)
+               return cmp;
+       return (len_b - len_a);
+ }
  static void show_filemodify(struct diff_queue_struct *q,
                            struct diff_options *options, void *data)
  {
        int i;
+       /*
+        * Handle files below a directory first, in case they are all deleted
+        * and the directory changes to a file or symlink.
+        */
+       qsort(q->queue, q->nr, sizeof(q->queue[0]), depth_first);
        for (i = 0; i < q->nr; i++) {
                struct diff_filespec *ospec = q->queue[i]->one;
                struct diff_filespec *spec = q->queue[i]->two;
@@@ -242,8 -270,7 +271,8 @@@ static void handle_commit(struct commi
                message += 2;
  
        if (commit->parents &&
 -          get_object_mark(&commit->parents->item->object) != 0) {
 +          get_object_mark(&commit->parents->item->object) != 0 &&
 +          !full_tree) {
                parse_commit(commit->parents->item);
                diff_tree_sha1(commit->parents->item->tree->object.sha1,
                               commit->tree->object.sha1, "", &rev->diffopt);
                i++;
        }
  
 +      if (full_tree)
 +              printf("deleteall\n");
        log_tree_diff_flush(rev);
        rev->diffopt.output_format = saved_output_format;
  
@@@ -442,7 -467,7 +471,7 @@@ static void get_tags_and_duplicates(str
                        /* handle nested tags */
                        while (tag && tag->object.type == OBJ_TAG) {
                                parse_object(tag->object.sha1);
 -                              string_list_append(full_name, extra_refs)->util = tag;
 +                              string_list_append(extra_refs, full_name)->util = tag;
                                tag = (struct tag *)tag->tagged;
                        }
                        if (!tag)
                }
                if (commit->util)
                        /* more than one name for the same object */
 -                      string_list_append(full_name, extra_refs)->util = commit;
 +                      string_list_append(extra_refs, full_name)->util = commit;
                else
                        commit->util = full_name;
        }
@@@ -569,8 -594,8 +598,8 @@@ static void import_marks(char *input_fi
  int cmd_fast_export(int argc, const char **argv, const char *prefix)
  {
        struct rev_info revs;
 -      struct object_array commits = { 0, 0, NULL };
 -      struct string_list extra_refs = { NULL, 0, 0, 0 };
 +      struct object_array commits = OBJECT_ARRAY_INIT;
 +      struct string_list extra_refs = STRING_LIST_INIT_NODUP;
        struct commit *commit;
        char *export_filename = NULL, *import_filename = NULL;
        struct option options[] = {
                             "Import marks from this file"),
                OPT_BOOLEAN(0, "fake-missing-tagger", &fake_missing_tagger,
                             "Fake a tagger when tags lack one"),
 +              OPT_BOOLEAN(0, "full-tree", &full_tree,
 +                           "Output full tree for each commit"),
                { OPTION_NEGBIT, 0, "data", &no_data, NULL,
                        "Skip output of blob data",
                        PARSE_OPT_NOARG | PARSE_OPT_NEGHELP, NULL, 1 },
        if (import_filename)
                import_marks(import_filename);
  
 +      if (import_filename && revs.prune_data)
 +              full_tree = 1;
 +
        get_tags_and_duplicates(&revs.pending, &extra_refs);
  
        if (prepare_revision_walk(&revs))
diff --combined fast-import.c
index dd51ac48b60d93031aa761d6731dd066c04e6989,75ed738b7c2847099df2ddb42b0d2d3c7dc70cbe..2317b0fe7509b957577234890135509143c0872b
@@@ -267,7 -267,7 +267,7 @@@ struct hash_lis
  typedef enum {
        WHENSPEC_RAW = 1,
        WHENSPEC_RFC2822,
 -      WHENSPEC_NOW,
 +      WHENSPEC_NOW
  } whenspec_type;
  
  struct recent_command
@@@ -1528,6 -1528,14 +1528,14 @@@ static int tree_content_remove
        for (i = 0; i < t->entry_count; i++) {
                e = t->entries[i];
                if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
+                       if (slash1 && !S_ISDIR(e->versions[1].mode))
+                               /*
+                                * If p names a file in some subdirectory, and a
+                                * file or symlink matching the name of the
+                                * parent directory of p exists, then p cannot
+                                * exist and need not be deleted.
+                                */
+                               return 1;
                        if (!slash1 || !S_ISDIR(e->versions[1].mode))
                                goto del_entry;
                        if (!e->tree)
@@@ -1666,7 -1674,7 +1674,7 @@@ static void dump_marks_helper(FILE *f
        if (m->shift) {
                for (k = 0; k < 1024; k++) {
                        if (m->data.sets[k])
 -                              dump_marks_helper(f, (base + k) << m->shift,
 +                              dump_marks_helper(f, base + (k << m->shift),
                                        m->data.sets[k]);
                }
        } else {
@@@ -2131,7 -2139,6 +2139,7 @@@ static void file_change_m(struct branc
        case S_IFREG | 0644:
        case S_IFREG | 0755:
        case S_IFLNK:
 +      case S_IFDIR:
        case S_IFGITLINK:
                /* ok */
                break;
                 * another repository.
                 */
        } else if (inline_data) {
 +              if (S_ISDIR(mode))
 +                      die("Directories cannot be specified 'inline': %s",
 +                              command_buf.buf);
                if (p != uq.buf) {
                        strbuf_addstr(&uq, p);
                        p = uq.buf;
                }
                read_next_command();
                parse_and_store_blob(&last_blob, sha1, 0);
 -      } else if (oe) {
 -              if (oe->type != OBJ_BLOB)
 -                      die("Not a blob (actually a %s): %s",
 -                              typename(oe->type), command_buf.buf);
        } else {
 -              enum object_type type = sha1_object_info(sha1, NULL);
 +              enum object_type expected = S_ISDIR(mode) ?
 +                                              OBJ_TREE: OBJ_BLOB;
 +              enum object_type type = oe ? oe->type :
 +                                      sha1_object_info(sha1, NULL);
                if (type < 0)
 -                      die("Blob not found: %s", command_buf.buf);
 -              if (type != OBJ_BLOB)
 -                      die("Not a blob (actually a %s): %s",
 -                          typename(type), command_buf.buf);
 +                      die("%s not found: %s",
 +                                      S_ISDIR(mode) ?  "Tree" : "Blob",
 +                                      command_buf.buf);
 +              if (type != expected)
 +                      die("Not a %s (actually a %s): %s",
 +                              typename(expected), typename(type),
 +                              command_buf.buf);
        }
  
        tree_content_set(&b->branch_tree, p, sha1, mode, NULL);
@@@ -2713,7 -2715,6 +2721,7 @@@ static void option_import_marks(const c
        }
  
        import_marks_file = make_fast_import_path(marks);
 +      safe_create_leading_directories_const(import_marks_file);
        import_marks_file_from_stream = from_stream;
  }
  
@@@ -2744,7 -2745,6 +2752,7 @@@ static void option_active_branches(cons
  static void option_export_marks(const char *marks)
  {
        export_marks_file = make_fast_import_path(marks);
 +      safe_create_leading_directories_const(export_marks_file);
  }
  
  static void option_export_pack_edges(const char *edges)
diff --combined merge-recursive.c
index 638076ec6ecde537b51041d1bdf4ef5a00a4b3cc,a576f9b10ec27cd46ee305301c4f68eddde1fe52..df90be44a51f3af5ebf9eb65e95b7d4fc18cc2f9
@@@ -20,7 -20,6 +20,7 @@@
  #include "attr.h"
  #include "merge-recursive.h"
  #include "dir.h"
 +#include "submodule.h"
  
  static struct tree *shift_tree_object(struct tree *one, struct tree *two,
                                      const char *subtree_shift)
@@@ -137,10 -136,16 +137,10 @@@ static void output_commit_title(struct 
                if (parse_commit(commit) != 0)
                        printf("(bad commit)\n");
                else {
 -                      const char *s;
 -                      int len;
 -                      for (s = commit->buffer; *s; s++)
 -                              if (*s == '\n' && s[1] == '\n') {
 -                                      s += 2;
 -                                      break;
 -                              }
 -                      for (len = 0; s[len] && '\n' != s[len]; len++)
 -                              ; /* do nothing */
 -                      printf("%.*s\n", len, s);
 +                      const char *title;
 +                      int len = find_commit_subject(commit->buffer, &title);
 +                      if (len)
 +                              printf("%.*s\n", len, title);
                }
        }
  }
@@@ -180,7 -185,7 +180,7 @@@ static int git_merge_trees(int index_on
        opts.fn = threeway_merge;
        opts.src_index = &the_index;
        opts.dst_index = &the_index;
 -      opts.msgs = get_porcelain_error_msgs();
 +      set_porcelain_error_msgs(opts.msgs, "merge");
  
        init_tree_desc_from_tree(t+0, common);
        init_tree_desc_from_tree(t+1, head);
@@@ -233,9 -238,9 +233,9 @@@ static int save_files_dirs(const unsign
        newpath[baselen + len] = '\0';
  
        if (S_ISDIR(mode))
 -              string_list_insert(newpath, &o->current_directory_set);
 +              string_list_insert(&o->current_directory_set, newpath);
        else
 -              string_list_insert(newpath, &o->current_file_set);
 +              string_list_insert(&o->current_file_set, newpath);
        free(newpath);
  
        return (S_ISDIR(mode) ? READ_TREE_RECURSIVE : 0);
@@@ -266,7 -271,7 +266,7 @@@ static struct stage_data *insert_stage_
                        e->stages[2].sha, &e->stages[2].mode);
        get_tree_entry(b->object.sha1, path,
                        e->stages[3].sha, &e->stages[3].mode);
 -      item = string_list_insert(path, entries);
 +      item = string_list_insert(entries, path);
        item->util = e;
        return e;
  }
@@@ -289,9 -294,9 +289,9 @@@ static struct string_list *get_unmerged
                if (!ce_stage(ce))
                        continue;
  
 -              item = string_list_lookup(ce->name, unmerged);
 +              item = string_list_lookup(unmerged, ce->name);
                if (!item) {
 -                      item = string_list_insert(ce->name, unmerged);
 +                      item = string_list_insert(unmerged, ce->name);
                        item->util = xcalloc(1, sizeof(struct stage_data));
                }
                e = item->util;
@@@ -351,20 -356,20 +351,20 @@@ static struct string_list *get_renames(
                re = xmalloc(sizeof(*re));
                re->processed = 0;
                re->pair = pair;
 -              item = string_list_lookup(re->pair->one->path, entries);
 +              item = string_list_lookup(entries, re->pair->one->path);
                if (!item)
                        re->src_entry = insert_stage_data(re->pair->one->path,
                                        o_tree, a_tree, b_tree, entries);
                else
                        re->src_entry = item->util;
  
 -              item = string_list_lookup(re->pair->two->path, entries);
 +              item = string_list_lookup(entries, re->pair->two->path);
                if (!item)
                        re->dst_entry = insert_stage_data(re->pair->two->path,
                                        o_tree, a_tree, b_tree, entries);
                else
                        re->dst_entry = item->util;
 -              item = string_list_insert(pair->one->path, renames);
 +              item = string_list_insert(renames, pair->one->path);
                item->util = re;
        }
        opts.output_format = DIFF_FORMAT_NO_OUTPUT;
@@@ -404,7 -409,7 +404,7 @@@ static int remove_file(struct merge_opt
                        return -1;
        }
        if (update_working_directory) {
 -              if (remove_path(path) && errno != ENOENT)
 +              if (remove_path(path))
                        return -1;
        }
        return 0;
@@@ -427,7 -432,7 +427,7 @@@ static char *unique_path(struct merge_o
               lstat(newpath, &st) == 0)
                sprintf(p, "_%d", suffix++);
  
 -      string_list_insert(newpath, &o->current_file_set);
 +      string_list_insert(&o->current_file_set, newpath);
        return newpath;
  }
  
@@@ -520,15 -525,13 +520,15 @@@ static void update_file_flags(struct me
                void *buf;
                unsigned long size;
  
 -              if (S_ISGITLINK(mode))
 +              if (S_ISGITLINK(mode)) {
                        /*
                         * We may later decide to recursively descend into
                         * the submodule directory and update its index
                         * and/or work tree, but we do not do that now.
                         */
 +                      update_wd = 0;
                        goto update_index;
 +              }
  
                buf = read_sha1_file(sha, &type, &size);
                if (!buf)
@@@ -713,8 -716,8 +713,8 @@@ static struct merge_file_info merge_fil
                        free(result_buf.ptr);
                        result.clean = (merge_status == 0);
                } else if (S_ISGITLINK(a->mode)) {
 -                      result.clean = 0;
 -                      hashcpy(result.sha, a->sha1);
 +                      result.clean = merge_submodule(result.sha, one->path, one->sha1,
 +                                                     a->sha1, b->sha1);
                } else if (S_ISLNK(a->mode)) {
                        hashcpy(result.sha, a->sha1);
  
@@@ -803,18 -806,17 +803,18 @@@ static int process_renames(struct merge
                           struct string_list *b_renames)
  {
        int clean_merge = 1, i, j;
 -      struct string_list a_by_dst = {NULL, 0, 0, 0}, b_by_dst = {NULL, 0, 0, 0};
 +      struct string_list a_by_dst = STRING_LIST_INIT_NODUP;
 +      struct string_list b_by_dst = STRING_LIST_INIT_NODUP;
        const struct rename *sre;
  
        for (i = 0; i < a_renames->nr; i++) {
                sre = a_renames->items[i].util;
 -              string_list_insert(sre->pair->two->path, &a_by_dst)->util
 +              string_list_insert(&a_by_dst, sre->pair->two->path)->util
                        = sre->dst_entry;
        }
        for (i = 0; i < b_renames->nr; i++) {
                sre = b_renames->items[i].util;
 -              string_list_insert(sre->pair->two->path, &b_by_dst)->util
 +              string_list_insert(&b_by_dst, sre->pair->two->path)->util
                        = sre->dst_entry;
        }
  
                                        output(o, 1, "Adding as %s instead", new_path);
                                        update_file(o, 0, dst_other.sha1, dst_other.mode, new_path);
                                }
 -                      } else if ((item = string_list_lookup(ren1_dst, renames2Dst))) {
 +                      } else if ((item = string_list_lookup(renames2Dst, ren1_dst))) {
                                ren2 = item->util;
                                clean_merge = 0;
                                ren2->processed = 1;
  
                                if (mfi.clean &&
                                    sha_eq(mfi.sha, ren1->pair->two->sha1) &&
-                                   mfi.mode == ren1->pair->two->mode)
+                                   mfi.mode == ren1->pair->two->mode) {
                                        /*
-                                        * This messaged is part of
+                                        * 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);
-                               else {
+                                       /* 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)
@@@ -1070,6 -1080,7 +1078,7 @@@ static int process_entry(struct merge_o
        unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode);
        unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode);
  
+       entry->processed = 1;
        if (o_sha && (!a_sha || !b_sha)) {
                /* Case A: Deleted in one */
                if ((!a_sha && !b_sha) ||
        } else if ((!o_sha && a_sha && !b_sha) ||
                   (!o_sha && !a_sha && b_sha)) {
                /* Case B: Added in one. */
-               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 (string_list_has_string(&o->current_directory_set, path)) {
-                       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);
-                       remove_file(o, 0, path, 0);
-                       update_file(o, 0, sha, mode, new_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 */
                } else {
                        output(o, 2, "Adding %s", path);
                        update_file(o, 1, sha, mode, path);
        return clean_merge;
  }
  
 -struct unpack_trees_error_msgs get_porcelain_error_msgs(void)
+ /*
+  * 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.
+  */
+ static int process_df_entry(struct merge_options *o,
+                        const char *path, struct stage_data *entry)
+ {
+       int clean_merge = 1;
+       unsigned o_mode = entry->stages[1].mode;
+       unsigned a_mode = entry->stages[2].mode;
+       unsigned b_mode = entry->stages[3].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);
+               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);
+       } else {
+               output(o, 2, "Adding %s", path);
+               update_file(o, 1, sha, mode, path);
+       }
+       return clean_merge;
+ }
 +void set_porcelain_error_msgs(const char **msgs, const char *cmd)
  {
 -      struct unpack_trees_error_msgs msgs = {
 -              /* would_overwrite */
 -              "Your local changes to '%s' would be overwritten by merge.  Aborting.",
 -              /* not_uptodate_file */
 -              "Your local changes to '%s' would be overwritten by merge.  Aborting.",
 -              /* not_uptodate_dir */
 -              "Updating '%s' would lose untracked files in it.  Aborting.",
 -              /* would_lose_untracked */
 -              "Untracked working tree file '%s' would be %s by merge.  Aborting",
 -              /* bind_overlap -- will not happen here */
 -              NULL,
 -      };
 -      if (advice_commit_before_merge) {
 -              msgs.would_overwrite = msgs.not_uptodate_file =
 -                      "Your local changes to '%s' would be overwritten by merge.  Aborting.\n"
 -                      "Please, commit your changes or stash them before you can merge.";
 -      }
 -      return msgs;
 +      const char *msg;
 +      char *tmp;
 +      const char *cmd2 = strcmp(cmd, "checkout") ? cmd : "switch branches";
 +      if (advice_commit_before_merge)
 +              msg = "Your local changes to the following files would be overwritten by %s:\n%%s"
 +                      "Please, commit your changes or stash them before you can %s.";
 +      else
 +              msg = "Your local changes to the following files would be overwritten by %s:\n%%s";
 +      tmp = xmalloc(strlen(msg) + strlen(cmd) + strlen(cmd2) - 2);
 +      sprintf(tmp, msg, cmd, cmd2);
 +      msgs[ERROR_WOULD_OVERWRITE] = tmp;
 +      msgs[ERROR_NOT_UPTODATE_FILE] = tmp;
 +
 +      msgs[ERROR_NOT_UPTODATE_DIR] =
 +              "Updating the following directories would lose untracked files in it:\n%s";
 +
 +      if (advice_commit_before_merge)
 +              msg = "The following untracked working tree files would be %s by %s:\n%%s"
 +                      "Please move or remove them before you can %s.";
 +      else
 +              msg = "The following untracked working tree files would be %s by %s:\n%%s";
 +      tmp = xmalloc(strlen(msg) + strlen(cmd) + strlen("removed") + strlen(cmd2) - 4);
 +      sprintf(tmp, msg, "removed", cmd, cmd2);
 +      msgs[ERROR_WOULD_LOSE_UNTRACKED_REMOVED] = tmp;
 +      tmp = xmalloc(strlen(msg) + strlen(cmd) + strlen("overwritten") + strlen(cmd2) - 4);
 +      sprintf(tmp, msg, "overwritten", cmd, cmd2);
 +      msgs[ERROR_WOULD_LOSE_UNTRACKED_OVERWRITTEN] = tmp;
 +
 +      /*
 +       * Special case: ERROR_BIND_OVERLAP refers to a pair of paths, we
 +       * cannot easily display it as a list.
 +       */
 +      msgs[ERROR_BIND_OVERLAP] = "Entry '%s' overlaps with '%s'.  Cannot bind.";
 +
 +      msgs[ERROR_SPARSE_NOT_UPTODATE_FILE] =
 +              "Cannot update sparse checkout: the following entries are not up-to-date:\n%s";
 +      msgs[ERROR_WOULD_LOSE_ORPHANED_OVERWRITTEN] =
 +              "The following Working tree files would be overwritten by sparse checkout update:\n%s";
 +      msgs[ERROR_WOULD_LOSE_ORPHANED_REMOVED] =
 +              "The following Working tree files would be removed by sparse checkout update:\n%s";
  }
  
  int merge_trees(struct merge_options *o,
        }
  
        if (sha_eq(common->object.sha1, merge->object.sha1)) {
 -              output(o, 0, "Already uptodate!");
 +              output(o, 0, "Already up-to-date!");
                *result = head;
                return 1;
        }
                                && !process_entry(o, path, e))
                                clean = 0;
                }
+               for (i = 0; i < entries->nr; i++) {
+                       const char *path = entries->items[i].string;
+                       struct stage_data *e = entries->items[i].util;
+                       if (!e->processed
+                               && !process_df_entry(o, path, e))
+                               clean = 0;
+               }
  
                string_list_clear(re_merge, 0);
                string_list_clear(re_head, 0);
index cd3190c4a61f0404491b41a1b22f5143b63f4992,40c4f4a9b6272075de1ef701474e8621477d7d2c..dc09513be5a78bf12139efd931ee0344b2710764
@@@ -5,7 -5,7 +5,7 @@@ test_description='merging when a direct
  
  if ! test_have_prereq SYMLINKS
  then
 -      say 'Symbolic links not supported, skipping tests.'
 +      skip_all='Symbolic links not supported, skipping tests.'
        test_done
  fi
  
@@@ -48,7 -48,7 +48,7 @@@ test_expect_success 'setup for merge te
        git tag baseline
  '
  
- test_expect_success 'do not lose a/b-2/c/d in merge (resolve)' '
+ test_expect_success 'Handle D/F conflict, do not lose a/b-2/c/d in merge (resolve)' '
        git reset --hard &&
        git checkout baseline^0 &&
        git merge -s resolve master &&
@@@ -56,7 -56,7 +56,7 @@@
        test -f a/b-2/c/d
  '
  
- test_expect_failure 'do not lose a/b-2/c/d in merge (recursive)' '
+ test_expect_success 'Handle D/F conflict, do not lose a/b-2/c/d in merge (recursive)' '
        git reset --hard &&
        git checkout baseline^0 &&
        git merge -s recursive master &&
        test -f a/b-2/c/d
  '
  
+ test_expect_success 'Handle F/D conflict, do not lose a/b-2/c/d in merge (resolve)' '
+       git reset --hard &&
+       git checkout master^0 &&
+       git merge -s resolve baseline^0 &&
+       test -h a/b &&
+       test -f a/b-2/c/d
+ '
+ test_expect_success 'Handle F/D conflict, do not lose a/b-2/c/d in merge (recursive)' '
+       git reset --hard &&
+       git checkout master^0 &&
+       git merge -s recursive baseline^0 &&
+       test -h a/b &&
+       test -f a/b-2/c/d
+ '
+ test_expect_failure 'do not lose untracked in merge (resolve)' '
+       git reset --hard &&
+       git checkout baseline^0 &&
+       >a/b/c/e &&
+       test_must_fail git merge -s resolve master &&
+       test -f a/b/c/e &&
+       test -f a/b-2/c/d
+ '
+ test_expect_success 'do not lose untracked in merge (recursive)' '
+       git reset --hard &&
+       git checkout baseline^0 &&
+       >a/b/c/e &&
+       test_must_fail git merge -s recursive master &&
+       test -f a/b/c/e &&
+       test -f a/b-2/c/d
+ '
+ test_expect_success 'do not lose modifications in merge (resolve)' '
+       git reset --hard &&
+       git checkout baseline^0 &&
+       echo more content >>a/b/c/d &&
+       test_must_fail git merge -s resolve master
+ '
+ test_expect_success 'do not lose modifications in merge (recursive)' '
+       git reset --hard &&
+       git checkout baseline^0 &&
+       echo more content >>a/b/c/d &&
+       test_must_fail git merge -s recursive master
+ '
  test_expect_success 'setup a merge where dir a/b-2 changed to symlink' '
        git reset --hard &&
        git checkout start^0 &&
        git tag test2
  '
  
- test_expect_success 'merge should not have conflicts (resolve)' '
+ test_expect_success 'merge should not have D/F conflicts (resolve)' '
        git reset --hard &&
        git checkout baseline^0 &&
        git merge -s resolve test2 &&
        test -f a/b/c/d
  '
  
- test_expect_failure 'merge should not have conflicts (recursive)' '
+ test_expect_success 'merge should not have D/F conflicts (recursive)' '
        git reset --hard &&
        git checkout baseline^0 &&
        git merge -s recursive test2 &&
        test -f a/b/c/d
  '
  
+ test_expect_success 'merge should not have F/D conflicts (recursive)' '
+       git reset --hard &&
+       git checkout -b foo test2 &&
+       git merge -s recursive baseline^0 &&
+       test -h a/b-2 &&
+       test -f a/b/c/d
+ '
  test_done
diff --combined t/t9350-fast-export.sh
index d831404fba8d982f4600d6ed310c3acb3b893ac8,27aea5c165d2fac9cb88409a062b8bb86862a5d3..8c8e679468f4b191f93ca68a973d4d58fa1b72d2
@@@ -355,20 -355,6 +355,20 @@@ test_expect_failure 'no exact-ref revis
        )
  '
  
 +test_expect_success 'path limiting with import-marks does not lose unmodified files'        '
 +      git checkout -b simple marks~2 &&
 +      git fast-export --export-marks=marks simple -- file > /dev/null &&
 +      echo more content >> file &&
 +      test_tick &&
 +      git commit -mnext file &&
 +      git fast-export --import-marks=marks simple -- file file0 | grep file0
 +'
 +
 +test_expect_success 'full-tree re-shows unmodified files'        '
 +      git checkout -f simple &&
 +      test $(git fast-export --full-tree simple | grep -c file0) -eq 3
 +'
 +
  test_expect_success 'set-up a few more tags for tag export tests' '
        git checkout -f master &&
        HEAD_TREE=`git show -s --pretty=raw HEAD | grep tree | sed "s/tree //"` &&
@@@ -390,4 -376,28 +390,28 @@@ test_expect_success 'tree_tag-obj'    '
  test_expect_success 'tag-obj_tag'     'git fast-export tag-obj_tag'
  test_expect_success 'tag-obj_tag-obj' 'git fast-export tag-obj_tag-obj'
  
+ test_expect_success SYMLINKS 'directory becomes symlink'        '
+       git init dirtosymlink &&
+       git init result &&
+       (
+               cd dirtosymlink &&
+               mkdir foo &&
+               mkdir bar &&
+               echo hello > foo/world &&
+               echo hello > bar/world &&
+               git add foo/world bar/world &&
+               git commit -q -mone &&
+               git rm -r foo &&
+               ln -s bar foo &&
+               git add foo &&
+               git commit -q -mtwo
+       ) &&
+       (
+               cd dirtosymlink &&
+               git fast-export master -- foo |
+               (cd ../result && git fast-import --quiet)
+       ) &&
+       (cd result && git show master:foo)
+ '
  test_done