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)
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;
}
};
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:
*
*/
struct stage_data *src_entry;
struct stage_data *dst_entry;
- unsigned add_turned_into_rename:1;
- unsigned processed:1;
};
struct rename_conflict_info {
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;
*/
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,
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,
&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.
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,
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)) {
_("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,
struct rename_conflict_info *ci)
{
const char *reason = _("content");
- struct merge_file_info mfi;
unsigned df_conflict_remains = 0;
if (!is_valid(o))
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;
/*
* 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;
/*
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);
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;
}
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,
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 */
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
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:
/*
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) ||
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) {
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);
}