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:
commit
be733e1200
@ -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
|
||||
---
|
||||
|
@ -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
25
diff.c
@ -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
2
git.c
@ -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 },
|
||||
|
464
merge-ort.c
464
merge-ort.c
@ -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);
|
||||
|
32
merge-ort.h
32
merge-ort.h
@ -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
240
t/t4301-merge-tree-write-tree.sh
Executable 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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user