Merge branch 'jf/merge-ignore-ws'

* jf/merge-ignore-ws:
  merge-recursive: options to ignore whitespace changes
  merge-recursive --patience
  ll-merge: replace flag argument with options struct
  merge-recursive: expose merge options for builtin merge
This commit is contained in:
Junio C Hamano 2010-10-26 21:40:54 -07:00
commit 75b17fee72
12 changed files with 351 additions and 97 deletions

View File

@ -40,6 +40,28 @@ the other tree did, declaring 'our' history contains all that happened in it.
theirs;;
This is opposite of 'ours'.
patience;;
With this option, 'merge-recursive' spends a little extra time
to avoid mismerges that sometimes occur due to unimportant
matching lines (e.g., braces from distinct functions). Use
this when the branches to be merged have diverged wildly.
See also linkgit:git-diff[1] `--patience`.
ignore-space-change;;
ignore-all-space;;
ignore-space-at-eol;;
Treats lines with the indicated type of whitespace change as
unchanged for the sake of a three-way merge. Whitespace
changes mixed with other changes to a line are not ignored.
See also linkgit:git-diff[1] `-b`, `-w`, and
`--ignore-space-at-eol`.
+
* If 'their' version only introduces whitespace changes to a line,
'our' version is used;
* If 'our' version introduces whitespace changes but 'their'
version includes a substantial change, 'their' version is used;
* Otherwise, the merge proceeds in the usual way.
renormalize;;
This runs a virtual check-out and check-in of all three stages
of a file when resolving a three-way merge. This option is

View File

@ -17,6 +17,40 @@ responsible for a few things.
path-specific merge drivers (specified in `.gitattributes`)
into account.
Data structures
---------------
* `mmbuffer_t`, `mmfile_t`
These store data usable for use by the xdiff backend, for writing and
for reading, respectively. See `xdiff/xdiff.h` for the definitions
and `diff.c` for examples.
* `struct ll_merge_options`
This describes the set of options the calling program wants to affect
the operation of a low-level (single file) merge. Some options:
`virtual_ancestor`::
Behave as though this were part of a merge between common
ancestors in a recursive merge.
If a helper program is specified by the
`[merge "<driver>"] recursive` configuration, it will
be used (see linkgit:gitattributes[5]).
`variant`::
Resolve local conflicts automatically in favor
of one side or the other (as in 'git merge-file'
`--ours`/`--theirs`/`--union`). Can be `0`,
`XDL_MERGE_FAVOR_OURS`, `XDL_MERGE_FAVOR_THEIRS`, or
`XDL_MERGE_FAVOR_UNION`.
`renormalize`::
Resmudge and clean the "base", "theirs" and "ours" files
before merging. Use this when the merge is likely to have
overlapped with a change in smudge/clean or end-of-line
normalization rules.
Low-level (single file) merge
-----------------------------
@ -28,15 +62,24 @@ Low-level (single file) merge
`.git/info/attributes` into account. Returns 0 for a
clean merge.
The caller:
Calling sequence:
1. allocates an mmbuffer_t variable for the result;
2. allocates and fills variables with the file's original content
and two modified versions (using `read_mmfile`, for example);
3. calls ll_merge();
4. reads the output from result_buf.ptr and result_buf.size;
5. releases buffers when finished (free(ancestor.ptr); free(ours.ptr);
free(theirs.ptr); free(result_buf.ptr);).
* Prepare a `struct ll_merge_options` to record options.
If you have no special requests, skip this and pass `NULL`
as the `opts` parameter to use the default options.
* Allocate an mmbuffer_t variable for the result.
* Allocate and fill variables with the file's original content
and two modified versions (using `read_mmfile`, for example).
* Call `ll_merge()`.
* Read the merged content from `result_buf.ptr` and `result_buf.size`.
* Release buffers when finished. A simple
`free(ancestor.ptr); free(ours.ptr); free(theirs.ptr);
free(result_buf.ptr);` will do.
If the modifications do not merge cleanly, `ll_merge` will return a
nonzero value and `result_buf` will generally include a description of
@ -47,18 +90,6 @@ The `ancestor_label`, `our_label`, and `their_label` parameters are
used to label the different sides of a conflict if the merge driver
supports this.
The `flag` parameter is a bitfield:
- The `LL_OPT_VIRTUAL_ANCESTOR` bit indicates whether this is an
internal merge to consolidate ancestors for a recursive merge.
- The `LL_OPT_FAVOR_MASK` bits allow local conflicts to be automatically
resolved in favor of one side or the other (as in 'git merge-file'
`--ours`/`--theirs`/`--union`).
They can be populated by `create_ll_flag`, whose argument can be
`XDL_MERGE_FAVOR_OURS`, `XDL_MERGE_FAVOR_THEIRS`, or
`XDL_MERGE_FAVOR_UNION`.
Everything else
---------------

