# # # add_dir "tests/git_export" # # add_dir "tests/git_export_rename_loop" # # add_file "tests/git_export/__driver__.lua" # content [72fd4cc32028c10e452023b2747d33fd1dde992e] # # add_file "tests/git_export_rename_loop/__driver__.lua" # content [a1e970df185b451d158f71d0897845d9befa1b89] # # patch "Makefile.am" # from [cc098fcd7d979e7002ab949e577ce5d6ba022dfa] # to [cff9b91b5e405dce157c262ae10fbc84400cd99d] # # patch "cmd_othervcs.cc" # from [001f139a078dd171823a09b81f22d65d8ef6159b] # to [8eab7f856845f8b8271ddbed6b5cfd89304ed2c5] # ============================================================ --- tests/git_export/__driver__.lua 72fd4cc32028c10e452023b2747d33fd1dde992e +++ tests/git_export/__driver__.lua 72fd4cc32028c10e452023b2747d33fd1dde992e @@ -0,0 +1,98 @@ +skip_if(not existsonpath("git")) + +mtn_setup() + +writefile("author.map", "address@hidden = \n") + +writefile("file1", "file1") +writefile("file2", "file2") + +check(mtn("add", "file1", "file2"), 0, false, false) +check(mtn("commit", "-m", "add file1 and file2", "-b", "branch1"), 0, false, false) +r1 = base_revision() +check(mtn("tag", r1, "tag1"), 0, false, false) + +writefile("file1", "file1 has changed") +check(mtn("commit", "-m", "edit file1", "-b", "branch1"), 0, false, false) +r2 = base_revision() +check(mtn("tag", r2, "tag2"), 0, false, false) + +check(mtn("update", "-r", r1), 0, false, false) + +check(mtn("rm", "file2"), 0, false, false) +check(mtn("commit", "-m", "remove file2", "-b", "branch2"), 0, false, false) +r3 = base_revision() +check(mtn("tag", r3, "tag3"), 0, false, false) + +check(mtn("mv", "file1", "file-one"), 0, false, false) +check(mtn("commit", "-m", "rename file1 to file-one", "-b", "branch2"), 0, false, false) +r4 = base_revision() +check(mtn("tag", r4, "tag4"), 0, false, false) + +check(mtn("propagate", "branch2", "branch1"), 0, false, false) +check(mtn("update", "-r", "h:branch1"), 0, false, false) +r5 = base_revision() +check(mtn("tag", r5, "tag5"), 0, false, false) + +-- export the monotone history and import it into git + +mkdir("git.dir") +check(mtn("git_export", "--authors-file", "author.map"), 0, true, false) +copy("stdout", "stdin") +check(indir("git.dir", {"git", "init"}), 0, false, false) +check(indir("git.dir", {"git", "fast-import"}), 0, false, false, true) + +-- check the tags we made on each rev above + +check(mtn("co", "-r", "t:tag1", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag1"}), 0, false, false) +check(samefile("mtn.dir/file1", "git.dir/file1")) +check(samefile("mtn.dir/file2", "git.dir/file2")) + +remove("mtn.dir") +check(mtn("co", "-r", "t:tag2", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag2"}), 0, false, false) +check(samefile("mtn.dir/file1", "git.dir/file1")) +check(samefile("mtn.dir/file2", "git.dir/file2")) + +remove("mtn.dir") +check(mtn("co", "-r", "t:tag3", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag3"}), 0, false, false) +check(samefile("mtn.dir/file1", "git.dir/file1")) +check(not exists("mtn.dir/file2")) +check(not exists("git.dir/file2")) + +remove("mtn.dir") +check(mtn("co", "-r", "t:tag4", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag4"}), 0, false, false) +check(samefile("mtn.dir/file-one", "git.dir/file-one")) +check(not exists("mtn.dir/file2")) +check(not exists("git.dir/file2")) + +remove("mtn.dir") +check(mtn("co", "-r", "t:tag5", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag5"}), 0, false, false) +check(samefile("mtn.dir/file-one", "git.dir/file-one")) +check(not exists("mtn.dir/file2")) +check(not exists("git.dir/file2")) + +-- log both repos (mainly for visual inspection) + +check(indir("mtn.dir", mtn("log")), 0, false, false) +check(indir("git.dir", {"git", "log", "--summary", "--pretty=raw"}), 0, false, false) + +-- check branch refs + +remove("mtn.dir") +check(mtn("co", "-r", "h:branch2", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "branch2"}), 0, false, false) +check(samefile("mtn.dir/file-one", "git.dir/file-one")) +check(not exists("mtn.dir/file2")) +check(not exists("git.dir/file2")) + +remove("mtn.dir") +check(mtn("co", "-r", "h:branch1", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "branch1"}), 0, false, false) +check(samefile("mtn.dir/file-one", "git.dir/file-one")) +check(not exists("mtn.dir/file2")) +check(not exists("git.dir/file2")) ============================================================ --- tests/git_export_rename_loop/__driver__.lua a1e970df185b451d158f71d0897845d9befa1b89 +++ tests/git_export_rename_loop/__driver__.lua a1e970df185b451d158f71d0897845d9befa1b89 @@ -0,0 +1,50 @@ +skip_if(not existsonpath("git")) + +mtn_setup() + +writefile("author.map", "address@hidden = \n") + +writefile("file1", "file1") +writefile("file2", "file2") +writefile("file3", "file3") + +check(mtn("add", "file1", "file2", "file3"), 0, false, false) +commit() +r1 = base_revision() +check(mtn("tag", r1, "tag1"), 0, false, false) + +check(mtn("mv", "file1", "tmp"), 0, false, false) +check(mtn("mv", "file3", "file1"), 0, false, false) +check(mtn("mv", "file2", "file3"), 0, false, false) +check(mtn("mv", "tmp", "file2"), 0, false, false) +commit() +r2 = base_revision() +check(mtn("tag", r2, "tag2"), 0, false, false) + +-- export the monotone history and import it into git + +mkdir("git.dir") +check(mtn("git_export", "--authors-file", "author.map"), 0, true, false) +copy("stdout", "stdin") +check(indir("git.dir", {"git", "init"}), 0, false, false) +check(indir("git.dir", {"git", "fast-import"}), 0, false, false, true) + +-- check the tags we made on each rev above + +check(mtn("co", "-r", "t:tag1", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag1"}), 0, false, false) +check(samefile("mtn.dir/file1", "git.dir/file1")) +check(samefile("mtn.dir/file2", "git.dir/file2")) +check(samefile("mtn.dir/file3", "git.dir/file3")) + +remove("mtn.dir") +check(mtn("co", "-r", "t:tag2", "mtn.dir"), 0, false, false) +check(indir("git.dir", {"git", "checkout", "tag2"}), 0, false, false) +check(samefile("mtn.dir/file1", "git.dir/file1")) +check(samefile("mtn.dir/file2", "git.dir/file2")) +check(samefile("mtn.dir/file3", "git.dir/file3")) + +-- log both repos (mainly for visual inspection) + +check(indir("mtn.dir", mtn("log")), 0, false, false) +check(indir("git.dir", {"git", "log", "-M", "--summary", "-p", "--pretty=raw"}), 0, false, false) ============================================================ --- Makefile.am cc098fcd7d979e7002ab949e577ce5d6ba022dfa +++ Makefile.am cff9b91b5e405dce157c262ae10fbc84400cd99d @@ -303,12 +303,12 @@ UNIT_TEST_SOURCES = \ packet.cc paths.cc refiner.cc restrictions.cc rev_height.cc \ revision.cc roster.cc roster_merge.cc simplestring_xform.cc \ string_queue.cc transforms.cc unit_tests.cc uri.cc vocab.cc \ - xdelta.cc + xdelta.cc cmd_othervcs.cc # these files do not contain unit tests, but are required for unit testing # and must be recompiled for that purpose UNIT_TEST_SRC_SUPPORT = \ - roster_delta.cc + roster_delta.cc rcs_import.cc rcs_file.cc # these files do not contain unit tests; they are required for unit # testing, but can be used "as is" from the main build. (many of ============================================================ --- cmd_othervcs.cc 001f139a078dd171823a09b81f22d65d8ef6159b +++ cmd_othervcs.cc 8eab7f856845f8b8271ddbed6b5cfd89304ed2c5 @@ -27,12 +27,15 @@ #include #include +#include using std::cout; using std::map; +using std::make_pair; using std::istringstream; using std::ostringstream; using std::set; +using std::stack; using std::string; using std::vector; @@ -125,6 +128,10 @@ namespace path(path), content(content), mode(mode) {} }; + typedef vector::const_iterator delete_iterator; + typedef vector::const_iterator rename_iterator; + typedef vector::const_iterator add_iterator; + attr_key exe_attr("mtn:execute"); void @@ -218,6 +225,76 @@ namespace } } + // re-order renames so that they occur in the correct order + // i.e. rename a->b + rename b->c will be re-ordered as + // rename b->c + rename a->b + // this will also insert temporary names to resolve circular + // renames and name swaps: + // i.e. rename a->b + rename b->a will be re-ordered as + // rename a->tmp + rename b->a + rename tmp->b + void + reorder_renames(vector & renames) + { + typedef map map_type; + + map_type rename_map; + + for (rename_iterator i = renames.begin(); i != renames.end(); ++i) + rename_map.insert(make_pair(i->old_path, i->new_path)); + + renames.clear(); + + while (!rename_map.empty()) + { + map_type::iterator i = rename_map.begin(); + I(i != rename_map.end()); + file_rename base(i->first, i->second); + rename_map.erase(i); + + map_type::iterator next = rename_map.find(base.new_path); + stack rename_stack; + + // stack renames so their order can be reversed + while (next != rename_map.end()) + { + file_rename rename(next->first, next->second); + rename_stack.push(rename); + rename_map.erase(next); + next = rename_map.find(rename.new_path); + } + + // break rename loops + if (!rename_stack.empty()) + { + file_rename const & top = rename_stack.top(); + // if there is a loop push another rename onto the stack that + // renames the old base to a temporary and adjust the base + // rename to account for this + if (base.old_path == top.new_path) + { + // the temporary path introduced here is pretty weak in + // terms of random filenames but should suffice for the + // already rare situations where any of this is required. + string path = top.new_path.as_internal(); + path += ".tmp.break-rename-loop"; + file_path tmp = file_path_internal(path); + rename_stack.push(file_rename(base.old_path, tmp)); + base.old_path = tmp; + } + } + + // insert the stacked renames in reverse order + while (!rename_stack.empty()) + { + file_rename rename = rename_stack.top(); + rename_stack.pop(); + renames.push_back(rename); + } + + renames.push_back(base); + } + } + }; static void @@ -449,11 +526,9 @@ CMD(git_export, "git_export", "", CMD_RE vector renames; vector additions; - typedef vector::const_iterator delete_iterator; - typedef vector::const_iterator rename_iterator; - typedef vector::const_iterator add_iterator; + get_changes(old_roster, new_roster, deletions, renames, additions); - get_changes(old_roster, new_roster, deletions, renames, additions); + reorder_renames(renames); // emit file data blobs for modified and added files @@ -522,7 +597,6 @@ CMD(git_export, "git_export", "", CMD_RE for (delete_iterator i = deletions.begin(); i != deletions.end(); ++i) cout << "D " << quote_path(i->path) << "\n"; - // FIXME: handle rename ordering issues for (rename_iterator i = renames.begin(); i != renames.end(); ++i) cout << "R " << quote_path(i->old_path) << " " @@ -606,6 +680,106 @@ CMD(git_export, "git_export", "", CMD_RE } } +#ifdef BUILD_UNIT_TESTS + +#include "unit_tests.hh" + +UNIT_TEST(git_rename_reordering, reorder_chained_renames) +{ + vector renames; + renames.push_back(file_rename(file_path_internal("a"), file_path_internal("b"))); + renames.push_back(file_rename(file_path_internal("b"), file_path_internal("c"))); + renames.push_back(file_rename(file_path_internal("c"), file_path_internal("d"))); + + // these should be reordered from a->b b->c c->d to c->d b->c a->b + reorder_renames(renames); + rename_iterator rename = renames.begin(); + UNIT_TEST_CHECK(rename->old_path == file_path_internal("c")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("d")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("b")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("c")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("a")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("b")); + ++rename; + UNIT_TEST_CHECK(rename == renames.end()); +} + +UNIT_TEST(git_rename_reordering, reorder_swapped_renames) +{ + vector renames; + renames.push_back(file_rename(file_path_internal("a"), file_path_internal("b"))); + renames.push_back(file_rename(file_path_internal("b"), file_path_internal("a"))); + + // these should be reordered from a->b b->a to a->tmp b->a tmp->b + reorder_renames(renames); + rename_iterator rename = renames.begin(); + UNIT_TEST_CHECK(rename->old_path == file_path_internal("a")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("a.tmp.break-rename-loop")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("b")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("a")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("a.tmp.break-rename-loop")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("b")); + ++rename; + UNIT_TEST_CHECK(rename == renames.end()); +} + +UNIT_TEST(git_rename_reordering, reorder_rename_loop) +{ + vector renames; + renames.push_back(file_rename(file_path_internal("a"), file_path_internal("b"))); + renames.push_back(file_rename(file_path_internal("b"), file_path_internal("c"))); + renames.push_back(file_rename(file_path_internal("c"), file_path_internal("a"))); + + // these should be reordered from a->b b->c c->a to a->tmp c->a b->c a->b tmp->b + reorder_renames(renames); + rename_iterator rename = renames.begin(); + UNIT_TEST_CHECK(rename->old_path == file_path_internal("a")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("a.tmp.break-rename-loop")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("c")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("a")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("b")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("c")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("a.tmp.break-rename-loop")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("b")); + ++rename; + UNIT_TEST_CHECK(rename == renames.end()); +} + +UNIT_TEST(git_rename_reordering, reorder_reversed_rename_loop) +{ + vector renames; + renames.push_back(file_rename(file_path_internal("z"), file_path_internal("y"))); + renames.push_back(file_rename(file_path_internal("y"), file_path_internal("x"))); + renames.push_back(file_rename(file_path_internal("x"), file_path_internal("z"))); + + // assuming that the x->z rename gets pulled from the rename map first + // these should be reordered from z->y y->x x->z to x->tmp y->x z->y tmp->z + reorder_renames(renames); + rename_iterator rename = renames.begin(); + UNIT_TEST_CHECK(rename->old_path == file_path_internal("x")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("x.tmp.break-rename-loop")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("y")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("x")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("z")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("y")); + ++rename; + UNIT_TEST_CHECK(rename->old_path == file_path_internal("x.tmp.break-rename-loop")); + UNIT_TEST_CHECK(rename->new_path == file_path_internal("z")); + ++rename; + UNIT_TEST_CHECK(rename == renames.end()); +} + +#endif // BUILD_UNIT_TESTS + // Local Variables: // mode: C++ // fill-column: 76