From cb2607759e27627aca614726dc50e98ac0ba6d19 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 18 Jun 2022 00:20:56 +0000 Subject: [PATCH] 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 Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- merge-ort.c | 267 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 212 insertions(+), 55 deletions(-) diff --git a/merge-ort.c b/merge-ort.c index f31c7029ce..6081b29705 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -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,16 +1757,18 @@ 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, - _("Failed to merge submodule %s (not checked out)"), - path); + path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0, + path, NULL, NULL, NULL, + _("Failed to merge submodule %s (not checked out)"), + path); return 0; } 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,21 +2219,24 @@ 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."), - new_path, collision_paths.buf); + 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"), - new_path, collision_paths.buf); + 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,13 +2290,14 @@ static void get_provisional_directory_renames(struct merge_options *opt, continue; if (bad_max == max) { - path_msg(opt, source_dir, 0, - _("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."), - source_dir); + 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."), + source_dir); *clean = 0; } else { strmap_put(&renames->dir_renames[side], @@ -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);