Merge branch 'en/merge-tree'

"git merge-tree" learned a new mode where it takes two commits and
computes a tree that would result in the merge commit, if the
histories leading to these two commits were to be merged.

* en/merge-tree:
  git-merge-tree.txt: add a section on potentional usage mistakes
  merge-tree: add a --allow-unrelated-histories flag
  merge-tree: allow `ls-files -u` style info to be NUL terminated
  merge-ort: optionally produce machine-readable output
  merge-ort: store more specific conflict information
  merge-ort: make `path_messages` a strmap to a string_list
  merge-ort: store messages in a list, not in a single strbuf
  merge-tree: provide easy access to `ls-files -u` style info
  merge-tree: provide a list of which files have conflicts
  merge-ort: remove command-line-centric submodule message from merge-ort
  merge-ort: provide a merge_get_conflicted_files() helper function
  merge-tree: support including merge messages in output
  merge-ort: split out a separate display_update_messages() function
  merge-tree: implement real merges
  merge-tree: add option parsing and initial shell for real merge function
  merge-tree: move logic for existing merge into new function
  merge-tree: rename merge_trees() to trivial_merge_trees()
This commit is contained in:
Junio C Hamano 2022-07-14 15:03:58 -07:00
commit be733e1200
8 changed files with 1031 additions and 166 deletions

View File