View File

@ -161,7 +161,7 @@ static int checkout_merged(int pos, struct checkout *state)
* merge.renormalize set, too
*/
status = ll_merge(&result_buf, path, &ancestor, "base",
&ours, "ours", &theirs, "theirs", 0);
&ours, "ours", &theirs, "theirs", NULL);
free(ancestor.ptr);
free(ours.ptr);
free(theirs.ptr);

View File

@ -2,6 +2,7 @@
#include "commit.h"
#include "tag.h"
#include "merge-recursive.h"
#include "xdiff-interface.h"
static const char builtin_merge_recursive_usage[] =
"git %s <base>... -- <head> <remote> ...";
@ -40,19 +41,7 @@ int cmd_merge_recursive(int argc, const char **argv, const char *prefix)
if (!prefixcmp(arg, "--")) {
if (!arg[2])
break;
if (!strcmp(arg+2, "ours"))
o.recursive_variant = MERGE_RECURSIVE_OURS;
else if (!strcmp(arg+2, "theirs"))
o.recursive_variant = MERGE_RECURSIVE_THEIRS;
else if (!strcmp(arg+2, "subtree"))
o.subtree_shift = "";
else if (!prefixcmp(arg+2, "subtree="))
o.subtree_shift = arg + 10;
else if (!strcmp(arg+2, "renormalize"))
o.renormalize = 1;
else if (!strcmp(arg+2, "no-renormalize"))
o.renormalize = 0;
else
if (parse_merge_opt(&o, arg + 2))
die("Unknown option %s", arg);
continue;
}

View File

