Commit 89ba9f1a by Edward Thomson

Merge pull request #2967 from jacquesg/merge-whitespace

Allow merges of files (and trees) with whitespace problems/fixes
parents 7568bdf4 70b0f128
......@@ -3,6 +3,10 @@ v0.22 + 1
### Changes or improvements
* Patience and minimal diff drivers can now be used for merges.
* Merges can now ignore whitespace changes.
* Updated binary identification in CRLF filtering to avoid false positives in
UTF-8 files.
......@@ -26,6 +30,8 @@ v0.22 + 1
### API additions
* The `git_merge_options` gained a `file_flags` member.
* Parsing and retrieving a configuration value as a path is exposed
via `git_config_parse_path()` and `git_config_get_path()`
respectively.
......@@ -55,6 +61,12 @@ v0.22 + 1
### Breaking API changes
* The `git_merge_options` structure member `flags` has been renamed
to `tree_flags`.
* The `git_merge_file_options` structure member `tree_flags` is now
an unsigned int. It was previously a `git_merge_file_flags_t`.
* `GIT_CHECKOUT_SAFE_CREATE` has been removed. Most users will generally
be able to switch to `GIT_CHECKOUT_SAFE`, but if you require missing
file handling during checkout, you may now use `GIT_CHECKOUT_SAFE |
......
......@@ -63,7 +63,7 @@ GIT_EXTERN(int) git_merge_file_init_input(
/**
* Flags for `git_merge_tree` options. A combination of these flags can be
* passed in via the `flags` value in the `git_merge_options`.
* passed in via the `tree_flags` value in the `git_merge_options`.
*/
typedef enum {
/**
......@@ -125,6 +125,21 @@ typedef enum {
/** Condense non-alphanumeric regions for simplified diff file */
GIT_MERGE_FILE_SIMPLIFY_ALNUM = (1 << 2),
/** Ignore all whitespace */
GIT_MERGE_FILE_IGNORE_WHITESPACE = (1 << 3),
/** Ignore changes in amount of whitespace */
GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE = (1 << 4),
/** Ignore whitespace at end of line */
GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL = (1 << 5),
/** Use the "patience diff" algorithm */
GIT_MERGE_FILE_DIFF_PATIENCE = (1 << 6),
/** Take extra time to find minimal diff */
GIT_MERGE_FILE_DIFF_MINIMAL = (1 << 7),
} git_merge_file_flags_t;
/**
......@@ -155,7 +170,7 @@ typedef struct {
git_merge_file_favor_t favor;
/** Merge file flags. */
git_merge_file_flags_t flags;
unsigned int flags;
} git_merge_file_options;
#define GIT_MERGE_FILE_OPTIONS_VERSION 1
......@@ -205,7 +220,7 @@ typedef struct {
*/
typedef struct {
unsigned int version;
git_merge_tree_flag_t flags;
git_merge_tree_flag_t tree_flags;
/**
* Similarity to consider a file renamed (default 50). If
......@@ -230,6 +245,8 @@ typedef struct {
/** Flags for handling conflicting content. */
git_merge_file_favor_t file_favor;
unsigned int file_flags;
} git_merge_options;
#define GIT_MERGE_OPTIONS_VERSION 1
......
......@@ -658,7 +658,8 @@ 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 merge_file_favor,
unsigned int file_flags)
{
const git_index_entry *ancestor = NULL, *ours = NULL, *theirs = NULL;
git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
......@@ -716,6 +717,7 @@ static int merge_conflict_resolve_automerge(
&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 ||
......@@ -749,7 +751,8 @@ 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 merge_file_favor,
unsigned int file_flags)
{
int resolved = 0;
int error = 0;
......@@ -765,7 +768,8 @@ 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)) < 0)
if (!resolved && (error = merge_conflict_resolve_automerge(&resolved, diff_list, conflict,
merge_file_favor, file_flags)) < 0)
goto done;
*out = resolved;
......@@ -1158,7 +1162,7 @@ int git_merge_diff_list__find_renames(
assert(diff_list && opts);
if ((opts->flags & GIT_MERGE_TREE_FIND_RENAMES) == 0)
if ((opts->tree_flags & GIT_MERGE_TREE_FIND_RENAMES) == 0)
return 0;
similarity_ours = git__calloc(diff_list->conflicts.length,
......@@ -1600,7 +1604,7 @@ static int merge_normalize_opts(
git_merge_options init = GIT_MERGE_OPTIONS_INIT;
memcpy(opts, &init, sizeof(init));
opts->flags = GIT_MERGE_TREE_FIND_RENAMES;
opts->tree_flags = GIT_MERGE_TREE_FIND_RENAMES;
opts->rename_threshold = GIT_MERGE_TREE_RENAME_THRESHOLD;
}
......@@ -1790,7 +1794,7 @@ int git_merge_trees(
git_vector_foreach(&changes, i, conflict) {
int resolved = 0;
if ((error = merge_conflict_resolve(&resolved, diff_list, conflict, opts.file_favor)) < 0)
if ((error = merge_conflict_resolve(&resolved, diff_list, conflict, opts.file_favor, opts.file_flags)) < 0)
goto done;
if (!resolved)
......
......@@ -151,6 +151,19 @@ static int git_merge_file__from_inputs(
if (options.flags & GIT_MERGE_FILE_STYLE_DIFF3)
xmparam.style = XDL_MERGE_DIFF3;
if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE)
xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE;
if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE)
xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE_CHANGE;
if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL)
xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE_AT_EOL;
if (options.flags & GIT_MERGE_FILE_DIFF_PATIENCE)
xmparam.xpp.flags |= XDF_PATIENCE_DIFF;
if (options.flags & GIT_MERGE_FILE_DIFF_MINIMAL)
xmparam.xpp.flags |= XDF_NEED_MINIMAL;
if ((xdl_result = xdl_merge(&ancestor_mmfile, &our_mmfile,
&their_mmfile, &xmparam, &mmbuffer)) < 0) {
giterr_set(GITERR_MERGE, "Failed to merge files.");
......
......@@ -300,7 +300,7 @@ void test_cherrypick_workdir__rename(void)
{ 0100644, "28d9eb4208074ad1cc84e71ccc908b34573f05d2", 0, "file3.txt.renamed" },
};
opts.merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "cfc4f0999a8367568e049af4f72e452d40828a15");
......@@ -335,7 +335,7 @@ void test_cherrypick_workdir__both_renamed(void)
{ 0100644, "28d9eb4208074ad1cc84e71ccc908b34573f05d2", 2, "file3.txt.renamed_on_branch" },
};
opts.merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "44cd2ed2052c9c68f9a439d208e9614dc2a55c70");
......
......@@ -173,3 +173,79 @@ void test_merge_files__automerge_from_index(void)
git_merge_file_result_free(&result);
}
void test_merge_files__automerge_whitespace_eol(void)
{
git_merge_file_input ancestor = GIT_MERGE_FILE_INPUT_INIT,
ours = GIT_MERGE_FILE_INPUT_INIT,
theirs = GIT_MERGE_FILE_INPUT_INIT;
git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
git_merge_file_result result = {0};
const char *expected = "Zero\n1\n2\n3\n4\n5\n6\n7\n8\n9\nTen\n";
ancestor.ptr = "0 \n1\n2\n3\n4\n5\n6\n7\n8\n9\n10 \n";
ancestor.size = strlen(ancestor.ptr);
ancestor.path = "testfile.txt";
ancestor.mode = 0100755;
ours.ptr = "Zero\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n";
ours.size = strlen(ours.ptr);
ours.path = "testfile.txt";
ours.mode = 0100755;
theirs.ptr = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\nTen\n";
theirs.size = strlen(theirs.ptr);
theirs.path = "testfile.txt";
theirs.mode = 0100755;
opts.flags |= GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL;
cl_git_pass(git_merge_file(&result, &ancestor, &ours, &theirs, &opts));
cl_assert_equal_i(1, result.automergeable);
cl_assert_equal_s("testfile.txt", result.path);
cl_assert_equal_i(0100755, result.mode);
cl_assert_equal_i(strlen(expected), result.len);
cl_assert_equal_strn(expected, result.ptr, result.len);
git_merge_file_result_free(&result);
}
void test_merge_files__automerge_whitespace_change(void)
{
git_merge_file_input ancestor = GIT_MERGE_FILE_INPUT_INIT,
ours = GIT_MERGE_FILE_INPUT_INIT,
theirs = GIT_MERGE_FILE_INPUT_INIT;
git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
git_merge_file_result result = {0};
const char *expected = "Zero\n1\n2\n3\n4\n5 XXX\n6 YYY\n7\n8\n9\nTen\n";
ancestor.ptr = "0\n1\n2\n3\n4\n5 XXX\n6YYY\n7\n8\n9\n10\n";
ancestor.size = strlen(ancestor.ptr);
ancestor.path = "testfile.txt";
ancestor.mode = 0100755;
ours.ptr = "Zero\n1\n2\n3\n4\n5 XXX\n6 YYY\n7\n8\n9\n10\n";
ours.size = strlen(ours.ptr);
ours.path = "testfile.txt";
ours.mode = 0100755;
theirs.ptr = "0\n1\n2\n3\n4\n5 XXX\n6 YYY\n7\n8\n9\nTen\n";
theirs.size = strlen(theirs.ptr);
theirs.path = "testfile.txt";
theirs.mode = 0100755;
opts.flags |= GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE;
cl_git_pass(git_merge_file(&result, &ancestor, &ours, &theirs, &opts));
cl_assert_equal_i(1, result.automergeable);
cl_assert_equal_s("testfile.txt", result.path);
cl_assert_equal_i(0100755, result.mode);
cl_assert_equal_i(strlen(expected), result.len);
cl_assert_equal_strn(expected, result.ptr, result.len);
git_merge_file_result_free(&result);
}
......@@ -45,7 +45,7 @@ static void test_find_differences(
git_tree *ancestor_tree, *ours_tree, *theirs_tree;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.target_limit = 1000;
opts.rename_threshold = 50;
......
#include "clar_libgit2.h"
#include "git2/repository.h"
#include "git2/merge.h"
#include "buffer.h"
#include "merge.h"
#include "../merge_helpers.h"
#include "fileops.h"
static git_repository *repo;
#define TEST_REPO_PATH "merge-whitespace"
#define BRANCH_A_EOL "branch_a_eol"
#define BRANCH_B_EOL "branch_b_eol"
#define BRANCH_A_CHANGE "branch_a_change"
#define BRANCH_B_CHANGE "branch_b_change"
// Fixture setup and teardown
void test_merge_trees_whitespace__initialize(void)
{
repo = cl_git_sandbox_init(TEST_REPO_PATH);
}
void test_merge_trees_whitespace__cleanup(void)
{
cl_git_sandbox_cleanup();
}
void test_merge_trees_whitespace__conflict(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "4026a6c83f39c56881c9ac62e7582db9e3d33a4f", 1, "test.txt" },
{ 0100644, "c3b1fb31424c98072542cc8e42b48c92e52f494a", 2, "test.txt" },
{ 0100644, "262f67de0de2e535a59ae1bc3c739601e98c354d", 3, "test.txt" },
};
cl_git_pass(merge_trees_from_branches(&index, repo, BRANCH_A_EOL, BRANCH_B_EOL, &opts));
cl_assert(merge_test_index(index, merge_index_entries, 3));
git_index_free(index);
}
void test_merge_trees_whitespace__eol(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "ee3c2aac8e03224c323b58ecb1f9eef616745467", 0, "test.txt" },
};
opts.file_flags |= GIT_MERGE_FILE_IGNORE_WHITESPACE_EOL;
cl_git_pass(merge_trees_from_branches(&index, repo, BRANCH_A_EOL, BRANCH_B_EOL, &opts));
cl_assert(merge_test_index(index, merge_index_entries, 1));
git_index_free(index);
}
void test_merge_trees_whitespace__change(void)
{
git_index *index;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
struct merge_index_entry merge_index_entries[] = {
{ 0100644, "a827eab4fd66ab37a6ebcfaa7b7e341abfd55947", 0, "test.txt" },
};
opts.file_flags |= GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE;
cl_git_pass(merge_trees_from_branches(&index, repo, BRANCH_A_CHANGE, BRANCH_B_CHANGE, &opts));
cl_assert(merge_test_index(index, merge_index_entries, 1));
git_index_free(index);
}
......@@ -63,7 +63,7 @@ void test_merge_workdir_renames__renames(void)
{ 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed.txt~rename_conflict_theirs" },
};
merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.rename_threshold = 50;
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &merge_opts, NULL));
......@@ -99,7 +99,7 @@ void test_merge_workdir_renames__ours(void)
{ 0100644, "b42712cfe99a1a500b2a51fe984e0b8a7702ba11", 0, "7-both-renamed.txt" },
};
merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.rename_threshold = 50;
checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_USE_OURS;
......@@ -147,7 +147,7 @@ void test_merge_workdir_renames__similar(void)
{ 0100644, "b69fe837e4cecfd4c9a40cdca7c138468687df07", 0, "7-both-renamed.txt~rename_conflict_theirs" },
};
merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
merge_opts.rename_threshold = 50;
cl_git_pass(merge_branches(repo, GIT_REFS_HEADS_DIR BRANCH_RENAME_OURS, GIT_REFS_HEADS_DIR BRANCH_RENAME_THEIRS, &merge_opts, NULL));
......
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
1189e10a62aadf2fea8cd018afb52c1980f40b4f
......@@ -408,7 +408,7 @@ void test_revert_workdir__rename_1_of_2(void)
{ 0100644, "0f5bfcf58c558d865da6be0281d7795993646cee", 2, "file6.txt" },
};
opts.merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "cef56612d71a6af8d8015691e4865f7fece905b5");
......@@ -442,7 +442,7 @@ void test_revert_workdir__rename(void)
{ "file4.txt", "file5.txt", "" },
};
opts.merge_opts.flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.tree_flags |= GIT_MERGE_TREE_FIND_RENAMES;
opts.merge_opts.rename_threshold = 50;
git_oid_fromstr(&head_oid, "55568c8de5322ff9a95d72747a239cdb64a19965");
......
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