Merge branch 'en/merge-recursive'

* en/merge-recursive: (41 commits)
  t6022: Use -eq not = to test output of wc -l
  merge-recursive:make_room_for_directories - work around dumb compilers
  merge-recursive: Remove redundant path clearing for D/F conflicts
  merge-recursive: Make room for directories in D/F conflicts
  handle_delete_modify(): Check whether D/F conflicts are still present
  merge_content(): Check whether D/F conflicts are still present
  conflict_rename_rename_1to2(): Fix checks for presence of D/F conflicts
  conflict_rename_delete(): Check whether D/F conflicts are still present
  merge-recursive: Delay modify/delete conflicts if D/F conflict present
  merge-recursive: Delay content merging for renames
  merge-recursive: Delay handling of rename/delete conflicts
  merge-recursive: Move handling of double rename of one file to other file
  merge-recursive: Move handling of double rename of one file to two
  merge-recursive: Avoid doubly merging rename/add conflict contents
  merge-recursive: Update merge_content() call signature
  merge-recursive: Update conflict_rename_rename_1to2() call signature
  merge-recursive: Structure process_df_entry() to handle more cases
  merge-recursive: Have process_entry() skip D/F or rename entries
  merge-recursive: New function to assist resolving renames in-core only
  merge-recursive: New data structures for deferring of D/F conflicts
  ...

Conflicts:
	t/t6020-merge-df.sh
	t/t6036-recursive-corner-cases.sh
This commit is contained in:
Junio C Hamano 2010-11-29 17:52:35 -08:00
Родитель 106e3afa6f 9f6cea97c9
Коммит 5acb623b72
7 изменённых файлов: 1172 добавлений и 240 удалений

Просмотреть файл

