Merge branch 'en/remerge-diff'

"git log --remerge-diff" shows the difference from mechanical merge
result and the result that is actually recorded in a merge commit.

* en/remerge-diff:
  diff-merges: avoid history simplifications when diffing merges
  merge-ort: mark conflict/warning messages from inner merges as omittable
  show, log: include conflict/warning messages in --remerge-diff headers
  diff: add ability to insert additional headers for paths
  merge-ort: format messages slightly different for use in headers
  merge-ort: mark a few more conflict messages as omittable
  merge-ort: capture and print ll-merge warnings in our preferred fashion
  ll-merge: make callers responsible for showing warnings
  log: clean unneeded objects during `log --remerge-diff`
  show, log: provide a --remerge-diff capability
This commit is contained in:
Junio C Hamano 2022-02-16 15:14:29 -08:00
commit 90b7153806
23 changed files with 727 additions and 48 deletions

View File

@ -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:::

View File

@ -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);

View File

@ -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);
}

View File

@ -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 ||

View File

@ -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")) {

124
diff.c
View File

@ -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 |

3
diff.h
View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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) {

View File

@ -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;

View File

@ -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 @@ redo:
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);

View File

@ -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

View File

@ -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"),

View File

@ -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;

View File

@ -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");

View File

@ -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);

View File

@ -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);

291
t/t4069-remerge-diff.sh Executable file
View File

@ -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

View File

@ -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' '

View File

@ -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

View File

@ -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

View File

@ -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.