@ -3,26 +3,237 @@ git-merge-tree(1)
NAME
----
git-merge-tree - Show three-way merge without touching index
git-merge-tree - Perform merge without touching index or working tree
SYNOPSIS
--------
[verse]
'git merge-tree' <base-tree> <branch1> <branch2>
'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
[[NEWMERGE]]
DESCRIPTION
-----------
Reads three tree-ish, and output trivial merge results and
conflicting stages to the standard output. This is similar to
what three-way 'git read-tree -m' does, but instead of storing the
results in the index, the command outputs the entries to the
standard output.
This is meant to be used by higher level scripts to compute
merge results outside of the index, and stuff the results back into the
index. For this reason, the output from the command omits
entries that match the <branch1> tree.
This command has a modern `--write-tree` mode and a deprecated
`--trivial-merge` mode. With the exception of the
<<DEPMERGE,DEPRECATED DESCRIPTION>> section at the end, the rest of
this documentation describes modern `--write-tree` mode.
Performs a merge, but does not make any new commits and does not read
from or write to either the working tree or index.
The performed merge will use the same feature as the "real"
linkgit:git-merge[1], including:
* three way content merges of individual files
* rename detection
* proper directory/file conflict handling
* recursive ancestor consolidation (i.e. when there is more than one
merge base, creating a virtual merge base by merging the merge bases)
* etc.
After the merge completes, a new toplevel tree object is created. See
`OUTPUT` below for details.
OPTIONS
-------
-z::
Do not quote filenames in the <Conflicted file info> section,
and end each filename with a NUL character rather than
newline. Also begin the messages section with a NUL character
instead of a newline. See <<OUTPUT>> below for more information.
--name-only::
In the Conflicted file info section, instead of writing a list
of (mode, oid, stage, path) tuples to output for conflicted
files, just provide a list of filenames with conflicts (and
do not list filenames multiple times if they have multiple
conflicting stages).
--[no-]messages::
Write any informational messages such as "Auto-merging <path>"
or CONFLICT notices to the end of stdout. If unspecified, the
default is to include these messages if there are merge
conflicts, and to omit them otherwise.
--allow-unrelated-histories::
merge-tree will by default error out if the two branches specified
share no common history. This flag can be given to override that
check and make the merge proceed anyway.
[[OUTPUT]]
OUTPUT
------
For a successful merge, the output from git-merge-tree is simply one
line:
<OID of toplevel tree>
Whereas for a conflicted merge, the output is by default of the form:
<OID of toplevel tree>
<Conflicted file info>
<Informational messages>
These are discussed individually below.
[[OIDTLT]]
OID of toplevel tree
~~~~~~~~~~~~~~~~~~~~
This is a tree object that represents what would be checked out in the
working tree at the end of `git merge`. If there were conflicts, then
files within this tree may have embedded conflict markers. This section
is always followed by a newline (or NUL if `-z` is passed).
[[CFI]]
Conflicted file info
~~~~~~~~~~~~~~~~~~~~
This is a sequence of lines with the format
<mode> <object> <stage> <filename>
The filename will be quoted as explained for the configuration
variable `core.quotePath` (see linkgit:git-config[1]). However, if
the `--name-only` option is passed, the mode, object, and stage will
be omitted. If `-z` is passed, the "lines" are terminated by a NUL
character instead of a newline character.
[[IM]]
Informational messages
~~~~~~~~~~~~~~~~~~~~~~
This always starts with a blank line (or NUL if `-z` is passed) to
separate it from the previous sections, and then has free-form
messages about the merge, such as:
* "Auto-merging <file>"
* "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
* "Failed to merge submodule <submodule> (<reason>)"
* "Warning: cannot merge binary files: <filename>"
Note that these free-form messages will never have a NUL character
in or between them, even if -z is passed. It is simply a large block
of text taking up the remainder of the output.
EXIT STATUS
-----------
For a successful, non-conflicted merge, the exit status is 0. When the
merge has conflicts, the exit status is 1. If the merge is not able to
complete (or start) due to some kind of error, the exit status is
something other than 0 or 1 (and the output is unspecified).
USAGE NOTES
-----------
This command is intended as low-level plumbing, similar to
linkgit:git-hash-object[1], linkgit:git-mktree[1],
linkgit:git-commit-tree[1], linkgit:git-write-tree[1],
linkgit:git-update-ref[1], and linkgit:git-mktag[1]. Thus, it can be
used as a part of a series of steps such as:
NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
test $? -eq 0 || die "There were conflicts..."
NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
git update-ref $BRANCH1 $NEWCOMMIT
Note that when the exit status is non-zero, `NEWTREE` in this sequence
will contain a lot more output than just a tree.
For conflicts, the output includes the same information that you'd get
with linkgit:git-merge[1]:
* what would be written to the working tree (the
<<OIDTLT,OID of toplevel tree>>)
* the higher order stages that would be written to the index (the
<<CFI,Conflicted file info>>)
* any messages that would have been printed to stdout (the
<<IM,Informational messages>>)
MISTAKES TO AVOID
-----------------
Do NOT look through the resulting toplevel tree to try to find which
files conflict; parse the <<CFI,Conflicted file info>> section instead.
Not only would parsing an entire tree be horrendously slow in large
repositories, there are numerous types of conflicts not representable by
conflict markers (modify/delete, mode conflict, binary file changed on
both sides, file/directory conflicts, various rename conflict
permutations, etc.)
Do NOT interpret an empty <<CFI,Conflicted file info>> list as a clean
merge; check the exit status. A merge can have conflicts without having
individual files conflict (there are a few types of directory rename
conflicts that fall into this category, and others might also be added
in the future).
Do NOT attempt to guess or make the user guess the conflict types from
the <<CFI,Conflicted file info>> list. The information there is
insufficient to do so. For example: Rename/rename(1to2) conflicts (both
sides renamed the same file differently) will result in three different
file having higher order stages (but each only has one higher order
stage), with no way (short of the <<IM,Informational messages>> section)
to determine which three files are related. File/directory conflicts
also result in a file with exactly one higher order stage.
Possibly-involved-in-directory-rename conflicts (when
"merge.directoryRenames" is unset or set to "conflicts") also result in
a file with exactly one higher order stage. In all cases, the
<<IM,Informational messages>> section has the necessary info, though it
is not designed to be machine parseable.
Do NOT assume that each paths from <<CFI,Conflicted file info>>, and
the logical conflicts in the <<IM,Informational messages>> have a
one-to-one mapping, nor that there is a one-to-many mapping, nor a
many-to-one mapping. Many-to-many mappings exist, meaning that each
path can have many logical conflict types in a single merge, and each
logical conflict type can affect many paths.
Do NOT assume all filenames listed in the <<IM,Informational messages>>
section had conflicts. Messages can be included for files that have no
conflicts, such as "Auto-merging <file>".
AVOID taking the OIDS from the <<CFI,Conflicted file info>> and
re-merging them to present the conflicts to the user. This will lose
information. Instead, look up the version of the file found within the
<<OIDTLT,OID of toplevel tree>> and show that instead. In particular,
the latter will have conflict markers annotated with the original
branch/commit being merged and, if renames were involved, the original
filename. While you could include the original branch/commit in the
conflict marker annotations when re-merging, the original filename is
not available from the <<CFI,Conflicted file info>> and thus you would
be losing information that might help the user resolve the conflict.
[[DEPMERGE]]
DEPRECATED DESCRIPTION
----------------------
Per the <<NEWMERGE,DESCRIPTION>> and unlike the rest of this
documentation, this section describes the deprecated `--trivial-merge`
mode.
Other than the optional `--trivial-merge`, this mode accepts no
options.
This mode reads three tree-ish, and outputs trivial merge results and
conflicting stages to the standard output in a semi-diff format.
Since this was designed for higher level scripts to consume and merge
the results back into the index, it omits entries that match
<branch1>. The result of this second form is similar to what
three-way 'git read-tree -m' does, but instead of storing the results
in the index, the command outputs the entries to the standard output.
This form not only has limited applicability (a trivial merge cannot
handle content merges of individual files, rename detection, proper
directory/file conflict handling, etc.), the output format is also
difficult to work with, and it will generally be less performant than
the first form even on successful merges (especially if working in
large repositories).
GIT
---

View File

