Commit 78859c63 by Edward Thomson

merge: handle conflicts in recursive base building

When building a recursive merge base, allow conflicts to occur.
Use the file (with conflict markers) as the common ancestor.

The user has already seen and dealt with this conflict by virtue
of having a criss-cross merge.  If they resolved this conflict
identically in both branches, then there will be no conflict in the
result.  This is the best case scenario.

If they did not resolve the conflict identically in the two branches,
then we will generate a new conflict.  If the user is simply using
standard conflict output then the results will be fairly sensible.
But if the user is using a mergetool or using diff3 output, then the
common ancestor will be a conflict file (itself with diff3 output,
haha!).  This is quite terrible, but it matches git's behavior.
parent 34a51428
......@@ -49,6 +49,19 @@
#define GIT_MERGE_INDEX_ENTRY_EXISTS(X) ((X).mode != 0)
#define GIT_MERGE_INDEX_ENTRY_ISFILE(X) S_ISREG((X).mode)
/** Internal merge flags. */
enum {
/** The merge is for a virtual base in a recursive merge. */
GIT_MERGE__VIRTUAL_BASE = (1 << 31),
};
enum {
/** Accept the conflict file, staging it as the merge result. */
GIT_MERGE_FILE_FAVOR__CONFLICTED = 4,
};
typedef enum {
TREE_IDX_ANCESTOR = 0,
TREE_IDX_OURS = 1,
......@@ -801,11 +814,9 @@ static int merge_conflict_resolve_automerge(
int *resolved,
git_merge_diff_list *diff_list,
const git_merge_diff *conflict,
unsigned int merge_file_favor,
unsigned int file_flags)
const git_merge_file_options *file_opts)
{
const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL;
git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
git_merge_file_result result = {0};
git_index_entry *index_entry;
git_odb *odb = NULL;
......@@ -852,12 +863,9 @@ static int merge_conflict_resolve_automerge(
theirs = GIT_MERGE_INDEX_ENTRY_EXISTS(conflict->their_entry) ?
&conflict->their_entry : NULL;
opts.favor = merge_file_favor;
opts.flags = file_flags;
if ((error = git_repository_odb(&odb, diff_list->repo)) < 0 ||
(error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, &opts)) < 0 ||
!result.automergeable ||
(error = git_merge_file_from_index(&result, diff_list->repo, ancestor, ours, theirs, file_opts)) < 0 ||
(!result.automergeable && !(file_opts->flags & GIT_MERGE_FILE_FAVOR__CONFLICTED)) ||
(error = git_odb_write(&automerge_oid, odb, result.ptr, result.len, GIT_OBJ_BLOB)) < 0)
goto done;
......@@ -887,8 +895,7 @@ static int merge_conflict_resolve(
int *out,
git_merge_diff_list *diff_list,
const git_merge_diff *conflict,
unsigned int merge_file_favor,
unsigned int file_flags)
const git_merge_file_options *file_opts)
{
int resolved = 0;
int error = 0;
......@@ -904,8 +911,7 @@ static int merge_conflict_resolve(
if (!resolved && (error = merge_conflict_resolve_one_renamed(&resolved, diff_list, conflict)) < 0)
goto done;
if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict,
merge_file_favor, file_flags)) < 0)
if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict, file_opts)) < 0)
goto done;
*out = resolved;
......@@ -1829,6 +1835,7 @@ int git_merge__iterators(
*empty_theirs = NULL;
git_merge_diff_list *diff_list;
git_merge_options opts;
git_merge_file_options file_opts = GIT_MERGE_FILE_OPTIONS_INIT;
git_merge_diff *conflict;
git_vector changes;
size_t i;
......@@ -1844,6 +1851,17 @@ int git_merge__iterators(
if ((error = merge_normalize_opts(repo, &opts, given_opts)) < 0)
return error;
file_opts.favor = opts.file_favor;
file_opts.flags = opts.file_flags;
/* use the git-inspired labels when virtual base building */
if (opts.flags & GIT_MERGE__VIRTUAL_BASE) {
file_opts.ancestor_label = "merged common ancestors";
file_opts.our_label = "Temporary merge branch 1";
file_opts.their_label = "Temporary merge branch 2";
file_opts.flags |= GIT_MERGE_FILE_FAVOR__CONFLICTED;
}
diff_list = git_merge_diff_list__alloc(repo);
GITERR_CHECK_ALLOC(diff_list);
......@@ -1862,7 +1880,8 @@ int git_merge__iterators(
git_vector_foreach(&changes, i, conflict) {
int resolved = 0;
if ((error = merge_conflict_resolve(&resolved, diff_list, conflict, opts.file_favor, opts.file_flags)) < 0)
if ((error = merge_conflict_resolve(
&resolved, diff_list, conflict, &file_opts)) < 0)
goto done;
if (!resolved) {
......@@ -1962,16 +1981,27 @@ static int create_virtual_base(
git_repository *repo,
git_annotated_commit *one,
git_annotated_commit *two,
const git_merge_options *opts,
size_t recursion_level)
{
git_annotated_commit *result = NULL;
git_index *index = NULL;
git_merge_options virtual_opts = GIT_MERGE_OPTIONS_INIT;
result = git__calloc(1, sizeof(git_annotated_commit));
GITERR_CHECK_ALLOC(result);
/* Conflicts in the merge base creation do not propagate to conflicts
* in the result; the conflicted base will act as the common ancestor.
*/
if (opts)
memcpy(&virtual_opts, opts, sizeof(git_merge_options));
virtual_opts.flags &= ~GIT_MERGE_FAIL_ON_CONFLICT;
virtual_opts.flags |= GIT_MERGE__VIRTUAL_BASE;
if ((merge_annotated_commits(&index, NULL, repo, one, two,
recursion_level + 1, NULL)) < 0)
recursion_level + 1, &virtual_opts)) < 0)
return -1;
result->type = GIT_ANNOTATED_COMMIT_VIRTUAL;
......@@ -1989,7 +2019,7 @@ static int compute_base(
git_repository *repo,
const git_annotated_commit *one,
const git_annotated_commit *two,
bool recurse,
const git_merge_options *opts,
size_t recursion_level)
{
git_array_oid_t head_ids = GIT_ARRAY_INIT;
......@@ -2007,7 +2037,7 @@ static int compute_base(
if ((error = git_merge_bases_many(&bases, repo,
head_ids.size, head_ids.ptr)) < 0 ||
(error = git_annotated_commit_lookup(&base, repo, &bases.ids[0])) < 0 ||
!recurse)
(opts && (opts->flags & GIT_MERGE_NO_RECURSIVE)))
goto done;
for (i = 1; i < bases.count; i++) {
......@@ -2015,7 +2045,7 @@ static int compute_base(
if ((error = git_annotated_commit_lookup(&other, repo,
&bases.ids[i])) < 0 ||
(error = create_virtual_base(&new_base, repo, base, other,
(error = create_virtual_base(&new_base, repo, base, other, opts,
recursion_level)) < 0)
goto done;
......@@ -2076,10 +2106,9 @@ static int merge_annotated_commits(
{
git_annotated_commit *base = NULL;
git_iterator *base_iter = NULL, *our_iter = NULL, *their_iter = NULL;
bool recurse = !opts || !(opts->flags & GIT_MERGE_NO_RECURSIVE);
int error;
if ((error = compute_base(&base, repo, ours, theirs, recurse,
if ((error = compute_base(&base, repo, ours, theirs, opts,
recursion_level)) < 0) {
if (error != GIT_ENOTFOUND)
......
......@@ -70,3 +70,34 @@
"This is a mighty fine recipe!\n" \
">>>>>>> branchF-2\n"
#define CONFLICTING_RECURSIVE_H1_TO_H2_WITH_DIFF3 \
"VEAL SOUP.\n" \
"\n" \
"<<<<<<< HEAD\n" \
"put into a pot three quarts of water, three onions cut small, one\n" \
"||||||| merged common ancestors\n" \
"<<<<<<< Temporary merge branch 1\n" \
"Put into a pot three quarts of water, THREE ONIONS CUT SMALL, one\n" \
"||||||| merged common ancestors\n" \
"Put into a pot three quarts of water, three onions cut small, one\n" \
"=======\n" \
"PUT INTO A POT three quarts of water, three onions cut small, one\n" \
">>>>>>> Temporary merge branch 2\n" \
"=======\n" \
"Put Into A Pot Three Quarts of Water, Three Onions Cut Small, One\n" \
">>>>>>> branchH-2\n" \
"spoonful of black pepper pounded, and two of salt, with two or three\n" \
"slices of lean ham; let it boil steadily two hours; skim it\n" \
"occasionally, then put into it a shin of veal, let it boil two hours\n" \
"longer; take out the slices of ham, and skim off the grease if any\n" \
"should rise, take a gill of good cream, mix with it two table-spoonsful\n" \
"of flour very nicely, and the yelks of two eggs beaten well, strain this\n" \
"mixture, and add some chopped parsley; pour some soup on by degrees,\n" \
"stir it well, and pour it into the pot, continuing to stir until it has\n" \
"boiled two or three minutes to take off the raw taste of the eggs. If\n" \
"the cream be not perfectly sweet, and the eggs quite new, the thickening\n" \
"will curdle in the soup. For a change you may put a dozen ripe tomatos\n" \
"in, first taking off their skins, by letting them stand a few minutes in\n" \
"hot water, when they may be easily peeled. When made in this way you\n" \
"must thicken it with the flour only. Any part of the veal may be used,\n" \
"but the shin or knuckle is the nicest.\n"
......@@ -297,3 +297,83 @@ void test_merge_trees_recursive__oh_so_many_levels_of_recursion(void)
git_index_free(index);
}
/* Branch H-1 and H-2 have two common ancestors (aa9e263, 6ef31d3). The two
* ancestors themselves conflict.
*/
void test_merge_trees_recursive__conflicting_merge_base(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "3a66812fed1e03ea4a6a7ee28d8a57aec1ca6537", 1, "veal.txt" },
{ 0100644, "d604c75019c282144bdbbf3fd3462ba74b240efc", 2, "veal.txt" },
{ 0100644, "37a5054a9f9b4628e3924c5cb8f2147c6e2a3efc", 3, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchH-1", "branchH-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
/* Branch H-1 and H-2 have two common ancestors (aa9e263, 6ef31d3). The two
* ancestors themselves conflict. The generated common ancestor file will
* have diff3 style conflicts inside it.
*/
void test_merge_trees_recursive__conflicting_merge_base_with_diff3(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "cd17a91513f3aee9e44114d1ede67932dd41d2fc", 1, "veal.txt" },
{ 0100644, "d604c75019c282144bdbbf3fd3462ba74b240efc", 2, "veal.txt" },
{ 0100644, "37a5054a9f9b4628e3924c5cb8f2147c6e2a3efc", 3, "veal.txt" },
};
opts.file_flags |= GIT_MERGE_FILE_STYLE_DIFF3;
cl_git_pass(merge_commits_from_branches(&index, repo, "branchH-1", "branchH-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 8));
git_index_free(index);
}
/* Branch I-1 and I-2 have two common ancestors (aa9e263, 6ef31d3). The two
* ancestors themselves conflict, but when each was merged, the conflicts were
* resolved identically, thus merging I-1 into I-2 does not conflict.
*/
void test_merge_trees_recursive__conflicting_merge_base_since_resolved(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "a02d4fd126e0cc8fb46ee48cf38bad36d44f2dbc", 0, "veal.txt" },
};
cl_git_pass(merge_commits_from_branches(&index, repo, "branchI-1", "branchI-2", &opts));
cl_assert(merge_test_index(index, merge_index_entries, 6));
git_index_free(index);
}
......@@ -36,7 +36,6 @@ void test_merge_workdir_recursive__writes_conflict_with_virtual_base(void)
{ 0100644, "3855170cef875708da06ab9ad7fc6a73b531cda1", 3, "veal.txt" },
};
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR "branchF-1", GIT_REFS_HEADS_DIR "branchF-2", &opts, NULL));
cl_git_pass(git_repository_index(&index, repo));
......@@ -49,3 +48,37 @@ void test_merge_workdir_recursive__writes_conflict_with_virtual_base(void)
git_index_free(index);
git_buf_free(&conflicting_buf);
}
void test_merge_workdir_recursive__conflicting_merge_base_with_diff3(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
git_buf conflicting_buf = GIT_BUF_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" },
{ 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" },
{ 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" },
{ 0100644, "c4e6cca3ec6ae0148ed231f97257df8c311e015f", 0, "gravy.txt" },
{ 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" },
{ 0100644, "cd17a91513f3aee9e44114d1ede67932dd41d2fc", 1, "veal.txt" },
{ 0100644, "d604c75019c282144bdbbf3fd3462ba74b240efc", 2, "veal.txt" },
{ 0100644, "37a5054a9f9b4628e3924c5cb8f2147c6e2a3efc", 3, "veal.txt" },
};
opts.file_flags |= GIT_MERGE_FILE_STYLE_DIFF3;
checkout_opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_DIFF3;
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR "branchH-1", GIT_REFS_HEADS_DIR "branchH-2", &opts, &checkout_opts));
cl_git_pass(git_repository_index(&index, repo));
cl_assert(merge_test_index(index, merge_index_entries, 8));
cl_git_pass(git_futils_readbuffer(&conflicting_buf, "merge-recursive/veal.txt"));
cl_assert_equal_s(CONFLICTING_RECURSIVE_H1_TO_H2_WITH_DIFF3, conflicting_buf.ptr);
git_index_free(index);
git_buf_free(&conflicting_buf);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment