# # # add_file "tests/resolve_duplicate_name_conflict/checkout.sh-merged" # content [3390893b9a31eaa9ef0e9364b27ee1e617e6891a] # # add_file "tests/resolve_duplicate_name_conflict/conflicts-resolved" # content [9630663631477e886a63eea58ad6898adb81b4a4] # # patch "cset.cc" # from [ee0aba985f36fe78699987b4ea2cf6d9dc870a68] # to [a2cb867731e1d283fa60165e534157d0b38fb364] # # patch "cset.hh" # from [5120df6bd9c521f03437fc04e5f02ed7dc610b97] # to [53dcfec8e054e927c0615188e716548390460a3f] # # patch "roster.cc" # from [70af963b64dd2729562ca29ae346392ff299c108] # to [8befc5f69097a05b8c58ef24bb37d97378b7e7b0] # # patch "roster.hh" # from [0a976adbca1e66062d5bd9fcfb063eef07427f73] # to [e22fbd6c2eea9615337de4319fca8353c587b13d] # # patch "roster_merge.cc" # from [992ee1d3932808bd0e059c4e8f984c4fcd39b586] # to [178917d07e72bb9cff7c9616e0e25e0b89eccdfb] # # patch "tests/resolve_duplicate_name_conflict/__driver__.lua" # from [8f477bd075f50532cdb3a519e136122443596aa5] # to [c6652312c6765180630aff4ebacc7af45e6c8472] # ============================================================ --- tests/resolve_duplicate_name_conflict/checkout.sh-merged 3390893b9a31eaa9ef0e9364b27ee1e617e6891a +++ tests/resolve_duplicate_name_conflict/checkout.sh-merged 3390893b9a31eaa9ef0e9364b27ee1e617e6891a @@ -0,0 +1 @@ +checkout.sh merged ============================================================ --- tests/resolve_duplicate_name_conflict/conflicts-resolved 9630663631477e886a63eea58ad6898adb81b4a4 +++ tests/resolve_duplicate_name_conflict/conflicts-resolved 9630663631477e886a63eea58ad6898adb81b4a4 @@ -0,0 +1,22 @@ + left [1337cb1059c4bc3e376b14381b43e9383c654da1] + right [d5f1dd136c86b5bbd5e71b0c3365667e328af492] +ancestor [b3ac8a77cee78263b66800c635441ecb1f259a42] + + conflict duplicate_name + left_type "added file" + left_name "checkout.sh" + left_file_id [61b8d4fb0e5d78be111f691b955d523c782fa92e] + right_type "added file" + right_name "checkout.sh" +right_file_id [dd6805ae36432d6edcbdff6ea578ea981ffa2144] +resolved_suture "checkout.sh" + + conflict duplicate_name + left_type "added file" + left_name "thermostat.c" + left_file_id [4cdcec6fa2f9d5c075d5b80d03c708c8e4801196] + right_type "added file" + right_name "thermostat.c" +right_file_id [7e9f2712c5d3570815f1546772d9119474d32afc] +resolved_rename_left "thermostat-westinghouse.c" +resolved_rename_right "thermostat-honeywell.c" ============================================================ --- cset.cc ee0aba985f36fe78699987b4ea2cf6d9dc870a68 +++ cset.cc a2cb867731e1d283fa60165e534157d0b38fb364 @@ -1,3 +1,4 @@ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2005 Nathaniel Smith // // This program is made available under the GNU GPL version 2.0 or @@ -78,6 +79,7 @@ cset::empty() const nodes_deleted.empty() && dirs_added.empty() && files_added.empty() + && nodes_sutured.empty() && nodes_renamed.empty() && deltas_applied.empty() && attrs_cleared.empty() @@ -90,6 +92,7 @@ cset::clear() nodes_deleted.clear(); dirs_added.clear(); files_added.clear(); + nodes_sutured.clear(); nodes_renamed.clear(); deltas_applied.clear(); attrs_cleared.clear(); @@ -177,11 +180,24 @@ cset::apply_to(editable_tree & t) const i != files_added.end(); ++i) safe_insert(attaches, attach(t.create_file_node(i->second), i->first)); - // Decompose all path deletion and the first-half of renamings on // existing paths into the set of pending detaches, to be executed // bottom-up. + for (map::const_iterator i = nodes_sutured.begin(); + i != nodes_sutured.end(); ++i) + { + // Sutured node is added + safe_insert(attaches, attach(t.create_file_node(i->second.sutured_id), i->first)); + + // Ancestor nodes are dropped; detach here, drop later. + safe_insert(detaches, detach(i->second.first_ancestor)); + + if (!i->second.second_ancestor.empty()) + safe_insert(detaches, detach(i->second.second_ancestor)); + } + + for (set::const_iterator i = nodes_deleted.begin(); i != nodes_deleted.end(); ++i) safe_insert(detaches, detach(*i)); @@ -247,6 +263,9 @@ namespace symbol const content("content"); symbol const add_file("add_file"); symbol const add_dir("add_dir"); + symbol const sutured_file("sutured_file"); + symbol const first_ancestor("first_ancestor"); + symbol const second_ancestor("second_ancestor"); symbol const patch("patch"); symbol const from("from"); symbol const to("to"); @@ -295,6 +314,17 @@ print_cset(basic_io::printer & printer, printer.print_stanza(st); } + for (map::const_iterator i = cs.nodes_sutured.begin(); + i != cs.nodes_sutured.end(); ++i) + { + basic_io::stanza st; + st.push_file_pair(syms::sutured_file, i->first); + st.push_file_pair(syms::first_ancestor, i->second.first_ancestor); + st.push_file_pair(syms::second_ancestor, i->second.second_ancestor); + st.push_binary_pair(syms::content, i->second.sutured_id.inner()); + printer.print_stanza(st); + } + for (map >::const_iterator i = cs.deltas_applied.begin(); i != cs.deltas_applied.end(); ++i) { @@ -342,7 +372,7 @@ parse_cset(basic_io::parser & parser, string t1, t2; MM(t1); MM(t2); - file_path p1, p2; + file_path p1, p2, p3; MM(p1); MM(p2); @@ -398,6 +428,22 @@ parse_cset(basic_io::parser & parser, } prev_path.clear(); + while (parser.symp(syms::sutured_file)) + { + parser.sym(); + parse_path(parser, p1); + I(prev_path.empty() || prev_path < p1); + prev_path = p1; + parser.esym(syms::first_ancestor); + parse_path(parser, p2); + parser.esym(syms::second_ancestor); + parse_path(parser, p3); + parser.esym(syms::content); + parser.hex(t1); + safe_insert(cs.nodes_sutured, make_pair(p1, cset::sutured_t(p2, p3, file_id(decode_hexenc(t1))))); + } + + prev_path.clear(); while (parser.symp(syms::patch)) { parser.sym(); ============================================================ --- cset.hh 5120df6bd9c521f03437fc04e5f02ed7dc610b97 +++ cset.hh 53dcfec8e054e927c0615188e716548390460a3f @@ -1,6 +1,7 @@ #ifndef __CSET_HH__ #define __CSET_HH__ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2005 Nathaniel Smith // // This program is made available under the GNU GPL version 2.0 or @@ -56,6 +57,26 @@ struct cset std::set dirs_added; std::map files_added; + // Sutures. + struct sutured_t + { + // If the suture is resolving a merge conflict, then one ancestor is + // from the left side of the merge, and the other ancestor is from the + // other side of the merge. However, each changeset only shows one of + // these ancestors; there are two changesets for a merged revision. Only + // first_ancestor is non-null in this case. + // + // If the suture is a user command, then both ancestors are from the + // same revision, and both are non-null. + file_path first_ancestor; + file_path second_ancestor; + file_id sutured_id; + + sutured_t(file_path the_first, file_path the_second, file_id the_sutured_id) : + first_ancestor (the_first), second_ancestor (the_second), sutured_id (the_sutured_id) {}; + }; + std::map nodes_sutured; + // Pure renames. std::map nodes_renamed; ============================================================ --- roster.cc 70af963b64dd2729562ca29ae346392ff299c108 +++ roster.cc 8befc5f69097a05b8c58ef24bb37d97378b7e7b0 @@ -1,3 +1,4 @@ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2005 Nathaniel Smith // // This program is made available under the GNU GPL version 2.0 or @@ -263,6 +264,7 @@ dump(node_t const & n, string & out) string attr_map_s; dump(n->attrs, attr_map_s); oss << "attrs:\n" << attr_map_s; + oss << "ancestors: " << n->ancestors.first << ' ' << n->ancestors.second << '\n'; oss << "type: "; if (is_file_t(n)) { @@ -826,17 +828,23 @@ node_id // for it into the old_locations member, because there is no old_location to // forbid node_id -roster_t::create_dir_node(node_id_source & nis) +roster_t::create_dir_node(node_id_source & nis, std::pair ancestors) { node_id nid = nis.next(); - create_dir_node(nid); + create_dir_node(nid, ancestors); return nid; } void roster_t::create_dir_node(node_id nid) { + create_dir_node(nid, make_pair(nid, the_null_node)); +} +void +roster_t::create_dir_node(node_id nid, std::pair ancestors) +{ dir_t d = dir_t(new dir_node()); d->self = nid; + d->ancestors = ancestors; safe_insert(nodes, make_pair(nid, d)); } @@ -845,18 +853,24 @@ node_id // for it into the old_locations member, because there is no old_location to // forbid node_id -roster_t::create_file_node(file_id const & content, node_id_source & nis) +roster_t::create_file_node(file_id const & content, node_id_source & nis, std::pair ancestors) { node_id nid = nis.next(); - create_file_node(content, nid); + create_file_node(content, nid, ancestors); return nid; } void roster_t::create_file_node(file_id const & content, node_id nid) { + create_file_node(content, nid, make_pair(nid, the_null_node)); +} +void +roster_t::create_file_node(file_id const & content, node_id nid, std::pair ancestors) +{ file_t f = file_t(new file_node()); f->self = nid; f->content = content; + f->ancestors = ancestors; safe_insert(nodes, make_pair(nid, f)); } @@ -1376,12 +1390,12 @@ namespace // this, the two rosters will have identical node_ids at every path. union_new_nodes(left, left_new, right, right_new, nis); - // The other thing we need to fix up is attr corpses. Live attrs are made - // identical by the csets; but if, say, on one side of a fork an attr is - // added and then deleted, then one of our incoming merge rosters will - // have a corpse for that attr, and the other will not. We need to make - // sure at both of them end up with the corpse. This function fixes up - // that. + // The other thing we need to fix up is attr corpses. Live attrs are + // made identical by the csets; but if, say, on one side of a fork an + // attr is added and then deleted, then one of our incoming merge + // rosters will have a corpse for that attr, and the other will not. We + // need to make sure that both of them end up with the corpse. This + // function does that. union_corpses(left, right); } @@ -2043,26 +2057,84 @@ namespace node_id nid, node_t n, cset & cs) { + // Node is deleted; it may have been sutured into another new node + // + // We cannot easily tell which here - we'd have to search the 'to' + // roster for a node with this node as an ancestor. However, we can just + // record this node as deleted now, and change it later when the suture + // is seen. Note that the suture will be on a later node; nodes are + // processed in order, and new nodes occur after deleted nodes. + file_path pth; from.get_name(nid, pth); safe_insert(cs.nodes_deleted, pth); } - void delta_only_in_to(roster_t const & to, node_id nid, node_t n, + void delta_only_in_to(roster_t const & from, + roster_t const & to, + node_id nid, + node_t n, cset & cs) { + MM(nid); + + // Node is new; it may be a suture of two deleted nodes file_path pth; to.get_name(nid, pth); - if (is_file_t(n)) + + // Workspace rosters always have ancestors = null. The root node cannot + // be sutured. + if (n->ancestors.first != the_null_node && n->ancestors.first != nid) { - safe_insert(cs.files_added, - make_pair(pth, downcast_to_file_t(n)->content)); + // copy not implemented yet; this is a suture + I(n->ancestors.second != the_null_node); + + I(is_file_t(n)); // can't suture directories + + file_path first_anc, second_anc; + + // 'from' may be either left or right merge parent, or this could + // be a user suture. So either ancestor could be in either 'from' or + // the other parent revision of the merge. If it's in the other + // parent, it will show up in the other changeset. + // + // If we only have one ancestor name, it goes in first_anc. + + if (from.has_node(n->ancestors.first)) + { + from.get_name(n->ancestors.first, first_anc); + safe_erase(cs.nodes_deleted, first_anc); + + if (from.has_node(n->ancestors.second)) + { + from.get_name (n->ancestors.second, second_anc); + safe_erase(cs.nodes_deleted, second_anc); + } + } + else + { + from.get_name(n->ancestors.second, first_anc); + safe_erase(cs.nodes_deleted, first_anc); + } + + safe_insert(cs.nodes_sutured, + make_pair(pth, cset::sutured_t(first_anc, second_anc, downcast_to_file_t(n)->content))); } else { - safe_insert(cs.dirs_added, pth); + // just added + if (is_file_t(n)) + { + safe_insert(cs.files_added, + make_pair(pth, downcast_to_file_t(n)->content)); + } + else + { + safe_insert(cs.dirs_added, pth); + } } + for (full_attr_map_t::const_iterator i = n->attrs.begin(); i != n->attrs.end(); ++i) if (i->second.first) @@ -2140,6 +2212,8 @@ make_cset(roster_t const & from, roster_ void make_cset(roster_t const & from, roster_t const & to, cset & cs) { + MM(from); + MM(to); cs.clear(); parallel::iter i(from.all_nodes(), to.all_nodes()); while (i.next()) @@ -2151,13 +2225,13 @@ make_cset(roster_t const & from, roster_ I(false); case parallel::in_left: - // deleted + // deleted or sutured delta_only_in_from(from, i.left_key(), i.left_data(), cs); break; case parallel::in_right: - // added - delta_only_in_to(to, i.right_key(), i.right_data(), cs); + // added or sutured + delta_only_in_to(from, to, i.right_key(), i.right_data(), cs); break; case parallel::in_both: @@ -2390,6 +2464,14 @@ select_nodes_modified_by_cset(cset const i != cs.nodes_renamed.end(); ++i) modified_prestate_nodes.insert(i->first); + for (map::const_iterator i = cs.nodes_sutured.begin(); + i != cs.nodes_sutured.end(); ++i) + { + modified_prestate_nodes.insert(i->first); // post-state; sutured file added + modified_prestate_nodes.insert(i->second.first_ancestor); // pre-state; ancestors deleted + modified_prestate_nodes.insert(i->second.second_ancestor); + } + // Post-state damage copy(cs.dirs_added.begin(), cs.dirs_added.end(), ============================================================ --- roster.hh 0a976adbca1e66062d5bd9fcfb063eef07427f73 +++ roster.hh e22fbd6c2eea9615337de4319fca8353c587b13d @@ -1,6 +1,7 @@ #ifndef __ROSTER_HH__ #define __ROSTER_HH__ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2005 Nathaniel Smith // // This program is made available under the GNU GPL version 2.0 or @@ -24,6 +25,7 @@ node_id const the_null_node = 0; /////////////////////////////////////////////////////////////////// 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) @@ -39,10 +41,24 @@ struct node node(); node(node_id i); node_id self; - node_id parent; // the_null_node iff this is a root dir + node_id parent; // directory containing this node; the_null_node iff this is a root dir path_component name; // the_null_component iff this is a root dir full_attr_map_t attrs; + std::pair ancestors; + // new, resurrected: first, second = the_null_node + // sutured: first = left, second = right + // copied: first = copy source, second = the_null_node + // otherwise: first = self, second = the_null_node + // + // in workspace rosters, ancestors is always null + // + // We currently only support suture as a merge conflict resolution, so + // first and second are from different parent rosters. FIXME: if we add + // support for a user suture command, they will be from the same parent + // roster; we will need to record that somehow, and indicate it in + // changesets. + // need a virtual function to make dynamic_cast work virtual node_t clone() = 0; virtual ~node() {} @@ -166,12 +182,18 @@ public: // editable_tree operations node_id detach_node(file_path const & src); void drop_detached_node(node_id nid); - node_id create_dir_node(node_id_source & nis); - void create_dir_node(node_id nid); + node_id create_dir_node(node_id_source & nis, + std::pair const ancestors = null_ancestors); + void create_dir_node(node_id nid); // ancestors = (nid, null) + void create_dir_node(node_id nid, std::pair const ancestors); node_id create_file_node(file_id const & content, - node_id_source & nis); + node_id_source & nis, + std::pair const ancestors = null_ancestors); void create_file_node(file_id const & content, - node_id nid); + node_id nid); // ancestors = (nid, null) + void create_file_node(file_id const & content, + node_id nid, + std::pair const ancestors); void attach_node(node_id nid, file_path const & dst); void attach_node(node_id nid, node_id parent, path_component name); void apply_delta(file_path const & pth, @@ -235,51 +257,29 @@ private: void do_deep_copy_from(roster_t const & other); dir_t root_dir; node_map nodes; - // This requires some explanation. There is a particular kind of + // This requires some explanation. There is a particular kind of // nonsensical behavior which we wish to discourage -- when a node is - // detached from some location, and then re-attached at that same location - // (or similarly, if a new node is created, then immediately deleted -- this - // is like the previous case, if you think of "does not exist" as a - // location). In particular, we _must_ error out if a cset attempts to do + // detached from some location, and then re-attached at that same + // location. In particular, we _must_ error out if a cset attempts to do // this, because it indicates that the cset had something non-normalized, - // like "rename a a" in it, and that is illegal. There are two options for - // detecting this. The more natural approach, perhaps, is to keep a chunk + // like "rename a a" in it, and that is illegal. There are two options for + // detecting this. The more natural approach, perhaps, is to keep a chunk // of state around while performing any particular operation (like cset // application) for which we wish to detect these kinds of redundant - // computations. The other option is to keep this state directly within the - // roster, at all times. In the first case, we explicitly turn on checking - // when we want it; the the latter, we must explicitly turn _off_ checking - // when we _don't_ want it. We choose the latter, because it is more - // conservative --- perhaps it will turn out that it is _too_ conservative - // and causes problems, in which case we should probably switch to the - // former. + // computations. The other option is to keep this state directly within + // the roster, at all times. In the first case, we explicitly turn on + // checking when we want it; the the latter, we must explicitly turn _off_ + // checking when we _don't_ want it. We choose the latter, because it is + // more conservative --- perhaps it will turn out that it is _too_ + // conservative and causes problems, in which case we should probably + // switch to the former. // - // FIXME: This _is_ all a little nasty, because this can be a source of - // abstraction leak -- for instance, roster_merge's contract is that nodes - // involved in name-related conflicts will be detached in the roster it returns. - // Those nodes really should be allowed to be attached anywhere, or dropped, - // which is not actually expressible right now. Worse, whether or not they - // are in old_locations map is an implementation detail of roster_merge -- - // it may temporarily attach and then detach the nodes it creates, but this - // is not deterministic or part of its interface. The main time this would - // be a _problem_ is if we add interactive resolution of tree rearrangement - // conflicts -- if someone resolves a rename conflict by saying that one - // side wins, or by deleting one of the conflicting nodes, and this all - // happens in memory, then it may trigger a spurious invariant failure here. - // If anyone ever decides to add this kind of functionality, then it would - // definitely make sense to move this checking into editable_tree. For now, - // though, no such functionality is planned, so we'll see what happens. - // - // Update; now we are adding precisely these operations! workaround; allow - // deleting a detached node with no entry in old_locations - // // The implementation itself uses the map old_locations. A node can be in // the following states: // -- attached, no entry in old_locations map // -- detached, no entry in old_locations map // -- create_dir_node, create_file_node put a node into this state - // -- a node in this state can be attached, anywhere, but may not be - // deleted. Update; can now be deleted. + // -- a node in this state can be attached, anywhere, or deleted. // -- detached, an entry in old_locations map // -- detach_node puts a node into this state // -- a node in this state can be attached anywhere _except_ the @@ -291,6 +291,10 @@ struct temp_node_id_source struct temp_node_id_source : public node_id_source { + // Temp node ids are used for new nodes in rosters. They are converted to + // true node ids when the roster is actually written to the database; see + // union_new_nodes in roster.cc, ultimately called from + // make_roster_for_revision with true_node_id_source temp_node_id_source(); virtual node_id next(); node_id curr; ============================================================ --- roster_merge.cc 992ee1d3932808bd0e059c4e8f984c4fcd39b586 +++ roster_merge.cc 178917d07e72bb9cff7c9616e0e25e0b89eccdfb @@ -1440,47 +1440,46 @@ parse_resolve_conflicts_str(basic_io::pa static void parse_resolve_conflicts_str(basic_io::parser & pars, roster_merge_result & result) { + char const * error_message = "can't specify a %s conflict resolution for more than one conflict"; + while (pars.tok.in.lookahead != EOF) { if (pars.symp (syms::resolved_suture)) { - pars.sym(); + N(result.duplicate_name_conflicts.size() == 1, + F(error_message) % syms::resolved_suture); - for (std::vector::iterator i = result.duplicate_name_conflicts.begin(); - i != result.duplicate_name_conflicts.end(); - ++i) - { - duplicate_name_conflict & conflict = *i; + duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); - conflict.left_resolution.first = resolve_conflicts::suture; - conflict.right_resolution.first = resolve_conflicts::suture; - } + conflict.left_resolution.first = resolve_conflicts::suture; + conflict.right_resolution.first = resolve_conflicts::suture; + pars.sym(); + conflict.left_resolution.second = file_path_internal (pars.token); + pars.str(); } else if (pars.symp (syms::resolved_rename_left)) { - pars.sym(); - N(result.duplicate_name_conflicts.size() == 1, - F("can't specify a rename conflict resolution for more than one conflict")); + F(error_message) % syms::resolved_rename_left); duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); conflict.left_resolution.first = resolve_conflicts::rename; - pars.str(); + pars.sym(); conflict.left_resolution.second = file_path_internal (pars.token); + pars.str(); } else if (pars.symp (syms::resolved_rename_right)) { - pars.sym(); - N(result.duplicate_name_conflicts.size() == 1, - F("can't specify a rename conflict resolution for more than one conflict")); + F(error_message) % syms::resolved_rename_right); duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); conflict.right_resolution.first = resolve_conflicts::rename; - pars.str(); + pars.sym(); conflict.right_resolution.second = file_path_internal (pars.token); + pars.str(); } else N(false, F("%s is not a supported conflict resolution") % pars.token); @@ -1622,49 +1621,44 @@ roster_merge_result::resolve_duplicate_n switch (conflict.left_resolution.first) { case resolve_conflicts::suture: - I(conflict.right_resolution.first == resolve_conflicts::suture); + { + I(conflict.right_resolution.first == resolve_conflicts::suture); - P(F("suturing %s, %s into %s") % left_name % right_name % conflict.left_resolution.second); + N(!is_dir_t(left_roster.get_node (left_nid)), F("can't suture directory : %s") % left_name); - // Create a single new node, delete the two old ones. FIXME: need to - // record the links between the nodes somewhere. - { + P(F("suturing %s, %s into %s") % left_name % right_name % conflict.left_resolution.second); + + // Create a single new node, delete the two old ones, set ancestors. node_id new_nid; file_path const new_file_name = conflict.left_resolution.second; - if (is_dir_t(left_roster.get_node (left_nid))) - new_nid = roster.create_dir_node (nis); - else - { - file_t const left_node = downcast_to_file_t(left_roster.get_node (left_nid)); - file_t const right_node = downcast_to_file_t(right_roster.get_node (right_nid)); + file_t const left_node = downcast_to_file_t(left_roster.get_node (left_nid)); + file_t const right_node = downcast_to_file_t(right_roster.get_node (right_nid)); - N(path::file == get_path_status(new_file_name), - F("%s does not exist or is a directory") % new_file_name); + N(path::file == get_path_status(new_file_name), + F("%s does not exist or is a directory") % new_file_name); - file_id const & left_file_id = left_node->content; - file_id const & right_file_id = right_node->content; - file_id new_file_id; - data new_raw_data; - read_data (new_file_name, new_raw_data); - file_data new_data (new_raw_data); - file_data left_data, right_data; + file_id const & left_file_id = left_node->content; + file_id const & right_file_id = right_node->content; + file_id new_file_id; + data new_raw_data; + read_data (new_file_name, new_raw_data); + file_data new_data (new_raw_data); + file_data left_data, right_data; - adaptor.get_version(left_file_id, left_data); - adaptor.get_version(right_file_id, right_data); - calculate_ident (new_data, new_file_id); + adaptor.get_version(left_file_id, left_data); + adaptor.get_version(right_file_id, right_data); + calculate_ident (new_data, new_file_id); - new_nid = roster.create_file_node (new_file_id, nis); + new_nid = roster.create_file_node (new_file_id, nis, make_pair(left_nid, right_nid)); - adaptor.record_merge(left_file_id, right_file_id, new_file_id, left_data, right_data, new_data); - } + adaptor.record_merge(left_file_id, right_file_id, new_file_id, left_data, right_data, new_data); attach_node (lua, roster, new_nid, new_file_name); roster.drop_detached_node(left_nid); roster.drop_detached_node(right_nid); - } break; ============================================================ --- tests/resolve_duplicate_name_conflict/__driver__.lua 8f477bd075f50532cdb3a519e136122443596aa5 +++ tests/resolve_duplicate_name_conflict/__driver__.lua c6652312c6765180630aff4ebacc7af45e6c8472 @@ -100,8 +100,5 @@ check("thermostat honeywell" == readfile -- Verify file contents check("thermostat westinghouse" == readfile("thermostat-westinghouse.c")) check("thermostat honeywell" == readfile("thermostat-honeywell.c")) - --- This currently fails; the merge during update first adds then drops --- checkout.sh. Need to change diediedie. -check("checkout.sh merged" == readfile("checkout.sh")) +check("checkout.sh merged\n" == readfile("checkout.sh")) -- end of file