@ -639,25 +639,9 @@ static int try_merge_strategy(const char *strategy, struct commit_list *common,
o.renormalize = option_renormalize;
/*
* NEEDSWORK: merge with table in builtin/merge-recursive
*/
for (x = 0; x < xopts_nr; x++) {
if (!strcmp(xopts[x], "ours"))
o.recursive_variant = MERGE_RECURSIVE_OURS;
else if (!strcmp(xopts[x], "theirs"))
o.recursive_variant = MERGE_RECURSIVE_THEIRS;
else if (!strcmp(xopts[x], "subtree"))
o.subtree_shift = "";
else if (!prefixcmp(xopts[x], "subtree="))
o.subtree_shift = xopts[x]+8;
else if (!strcmp(xopts[x], "renormalize"))
o.renormalize = 1;
else if (!strcmp(xopts[x], "no-renormalize"))
o.renormalize = 0;
else
for (x = 0; x < xopts_nr; x++)
if (parse_merge_opt(&o, xopts[x]))
die("Unknown option for merge-recursive: -X%s", xopts[x]);
}
o.branch1 = head_arg;
o.branch2 = remoteheads->item->util;

View File

@ -18,7 +18,7 @@ typedef int (*ll_merge_fn)(const struct ll_merge_driver *,
mmfile_t *orig, const char *orig_name,
mmfile_t *src1, const char *name1,
mmfile_t *src2, const char *name2,
int flag,
const struct ll_merge_options *opts,
int marker_size);
struct ll_merge_driver {
@ -39,14 +39,18 @@ static int ll_binary_merge(const struct ll_merge_driver *drv_unused,
mmfile_t *orig, const char *orig_name,
mmfile_t *src1, const char *name1,
mmfile_t *src2, const char *name2,
int flag, int marker_size)
const struct ll_merge_options *opts,
int marker_size)
{
mmfile_t *stolen;
assert(opts);
/*
* The tentative merge result is "ours" for the final round,
* or common ancestor for an internal merge. Still return
* "conflicted merge" status.
*/
mmfile_t *stolen = (flag & LL_OPT_VIRTUAL_ANCESTOR) ? orig : src1;
stolen = opts->virtual_ancestor ? orig : src1;
result->ptr = stolen->ptr;
result->size = stolen->size;
@ -60,9 +64,11 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,
mmfile_t *orig, const char *orig_name,
mmfile_t *src1, const char *name1,
mmfile_t *src2, const char *name2,
int flag, int marker_size)
const struct ll_merge_options *opts,
int marker_size)
{
xmparam_t xmp;
assert(opts);
if (buffer_is_binary(orig->ptr, orig->size) ||
buffer_is_binary(src1->ptr, src1->size) ||
@ -74,12 +80,13 @@ static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,
orig, orig_name,
src1, name1,
src2, name2,
flag, marker_size);
opts, marker_size);
}
memset(&xmp, 0, sizeof(xmp));
xmp.level = XDL_MERGE_ZEALOUS;
xmp.favor = ll_opt_favor(flag);
xmp.favor = opts->variant;
xmp.xpp.flags = opts->xdl_opts;
if (git_xmerge_style >= 0)
xmp.style = git_xmerge_style;
if (marker_size > 0)
@ -96,15 +103,17 @@ static int ll_union_merge(const struct ll_merge_driver *drv_unused,
mmfile_t *orig, const char *orig_name,
mmfile_t *src1, const char *name1,
mmfile_t *src2, const char *name2,
int flag, int marker_size)
const struct ll_merge_options *opts,
int marker_size)
{
/* Use union favor */
flag &= ~LL_OPT_FAVOR_MASK;
flag |= create_ll_flag(XDL_MERGE_FAVOR_UNION);
struct ll_merge_options o;
assert(opts);
o = *opts;
o.variant = XDL_MERGE_FAVOR_UNION;
return ll_xdl_merge(drv_unused, result, path_unused,
orig, NULL, src1, NULL, src2, NULL,
flag, marker_size);
return 0;
&o, marker_size);
}
#define LL_BINARY_MERGE 0
@ -136,7 +145,8 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,
mmfile_t *orig, const char *orig_name,
mmfile_t *src1, const char *name1,
mmfile_t *src2, const char *name2,
int flag, int marker_size)
const struct ll_merge_options *opts,
int marker_size)
{
char temp[4][50];
struct strbuf cmd = STRBUF_INIT;
@ -144,6 +154,7 @@ static int ll_ext_merge(const struct ll_merge_driver *fn,
const char *args[] = { NULL, NULL };
int status, fd, i;
struct stat st;
assert(opts);
dict[0].placeholder = "O"; dict[0].value = temp[0];
dict[1].placeholder = "A"; dict[1].value = temp[1];
@ -337,15 +348,21 @@ int ll_merge(mmbuffer_t *result_buf,
mmfile_t *ancestor, const char *ancestor_label,
mmfile_t *ours, const char *our_label,
mmfile_t *theirs, const char *their_label,
int flag)
const struct ll_merge_options *opts)
{
static struct git_attr_check check[2];
const char *ll_driver_name = NULL;
int marker_size = DEFAULT_CONFLICT_MARKER_SIZE;
const struct ll_merge_driver *driver;
int virtual_ancestor = flag & LL_OPT_VIRTUAL_ANCESTOR;
if (flag & LL_OPT_RENORMALIZE) {
if (!opts) {
struct ll_merge_options default_opts = {0};
return ll_merge(result_buf, path, ancestor, ancestor_label,
ours, our_label, theirs, their_label,
&default_opts);
}
if (opts->renormalize) {
normalize_file(ancestor, path);
normalize_file(ours, path);
normalize_file(theirs, path);
@ -359,11 +376,11 @@ int ll_merge(mmbuffer_t *result_buf,
}
}
driver = find_ll_merge_driver(ll_driver_name);
if (virtual_ancestor && driver->recursive)
if (opts->virtual_ancestor && driver->recursive)
driver = find_ll_merge_driver(driver->recursive);
return driver->fn(driver, result_buf, path, ancestor, ancestor_label,
ours, our_label, theirs, their_label,
flag, marker_size);
opts, marker_size);
}
int ll_merge_marker_size(const char *path)

View File

@ -5,27 +5,19 @@
#ifndef LL_MERGE_H
#define LL_MERGE_H
#define LL_OPT_VIRTUAL_ANCESTOR (1 << 0)
#define LL_OPT_FAVOR_MASK ((1 << 1) | (1 << 2))
#define LL_OPT_FAVOR_SHIFT 1
#define LL_OPT_RENORMALIZE (1 << 3)
static inline int ll_opt_favor(int flag)
{
return (flag & LL_OPT_FAVOR_MASK) >> LL_OPT_FAVOR_SHIFT;
}
static inline int create_ll_flag(int favor)
{
return ((favor << LL_OPT_FAVOR_SHIFT) & LL_OPT_FAVOR_MASK);
}
struct ll_merge_options {
unsigned virtual_ancestor : 1;
unsigned variant : 2; /* favor ours, favor theirs, or union merge */
unsigned renormalize : 1;
long xdl_opts;
};
int ll_merge(mmbuffer_t *result_buf,
const char *path,
mmfile_t *ancestor, const char *ancestor_label,
mmfile_t *ours, const char *our_label,
mmfile_t *theirs, const char *their_label,
int flag);
const struct ll_merge_options *opts);
int ll_merge_marker_size(const char *path);