@ -63,6 +63,22 @@ static int sha_eq(const unsigned char *a, const unsigned char *b)
return a && b && hashcmp(a, b) == 0;
}
enum rename_type {
RENAME_NORMAL = 0,
RENAME_DELETE,
RENAME_ONE_FILE_TO_TWO
};
struct rename_df_conflict_info {
enum rename_type rename_type;
struct diff_filepair *pair1;
struct diff_filepair *pair2;
const char *branch1;
const char *branch2;
struct stage_data *dst_entry1;
struct stage_data *dst_entry2;
};
/*
* Since we want to write the index eventually, we cannot reuse the index
* for these (temporary) data.
@ -74,9 +90,37 @@ struct stage_data
unsigned mode;
unsigned char sha[20];
} stages[4];
struct rename_df_conflict_info *rename_df_conflict_info;
unsigned processed:1;
};
static inline void setup_rename_df_conflict_info(enum rename_type rename_type,
struct diff_filepair *pair1,
struct diff_filepair *pair2,
const char *branch1,
const char *branch2,
struct stage_data *dst_entry1,
struct stage_data *dst_entry2)
{
struct rename_df_conflict_info *ci = xcalloc(1, sizeof(struct rename_df_conflict_info));
ci->rename_type = rename_type;
ci->pair1 = pair1;
ci->branch1 = branch1;
ci->branch2 = branch2;
ci->dst_entry1 = dst_entry1;
dst_entry1->rename_df_conflict_info = ci;
dst_entry1->processed = 0;
assert(!pair2 == !dst_entry2);
if (dst_entry2) {
ci->dst_entry2 = dst_entry2;
ci->pair2 = pair2;
dst_entry2->rename_df_conflict_info = ci;
dst_entry2->processed = 0;
}
}
static int show(struct merge_options *o, int v)
{
return (!o->call_depth && o->verbosity >= v) || o->verbosity >= 5;
@ -302,6 +346,63 @@ static struct string_list *get_unmerged(void)
return unmerged;
}
static void make_room_for_directories_of_df_conflicts(struct merge_options *o,
struct string_list *entries)
{
/* If there are D/F conflicts, and the paths currently exist
* in the working copy as a file, we want to remove them to
* make room for the corresponding directory. Such paths will
* later be processed in process_df_entry() at the end. If
* the corresponding directory ends up being removed by the
* merge, then the file will be reinstated at that time;
* otherwise, if the file is not supposed to be removed by the
* merge, the contents of the file will be placed in another
* unique filename.
*
* NOTE: This function relies on the fact that entries for a
* D/F conflict will appear adjacent in the index, with the
* entries for the file appearing before entries for paths
* below the corresponding directory.
*/
const char *last_file = NULL;
int last_len = 0;
struct stage_data *last_e;
int i;
for (i = 0; i < entries->nr; i++) {
const char *path = entries->items[i].string;
int len = strlen(path);
struct stage_data *e = entries->items[i].util;
/*
* Check if last_file & path correspond to a D/F conflict;
* i.e. whether path is last_file+'/'+<something>.
* If so, remove last_file to make room for path and friends.
*/
if (last_file &&
len > last_len &&
memcmp(path, last_file, last_len) == 0 &&
path[last_len] == '/') {
output(o, 3, "Removing %s to make room for subdirectory; may re-add later.", last_file);
unlink(last_file);
}
/*
* Determine whether path could exist as a file in the
* working directory as a possible D/F conflict. This
* will only occur when it exists in stage 2 as a
* file.
*/
if (S_ISREG(e->stages[2].mode) || S_ISLNK(e->stages[2].mode)) {
last_file = path;
last_len = len;
last_e = e;
} else {
last_file = NULL;
}
}
}
struct rename
{
struct diff_filepair *pair;
@ -374,11 +475,10 @@ static struct string_list *get_renames(struct merge_options *o,
return renames;
}
static int update_stages(const char *path, struct diff_filespec *o,
static int update_stages_options(const char *path, struct diff_filespec *o,
struct diff_filespec *a, struct diff_filespec *b,
int clear)
int clear, int options)
{
int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
if (clear)
if (remove_file_from_cache(path))
return -1;
@ -394,6 +494,34 @@ static int update_stages(const char *path, struct diff_filespec *o,
return 0;
}
static int update_stages(const char *path, struct diff_filespec *o,
struct diff_filespec *a, struct diff_filespec *b,
int clear)
{
int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE;
return update_stages_options(path, o, a, b, clear, options);
}
static int update_stages_and_entry(const char *path,
struct stage_data *entry,
struct diff_filespec *o,
struct diff_filespec *a,
struct diff_filespec *b,
int clear)
{
int options;
entry->processed = 0;
entry->stages[1].mode = o->mode;
entry->stages[2].mode = a->mode;
entry->stages[3].mode = b->mode;
hashcpy(entry->stages[1].sha, o->sha1);
hashcpy(entry->stages[2].sha, a->sha1);
hashcpy(entry->stages[3].sha, b->sha1);
options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_SKIP_DFCHECK;
return update_stages_options(path, o, a, b, clear, options);
}
static int remove_file(struct merge_options *o, int clean,
const char *path, int no_wd)
{
@ -732,29 +860,56 @@ static struct merge_file_info merge_file(struct merge_options *o,
return result;
}
static void conflict_rename_rename(struct merge_options *o,
struct rename *ren1,
const char *branch1,
struct rename *ren2,
const char *branch2)
static void conflict_rename_delete(struct merge_options *o,
struct diff_filepair *pair,
const char *rename_branch,
const char *other_branch)
{
char *dest_name = pair->two->path;
int df_conflict = 0;
struct stat st;
output(o, 1, "CONFLICT (rename/delete): Rename %s->%s in %s "
"and deleted in %s",
pair->one->path, pair->two->path, rename_branch,
other_branch);
if (!o->call_depth)
update_stages(dest_name, NULL,
rename_branch == o->branch1 ? pair->two : NULL,
rename_branch == o->branch1 ? NULL : pair->two,
1);
if (lstat(dest_name, &st) == 0 && S_ISDIR(st.st_mode)) {
dest_name = unique_path(o, dest_name, rename_branch);
df_conflict = 1;
}
update_file(o, 0, pair->two->sha1, pair->two->mode, dest_name);
if (df_conflict)
free(dest_name);
}
static void conflict_rename_rename_1to2(struct merge_options *o,
struct diff_filepair *pair1,
const char *branch1,
struct diff_filepair *pair2,
const char *branch2)
{
/* One file was renamed in both branches, but to different names. */
char *del[2];
int delp = 0;
const char *ren1_dst = ren1->pair->two->path;
const char *ren2_dst = ren2->pair->two->path;
const char *ren1_dst = pair1->two->path;
const char *ren2_dst = pair2->two->path;
const char *dst_name1 = ren1_dst;
const char *dst_name2 = ren2_dst;
if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
struct stat st;
if (lstat(ren1_dst, &st) == 0 && S_ISDIR(st.st_mode)) {
dst_name1 = del[delp++] = unique_path(o, ren1_dst, branch1);
output(o, 1, "%s is a directory in %s adding as %s instead",
ren1_dst, branch2, dst_name1);
remove_file(o, 0, ren1_dst, 0);
}
if (string_list_has_string(&o->current_directory_set, ren2_dst)) {
if (lstat(ren2_dst, &st) == 0 && S_ISDIR(st.st_mode)) {
dst_name2 = del[delp++] = unique_path(o, ren2_dst, branch2);
output(o, 1, "%s is a directory in %s adding as %s instead",
ren2_dst, branch1, dst_name2);
remove_file(o, 0, ren2_dst, 0);
}
if (o->call_depth) {
remove_file_from_cache(dst_name1);
@ -762,34 +917,27 @@ static void conflict_rename_rename(struct merge_options *o,
/*
* Uncomment to leave the conflicting names in the resulting tree
*
* update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, dst_name1);
* update_file(o, 0, ren2->pair->two->sha1, ren2->pair->two->mode, dst_name2);
* update_file(o, 0, pair1->two->sha1, pair1->two->mode, dst_name1);
* update_file(o, 0, pair2->two->sha1, pair2->two->mode, dst_name2);
*/
} else {
update_stages(dst_name1, NULL, ren1->pair->two, NULL, 1);
update_stages(dst_name2, NULL, NULL, ren2->pair->two, 1);
update_stages(ren1_dst, NULL, pair1->two, NULL, 1);
update_stages(ren2_dst, NULL, NULL, pair2->two, 1);
update_file(o, 0, pair1->two->sha1, pair1->two->mode, dst_name1);
update_file(o, 0, pair2->two->sha1, pair2->two->mode, dst_name2);
}
while (delp--)
free(del[delp]);
}
static void conflict_rename_dir(struct merge_options *o,
struct rename *ren1,
const char *branch1)
{
char *new_path = unique_path(o, ren1->pair->two->path, branch1);
output(o, 1, "Renaming %s to %s instead", ren1->pair->one->path, new_path);
remove_file(o, 0, ren1->pair->two->path, 0);
update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path);
free(new_path);
}
static void conflict_rename_rename_2(struct merge_options *o,
struct rename *ren1,
const char *branch1,
struct rename *ren2,
const char *branch2)
static void conflict_rename_rename_2to1(struct merge_options *o,
struct rename *ren1,
const char *branch1,
struct rename *ren2,
const char *branch2)
{
/* Two files were renamed to the same thing. */
char *new_path1 = unique_path(o, ren1->pair->two->path, branch1);
char *new_path2 = unique_path(o, ren2->pair->two->path, branch2);
output(o, 1, "Renaming %s to %s and %s to %s instead",
@ -879,84 +1027,60 @@ static int process_renames(struct merge_options *o,
ren2->dst_entry->processed = 1;
ren2->processed = 1;
if (strcmp(ren1_dst, ren2_dst) != 0) {
clean_merge = 0;
output(o, 1, "CONFLICT (rename/rename): "
"Rename \"%s\"->\"%s\" in branch \"%s\" "
"rename \"%s\"->\"%s\" in \"%s\"%s",
src, ren1_dst, branch1,
src, ren2_dst, branch2,
o->call_depth ? " (left unresolved)": "");
if (o->call_depth) {
remove_file_from_cache(src);
update_file(o, 0, ren1->pair->one->sha1,
ren1->pair->one->mode, src);
}
conflict_rename_rename(o, ren1, branch1, ren2, branch2);
setup_rename_df_conflict_info(RENAME_ONE_FILE_TO_TWO,
ren1->pair,
ren2->pair,
branch1,
branch2,
ren1->dst_entry,
ren2->dst_entry);
} else {
struct merge_file_info mfi;
remove_file(o, 1, ren1_src, 1);
mfi = merge_file(o,
ren1->pair->one,
ren1->pair->two,
ren2->pair->two,
branch1,
branch2);
if (mfi.merge || !mfi.clean)
output(o, 1, "Renaming %s->%s", src, ren1_dst);
if (mfi.merge)
output(o, 2, "Auto-merging %s", ren1_dst);
if (!mfi.clean) {
output(o, 1, "CONFLICT (content): merge conflict in %s",
ren1_dst);
clean_merge = 0;
if (!o->call_depth)
update_stages(ren1_dst,
ren1->pair->one,
ren1->pair->two,
ren2->pair->two,
1 /* clear */);
}
update_file(o, mfi.clean, mfi.sha, mfi.mode, ren1_dst);
update_stages_and_entry(ren1_dst,
ren1->dst_entry,
ren1->pair->one,
ren1->pair->two,
ren2->pair->two,
1 /* clear */);
}
} else {
/* Renamed in 1, maybe changed in 2 */
struct string_list_item *item;
/* we only use sha1 and mode of these */
struct diff_filespec src_other, dst_other;
int try_merge, stage = a_renames == renames1 ? 3: 2;
int try_merge;
remove_file(o, 1, ren1_src, o->call_depth || stage == 3);
/*
* unpack_trees loads entries from common-commit
* into stage 1, from head-commit into stage 2, and
* from merge-commit into stage 3. We keep track
* of which side corresponds to the rename.
*/
int renamed_stage = a_renames == renames1 ? 2 : 3;
int other_stage = a_renames == renames1 ? 3 : 2;
hashcpy(src_other.sha1, ren1->src_entry->stages[stage].sha);
src_other.mode = ren1->src_entry->stages[stage].mode;
hashcpy(dst_other.sha1, ren1->dst_entry->stages[stage].sha);
dst_other.mode = ren1->dst_entry->stages[stage].mode;
remove_file(o, 1, ren1_src, o->call_depth || renamed_stage == 2);
hashcpy(src_other.sha1, ren1->src_entry->stages[other_stage].sha);
src_other.mode = ren1->src_entry->stages[other_stage].mode;
hashcpy(dst_other.sha1, ren1->dst_entry->stages[other_stage].sha);
dst_other.mode = ren1->dst_entry->stages[other_stage].mode;
try_merge = 0;
if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
clean_merge = 0;
output(o, 1, "CONFLICT (rename/directory): Rename %s->%s in %s "
" directory %s added in %s",
ren1_src, ren1_dst, branch1,
ren1_dst, branch2);
conflict_rename_dir(o, ren1, branch1);
} else if (sha_eq(src_other.sha1, null_sha1)) {
clean_merge = 0;
output(o, 1, "CONFLICT (rename/delete): Rename %s->%s in %s "
"and deleted in %s",
ren1_src, ren1_dst, branch1,
branch2);
update_file(o, 0, ren1->pair->two->sha1, ren1->pair->two->mode, ren1_dst);
if (!o->call_depth)
update_stages(ren1_dst, NULL,
branch1 == o->branch1 ?
ren1->pair->two : NULL,
branch1 == o->branch1 ?
NULL : ren1->pair->two, 1);
if (sha_eq(src_other.sha1, null_sha1)) {
if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
ren1->dst_entry->processed = 0;
setup_rename_df_conflict_info(RENAME_DELETE,
ren1->pair,
NULL,
branch1,
branch2,
ren1->dst_entry,
NULL);
} else {
clean_merge = 0;
conflict_rename_delete(o, ren1->pair, branch1, branch2);
}
} else if ((dst_other.mode == ren1->pair->two->mode) &&
sha_eq(dst_other.sha1, ren1->pair->two->sha1)) {
/* Added file on the other side
@ -991,6 +1115,7 @@ static int process_renames(struct merge_options *o,
mfi.sha,
mfi.mode,
ren1_dst);
try_merge = 0;
} else {
new_path = unique_path(o, ren1_dst, branch2);
output(o, 1, "Adding as %s instead", new_path);
@ -1005,13 +1130,12 @@ static int process_renames(struct merge_options *o,
"Rename %s->%s in %s",
ren1_src, ren1_dst, branch1,
ren2->pair->one->path, ren2->pair->two->path, branch2);
conflict_rename_rename_2(o, ren1, branch1, ren2, branch2);
conflict_rename_rename_2to1(o, ren1, branch1, ren2, branch2);
} else
try_merge = 1;
if (try_merge) {
struct diff_filespec *one, *a, *b;
struct merge_file_info mfi;
src_other.path = (char *)ren1_src;
one = ren1->pair->one;
@ -1022,41 +1146,15 @@ static int process_renames(struct merge_options *o,
b = ren1->pair->two;
a = &src_other;
}
mfi = merge_file(o, one, a, b,
o->branch1, o->branch2);
if (mfi.clean &&
sha_eq(mfi.sha, ren1->pair->two->sha1) &&
mfi.mode == ren1->pair->two->mode) {
/*
* This message is part of
* t6022 test. If you change
* it update the test too.
*/
output(o, 3, "Skipped %s (merged same as existing)", ren1_dst);
/* There may be higher stage entries left
* in the index (e.g. due to a D/F
* conflict) that need to be resolved.
*/
if (!ren1->dst_entry->stages[2].mode !=
!ren1->dst_entry->stages[3].mode)
ren1->dst_entry->processed = 0;
} else {
if (mfi.merge || !mfi.clean)
output(o, 1, "Renaming %s => %s", ren1_src, ren1_dst);
if (mfi.merge)
output(o, 2, "Auto-merging %s", ren1_dst);
if (!mfi.clean) {
output(o, 1, "CONFLICT (rename/modify): Merge conflict in %s",
ren1_dst);
clean_merge = 0;
if (!o->call_depth)
update_stages(ren1_dst,
one, a, b, 1);
}
update_file(o, mfi.clean, mfi.sha, mfi.mode, ren1_dst);
update_stages_and_entry(ren1_dst, ren1->dst_entry, one, a, b, 1);
if (string_list_has_string(&o->current_directory_set, ren1_dst)) {
setup_rename_df_conflict_info(RENAME_NORMAL,
ren1->pair,
NULL,
branch1,
NULL,
ren1->dst_entry,
NULL);
}
}
}
@ -1119,6 +1217,90 @@ error_return:
return ret;
}
static void handle_delete_modify(struct merge_options *o,
const char *path,
const char *new_path,
unsigned char *a_sha, int a_mode,
unsigned char *b_sha, int b_mode)
{
if (!a_sha) {
output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
"and modified in %s. Version %s of %s left in tree%s%s.",
path, o->branch1,
o->branch2, o->branch2, path,
path == new_path ? "" : " at ",
path == new_path ? "" : new_path);
update_file(o, 0, b_sha, b_mode, new_path);
} else {
output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
"and modified in %s. Version %s of %s left in tree%s%s.",
path, o->branch2,
o->branch1, o->branch1, path,
path == new_path ? "" : " at ",
path == new_path ? "" : new_path);
update_file(o, 0, a_sha, a_mode, new_path);
}
}
static int merge_content(struct merge_options *o,
const char *path,
unsigned char *o_sha, int o_mode,
unsigned char *a_sha, int a_mode,
unsigned char *b_sha, int b_mode,
const char *df_rename_conflict_branch)
{
const char *reason = "content";
struct merge_file_info mfi;
struct diff_filespec one, a, b;
struct stat st;
unsigned df_conflict_remains = 0;
if (!o_sha) {
reason = "add/add";
o_sha = (unsigned char *)null_sha1;
}
one.path = a.path = b.path = (char *)path;
hashcpy(one.sha1, o_sha);
one.mode = o_mode;
hashcpy(a.sha1, a_sha);
a.mode = a_mode;
hashcpy(b.sha1, b_sha);
b.mode = b_mode;
mfi = merge_file(o, &one, &a, &b, o->branch1, o->branch2);
if (df_rename_conflict_branch &&
lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
df_conflict_remains = 1;
}
if (mfi.clean && !df_conflict_remains &&
sha_eq(mfi.sha, a_sha) && mfi.mode == a.mode)
output(o, 3, "Skipped %s (merged same as existing)", path);
else
output(o, 2, "Auto-merging %s", path);
if (!mfi.clean) {
if (S_ISGITLINK(mfi.mode))
reason = "submodule";
output(o, 1, "CONFLICT (%s): Merge conflict in %s",
reason, path);
}
if (df_conflict_remains) {
const char *new_path;
update_file_flags(o, mfi.sha, mfi.mode, path,
o->call_depth || mfi.clean, 0);
new_path = unique_path(o, path, df_rename_conflict_branch);
mfi.clean = 0;
output(o, 1, "Adding as %s instead", new_path);
update_file_flags(o, mfi.sha, mfi.mode, new_path, 0, 1);
} else {
update_file(o, mfi.clean, mfi.sha, mfi.mode, path);
}
return mfi.clean;
}
/* Per entry merge function */
static int process_entry(struct merge_options *o,
const char *path, struct stage_data *entry)
@ -1136,6 +1318,9 @@ static int process_entry(struct merge_options *o,
unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode);
unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode);
if (entry->rename_df_conflict_info)
return 1; /* Such cases are handled elsewhere. */
entry->processed = 1;
if (o_sha && (!a_sha || !b_sha)) {
/* Case A: Deleted in one */
@ -1148,22 +1333,15 @@ static int process_entry(struct merge_options *o,
output(o, 2, "Removing %s", path);
/* do not touch working file if it did not exist */
remove_file(o, 1, path, !a_sha);
} else if (string_list_has_string(&o->current_directory_set,
path)) {
entry->processed = 0;
return 1; /* Assume clean until processed */
} else {
/* Deleted in one and changed in the other */
clean_merge = 0;
if (!a_sha) {
output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
"and modified in %s. Version %s of %s left in tree.",
path, o->branch1,
o->branch2, o->branch2, path);
update_file(o, 0, b_sha, b_mode, path);
} else {
output(o, 1, "CONFLICT (delete/modify): %s deleted in %s "
"and modified in %s. Version %s of %s left in tree.",
path, o->branch2,
o->branch1, o->branch1, path);
update_file(o, 0, a_sha, a_mode, path);
}
handle_delete_modify(o, path, path,
a_sha, a_mode, b_sha, b_mode);
}
} else if ((!o_sha && a_sha && !b_sha) ||
@ -1182,15 +1360,7 @@ static int process_entry(struct merge_options *o,
if (string_list_has_string(&o->current_directory_set, path)) {
/* Handle D->F conflicts after all subfiles */
entry->processed = 0;
/* But get any file out of the way now, so conflicted
* entries below the directory of the same name can
* be put in the working directory.
*/
if (a_sha)
output(o, 2, "Removing %s", path);
/* do not touch working file if it did not exist */
remove_file(o, 0, path, !a_sha);
return 1; /* Assume clean till processed */
return 1; /* Assume clean until processed */
} else {
output(o, 2, "Adding %s", path);
update_file(o, 1, sha, mode, path);
@ -1198,34 +1368,9 @@ static int process_entry(struct merge_options *o,
} else if (a_sha && b_sha) {
/* Case C: Added in both (check for same permissions) and */
/* case D: Modified in both, but differently. */
const char *reason = "content";
struct merge_file_info mfi;
struct diff_filespec one, a, b;
if (!o_sha) {
reason = "add/add";
o_sha = (unsigned char *)null_sha1;
}
output(o, 2, "Auto-merging %s", path);
one.path = a.path = b.path = (char *)path;
hashcpy(one.sha1, o_sha);
one.mode = o_mode;
hashcpy(a.sha1, a_sha);
a.mode = a_mode;
hashcpy(b.sha1, b_sha);
b.mode = b_mode;
mfi = merge_file(o, &one, &a, &b,
o->branch1, o->branch2);
clean_merge = mfi.clean;
if (!mfi.clean) {
if (S_ISGITLINK(mfi.mode))
reason = "submodule";
output(o, 1, "CONFLICT (%s): Merge conflict in %s",
reason, path);
}
update_file(o, mfi.clean, mfi.sha, mfi.mode, path);
clean_merge = merge_content(o, path,
o_sha, o_mode, a_sha, a_mode, b_sha, b_mode,
NULL);
} else if (!o_sha && !a_sha && !b_sha) {
/*
* this entry was deleted altogether. a_mode == 0 means
@ -1239,13 +1384,19 @@ static int process_entry(struct merge_options *o,
}
/*
* Per entry merge function for D/F conflicts, to be called only after
* all files below dir have been processed. We do this because in the
* cases we can cleanly resolve D/F conflicts, process_entry() can clean
* out all the files below the directory for us.
* Per entry merge function for D/F (and/or rename) conflicts. In the
* cases we can cleanly resolve D/F conflicts, process_entry() can
* clean out all the files below the directory for us. All D/F
* conflict cases must be handled here at the end to make sure any
* directories that can be cleaned out, are.
*
* Some rename conflicts may also be handled here that don't necessarily
* involve D/F conflicts, since the code to handle them is generic enough
* to handle those rename conflicts with or without D/F conflicts also
* being involved.
*/
static int process_df_entry(struct merge_options *o,
const char *path, struct stage_data *entry)
const char *path, struct stage_data *entry)
{
int clean_merge = 1;
unsigned o_mode = entry->stages[1].mode;
@ -1254,43 +1405,91 @@ static int process_df_entry(struct merge_options *o,
unsigned char *o_sha = stage_sha(entry->stages[1].sha, o_mode);
unsigned char *a_sha = stage_sha(entry->stages[2].sha, a_mode);
unsigned char *b_sha = stage_sha(entry->stages[3].sha, b_mode);
const char *add_branch;
const char *other_branch;
unsigned mode;
const unsigned char *sha;
const char *conf;
struct stat st;
/* We currently only handle D->F cases */
assert((!o_sha && a_sha && !b_sha) ||
(!o_sha && !a_sha && b_sha));
entry->processed = 1;
if (a_sha) {
add_branch = o->branch1;
other_branch = o->branch2;
mode = a_mode;
sha = a_sha;
conf = "file/directory";
} else {
add_branch = o->branch2;
other_branch = o->branch1;
mode = b_mode;
sha = b_sha;
conf = "directory/file";
}
if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
const char *new_path = unique_path(o, path, add_branch);
if (entry->rename_df_conflict_info) {
struct rename_df_conflict_info *conflict_info = entry->rename_df_conflict_info;
char *src;
switch (conflict_info->rename_type) {
case RENAME_NORMAL:
clean_merge = merge_content(o, path,
o_sha, o_mode, a_sha, a_mode, b_sha, b_mode,
conflict_info->branch1);
break;
case RENAME_DELETE:
clean_merge = 0;
conflict_rename_delete(o, conflict_info->pair1,
conflict_info->branch1,
conflict_info->branch2);
break;
case RENAME_ONE_FILE_TO_TWO:
src = conflict_info->pair1->one->path;
clean_merge = 0;
output(o, 1, "CONFLICT (rename/rename): "
"Rename \"%s\"->\"%s\" in branch \"%s\" "
"rename \"%s\"->\"%s\" in \"%s\"%s",
src, conflict_info->pair1->two->path, conflict_info->branch1,
src, conflict_info->pair2->two->path, conflict_info->branch2,
o->call_depth ? " (left unresolved)" : "");
if (o->call_depth) {
remove_file_from_cache(src);
update_file(o, 0, conflict_info->pair1->one->sha1,
conflict_info->pair1->one->mode, src);
}
conflict_rename_rename_1to2(o, conflict_info->pair1,
conflict_info->branch1,
conflict_info->pair2,
conflict_info->branch2);
conflict_info->dst_entry2->processed = 1;
break;
default:
entry->processed = 0;
break;
}
} else if (o_sha && (!a_sha || !b_sha)) {
/* Modify/delete; deleted side may have put a directory in the way */
const char *new_path = path;
if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode))
new_path = unique_path(o, path, a_sha ? o->branch1 : o->branch2);
clean_merge = 0;
output(o, 1, "CONFLICT (%s): There is a directory with name %s in %s. "
"Adding %s as %s",
conf, path, other_branch, path, new_path);
remove_file(o, 0, path, 0);
update_file(o, 0, sha, mode, new_path);
handle_delete_modify(o, path, new_path,
a_sha, a_mode, b_sha, b_mode);
} else if (!o_sha && !!a_sha != !!b_sha) {
/* directory -> (directory, file) */
const char *add_branch;
const char *other_branch;
unsigned mode;
const unsigned char *sha;
const char *conf;
if (a_sha) {
add_branch = o->branch1;
other_branch = o->branch2;
mode = a_mode;
sha = a_sha;
conf = "file/directory";
} else {
add_branch = o->branch2;
other_branch = o->branch1;
mode = b_mode;
sha = b_sha;
conf = "directory/file";
}
if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
const char *new_path = unique_path(o, path, add_branch);
clean_merge = 0;
output(o, 1, "CONFLICT (%s): There is a directory with name %s in %s. "
"Adding %s as %s",
conf, path, other_branch, path, new_path);
update_file(o, 0, sha, mode, new_path);
} else {
output(o, 2, "Adding %s", path);
update_file(o, 1, sha, mode, path);
}
} else {
output(o, 2, "Adding %s", path);
update_file(o, 1, sha, mode, path);
entry->processed = 0;
return 1; /* not handled; assume clean until processed */
}
return clean_merge;
@ -1335,6 +1534,7 @@ int merge_trees(struct merge_options *o,
get_files_dirs(o, merge);
entries = get_unmerged();
make_room_for_directories_of_df_conflicts(o, entries);
re_head = get_renames(o, head, common, head, merge, entries);
re_merge = get_renames(o, merge, common, head, merge, entries);
clean = process_renames(o, re_head, re_merge);
@ -1352,6 +1552,12 @@ int merge_trees(struct merge_options *o,
&& !process_df_entry(o, path, e))
clean = 0;
}
for (i = 0; i < entries->nr; i++) {
struct stage_data *e = entries->items[i].util;
if (!e->processed)
die("Unprocessed path??? %s",
entries->items[i].string);
}
string_list_clear(re_merge, 0);
string_list_clear(re_head, 0);

Просмотреть файл

@ -25,6 +25,10 @@ test_expect_success 'setup 1' '
git branch submod &&
git branch copy &&
git branch rename &&
if test_have_prereq SYMLINKS
then
git branch rename-ln
fi &&
echo hello >>a &&
cp a d/e &&
@ -255,7 +259,16 @@ test_expect_success 'setup 8' '
git mv a e &&
git add e &&
test_tick &&
git commit -m "rename a->e"
git commit -m "rename a->e" &&
if test_have_prereq SYMLINKS
then
git checkout rename-ln &&
git mv a e &&
ln -s e a &&
git add a e &&
test_tick &&
git commit -m "rename a->e, symlink a->e"
fi
'
test_expect_success 'setup 9' '
@ -615,4 +628,26 @@ test_expect_success 'merge-recursive copy vs. rename' '
test_cmp expected actual
'
if test_have_prereq SYMLINKS
then
test_expect_success 'merge-recursive rename vs. rename/symlink' '
git checkout -f rename &&
git merge rename-ln &&
( git ls-tree -r HEAD ; git ls-files -s ) >actual &&
(
echo "100644 blob $o0 b"
echo "100644 blob $o0 c"
echo "100644 blob $o0 d/e"
echo "100644 blob $o0 e"
echo "100644 $o0 0 b"
echo "100644 $o0 0 c"
echo "100644 $o0 0 d/e"
echo "100644 $o0 0 e"
) >expected &&
test_cmp expected actual
'
fi
test_done

Просмотреть файл

@ -32,4 +32,70 @@ test_expect_success SYMLINKS 'Cherry-pick succeeds with rename across D/F confli
git cherry-pick branch
'
test_expect_success 'Setup rename with file on one side matching directory name on other' '
git checkout --orphan nick-testcase &&
git rm -rf . &&
>empty &&
git add empty &&
git commit -m "Empty file" &&
git checkout -b simple &&
mv empty file &&
mkdir empty &&
mv file empty &&
git add empty/file &&
git commit -m "Empty file under empty dir" &&
echo content >newfile &&
git add newfile &&
git commit -m "New file"
'
test_expect_success 'Cherry-pick succeeds with was_a_dir/file -> was_a_dir (resolve)' '
git reset --hard &&
git checkout -q nick-testcase^0 &&
git cherry-pick --strategy=resolve simple
'
test_expect_success 'Cherry-pick succeeds with was_a_dir/file -> was_a_dir (recursive)' '
git reset --hard &&
git checkout -q nick-testcase^0 &&
git cherry-pick --strategy=recursive simple
'
test_expect_success 'Setup rename with file on one side matching different dirname on other' '
git reset --hard &&
git checkout --orphan mergeme &&
git rm -rf . &&
mkdir sub &&
mkdir othersub &&
echo content > sub/file &&
echo foo > othersub/whatever &&
git add -A &&
git commit -m "Common commmit" &&
git rm -rf othersub &&
git mv sub/file othersub &&
git commit -m "Commit to merge" &&
git checkout -b newhead mergeme~1 &&
>independent-change &&
git add independent-change &&
git commit -m "Completely unrelated change"
'
test_expect_success 'Cherry-pick with rename to different D/F conflict succeeds (resolve)' '
git reset --hard &&
git checkout -q newhead^0 &&
git cherry-pick --strategy=resolve mergeme
'
test_expect_success 'Cherry-pick with rename to different D/F conflict succeeds (recursive)' '
git reset --hard &&
git checkout -q newhead^0 &&
git cherry-pick --strategy=recursive mergeme
'
test_done

Просмотреть файл

@ -6,19 +6,22 @@
test_description='Test merge with directory/file conflicts'
. ./test-lib.sh
test_expect_success 'prepare repository' \
'echo "Hello" > init &&
git add init &&
git commit -m "Initial commit" &&
git branch B &&
mkdir dir &&
echo "foo" > dir/foo &&
git add dir/foo &&
git commit -m "File: dir/foo" &&
git checkout B &&
echo "file dir" > dir &&
git add dir &&
git commit -m "File: dir"'
test_expect_success 'prepare repository' '
echo Hello >init &&
git add init &&
git commit -m initial &&
git branch B &&
mkdir dir &&
echo foo >dir/foo &&
git add dir/foo &&
git commit -m "File: dir/foo" &&
git checkout B &&
echo file dir >dir &&
git add dir &&
git commit -m "File: dir"
'
test_expect_success 'Merge with d/f conflicts' '
test_expect_code 1 git merge "merge msg" B master
@ -47,4 +50,51 @@ test_expect_success 'F/D conflict' '
git merge master
'
test_expect_success 'setup modify/delete + directory/file conflict' '
git checkout --orphan modify &&
git rm -rf . &&
git clean -fdqx &&
printf "a\nb\nc\nd\ne\nf\ng\nh\n" >letters &&
git add letters &&
git commit -m initial &&
echo i >>letters &&
git add letters &&
git commit -m modified &&
git checkout -b delete HEAD^ &&
git rm letters &&
mkdir letters &&
>letters/file &&
git add letters &&
git commit -m deleted
'
test_expect_success 'modify/delete + directory/file conflict' '
git checkout delete^0 &&
test_must_fail git merge modify &&
test 3 = $(git ls-files -s | wc -l) &&
test 2 = $(git ls-files -u | wc -l) &&
test 1 = $(git ls-files -o | wc -l) &&
test -f letters/file &&
test -f letters~modify
'
test_expect_success 'modify/delete + directory/file conflict; other way' '
git reset --hard &&
git clean -f &&
git checkout modify^0 &&
test_must_fail git merge delete &&
test 3 = $(git ls-files -s | wc -l) &&
test 2 = $(git ls-files -u | wc -l) &&
test 1 = $(git ls-files -o | wc -l) &&
test -f letters/file &&
test -f letters~HEAD
'
test_done

Просмотреть файл

@ -3,6 +3,11 @@
test_description='Merge-recursive merging renames'
. ./test-lib.sh
modify () {
sed -e "$1" <"$2" >"$2.x" &&
mv "$2.x" "$2"
}
test_expect_success setup \
'
cat >A <<\EOF &&
@ -243,4 +248,365 @@ test_expect_success 'merge of identical changes in a renamed file' '
GIT_MERGE_VERBOSITY=3 git merge change+rename | grep "^Skipped B"
'
test_expect_success 'setup for rename + d/f conflicts' '
git reset --hard &&
git checkout --orphan dir-in-way &&
git rm -rf . &&
mkdir sub &&
mkdir dir &&
printf "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n" >sub/file &&
echo foo >dir/file-in-the-way &&
git add -A &&
git commit -m "Common commmit" &&
echo 11 >>sub/file &&
echo more >>dir/file-in-the-way &&
git add -u &&
git commit -m "Commit to merge, with dir in the way" &&
git checkout -b dir-not-in-way &&
git reset --soft HEAD^ &&
git rm -rf dir &&
git commit -m "Commit to merge, with dir removed" -- dir sub/file &&
git checkout -b renamed-file-has-no-conflicts dir-in-way~1 &&
git rm -rf dir &&
git rm sub/file &&
printf "1\n2\n3\n4\n5555\n6\n7\n8\n9\n10\n" >dir &&
git add dir &&
git commit -m "Independent change" &&
git checkout -b renamed-file-has-conflicts dir-in-way~1 &&
git rm -rf dir &&
git mv sub/file dir &&
echo 12 >>dir &&
git add dir &&
git commit -m "Conflicting change"
'
printf "1\n2\n3\n4\n5555\n6\n7\n8\n9\n10\n11\n" >expected
test_expect_success 'Rename+D/F conflict; renamed file merges + dir not in way' '
git reset --hard &&
git checkout -q renamed-file-has-no-conflicts^0 &&
git merge --strategy=recursive dir-not-in-way &&
git diff --quiet &&
test -f dir &&
test_cmp expected dir
'
test_expect_success 'Rename+D/F conflict; renamed file merges but dir in way' '
git reset --hard &&
rm -rf dir~* &&
git checkout -q renamed-file-has-no-conflicts^0 &&
test_must_fail git merge --strategy=recursive dir-in-way >output &&
grep "CONFLICT (delete/modify): dir/file-in-the-way" output &&
grep "Auto-merging dir" output &&
grep "Adding as dir~HEAD instead" output &&
test 2 -eq "$(git ls-files -u | wc -l)" &&
test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
test_must_fail git diff --quiet &&
test_must_fail git diff --cached --quiet &&
test -f dir/file-in-the-way &&
test -f dir~HEAD &&
test_cmp expected dir~HEAD
'
test_expect_success 'Same as previous, but merged other way' '
git reset --hard &&
rm -rf dir~* &&
git checkout -q dir-in-way^0 &&
test_must_fail git merge --strategy=recursive renamed-file-has-no-conflicts >output 2>errors &&
! grep "error: refusing to lose untracked file at" errors &&
grep "CONFLICT (delete/modify): dir/file-in-the-way" output &&
grep "Auto-merging dir" output &&
grep "Adding as dir~renamed-file-has-no-conflicts instead" output &&
test 2 -eq "$(git ls-files -u | wc -l)" &&
test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
test_must_fail git diff --quiet &&
test_must_fail git diff --cached --quiet &&
test -f dir/file-in-the-way &&
test -f dir~renamed-file-has-no-conflicts &&
test_cmp expected dir~renamed-file-has-no-conflicts
'
cat >expected <<\EOF &&
1
2
3
4
5
6
7
8
9
10
<<<<<<< HEAD
12
=======
11
>>>>>>> dir-not-in-way
EOF
test_expect_success 'Rename+D/F conflict; renamed file cannot merge, dir not in way' '
git reset --hard &&
rm -rf dir~* &&
git checkout -q renamed-file-has-conflicts^0 &&
test_must_fail git merge --strategy=recursive dir-not-in-way &&
test 3 -eq "$(git ls-files -u | wc -l)" &&
test 3 -eq "$(git ls-files -u dir | wc -l)" &&
test_must_fail git diff --quiet &&
test_must_fail git diff --cached --quiet &&
test -f dir &&
test_cmp expected dir
'
test_expect_success 'Rename+D/F conflict; renamed file cannot merge and dir in the way' '
modify s/dir-not-in-way/dir-in-way/ expected &&
git reset --hard &&
rm -rf dir~* &&
git checkout -q renamed-file-has-conflicts^0 &&
test_must_fail git merge --strategy=recursive dir-in-way &&
test 5 -eq "$(git ls-files -u | wc -l)" &&
test 3 -eq "$(git ls-files -u dir | grep -v file-in-the-way | wc -l)" &&
test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
test_must_fail git diff --quiet &&
test_must_fail git diff --cached --quiet &&
test -f dir/file-in-the-way &&
test -f dir~HEAD &&
test_cmp expected dir~HEAD
'
cat >expected <<\EOF &&
1
2
3
4
5
6
7
8
9
10
<<<<<<< HEAD
11
=======
12
>>>>>>> renamed-file-has-conflicts
EOF
test_expect_success 'Same as previous, but merged other way' '
git reset --hard &&
rm -rf dir~* &&
git checkout -q dir-in-way^0 &&
test_must_fail git merge --strategy=recursive renamed-file-has-conflicts &&
test 5 -eq "$(git ls-files -u | wc -l)" &&
test 3 -eq "$(git ls-files -u dir | grep -v file-in-the-way | wc -l)" &&
test 2 -eq "$(git ls-files -u dir/file-in-the-way | wc -l)" &&
test_must_fail git diff --quiet &&
test_must_fail git diff --cached --quiet &&
test -f dir/file-in-the-way &&
test -f dir~renamed-file-has-conflicts &&
test_cmp expected dir~renamed-file-has-conflicts
'
test_expect_success 'setup both rename source and destination involved in D/F conflict' '
git reset --hard &&
git checkout --orphan rename-dest &&
git rm -rf . &&
git clean -fdqx &&
mkdir one &&
echo stuff >one/file &&
git add -A &&
git commit -m "Common commmit" &&
git mv one/file destdir &&
git commit -m "Renamed to destdir" &&
git checkout -b source-conflict HEAD~1 &&
git rm -rf one &&
mkdir destdir &&
touch one destdir/foo &&
git add -A &&
git commit -m "Conflicts in the way"
'
test_expect_success 'both rename source and destination involved in D/F conflict' '
git reset --hard &&
rm -rf dir~* &&
git checkout -q rename-dest^0 &&
test_must_fail git merge --strategy=recursive source-conflict &&
test 1 -eq "$(git ls-files -u | wc -l)" &&
test_must_fail git diff --quiet &&
test -f destdir/foo &&
test -f one &&
test -f destdir~HEAD &&
test "stuff" = "$(cat destdir~HEAD)"
'
test_expect_success 'setup pair rename to parent of other (D/F conflicts)' '
git reset --hard &&
git checkout --orphan rename-two &&
git rm -rf . &&
git clean -fdqx &&
mkdir one &&
mkdir two &&
echo stuff >one/file &&
echo other >two/file &&
git add -A &&
git commit -m "Common commmit" &&
git rm -rf one &&
git mv two/file one &&
git commit -m "Rename two/file -> one" &&
git checkout -b rename-one HEAD~1 &&
git rm -rf two &&
git mv one/file two &&
rm -r one &&
git commit -m "Rename one/file -> two"
'
test_expect_success 'pair rename to parent of other (D/F conflicts) w/ untracked dir' '
git checkout -q rename-one^0 &&
mkdir one &&
test_must_fail git merge --strategy=recursive rename-two &&
test 2 -eq "$(git ls-files -u | wc -l)" &&
test 1 -eq "$(git ls-files -u one | wc -l)" &&
test 1 -eq "$(git ls-files -u two | wc -l)" &&
test_must_fail git diff --quiet &&
test 4 -eq $(find . | grep -v .git | wc -l) &&
test -d one &&
test -f one~rename-two &&
test -f two &&
test "other" = $(cat one~rename-two) &&
test "stuff" = $(cat two)
'
test_expect_success 'pair rename to parent of other (D/F conflicts) w/ clean start' '
git reset --hard &&
git clean -fdqx &&
test_must_fail git merge --strategy=recursive rename-two &&
test 2 -eq "$(git ls-files -u | wc -l)" &&
test 1 -eq "$(git ls-files -u one | wc -l)" &&
test 1 -eq "$(git ls-files -u two | wc -l)" &&
test_must_fail git diff --quiet &&
test 3 -eq $(find . | grep -v .git | wc -l) &&
test -f one &&
test -f two &&
test "other" = $(cat one) &&
test "stuff" = $(cat two)
'
test_expect_success 'setup rename of one file to two, with directories in the way' '
git reset --hard &&
git checkout --orphan first-rename &&
git rm -rf . &&
git clean -fdqx &&
echo stuff >original &&
git add -A &&
git commit -m "Common commmit" &&
mkdir two &&
>two/file &&
git add two/file &&
git mv original one &&
git commit -m "Put two/file in the way, rename to one" &&
git checkout -b second-rename HEAD~1 &&
mkdir one &&
>one/file &&
git add one/file &&
git mv original two &&
git commit -m "Put one/file in the way, rename to two"
'
test_expect_success 'check handling of differently renamed file with D/F conflicts' '
git checkout -q first-rename^0 &&
test_must_fail git merge --strategy=recursive second-rename &&
test 5 -eq "$(git ls-files -s | wc -l)" &&
test 3 -eq "$(git ls-files -u | wc -l)" &&
test 1 -eq "$(git ls-files -u one | wc -l)" &&
test 1 -eq "$(git ls-files -u two | wc -l)" &&
test 1 -eq "$(git ls-files -u original | wc -l)" &&
test 2 -eq "$(git ls-files -o | wc -l)" &&
test -f one/file &&
test -f two/file &&
test -f one~HEAD &&
test -f two~second-rename &&
! test -f original
'
test_expect_success 'setup rename one file to two; directories moving out of the way' '
git reset --hard &&
git checkout --orphan first-rename-redo &&
git rm -rf . &&
git clean -fdqx &&
echo stuff >original &&
mkdir one two &&
touch one/file two/file &&
git add -A &&
git commit -m "Common commmit" &&
git rm -rf one &&
git mv original one &&
git commit -m "Rename to one" &&
git checkout -b second-rename-redo HEAD~1 &&
git rm -rf two &&
git mv original two &&
git commit -m "Rename to two"
'
test_expect_success 'check handling of differently renamed file with D/F conflicts' '
git checkout -q first-rename-redo^0 &&
test_must_fail git merge --strategy=recursive second-rename-redo &&
test 3 -eq "$(git ls-files -u | wc -l)" &&
test 1 -eq "$(git ls-files -u one | wc -l)" &&
test 1 -eq "$(git ls-files -u two | wc -l)" &&
test 1 -eq "$(git ls-files -u original | wc -l)" &&
test 0 -eq "$(git ls-files -o | wc -l)" &&
test -f one &&
test -f two &&
! test -f original
'
test_done

Просмотреть файл

@ -70,4 +70,34 @@ test_expect_success 'set merge.renamelimit to 5' '
test_rename 5 ok
test_rename 6 fail
test_expect_success 'setup large simple rename' '
git config --unset merge.renamelimit &&
git config --unset diff.renamelimit &&
git reset --hard initial &&
for i in $(count 200); do
make_text foo bar baz >$i
done &&
git add . &&
git commit -m create-files &&
git branch simple-change &&
git checkout -b simple-rename &&
mkdir builtin &&
git mv [0-9]* builtin/ &&
git commit -m renamed &&
git checkout simple-change &&
>unrelated-change &&
git add unrelated-change &&
git commit -m unrelated-change
'
test_expect_success 'massive simple rename does not spam added files' '
unset GIT_MERGE_VERBOSITY &&
git merge --no-stat simple-rename | grep -v Removing >output &&
test 5 -gt "$(wc -l < output)"
'
test_done

Просмотреть файл

@ -14,7 +14,7 @@ test_description='recursive merge corner cases'
# R1 R2
#
test_expect_success setup '
test_expect_success 'setup basic criss-cross + rename with no modifications' '
ten="0 1 2 3 4 5 6 7 8 9" &&
for i in $ten
do
@ -45,11 +45,190 @@ test_expect_success setup '
git tag R2
'
test_expect_success merge '
test_expect_success 'merge simple rename+criss-cross with no modifications' '
git reset --hard &&
git checkout L2^0 &&
test_must_fail git merge -s recursive R2^0
test_must_fail git merge -s recursive R2^0 &&
test 5 = $(git ls-files -s | wc -l) &&
test 3 = $(git ls-files -u | wc -l) &&
test 0 = $(git ls-files -o | wc -l) &&
test $(git rev-parse :0:one) = $(git rev-parse L2:one) &&
test $(git rev-parse :0:two) = $(git rev-parse R2:two) &&
test $(git rev-parse :2:three) = $(git rev-parse L2:three) &&
test $(git rev-parse :3:three) = $(git rev-parse R2:three) &&
cp two merged &&
>empty &&
test_must_fail git merge-file \
-L "Temporary merge branch 2" \
-L "" \
-L "Temporary merge branch 1" \
merged empty one &&
test $(git rev-parse :1:three) = $(git hash-object merged)
'
#
# Same as before, but modify L1 slightly:
#
# L1m L2
# o---o
# / \ / \
# o X ?
# \ / \ /
# o---o
# R1 R2
#
test_expect_success 'setup criss-cross + rename merges with basic modification' '
git rm -rf . &&
git clean -fdqx &&
rm -rf .git &&
git init &&
ten="0 1 2 3 4 5 6 7 8 9"
for i in $ten
do
echo line $i in a sample file
done >one &&
for i in $ten
do
echo line $i in another sample file
done >two &&
git add one two &&
test_tick && git commit -m initial &&
git branch L1 &&
git checkout -b R1 &&
git mv one three &&
echo more >>two &&
git add two &&
test_tick && git commit -m R1 &&
git checkout L1 &&
git mv two three &&
test_tick && git commit -m L1 &&
git checkout L1^0 &&
test_tick && git merge -s ours R1 &&
git tag L2 &&
git checkout R1^0 &&
test_tick && git merge -s ours L1 &&
git tag R2
'
test_expect_success 'merge criss-cross + rename merges with basic modification' '
git reset --hard &&
git checkout L2^0 &&
test_must_fail git merge -s recursive R2^0 &&
test 5 = $(git ls-files -s | wc -l) &&
test 3 = $(git ls-files -u | wc -l) &&
test 0 = $(git ls-files -o | wc -l) &&
test $(git rev-parse :0:one) = $(git rev-parse L2:one) &&
test $(git rev-parse :0:two) = $(git rev-parse R2:two) &&
test $(git rev-parse :2:three) = $(git rev-parse L2:three) &&
test $(git rev-parse :3:three) = $(git rev-parse R2:three) &&
head -n 10 two >merged &&
cp one merge-me &&
>empty &&
test_must_fail git merge-file \
-L "Temporary merge branch 2" \
-L "" \
-L "Temporary merge branch 1" \
merged empty merge-me &&
test $(git rev-parse :1:three) = $(git hash-object merged)
'
#
# For the next test, we start with three commits in two lines of development
# which setup a rename/add conflict:
# Commit A: File 'a' exists
# Commit B: Rename 'a' -> 'new_a'
# Commit C: Modify 'a', create different 'new_a'
# Later, two different people merge and resolve differently:
# Commit D: Merge B & C, ignoring separately created 'new_a'
# Commit E: Merge B & C making use of some piece of secondary 'new_a'
# Finally, someone goes to merge D & E. Does git detect the conflict?
#
# B D
# o---o
# / \ / \
# A o X ? F
# \ / \ /
# o---o
# C E
#
test_expect_success 'setup differently handled merges of rename/add conflict' '
git rm -rf . &&
git clean -fdqx &&
rm -rf .git &&
git init &&
printf "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n" >a &&
git add a &&
test_tick && git commit -m A &&
git branch B &&
git checkout -b C &&
echo 10 >>a &&
echo "other content" >>new_a &&
git add a new_a &&
test_tick && git commit -m C &&
git checkout B &&
git mv a new_a &&
test_tick && git commit -m B &&
git checkout B^0 &&
test_must_fail git merge C &&
git clean -f &&
test_tick && git commit -m D &&
git tag D &&
git checkout C^0 &&
test_must_fail git merge B &&
rm new_a~HEAD new_a &&
printf "Incorrectly merged content" >>new_a &&
git add -u &&
test_tick && git commit -m E &&
git tag E
'
test_expect_success 'git detects differently handled merges conflict' '
git reset --hard &&
git checkout D^0 &&
git merge -s recursive E^0 && {
echo "BAD: should have conflicted"
test "Incorrectly merged content" = "$(cat new_a)" &&
echo "BAD: Silently accepted wrong content"
return 1
}
test 3 = $(git ls-files -s | wc -l) &&
test 3 = $(git ls-files -u | wc -l) &&
test 0 = $(git ls-files -o | wc -l) &&
test $(git rev-parse :2:new_a) = $(git rev-parse D:new_a) &&
test $(git rev-parse :3:new_a) = $(git rev-parse E:new_a) &&
git cat-file -p B:new_a >>merged &&
git cat-file -p C:new_a >>merge-me &&
>empty &&
test_must_fail git merge-file \
-L "Temporary merge branch 2" \
-L "" \
-L "Temporary merge branch 1" \
merged empty merge-me &&
test $(git rev-parse :1:new_a) = $(git hash-object merged)
'
test_done