# # # add_file "tests/resolve_duplicate_name_conflict/expected-merge-messages-abe_1-beth_1" # content [8cc5c13905078a96dd1e308964537c69ed78f4e8] # # add_file "tests/resolve_duplicate_name_conflict/expected-merge-messages-abe_2-jim_1" # content [318b3562e18678195b6209aedc9a157a8f2937e2] # # add_file "tests/resolve_duplicate_name_conflict/expected-merged-revision-jim_1" # content [257729bebdb32819cd1fc059806e0fb4144f7ec7] # # add_file "tests/resolve_duplicate_name_conflict/expected-update-messages-jim_1" # content [79eb4985a0fdab8524e19de458835db8b9dcf51c] # # patch "basic_io.cc" # from [5aacaa8dcf17e4113dbaf2e91b476a9b795ccbf5] # to [f9f5453f817910a7ac6cfc1ec0591b7d938642b5] # # patch "basic_io.hh" # from [df2ce0a862161cd5c0d7061b75fe3804d1cfec87] # to [6c148df1eafd7316edd472e231541f98c62fcb0b] # # patch "cset.cc" # from [a2cb867731e1d283fa60165e534157d0b38fb364] # to [f048d2d67e8a0730dcdc0e18e04f5f80a306c2fd] # # patch "cset.hh" # from [53dcfec8e054e927c0615188e716548390460a3f] # to [5f09b70523f6d8afad6f5b37c8edc90e99d7a979] # # patch "rev_types.hh" # from [9a3a1a69643278a8b2c5c6c85d25623ae079227c] # to [4cb4ab589c482cdd0d4afb9e23a53d2110011ba6] # # patch "roster.cc" # from [c7cdd1cb8e944b19a946cac03311b025c621bb85] # to [9eb37f9fb2be303fc606c698427c3450b77c22d6] # # patch "roster.hh" # from [e0d2b16d79c0d818fb951f7b32726d6eaebdb725] # to [249507b96d4cd90e81b8903cbcbb044f0c40510a] # # patch "roster_merge.cc" # from [178917d07e72bb9cff7c9616e0e25e0b89eccdfb] # to [7213f45b1eaf58a223235a3569e07ba8d724e4bd] # # patch "tests/resolve_duplicate_name_conflict/__driver__.lua" # from [5bcba012902ee356d5ef041847cef3aa725c53c1] # to [8bde1a08cec21f13e69bbb1ccae604dc3d5146d7] # # patch "work.cc" # from [0261cdf7dbc3baed0cdfc0e75a3a858f64eefa97] # to [a01164a4e32079091f126c5018a187cea96e0dcd] # ============================================================ --- tests/resolve_duplicate_name_conflict/expected-merge-messages-abe_1-beth_1 8cc5c13905078a96dd1e308964537c69ed78f4e8 +++ tests/resolve_duplicate_name_conflict/expected-merge-messages-abe_1-beth_1 8cc5c13905078a96dd1e308964537c69ed78f4e8 @@ -0,0 +1,8 @@ +mtn: 2 heads on branch 'testbranch' +mtn: [left] 5285636b9d9f988e79b3dcd9a40e64d15fb7fc9f +mtn: [right] e9ad84a3fc40ef1109251c308428439c21ad1de9 +mtn: suturing checkout.sh, checkout.sh into checkout.sh +mtn: renaming thermostat.c to thermostat-westinghouse.c +mtn: renaming thermostat.c to thermostat-honeywell.c +mtn: [merged] 257729bebdb32819cd1fc059806e0fb4144f7ec7 +mtn: note: your workspaces have not been updated ============================================================ --- tests/resolve_duplicate_name_conflict/expected-merge-messages-abe_2-jim_1 318b3562e18678195b6209aedc9a157a8f2937e2 +++ tests/resolve_duplicate_name_conflict/expected-merge-messages-abe_2-jim_1 318b3562e18678195b6209aedc9a157a8f2937e2 @@ -0,0 +1 @@ +should get suture/edit conflict, or just merge ============================================================ --- tests/resolve_duplicate_name_conflict/expected-merged-revision-jim_1 257729bebdb32819cd1fc059806e0fb4144f7ec7 +++ tests/resolve_duplicate_name_conflict/expected-merged-revision-jim_1 257729bebdb32819cd1fc059806e0fb4144f7ec7 @@ -0,0 +1,29 @@ +format_version "2" + +new_manifest [9a3352571f7379d5114aa57fb650fe2acb7588f6] + +old_revision [5285636b9d9f988e79b3dcd9a40e64d15fb7fc9f] + +rename "thermostat.c" + to "thermostat-westinghouse.c" + +add_file "thermostat-honeywell.c" + content [770bfb0965db64d8b7a267f61ebd50803f27d5e7] + + sutured_file "checkout.sh" + first_ancestor "checkout.sh" +second_ancestor "" + content [3390893b9a31eaa9ef0e9364b27ee1e617e6891a] + +old_revision [e9ad84a3fc40ef1109251c308428439c21ad1de9] + +rename "thermostat.c" + to "thermostat-honeywell.c" + +add_file "thermostat-westinghouse.c" + content [c2f67aa3b29c2bdab4790213c7f3bf73e58440a7] + + sutured_file "checkout.sh" + first_ancestor "checkout.sh" +second_ancestor "" + content [3390893b9a31eaa9ef0e9364b27ee1e617e6891a] ============================================================ --- tests/resolve_duplicate_name_conflict/expected-update-messages-jim_1 79eb4985a0fdab8524e19de458835db8b9dcf51c +++ tests/resolve_duplicate_name_conflict/expected-update-messages-jim_1 79eb4985a0fdab8524e19de458835db8b9dcf51c @@ -0,0 +1,13 @@ +mtn: updating along branch 'testbranch' +mtn: selected update target 257729bebdb32819cd1fc059806e0fb4144f7ec7 +mtn: [left] 30065575a95fc60f8deb84b15d9b8c8b3944d37c +mtn: [right] 257729bebdb32819cd1fc059806e0fb4144f7ec7 +mtn: warning: Content changes to the file 'checkout.sh' +mtn: warning: will be ignored during this merge as the file has been +mtn: warning: removed on one side of the merge. Affected revisions include: +mtn: warning: Revision: 30065575a95fc60f8deb84b15d9b8c8b3944d37c +mtn: adding checkout.sh +mtn: renaming thermostat.c to thermostat-honeywell.c +mtn: adding thermostat-westinghouse.c +mtn: dropping checkout.sh +mtn: updated to base revision 257729bebdb32819cd1fc059806e0fb4144f7ec7 ============================================================ --- basic_io.cc 5aacaa8dcf17e4113dbaf2e91b476a9b795ccbf5 +++ basic_io.cc f9f5453f817910a7ac6cfc1ec0591b7d938642b5 @@ -1,3 +1,4 @@ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2004 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -129,6 +130,22 @@ void basic_io::stanza::push_str_multi(sy indent = k().size(); } +void basic_io::stanza::push_str_multi(symbol const & k1, + symbol const & k2, + vector const & v) +{ + string val = k2(); + for (vector::const_iterator i = v.begin(); + i != v.end(); ++i) + { + val += " "; + val += escape(*i); + } + entries.push_back(make_pair(k1, val)); + if (k1().size() > indent) + indent = k1().size(); +} + void basic_io::stanza::push_str_triple(symbol const & k, string const & n, string const & v) ============================================================ --- basic_io.hh df2ce0a862161cd5c0d7061b75fe3804d1cfec87 +++ basic_io.hh 6c148df1eafd7316edd472e231541f98c62fcb0b @@ -1,6 +1,7 @@ #ifndef __BASIC_IO_HH__ #define __BASIC_IO_HH__ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2004 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -34,21 +35,13 @@ namespace basic_io // general format symbol symbol const format_version("format_version"); - // roster symbols + // common symbols symbol const dir("dir"); symbol const file("file"); symbol const content("content"); symbol const attr("attr"); - // 'local' roster and marking symbols - // FIXME: should these be listed as "general" symbols here as well? - symbol const ident("ident"); - symbol const birth("birth"); - symbol const dormant_attr("dormant_attr"); - - symbol const path_mark("path_mark"); symbol const content_mark("content_mark"); - symbol const attr_mark("attr_mark"); } } @@ -264,6 +257,9 @@ namespace basic_io void push_file_pair(symbol const & k, file_path const & v); void push_str_multi(symbol const & k, std::vector const & v); + void push_str_multi(symbol const & k1, + symbol const & k2, + std::vector const & v); }; ============================================================ --- cset.cc a2cb867731e1d283fa60165e534157d0b38fb364 +++ cset.cc f048d2d67e8a0730dcdc0e18e04f5f80a306c2fd @@ -178,7 +178,7 @@ cset::apply_to(editable_tree & t) const for (map::const_iterator i = files_added.begin(); i != files_added.end(); ++i) - safe_insert(attaches, attach(t.create_file_node(i->second), i->first)); + safe_insert(attaches, attach(t.create_file_node(i->second, null_ancestors), i->first)); // Decompose all path deletion and the first-half of renamings on // existing paths into the set of pending detaches, to be executed @@ -187,8 +187,43 @@ cset::apply_to(editable_tree & t) const for (map::const_iterator i = nodes_sutured.begin(); i != nodes_sutured.end(); ++i) { + node_id left_anc_nid; + node_id right_anc_nid; + + std::pair ancestors; + + // get_node ("") returns the root node, which is not what we want. + if (i->second.first_ancestor.empty()) + left_anc_nid = the_null_node; + else + left_anc_nid = t.get_node(i->second.first_ancestor); + + if (i->second.second_ancestor.empty()) + right_anc_nid = the_null_node; + else + right_anc_nid = t.get_node(i->second.second_ancestor); + + if (right_anc_nid == the_null_node) + { + // suture is from merge; t.r is left side + ancestors.first = left_anc_nid; + ancestors.second = the_null_node; + } + else if (left_anc_nid == the_null_node) + { + // suture is from merge; t.r is right side + ancestors.first = right_anc_nid; + ancestors.second = the_null_node; + } + else + { + // user suture + ancestors.first = left_anc_nid; + ancestors.second = right_anc_nid; + } + // Sutured node is added - safe_insert(attaches, attach(t.create_file_node(i->second.sutured_id), i->first)); + safe_insert(attaches, attach(t.create_file_node(i->second.sutured_id, ancestors), i->first)); // Ancestor nodes are dropped; detach here, drop later. safe_insert(detaches, detach(i->second.first_ancestor)); ============================================================ --- cset.hh 53dcfec8e054e927c0615188e716548390460a3f +++ cset.hh 5f09b70523f6d8afad6f5b37c8edc90e99d7a979 @@ -15,9 +15,10 @@ #include "paths.hh" #include "rev_types.hh" -// Virtual interface to a tree-of-files which you can edit -// destructively; this may be the filesystem or an in-memory -// representation (a roster / mfest). +// Virtual interface to a tree-of-files which you can edit destructively; +// this may be the filesystem or an in-memory representation (a roster / +// mfest). The operations maintain both the roster and the marking_map (if +// any). struct editable_tree { @@ -25,9 +26,11 @@ struct editable_tree virtual node_id detach_node(file_path const & src) = 0; virtual void drop_detached_node(node_id nid) = 0; - // Attaching new nodes (via creation or as the tail end of renaming) + // Attaching new nodes (via creation, as the tail end of renaming, suturing, or splitting) virtual node_id create_dir_node() = 0; - virtual node_id create_file_node(file_id const & content) = 0; + virtual node_id create_file_node(file_id const & content, + std::pair const ancestors) = 0; + virtual node_id get_node(file_path const &pth) = 0; virtual void attach_node(node_id nid, file_path const & dst) = 0; // Modifying elements in-place @@ -99,7 +102,9 @@ struct cset ; } + // Apply changeset to roster and marking map in tree. void apply_to(editable_tree & t) const; + bool empty() const; void clear(); }; ============================================================ --- rev_types.hh 9a3a1a69643278a8b2c5c6c85d25623ae079227c +++ rev_types.hh 4cb4ab589c482cdd0d4afb9e23a53d2110011ba6 @@ -1,3 +1,4 @@ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2007 Zack Weinberg // // This program is made available under the GNU GPL version 2.0 or @@ -30,9 +31,13 @@ namespace basic_io struct stanza; } -// full definitions in cset.hh typedef std::map attr_map_t; typedef u32 node_id; +node_id const the_null_node = 0; +std::pair const null_ancestors = std::pair(the_null_node, the_null_node); + + +// full definitions in cset.hh struct cset; struct editable_tree; ============================================================ --- roster.cc c7cdd1cb8e944b19a946cac03311b025c621bb85 +++ roster.cc 9eb37f9fb2be303fc606c698427c3450b77c22d6 @@ -47,6 +47,28 @@ using boost::lexical_cast; /////////////////////////////////////////////////////////////////// +namespace +{ + namespace syms + { + symbol const ident("ident"); + symbol const birth("birth"); + symbol const birth_cause("birth_cause"); + symbol const birth_add("birth_add"); + symbol const birth_suture("birth_suture"); + symbol const birth_split("birth_split"); + symbol const dormant_attr("dormant_attr"); + + symbol const path_mark("path_mark"); + symbol const attr_mark("attr_mark"); + } +} + +// The marking_map format number must be incremented whenever any basic_io +// format used for marking_map data in the database changes, but only once +// per monotone release. +static const unsigned int current_marking_map_format = 2; + template <> void dump(node_id const & val, string & out) { @@ -65,6 +87,37 @@ template <> void } template <> void +dump(std::pair > const & birth_cause, string & out) +{ + ostringstream oss; + string tmp; + switch (birth_cause.first) + { + case marking_t::invalid: + out = "invalid\n"; + return; + + case marking_t::add: + out = syms::birth_add() + "\n"; + return; + + case marking_t::suture: + dump(birth_cause.second.first, tmp); + oss << syms::birth_suture() << tmp; + dump(birth_cause.second.second, tmp); + oss << tmp << '\n'; + out = oss.str(); + return; + + case marking_t::split: + dump(birth_cause.second.first, tmp); + oss << syms::birth_split() << tmp << '\n'; + out = oss.str(); + return; + } +} + +template <> void dump(set const & revids, string & out) { out.clear(); @@ -85,6 +138,8 @@ dump(marking_t const & marking, string & ostringstream oss; string tmp; oss << "birth_revision: " << marking.birth_revision << '\n'; + dump(marking.birth_cause, tmp); + oss << "birth_cause: " << tmp << '\n'; dump(marking.parent_name, tmp); oss << "parent_name: " << tmp << '\n'; dump(marking.file_content, tmp); @@ -1102,6 +1157,7 @@ roster_t::check_sane_against(marking_map ++ri, ++mi) { I(!null_id(mi->second.birth_revision)); + I(mi->second.birth_cause.first != marking_t::invalid); I(!mi->second.parent_name.empty()); if (is_file_t(ri->second)) @@ -1166,14 +1222,21 @@ node_id } node_id -editable_roster_base::create_file_node(file_id const & content) +editable_roster_base::create_file_node(file_id const & content, + std::pair const ancestors) { // L(FL("create_file_node('%s')") % content); - node_id n = r.create_file_node(content, nis); + node_id n = r.create_file_node(content, nis, ancestors); // L(FL("create_file_node('%s') -> %d") % content % n); return n; } +node_id +editable_roster_base::get_node(file_path const &pth) +{ + return r.get_node(pth)->self; +} + void editable_roster_base::attach_node(node_id nid, file_path const & dst) { @@ -1244,9 +1307,10 @@ namespace new_nodes.insert(nid); return nid; } - virtual node_id create_file_node(file_id const & content) + virtual node_id create_file_node(file_id const & content, + std::pair const ancestors = null_ancestors) { - node_id nid = this->editable_roster_base::create_file_node(content); + node_id nid = this->editable_roster_base::create_file_node(content, ancestors); new_nodes.insert(nid); return nid; } @@ -1275,9 +1339,11 @@ namespace // but may not be worth the effort (since it doesn't take that long to // get out in any case) a.get_name(aid, p); - node_id bid = b.get_node(p)->self; + node_t bn = b.get_node(p); + node_id bid = bn->self; if (b_new.find(bid) != b_new.end()) { + // New in both. I(temp_node(bid)); node_id new_nid; do @@ -1286,6 +1352,18 @@ namespace a.replace_node_id(aid, new_nid); b.replace_node_id(bid, new_nid); + + if (bn->ancestors.first != the_null_node) + { + // This is a suture; unify the ancestors. + node_t an = a.get_node(new_nid); + an->ancestors.second = bn->ancestors.first; + + node_t new_bn = b.get_node(new_nid); + new_bn->ancestors.first = an->ancestors.first; + new_bn->ancestors.second = bn->ancestors.first; + }; + b_new.erase(bid); } else @@ -1509,6 +1587,7 @@ namespace mark_new_node(revision_id const & new_rid, node_t n, marking_t & new_marking) { new_marking.birth_revision = new_rid; + new_marking.birth_cause = make_pair(marking_t::add, null_ancestors); I(new_marking.parent_name.empty()); new_marking.parent_name.insert(new_rid); I(new_marking.file_content.empty()); @@ -1536,6 +1615,7 @@ namespace I(same_type(parent_n, n) && parent_n->self == n->self); new_marking.birth_revision = parent_marking.birth_revision; + new_marking.birth_cause = parent_marking.birth_cause; mark_unmerged_scalar(parent_marking.parent_name, make_pair(parent_n->parent, parent_n->name), @@ -1577,9 +1657,22 @@ namespace marking_t & new_marking) { I(same_type(ln, n) && same_type(rn, n)); - I(left_marking.birth_revision == right_marking.birth_revision); - new_marking.birth_revision = left_marking.birth_revision; + // birth. + if (ln->self == rn->self) + { + // not a suture; a user add in a two-parent workspace. + I(left_marking.birth_revision == right_marking.birth_revision); + new_marking.birth_revision = left_marking.birth_revision; + new_marking.birth_cause = make_pair (marking_t::add, null_ancestors); + } + else + { + // suture. + new_marking.birth_revision = new_rid; + new_marking.birth_cause = make_pair (marking_t::suture, make_pair(ln->self, rn->self)); + } + // name mark_merged_scalar(left_marking.parent_name, left_uncommon_ancestors, make_pair(ln->parent, ln->name), @@ -1683,13 +1776,45 @@ mark_merge_roster(roster_t const & left_ marking_t new_marking; if (!exists_in_left && !exists_in_right) - mark_new_node(new_rid, n, new_marking); + { + // We have a new node in a merge. There are two cases: + // + // 1) Adding a file in a two-parent workspace, which is created by + // merge_into_workspace + // + // 2) Doing a suture + // + // There's a third case; doing a unit test. But we can change the + // unit test to handle the real code :). + if (the_null_node == n->ancestors.first) + { + // not a suture; two-parent workspace + mark_new_node(new_rid, n, new_marking); + } + else + { + // suture + node_map::const_iterator left = left_roster.all_nodes().find(n->ancestors.first); + node_map::const_iterator right = right_roster.all_nodes().find(n->ancestors.second); + I(left != left_roster.all_nodes().end()); + I(right != right_roster.all_nodes().end()); + mark_merged_node(safe_get(left_markings, n->ancestors.first), left_uncommon_ancestors, left->second, + safe_get(right_markings, n->ancestors.second), right_uncommon_ancestors, right->second, + new_rid, n, new_marking); + } + } + else if (!exists_in_left && exists_in_right) { + // FIXME_SUTURE: handle case where n is an ancestor of a sutured node on + // the left; beth_3 in test. node_t const & right_node = rni->second; marking_t const & right_marking = safe_get(right_markings, n->self); - // must be unborn on the left (as opposed to dead) + + // Must be unborn on the left (as opposed to dead); otherwise it + // would not be in the merge. Therefore the birth revision for + // this node must be in the uncommon ancestors on the right: I(right_uncommon_ancestors.find(right_marking.birth_revision) != right_uncommon_ancestors.end()); mark_unmerged_node(right_marking, right_node, @@ -1697,16 +1822,23 @@ mark_merge_roster(roster_t const & left_ } else if (exists_in_left && !exists_in_right) { + // FIXME_SUTURE: handle case where n is an ancestor of a sutured node on + // the right; abe_3 in test. node_t const & left_node = lni->second; marking_t const & left_marking = safe_get(left_markings, n->self); - // must be unborn on the right (as opposed to dead) + + // Must be unborn on the right (as opposed to dead); otherwise it + // would not be in the merge. Therefore the birth revision for + // this node must be in the uncommon ancestors on the left: I(left_uncommon_ancestors.find(left_marking.birth_revision) != left_uncommon_ancestors.end()); + mark_unmerged_node(left_marking, left_node, new_rid, n, new_marking); } else { + // exists in both left and right parents node_t const & left_node = lni->second; node_t const & right_node = rni->second; mark_merged_node(safe_get(left_markings, n->self), @@ -1754,7 +1886,8 @@ namespace { return handle_new(this->editable_roster_base::create_dir_node()); } - virtual node_id create_file_node(file_id const & content) + virtual node_id create_file_node(file_id const & content, + std::pair const ancestors) { return handle_new(this->editable_roster_base::create_file_node(content)); } @@ -2380,7 +2513,7 @@ make_restricted_roster(roster_t const & if (is_file_t(n->second)) { file_t const f = downcast_to_file_t(n->second); - restricted.create_file_node(f->content, f->self); + restricted.create_file_node(f->content, f->self, f->ancestors); } else restricted.create_dir_node(n->second->self); @@ -2559,11 +2692,39 @@ push_marking(basic_io::stanza & st, { I(!null_id(mark.birth_revision)); - st.push_binary_pair(basic_io::syms::birth, mark.birth_revision.inner()); + st.push_binary_pair(syms::birth, mark.birth_revision.inner()); + switch (mark.birth_cause.first) + { + case marking_t::invalid: + I(false); + break; + + case marking_t::add: + st.push_str_pair(syms::birth_cause, syms::birth_add); + break; + + case marking_t::suture: + { + std::vector data; + data.push_back(lexical_cast(mark.birth_cause.second.first)); + data.push_back(lexical_cast(mark.birth_cause.second.second)); + st.push_str_multi(syms::birth_cause, syms::birth_suture, data); + } + break; + + case marking_t::split: + { + std::vector data; + data.push_back(lexical_cast(mark.birth_cause.second.first)); + st.push_str_multi(syms::birth_cause, syms::birth_split, data); + } + break; + }; + for (set::const_iterator i = mark.parent_name.begin(); i != mark.parent_name.end(); ++i) - st.push_binary_pair(basic_io::syms::path_mark, i->inner()); + st.push_binary_pair(syms::path_mark, i->inner()); if (is_file) { @@ -2579,7 +2740,7 @@ push_marking(basic_io::stanza & st, { for (set::const_iterator j = i->second.begin(); j != i->second.end(); ++j) - st.push_binary_triple(basic_io::syms::attr_mark, i->first(), j->inner()); + st.push_binary_triple(syms::attr_mark, i->first(), j->inner()); } } @@ -2591,15 +2752,47 @@ parse_marking(basic_io::parser & pa, while (pa.symp()) { string rev; - if (pa.symp(basic_io::syms::birth)) + if (pa.symp(syms::birth)) { pa.sym(); pa.hex(rev); marking.birth_revision = revision_id(decode_hexenc(rev)); } - else if (pa.symp(basic_io::syms::path_mark)) + else if (pa.symp(syms::birth_cause)) { + std::string tmp_1, tmp_2; pa.sym(); + + if (pa.symp (syms::birth_add)) + { + pa.sym(); + marking.birth_cause = make_pair (marking_t::add, null_ancestors); + } + else if (pa.symp (syms::birth_suture)) + { + pa.sym(); + pa.str(tmp_1); + pa.str(tmp_2); + marking.birth_cause = make_pair (marking_t::add, + make_pair(lexical_cast(tmp_1), + lexical_cast(tmp_2))); + } + else if (pa.symp (syms::birth_suture)) + { + pa.sym(); + pa.str(tmp_1); + marking.birth_cause = make_pair (marking_t::add, + make_pair(lexical_cast(tmp_1), + the_null_node)); + } + else + { + E(false, F("unrecognized birth_cause '%s'") % pa.token); + } + } + else if (pa.symp(syms::path_mark)) + { + pa.sym(); pa.hex(rev); safe_insert(marking.parent_name, revision_id(decode_hexenc(rev))); } @@ -2609,7 +2802,7 @@ parse_marking(basic_io::parser & pa, pa.hex(rev); safe_insert(marking.file_content, revision_id(decode_hexenc(rev))); } - else if (pa.symp(basic_io::syms::attr_mark)) + else if (pa.symp(syms::attr_mark)) { string k; pa.sym(); @@ -2635,7 +2828,7 @@ roster_t::print_to(basic_io::printer & p I(has_root()); { basic_io::stanza st; - st.push_str_pair(basic_io::syms::format_version, "1"); + st.push_str_pair(basic_io::syms::format_version, lexical_cast(current_marking_map_format)); pr.print_stanza(st); } for (dfs_iter i(root_dir, true); !i.finished(); ++i) @@ -2659,7 +2852,7 @@ roster_t::print_to(basic_io::printer & p if (print_local_parts) { I(curr->self != the_null_node); - st.push_str_pair(basic_io::syms::ident, lexical_cast(curr->self)); + st.push_str_pair(syms::ident, lexical_cast(curr->self)); } // Push the non-dormant part of the attr map @@ -2682,7 +2875,7 @@ roster_t::print_to(basic_io::printer & p if (!j->second.first) { I(j->second.second().empty()); - st.push_str_pair(basic_io::syms::dormant_attr, j->first()); + st.push_str_pair(syms::dormant_attr, j->first()); } } @@ -2733,7 +2926,7 @@ roster_t::parse_from(basic_io::parser & pa.esym(basic_io::syms::format_version); string vers; pa.str(vers); - I(vers == "1"); + I(vers == lexical_cast(current_marking_map_format)); } while(pa.symp()) @@ -2748,7 +2941,7 @@ roster_t::parse_from(basic_io::parser & pa.str(pth); pa.esym(basic_io::syms::content); pa.hex(content); - pa.esym(basic_io::syms::ident); + pa.esym(syms::ident); pa.str(ident); n = file_t(new file_node(read_num(ident), file_id(decode_hexenc(content)))); @@ -2757,7 +2950,7 @@ roster_t::parse_from(basic_io::parser & { pa.sym(); pa.str(pth); - pa.esym(basic_io::syms::ident); + pa.esym(syms::ident); pa.str(ident); n = dir_t(new dir_node(read_num(ident))); } @@ -2790,7 +2983,7 @@ roster_t::parse_from(basic_io::parser & } // Dormant attrs - while(pa.symp(basic_io::syms::dormant_attr)) + while(pa.symp(syms::dormant_attr)) { pa.sym(); string k; @@ -3496,6 +3689,8 @@ UNIT_TEST(roster, bad_attr) // purpose of this section is to systematically and exhaustively test every // possible case. // +// FIXME_SUTURE: reference ss-mark-merge.text, add suture, split tests +// // Our underlying merger, *-merge, works on scalars, case-by-case. // The cases are: // 0 parent: ============================================================ --- roster.hh e0d2b16d79c0d818fb951f7b32726d6eaebdb725 +++ roster.hh 249507b96d4cd90e81b8903cbcbb044f0c40510a @@ -24,9 +24,6 @@ struct node_id_source /////////////////////////////////////////////////////////////////// -node_id const the_null_node = 0; -std::pair const null_ancestors = std::pair(the_null_node, the_null_node); - inline bool null_node(node_id n) { @@ -143,14 +140,20 @@ struct marking_t struct marking_t { + typedef enum {invalid, add, suture, split} birth_cause_t; + revision_id birth_revision; + std::pair > birth_cause; + // if suture, the node_ids indicate the ancestors. If split, the first + // node_id indicates the ancestor. std::set parent_name; std::set file_content; std::map > attrs; - marking_t() {}; + marking_t() : birth_cause (std::make_pair (invalid, null_ancestors)) {}; bool operator==(marking_t const & other) const { return birth_revision == other.birth_revision + && birth_cause == birth_cause && parent_name == other.parent_name && file_content == other.file_content && attrs == other.attrs; @@ -309,7 +312,9 @@ public: virtual node_id detach_node(file_path const & src); virtual void drop_detached_node(node_id nid); virtual node_id create_dir_node(); - virtual node_id create_file_node(file_id const & content); + virtual node_id create_file_node(file_id const & content, + std::pair const ancestors = null_ancestors); + virtual node_id get_node(file_path const &pth); virtual void attach_node(node_id nid, file_path const & dst); virtual void apply_delta(file_path const & pth, file_id const & old_id, ============================================================ --- roster_merge.cc 178917d07e72bb9cff7c9616e0e25e0b89eccdfb +++ roster_merge.cc 7213f45b1eaf58a223235a3569e07ba8d724e4bd @@ -1624,6 +1624,8 @@ roster_merge_result::resolve_duplicate_n { I(conflict.right_resolution.first == resolve_conflicts::suture); + // There's no inherent reason suturing directories can't be + // supported; we just haven't worked on it yet. N(!is_dir_t(left_roster.get_node (left_nid)), F("can't suture directory : %s") % left_name); P(F("suturing %s, %s into %s") % left_name % right_name % conflict.left_resolution.second); @@ -1796,15 +1798,68 @@ namespace } inline void - insert_if_unborn(node_t const & n, - marking_map const & markings, - set const & uncommon_ancestors, - roster_t const & parent_roster, - roster_t & new_roster) + create_node_for(node_t const & n, std::pair const ancestors, roster_t & new_roster) { + if (is_dir_t(n)) + I(false); + else if (is_file_t(n)) + return new_roster.create_file_node(file_id(), n->self, ancestors); + else + I(false); + } + + inline void + insert_if_unborn_or_sutured(node_t const & n, + marking_map const & markings, + set const & uncommon_ancestors, + roster_t const & parent_roster, + roster_t & new_roster) + { + MM(markings); + MM(uncommon_ancestors); + // See comment in roster_merge below for cases. n is either the left or + // right parent node. We are in case iii, iv, or v. We determine which + // by searching for the birth revision in uncommon_ancestors. + revision_id const & birth = safe_get(markings, n->self).birth_revision; - if (uncommon_ancestors.find(birth) != uncommon_ancestors.end()) - create_node_for(n, new_roster); + + set::const_iterator const uncommon_birth_i = uncommon_ancestors.find(birth); + + if (uncommon_birth_i != uncommon_ancestors.end()) + { + // case iii or iv + std::pair > const & birth_cause = + safe_get(markings, n->self).birth_cause; + + switch (birth_cause.first) + { + case marking_t::invalid: + I(false); + + case marking_t::add: + // case iv + create_node_for(n, new_roster); + break; + + case marking_t::suture: + case marking_t::split: + // case iii + { + std::pair birth_ancestors = birth_cause.second; + + if (n->self == birth_ancestors.first) + { + create_node_for(n, make_pair (n->self, birth_ancestors.second), new_roster); + } + else + { + I(n->self == birth_ancestors.second); + create_node_for(n, make_pair (birth_ancestors.second, n->self), new_roster); + } + } + break; + } + } else { // In this branch we are NOT inserting the node into the new roster as it @@ -1965,6 +2020,120 @@ namespace assign_name(result, n->self, old_n->parent, old_n->name, side); } + void + merge_nodes(node_t const left_n, + marking_t const & left_marking, + set const & left_uncommon_ancestors, + node_t const right_n, + marking_t const & right_marking, + set const & right_uncommon_ancestors, + node_t const new_n, + roster_merge_result & result) + { + // merge name + pair left_name, right_name, new_name; + multiple_name_conflict conflict(new_n->self); + left_name = make_pair(left_n->parent, left_n->name); + right_name = make_pair(right_n->parent, right_n->name); + if (merge_scalar(left_name, + left_marking.parent_name, + left_uncommon_ancestors, + right_name, + right_marking.parent_name, + right_uncommon_ancestors, + new_name, conflict)) + { + side_t winning_side; + + if (new_name == left_name) + winning_side = left_side; + else if (new_name == right_name) + winning_side = right_side; + else + I(false); + + assign_name(result, new_n->self, + new_name.first, new_name.second, winning_side); + + } + else + { + // unsuccessful merge; leave node detached and save + // conflict object + result.multiple_name_conflicts.push_back(conflict); + } + // if a file, merge content + if (is_file_t(new_n)) + { + file_content_conflict conflict(new_n->self); + if (merge_scalar(downcast_to_file_t(left_n)->content, + left_marking.file_content, + left_uncommon_ancestors, + downcast_to_file_t(right_n)->content, + right_marking.file_content, + right_uncommon_ancestors, + downcast_to_file_t(new_n)->content, + conflict)) + { + // successful merge + } + else + { + downcast_to_file_t(new_n)->content = file_id(); + result.file_content_conflicts.push_back(conflict); + } + } + // merge attributes + { + full_attr_map_t::const_iterator left_ai = left_n->attrs.begin(); + full_attr_map_t::const_iterator right_ai = right_n->attrs.begin(); + parallel::iter attr_i(left_n->attrs, + right_n->attrs); + while(attr_i.next()) + { + switch (attr_i.state()) + { + case parallel::invalid: + I(false); + case parallel::in_left: + safe_insert(new_n->attrs, attr_i.left_value()); + break; + case parallel::in_right: + safe_insert(new_n->attrs, attr_i.right_value()); + break; + case parallel::in_both: + pair new_value; + attribute_conflict conflict(new_n->self); + conflict.key = attr_i.left_key(); + I(conflict.key == attr_i.right_key()); + if (merge_scalar(attr_i.left_data(), + safe_get(left_marking.attrs, + attr_i.left_key()), + left_uncommon_ancestors, + attr_i.right_data(), + safe_get(right_marking.attrs, + attr_i.right_key()), + right_uncommon_ancestors, + new_value, + conflict)) + { + // successful merge + safe_insert(new_n->attrs, + make_pair(attr_i.left_key(), + new_value)); + } + else + { + // unsuccessful merge + // leave out the attr entry entirely, and save the + // conflict + result.attribute_conflicts.push_back(conflict); + } + break; + } + } + } + } } // end anonymous namespace void @@ -1985,9 +2154,69 @@ roster_merge(roster_t const & left_paren MM(right_markings); MM(result); - // First handle lifecycles, by die-die-die merge -- our result will contain - // everything that is alive in both parents, or alive in one and unborn in - // the other, exactly. + // First handle lifecycles. Several cases: + // + // A1 B1 + // i) \ / + // C1 + // + // The node is merged in the child + // + // + // A1 B2 + // ii) \ / + // C3 + // + // The node is sutured in the child. This is only possible in a + // conflict resolution, which will occur later in the merge process. + // FIXME_SUTURE: support user sutures. + // + // + // A1 B3 + // iiia) \ / + // C3 + // + // The node was born in a suture or split in an uncommon ancestor of B + // + // A3 B1 + // iiib) \ / + // C3 + // + // The node was born in a suture or split in an uncommon ancestor of A + // + // + // A1 B + // iva) \ / + // C1 + // + // The node was born new in A's uncommon ancestors + // + // A B1 + // ivb) \ / + // C1 + // + // The node was born new in B's uncommon ancestors + // + // + // A1 B + // v) \ / + // C + // + // The node was deleted in B's uncommon ancestors + // + // A B1 + // \ / + // C + // + // The node was deleted in A's uncommon ancestors + // + // + // In monotone 0.4 and earlier, cases ii and iii did not exist. + // + // In case v, deletion wins over any other change; it might be better to + // have deletion conflict with any other change. But that's left for + // another time. + { parallel::iter i(left_parent.all_nodes(), right_parent.all_nodes()); while (i.next()) @@ -1998,15 +2227,17 @@ roster_merge(roster_t const & left_paren I(false); case parallel::in_left: - insert_if_unborn(i.left_data(), - left_markings, left_uncommon_ancestors, left_parent, - result.roster); + // case iii, iva, or va + insert_if_unborn_or_sutured(i.left_data(), + left_markings, left_uncommon_ancestors, left_parent, + result.roster); break; case parallel::in_right: - insert_if_unborn(i.right_data(), - right_markings, right_uncommon_ancestors, right_parent, - result.roster); + // case iii, ivb, or vb + insert_if_unborn_or_sutured(i.right_data(), + right_markings, right_uncommon_ancestors, right_parent, + result.roster); break; case parallel::in_both: @@ -2033,16 +2264,48 @@ roster_merge(roster_t const & left_paren case parallel::in_left: { - node_t const & left_n = i.left_data(); - // we skip nodes that aren't in the result roster (were - // deleted in the lifecycles step above) + node_t const left_n = i.left_data(); + // We skip nodes that aren't in the result roster (were + // deleted in the lifecycles step above), unless they are + // parents of a suture. + if (result.roster.has_node(left_n->self)) { - // attach this node from the left roster. this may cause - // a name collision with the previously attached node from - // the other side of the merge. - copy_node_forward(result, new_i->second, left_n, left_side); - ++new_i; + if (left_n->ancestors.first != the_null_node && + left_n->ancestors.first != left_n->self) + { + // This node is the child of a suture; if right parent + // exists in right, merge with it. + node_map::const_iterator right_i = right_parent.all_nodes().find (left_n->ancestors.second); + if (right_i != right_parent.all_nodes().end()) + { + marking_map::const_iterator right_mi = right_markings.find(right_i->second->ancestors.first); + + // check that iterators are in sync. + I(new_i->first == i.left_key()); + I(left_mi->first == i.left_key()); + + merge_nodes(i.left_data(), // left_n + left_mi->second, // left_marking + left_uncommon_ancestors, + right_i->second, // right_n + right_mi->second, + right_uncommon_ancestors, + new_i->second, // new_n + result); + ++new_i; + } + } + else + { + // Not child of a suture. + // + // Attach this node from the left roster. This may cause + // a name collision with the previously attached node from + // the other side of the merge. + copy_node_forward(result, new_i->second, left_n, left_side); + ++new_i; + } } ++left_mi; break; @@ -2051,14 +2314,46 @@ roster_merge(roster_t const & left_paren case parallel::in_right: { node_t const & right_n = i.right_data(); - // we skip nodes that aren't in the result roster + // We skip nodes that aren't in the result roster, unless they are + // parents of a suture. + if (result.roster.has_node(right_n->self)) { - // attach this node from the right roster. this may cause - // a name collision with the previously attached node from - // the other side of the merge. - copy_node_forward(result, new_i->second, right_n, right_side); - ++new_i; + if (right_n->ancestors.first != the_null_node && + right_n->ancestors.first != right_n->self) + { + // This node is the child of a suture; if right parent + // exists in right, merge with it. + node_map::const_iterator left_i = left_parent.all_nodes().find (right_n->ancestors.first); + if (left_i != left_parent.all_nodes().end()) + { + marking_map::const_iterator left_mi = left_markings.find(right_n->ancestors.first); + + // check that iterators are in sync. + I(new_i->first == i.right_key()); + I(right_mi->first == i.right_key()); + + merge_nodes(left_i->second, // left_n + left_mi->second, // left_marking + left_uncommon_ancestors, + i.right_data(), // right_n + right_mi->second, // right_marking + right_uncommon_ancestors, + new_i->second, // new_n + result); + ++new_i; + } + } + else + { + // Not child of a suture. + // + // Attach this node from the right roster. This may + // cause a name collision with the previously attached + // node from the other side of the merge. + copy_node_forward(result, new_i->second, right_n, right_side); + ++new_i; + } } ++right_mi; break; @@ -2069,121 +2364,15 @@ roster_merge(roster_t const & left_paren I(new_i->first == i.left_key()); I(left_mi->first == i.left_key()); I(right_mi->first == i.right_key()); - node_t const & left_n = i.left_data(); - marking_t const & left_marking = left_mi->second; - node_t const & right_n = i.right_data(); - marking_t const & right_marking = right_mi->second; - node_t const & new_n = new_i->second; - // merge name - { - pair left_name, right_name, new_name; - multiple_name_conflict conflict(new_n->self); - left_name = make_pair(left_n->parent, left_n->name); - right_name = make_pair(right_n->parent, right_n->name); - if (merge_scalar(left_name, - left_marking.parent_name, - left_uncommon_ancestors, - right_name, - right_marking.parent_name, - right_uncommon_ancestors, - new_name, conflict)) - { - side_t winning_side; - if (new_name == left_name) - winning_side = left_side; - else if (new_name == right_name) - winning_side = right_side; - else - I(false); - - // attach this node from the winning side of the merge. if - // there is a name collision the previously attached node - // (which is blocking this one) must come from the other - // side of the merge. - assign_name(result, new_n->self, - new_name.first, new_name.second, winning_side); - - } - else - { - // unsuccessful merge; leave node detached and save - // conflict object - result.multiple_name_conflicts.push_back(conflict); - } - } - // if a file, merge content - if (is_file_t(new_n)) - { - file_content_conflict conflict(new_n->self); - if (merge_scalar(downcast_to_file_t(left_n)->content, - left_marking.file_content, - left_uncommon_ancestors, - downcast_to_file_t(right_n)->content, - right_marking.file_content, - right_uncommon_ancestors, - downcast_to_file_t(new_n)->content, - conflict)) - { - // successful merge - } - else - { - downcast_to_file_t(new_n)->content = file_id(); - result.file_content_conflicts.push_back(conflict); - } - } - // merge attributes - { - full_attr_map_t::const_iterator left_ai = left_n->attrs.begin(); - full_attr_map_t::const_iterator right_ai = right_n->attrs.begin(); - parallel::iter attr_i(left_n->attrs, - right_n->attrs); - while(attr_i.next()) - { - switch (attr_i.state()) - { - case parallel::invalid: - I(false); - case parallel::in_left: - safe_insert(new_n->attrs, attr_i.left_value()); - break; - case parallel::in_right: - safe_insert(new_n->attrs, attr_i.right_value()); - break; - case parallel::in_both: - pair new_value; - attribute_conflict conflict(new_n->self); - conflict.key = attr_i.left_key(); - I(conflict.key == attr_i.right_key()); - if (merge_scalar(attr_i.left_data(), - safe_get(left_marking.attrs, - attr_i.left_key()), - left_uncommon_ancestors, - attr_i.right_data(), - safe_get(right_marking.attrs, - attr_i.right_key()), - right_uncommon_ancestors, - new_value, - conflict)) - { - // successful merge - safe_insert(new_n->attrs, - make_pair(attr_i.left_key(), - new_value)); - } - else - { - // unsuccessful merge - // leave out the attr entry entirely, and save the - // conflict - result.attribute_conflicts.push_back(conflict); - } - break; - } - - } - } + merge_nodes(i.left_data(), // left_n + left_mi->second, // left_marking + left_uncommon_ancestors, + i.right_data(), // right_n + right_mi->second, // right_marking + right_uncommon_ancestors, + new_i->second, // new_n + result); } ++left_mi; ++right_mi; ============================================================ --- tests/resolve_duplicate_name_conflict/__driver__.lua 5bcba012902ee356d5ef041847cef3aa725c53c1 +++ tests/resolve_duplicate_name_conflict/__driver__.lua 8bde1a08cec21f13e69bbb1ccae604dc3d5146d7 @@ -1,11 +1,28 @@ --- Test/demonstrate handling of a duplicate name conflict; Abe and --- Beth add files with the same names. +-- Test/demonstrate handling of a duplicate name conflict. -- --- For checkout.sh, the user intent is that there be --- one file with that name; the contents should be merged. +-- In the first step, we have this revision history graph: -- --- For thermostat.c, there should be two files; --- thermostat-westinghouse.c and thermostat-honeywell.c +-- o +-- / \ +-- a b +-- \ / +-- d +-- +-- 'o' is the base revision. In 'a' and 'b', Abe and Beth each add two +-- files with the same names: +-- +-- checkout.sh +-- the user intent is that there be one file with that name; +-- the contents should be merged. +-- +-- thermostat.c +-- the user intent is that there should be two files; +-- thermostat-westinghouse.c and thermostat-honeywell.c +-- +-- in 'd', the duplicate name conflicts are resolved by suturing +-- checkout.sh and renaming thermostat.c. +-- +-- After that, we extend the history graph; see below. mtn_setup() include ("common/test_utils_inventory.lua") @@ -16,7 +33,7 @@ base = base_revision() base = base_revision() -- Abe adds conflict files -addfile("thermostat.c", "thermostat westinghouse") +addfile("thermostat.c", "thermostat westinghouse abe 1") addfile("checkout.sh", "checkout.sh abe 1") commit("testbranch", "abe_1") abe_1 = base_revision() @@ -24,13 +41,13 @@ revert_to(base) revert_to(base) -- Beth adds files, and attempts to merge -addfile("thermostat.c", "thermostat honeywell") +addfile("thermostat.c", "thermostat honeywell beth 1") addfile("checkout.sh", "checkout.sh beth 1") commit("testbranch", "beth_1") beth_1 = base_revision() -- This fails due to duplicate name conflicts -check(mtn("merge"), 1, false, false) +check(mtn("merge"), 1, nil, false) -- Beth uses 'automate show_conflicts' and 'merge --resolve-conflicts-file' -- to fix the conflicts @@ -46,7 +63,7 @@ check(mtn("merge"), 1, false, false) -- _MTN/conflicts, she records the resolution as -- 'resolved_content_ws'. -check (mtn("automate", "show_conflicts"), 0, true, false) +check (mtn("automate", "show_conflicts"), 0, true, nil) -- Verify that we got the expected revisions, conflicts and file ids parsed = parse_basic_io(readfile("stdout")) @@ -65,18 +82,18 @@ check_basic_io_line (15, parsed[15], "le abe_checkout = parsed[8].values[1] check_basic_io_line (15, parsed[15], "left_name", "thermostat.c") -check_basic_io_line (16, parsed[16], "left_file_id", "4cdcec6fa2f9d5c075d5b80d03c708c8e4801196") +check_basic_io_line (16, parsed[16], "left_file_id", "c2f67aa3b29c2bdab4790213c7f3bf73e58440a7") abe_thermostat = parsed[16].values[1] -- Do the filesystem rename for Beth's thermostat.c, and retrieve Abe's version rename ("thermostat.c", "thermostat-honeywell.c") -check (mtn ("automate", "get_file", abe_thermostat), 0, true, false) +check (mtn ("automate", "get_file", abe_thermostat), 0, true, nil) rename ("stdout", "thermostat-westinghouse.c") -check ("thermostat westinghouse" == readfile ("thermostat-westinghouse.c")) +check ("thermostat westinghouse abe 1" == readfile ("thermostat-westinghouse.c")) -- Do the manual merge for checkout.sh; retrieve Abe's version -check (mtn ("automate", "get_file", abe_checkout), 0, true, false) +check (mtn ("automate", "get_file", abe_checkout), 0, true, nil) rename ("stdout", "checkout.sh-abe") check ("checkout.sh abe 1" == readfile ("checkout.sh-abe")) @@ -86,27 +103,87 @@ get ("conflicts-resolved", "_MTN/conflic get ("conflicts-resolved", "_MTN/conflicts") -- This succeeds -check(mtn("merge", "--resolve-conflicts-file=_MTN/conflicts"), 0, true, true) +check(mtn("merge", "--resolve-conflicts-file=_MTN/conflicts"), 0, nil, true) +canonicalize("stderr") +get ("expected-merge-messages-abe_1-beth_1") +check(samefile("expected-merge-messages-abe_1-beth_1", "stderr")) --- FIXME: check for reasonable messages from merge - -- update fails if thermostat.c is missing, and if -- thermostat-honeywell.c, thermostat-westinghouse.c are in the way. -- So clean that up first. FIXME: update needs --ignore-missing, -- --overwrite-ws or something. -check(mtn("revert", "--missing"), 0, false, false) +check(mtn("revert", "--missing"), 0, nil, false) remove ("thermostat-honeywell.c") remove ("thermostat-westinghouse.c") -check(mtn("update"), 0, true, true) +check(mtn("update"), 0, nil, true) +-- FIXME: this warns about changes in checkout.sh being lost, even though they aren't; it doesn't understand suture +canonicalize("stderr") +get ("expected-update-messages-jim_1") +check(samefile("expected-update-messages-jim_1", "stderr")) -- verify that we got revision_format 2 -get ("expected-merged-revision") -check(mtn("automate", "get_revision", base_revision()), 1, true, nil) +check(mtn("automate", "get_revision", base_revision()), 0, true, nil) canonicalize("stdout") -check_same_file("expected-merged-revision", "stdout") +get ("expected-merged-revision-jim_1") +check(samefile("expected-merged-revision-jim_1", "stdout")) -- Verify file contents -check("thermostat westinghouse" == readfile("thermostat-westinghouse.c")) -check("thermostat honeywell" == readfile("thermostat-honeywell.c")) +check("thermostat westinghouse abe 1" == readfile("thermostat-westinghouse.c")) +check("thermostat honeywell beth 1" == readfile("thermostat-honeywell.c")) check("checkout.sh merged\n" == readfile("checkout.sh")) + +-- In the second step, we extend the revision history graph: +-- +-- o +-- / \ +-- a b +-- / \ / \ +-- c d e +-- \ / \ / +-- g h +-- \ / +-- f +-- +-- Here we assume that the 'd' merge was done by Jim, and Abe and Beth +-- edit their new files in parallel with the merge. We pretend Abe, +-- Beth, and Jim have separate development databases, shared via +-- netsync. Eventually everything is merged properly. +-- +-- in 'c' and 'e', Abe and Beth edit checkout.sh and thermostat.c +-- +-- in 'g' and 'h', Abe and Beth each merge their changes with Jim's merge. +-- +-- in 'f', Jim merges one more time. + +jim_1 = base_revision() + +-- Abe edits his files and merges +revert_to(abe_1) + +writefile("thermostat.c", "thermostat westinghouse abe 2") +writefile("checkout.sh", "checkout.sh abe 2") +commit("testbranch", "abe_2") +abe_2 = base_revision() + +check(mtn("merge"), 0, nil, true) +canonicalize("stderr") +get ("expected-merge-messages-abe_2-jim_1") +check(samefile("expected-merge-messages-abe_2-jim_1", "stderr")) + +-- Beth edits her files and merges +revert_to(beth_1) + +writefile("thermostat.c", "thermostat honeywell beth 1") +writefile("checkout.sh", "checkout.sh beth 1") +commit("testbranch", "beth_2") +beth_2 = base_revision() + +-- If we just do 'merge', mtn will merge 'e' and 'g', since those are +-- the current heads. To emulate separate development databases, we +-- specify the revisions to merge. +check(mtn("merge", jim_1, beth_2), 0, nil, true) +canonicalize("stderr") +get ("expected-merge-messages-jim_1-beth_2") +check(samefile("expected-merge-messages-jim_1-beth_2", "stderr")) + -- end of file ============================================================ --- work.cc 0261cdf7dbc3baed0cdfc0e75a3a858f64eefa97 +++ work.cc a01164a4e32079091f126c5018a187cea96e0dcd @@ -1,3 +1,4 @@ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2002 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -858,7 +859,9 @@ struct editable_working_tree : public ed virtual void drop_detached_node(node_id nid); virtual node_id create_dir_node(); - virtual node_id create_file_node(file_id const & content); + virtual node_id create_file_node(file_id const & content, + std::pair const ancestors = null_ancestors); + virtual node_id get_node(file_path const &pth); virtual void attach_node(node_id nid, file_path const & dst); virtual void apply_delta(file_path const & pth, @@ -899,7 +902,9 @@ struct simulated_working_tree : public e virtual void drop_detached_node(node_id nid); virtual node_id create_dir_node(); - virtual node_id create_file_node(file_id const & content); + virtual node_id create_file_node(file_id const & content, + std::pair const ancestors = null_ancestors); + virtual node_id get_node(file_path const &pth); virtual void attach_node(node_id nid, file_path const & dst); virtual void apply_delta(file_path const & pth, @@ -1013,8 +1018,10 @@ node_id } node_id -editable_working_tree::create_file_node(file_id const & content) +editable_working_tree::create_file_node(file_id const & content, + std::pair const ancestors) { + I(ancestors == null_ancestors); node_id nid = next_nid++; bookkeeping_path pth = path_for_detached_nid(nid); require_path_is_nonexistent(pth, @@ -1026,6 +1033,16 @@ editable_working_tree::create_file_node( return nid; } +node_id +editable_working_tree::get_node(file_path const &pth) +{ + // The mapping from node ids to file_paths is not stored anywhere. So we + // can't do whatever operation needs this. So far, it's only looking up + // ancestors for sutures while applying a changeset; a working tree can't + // be an ancestor, so we're ok. + I(false); +} + void editable_working_tree::attach_node(node_id nid, file_path const & dst_pth) { @@ -1145,11 +1162,18 @@ node_id } node_id -simulated_working_tree::create_file_node(file_id const & content) +simulated_working_tree::create_file_node(file_id const & content, + std::pair const ancestors) { - return workspace.create_file_node(content, nis); + return workspace.create_file_node(content, nis, ancestors); } +node_id +simulated_working_tree::get_node(file_path const &pth) +{ + return workspace.get_node(pth)->self; +} + void simulated_working_tree::attach_node(node_id nid, file_path const & dst) {