1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-09 10:16:08 +02:00

Merge branch 'en/ort-conflict-handling'

ORT merge strategy learns more support for merge conflicts.

* en/ort-conflict-handling:
  merge-ort: add handling for different types of files at same path
  merge-ort: copy find_first_merges() implementation from merge-recursive.c
  merge-ort: implement format_commit()
  merge-ort: copy and adapt merge_submodule() from merge-recursive.c
  merge-ort: copy and adapt merge_3way() from merge-recursive.c
  merge-ort: flesh out implementation of handle_content_merge()
  merge-ort: handle book-keeping around two- and three-way content merge
  merge-ort: implement unique_path() helper
  merge-ort: handle directory/file conflicts that remain
  merge-ort: handle D/F conflict where directory disappears due to merge
This commit is contained in:
Junio C Hamano 2021-02-05 16:40:44 -08:00
commit b65b9ff1ff

View File

@ -25,8 +25,11 @@
#include "diff.h"
#include "diffcore.h"
#include "dir.h"
#include "ll-merge.h"
#include "object-store.h"
#include "revision.h"
#include "strmap.h"
#include "submodule.h"
#include "tree.h"
#include "unpack-trees.h"
#include "xdiff-interface.h"
@ -349,6 +352,25 @@ static int err(struct merge_options *opt, const char *err, ...)
return -1;
}
static void format_commit(struct strbuf *sb,
int indent,
struct commit *commit)
{
struct merge_remote_desc *desc;
struct pretty_print_context ctx = {0};
ctx.abbrev = DEFAULT_ABBREV;
strbuf_addchars(sb, ' ', indent);
desc = merge_remote_util(commit);
if (desc) {
strbuf_addf(sb, "virtual %s\n", desc->name);
return;
}
format_commit_message(commit, "%h %s", sb, &ctx);
strbuf_addch(sb, '\n');
}
__attribute__((format (printf, 4, 5)))
static void path_msg(struct merge_options *opt,
const char *path,
@ -370,6 +392,36 @@ static void path_msg(struct merge_options *opt,
strbuf_addch(sb, '\n');
}
/* add a string to a strbuf, but converting "/" to "_" */
static void add_flattened_path(struct strbuf *out, const char *s)
{
size_t i = out->len;
strbuf_addstr(out, s);
for (; i < out->len; i++)
if (out->buf[i] == '/')
out->buf[i] = '_';
}
static char *unique_path(struct strmap *existing_paths,
const char *path,
const char *branch)
{
struct strbuf newpath = STRBUF_INIT;
int suffix = 0;
size_t base_len;
strbuf_addf(&newpath, "%s~", path);
add_flattened_path(&newpath, branch);
base_len = newpath.len;
while (strmap_contains(existing_paths, newpath.buf)) {
strbuf_setlen(&newpath, base_len);
strbuf_addf(&newpath, "_%d", suffix++);
}
return strbuf_detach(&newpath, NULL);
}
/*** Function Grouping: functions related to collect_merge_info() ***/
static void setup_path_info(struct merge_options *opt,
@ -628,6 +680,249 @@ static int collect_merge_info(struct merge_options *opt,
/*** Function Grouping: functions related to threeway content merges ***/
static int find_first_merges(struct repository *repo,
const char *path,
struct commit *a,
struct commit *b,
struct object_array *result)
{
int i, j;
struct object_array merges = OBJECT_ARRAY_INIT;
struct commit *commit;
int contains_another;
char merged_revision[GIT_MAX_HEXSZ + 2];
const char *rev_args[] = { "rev-list", "--merges", "--ancestry-path",
"--all", merged_revision, NULL };
struct rev_info revs;
struct setup_revision_opt rev_opts;
memset(result, 0, sizeof(struct object_array));
memset(&rev_opts, 0, sizeof(rev_opts));
/* get all revisions that merge commit a */
xsnprintf(merged_revision, sizeof(merged_revision), "^%s",
oid_to_hex(&a->object.oid));
repo_init_revisions(repo, &revs, NULL);
rev_opts.submodule = path;
/* FIXME: can't handle linked worktrees in submodules yet */
revs.single_worktree = path != NULL;
setup_revisions(ARRAY_SIZE(rev_args)-1, rev_args, &revs, &rev_opts);
/* save all revisions from the above list that contain b */
if (prepare_revision_walk(&revs))
die("revision walk setup failed");
while ((commit = get_revision(&revs)) != NULL) {
struct object *o = &(commit->object);
if (in_merge_bases(b, commit))
add_object_array(o, NULL, &merges);
}
reset_revision_walk();
/* Now we've got all merges that contain a and b. Prune all
* merges that contain another found merge and save them in
* result.
*/
for (i = 0; i < merges.nr; i++) {
struct commit *m1 = (struct commit *) merges.objects[i].item;
contains_another = 0;
for (j = 0; j < merges.nr; j++) {
struct commit *m2 = (struct commit *) merges.objects[j].item;
if (i != j && in_merge_bases(m2, m1)) {
contains_another = 1;
break;
}
}
if (!contains_another)
add_object_array(merges.objects[i].item, NULL, result);
}
object_array_clear(&merges);
return result->nr;
}
static int merge_submodule(struct merge_options *opt,
const char *path,
const struct object_id *o,
const struct object_id *a,
const struct object_id *b,
struct object_id *result)
{
struct commit *commit_o, *commit_a, *commit_b;
int parent_count;
struct object_array merges;
struct strbuf sb = STRBUF_INIT;
int i;
int search = !opt->priv->call_depth;
/* store fallback answer in result in case we fail */
oidcpy(result, opt->priv->call_depth ? o : a);
/* we can not handle deletion conflicts */
if (is_null_oid(o))
return 0;
if (is_null_oid(a))
return 0;
if (is_null_oid(b))
return 0;
if (add_submodule_odb(path)) {
path_msg(opt, path, 0,
_("Failed to merge submodule %s (not checked out)"),
path);
return 0;
}
if (!(commit_o = lookup_commit_reference(opt->repo, o)) ||
!(commit_a = lookup_commit_reference(opt->repo, a)) ||
!(commit_b = lookup_commit_reference(opt->repo, b))) {
path_msg(opt, path, 0,
_("Failed to merge submodule %s (commits not present)"),
path);
return 0;
}
/* check whether both changes are forward */
if (!in_merge_bases(commit_o, commit_a) ||
!in_merge_bases(commit_o, commit_b)) {
path_msg(opt, path, 0,
_("Failed to merge submodule %s "
"(commits don't follow merge-base)"),
path);
return 0;
}
/* Case #1: a is contained in b or vice versa */
if (in_merge_bases(commit_a, commit_b)) {
oidcpy(result, b);
path_msg(opt, path, 1,
_("Note: Fast-forwarding submodule %s to %s"),
path, oid_to_hex(b));
return 1;
}
if (in_merge_bases(commit_b, commit_a)) {
oidcpy(result, a);
path_msg(opt, path, 1,
_("Note: Fast-forwarding submodule %s to %s"),
path, oid_to_hex(a));
return 1;
}
/*
* Case #2: There are one or more merges that contain a and b in
* the submodule. If there is only one, then present it as a
* suggestion to the user, but leave it marked unmerged so the
* user needs to confirm the resolution.
*/
/* Skip the search if makes no sense to the calling context. */
if (!search)
return 0;
/* find commit which merges them */
parent_count = find_first_merges(opt->repo, path, commit_a, commit_b,
&merges);
switch (parent_count) {
case 0:
path_msg(opt, path, 0, _("Failed to merge submodule %s"), path);
break;
case 1:
format_commit(&sb, 4,
(struct commit *)merges.objects[0].item);
path_msg(opt, path, 0,
_("Failed to merge submodule %s, but a possible merge "
"resolution exists:\n%s\n"),
path, sb.buf);
path_msg(opt, path, 1,
_("If this is correct simply add it to the index "
"for example\n"
"by using:\n\n"
" git update-index --cacheinfo 160000 %s \"%s\"\n\n"
"which will accept this suggestion.\n"),
oid_to_hex(&merges.objects[0].item->oid), path);
strbuf_release(&sb);
break;
default:
for (i = 0; i < merges.nr; i++)
format_commit(&sb, 4,
(struct commit *)merges.objects[i].item);
path_msg(opt, path, 0,
_("Failed to merge submodule %s, but multiple "
"possible merges exist:\n%s"), path, sb.buf);
strbuf_release(&sb);
}
object_array_clear(&merges);
return 0;
}
static int merge_3way(struct merge_options *opt,
const char *path,
const struct object_id *o,
const struct object_id *a,
const struct object_id *b,
const char *pathnames[3],
const int extra_marker_size,
mmbuffer_t *result_buf)
{
mmfile_t orig, src1, src2;
struct ll_merge_options ll_opts = {0};
char *base, *name1, *name2;
int merge_status;
ll_opts.renormalize = opt->renormalize;
ll_opts.extra_marker_size = extra_marker_size;
ll_opts.xdl_opts = opt->xdl_opts;
if (opt->priv->call_depth) {
ll_opts.virtual_ancestor = 1;
ll_opts.variant = 0;
} else {
switch (opt->recursive_variant) {
case MERGE_VARIANT_OURS:
ll_opts.variant = XDL_MERGE_FAVOR_OURS;
break;
case MERGE_VARIANT_THEIRS:
ll_opts.variant = XDL_MERGE_FAVOR_THEIRS;
break;
default:
ll_opts.variant = 0;
break;
}
}
assert(pathnames[0] && pathnames[1] && pathnames[2] && opt->ancestor);
if (pathnames[0] == pathnames[1] && pathnames[1] == pathnames[2]) {
base = mkpathdup("%s", opt->ancestor);
name1 = mkpathdup("%s", opt->branch1);
name2 = mkpathdup("%s", opt->branch2);
} else {
base = mkpathdup("%s:%s", opt->ancestor, pathnames[0]);
name1 = mkpathdup("%s:%s", opt->branch1, pathnames[1]);
name2 = mkpathdup("%s:%s", opt->branch2, pathnames[2]);
}
read_mmblob(&orig, o);
read_mmblob(&src1, a);
read_mmblob(&src2, b);
merge_status = ll_merge(result_buf, path, &orig, base,
&src1, name1, &src2, name2,
opt->repo->index, &ll_opts);
free(base);
free(name1);
free(name2);
free(orig.ptr);
free(src1.ptr);
free(src2.ptr);
return merge_status;
}
static int handle_content_merge(struct merge_options *opt,
const char *path,
const struct version_info *o,
@ -637,7 +932,130 @@ static int handle_content_merge(struct merge_options *opt,
const int extra_marker_size,
struct version_info *result)
{
die("Not yet implemented");
/*
* path is the target location where we want to put the file, and
* is used to determine any normalization rules in ll_merge.
*
* The normal case is that path and all entries in pathnames are
* identical, though renames can affect which path we got one of
* the three blobs to merge on various sides of history.
*
* extra_marker_size is the amount to extend conflict markers in
* ll_merge; this is neeed if we have content merges of content
* merges, which happens for example with rename/rename(2to1) and
* rename/add conflicts.
*/
unsigned clean = 1;
/*
* handle_content_merge() needs both files to be of the same type, i.e.
* both files OR both submodules OR both symlinks. Conflicting types
* needs to be handled elsewhere.
*/
assert((S_IFMT & a->mode) == (S_IFMT & b->mode));
/* Merge modes */
if (a->mode == b->mode || a->mode == o->mode)
result->mode = b->mode;
else {
/* must be the 100644/100755 case */
assert(S_ISREG(a->mode));
result->mode = a->mode;
clean = (b->mode == o->mode);
/*
* FIXME: If opt->priv->call_depth && !clean, then we really
* should not make result->mode match either a->mode or
* b->mode; that causes t6036 "check conflicting mode for
* regular file" to fail. It would be best to use some other
* mode, but we'll confuse all kinds of stuff if we use one
* where S_ISREG(result->mode) isn't true, and if we use
* something like 0100666, then tree-walk.c's calls to
* canon_mode() will just normalize that to 100644 for us and
* thus not solve anything.
*
* Figure out if there's some kind of way we can work around
* this...
*/
}
/*
* Trivial oid merge.
*
* Note: While one might assume that the next four lines would
* be unnecessary due to the fact that match_mask is often
* setup and already handled, renames don't always take care
* of that.
*/
if (oideq(&a->oid, &b->oid) || oideq(&a->oid, &o->oid))
oidcpy(&result->oid, &b->oid);
else if (oideq(&b->oid, &o->oid))
oidcpy(&result->oid, &a->oid);
/* Remaining rules depend on file vs. submodule vs. symlink. */
else if (S_ISREG(a->mode)) {
mmbuffer_t result_buf;
int ret = 0, merge_status;
int two_way;
/*
* If 'o' is different type, treat it as null so we do a
* two-way merge.
*/
two_way = ((S_IFMT & o->mode) != (S_IFMT & a->mode));
merge_status = merge_3way(opt, path,
two_way ? &null_oid : &o->oid,
&a->oid, &b->oid,
pathnames, extra_marker_size,
&result_buf);
if ((merge_status < 0) || !result_buf.ptr)
ret = err(opt, _("Failed to execute internal merge"));
if (!ret &&
write_object_file(result_buf.ptr, result_buf.size,
blob_type, &result->oid))
ret = err(opt, _("Unable to add %s to database"),
path);
free(result_buf.ptr);
if (ret)
return -1;
clean &= (merge_status == 0);
path_msg(opt, path, 1, _("Auto-merging %s"), path);
} else if (S_ISGITLINK(a->mode)) {
int two_way = ((S_IFMT & o->mode) != (S_IFMT & a->mode));
clean = merge_submodule(opt, pathnames[0],
two_way ? &null_oid : &o->oid,
&a->oid, &b->oid, &result->oid);
if (opt->priv->call_depth && two_way && !clean) {
result->mode = o->mode;
oidcpy(&result->oid, &o->oid);
}
} else if (S_ISLNK(a->mode)) {
if (opt->priv->call_depth) {
clean = 0;
result->mode = o->mode;
oidcpy(&result->oid, &o->oid);
} else {
switch (opt->recursive_variant) {
case MERGE_VARIANT_NORMAL:
clean = 0;
oidcpy(&result->oid, &a->oid);
break;
case MERGE_VARIANT_OURS:
oidcpy(&result->oid, &a->oid);
break;
case MERGE_VARIANT_THEIRS:
oidcpy(&result->oid, &b->oid);
break;
}
}
} else
BUG("unsupported object type in the tree: %06o for %s",
a->mode, path);
return clean;
}
/*** Function Grouping: functions related to detect_and_process_renames(), ***
@ -1366,6 +1784,8 @@ static void process_entry(struct merge_options *opt,
struct conflict_info *ci,
struct directory_versions *dir_metadata)
{
int df_file_index = 0;
VERIFY_CI(ci);
assert(ci->filemask >= 0 && ci->filemask <= 7);
/* ci->match_mask == 7 was handled in collect_merge_info_callback() */
@ -1380,14 +1800,108 @@ static void process_entry(struct merge_options *opt,
assert(ci->df_conflict);
}
if (ci->df_conflict) {
die("Not yet implemented.");
if (ci->df_conflict && ci->merged.result.mode == 0) {
int i;
/*
* directory no longer in the way, but we do have a file we
* need to place here so we need to clean away the "directory
* merges to nothing" result.
*/
ci->df_conflict = 0;
assert(ci->filemask != 0);
ci->merged.clean = 0;
ci->merged.is_null = 0;
/* and we want to zero out any directory-related entries */
ci->match_mask = (ci->match_mask & ~ci->dirmask);
ci->dirmask = 0;
for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
if (ci->filemask & (1 << i))
continue;
ci->stages[i].mode = 0;
oidcpy(&ci->stages[i].oid, &null_oid);
}
} else if (ci->df_conflict && ci->merged.result.mode != 0) {
/*
* This started out as a D/F conflict, and the entries in
* the competing directory were not removed by the merge as
* evidenced by write_completed_directory() writing a value
* to ci->merged.result.mode.
*/
struct conflict_info *new_ci;
const char *branch;
const char *old_path = path;
int i;
assert(ci->merged.result.mode == S_IFDIR);
/*
* If filemask is 1, we can just ignore the file as having
* been deleted on both sides. We do not want to overwrite
* ci->merged.result, since it stores the tree for all the
* files under it.
*/
if (ci->filemask == 1) {
ci->filemask = 0;
return;
}
/*
* This file still exists on at least one side, and we want
* the directory to remain here, so we need to move this
* path to some new location.
*/
new_ci = xcalloc(1, sizeof(*new_ci));
/* We don't really want new_ci->merged.result copied, but it'll
* be overwritten below so it doesn't matter. We also don't
* want any directory mode/oid values copied, but we'll zero
* those out immediately. We do want the rest of ci copied.
*/
memcpy(new_ci, ci, sizeof(*ci));
new_ci->match_mask = (new_ci->match_mask & ~new_ci->dirmask);
new_ci->dirmask = 0;
for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
if (new_ci->filemask & (1 << i))
continue;
/* zero out any entries related to directories */
new_ci->stages[i].mode = 0;
oidcpy(&new_ci->stages[i].oid, &null_oid);
}
/*
* Find out which side this file came from; note that we
* cannot just use ci->filemask, because renames could cause
* the filemask to go back to 7. So we use dirmask, then
* pick the opposite side's index.
*/
df_file_index = (ci->dirmask & (1 << 1)) ? 2 : 1;
branch = (df_file_index == 1) ? opt->branch1 : opt->branch2;
path = unique_path(&opt->priv->paths, path, branch);
strmap_put(&opt->priv->paths, path, new_ci);
path_msg(opt, path, 0,
_("CONFLICT (file/directory): directory in the way "
"of %s from %s; moving it to %s instead."),
old_path, branch, path);
/*
* Zero out the filemask for the old ci. At this point, ci
* was just an entry for a directory, so we don't need to
* do anything more with it.
*/
ci->filemask = 0;
/*
* Now note that we're working on the new entry (path was
* updated above.
*/
ci = new_ci;
}
/*
* NOTE: Below there is a long switch-like if-elseif-elseif... block
* which the code goes through even for the df_conflict cases
* above. Well, it will once we don't die-not-implemented above.
* above.
*/
if (ci->match_mask) {
ci->merged.clean = 1;
@ -1411,21 +1925,142 @@ static void process_entry(struct merge_options *opt,
} else if (ci->filemask >= 6 &&
(S_IFMT & ci->stages[1].mode) !=
(S_IFMT & ci->stages[2].mode)) {
/*
* Two different items from (file/submodule/symlink)
*/
die("Not yet implemented.");
/* Two different items from (file/submodule/symlink) */
if (opt->priv->call_depth) {
/* Just use the version from the merge base */
ci->merged.clean = 0;
oidcpy(&ci->merged.result.oid, &ci->stages[0].oid);
ci->merged.result.mode = ci->stages[0].mode;
ci->merged.is_null = (ci->merged.result.mode == 0);
} else {
/* Handle by renaming one or both to separate paths. */
unsigned o_mode = ci->stages[0].mode;
unsigned a_mode = ci->stages[1].mode;
unsigned b_mode = ci->stages[2].mode;
struct conflict_info *new_ci;
const char *a_path = NULL, *b_path = NULL;
int rename_a = 0, rename_b = 0;
new_ci = xmalloc(sizeof(*new_ci));
if (S_ISREG(a_mode))
rename_a = 1;
else if (S_ISREG(b_mode))
rename_b = 1;
else {
rename_a = 1;
rename_b = 1;
}
path_msg(opt, path, 0,
_("CONFLICT (distinct types): %s had different "
"types on each side; renamed %s of them so "
"each can be recorded somewhere."),
path,
(rename_a && rename_b) ? _("both") : _("one"));
ci->merged.clean = 0;
memcpy(new_ci, ci, sizeof(*new_ci));
/* Put b into new_ci, removing a from stages */
new_ci->merged.result.mode = ci->stages[2].mode;
oidcpy(&new_ci->merged.result.oid, &ci->stages[2].oid);
new_ci->stages[1].mode = 0;
oidcpy(&new_ci->stages[1].oid, &null_oid);
new_ci->filemask = 5;
if ((S_IFMT & b_mode) != (S_IFMT & o_mode)) {
new_ci->stages[0].mode = 0;
oidcpy(&new_ci->stages[0].oid, &null_oid);
new_ci->filemask = 4;
}
/* Leave only a in ci, fixing stages. */
ci->merged.result.mode = ci->stages[1].mode;
oidcpy(&ci->merged.result.oid, &ci->stages[1].oid);
ci->stages[2].mode = 0;
oidcpy(&ci->stages[2].oid, &null_oid);
ci->filemask = 3;
if ((S_IFMT & a_mode) != (S_IFMT & o_mode)) {
ci->stages[0].mode = 0;
oidcpy(&ci->stages[0].oid, &null_oid);
ci->filemask = 2;
}
/* Insert entries into opt->priv_paths */
assert(rename_a || rename_b);
if (rename_a) {
a_path = unique_path(&opt->priv->paths,
path, opt->branch1);
strmap_put(&opt->priv->paths, a_path, ci);
}
if (rename_b)
b_path = unique_path(&opt->priv->paths,
path, opt->branch2);
else
b_path = path;
strmap_put(&opt->priv->paths, b_path, new_ci);
if (rename_a && rename_b) {
strmap_remove(&opt->priv->paths, path, 0);
/*
* We removed path from opt->priv->paths. path
* will also eventually need to be freed, but
* it may still be used by e.g. ci->pathnames.
* So, store it in another string-list for now.
*/
string_list_append(&opt->priv->paths_to_free,
path);
}
/*
* Do special handling for b_path since process_entry()
* won't be called on it specially.
*/
strmap_put(&opt->priv->conflicted, b_path, new_ci);
record_entry_for_tree(dir_metadata, b_path,
&new_ci->merged);
/*
* Remaining code for processing this entry should
* think in terms of processing a_path.
*/
if (a_path)
path = a_path;
}
} else if (ci->filemask >= 6) {
/*
* TODO: Needs a two-way or three-way content merge, but we're
* just being lazy and copying the version from HEAD and
* leaving it as conflicted.
*/
ci->merged.clean = 0;
ci->merged.result.mode = ci->stages[1].mode;
oidcpy(&ci->merged.result.oid, &ci->stages[1].oid);
/* When we fix above, we'll call handle_content_merge() */
(void)handle_content_merge;
/* Need a two-way or three-way content merge */
struct version_info merged_file;
unsigned clean_merge;
struct version_info *o = &ci->stages[0];
struct version_info *a = &ci->stages[1];
struct version_info *b = &ci->stages[2];
clean_merge = handle_content_merge(opt, path, o, a, b,
ci->pathnames,
opt->priv->call_depth * 2,
&merged_file);
ci->merged.clean = clean_merge &&
!ci->df_conflict && !ci->path_conflict;
ci->merged.result.mode = merged_file.mode;
ci->merged.is_null = (merged_file.mode == 0);
oidcpy(&ci->merged.result.oid, &merged_file.oid);
if (clean_merge && ci->df_conflict) {
assert(df_file_index == 1 || df_file_index == 2);
ci->filemask = 1 << df_file_index;
ci->stages[df_file_index].mode = merged_file.mode;
oidcpy(&ci->stages[df_file_index].oid, &merged_file.oid);
}
if (!clean_merge) {
const char *reason = _("content");
if (ci->filemask == 6)
reason = _("add/add");
if (S_ISGITLINK(merged_file.mode))
reason = _("submodule");
path_msg(opt, path, 0,
_("CONFLICT (%s): Merge conflict in %s"),
reason, path);
}
} else if (ci->filemask == 3 || ci->filemask == 5) {
/* Modify/delete */
const char *modify_branch, *delete_branch;