View File

@ -37,7 +37,7 @@ static void *three_way_filemerge(const char *path, mmfile_t *base, mmfile_t *our
* common ancestor.
*/
merge_status = ll_merge(&res, path, base, NULL,
our, ".our", their, ".their", 0);
our, ".our", their, ".their", NULL);
if (merge_status < 0)
return NULL;

View File

@ -605,22 +605,26 @@ static int merge_3way(struct merge_options *o,
const char *branch2)
{
mmfile_t orig, src1, src2;
struct ll_merge_options ll_opts = {0};
char *base_name, *name1, *name2;
int merge_status;
int favor;
if (o->call_depth)
favor = 0;
else {
ll_opts.renormalize = o->renormalize;
ll_opts.xdl_opts = o->xdl_opts;
if (o->call_depth) {
ll_opts.virtual_ancestor = 1;
ll_opts.variant = 0;
} else {
switch (o->recursive_variant) {
case MERGE_RECURSIVE_OURS:
favor = XDL_MERGE_FAVOR_OURS;
ll_opts.variant = XDL_MERGE_FAVOR_OURS;
break;
case MERGE_RECURSIVE_THEIRS:
favor = XDL_MERGE_FAVOR_THEIRS;
ll_opts.variant = XDL_MERGE_FAVOR_THEIRS;
break;
default:
favor = 0;
ll_opts.variant = 0;
break;
}
}
@ -643,10 +647,7 @@ static int merge_3way(struct merge_options *o,
read_mmblob(&src2, b->sha1);
merge_status = ll_merge(result_buf, a->path, &orig, base_name,
&src1, name1, &src2, name2,
((o->call_depth ? LL_OPT_VIRTUAL_ANCESTOR : 0) |
(o->renormalize ? LL_OPT_RENORMALIZE : 0) |
create_ll_flag(favor)));
&src1, name1, &src2, name2, &ll_opts);
free(name1);
free(name2);
@ -1550,3 +1551,32 @@ void init_merge_options(struct merge_options *o)
memset(&o->current_directory_set, 0, sizeof(struct string_list));
o->current_directory_set.strdup_strings = 1;
}
int parse_merge_opt(struct merge_options *o, const char *s)
{
if (!s || !*s)
return -1;
if (!strcmp(s, "ours"))
o->recursive_variant = MERGE_RECURSIVE_OURS;
else if (!strcmp(s, "theirs"))
o->recursive_variant = MERGE_RECURSIVE_THEIRS;
else if (!strcmp(s, "subtree"))
o->subtree_shift = "";
else if (!prefixcmp(s, "subtree="))
o->subtree_shift = s + strlen("subtree=");
else if (!strcmp(s, "patience"))
o->xdl_opts |= XDF_PATIENCE_DIFF;
else if (!strcmp(s, "ignore-space-change"))
o->xdl_opts |= XDF_IGNORE_WHITESPACE_CHANGE;
else if (!strcmp(s, "ignore-all-space"))
o->xdl_opts |= XDF_IGNORE_WHITESPACE;
else if (!strcmp(s, "ignore-space-at-eol"))
o->xdl_opts |= XDF_IGNORE_WHITESPACE_AT_EOL;
else if (!strcmp(s, "renormalize"))
o->renormalize = 1;
else if (!strcmp(s, "no-renormalize"))
o->renormalize = 0;
else
return -1;
return 0;
}

View File

@ -15,6 +15,7 @@ struct merge_options {
const char *subtree_shift;
unsigned buffer_output : 1;
unsigned renormalize : 1;
long xdl_opts;
int verbosity;
int diff_rename_limit;
int merge_rename_limit;
@ -52,6 +53,8 @@ int merge_recursive_generic(struct merge_options *o,
void init_merge_options(struct merge_options *o);
struct tree *write_tree_from_memory(struct merge_options *o);
int parse_merge_opt(struct merge_options *out, const char *s);
/* builtin/merge.c */
int try_merge_command(const char *strategy, struct commit_list *common, const char *head_arg, struct commit_list *remotes);

View File

@ -325,7 +325,7 @@ static int handle_cache(const char *path, unsigned char *sha1, const char *outpu
*/
ll_merge(&result, path, &mmfile[0], NULL,
&mmfile[1], "ours",
&mmfile[2], "theirs", 0);
&mmfile[2], "theirs", NULL);
for (i = 0; i < 3; i++)
free(mmfile[i].ptr);

View File

@ -0,0 +1,186 @@
#!/bin/sh
test_description='merge-recursive options
* [master] Clarify
! [remote] Remove cruft
--
+ [remote] Remove cruft
* [master] Clarify
*+ [remote^] Initial revision
* ok 1: setup
'
. ./test-lib.sh
test_expect_success 'setup' '
conflict_hunks () {
sed -n -e "
/^<<<</ b inconflict
b
: inconflict
p
/^>>>>/ b
n
b inconflict
" "$@"
} &&
cat <<-\EOF >text.txt &&
Hope, he says, cherishes the soul of him who lives in
justice and holiness and is the nurse of his age and the
companion of his journey;--hope which is mightiest to sway
the restless soul of man.
How admirable are his words! And the great blessing of riches, I do
not say to every man, but to a good man, is, that he has had no
occasion to deceive or to defraud others, either intentionally or
unintentionally; and when he departs to the world below he is not in
any apprehension about offerings due to the gods or debts which he owes
to men. Now to this peace of mind the possession of wealth greatly
contributes; and therefore I say, that, setting one thing against
another, of the many advantages which wealth has to give, to a man of
sense this is in my opinion the greatest.
Well said, Cephalus, I replied; but as concerning justice, what is
it?--to speak the truth and to pay your debts--no more than this? And
even to this are there not exceptions? Suppose that a friend when in
his right mind has deposited arms with me and he asks for them when he
is not in his right mind, ought I to give them back to him? No one
would say that I ought or that I should be right in doing so, any more
than they would say that I ought always to speak the truth to one who
is in his condition.
You are quite right, he replied.
But then, I said, speaking the truth and paying your debts is not a
correct definition of justice.
CEPHALUS - SOCRATES - POLEMARCHUS
Quite correct, Socrates, if Simonides is to be believed, said
Polemarchus interposing.
I fear, said Cephalus, that I must go now, for I have to look after the
sacrifices, and I hand over the argument to Polemarchus and the company.
EOF
git add text.txt &&
test_tick &&
git commit -m "Initial revision" &&
git checkout -b remote &&
sed -e "
s/\. /\. /g
s/[?] /? /g
s/ / /g
s/--/---/g
s/but as concerning/but as con cerning/
/CEPHALUS - SOCRATES - POLEMARCHUS/ d
" text.txt >text.txt+ &&
mv text.txt+ text.txt &&
git commit -a -m "Remove cruft" &&
git checkout master &&
sed -e "
s/\(not in his right mind\),\(.*\)/\1;\2Q/
s/Quite correct\(.*\)/It is too correct\1Q/
s/unintentionally/un intentionally/
/un intentionally/ s/$/Q/
s/Polemarchus interposing./Polemarchus, interposing.Q/
/justice and holiness/ s/$/Q/
/pay your debts/ s/$/Q/
" text.txt | q_to_cr >text.txt+ &&
mv text.txt+ text.txt &&
git commit -a -m "Clarify" &&
git show-branch --all
'
test_expect_success 'naive merge fails' '
git read-tree --reset -u HEAD &&
test_must_fail git merge-recursive HEAD^ -- HEAD remote &&
test_must_fail git update-index --refresh &&
grep "<<<<<<" text.txt
'
test_expect_success '--ignore-space-change makes merge succeed' '
git read-tree --reset -u HEAD &&
git merge-recursive --ignore-space-change HEAD^ -- HEAD remote
'
test_expect_success '--ignore-space-change: our w/s-only change wins' '
q_to_cr <<-\EOF >expected &&
justice and holiness and is the nurse of his age and theQ
EOF
git read-tree --reset -u HEAD &&
git merge-recursive --ignore-space-change HEAD^ -- HEAD remote &&
grep "justice and holiness" text.txt >actual &&
test_cmp expected actual
'
test_expect_success '--ignore-space-change: their real change wins over w/s' '
cat <<-\EOF >expected &&
it?---to speak the truth and to pay your debts---no more than this? And
EOF
git read-tree --reset -u HEAD &&
git merge-recursive --ignore-space-change HEAD^ -- HEAD remote &&
grep "pay your debts" text.txt >actual &&
test_cmp expected actual
'
test_expect_success '--ignore-space-change: does not ignore new spaces' '
cat <<-\EOF >expected1 &&
Well said, Cephalus, I replied; but as con cerning justice, what is
EOF
q_to_cr <<-\EOF >expected2 &&
un intentionally; and when he departs to the world below he is not inQ
EOF
git read-tree --reset -u HEAD &&
git merge-recursive --ignore-space-change HEAD^ -- HEAD remote &&
grep "Well said" text.txt >actual1 &&
grep "when he departs" text.txt >actual2 &&
test_cmp expected1 actual1 &&
test_cmp expected2 actual2
'
test_expect_success '--ignore-all-space drops their new spaces' '
cat <<-\EOF >expected &&
Well said, Cephalus, I replied; but as concerning justice, what is
EOF
git read-tree --reset -u HEAD &&
git merge-recursive --ignore-all-space HEAD^ -- HEAD remote &&
grep "Well said" text.txt >actual &&
test_cmp expected actual
'
test_expect_success '--ignore-all-space keeps our new spaces' '
q_to_cr <<-\EOF >expected &&
un intentionally; and when he departs to the world below he is not inQ
EOF
git read-tree --reset -u HEAD &&
git merge-recursive --ignore-all-space HEAD^ -- HEAD remote &&
grep "when he departs" text.txt >actual &&
test_cmp expected actual
'
test_expect_success '--ignore-space-at-eol' '
q_to_cr <<-\EOF >expected &&
<<<<<<< HEAD
is not in his right mind; ought I to give them back to him? No oneQ
=======
is not in his right mind, ought I to give them back to him? No one
>>>>>>> remote
EOF
git read-tree --reset -u HEAD &&
test_must_fail git merge-recursive --ignore-space-at-eol \
HEAD^ -- HEAD remote &&
conflict_hunks text.txt >actual &&
test_cmp expected actual
'
test_done