merge-ort: store more specific conflict information

It is all fine and dandy for a regular Git command that is intended to
be run interactively to produce a bunch of messages upon an error.

However, in `merge-ort`'s case, we want to call the command e.g. in
server-side software, where the actual error messages are not quite as
interesting as machine-readable, immutable terms that describe the exact
nature of any given conflict.

With this patch, the `merge-ort` machinery records the exact type (as
specified via an `enum` value) as well as the involved path(s) together
with the conflict's message.

Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Elijah Newren 2022-06-18 00:20:56 +00:00 committed by Junio C Hamano
parent 2715e8a931
commit cb2607759e

View File

@ -483,6 +483,100 @@ struct conflict_info {
unsigned match_mask:3;
};
enum conflict_and_info_types {
/* "Simple" conflicts and informational messages */
INFO_AUTO_MERGING = 0,
CONFLICT_CONTENTS, /* text file that failed to merge */
CONFLICT_BINARY,
CONFLICT_FILE_DIRECTORY,
CONFLICT_DISTINCT_MODES,
CONFLICT_MODIFY_DELETE,
CONFLICT_PRESENT_DESPITE_SKIPPED,
/* Regular rename */
CONFLICT_RENAME_RENAME, /* same file renamed differently */
CONFLICT_RENAME_COLLIDES, /* rename/add or two files renamed to 1 */
CONFLICT_RENAME_DELETE,
/* Basic directory rename */
CONFLICT_DIR_RENAME_SUGGESTED,
INFO_DIR_RENAME_APPLIED,
/* Special directory rename cases */
INFO_DIR_RENAME_SKIPPED_DUE_TO_RERENAME,
CONFLICT_DIR_RENAME_FILE_IN_WAY,
CONFLICT_DIR_RENAME_COLLISION,
CONFLICT_DIR_RENAME_SPLIT,
/* Basic submodule */
INFO_SUBMODULE_FAST_FORWARDING,
CONFLICT_SUBMODULE_FAILED_TO_MERGE,
/* Special submodule cases broken out from FAILED_TO_MERGE */
CONFLICT_SUBMODULE_FAILED_TO_MERGE_BUT_POSSIBLE_RESOLUTION,
CONFLICT_SUBMODULE_NOT_INITIALIZED,
CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE,
CONFLICT_SUBMODULE_MAY_HAVE_REWINDS,
/* Keep this entry _last_ in the list */
NB_CONFLICT_TYPES,
};
/*
* Short description of conflict type, relied upon by external tools.
*
* We can add more entries, but DO NOT change any of these strings. Also,
* Order MUST match conflict_info_and_types.
*/
static const char *type_short_descriptions[] = {
/*** "Simple" conflicts and informational messages ***/
[INFO_AUTO_MERGING] = "Auto-merging",
[CONFLICT_CONTENTS] = "CONFLICT (contents)",
[CONFLICT_BINARY] = "CONFLICT (binary)",
[CONFLICT_FILE_DIRECTORY] = "CONFLICT (file/directory)",
[CONFLICT_DISTINCT_MODES] = "CONFLICT (distinct modes)",
[CONFLICT_MODIFY_DELETE] = "CONFLICT (modify/delete)",
[CONFLICT_PRESENT_DESPITE_SKIPPED] =
"CONFLICT (upgrade your version of git)",
/*** Regular rename ***/
[CONFLICT_RENAME_RENAME] = "CONFLICT (rename/rename)",
[CONFLICT_RENAME_COLLIDES] = "CONFLICT (rename involved in collision)",
[CONFLICT_RENAME_DELETE] = "CONFLICT (rename/delete)",
/*** Basic directory rename ***/
[CONFLICT_DIR_RENAME_SUGGESTED] =
"CONFLICT (directory rename suggested)",
[INFO_DIR_RENAME_APPLIED] = "Path updated due to directory rename",
/*** Special directory rename cases ***/
[INFO_DIR_RENAME_SKIPPED_DUE_TO_RERENAME] =
"Directory rename skipped since directory was renamed on both sides",
[CONFLICT_DIR_RENAME_FILE_IN_WAY] =
"CONFLICT (file in way of directory rename)",
[CONFLICT_DIR_RENAME_COLLISION] = "CONFLICT(directory rename collision)",
[CONFLICT_DIR_RENAME_SPLIT] = "CONFLICT(directory rename unclear split)",
/*** Basic submodule ***/
[INFO_SUBMODULE_FAST_FORWARDING] = "Fast forwarding submodule",
[CONFLICT_SUBMODULE_FAILED_TO_MERGE] = "CONFLICT (submodule)",
/*** Special submodule cases broken out from FAILED_TO_MERGE ***/
[CONFLICT_SUBMODULE_FAILED_TO_MERGE_BUT_POSSIBLE_RESOLUTION] =
"CONFLICT (submodule with possible resolution)",
[CONFLICT_SUBMODULE_NOT_INITIALIZED] =
"CONFLICT (submodule not initialized)",
[CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE] =
"CONFLICT (submodule history not available)",
[CONFLICT_SUBMODULE_MAY_HAVE_REWINDS] =
"CONFLICT (submodule may have rewinds)",
};
struct logical_conflict_info {
enum conflict_and_info_types type;
struct strvec paths;
};
/*** Function Grouping: various utility functions ***/
/*
@ -571,6 +665,11 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
/* Release and free each strbuf found in output */
strmap_for_each_entry(&opti->conflicts, &iter, e) {
struct string_list *list = e->value;
for (int i = 0; i < list->nr; i++) {
struct logical_conflict_info *info =
list->items[i].util;
strvec_clear(&info->paths);
}
/*
* While strictly speaking we don't need to
* free(conflicts) here because we could pass
@ -629,31 +728,56 @@ static void format_commit(struct strbuf *sb,
strbuf_addch(sb, '\n');
}
__attribute__((format (printf, 4, 5)))
__attribute__((format (printf, 8, 9)))
static void path_msg(struct merge_options *opt,
const char *path,
enum conflict_and_info_types type,
int omittable_hint, /* skippable under --remerge-diff */
const char *primary_path,
const char *other_path_1, /* may be NULL */
const char *other_path_2, /* may be NULL */
struct string_list *other_paths, /* may be NULL */
const char *fmt, ...)
{
va_list ap;
struct string_list *path_conflicts;
struct logical_conflict_info *info;
struct strbuf buf = STRBUF_INIT;
struct strbuf *dest;
struct strbuf tmp = STRBUF_INIT;
/* Sanity checks */
assert(omittable_hint ==
!starts_with(type_short_descriptions[type], "CONFLICT") ||
type == CONFLICT_DIR_RENAME_SUGGESTED ||
type == CONFLICT_PRESENT_DESPITE_SKIPPED);
if (opt->record_conflict_msgs_as_headers && omittable_hint)
return; /* Do not record mere hints in headers */
if (opt->priv->call_depth && opt->verbosity < 5)
return; /* Ignore messages from inner merges */
/* Ensure path_conflicts (ptr to array of logical_conflict) allocated */
path_conflicts = strmap_get(&opt->priv->conflicts, path);
path_conflicts = strmap_get(&opt->priv->conflicts, primary_path);
if (!path_conflicts) {
path_conflicts = xmalloc(sizeof(*path_conflicts));
string_list_init_dup(path_conflicts);
strmap_put(&opt->priv->conflicts, path, path_conflicts);
strmap_put(&opt->priv->conflicts, primary_path, path_conflicts);
}
/* Add a logical_conflict at the end to store info from this call */
info = xcalloc(1, sizeof(*info));
info->type = type;
strvec_init(&info->paths);
/* Handle the list of paths */
strvec_push(&info->paths, primary_path);
if (other_path_1)
strvec_push(&info->paths, other_path_1);
if (other_path_2)
strvec_push(&info->paths, other_path_2);
if (other_paths)
for (int i = 0; i < other_paths->nr; i++)
strvec_push(&info->paths, other_paths->items[i].string);
/* Handle message and its format, in normal case */
dest = (opt->record_conflict_msgs_as_headers ? &tmp : &buf);
@ -690,7 +814,8 @@ static void path_msg(struct merge_options *opt,
strbuf_release(&tmp);
}
string_list_append_nodup(path_conflicts, strbuf_detach(&buf, NULL));
string_list_append_nodup(path_conflicts, strbuf_detach(&buf, NULL))
->util = info;
}
static struct diff_filespec *pool_alloc_filespec(struct mem_pool *pool,
@ -1632,7 +1757,8 @@ static int merge_submodule(struct merge_options *opt,
return 0;
if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s (not checked out)"),
path);
return 0;
@ -1641,7 +1767,8 @@ static int merge_submodule(struct merge_options *opt,
if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
!(commit_a = lookup_commit_reference(&subrepo, a)) ||
!(commit_b = lookup_commit_reference(&subrepo, b))) {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s (commits not present)"),
path);
goto cleanup;
@ -1650,7 +1777,8 @@ static int merge_submodule(struct merge_options *opt,
/* check whether both changes are forward */
if (!repo_in_merge_bases(&subrepo, commit_o, commit_a) ||
!repo_in_merge_bases(&subrepo, commit_o, commit_b)) {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_MAY_HAVE_REWINDS, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s "
"(commits don't follow merge-base)"),
path);
@ -1660,7 +1788,8 @@ static int merge_submodule(struct merge_options *opt,
/* Case #1: a is contained in b or vice versa */
if (repo_in_merge_bases(&subrepo, commit_a, commit_b)) {
oidcpy(result, b);
path_msg(opt, path, 1,
path_msg(opt, INFO_SUBMODULE_FAST_FORWARDING, 1,
path, NULL, NULL, NULL,
_("Note: Fast-forwarding submodule %s to %s"),
path, oid_to_hex(b));
ret = 1;
@ -1668,7 +1797,8 @@ static int merge_submodule(struct merge_options *opt,
}
if (repo_in_merge_bases(&subrepo, commit_b, commit_a)) {
oidcpy(result, a);
path_msg(opt, path, 1,
path_msg(opt, INFO_SUBMODULE_FAST_FORWARDING, 1,
path, NULL, NULL, NULL,
_("Note: Fast-forwarding submodule %s to %s"),
path, oid_to_hex(a));
ret = 1;
@ -1691,13 +1821,16 @@ static int merge_submodule(struct merge_options *opt,
&merges);
switch (parent_count) {
case 0:
path_msg(opt, path, 0, _("Failed to merge submodule %s"), path);
path_msg(opt, CONFLICT_SUBMODULE_FAILED_TO_MERGE, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s"), path);
break;
case 1:
format_commit(&sb, 4, &subrepo,
(struct commit *)merges.objects[0].item);
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_FAILED_TO_MERGE_BUT_POSSIBLE_RESOLUTION, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s, but a possible merge "
"resolution exists: %s"),
path, sb.buf);
@ -1707,7 +1840,8 @@ static int merge_submodule(struct merge_options *opt,
for (i = 0; i < merges.nr; i++)
format_commit(&sb, 4, &subrepo,
(struct commit *)merges.objects[i].item);
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_FAILED_TO_MERGE_BUT_POSSIBLE_RESOLUTION, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s, but multiple "
"possible merges exist:\n%s"), path, sb.buf);
strbuf_release(&sb);
@ -1833,7 +1967,8 @@ static int merge_3way(struct merge_options *opt,
&src1, name1, &src2, name2,
&opt->priv->attr_index, &ll_opts);
if (merge_status == LL_MERGE_BINARY_CONFLICT)
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_BINARY, 0,
path, NULL, NULL, NULL,
"warning: Cannot merge binary files: %s (%s vs. %s)",
path, name1, name2);
@ -1945,7 +2080,8 @@ static int handle_content_merge(struct merge_options *opt,
if (ret)
return -1;
clean &= (merge_status == 0);
path_msg(opt, path, 1, _("Auto-merging %s"), path);
path_msg(opt, INFO_AUTO_MERGING, 1, path, NULL, NULL, NULL,
_("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],
@ -2083,20 +2219,23 @@ static char *handle_path_level_conflicts(struct merge_options *opt,
c_info->reported_already = 1;
strbuf_add_separated_string_list(&collision_paths, ", ",
&c_info->source_files);
path_msg(opt, new_path, 0,
_("CONFLICT (implicit dir rename): Existing file/dir "
"at %s in the way of implicit directory rename(s) "
"putting the following path(s) there: %s."),
path_msg(opt, CONFLICT_DIR_RENAME_FILE_IN_WAY, 0,
new_path, NULL, NULL, &c_info->source_files,
_("CONFLICT (implicit dir rename): Existing "
"file/dir at %s in the way of implicit "
"directory rename(s) putting the following "
"path(s) there: %s."),
new_path, collision_paths.buf);
clean = 0;
} else if (c_info->source_files.nr > 1) {
c_info->reported_already = 1;
strbuf_add_separated_string_list(&collision_paths, ", ",
&c_info->source_files);
path_msg(opt, new_path, 0,
_("CONFLICT (implicit dir rename): Cannot map more "
"than one path to %s; implicit directory renames "
"tried to put these paths there: %s"),
path_msg(opt, CONFLICT_DIR_RENAME_COLLISION, 0,
new_path, NULL, NULL, &c_info->source_files,
_("CONFLICT (implicit dir rename): Cannot map "
"more than one path to %s; implicit directory "
"renames tried to put these paths there: %s"),
new_path, collision_paths.buf);
clean = 0;
}
@ -2151,12 +2290,13 @@ static void get_provisional_directory_renames(struct merge_options *opt,
continue;
if (bad_max == max) {
path_msg(opt, source_dir, 0,
path_msg(opt, CONFLICT_DIR_RENAME_SPLIT, 0,
source_dir, NULL, NULL, NULL,
_("CONFLICT (directory rename split): "
"Unclear where to rename %s to; it was "
"renamed to multiple other directories, with "
"no destination getting a majority of the "
"files."),
"renamed to multiple other directories, "
"with no destination getting a majority of "
"the files."),
source_dir);
*clean = 0;
} else {
@ -2305,7 +2445,8 @@ static char *check_for_directory_rename(struct merge_options *opt,
*/
otherinfo = strmap_get_entry(dir_rename_exclusions, new_dir);
if (otherinfo) {
path_msg(opt, rename_info->key, 1,
path_msg(opt, INFO_DIR_RENAME_SKIPPED_DUE_TO_RERENAME, 1,
rename_info->key, path, new_dir, NULL,
_("WARNING: Avoiding applying %s -> %s rename "
"to %s, because %s itself was renamed."),
rename_info->key, new_dir, path, new_dir);
@ -2445,14 +2586,16 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
if (opt->detect_directory_renames == MERGE_DIRECTORY_RENAMES_TRUE) {
/* Notify user of updated path */
if (pair->status == 'A')
path_msg(opt, new_path, 1,
path_msg(opt, INFO_DIR_RENAME_APPLIED, 1,
new_path, old_path, NULL, NULL,
_("Path updated: %s added in %s inside a "
"directory that was renamed in %s; moving "
"it to %s."),
old_path, branch_with_new_path,
branch_with_dir_rename, new_path);
else
path_msg(opt, new_path, 1,
path_msg(opt, INFO_DIR_RENAME_APPLIED, 1,
new_path, old_path, NULL, NULL,
_("Path updated: %s renamed to %s in %s, "
"inside a directory that was renamed in %s; "
"moving it to %s."),
@ -2465,7 +2608,8 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
*/
ci->path_conflict = 1;
if (pair->status == 'A')
path_msg(opt, new_path, 1,
path_msg(opt, CONFLICT_DIR_RENAME_SUGGESTED, 1,
new_path, old_path, NULL, NULL,
_("CONFLICT (file location): %s added in %s "
"inside a directory that was renamed in %s, "
"suggesting it should perhaps be moved to "
@ -2473,7 +2617,8 @@ static void apply_directory_rename_modifications(struct merge_options *opt,
old_path, branch_with_new_path,
branch_with_dir_rename, new_path);
else
path_msg(opt, new_path, 1,
path_msg(opt, CONFLICT_DIR_RENAME_SUGGESTED, 1,
new_path, old_path, NULL, NULL,
_("CONFLICT (file location): %s renamed to %s "
"in %s, inside a directory that was renamed "
"in %s, suggesting it should perhaps be "
@ -2629,7 +2774,8 @@ static int process_renames(struct merge_options *opt,
* and remove the setting of base->path_conflict to 1.
*/
base->path_conflict = 1;
path_msg(opt, oldpath, 0,
path_msg(opt, CONFLICT_RENAME_RENAME, 0,
pathnames[0], pathnames[1], pathnames[2], NULL,
_("CONFLICT (rename/rename): %s renamed to "
"%s in %s and to %s in %s."),
pathnames[0],
@ -2724,7 +2870,8 @@ static int process_renames(struct merge_options *opt,
memcpy(&newinfo->stages[target_index], &merged,
sizeof(merged));
if (!clean) {
path_msg(opt, newpath, 0,
path_msg(opt, CONFLICT_RENAME_COLLIDES, 0,
newpath, oldpath, NULL, NULL,
_("CONFLICT (rename involved in "
"collision): rename of %s -> %s has "
"content conflicts AND collides "
@ -2743,7 +2890,8 @@ static int process_renames(struct merge_options *opt,
*/
newinfo->path_conflict = 1;
path_msg(opt, newpath, 0,
path_msg(opt, CONFLICT_RENAME_DELETE, 0,
newpath, oldpath, NULL, NULL,
_("CONFLICT (rename/delete): %s renamed "
"to %s in %s, but deleted in %s."),
oldpath, newpath, rename_branch, delete_branch);
@ -2767,7 +2915,8 @@ static int process_renames(struct merge_options *opt,
} else if (source_deleted) {
/* rename/delete */
newinfo->path_conflict = 1;
path_msg(opt, newpath, 0,
path_msg(opt, CONFLICT_RENAME_DELETE, 0,
newpath, oldpath, NULL, NULL,
_("CONFLICT (rename/delete): %s renamed"
" to %s in %s, but deleted in %s."),
oldpath, newpath,
@ -3688,7 +3837,8 @@ static void process_entry(struct merge_options *opt,
path = unique_path(opt, path, branch);
strmap_put(&opt->priv->paths, path, new_ci);
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_FILE_DIRECTORY, 0,
path, old_path, NULL, NULL,
_("CONFLICT (file/directory): directory in the way "
"of %s from %s; moving it to %s instead."),
old_path, branch, path);
@ -3764,15 +3914,23 @@ static void process_entry(struct merge_options *opt,
rename_b = 1;
}
if (rename_a)
a_path = unique_path(opt, path, opt->branch1);
if (rename_b)
b_path = unique_path(opt, path, opt->branch2);
if (rename_a && rename_b) {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_DISTINCT_MODES, 0,
path, a_path, b_path, NULL,
_("CONFLICT (distinct types): %s had "
"different types on each side; "
"renamed both of them so each can "
"be recorded somewhere."),
path);
} else {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_DISTINCT_MODES, 0,
path, rename_a ? a_path : b_path,
NULL, NULL,
_("CONFLICT (distinct types): %s had "
"different types on each side; "
"renamed one of them so each can be "
@ -3809,14 +3967,10 @@ static void process_entry(struct merge_options *opt,
/* Insert entries into opt->priv_paths */
assert(rename_a || rename_b);
if (rename_a) {
a_path = unique_path(opt, path, opt->branch1);
if (rename_a)
strmap_put(&opt->priv->paths, a_path, ci);
}
if (rename_b)
b_path = unique_path(opt, path, opt->branch2);
else
if (!rename_b)
b_path = path;
strmap_put(&opt->priv->paths, b_path, new_ci);
@ -3867,7 +4021,8 @@ static void process_entry(struct merge_options *opt,
reason = _("add/add");
if (S_ISGITLINK(merged_file.mode))
reason = _("submodule");
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_CONTENTS, 0,
path, NULL, NULL, NULL,
_("CONFLICT (%s): Merge conflict in %s"),
reason, path);
}
@ -3911,7 +4066,8 @@ static void process_entry(struct merge_options *opt,
* since the contents were not modified.
*/
} else {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_MODIFY_DELETE, 0,
path, NULL, NULL, NULL,
_("CONFLICT (modify/delete): %s deleted in %s "
"and modified in %s. Version %s of %s left "
"in tree."),
@ -4207,7 +4363,8 @@ static int record_conflicted_index_entries(struct merge_options *opt)
path,
"cruft");
path_msg(opt, path, 1,
path_msg(opt, CONFLICT_PRESENT_DESPITE_SKIPPED, 1,
path, NULL, NULL, NULL,
_("Note: %s not up to date and in way of checking out conflicted version; old copy renamed to %s"),
path, new_name);
errs |= rename(path, new_name);