зеркало из https://github.com/microsoft/git.git
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:
Коммит
5acb623b72
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче