struct object_id shifted;
if (!*subtree_shift) {
- shift_tree(&one->object.oid, &two->object.oid, &shifted, 0);
+ shift_tree(repo, &one->object.oid, &two->object.oid, &shifted, 0);
} else {
- shift_tree_by(&one->object.oid, &two->object.oid, &shifted,
+ shift_tree_by(repo, &one->object.oid, &two->object.oid, &shifted,
subtree_shift);
}
if (oideq(&two->object.oid, &shifted))
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 pathspec match_all;
memset(&match_all, 0, sizeof(match_all));
- read_tree_recursive(the_repository, tree, "", 0, 0,
+ read_tree_recursive(opt->repo, tree, "", 0, 0,
&match_all, save_files_dirs, opt);
}
-static int get_tree_entry_if_blob(const struct object_id *tree,
+static int get_tree_entry_if_blob(struct repository *r,
+ const struct object_id *tree,
const char *path,
struct diff_filespec *dfs)
{
int ret;
- ret = get_tree_entry(tree, path, &dfs->oid, &dfs->mode);
+ ret = get_tree_entry(r, tree, path, &dfs->oid, &dfs->mode);
if (S_ISDIR(dfs->mode)) {
oidcpy(&dfs->oid, &null_oid);
dfs->mode = 0;
* Returns an index_entry instance which doesn't have to correspond to
* a real cache entry in Git's index.
*/
-static struct stage_data *insert_stage_data(const char *path,
+static struct stage_data *insert_stage_data(struct repository *r,
+ const char *path,
struct tree *o, struct tree *a, struct tree *b,
struct string_list *entries)
{
struct string_list_item *item;
struct stage_data *e = xcalloc(1, sizeof(struct stage_data));
- get_tree_entry_if_blob(&o->object.oid, path, &e->stages[1]);
- get_tree_entry_if_blob(&a->object.oid, path, &e->stages[2]);
- get_tree_entry_if_blob(&b->object.oid, path, &e->stages[3]);
+ get_tree_entry_if_blob(r, &o->object.oid, path, &e->stages[1]);
+ get_tree_entry_if_blob(r, &a->object.oid, path, &e->stages[2]);
+ get_tree_entry_if_blob(r, &b->object.oid, path, &e->stages[3]);
item = string_list_insert(entries, path);
item->util = e;
return e;
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,
return ret;
}
-static int tree_has_path(struct tree *tree, const char *path)
+static int tree_has_path(struct repository *r, struct tree *tree,
+ const char *path)
{
struct object_id hashy;
unsigned short mode_o;
- return !get_tree_entry(&tree->object.oid, path,
+ return !get_tree_entry(r,
+ &tree->object.oid, path,
&hashy, &mode_o);
}
*/
if (collision_ent->reported_already) {
clean = 0;
- } else if (tree_has_path(tree, new_path)) {
+ } else if (tree_has_path(opt->repo, tree, new_path)) {
collision_ent->reported_already = 1;
strbuf_add_separated_string_list(&collision_paths, ", ",
&collision_ent->source_files);
string_list_append(&remove_from_merge,
merge_ent->dir)->util = merge_ent;
strbuf_release(&merge_ent->new_dir);
- } else if (tree_has_path(head, head_ent->dir)) {
+ } else if (tree_has_path(opt->repo, head, head_ent->dir)) {
/* 2. This wasn't a directory rename after all */
string_list_append(&remove_from_head,
head_ent->dir)->util = head_ent;
hashmap_iter_init(dir_re_merge, &iter);
while ((merge_ent = hashmap_iter_next(&iter))) {
head_ent = dir_rename_find_entry(dir_re_head, merge_ent->dir);
- if (tree_has_path(merge, merge_ent->dir)) {
+ if (tree_has_path(opt->repo, merge, merge_ent->dir)) {
/* 2. This wasn't a directory rename after all */
string_list_append(&remove_from_merge,
merge_ent->dir)->util = merge_ent;
if (pair->status == 'R')
re->dst_entry->processed = 1;
- re->dst_entry = insert_stage_data(new_path,
+ re->dst_entry = insert_stage_data(opt->repo, new_path,
o_tree, a_tree, b_tree,
entries);
item = string_list_insert(entries, new_path);
* the various handle_rename_*() functions update the index
* explicitly rather than relying on unpack_trees() to have done it.
*/
- get_tree_entry(&tree->object.oid,
+ get_tree_entry(opt->repo,
+ &tree->object.oid,
pair->two->path,
&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,
+ re->src_entry = insert_stage_data(opt->repo,
+ re->pair->one->path,
o_tree, a_tree, b_tree, entries);
else
re->src_entry = item->util;
item = string_list_lookup(entries, re->pair->two->path);
if (!item)
- re->dst_entry = insert_stage_data(re->pair->two->path,
+ re->dst_entry = insert_stage_data(opt->repo,
+ re->pair->two->path,
o_tree, a_tree, b_tree, entries);
else
re->dst_entry = item->util;
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)) {
head_pairs = get_diffpairs(opt, common, head);
merge_pairs = get_diffpairs(opt, common, merge);
- if (opt->detect_directory_renames) {
+ if ((opt->detect_directory_renames == 2) ||
+ (opt->detect_directory_renames == 1 && !opt->call_depth)) {
dir_re_head = get_directory_renames(head_pairs);
dir_re_merge = get_directory_renames(merge_pairs);
_("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);
}