@ -2,13 +2,18 @@
#include "builtin.h"
#include "tree-walk.h"
#include "xdiff-interface.h"
#include "help.h"
#include "commit-reach.h"
#include "merge-ort.h"
#include "object-store.h"
#include "parse-options.h"
#include "repository.h"
#include "blob.h"
#include "exec-cmd.h"
#include "merge-blobs.h"
#include "quote.h"
static const char merge_tree_usage[] = "git merge-tree <base-tree> <branch1> <branch2>";
static int line_termination = '\n';
struct merge_list {
struct merge_list *next;
@ -28,7 +33,7 @@ static void add_merge_entry(struct merge_list *entry)
merge_result_end = &entry->next;
}
static void merge_trees(struct tree_desc t[3], const char *base);
static void trivial_merge_trees(struct tree_desc t[3], const char *base);
static const char *explanation(struct merge_list *entry)
{
@ -225,7 +230,7 @@ static void unresolved_directory(const struct traverse_info *info,
buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2));
#undef ENTRY_OID
merge_trees(t, newbase);
trivial_merge_trees(t, newbase);
free(buf0);
free(buf1);
@ -342,7 +347,7 @@ static int threeway_callback(int n, unsigned long mask, unsigned long dirmask, s
return mask;
}
static void merge_trees(struct tree_desc t[3], const char *base)
static void trivial_merge_trees(struct tree_desc t[3], const char *base)
{
struct traverse_info info;
@ -366,19 +371,18 @@ static void *get_tree_descriptor(struct repository *r,
return buf;
}
int cmd_merge_tree(int argc, const char **argv, const char *prefix)
static int trivial_merge(const char *base,
const char *branch1,
const char *branch2)
{
struct repository *r = the_repository;
struct tree_desc t[3];
void *buf1, *buf2, *buf3;
if (argc != 4)
usage(merge_tree_usage);
buf1 = get_tree_descriptor(r, t+0, argv[1]);
buf2 = get_tree_descriptor(r, t+1, argv[2]);
buf3 = get_tree_descriptor(r, t+2, argv[3]);
merge_trees(t, "");
buf1 = get_tree_descriptor(r, t+0, base);
buf2 = get_tree_descriptor(r, t+1, branch1);
buf3 = get_tree_descriptor(r, t+2, branch2);
trivial_merge_trees(t, "");
free(buf1);
free(buf2);
free(buf3);
@ -386,3 +390,162 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
show_result();
return 0;
}
enum mode {
MODE_UNKNOWN,
MODE_TRIVIAL,
MODE_REAL,
};
struct merge_tree_options {
int mode;
int allow_unrelated_histories;
int show_messages;
int name_only;
};
static int real_merge(struct merge_tree_options *o,
const char *branch1, const char *branch2,
const char *prefix)
{
struct commit *parent1, *parent2;
struct commit_list *merge_bases = NULL;
struct merge_options opt;
struct merge_result result = { 0 };
parent1 = get_merge_parent(branch1);
if (!parent1)
help_unknown_ref(branch1, "merge-tree",
_("not something we can merge"));
parent2 = get_merge_parent(branch2);
if (!parent2)
help_unknown_ref(branch2, "merge-tree",
_("not something we can merge"));
init_merge_options(&opt, the_repository);
opt.show_rename_progress = 0;
opt.branch1 = branch1;
opt.branch2 = branch2;
/*
* Get the merge bases, in reverse order; see comment above
* merge_incore_recursive in merge-ort.h
*/
merge_bases = get_merge_bases(parent1, parent2);
if (!merge_bases && !o->allow_unrelated_histories)
die(_("refusing to merge unrelated histories"));
merge_bases = reverse_commit_list(merge_bases);
merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
if (result.clean < 0)
die(_("failure to merge"));
if (o->show_messages == -1)
o->show_messages = !result.clean;
printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination);
if (!result.clean) {
struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
const char *last = NULL;
int i;
merge_get_conflicted_files(&result, &conflicted_files);
for (i = 0; i < conflicted_files.nr; i++) {
const char *name = conflicted_files.items[i].string;
struct stage_info *c = conflicted_files.items[i].util;
if (!o->name_only)
printf("%06o %s %d\t",
c->mode, oid_to_hex(&c->oid), c->stage);
else if (last && !strcmp(last, name))
continue;
write_name_quoted_relative(
name, prefix, stdout, line_termination);
last = name;
}
string_list_clear(&conflicted_files, 1);
}
if (o->show_messages) {
putchar(line_termination);
merge_display_update_messages(&opt, line_termination == '\0',
&result);
}
merge_finalize(&opt, &result);
return !result.clean; /* result.clean < 0 handled above */
}
int cmd_merge_tree(int argc, const char **argv, const char *prefix)
{
struct merge_tree_options o = { .show_messages = -1 };
int expected_remaining_argc;
int original_argc;
const char * const merge_tree_usage[] = {
N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
NULL
};
struct option mt_options[] = {
OPT_CMDMODE(0, "write-tree", &o.mode,
N_("do a real merge instead of a trivial merge"),
MODE_REAL),
OPT_CMDMODE(0, "trivial-merge", &o.mode,
N_("do a trivial merge only"), MODE_TRIVIAL),
OPT_BOOL(0, "messages", &o.show_messages,
N_("also show informational/conflict messages")),
OPT_SET_INT('z', NULL, &line_termination,
N_("separate paths with the NUL character"), '\0'),
OPT_BOOL_F(0, "name-only",
&o.name_only,
N_("list filenames without modes/oids/stages"),
PARSE_OPT_NONEG),
OPT_BOOL_F(0, "allow-unrelated-histories",
&o.allow_unrelated_histories,
N_("allow merging unrelated histories"),
PARSE_OPT_NONEG),
OPT_END()
};
/* Parse arguments */
original_argc = argc - 1; /* ignoring argv[0] */
argc = parse_options(argc, argv, prefix, mt_options,
merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
switch (o.mode) {
default:
BUG("unexpected command mode %d", o.mode);
case MODE_UNKNOWN:
switch (argc) {
default:
usage_with_options(merge_tree_usage, mt_options);
case 2:
o.mode = MODE_REAL;
break;
case 3:
o.mode = MODE_TRIVIAL;
break;
}
expected_remaining_argc = argc;
break;
case MODE_REAL:
expected_remaining_argc = 2;
break;
case MODE_TRIVIAL:
expected_remaining_argc = 3;
/* Removal of `--trivial-merge` is expected */
original_argc--;
break;
}
if (o.mode == MODE_TRIVIAL && argc < original_argc)
die(_("--trivial-merge is incompatible with all other options"));
if (argc != expected_remaining_argc)
usage_with_options(merge_tree_usage, mt_options);
/* Do the relevant type of merge */
if (o.mode == MODE_REAL)
return real_merge(&o, argv[0], argv[1], prefix);
else
return trivial_merge(argv[0], argv[1], argv[2]);
}

25
diff.c
View File

@ -3362,7 +3362,7 @@ struct userdiff_driver *get_textconv(struct repository *r,
return userdiff_get_textconv(r, one->driver);
}
static struct strbuf *additional_headers(struct diff_options *o,
static struct string_list *additional_headers(struct diff_options *o,
const char *path)
{
if (!o->additional_path_headers)
@ -3370,15 +3370,15 @@ static struct strbuf *additional_headers(struct diff_options *o,
return strmap_get(o->additional_path_headers, path);
}
static void add_formatted_headers(struct strbuf *msg,
struct strbuf *more_headers,
static void add_formatted_header(struct strbuf *msg,
const char *header,
const char *line_prefix,
const char *meta,
const char *reset)
{
char *next, *newline;
const char *next, *newline;
for (next = more_headers->buf; *next; next = newline) {
for (next = header; *next; next = newline) {
newline = strchrnul(next, '\n');
strbuf_addf(msg, "%s%s%.*s%s\n", line_prefix, meta,
(int)(newline - next), next, reset);
@ -3387,6 +3387,19 @@ static void add_formatted_headers(struct strbuf *msg,
}
}
static void add_formatted_headers(struct strbuf *msg,
struct string_list *more_headers,
const char *line_prefix,
const char *meta,
const char *reset)
{
int i;
for (i = 0; i < more_headers->nr; i++)
add_formatted_header(msg, more_headers->items[i].string,
line_prefix, meta, reset);
}
static void builtin_diff(const char *name_a,
const char *name_b,
struct diff_filespec *one,
@ -4314,7 +4327,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;
struct string_list *more_headers = NULL;
*must_show_header = 1;
strbuf_init(msg, PATH_MAX * 2 + 300);

2
git.c
View File

@ -565,7 +565,7 @@ static struct cmd_struct commands[] = {
{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
{ "merge-tree", cmd_merge_tree, RUN_SETUP },
{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
{ "mktree", cmd_mktree, RUN_SETUP },
{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },

View File

@ -349,13 +349,15 @@ struct merge_options_internal {
struct mem_pool pool;
/*
* output: special messages and conflict notices for various paths
* conflicts: logical conflicts and messages stored by _primary_ path
*
* This is a map of pathnames (a subset of the keys in "paths" above)
* to strbufs. It gathers various warning/conflict/notice messages
* for later processing.
* to struct string_list, with each item's `util` containing a
* `struct logical_conflict_info`. Note, though, that for each path,
* it only stores the logical conflicts for which that path is the
* primary path; the path might be part of additional conflicts.
*/
struct strmap output;
struct strmap conflicts;
/*
* renames: various data relating to rename detection
@ -481,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 ***/
/*
@ -567,20 +663,25 @@ static void clear_or_reinit_internal_opts(struct merge_options_internal *opti,
struct strmap_entry *e;
/* Release and free each strbuf found in output */
strmap_for_each_entry(&opti->output, &iter, e) {
struct strbuf *sb = e->value;
strbuf_release(sb);
/*
* While strictly speaking we don't need to free(sb)
* here because we could pass free_values=1 when
* calling strmap_clear() on opti->output, that would
* require strmap_clear to do another
* strmap_for_each_entry() loop, so we just free it
* while we're iterating anyway.
*/
free(sb);
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);
}
strmap_clear(&opti->output, 0);
/*
* While strictly speaking we don't need to
* free(conflicts) here because we could pass
* free_values=1 when calling strmap_clear() on
* opti->conflicts, that would require strmap_clear
* to do another strmap_for_each_entry() loop, so we
* just free it while we're iterating anyway.
*/
string_list_clear(list, 1);
free(list);
}
strmap_clear(&opti->conflicts, 0);
}
mem_pool_discard(&opti->pool, 0);
@ -627,29 +728,58 @@ 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 strbuf *sb, *dest;
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 */
sb = strmap_get(&opt->priv->output, path);
if (!sb) {
sb = xmalloc(sizeof(*sb));
strbuf_init(sb, 0);
strmap_put(&opt->priv->output, path, sb);
/* Ensure path_conflicts (ptr to array of logical_conflict) allocated */
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, primary_path, path_conflicts);
}
dest = (opt->record_conflict_msgs_as_headers ? &tmp : sb);
/* 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);
va_start(ap, fmt);
if (opt->priv->call_depth) {
@ -660,32 +790,32 @@ static void path_msg(struct merge_options *opt,
strbuf_vaddf(dest, fmt, ap);
va_end(ap);
/* Handle specialized formatting of message under --remerge-diff */
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);
strbuf_addf(&buf, "%s ", opt->msg_header_prefix);
/* Copy tmp to sb, adding spaces after newlines */
strbuf_grow(sb, sb->len + 2*tmp.len); /* more than sufficient */
strbuf_grow(&buf, buf.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];
buf.buf[buf.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] = ' ';
buf.buf[++i_sb] = ' ';
}
/* Update length and ensure it's NUL-terminated */
sb->len += i_sb;
sb->buf[sb->len] = '\0';
buf.len += i_sb;
buf.buf[buf.len] = '\0';
strbuf_release(&tmp);
}
/* Add final newline character to sb */
strbuf_addch(sb, '\n');
string_list_append_nodup(path_conflicts, strbuf_detach(&buf, NULL))
->util = info;
}
static struct diff_filespec *pool_alloc_filespec(struct mem_pool *pool,
@ -1627,7 +1757,8 @@ static int merge_submodule(struct merge_options *opt,
return 0;
if (repo_submodule_init(&subrepo, opt->repo, path, null_oid())) {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_NOT_INITIALIZED, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s (not checked out)"),
path);
return 0;
@ -1636,7 +1767,8 @@ static int merge_submodule(struct merge_options *opt,
if (!(commit_o = lookup_commit_reference(&subrepo, o)) ||
!(commit_a = lookup_commit_reference(&subrepo, a)) ||
!(commit_b = lookup_commit_reference(&subrepo, b))) {
path_msg(opt, path, 0,
path_msg(opt, CONFLICT_SUBMODULE_HISTORY_NOT_AVAILABLE, 0,
path, NULL, NULL, NULL,
_("Failed to merge submodule %s (commits not present)"),
path);
goto cleanup;
@ -1645,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);
@ -1655,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;
@ -1663,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;
@ -1686,30 +1821,27 @@ 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:\n%s\n"),
"resolution exists: %s"),
path, sb.buf);
path_msg(opt, path, 1,
_("If this is correct simply add it to the index "
"for example\n"
"by using:\n\n"
" git update-index --cacheinfo 160000 %s \"%s\"\n\n"
"which will accept this suggestion.\n"),
oid_to_hex(&merges.objects[0].item->oid), path);
strbuf_release(&sb);
break;
default:
for (i = 0; i < merges.nr; i++)
format_commit(&sb, 4, &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);
@ -1835,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);
@ -1947,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],
@ -2085,20 +2219,23 @@ static char *handle_path_level_conflicts(struct merge_options *opt,
c_info->reported_already = 1;
strbuf_add_separated_string_list(&collision_paths, ", ",
&c_info->source_files);
path_msg(opt, new_path, 0,
_("CONFLICT (implicit dir rename): Existing file/dir "
"at %s in the way of implicit directory rename(s) "
"putting the following path(s) there: %s."),
path_msg(opt, CONFLICT_DIR_RENAME_FILE_IN_WAY, 0,
new_path, NULL, NULL, &c_info->source_files,
_("CONFLICT (implicit dir rename): Existing "
"file/dir at %s in the way of implicit "
"directory rename(s) putting the following "
"path(s) there: %s."),
new_path, collision_paths.buf);
clean = 0;
} else if (c_info->source_files.nr > 1) {
c_info->reported_already = 1;
strbuf_add_separated_string_list(&collision_paths, ", ",
&c_info->source_files);
path_msg(opt, new_path, 0,
_("CONFLICT (implicit dir rename): Cannot map more "
"than one path to %s; implicit directory renames "
"tried to put these paths there: %s"),
path_msg(opt, CONFLICT_DIR_RENAME_COLLISION, 0,
new_path, NULL, NULL, &c_info->source_files,
_("CONFLICT (implicit dir rename): Cannot map "
"more than one path to %s; implicit directory "
"renames tried to put these paths there: %s"),
new_path, collision_paths.buf);
clean = 0;
}
@ -2153,12 +2290,13 @@ static void get_provisional_directory_renames(struct merge_options *opt,
continue;
if (bad_max == max) {
path_msg(opt, source_dir, 0,
path_msg(opt, CONFLICT_DIR_RENAME_SPLIT, 0,
source_dir, NULL, NULL, NULL,
_("CONFLICT (directory rename split): "
"Unclear where to rename %s to; it was "
"renamed to multiple other directories, with "
"no destination getting a majority of the "
"files."),
"renamed to multiple other directories, "
"with no destination getting a majority of "
"the files."),
source_dir);
*clean = 0;
} else {
@ -2307,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);
@ -2447,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."),
@ -2467,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 "
@ -2475,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 "
@ -2631,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],
@ -2726,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 "
@ -2745,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);
@ -2769,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,
@ -3690,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);
@ -3766,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 "
@ -3811,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);
@ -3869,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);
}
@ -3913,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."),
@ -4209,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);
@ -4257,6 +4412,93 @@ static int record_conflicted_index_entries(struct merge_options *opt)
return errs;
}
void merge_display_update_messages(struct merge_options *opt,
int detailed,
struct merge_result *result)
{
struct merge_options_internal *opti = result->priv;
struct hashmap_iter iter;
struct strmap_entry *e;
struct string_list olist = STRING_LIST_INIT_NODUP;
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 */
ALLOC_GROW(olist.items, strmap_get_size(&opti->conflicts),
olist.alloc);
/* Put every entry from output into olist, then sort */
strmap_for_each_entry(&opti->conflicts, &iter, e) {
string_list_append(&olist, e->key)->util = e->value;
}
string_list_sort(&olist);
/* Iterate over the items, printing them */
for (int path_nr = 0; path_nr < olist.nr; ++path_nr) {
struct string_list *conflicts = olist.items[path_nr].util;
for (int i = 0; i < conflicts->nr; i++) {
struct logical_conflict_info *info =
conflicts->items[i].util;
if (detailed) {
printf("%lu", (unsigned long)info->paths.nr);
putchar('\0');
for (int n = 0; n < info->paths.nr; n++) {
fputs(info->paths.v[n], stdout);
putchar('\0');
}
fputs(type_short_descriptions[info->type],
stdout);
putchar('\0');
}
puts(conflicts->items[i].string);
if (detailed)
putchar('\0');
}
}
string_list_clear(&olist, 0);
/* Also include needed rename limit adjustment now */
diff_warn_rename_limit("merge.renamelimit",
opti->renames.needed_limit, 0);
trace2_region_leave("merge", "display messages", opt->repo);
}
void merge_get_conflicted_files(struct merge_result *result,
struct string_list *conflicted_files)
{
struct hashmap_iter iter;
struct strmap_entry *e;
struct merge_options_internal *opti = result->priv;
strmap_for_each_entry(&opti->conflicted, &iter, e) {
const char *path = e->key;
struct conflict_info *ci = e->value;
int i;
VERIFY_CI(ci);
for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
struct stage_info *si;
if (!(ci->filemask & (1ul << i)))
continue;
si = xmalloc(sizeof(*si));
si->stage = i+1;
si->mode = ci->stages[i].mode;
oidcpy(&si->oid, &ci->stages[i].oid);
string_list_append(conflicted_files, path)->util = si;
}
}
/* string_list_sort() uses a stable sort, so we're good */
string_list_sort(conflicted_files);
}
void merge_switch_to_result(struct merge_options *opt,
struct tree *head,
struct merge_result *result,
@ -4294,43 +4536,8 @@ void merge_switch_to_result(struct merge_options *opt,
fclose(fp);
trace2_region_leave("merge", "write_auto_merge", opt->repo);
}
if (display_update_msgs) {
struct merge_options_internal *opti = result->priv;
struct hashmap_iter iter;
struct strmap_entry *e;
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 */
ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
olist.alloc);
/* Put every entry from output into olist, then sort */
strmap_for_each_entry(&opti->output, &iter, e) {
string_list_append(&olist, e->key)->util = e->value;
}
string_list_sort(&olist);
/* Iterate over the items, printing them */
for (i = 0; i < olist.nr; ++i) {
struct strbuf *sb = olist.items[i].util;
printf("%s", sb->buf);
}
string_list_clear(&olist, 0);
/* Also include needed rename limit adjustment now */
diff_warn_rename_limit("merge.renamelimit",
opti->renames.needed_limit, 0);
trace2_region_leave("merge", "display messages", opt->repo);
}
if (display_update_msgs)
merge_display_update_messages(opt, /* detailed */ 0, result);
merge_finalize(opt, result);
}
@ -4504,11 +4711,11 @@ static void merge_start(struct merge_options *opt, struct merge_result *result)
strmap_init_with_options(&opt->priv->conflicted, pool, 0);
/*
* keys & strbufs in output will sometimes need to outlive "paths",
* so it will have a copy of relevant keys. It's probably a small
* subset of the overall paths that have special output.
* keys & string_lists in conflicts will sometimes need to outlive
* "paths", so it will have a copy of relevant keys. It's probably
* a small subset of the overall paths that have special output.
*/
strmap_init(&opt->priv->output);
strmap_init(&opt->priv->conflicts);
trace2_region_leave("merge", "allocate/init", opt->repo);
}
@ -4609,7 +4816,8 @@ redo:
trace2_region_leave("merge", "process_entries", opt->repo);
/* Set return values */
result->path_messages = &opt->priv->output;
result->path_messages = &opt->priv->conflicts;
result->tree = parse_tree_indirect(&working_tree_oid);
/* existence of conflicted entries implies unclean */
result->clean &= strmap_empty(&opt->priv->conflicted);

