From f15652d90c37ca56db08d325c871f2cc8f7d509e Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 9 Jul 2010 07:10:51 -0600 Subject: [PATCH 1/6] Add additional testcases for D/F conflicts Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- t/t6035-merge-dir-to-symlink.sh | 64 ++++++++++++++++++++++++++++++--- t/t9350-fast-export.sh | 24 +++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/t/t6035-merge-dir-to-symlink.sh b/t/t6035-merge-dir-to-symlink.sh index 3202e1de6d..761ad9d154 100755 --- a/t/t6035-merge-dir-to-symlink.sh +++ b/t/t6035-merge-dir-to-symlink.sh @@ -48,7 +48,7 @@ test_expect_success 'setup for merge test' ' git tag baseline ' -test_expect_success 'do not lose a/b-2/c/d in merge (resolve)' ' +test_expect_success 'Handle D/F conflict, do not lose a/b-2/c/d in merge (resolve)' ' git reset --hard && git checkout baseline^0 && git merge -s resolve master && @@ -56,7 +56,7 @@ test_expect_success 'do not lose a/b-2/c/d in merge (resolve)' ' test -f a/b-2/c/d ' -test_expect_failure 'do not lose a/b-2/c/d in merge (recursive)' ' +test_expect_failure 'Handle D/F conflict, do not lose a/b-2/c/d in merge (recursive)' ' git reset --hard && git checkout baseline^0 && git merge -s recursive master && @@ -64,6 +64,54 @@ test_expect_failure 'do not lose a/b-2/c/d in merge (recursive)' ' test -f a/b-2/c/d ' +test_expect_success 'Handle F/D conflict, do not lose a/b-2/c/d in merge (resolve)' ' + git reset --hard && + git checkout master^0 && + git merge -s resolve baseline^0 && + test -h a/b && + test -f a/b-2/c/d +' + +test_expect_failure 'Handle F/D conflict, do not lose a/b-2/c/d in merge (recursive)' ' + git reset --hard && + git checkout master^0 && + git merge -s recursive baseline^0 && + test -h a/b && + test -f a/b-2/c/d +' + +test_expect_failure 'do not lose untracked in merge (resolve)' ' + git reset --hard && + git checkout baseline^0 && + >a/b/c/e && + test_must_fail git merge -s resolve master && + test -f a/b/c/e && + test -f a/b-2/c/d +' + +test_expect_success 'do not lose untracked in merge (recursive)' ' + git reset --hard && + git checkout baseline^0 && + >a/b/c/e && + test_must_fail git merge -s recursive master && + test -f a/b/c/e && + test -f a/b-2/c/d +' + +test_expect_success 'do not lose modifications in merge (resolve)' ' + git reset --hard && + git checkout baseline^0 && + echo more content >>a/b/c/d && + test_must_fail git merge -s resolve master +' + +test_expect_success 'do not lose modifications in merge (recursive)' ' + git reset --hard && + git checkout baseline^0 && + echo more content >>a/b/c/d && + test_must_fail git merge -s recursive master +' + test_expect_success 'setup a merge where dir a/b-2 changed to symlink' ' git reset --hard && git checkout start^0 && @@ -74,7 +122,7 @@ test_expect_success 'setup a merge where dir a/b-2 changed to symlink' ' git tag test2 ' -test_expect_success 'merge should not have conflicts (resolve)' ' +test_expect_success 'merge should not have D/F conflicts (resolve)' ' git reset --hard && git checkout baseline^0 && git merge -s resolve test2 && @@ -82,7 +130,7 @@ test_expect_success 'merge should not have conflicts (resolve)' ' test -f a/b/c/d ' -test_expect_failure 'merge should not have conflicts (recursive)' ' +test_expect_failure 'merge should not have D/F conflicts (recursive)' ' git reset --hard && git checkout baseline^0 && git merge -s recursive test2 && @@ -90,4 +138,12 @@ test_expect_failure 'merge should not have conflicts (recursive)' ' test -f a/b/c/d ' +test_expect_failure 'merge should not have F/D conflicts (recursive)' ' + git reset --hard && + git checkout -b foo test2 && + git merge -s recursive baseline^0 && + test -h a/b-2 && + test -f a/b/c/d +' + test_done diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index d43f37ccaf..69179c6124 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -376,4 +376,28 @@ test_expect_success 'tree_tag-obj' 'git fast-export tree_tag-obj' test_expect_success 'tag-obj_tag' 'git fast-export tag-obj_tag' test_expect_success 'tag-obj_tag-obj' 'git fast-export tag-obj_tag-obj' +test_expect_failure 'directory becomes symlink' ' + git init dirtosymlink && + git init result && + ( + cd dirtosymlink && + mkdir foo && + mkdir bar && + echo hello > foo/world && + echo hello > bar/world && + git add foo/world bar/world && + git commit -q -mone && + git rm -r foo && + ln -s bar foo && + git add foo && + git commit -q -mtwo + ) && + ( + cd dirtosymlink && + git fast-export master -- foo | + (cd ../result && git fast-import --quiet) + ) && + (cd result && git show master:foo) +' + test_done From f433f70547ebf038a862ca72565ae42e50d4adfa Mon Sep 17 00:00:00 2001 From: Alexander Gladysh Date: Fri, 9 Jul 2010 07:10:52 -0600 Subject: [PATCH 2/6] Add a rename + D/F conflict testcase This is a simple testcase where both sides of the rename are paths involved in (separate) D/F merge conflicts Signed-off-by: Alexander Gladysh Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- t/t3509-cherry-pick-merge-df.sh | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 t/t3509-cherry-pick-merge-df.sh diff --git a/t/t3509-cherry-pick-merge-df.sh b/t/t3509-cherry-pick-merge-df.sh new file mode 100755 index 0000000000..7c05e16843 --- /dev/null +++ b/t/t3509-cherry-pick-merge-df.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +test_description='Test cherry-pick with directory/file conflicts' +. ./test-lib.sh + +test_expect_success 'Setup rename across paths each below D/F conflicts' ' + mkdir a && + >a/f && + git add a && + git commit -m a && + + mkdir b && + ln -s ../a b/a && + git add b && + git commit -m b && + + git checkout -b branch && + rm b/a && + mv a b/a && + ln -s b/a a && + git add . && + git commit -m swap && + + >f1 && + git add f1 && + git commit -m f1 +' + +test_expect_failure 'Cherry-pick succeeds with rename across D/F conflicts' ' + git reset --hard && + git checkout master^0 && + git cherry-pick branch +' + +test_done From 37348937ff391f01981e8af10b2f615268fd2509 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 9 Jul 2010 07:10:53 -0600 Subject: [PATCH 3/6] merge-recursive: Fix D/F conflicts The D/F conflicts that can be automatically resolved (file or directory unmodified on one side of history), have the nice property that process_entry() can correctly handle all subpaths of the D/F conflict. In the case of D->F conversions, it will correctly delete all non-conflicting files below the relevant directory and the directory itself (note that both untracked and conflicting files below the directory will prevent its removal). So if we handle D/F conflicts after all other conflicts, they become fairly simple to handle -- we just need to check for whether or not a path (file/directory) is in the way of creating the new content. We do this by having process_entry() defer handling such entries to a subsequent process_df_entry() step. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-recursive.c | 93 +++++++++++++++++++++++++++------ t/t6035-merge-dir-to-symlink.sh | 6 +-- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/merge-recursive.c b/merge-recursive.c index 917397ca7a..c8d5362191 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -1072,6 +1072,7 @@ 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); + entry->processed = 1; if (o_sha && (!a_sha || !b_sha)) { /* Case A: Deleted in one */ if ((!a_sha && !b_sha) || @@ -1104,33 +1105,28 @@ static int process_entry(struct merge_options *o, } else if ((!o_sha && a_sha && !b_sha) || (!o_sha && !a_sha && b_sha)) { /* Case B: Added in one. */ - 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 (string_list_has_string(&o->current_directory_set, path)) { - 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); - remove_file(o, 0, path, 0); - update_file(o, 0, sha, mode, new_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 */ } else { output(o, 2, "Adding %s", path); update_file(o, 1, sha, mode, path); @@ -1178,6 +1174,64 @@ static int process_entry(struct merge_options *o, return clean_merge; } +/* + * 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. + */ +static int process_df_entry(struct merge_options *o, + const char *path, struct stage_data *entry) +{ + int clean_merge = 1; + unsigned o_mode = entry->stages[1].mode; + unsigned a_mode = entry->stages[2].mode; + unsigned b_mode = entry->stages[3].mode; + 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); + 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); + } else { + output(o, 2, "Adding %s", path); + update_file(o, 1, sha, mode, path); + } + + return clean_merge; +} + struct unpack_trees_error_msgs get_porcelain_error_msgs(void) { struct unpack_trees_error_msgs msgs = { @@ -1249,6 +1303,13 @@ int merge_trees(struct merge_options *o, && !process_entry(o, path, e)) clean = 0; } + for (i = 0; i < entries->nr; i++) { + const char *path = entries->items[i].string; + struct stage_data *e = entries->items[i].util; + if (!e->processed + && !process_df_entry(o, path, e)) + clean = 0; + } string_list_clear(re_merge, 0); string_list_clear(re_head, 0); diff --git a/t/t6035-merge-dir-to-symlink.sh b/t/t6035-merge-dir-to-symlink.sh index 761ad9d154..f6972bed06 100755 --- a/t/t6035-merge-dir-to-symlink.sh +++ b/t/t6035-merge-dir-to-symlink.sh @@ -56,7 +56,7 @@ test_expect_success 'Handle D/F conflict, do not lose a/b-2/c/d in merge (resolv test -f a/b-2/c/d ' -test_expect_failure 'Handle D/F conflict, do not lose a/b-2/c/d in merge (recursive)' ' +test_expect_success 'Handle D/F conflict, do not lose a/b-2/c/d in merge (recursive)' ' git reset --hard && git checkout baseline^0 && git merge -s recursive master && @@ -130,7 +130,7 @@ test_expect_success 'merge should not have D/F conflicts (resolve)' ' test -f a/b/c/d ' -test_expect_failure 'merge should not have D/F conflicts (recursive)' ' +test_expect_success 'merge should not have D/F conflicts (recursive)' ' git reset --hard && git checkout baseline^0 && git merge -s recursive test2 && @@ -138,7 +138,7 @@ test_expect_failure 'merge should not have D/F conflicts (recursive)' ' test -f a/b/c/d ' -test_expect_failure 'merge should not have F/D conflicts (recursive)' ' +test_expect_success 'merge should not have F/D conflicts (recursive)' ' git reset --hard && git checkout -b foo test2 && git merge -s recursive baseline^0 && From 5a2580d62f9aaa30f0acd10df9dbe7a581dd77d9 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 9 Jul 2010 07:10:54 -0600 Subject: [PATCH 4/6] merge_recursive: Fix renames across paths below D/F conflicts The rename logic in process_renames() handles renames and merging of file contents and then marks files as processed. However, there may be higher stage entries left in the index for other reasons (e.g., due to D/F conflicts). By checking for such cases and marking the entry as not processed, it allows process_entry() later to look at it and handle those higher stages. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-recursive.c | 15 +++++++++++++-- t/t3509-cherry-pick-merge-df.sh | 2 +- t/t6020-merge-df.sh | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/merge-recursive.c b/merge-recursive.c index c8d5362191..b0f055ecd4 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -1019,14 +1019,25 @@ static int process_renames(struct merge_options *o, if (mfi.clean && sha_eq(mfi.sha, ren1->pair->two->sha1) && - mfi.mode == ren1->pair->two->mode) + mfi.mode == ren1->pair->two->mode) { /* * This messaged is part of * t6022 test. If you change * it update the test too. */ output(o, 3, "Skipped %s (merged same as existing)", ren1_dst); - else { + + /* There may be higher stage entries left + * in the index (e.g. due to a D/F + * conflict) that need to be resolved. + */ + for (i = 1; i <= 3; i++) { + if (!ren1->dst_entry->stages[i].mode) + continue; + ren1->dst_entry->processed = 0; + break; + } + } else { if (mfi.merge || !mfi.clean) output(o, 1, "Renaming %s => %s", ren1_src, ren1_dst); if (mfi.merge) diff --git a/t/t3509-cherry-pick-merge-df.sh b/t/t3509-cherry-pick-merge-df.sh index 7c05e16843..6e7ef8483f 100755 --- a/t/t3509-cherry-pick-merge-df.sh +++ b/t/t3509-cherry-pick-merge-df.sh @@ -26,7 +26,7 @@ test_expect_success 'Setup rename across paths each below D/F conflicts' ' git commit -m f1 ' -test_expect_failure 'Cherry-pick succeeds with rename across D/F conflicts' ' +test_expect_success 'Cherry-pick succeeds with rename across D/F conflicts' ' git reset --hard && git checkout master^0 && git cherry-pick branch diff --git a/t/t6020-merge-df.sh b/t/t6020-merge-df.sh index e71c687f2b..490d397114 100755 --- a/t/t6020-merge-df.sh +++ b/t/t6020-merge-df.sh @@ -22,7 +22,7 @@ git commit -m "File: dir"' test_expect_code 1 'Merge with d/f conflicts' 'git merge "merge msg" B master' -test_expect_failure 'F/D conflict' ' +test_expect_success 'F/D conflict' ' git reset --hard && git checkout master && rm .git/index && From 060df624228187c77ca53a437ae0e9896076f045 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 9 Jul 2010 07:10:55 -0600 Subject: [PATCH 5/6] fast-export: Fix output order of D/F changes The fast-import stream format requires incremental changes which take place immediately, meaning that for D->F conversions all files below the relevant directory must be deleted before the resulting file of the same name is created. Reversing the order can result in fast-import silently deleting the file right after creating it, resulting in the file missing from the resulting repository. We correct this by first sorting the diff_queue_struct in depth-first order. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- builtin/fast-export.c | 29 +++++++++++++++++++++++++++++ t/t9350-fast-export.sh | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/builtin/fast-export.c b/builtin/fast-export.c index c6dd71a7bc..965e90e5e8 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -147,10 +147,39 @@ static void handle_object(const unsigned char *sha1) free(buf); } +static int depth_first(const void *a_, const void *b_) +{ + const struct diff_filepair *a = *((const struct diff_filepair **)a_); + const struct diff_filepair *b = *((const struct diff_filepair **)b_); + const char *name_a, *name_b; + int len_a, len_b, len; + int cmp; + + name_a = a->one ? a->one->path : a->two->path; + name_b = b->one ? b->one->path : b->two->path; + + len_a = strlen(name_a); + len_b = strlen(name_b); + len = (len_a < len_b) ? len_a : len_b; + + /* strcmp will sort 'd' before 'd/e', we want 'd/e' before 'd' */ + cmp = memcmp(name_a, name_b, len); + if (cmp) + return cmp; + return (len_b - len_a); +} + static void show_filemodify(struct diff_queue_struct *q, struct diff_options *options, void *data) { int i; + + /* + * Handle files below a directory first, in case they are all deleted + * and the directory changes to a file or symlink. + */ + qsort(q->queue, q->nr, sizeof(q->queue[0]), depth_first); + for (i = 0; i < q->nr; i++) { struct diff_filespec *ospec = q->queue[i]->one; struct diff_filespec *spec = q->queue[i]->two; diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index 69179c6124..1ee1461c9b 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -376,7 +376,7 @@ test_expect_success 'tree_tag-obj' 'git fast-export tree_tag-obj' test_expect_success 'tag-obj_tag' 'git fast-export tag-obj_tag' test_expect_success 'tag-obj_tag-obj' 'git fast-export tag-obj_tag-obj' -test_expect_failure 'directory becomes symlink' ' +test_expect_success 'directory becomes symlink' ' git init dirtosymlink && git init result && ( From 253fb5f8897d988d93ce276f8147c2964da3eefb Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Fri, 9 Jul 2010 07:10:56 -0600 Subject: [PATCH 6/6] fast-import: Improve robustness when D->F changes provided in wrong order When older versions of fast-export came across a directory changing to a symlink (or regular file), it would output the changes in the form M 120000 :239821 dir-changing-to-symlink D dir-changing-to-symlink/filename1 When fast-import sees the first line, it deletes the directory named dir-changing-to-symlink (and any files below it) and creates a symlink in its place. When fast-import came across the second line, it was previously trying to remove the file and relevant leading directories in tree_content_remove(), and as a side effect it would delete the symlink that was just created. This resulted in the symlink silently missing from the resulting repository. To improve robustness, we ignore file deletions underneath directory names that correspond to non-directories. This can also be viewed as a minor optimization: since there cannot be a file and a directory with the same name in the same directory, the file clearly can't exist so nothing needs to be done to delete it. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- fast-import.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fast-import.c b/fast-import.c index 309f2c58a2..75ed738b7c 100644 --- a/fast-import.c +++ b/fast-import.c @@ -1528,6 +1528,14 @@ static int tree_content_remove( for (i = 0; i < t->entry_count; i++) { e = t->entries[i]; if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (slash1 && !S_ISDIR(e->versions[1].mode)) + /* + * If p names a file in some subdirectory, and a + * file or symlink matching the name of the + * parent directory of p exists, then p cannot + * exist and need not be deleted. + */ + return 1; if (!slash1 || !S_ISDIR(e->versions[1].mode)) goto del_entry; if (!e->tree)