diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt index c89d530d3d..7e27841a95 100644 --- a/Documentation/diff-options.txt +++ b/Documentation/diff-options.txt @@ -34,7 +34,7 @@ endif::git-diff[] endif::git-format-patch[] ifdef::git-log[] ---diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc):: +--diff-merges=(off|none|on|first-parent|1|separate|m|combined|c|dense-combined|cc|remerge|r):: --no-diff-merges:: Specify diff format to be used for merge commits. Default is {diff-merges-default} unless `--first-parent` is in use, in which case @@ -64,6 +64,18 @@ ifdef::git-log[] each of the parents. Separate log entry and diff is generated for each parent. + +--diff-merges=remerge::: +--diff-merges=r::: +--remerge-diff::: + With this option, two-parent merge commits are remerged to + create a temporary tree object -- potentially containing files + with conflict markers and such. A diff is then shown between + that temporary tree and the actual merge commit. ++ +The output emitted when this option is used is subject to change, and +so is its interaction with other options (unless explicitly +documented). ++ --diff-merges=combined::: --diff-merges=c::: -c::: diff --git a/apply.c b/apply.c index f66d48ba2f..0912307bd9 100644 --- a/apply.c +++ b/apply.c @@ -3494,7 +3494,7 @@ static int three_way_merge(struct apply_state *state, { mmfile_t base_file, our_file, their_file; mmbuffer_t result = { NULL }; - int status; + enum ll_merge_result status; /* resolve trivial cases first */ if (oideq(base, ours)) @@ -3511,6 +3511,9 @@ static int three_way_merge(struct apply_state *state, &their_file, "theirs", state->repo->index, NULL); + if (status == LL_MERGE_BINARY_CONFLICT) + warning("Cannot merge binary files: %s (%s vs. %s)", + path, "ours", "theirs"); free(base_file.ptr); free(our_file.ptr); free(their_file.ptr); diff --git a/builtin/checkout.c b/builtin/checkout.c index 8f010412a9..f6e65fedfa 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -246,6 +246,7 @@ static int checkout_merged(int pos, const struct checkout *state, struct cache_entry *ce = active_cache[pos]; const char *path = ce->name; mmfile_t ancestor, ours, theirs; + enum ll_merge_result merge_status; int status; struct object_id oid; mmbuffer_t result_buf; @@ -276,13 +277,16 @@ static int checkout_merged(int pos, const struct checkout *state, memset(&ll_opts, 0, sizeof(ll_opts)); git_config_get_bool("merge.renormalize", &renormalize); ll_opts.renormalize = renormalize; - status = ll_merge(&result_buf, path, &ancestor, "base", - &ours, "ours", &theirs, "theirs", - state->istate, &ll_opts); + merge_status = ll_merge(&result_buf, path, &ancestor, "base", + &ours, "ours", &theirs, "theirs", + state->istate, &ll_opts); free(ancestor.ptr); free(ours.ptr); free(theirs.ptr); - if (status < 0 || !result_buf.ptr) { + if (merge_status == LL_MERGE_BINARY_CONFLICT) + warning("Cannot merge binary files: %s (%s vs. %s)", + path, "ours", "theirs"); + if (merge_status < 0 || !result_buf.ptr) { free(result_buf.ptr); return error(_("path '%s': cannot merge"), path); } diff --git a/builtin/log.c b/builtin/log.c index 4b493408cc..093d0d2655 100644 --- a/builtin/log.c +++ b/builtin/log.c @@ -35,6 +35,7 @@ #include "repository.h" #include "commit-reach.h" #include "range-diff.h" +#include "tmp-objdir.h" #define MAIL_DEFAULT_WRAP 72 #define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100 @@ -422,6 +423,13 @@ static int cmd_log_walk(struct rev_info *rev) int saved_nrl = 0; int saved_dcctc = 0; + if (rev->remerge_diff) { + rev->remerge_objdir = tmp_objdir_create("remerge-diff"); + if (!rev->remerge_objdir) + die(_("unable to create temporary object directory")); + tmp_objdir_replace_primary_odb(rev->remerge_objdir, 1); + } + if (rev->early_output) setup_early_output(); @@ -464,6 +472,11 @@ static int cmd_log_walk(struct rev_info *rev) rev->diffopt.no_free = 0; diff_free(&rev->diffopt); + if (rev->remerge_diff) { + tmp_objdir_destroy(rev->remerge_objdir); + rev->remerge_objdir = NULL; + } + if (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF && rev->diffopt.flags.check_failed) { return 02; @@ -1958,6 +1971,8 @@ int cmd_format_patch(int argc, const char **argv, const char *prefix) die(_("--name-status does not make sense")); if (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF) die(_("--check does not make sense")); + if (rev.remerge_diff) + die(_("--remerge-diff does not make sense")); if (!use_patch_format && (!rev.diffopt.output_format || diff --git a/diff-merges.c b/diff-merges.c index 5060ccd890..a833fd747a 100644 --- a/diff-merges.c +++ b/diff-merges.c @@ -17,12 +17,14 @@ static void suppress(struct rev_info *revs) revs->combined_all_paths = 0; revs->merges_imply_patch = 0; revs->merges_need_diff = 0; + revs->remerge_diff = 0; } static void set_separate(struct rev_info *revs) { suppress(revs); revs->separate_merges = 1; + revs->simplify_history = 0; } static void set_first_parent(struct rev_info *revs) @@ -45,6 +47,13 @@ static void set_dense_combined(struct rev_info *revs) revs->dense_combined_merges = 1; } +static void set_remerge_diff(struct rev_info *revs) +{ + suppress(revs); + revs->remerge_diff = 1; + revs->simplify_history = 0; +} + static diff_merges_setup_func_t func_by_opt(const char *optarg) { if (!strcmp(optarg, "off") || !strcmp(optarg, "none")) @@ -57,6 +66,8 @@ static diff_merges_setup_func_t func_by_opt(const char *optarg) return set_combined; else if (!strcmp(optarg, "cc") || !strcmp(optarg, "dense-combined")) return set_dense_combined; + else if (!strcmp(optarg, "r") || !strcmp(optarg, "remerge")) + return set_remerge_diff; else if (!strcmp(optarg, "m") || !strcmp(optarg, "on")) return set_to_default; return NULL; @@ -110,6 +121,9 @@ int diff_merges_parse_opts(struct rev_info *revs, const char **argv) } else if (!strcmp(arg, "--cc")) { set_dense_combined(revs); revs->merges_imply_patch = 1; + } else if (!strcmp(arg, "--remerge-diff")) { + set_remerge_diff(revs); + revs->merges_imply_patch = 1; } else if (!strcmp(arg, "--no-diff-merges")) { suppress(revs); } else if (!strcmp(arg, "--combined-all-paths")) { diff --git a/diff.c b/diff.c index c862771a58..32f0c01236 100644 --- a/diff.c +++ b/diff.c @@ -28,6 +28,7 @@ #include "help.h" #include "promisor-remote.h" #include "dir.h" +#include "strmap.h" #ifdef NO_FAST_WORKING_DIRECTORY #define FAST_WORKING_DIRECTORY 0 @@ -3353,6 +3354,31 @@ struct userdiff_driver *get_textconv(struct repository *r, return userdiff_get_textconv(r, one->driver); } +static struct strbuf *additional_headers(struct diff_options *o, + const char *path) +{ + if (!o->additional_path_headers) + return NULL; + return strmap_get(o->additional_path_headers, path); +} + +static void add_formatted_headers(struct strbuf *msg, + struct strbuf *more_headers, + const char *line_prefix, + const char *meta, + const char *reset) +{ + char *next, *newline; + + for (next = more_headers->buf; *next; next = newline) { + newline = strchrnul(next, '\n'); + strbuf_addf(msg, "%s%s%.*s%s\n", line_prefix, meta, + (int)(newline - next), next, reset); + if (*newline) + newline++; + } +} + static void builtin_diff(const char *name_a, const char *name_b, struct diff_filespec *one, @@ -3411,6 +3437,17 @@ static void builtin_diff(const char *name_a, b_two = quote_two(b_prefix, name_b + (*name_b == '/')); lbl[0] = DIFF_FILE_VALID(one) ? a_one : "/dev/null"; lbl[1] = DIFF_FILE_VALID(two) ? b_two : "/dev/null"; + if (!DIFF_FILE_VALID(one) && !DIFF_FILE_VALID(two)) { + /* + * We should only reach this point for pairs from + * create_filepairs_for_header_only_notifications(). For + * these, we should avoid the "/dev/null" special casing + * above, meaning we avoid showing such pairs as either + * "new file" or "deleted file" below. + */ + lbl[0] = a_one; + lbl[1] = b_two; + } strbuf_addf(&header, "%s%sdiff --git %s %s%s\n", line_prefix, meta, a_one, b_two, reset); if (lbl[0][0] == '/') { /* /dev/null */ @@ -4275,6 +4312,7 @@ static void fill_metainfo(struct strbuf *msg, const char *set = diff_get_color(use_color, DIFF_METAINFO); const char *reset = diff_get_color(use_color, DIFF_RESET); const char *line_prefix = diff_line_prefix(o); + struct strbuf *more_headers = NULL; *must_show_header = 1; strbuf_init(msg, PATH_MAX * 2 + 300); @@ -4311,6 +4349,11 @@ static void fill_metainfo(struct strbuf *msg, default: *must_show_header = 0; } + if ((more_headers = additional_headers(o, name))) { + add_formatted_headers(msg, more_headers, + line_prefix, set, reset); + *must_show_header = 1; + } if (one && two && !oideq(&one->oid, &two->oid)) { const unsigned hexsz = the_hash_algo->hexsz; int abbrev = o->abbrev ? o->abbrev : DEFAULT_ABBREV; @@ -5803,12 +5846,27 @@ int diff_unmodified_pair(struct diff_filepair *p) static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o) { - if (diff_unmodified_pair(p)) + int include_conflict_headers = + (additional_headers(o, p->one->path) && + (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o))); + + /* + * Check if we can return early without showing a diff. Note that + * diff_filepair only stores {oid, path, mode, is_valid} + * information for each path, and thus diff_unmodified_pair() only + * considers those bits of info. However, we do not want pairs + * created by create_filepairs_for_header_only_notifications() + * (which always look like unmodified pairs) to be ignored, so + * return early if both p is unmodified AND we don't want to + * include_conflict_headers. + */ + if (diff_unmodified_pair(p) && !include_conflict_headers) return; + /* Actually, we can also return early to avoid showing tree diffs */ if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) || (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode))) - return; /* no tree diffs in patch format */ + return; run_diff(p, o); } @@ -5839,10 +5897,17 @@ static void diff_flush_checkdiff(struct diff_filepair *p, run_checkdiff(p, o); } -int diff_queue_is_empty(void) +int diff_queue_is_empty(struct diff_options *o) { struct diff_queue_struct *q = &diff_queued_diff; int i; + int include_conflict_headers = + (o->additional_path_headers && + (!o->filter || filter_bit_tst(DIFF_STATUS_UNMERGED, o))); + + if (include_conflict_headers) + return 0; + for (i = 0; i < q->nr; i++) if (!diff_unmodified_pair(q->queue[i])) return 0; @@ -6276,6 +6341,54 @@ void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc) warning(_(rename_limit_advice), varname, needed); } +static void create_filepairs_for_header_only_notifications(struct diff_options *o) +{ + struct strset present; + struct diff_queue_struct *q = &diff_queued_diff; + struct hashmap_iter iter; + struct strmap_entry *e; + int i; + + strset_init_with_options(&present, /*pool*/ NULL, /*strdup*/ 0); + + /* + * Find out which paths exist in diff_queued_diff, preferring + * one->path for any pair that has multiple paths. + */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + char *path = p->one->path ? p->one->path : p->two->path; + + if (strmap_contains(o->additional_path_headers, path)) + strset_add(&present, path); + } + + /* + * Loop over paths in additional_path_headers; for each NOT already + * in diff_queued_diff, create a synthetic filepair and insert that + * into diff_queued_diff. + */ + strmap_for_each_entry(o->additional_path_headers, &iter, e) { + if (!strset_contains(&present, e->key)) { + struct diff_filespec *one, *two; + struct diff_filepair *p; + + one = alloc_filespec(e->key); + two = alloc_filespec(e->key); + fill_filespec(one, null_oid(), 0, 0); + fill_filespec(two, null_oid(), 0, 0); + p = diff_queue(q, one, two); + p->status = DIFF_STATUS_MODIFIED; + } + } + + /* Re-sort the filepairs */ + diffcore_fix_diff_index(); + + /* Cleanup */ + strset_clear(&present); +} + static void diff_flush_patch_all_file_pairs(struct diff_options *o) { int i; @@ -6288,6 +6401,9 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o) if (o->color_moved) o->emitted_symbols = &esm; + if (o->additional_path_headers) + create_filepairs_for_header_only_notifications(o); + for (i = 0; i < q->nr; i++) { struct diff_filepair *p = q->queue[i]; if (check_pair_status(p)) @@ -6358,7 +6474,7 @@ void diff_flush(struct diff_options *options) * Order: raw, stat, summary, patch * or: name/name-status/checkdiff (other bits clear) */ - if (!q->nr) + if (!q->nr && !options->additional_path_headers) goto free_queue; if (output_format & (DIFF_FORMAT_RAW | diff --git a/diff.h b/diff.h index 8ba85c5e60..ce9e2cf2e4 100644 --- a/diff.h +++ b/diff.h @@ -395,6 +395,7 @@ struct diff_options { struct repository *repo; struct option *parseopts; + struct strmap *additional_path_headers; int no_free; }; @@ -593,7 +594,7 @@ void diffcore_fix_diff_index(void); " show all files diff when -S is used and hit is found.\n" \ " -a --text treat all files as text.\n" -int diff_queue_is_empty(void); +int diff_queue_is_empty(struct diff_options *o); void diff_flush(struct diff_options*); void diff_free(struct diff_options*); void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc); diff --git a/ll-merge.c b/ll-merge.c index 261657578c..a937cec59a 100644 --- a/ll-merge.c +++ b/ll-merge.c @@ -14,7 +14,7 @@ struct ll_merge_driver; -typedef int (*ll_merge_fn)(const struct ll_merge_driver *, +typedef enum ll_merge_result (*ll_merge_fn)(const struct ll_merge_driver *, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name, @@ -49,7 +49,7 @@ void reset_merge_attributes(void) /* * Built-in low-levels */ -static int ll_binary_merge(const struct ll_merge_driver *drv_unused, +static enum ll_merge_result ll_binary_merge(const struct ll_merge_driver *drv_unused, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name, @@ -58,6 +58,7 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused, const struct ll_merge_options *opts, int marker_size) { + enum ll_merge_result ret; mmfile_t *stolen; assert(opts); @@ -68,16 +69,19 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused, */ if (opts->virtual_ancestor) { stolen = orig; + ret = LL_MERGE_OK; } else { switch (opts->variant) { default: - warning("Cannot merge binary files: %s (%s vs. %s)", - path, name1, name2); - /* fallthru */ + ret = LL_MERGE_BINARY_CONFLICT; + stolen = src1; + break; case XDL_MERGE_FAVOR_OURS: + ret = LL_MERGE_OK; stolen = src1; break; case XDL_MERGE_FAVOR_THEIRS: + ret = LL_MERGE_OK; stolen = src2; break; } @@ -87,16 +91,10 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused, result->size = stolen->size; stolen->ptr = NULL; - /* - * With -Xtheirs or -Xours, we have cleanly merged; - * otherwise we got a conflict. - */ - return opts->variant == XDL_MERGE_FAVOR_OURS || - opts->variant == XDL_MERGE_FAVOR_THEIRS ? - 0 : 1; + return ret; } -static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, +static enum ll_merge_result ll_xdl_merge(const struct ll_merge_driver *drv_unused, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name, @@ -105,7 +103,9 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, const struct ll_merge_options *opts, int marker_size) { + enum ll_merge_result ret; xmparam_t xmp; + int status; assert(opts); if (orig->size > MAX_XDIFF_SIZE || @@ -133,10 +133,12 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused, xmp.ancestor = orig_name; xmp.file1 = name1; xmp.file2 = name2; - return xdl_merge(orig, src1, src2, &xmp, result); + status = xdl_merge(orig, src1, src2, &xmp, result); + ret = (status > 0) ? LL_MERGE_CONFLICT : status; + return ret; } -static int ll_union_merge(const struct ll_merge_driver *drv_unused, +static enum ll_merge_result ll_union_merge(const struct ll_merge_driver *drv_unused, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name, @@ -178,7 +180,7 @@ static void create_temp(mmfile_t *src, char *path, size_t len) /* * User defined low-level merge driver support. */ -static int ll_ext_merge(const struct ll_merge_driver *fn, +static enum ll_merge_result ll_ext_merge(const struct ll_merge_driver *fn, mmbuffer_t *result, const char *path, mmfile_t *orig, const char *orig_name, @@ -194,6 +196,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn, const char *args[] = { NULL, NULL }; int status, fd, i; struct stat st; + enum ll_merge_result ret; assert(opts); sq_quote_buf(&path_sq, path); @@ -236,7 +239,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn, unlink_or_warn(temp[i]); strbuf_release(&cmd); strbuf_release(&path_sq); - return status; + ret = (status > 0) ? LL_MERGE_CONFLICT : status; + return ret; } /* @@ -362,7 +366,7 @@ static void normalize_file(mmfile_t *mm, const char *path, struct index_state *i } } -int ll_merge(mmbuffer_t *result_buf, +enum ll_merge_result ll_merge(mmbuffer_t *result_buf, const char *path, mmfile_t *ancestor, const char *ancestor_label, mmfile_t *ours, const char *our_label, diff --git a/ll-merge.h b/ll-merge.h index aceb1b2413..e4a20e81a3 100644 --- a/ll-merge.h +++ b/ll-merge.h @@ -82,13 +82,20 @@ struct ll_merge_options { long xdl_opts; }; +enum ll_merge_result { + LL_MERGE_ERROR = -1, + LL_MERGE_OK = 0, + LL_MERGE_CONFLICT, + LL_MERGE_BINARY_CONFLICT, +}; + /** * Perform a three-way single-file merge in core. This is a thin wrapper * around `xdl_merge` that takes the path and any merge backend specified in * `.gitattributes` or `.git/info/attributes` into account. * Returns 0 for a clean merge. */ -int ll_merge(mmbuffer_t *result_buf, +enum ll_merge_result ll_merge(mmbuffer_t *result_buf, const char *path, mmfile_t *ancestor, const char *ancestor_label, mmfile_t *ours, const char *our_label, diff --git a/log-tree.c b/log-tree.c index d3e7a40b64..25165e2a91 100644 --- a/log-tree.c +++ b/log-tree.c @@ -1,12 +1,15 @@ #include "cache.h" +#include "commit-reach.h" #include "config.h" #include "diff.h" #include "object-store.h" #include "repository.h" +#include "tmp-objdir.h" #include "commit.h" #include "tag.h" #include "graph.h" #include "log-tree.h" +#include "merge-ort.h" #include "reflog-walk.h" #include "refs.h" #include "string-list.h" @@ -16,6 +19,7 @@ #include "line-log.h" #include "help.h" #include "range-diff.h" +#include "strmap.h" static struct decoration name_decoration = { "object names" }; static int decoration_loaded; @@ -849,7 +853,7 @@ int log_tree_diff_flush(struct rev_info *opt) opt->shown_dashes = 0; diffcore_std(&opt->diffopt); - if (diff_queue_is_empty()) { + if (diff_queue_is_empty(&opt->diffopt)) { int saved_fmt = opt->diffopt.output_format; opt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT; diff_flush(&opt->diffopt); @@ -904,6 +908,106 @@ static int do_diff_combined(struct rev_info *opt, struct commit *commit) return !opt->loginfo; } +static void setup_additional_headers(struct diff_options *o, + struct strmap *all_headers) +{ + struct hashmap_iter iter; + struct strmap_entry *entry; + + /* + * Make o->additional_path_headers contain the subset of all_headers + * that match o->pathspec. If there aren't any that match o->pathspec, + * then make o->additional_path_headers be NULL. + */ + + if (!o->pathspec.nr) { + o->additional_path_headers = all_headers; + return; + } + + o->additional_path_headers = xmalloc(sizeof(struct strmap)); + strmap_init_with_options(o->additional_path_headers, NULL, 0); + strmap_for_each_entry(all_headers, &iter, entry) { + if (match_pathspec(the_repository->index, &o->pathspec, + entry->key, strlen(entry->key), + 0 /* prefix */, NULL /* seen */, + 0 /* is_dir */)) + strmap_put(o->additional_path_headers, + entry->key, entry->value); + } + if (!strmap_get_size(o->additional_path_headers)) { + strmap_clear(o->additional_path_headers, 0); + FREE_AND_NULL(o->additional_path_headers); + } +} + +static void cleanup_additional_headers(struct diff_options *o) +{ + if (!o->pathspec.nr) { + o->additional_path_headers = NULL; + return; + } + if (!o->additional_path_headers) + return; + + strmap_clear(o->additional_path_headers, 0); + FREE_AND_NULL(o->additional_path_headers); +} + +static int do_remerge_diff(struct rev_info *opt, + struct commit_list *parents, + struct object_id *oid, + struct commit *commit) +{ + struct merge_options o; + struct commit_list *bases; + struct merge_result res = {0}; + struct pretty_print_context ctx = {0}; + struct commit *parent1 = parents->item; + struct commit *parent2 = parents->next->item; + struct strbuf parent1_desc = STRBUF_INIT; + struct strbuf parent2_desc = STRBUF_INIT; + + /* Setup merge options */ + init_merge_options(&o, the_repository); + o.show_rename_progress = 0; + o.record_conflict_msgs_as_headers = 1; + o.msg_header_prefix = "remerge"; + + ctx.abbrev = DEFAULT_ABBREV; + format_commit_message(parent1, "%h (%s)", &parent1_desc, &ctx); + format_commit_message(parent2, "%h (%s)", &parent2_desc, &ctx); + o.branch1 = parent1_desc.buf; + o.branch2 = parent2_desc.buf; + + /* Parse the relevant commits and get the merge bases */ + parse_commit_or_die(parent1); + parse_commit_or_die(parent2); + bases = get_merge_bases(parent1, parent2); + + /* Re-merge the parents */ + merge_incore_recursive(&o, bases, parent1, parent2, &res); + + /* Show the diff */ + setup_additional_headers(&opt->diffopt, res.path_messages); + diff_tree_oid(&res.tree->object.oid, oid, "", &opt->diffopt); + log_tree_diff_flush(opt); + + /* Cleanup */ + cleanup_additional_headers(&opt->diffopt); + strbuf_release(&parent1_desc); + strbuf_release(&parent2_desc); + merge_finalize(&o, &res); + + /* Clean up the contents of the temporary object directory */ + if (opt->remerge_objdir) + tmp_objdir_discard_objects(opt->remerge_objdir); + else + BUG("did a remerge diff without remerge_objdir?!?"); + + return !opt->loginfo; +} + /* * Show the diff of a commit. * @@ -938,6 +1042,18 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log } if (is_merge) { + int octopus = (parents->next->next != NULL); + + if (opt->remerge_diff) { + if (octopus) { + show_log(opt); + fprintf(opt->diffopt.file, + "diff: warning: Skipping remerge-diff " + "for octopus merges.\n"); + return 1; + } + return do_remerge_diff(opt, parents, oid, commit); + } if (opt->combine_merges) return do_diff_combined(opt, commit); if (opt->separate_merges) { diff --git a/merge-blobs.c b/merge-blobs.c index ee0a0e90c9..8138090f81 100644 --- a/merge-blobs.c +++ b/merge-blobs.c @@ -36,7 +36,7 @@ static void *three_way_filemerge(struct index_state *istate, mmfile_t *their, unsigned long *size) { - int merge_status; + enum ll_merge_result merge_status; mmbuffer_t res; /* @@ -50,6 +50,9 @@ static void *three_way_filemerge(struct index_state *istate, istate, NULL); if (merge_status < 0) return NULL; + if (merge_status == LL_MERGE_BINARY_CONFLICT) + warning("Cannot merge binary files: %s (%s vs. %s)", + path, ".our", ".their"); *size = res.size; return res.ptr; diff --git a/merge-ort.c b/merge-ort.c index d455fca7d2..d85b1cd99e 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -634,17 +634,51 @@ static void path_msg(struct merge_options *opt, const char *fmt, ...) { va_list ap; - struct strbuf *sb = strmap_get(&opt->priv->output, path); + struct strbuf *sb, *dest; + struct strbuf tmp = STRBUF_INIT; + + if (opt->record_conflict_msgs_as_headers && omittable_hint) + return; /* Do not record mere hints in headers */ + if (opt->record_conflict_msgs_as_headers && opt->priv->call_depth) + return; /* Do not record inner merge issues in headers */ + sb = strmap_get(&opt->priv->output, path); if (!sb) { sb = xmalloc(sizeof(*sb)); strbuf_init(sb, 0); strmap_put(&opt->priv->output, path, sb); } + dest = (opt->record_conflict_msgs_as_headers ? &tmp : sb); + va_start(ap, fmt); - strbuf_vaddf(sb, fmt, ap); + strbuf_vaddf(dest, fmt, ap); va_end(ap); + if (opt->record_conflict_msgs_as_headers) { + int i_sb = 0, i_tmp = 0; + + /* Start with the specified prefix */ + if (opt->msg_header_prefix) + strbuf_addf(sb, "%s ", opt->msg_header_prefix); + + /* Copy tmp to sb, adding spaces after newlines */ + strbuf_grow(sb, sb->len + 2*tmp.len); /* more than sufficient */ + for (; i_tmp < tmp.len; i_tmp++, i_sb++) { + /* Copy next character from tmp to sb */ + sb->buf[sb->len + i_sb] = tmp.buf[i_tmp]; + + /* If we copied a newline, add a space */ + if (tmp.buf[i_tmp] == '\n') + sb->buf[++i_sb] = ' '; + } + /* Update length and ensure it's NUL-terminated */ + sb->len += i_sb; + sb->buf[sb->len] = '\0'; + + strbuf_release(&tmp); + } + + /* Add final newline character to sb */ strbuf_addch(sb, '\n'); } @@ -1743,7 +1777,7 @@ static int merge_3way(struct merge_options *opt, mmfile_t orig, src1, src2; struct ll_merge_options ll_opts = {0}; char *base, *name1, *name2; - int merge_status; + enum ll_merge_result merge_status; if (!opt->priv->attr_index.initialized) initialize_attr_index(opt); @@ -1787,6 +1821,10 @@ static int merge_3way(struct merge_options *opt, merge_status = ll_merge(result_buf, path, &orig, base, &src1, name1, &src2, name2, &opt->priv->attr_index, &ll_opts); + if (merge_status == LL_MERGE_BINARY_CONFLICT) + path_msg(opt, path, 0, + "warning: Cannot merge binary files: %s (%s vs. %s)", + path, name1, name2); free(base); free(name1); @@ -2416,7 +2454,7 @@ static void apply_directory_rename_modifications(struct merge_options *opt, */ ci->path_conflict = 1; if (pair->status == 'A') - path_msg(opt, new_path, 0, + path_msg(opt, new_path, 1, _("CONFLICT (file location): %s added in %s " "inside a directory that was renamed in %s, " "suggesting it should perhaps be moved to " @@ -2424,7 +2462,7 @@ 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, 0, + path_msg(opt, new_path, 1, _("CONFLICT (file location): %s renamed to %s " "in %s, inside a directory that was renamed " "in %s, suggesting it should perhaps be " @@ -4259,6 +4297,9 @@ void merge_switch_to_result(struct merge_options *opt, struct string_list olist = STRING_LIST_INIT_NODUP; int i; + if (opt->record_conflict_msgs_as_headers) + BUG("Either display conflict messages or record them as headers, not both"); + trace2_region_enter("merge", "display messages", opt->repo); /* Hack to pre-allocate olist to the desired size */ @@ -4360,6 +4401,9 @@ static void merge_start(struct merge_options *opt, struct merge_result *result) assert(opt->recursive_variant >= MERGE_VARIANT_NORMAL && opt->recursive_variant <= MERGE_VARIANT_THEIRS); + if (opt->msg_header_prefix) + assert(opt->record_conflict_msgs_as_headers); + /* * detect_renames, verbosity, buffer_output, and obuf are ignored * fields that were used by "recursive" rather than "ort" -- but @@ -4560,6 +4604,7 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt, trace2_region_leave("merge", "process_entries", opt->repo); /* Set return values */ + result->path_messages = &opt->priv->output; result->tree = parse_tree_indirect(&working_tree_oid); /* existence of conflicted entries implies unclean */ result->clean &= strmap_empty(&opt->priv->conflicted); diff --git a/merge-ort.h b/merge-ort.h index c011864ffe..fe599b8786 100644 --- a/merge-ort.h +++ b/merge-ort.h @@ -5,6 +5,7 @@ struct commit; struct tree; +struct strmap; struct merge_result { /* @@ -23,6 +24,15 @@ struct merge_result { */ struct tree *tree; + /* + * Special messages and conflict notices for various paths + * + * This is a map of pathnames to strbufs. It contains various + * warning/conflict/notice messages (possibly multiple per path) + * that callers may want to use. + */ + struct strmap *path_messages; + /* * Additional metadata used by merge_switch_to_result() or future calls * to merge_incore_*(). Includes data needed to update the index (if diff --git a/merge-recursive.c b/merge-recursive.c index d9457797db..9ec1e6d043 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -1044,7 +1044,7 @@ static int merge_3way(struct merge_options *opt, mmfile_t orig, src1, src2; struct ll_merge_options ll_opts = {0}; char *base, *name1, *name2; - int merge_status; + enum ll_merge_result merge_status; ll_opts.renormalize = opt->renormalize; ll_opts.extra_marker_size = extra_marker_size; @@ -1090,6 +1090,9 @@ static int merge_3way(struct merge_options *opt, merge_status = ll_merge(result_buf, a->path, &orig, base, &src1, name1, &src2, name2, opt->repo->index, &ll_opts); + if (merge_status == LL_MERGE_BINARY_CONFLICT) + warning("Cannot merge binary files: %s (%s vs. %s)", + a->path, name1, name2); free(base); free(name1); @@ -3711,6 +3714,10 @@ static int merge_start(struct merge_options *opt, struct tree *head) assert(opt->priv == NULL); + /* Not supported; option specific to merge-ort */ + assert(!opt->record_conflict_msgs_as_headers); + assert(!opt->msg_header_prefix); + /* Sanity check on repo state; index must match head */ if (repo_index_has_changes(opt->repo, head, &sb)) { err(opt, _("Your local changes to the following files would be overwritten by merge:\n %s"), diff --git a/merge-recursive.h b/merge-recursive.h index 0795a1d3ec..b88000e3c2 100644 --- a/merge-recursive.h +++ b/merge-recursive.h @@ -46,6 +46,8 @@ struct merge_options { /* miscellaneous control options */ const char *subtree_shift; unsigned renormalize : 1; + unsigned record_conflict_msgs_as_headers : 1; + const char *msg_header_prefix; /* internal fields used by the implementation */ struct merge_options_internal *priv; diff --git a/notes-merge.c b/notes-merge.c index b4a3a903e8..01d596920e 100644 --- a/notes-merge.c +++ b/notes-merge.c @@ -344,7 +344,7 @@ static int ll_merge_in_worktree(struct notes_merge_options *o, { mmbuffer_t result_buf; mmfile_t base, local, remote; - int status; + enum ll_merge_result status; read_mmblob(&base, &p->base); read_mmblob(&local, &p->local); @@ -358,6 +358,9 @@ static int ll_merge_in_worktree(struct notes_merge_options *o, free(local.ptr); free(remote.ptr); + if (status == LL_MERGE_BINARY_CONFLICT) + warning("Cannot merge binary files: %s (%s vs. %s)", + oid_to_hex(&p->obj), o->local_ref, o->remote_ref); if ((status < 0) || !result_buf.ptr) die("Failed to execute internal merge"); diff --git a/rerere.c b/rerere.c index d83d58df4f..d26627c593 100644 --- a/rerere.c +++ b/rerere.c @@ -609,19 +609,20 @@ static int try_merge(struct index_state *istate, const struct rerere_id *id, const char *path, mmfile_t *cur, mmbuffer_t *result) { - int ret; + enum ll_merge_result ret; mmfile_t base = {NULL, 0}, other = {NULL, 0}; if (read_mmfile(&base, rerere_path(id, "preimage")) || - read_mmfile(&other, rerere_path(id, "postimage"))) - ret = 1; - else + read_mmfile(&other, rerere_path(id, "postimage"))) { + ret = LL_MERGE_CONFLICT; + } else { /* * A three-way merge. Note that this honors user-customizable * low-level merge driver settings. */ ret = ll_merge(result, path, &base, NULL, cur, "", &other, "", istate, NULL); + } free(base.ptr); free(other.ptr); diff --git a/revision.h b/revision.h index 3f66147bfd..3c58c18c63 100644 --- a/revision.h +++ b/revision.h @@ -195,7 +195,8 @@ struct rev_info { combine_merges:1, combined_all_paths:1, dense_combined_merges:1, - first_parent_merges:1; + first_parent_merges:1, + remerge_diff:1; /* Format info */ int show_notes; @@ -315,6 +316,9 @@ struct rev_info { /* misc. flags related to '--no-kept-objects' */ unsigned keep_pack_cache_flags; + + /* Location where temporary objects for remerge-diff are written. */ + struct tmp_objdir *remerge_objdir; }; int ref_excluded(struct string_list *, const char *path); diff --git a/t/t4069-remerge-diff.sh b/t/t4069-remerge-diff.sh new file mode 100755 index 0000000000..35f94957fc --- /dev/null +++ b/t/t4069-remerge-diff.sh @@ -0,0 +1,291 @@ +#!/bin/sh + +test_description='remerge-diff handling' + +. ./test-lib.sh + +# This test is ort-specific +if test "${GIT_TEST_MERGE_ALGORITHM}" != ort +then + skip_all="GIT_TEST_MERGE_ALGORITHM != ort" + test_done +fi + +test_expect_success 'setup basic merges' ' + test_write_lines 1 2 3 4 5 6 7 8 9 >numbers && + git add numbers && + git commit -m base && + + git branch feature_a && + git branch feature_b && + git branch feature_c && + + git branch ab_resolution && + git branch bc_resolution && + + git checkout feature_a && + test_write_lines 1 2 three 4 5 6 7 eight 9 >numbers && + git commit -a -m change_a && + + git checkout feature_b && + test_write_lines 1 2 tres 4 5 6 7 8 9 >numbers && + git commit -a -m change_b && + + git checkout feature_c && + test_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers && + git commit -a -m change_c && + + git checkout bc_resolution && + git merge --ff-only feature_b && + # no conflict + git merge feature_c && + + git checkout ab_resolution && + git merge --ff-only feature_a && + # conflicts! + test_must_fail git merge feature_b && + # Resolve conflict...and make another change elsewhere + test_write_lines 1 2 drei 4 5 6 7 acht 9 >numbers && + git add numbers && + git merge --continue +' + +test_expect_success 'remerge-diff on a clean merge' ' + git log -1 --oneline bc_resolution >expect && + git show --oneline --remerge-diff bc_resolution >actual && + test_cmp expect actual +' + +test_expect_success 'remerge-diff with both a resolved conflict and an unrelated change' ' + git log -1 --oneline ab_resolution >tmp && + cat <<-EOF >>tmp && + diff --git a/numbers b/numbers + remerge CONFLICT (content): Merge conflict in numbers + index a1fb731..6875544 100644 + --- a/numbers + +++ b/numbers + @@ -1,13 +1,9 @@ + 1 + 2 + -<<<<<<< b0ed5cb (change_a) + -three + -======= + -tres + ->>>>>>> 6cd3f82 (change_b) + +drei + 4 + 5 + 6 + 7 + -eight + +acht + 9 + EOF + # Hashes above are sha1; rip them out so test works with sha256 + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >expect && + + git show --oneline --remerge-diff ab_resolution >tmp && + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >actual && + test_cmp expect actual +' + +test_expect_success 'setup non-content conflicts' ' + git switch --orphan base && + + test_write_lines 1 2 3 4 5 6 7 8 9 >numbers && + test_write_lines a b c d e f g h i >letters && + test_write_lines in the way >content && + git add numbers letters content && + git commit -m base && + + git branch side1 && + git branch side2 && + + git checkout side1 && + test_write_lines 1 2 three 4 5 6 7 8 9 >numbers && + git mv letters letters_side1 && + git mv content file_or_directory && + git add numbers && + git commit -m side1 && + + git checkout side2 && + git rm numbers && + git mv letters letters_side2 && + mkdir file_or_directory && + echo hello >file_or_directory/world && + git add file_or_directory/world && + git commit -m side2 && + + git checkout -b resolution side1 && + test_must_fail git merge side2 && + test_write_lines 1 2 three 4 5 6 7 8 9 >numbers && + git add numbers && + git add letters_side1 && + git rm letters && + git rm letters_side2 && + git add file_or_directory~HEAD && + git mv file_or_directory~HEAD wanted_content && + git commit -m resolved +' + +test_expect_success 'remerge-diff with non-content conflicts' ' + git log -1 --oneline resolution >tmp && + cat <<-EOF >>tmp && + diff --git a/file_or_directory~HASH (side1) b/wanted_content + similarity index 100% + rename from file_or_directory~HASH (side1) + rename to wanted_content + remerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead. + diff --git a/letters b/letters + remerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2). + diff --git a/letters_side2 b/letters_side2 + deleted file mode 100644 + index b236ae5..0000000 + --- a/letters_side2 + +++ /dev/null + @@ -1,9 +0,0 @@ + -a + -b + -c + -d + -e + -f + -g + -h + -i + diff --git a/numbers b/numbers + remerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1). Version HASH (side1) of numbers left in tree. + EOF + # We still have some sha1 hashes above; rip them out so test works + # with sha256 + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >expect && + + git show --oneline --remerge-diff resolution >tmp && + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >actual && + test_cmp expect actual +' + +test_expect_success 'remerge-diff w/ diff-filter=U: all conflict headers, no diff content' ' + git log -1 --oneline resolution >tmp && + cat <<-EOF >>tmp && + diff --git a/file_or_directory~HASH (side1) b/file_or_directory~HASH (side1) + remerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead. + diff --git a/letters b/letters + remerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2). + diff --git a/numbers b/numbers + remerge CONFLICT (modify/delete): numbers deleted in HASH (side2) and modified in HASH (side1). Version HASH (side1) of numbers left in tree. + EOF + # We still have some sha1 hashes above; rip them out so test works + # with sha256 + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >expect && + + git show --oneline --remerge-diff --diff-filter=U resolution >tmp && + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >actual && + test_cmp expect actual +' + +test_expect_success 'remerge-diff w/ diff-filter=R: relevant file + conflict header' ' + git log -1 --oneline resolution >tmp && + cat <<-EOF >>tmp && + diff --git a/file_or_directory~HASH (side1) b/wanted_content + similarity index 100% + rename from file_or_directory~HASH (side1) + rename to wanted_content + remerge CONFLICT (file/directory): directory in the way of file_or_directory from HASH (side1); moving it to file_or_directory~HASH (side1) instead. + EOF + # We still have some sha1 hashes above; rip them out so test works + # with sha256 + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >expect && + + git show --oneline --remerge-diff --diff-filter=R resolution >tmp && + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >actual && + test_cmp expect actual +' + +test_expect_success 'remerge-diff w/ pathspec: limits to relevant file including conflict header' ' + git log -1 --oneline resolution >tmp && + cat <<-EOF >>tmp && + diff --git a/letters b/letters + remerge CONFLICT (rename/rename): letters renamed to letters_side1 in HASH (side1) and to letters_side2 in HASH (side2). + diff --git a/letters_side2 b/letters_side2 + deleted file mode 100644 + index b236ae5..0000000 + --- a/letters_side2 + +++ /dev/null + @@ -1,9 +0,0 @@ + -a + -b + -c + -d + -e + -f + -g + -h + -i + EOF + # We still have some sha1 hashes above; rip them out so test works + # with sha256 + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >expect && + + git show --oneline --remerge-diff resolution -- "letters*" >tmp && + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >actual && + test_cmp expect actual +' + +test_expect_success 'setup non-content conflicts' ' + git switch --orphan newbase && + + test_write_lines 1 2 3 4 5 6 7 8 9 >numbers && + git add numbers && + git commit -m base && + + git branch newside1 && + git branch newside2 && + + git checkout newside1 && + test_write_lines 1 2 three 4 5 6 7 8 9 >numbers && + git add numbers && + git commit -m side1 && + + git checkout newside2 && + test_write_lines 1 2 drei 4 5 6 7 8 9 >numbers && + git add numbers && + git commit -m side2 && + + git checkout -b newresolution newside1 && + test_must_fail git merge newside2 && + git checkout --theirs numbers && + git add -u numbers && + git commit -m resolved +' + +test_expect_success 'remerge-diff turns off history simplification' ' + git log -1 --oneline newresolution >tmp && + cat <<-EOF >>tmp && + diff --git a/numbers b/numbers + remerge CONFLICT (content): Merge conflict in numbers + index 070e9e7..5335e78 100644 + --- a/numbers + +++ b/numbers + @@ -1,10 +1,6 @@ + 1 + 2 + -<<<<<<< 96f1e45 (side1) + -three + -======= + drei + ->>>>>>> 4fd522f (side2) + 4 + 5 + 6 + EOF + # We still have some sha1 hashes above; rip them out so test works + # with sha256 + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >expect && + + git show --oneline --remerge-diff newresolution -- numbers >tmp && + sed -e "s/[0-9a-f]\{7,\}/HASH/g" tmp >actual && + test_cmp expect actual +' + +test_done diff --git a/t/t6404-recursive-merge.sh b/t/t6404-recursive-merge.sh index eaf48e941e..b8735c6db4 100755 --- a/t/t6404-recursive-merge.sh +++ b/t/t6404-recursive-merge.sh @@ -108,8 +108,13 @@ test_expect_success 'refuse to merge binary files' ' printf "\0\0" >binary-file && git add binary-file && git commit -m binary2 && - test_must_fail git merge F >merge.out 2>merge.err && - grep "Cannot merge binary files: binary-file (HEAD vs. F)" merge.err + if test "$GIT_TEST_MERGE_ALGORITHM" = ort + then + test_must_fail git merge F >merge_output + else + test_must_fail git merge F 2>merge_output + fi && + grep "Cannot merge binary files: binary-file (HEAD vs. F)" merge_output ' test_expect_success 'mark rename/delete as unmerged' ' diff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh index 57e6af5eaa..99abefd44b 100755 --- a/t/t6406-merge-attr.sh +++ b/t/t6406-merge-attr.sh @@ -221,8 +221,13 @@ test_expect_success 'binary files with union attribute' ' printf "two\0" >bin.txt && git commit -am two && - test_must_fail git merge bin-main 2>stderr && - grep -i "warning.*cannot merge.*HEAD vs. bin-main" stderr + if test "$GIT_TEST_MERGE_ALGORITHM" = ort + then + test_must_fail git merge bin-main >output + else + test_must_fail git merge bin-main 2>output + fi && + grep -i "warning.*cannot merge.*HEAD vs. bin-main" output ' test_done diff --git a/tmp-objdir.c b/tmp-objdir.c index 3d38eeab66..adf6033549 100644 --- a/tmp-objdir.c +++ b/tmp-objdir.c @@ -79,6 +79,11 @@ static void remove_tmp_objdir_on_signal(int signo) raise(signo); } +void tmp_objdir_discard_objects(struct tmp_objdir *t) +{ + remove_dir_recursively(&t->path, REMOVE_DIR_KEEP_TOPLEVEL); +} + /* * These env_* functions are for setting up the child environment; the * "replace" variant overrides the value of any existing variable with that diff --git a/tmp-objdir.h b/tmp-objdir.h index cda5ec7677..76efc7edee 100644 --- a/tmp-objdir.h +++ b/tmp-objdir.h @@ -46,6 +46,12 @@ int tmp_objdir_migrate(struct tmp_objdir *); */ int tmp_objdir_destroy(struct tmp_objdir *); +/* + * Remove all objects from the temporary object directory, while leaving it + * around so more objects can be added. + */ +void tmp_objdir_discard_objects(struct tmp_objdir *); + /* * Add the temporary object directory as an alternate object store in the * current process.