View File

@ -2,6 +2,7 @@
#define MERGE_ORT_H
#include "merge-recursive.h"
#include "hash.h"
struct commit;
struct tree;
@ -27,7 +28,7 @@ struct merge_result {
/*
* Special messages and conflict notices for various paths
*
* This is a map of pathnames to strbufs. It contains various
* This is a map of pathnames to a string_list. It contains various
* warning/conflict/notice messages (possibly multiple per path)
* that callers may want to use.
*/
@ -80,6 +81,35 @@ void merge_switch_to_result(struct merge_options *opt,
int update_worktree_and_index,
int display_update_msgs);
/*
* Display messages about conflicts and which files were 3-way merged.
* Automatically called by merge_switch_to_result() with stream == stdout,
* so only call this when bypassing merge_switch_to_result().
*/
void merge_display_update_messages(struct merge_options *opt,
int detailed,
struct merge_result *result);
struct stage_info {
struct object_id oid;
int mode;
int stage;
};
/*
* Provide a list of path -> {struct stage_info*} mappings for
* all conflicted files. Note that each path could appear up to three
* times in the list, corresponding to 3 different stage entries. In short,
* this basically provides the info that would be printed by `ls-files -u`.
*
* result should have been populated by a call to
* one of the merge_incore_[non]recursive() functions.
*
* conflicted_files should be empty before calling this function.
*/
void merge_get_conflicted_files(struct merge_result *result,
struct string_list *conflicted_files);
/* Do needed cleanup when not calling merge_switch_to_result() */
void merge_finalize(struct merge_options *opt,
struct merge_result *result);

240
t/t4301-merge-tree-write-tree.sh Executable file
View File

@ -0,0 +1,240 @@
#!/bin/sh
test_description='git merge-tree --write-tree'
. ./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 '
test_write_lines 1 2 3 4 5 >numbers &&
echo hello >greeting &&
echo foo >whatever &&
git add numbers greeting whatever &&
test_tick &&
git commit -m initial &&
git branch side1 &&
git branch side2 &&
git branch side3 &&
git checkout side1 &&
test_write_lines 1 2 3 4 5 6 >numbers &&
echo hi >greeting &&
echo bar >whatever &&
git add numbers greeting whatever &&
test_tick &&
git commit -m modify-stuff &&
git checkout side2 &&
test_write_lines 0 1 2 3 4 5 >numbers &&
echo yo >greeting &&
git rm whatever &&
mkdir whatever &&
>whatever/empty &&
git add numbers greeting whatever/empty &&
test_tick &&
git commit -m other-modifications &&
git checkout side3 &&
git mv numbers sequence &&
test_tick &&
git commit -m rename-numbers &&
git switch --orphan unrelated &&
>something-else &&
git add something-else &&
test_tick &&
git commit -m first-commit
'
test_expect_success 'Clean merge' '
TREE_OID=$(git merge-tree --write-tree side1 side3) &&
q_to_tab <<-EOF >expect &&
100644 blob $(git rev-parse side1:greeting)Qgreeting
100644 blob $(git rev-parse side1:numbers)Qsequence
100644 blob $(git rev-parse side1:whatever)Qwhatever
EOF
git ls-tree $TREE_OID >actual &&
test_cmp expect actual
'
test_expect_success 'Content merge and a few conflicts' '
git checkout side1^0 &&
test_must_fail git merge side2 &&
expected_tree=$(git rev-parse AUTO_MERGE) &&
# We will redo the merge, while we are still in a conflicted state!
git ls-files -u >conflicted-file-info &&
test_when_finished "git reset --hard" &&
test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
actual_tree=$(head -n 1 RESULT) &&
# Due to differences of e.g. "HEAD" vs "side1", the results will not
# exactly match. Dig into individual files.
# Numbers should have three-way merged cleanly
test_write_lines 0 1 2 3 4 5 6 >expect &&
git show ${actual_tree}:numbers >actual &&
test_cmp expect actual &&
# whatever and whatever~<branch> should have same HASHES
git rev-parse ${expected_tree}:whatever ${expected_tree}:whatever~HEAD >expect &&
git rev-parse ${actual_tree}:whatever ${actual_tree}:whatever~side1 >actual &&
test_cmp expect actual &&
# greeting should have a merge conflict
git show ${expected_tree}:greeting >tmp &&
sed -e s/HEAD/side1/ tmp >expect &&
git show ${actual_tree}:greeting >actual &&
test_cmp expect actual
'
test_expect_success 'Barf on misspelled option, with exit code other than 0 or 1' '
# Mis-spell with single "s" instead of double "s"
test_expect_code 129 git merge-tree --write-tree --mesages FOOBAR side1 side2 2>expect &&
grep "error: unknown option.*mesages" expect
'
test_expect_success 'Barf on too many arguments' '
test_expect_code 129 git merge-tree --write-tree side1 side2 invalid 2>expect &&
grep "^usage: git merge-tree" expect
'
anonymize_hash() {
sed -e "s/[0-9a-f]\{40,\}/HASH/g" "$@"
}
test_expect_success 'test conflict notices and such' '
test_expect_code 1 git merge-tree --write-tree --name-only side1 side2 >out &&
anonymize_hash out >actual &&
# Expected results:
# "greeting" should merge with conflicts
# "numbers" should merge cleanly
# "whatever" has *both* a modify/delete and a file/directory conflict
cat <<-EOF >expect &&
HASH
greeting
whatever~side1
Auto-merging greeting
CONFLICT (content): Merge conflict in greeting
Auto-merging numbers
CONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
CONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1. Version side1 of whatever~side1 left in tree.
EOF
test_cmp expect actual
'
for opt in $(git merge-tree --git-completion-helper-all)
do
if test $opt = "--trivial-merge" || test $opt = "--write-tree"
then
continue
fi
test_expect_success "usage: --trivial-merge is incompatible with $opt" '
test_expect_code 128 git merge-tree --trivial-merge $opt side1 side2 side3
'
done
test_expect_success 'Just the conflicted files without the messages' '
test_expect_code 1 git merge-tree --write-tree --no-messages --name-only side1 side2 >out &&
anonymize_hash out >actual &&
test_write_lines HASH greeting whatever~side1 >expect &&
test_cmp expect actual
'
test_expect_success 'Check conflicted oids and modes without messages' '
test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
anonymize_hash out >actual &&
# Compare the basic output format
q_to_tab >expect <<-\EOF &&
HASH
100644 HASH 1Qgreeting
100644 HASH 2Qgreeting
100644 HASH 3Qgreeting
100644 HASH 1Qwhatever~side1
100644 HASH 2Qwhatever~side1
EOF
test_cmp expect actual &&
# Check the actual hashes against the `ls-files -u` output too
tail -n +2 out | sed -e s/side1/HEAD/ >actual &&
test_cmp conflicted-file-info actual
'
test_expect_success 'NUL terminated conflicted file "lines"' '
git checkout -b tweak1 side1 &&
test_write_lines zero 1 2 3 4 5 6 >numbers &&
git add numbers &&
git mv numbers "Αυτά μου φαίνονται κινέζικα" &&
git commit -m "Renamed numbers" &&
test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
anonymize_hash out >actual &&
printf "\\n" >>actual &&
# Expected results:
# "greeting" should merge with conflicts
# "whatever" has *both* a modify/delete and a file/directory conflict
# "Αυτά μου φαίνονται κινέζικα" should have a conflict
echo HASH | lf_to_nul >expect &&
q_to_tab <<-EOF | lf_to_nul >>expect &&
100644 HASH 1Qgreeting
100644 HASH 2Qgreeting
100644 HASH 3Qgreeting
100644 HASH 1Qwhatever~tweak1
100644 HASH 2Qwhatever~tweak1
100644 HASH 1QΑυτά μου φαίνονται κινέζικα
100644 HASH 2QΑυτά μου φαίνονται κινέζικα
100644 HASH 3QΑυτά μου φαίνονται κινέζικα
EOF
q_to_nul <<-EOF >>expect &&
1QgreetingQAuto-mergingQAuto-merging greeting
Q1QgreetingQCONFLICT (contents)QCONFLICT (content): Merge conflict in greeting
Q2Qwhatever~tweak1QwhateverQCONFLICT (file/directory)QCONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
Q1Qwhatever~tweak1QCONFLICT (modify/delete)QCONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1. Version tweak1 of whatever~tweak1 left in tree.
Q1QΑυτά μου φαίνονται κινέζικαQAuto-mergingQAuto-merging Αυτά μου φαίνονται κινέζικα
Q1QΑυτά μου φαίνονται κινέζικαQCONFLICT (contents)QCONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
Q
EOF
test_cmp expect actual
'
test_expect_success 'error out by default for unrelated histories' '
test_expect_code 128 git merge-tree --write-tree side1 unrelated 2>error &&
grep "refusing to merge unrelated histories" error
'
test_expect_success 'can override merge of unrelated histories' '
git merge-tree --write-tree --allow-unrelated-histories side1 unrelated >tree &&
TREE=$(cat tree) &&
git rev-parse side1:numbers side1:greeting side1:whatever unrelated:something-else >expect &&
git rev-parse $TREE:numbers $TREE:greeting $TREE:whatever $TREE:something-else >actual &&
test_cmp expect actual
'
test_done

View File

@ -133,7 +133,7 @@ test_expect_success 'merging should conflict for non fast-forward' '
(cd merge-search &&
git checkout -b test-nonforward b &&
(cd sub &&
git rev-parse sub-d > ../expect) &&
git rev-parse --short sub-d > ../expect) &&
if test "$GIT_TEST_MERGE_ALGORITHM" = ort
then
test_must_fail git merge c >actual