test-lib: introduce test_commit_bulk
[gitweb.git] / merge-recursive.c
index 1d2c9e1772c39a87ee342fc37497323741ee6861..d2e380b7ed845e6fb2cd5616754a3f12a11c7b18 100644 (file)
@@ -163,6 +163,11 @@ static struct tree *shift_tree_object(struct repository *repo,
        return lookup_tree(repo, &shifted);
 }
 
+static inline void set_commit_tree(struct commit *c, struct tree *t)
+{
+       c->maybe_tree = t;
+}
+
 static struct commit *make_virtual_commit(struct repository *repo,
                                          struct tree *tree,
                                          const char *comment)
@@ -170,7 +175,7 @@ static struct commit *make_virtual_commit(struct repository *repo,
        struct commit *commit = alloc_commit_node(repo);
 
        set_merge_remote_desc(commit, comment, (struct object *)commit);
-       commit->maybe_tree = tree;
+       set_commit_tree(commit, tree);
        commit->object.parsed = 1;
        return commit;
 }
@@ -207,8 +212,16 @@ struct stage_data {
 };
 
 struct rename {
+       unsigned processed:1;
        struct diff_filepair *pair;
        const char *branch; /* branch that the rename occurred on */
+       /*
+        * If directory rename detection affected this rename, what was its
+        * original type ('A' or 'R') and it's original destination before
+        * the directory rename (otherwise, '\0' and NULL for these two vars).
+        */
+       char dir_rename_original_type;
+       char *dir_rename_original_dest;
        /*
         * Purpose of src_entry and dst_entry:
         *
@@ -230,8 +243,6 @@ struct rename {
         */
        struct stage_data *src_entry;
        struct stage_data *dst_entry;
-       unsigned add_turned_into_rename:1;
-       unsigned processed:1;
 };
 
 struct rename_conflict_info {
@@ -1070,7 +1081,7 @@ static int find_first_merges(struct repository *repo,
        struct commit *commit;
        int contains_another;
 
-       char merged_revision[42];
+       char merged_revision[GIT_MAX_HEXSZ + 2];
        const char *rev_args[] = { "rev-list", "--merges", "--ancestry-path",
                                   "--all", merged_revision, NULL };
        struct rev_info revs;
@@ -1364,30 +1375,39 @@ static int handle_rename_via_dir(struct merge_options *opt,
         */
        const struct rename *ren = ci->ren1;
        const struct diff_filespec *dest = ren->pair->two;
+       char *file_path = dest->path;
+       int mark_conflicted = (opt->detect_directory_renames == 1);
+       assert(ren->dir_rename_original_dest);
 
        if (!opt->call_depth && would_lose_untracked(opt, dest->path)) {
-               char *alt_path = unique_path(opt, dest->path, ren->branch);
-
+               mark_conflicted = 1;
+               file_path = unique_path(opt, dest->path, ren->branch);
                output(opt, 1, _("Error: Refusing to lose untracked file at %s; "
-                              "writing to %s instead."),
-                      dest->path, alt_path);
+                                "writing to %s instead."),
+                      dest->path, file_path);
+       }
+
+       if (mark_conflicted) {
                /*
-                * Write the file in worktree at alt_path, but not in the
-                * index.  Instead, write to dest->path for the index but
-                * only at the higher appropriate stage.
+                * Write the file in worktree at file_path.  In the index,
+                * only record the file at dest->path in the appropriate
+                * higher stage.
                 */
-               if (update_file(opt, 0, dest, alt_path))
+               if (update_file(opt, 0, dest, file_path))
                        return -1;
-               free(alt_path);
-               return update_stages(opt, dest->path, NULL,
-                                    ren->branch == opt->branch1 ? dest : NULL,
-                                    ren->branch == opt->branch1 ? NULL : dest);
+               if (file_path != dest->path)
+                       free(file_path);
+               if (update_stages(opt, dest->path, NULL,
+                                 ren->branch == opt->branch1 ? dest : NULL,
+                                 ren->branch == opt->branch1 ? NULL : dest))
+                       return -1;
+               return 0; /* not clean, but conflicted */
+       } else {
+               /* Update dest->path both in index and in worktree */
+               if (update_file(opt, 1, dest, dest->path))
+                       return -1;
+               return 1; /* clean */
        }
-
-       /* Update dest->path both in index and in worktree */
-       if (update_file(opt, 1, dest, dest->path))
-               return -1;
-       return 0;
 }
 
 static int handle_change_delete(struct merge_options *opt,
@@ -1640,6 +1660,7 @@ static int handle_rename_add(struct merge_options *opt,
               c->path, add_branch);
 
        prev_path_desc = xstrfmt("version of %s from %s", path, a->path);
+       ci->ren1->src_entry->stages[other_stage].path = a->path;
        if (merge_mode_and_contents(opt, a, c,
                                    &ci->ren1->src_entry->stages[other_stage],
                                    prev_path_desc,
@@ -2484,16 +2505,18 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
                       &re->dst_entry->stages[stage].oid,
                       &re->dst_entry->stages[stage].mode);
 
-       /* Update pair status */
-       if (pair->status == 'A') {
-               /*
-                * Recording rename information for this add makes it look
-                * like a rename/delete conflict.  Make sure we can
-                * correctly handle this as an add that was moved to a new
-                * directory instead of reporting a rename/delete conflict.
-                */
-               re->add_turned_into_rename = 1;
-       }
+       /*
+        * Record the original change status (or 'type' of change).  If it
+        * was originally an add ('A'), this lets us differentiate later
+        * between a RENAME_DELETE conflict and RENAME_VIA_DIR (they
+        * otherwise look the same).  If it was originally a rename ('R'),
+        * this lets us remember and report accurately about the transitive
+        * renaming that occurred via the directory rename detection.  Also,
+        * record the original destination name.
+        */
+       re->dir_rename_original_type = pair->status;
+       re->dir_rename_original_dest = pair->two->path;
+
        /*
         * We don't actually look at pair->status again, but it seems
         * pedagogically correct to adjust it.
@@ -2556,9 +2579,10 @@ static struct string_list *get_renames(struct merge_options *opt,
 
                re = xmalloc(sizeof(*re));
                re->processed = 0;
-               re->add_turned_into_rename = 0;
                re->pair = pair;
                re->branch = branch;
+               re->dir_rename_original_type = '\0';
+               re->dir_rename_original_dest = NULL;
                item = string_list_lookup(entries, re->pair->one->path);
                if (!item)
                        re->src_entry = insert_stage_data(re->pair->one->path,
@@ -2726,7 +2750,7 @@ static int process_renames(struct merge_options *opt,
                        try_merge = 0;
 
                        if (oid_eq(&src_other.oid, &null_oid) &&
-                           ren1->add_turned_into_rename) {
+                           ren1->dir_rename_original_type == 'A') {
                                setup_rename_conflict_info(RENAME_VIA_DIR,
                                                           opt, ren1, NULL);
                        } else if (oid_eq(&src_other.oid, &null_oid)) {
@@ -2973,7 +2997,8 @@ static int handle_modify_delete(struct merge_options *opt,
                                    _("modify"), _("modified"));
 }
 
-static int handle_content_merge(struct merge_options *opt,
+static int handle_content_merge(struct merge_file_info *mfi,
+                               struct merge_options *opt,
                                const char *path,
                                int is_dirty,
                                const struct diff_filespec *o,
@@ -2982,7 +3007,6 @@ static int handle_content_merge(struct merge_options *opt,
                                struct rename_conflict_info *ci)
 {
        const char *reason = _("content");
-       struct merge_file_info mfi;
        unsigned df_conflict_remains = 0;
 
        if (!is_valid(o))
@@ -2995,7 +3019,7 @@ static int handle_content_merge(struct merge_options *opt,
 
        if (merge_mode_and_contents(opt, o, a, b, path,
                                    opt->branch1, opt->branch2,
-                                   opt->call_depth * 2, &mfi))
+                                   opt->call_depth * 2, mfi))
                return -1;
 
        /*
@@ -3004,13 +3028,13 @@ static int handle_content_merge(struct merge_options *opt,
         *   b) The merge matches what was in HEAD (content, mode, pathname)
         *   c) The target path is usable (i.e. not involved in D/F conflict)
         */
-       if (mfi.clean && was_tracked_and_matches(opt, path, &mfi.blob) &&
+       if (mfi->clean && was_tracked_and_matches(opt, path, &mfi->blob) &&
            !df_conflict_remains) {
                int pos;
                struct cache_entry *ce;
 
                output(opt, 3, _("Skipped %s (merged same as existing)"), path);
-               if (add_cacheinfo(opt, &mfi.blob, path,
+               if (add_cacheinfo(opt, &mfi->blob, path,
                                  0, (!opt->call_depth && !is_dirty), 0))
                        return -1;
                /*
@@ -3026,11 +3050,11 @@ static int handle_content_merge(struct merge_options *opt,
                        ce = opt->repo->index->cache[pos];
                        ce->ce_flags |= CE_SKIP_WORKTREE;
                }
-               return mfi.clean;
+               return mfi->clean;
        }
 
-       if (!mfi.clean) {
-               if (S_ISGITLINK(mfi.blob.mode))
+       if (!mfi->clean) {
+               if (S_ISGITLINK(mfi->blob.mode))
                        reason = _("submodule");
                output(opt, 1, _("CONFLICT (%s): Merge conflict in %s"),
                                reason, path);
@@ -3044,15 +3068,15 @@ static int handle_content_merge(struct merge_options *opt,
                if (opt->call_depth) {
                        remove_file_from_index(opt->repo->index, path);
                } else {
-                       if (!mfi.clean) {
+                       if (!mfi->clean) {
                                if (update_stages(opt, path, o, a, b))
                                        return -1;
                        } else {
                                int file_from_stage2 = was_tracked(opt, path);
 
                                if (update_stages(opt, path, NULL,
-                                                 file_from_stage2 ? &mfi.blob : NULL,
-                                                 file_from_stage2 ? NULL : &mfi.blob))
+                                                 file_from_stage2 ? &mfi->blob : NULL,
+                                                 file_from_stage2 ? NULL : &mfi->blob))
                                        return -1;
                        }
 
@@ -3063,15 +3087,15 @@ static int handle_content_merge(struct merge_options *opt,
                               path);
                }
                output(opt, 1, _("Adding as %s instead"), new_path);
-               if (update_file(opt, 0, &mfi.blob, new_path)) {
+               if (update_file(opt, 0, &mfi->blob, new_path)) {
                        free(new_path);
                        return -1;
                }
                free(new_path);
-               mfi.clean = 0;
-       } else if (update_file(opt, mfi.clean, &mfi.blob, path))
+               mfi->clean = 0;
+       } else if (update_file(opt, mfi->clean, &mfi->blob, path))
                return -1;
-       return !is_dirty && mfi.clean;
+       return !is_dirty && mfi->clean;
 }
 
 static int handle_rename_normal(struct merge_options *opt,
@@ -3081,9 +3105,88 @@ static int handle_rename_normal(struct merge_options *opt,
                                const struct diff_filespec *b,
                                struct rename_conflict_info *ci)
 {
+       struct rename *ren = ci->ren1;
+       struct merge_file_info mfi;
+       int clean;
+       int side = (ren->branch == opt->branch1 ? 2 : 3);
+
        /* Merge the content and write it out */
-       return handle_content_merge(opt, path, was_dirty(opt, path),
-                                   o, a, b, ci);
+       clean = handle_content_merge(&mfi, opt, path, was_dirty(opt, path),
+                                    o, a, b, ci);
+
+       if (clean && opt->detect_directory_renames == 1 &&
+           ren->dir_rename_original_dest) {
+               if (update_stages(opt, path,
+                                 NULL,
+                                 side == 2 ? &mfi.blob : NULL,
+                                 side == 2 ? NULL : &mfi.blob))
+                       return -1;
+               clean = 0; /* not clean, but conflicted */
+       }
+       return clean;
+}
+
+static void dir_rename_warning(const char *msg,
+                              int is_add,
+                              int clean,
+                              struct merge_options *opt,
+                              struct rename *ren)
+{
+       const char *other_branch;
+       other_branch = (ren->branch == opt->branch1 ?
+                       opt->branch2 : opt->branch1);
+       if (is_add) {
+               output(opt, clean ? 2 : 1, msg,
+                      ren->pair->one->path, ren->branch,
+                      other_branch, ren->pair->two->path);
+               return;
+       }
+       output(opt, clean ? 2 : 1, msg,
+              ren->pair->one->path, ren->dir_rename_original_dest, ren->branch,
+              other_branch, ren->pair->two->path);
+}
+static int warn_about_dir_renamed_entries(struct merge_options *opt,
+                                         struct rename *ren)
+{
+       const char *msg;
+       int clean = 1, is_add;
+
+       if (!ren)
+               return clean;
+
+       /* Return early if ren was not affected/created by a directory rename */
+       if (!ren->dir_rename_original_dest)
+               return clean;
+
+       /* Sanity checks */
+       assert(opt->detect_directory_renames > 0);
+       assert(ren->dir_rename_original_type == 'A' ||
+              ren->dir_rename_original_type == 'R');
+
+       /* Check whether to treat directory renames as a conflict */
+       clean = (opt->detect_directory_renames == 2);
+
+       is_add = (ren->dir_rename_original_type == 'A');
+       if (ren->dir_rename_original_type == 'A' && clean) {
+               msg = _("Path updated: %s added in %s inside a "
+                       "directory that was renamed in %s; moving it to %s.");
+       } else if (ren->dir_rename_original_type == 'A' && !clean) {
+               msg = _("CONFLICT (file location): %s added in %s "
+                       "inside a directory that was renamed in %s, "
+                       "suggesting it should perhaps be moved to %s.");
+       } else if (ren->dir_rename_original_type == 'R' && clean) {
+               msg = _("Path updated: %s renamed to %s in %s, inside a "
+                       "directory that was renamed in %s; moving it to %s.");
+       } else if (ren->dir_rename_original_type == 'R' && !clean) {
+               msg = _("CONFLICT (file location): %s renamed to %s in %s, "
+                       "inside a directory that was renamed in %s, "
+                       "suggesting it should perhaps be moved to %s.");
+       } else {
+               BUG("Impossible dir_rename_original_type/clean combination");
+       }
+       dir_rename_warning(msg, is_add, clean, opt, ren);
+
+       return clean;
 }
 
 /* Per entry merge function */
@@ -3105,6 +3208,10 @@ static int process_entry(struct merge_options *opt,
        if (entry->rename_conflict_info) {
                struct rename_conflict_info *ci = entry->rename_conflict_info;
                struct diff_filespec *temp;
+               int path_clean;
+
+               path_clean = warn_about_dir_renamed_entries(opt, ci->ren1);
+               path_clean &= warn_about_dir_renamed_entries(opt, ci->ren2);
 
                /*
                 * For cases with a single rename, {o,a,b}->path have all been
@@ -3125,9 +3232,7 @@ static int process_entry(struct merge_options *opt,
                                                           ci);
                        break;
                case RENAME_VIA_DIR:
-                       clean_merge = 1;
-                       if (handle_rename_via_dir(opt, ci))
-                               clean_merge = -1;
+                       clean_merge = handle_rename_via_dir(opt, ci);
                        break;
                case RENAME_ADD:
                        /*
@@ -3177,6 +3282,8 @@ static int process_entry(struct merge_options *opt,
                        entry->processed = 0;
                        break;
                }
+               if (path_clean < clean_merge)
+                       clean_merge = path_clean;
        } else if (o_valid && (!a_valid || !b_valid)) {
                /* Case A: Deleted in one */
                if ((!a_valid && !b_valid) ||
@@ -3247,8 +3354,10 @@ static int process_entry(struct merge_options *opt,
                                                            a, b);
                } else {
                        /* case D: Modified in both, but differently. */
+                       struct merge_file_info mfi;
                        int is_dirty = 0; /* unpack_trees would have bailed if dirty */
-                       clean_merge = handle_content_merge(opt, path, is_dirty,
+                       clean_merge = handle_content_merge(&mfi, opt, path,
+                                                          is_dirty,
                                                           o, a, b, NULL);
                }
        } else if (!o_valid && !a_valid && !b_valid) {
@@ -3546,6 +3655,15 @@ static void merge_recursive_config(struct merge_options *opt)
                opt->merge_detect_rename = git_config_rename("merge.renames", value);
                free(value);
        }
+       if (!git_config_get_string("merge.directoryrenames", &value)) {
+               int boolval = git_parse_maybe_bool(value);
+               if (0 <= boolval) {
+                       opt->detect_directory_renames = boolval ? 2 : 0;
+               } else if (!strcasecmp(value, "conflict")) {
+                       opt->detect_directory_renames = 1;
+               } /* avoid erroring on values from future versions of git */
+               free(value);
+       }
        git_config(git_xmerge_config, NULL);
 }