# # # patch "Makefile.am" # from [238136fce23461bffa01f285d8a33f4ad2801258] # to [160e83e50254c7e2c88f08d3b4fe6301ee614a35] # # patch "cmd_netsync.cc" # from [fdb66061c29fdb835bc36237373c56ee851d66ef] # to [fdc1d80f91a50232e52e08f3b1a9d4c814470251] # # patch "cmd_ws_commit.cc" # from [b9d4815bb8a9a6659377894d3d5a53539627dbd7] # to [542d6ade8937dff9fa9516b800aef85650225e65] # # patch "diff_patch.cc" # from [b23b03a134bdb27df76d383dbb0b03a9b62f5e37] # to [be1a66379348f926f25b574f808cb0f158139f05] # # patch "diff_patch.hh" # from [639f3d8ca629d87c430cd8039336b3e00fbfaea5] # to [d377b6a5fab0b570b44533383244b7f03cac4c13] # # patch "merge.cc" # from [de79fac21bdff771d5c82c538cce1be1ade81668] # to [f913e742c88967bed03438ba82c443015fefb335] # # patch "merge.hh" # from [8cd4bcbd4dfc7b481569c7725003e42bee951969] # to [837e0f0510935be055622c626378e289e6f5a59e] # # patch "roster_merge.hh" # from [4fa9261ad66726a6d755d50ff54e6751c14a39dc] # to [f530fb6fe1086d629f82d12b73d5019754615c48] # # patch "work.cc" # from [e2ed785cc9a7ec181639bf9c5ce989855997ab9b] # to [56a3a0f37a20d60df660b7d0cf00092ca3b6470b] # ============================================================ --- Makefile.am 238136fce23461bffa01f285d8a33f4ad2801258 +++ Makefile.am 160e83e50254c7e2c88f08d3b4fe6301ee614a35 @@ -318,7 +318,7 @@ UNIT_TEST_OBJ_SUPPORT = \ mtn-epoch.$(OBJEXT) mtn-file_io.$(OBJEXT) mtn-gzip.$(OBJEXT) \ mtn-hmac.$(OBJEXT) mtn-inodeprint.$(OBJEXT) \ mtn-key_store.$(OBJEXT) mtn-keys.$(OBJEXT) mtn-lcs.$(OBJEXT) \ - mtn-lua.$(OBJEXT) mtn-lua_hooks.$(OBJEXT) \ + mtn-lua.$(OBJEXT) mtn-lua_hooks.$(OBJEXT) mtn-merge.$(OBJEXT) \ mtn-merkle_tree.$(OBJEXT) mtn-pcrewrap.$(OBJEXT) \ mtn-project.$(OBJEXT) mtn-sanity.$(OBJEXT) \ mtn-schema.$(OBJEXT) mtn-migrate_schema.$(OBJEXT) \ ============================================================ --- cmd_netsync.cc fdb66061c29fdb835bc36237373c56ee851d66ef +++ cmd_netsync.cc fdc1d80f91a50232e52e08f3b1a9d4c814470251 @@ -1,7 +1,7 @@ #include "base.hh" #include "cmd.hh" -#include "diff_patch.hh" +#include "merge.hh" #include "netcmd.hh" #include "globish.hh" #include "keys.hh" ============================================================ --- cmd_ws_commit.cc b9d4815bb8a9a6659377894d3d5a53539627dbd7 +++ cmd_ws_commit.cc 542d6ade8937dff9fa9516b800aef85650225e65 @@ -12,7 +12,7 @@ #include #include "cmd.hh" -#include "diff_patch.hh" +#include "merge.hh" #include "file_io.hh" #include "restrictions.hh" #include "revision.hh" ============================================================ --- diff_patch.cc b23b03a134bdb27df76d383dbb0b03a9b62f5e37 +++ diff_patch.cc be1a66379348f926f25b574f808cb0f158139f05 @@ -8,34 +8,19 @@ // implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. - #include "base.hh" -#include -#include -#include -#include "vector.hh" +#include "diff_patch.hh" -#include -#include -#include "diff_patch.hh" +#include "file_io.hh" #include "interner.hh" #include "lcs.hh" -#include "roster.hh" -#include "safe_map.hh" -#include "sanity.hh" -#include "xdelta.hh" +#include "pcrewrap.hh" #include "simplestring_xform.hh" -#include "vocab.hh" -#include "revision.hh" -#include "constants.hh" -#include "file_io.hh" -#include "pcrewrap.hh" -#include "lua_hooks.hh" -#include "database.hh" -#include "transforms.hh" -using std::make_pair; -using std::map; +#include +#include +#include + using std::min; using std::max; using std::ostream; @@ -43,8 +28,9 @@ using std::vector; using std::string; using std::swap; using std::vector; +using boost::scoped_ptr; -using boost::shared_ptr; +struct conflict {}; // // a 3-way merge works like this: @@ -485,502 +471,6 @@ bool merge3(vector const & ances return true; } - -/////////////////////////////////////////////////////////////////////////// -// content_merge_database_adaptor -/////////////////////////////////////////////////////////////////////////// - - -content_merge_database_adaptor::content_merge_database_adaptor(database & db, - revision_id const & left, - revision_id const & right, - marking_map const & left_mm, - marking_map const & right_mm) - : db(db), left_rid (left), right_rid (right), left_mm(left_mm), right_mm(right_mm) -{ - // FIXME: possibly refactor to run this lazily, as we don't - // need to find common ancestors if we're never actually - // called on to do content merging. - find_common_ancestor_for_merge(db, left, right, lca); -} - -void -content_merge_database_adaptor::record_merge(file_id const & left_ident, - file_id const & right_ident, - file_id const & merged_ident, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data) -{ - L(FL("recording successful merge of %s <-> %s into %s") - % left_ident - % right_ident - % merged_ident); - - transaction_guard guard(db); - - if (!(left_ident == merged_ident)) - { - delta left_delta; - diff(left_data.inner(), merged_data.inner(), left_delta); - db.put_file_version(left_ident, merged_ident, file_delta(left_delta)); - } - if (!(right_ident == merged_ident)) - { - delta right_delta; - diff(right_data.inner(), merged_data.inner(), right_delta); - db.put_file_version(right_ident, merged_ident, file_delta(right_delta)); - } - guard.commit(); -} - -void -content_merge_database_adaptor::record_file(file_id const & parent_ident, - file_id const & merged_ident, - file_data const & parent_data, - file_data const & merged_data) -{ - L(FL("recording file %s -> %s") - % parent_ident - % merged_ident); - - transaction_guard guard(db); - - if (!(parent_ident == merged_ident)) - { - delta parent_delta; - diff(parent_data.inner(), merged_data.inner(), parent_delta); - db.put_file_version(parent_ident, merged_ident, file_delta(parent_delta)); - } - guard.commit(); -} - -void -content_merge_database_adaptor::cache_roster(revision_id const & rid, - boost::shared_ptr roster) -{ - safe_insert(rosters, make_pair(rid, roster)); -}; - -static void -load_and_cache_roster(database & db, revision_id const & rid, - map > & rmap, - shared_ptr & rout) -{ - map >::const_iterator i = rmap.find(rid); - if (i != rmap.end()) - rout = i->second; - else - { - cached_roster cr; - db.get_roster(rid, cr); - safe_insert(rmap, make_pair(rid, cr.first)); - rout = cr.first; - } -} - -void -content_merge_database_adaptor::get_ancestral_roster(node_id nid, - revision_id & rid, - shared_ptr & anc) -{ - // Given a file, if the lca is nonzero and its roster contains the file, - // then we use its roster. Otherwise we use the roster at the file's - // birth revision, which is the "per-file worst case" lca. - - // Begin by loading any non-empty file lca roster - rid = lca; - if (!lca.inner()().empty()) - load_and_cache_roster(db, lca, rosters, anc); - - // If there is no LCA, or the LCA's roster doesn't contain the file, - // then use the file's birth roster. - if (!anc || !anc->has_node(nid)) - { - marking_map::const_iterator lmm = left_mm.find(nid); - marking_map::const_iterator rmm = right_mm.find(nid); - - MM(left_mm); - MM(right_mm); - - if (lmm == left_mm.end()) - { - I(rmm != right_mm.end()); - rid = rmm->second.birth_revision; - } - else if (rmm == right_mm.end()) - { - I(lmm != left_mm.end()); - rid = lmm->second.birth_revision; - } - else - { - I(lmm->second.birth_revision == rmm->second.birth_revision); - rid = lmm->second.birth_revision; - } - - load_and_cache_roster(db, rid, rosters, anc); - } - I(anc); -} - -void -content_merge_database_adaptor::get_version(file_id const & ident, - file_data & dat) const -{ - db.get_file_version(ident, dat); -} - - -/////////////////////////////////////////////////////////////////////////// -// content_merge_workspace_adaptor -/////////////////////////////////////////////////////////////////////////// - -void -content_merge_workspace_adaptor::cache_roster(revision_id const & rid, - boost::shared_ptr roster) -{ - rosters.insert(std::make_pair(rid, roster)); -} - -void -content_merge_workspace_adaptor::record_merge(file_id const & left_id, - file_id const & right_id, - file_id const & merged_id, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data) -{ - L(FL("temporarily recording merge of %s <-> %s into %s") - % left_id - % right_id - % merged_id); - // this is an insert instead of a safe_insert because it is perfectly - // legal (though rare) to have multiple merges resolve to the same file - // contents. - temporary_store.insert(make_pair(merged_id, merged_data)); -} - -void -content_merge_workspace_adaptor::record_file(file_id const & parent_id, - file_id const & merged_id, - file_data const & parent_data, - file_data const & merged_data) -{ - L(FL("temporarily recording file %s -> %s") - % parent_id - % merged_id); - // this is an insert instead of a safe_insert because it is perfectly - // legal (though rare) to have multiple merges resolve to the same file - // contents. - temporary_store.insert(make_pair(merged_id, merged_data)); -} - -void -content_merge_workspace_adaptor::get_ancestral_roster(node_id nid, - revision_id & rid, - shared_ptr & anc) -{ - // Begin by loading any non-empty file lca roster - if (base->has_node(nid)) - { - rid = lca; - anc = base; - } - else - { - marking_map::const_iterator lmm = left_mm.find(nid); - marking_map::const_iterator rmm = right_mm.find(nid); - - MM(left_mm); - MM(right_mm); - - if (lmm == left_mm.end()) - { - I(rmm != right_mm.end()); - rid = rmm->second.birth_revision; - } - else if (rmm == right_mm.end()) - { - I(lmm != left_mm.end()); - rid = lmm->second.birth_revision; - } - else - { - I(lmm->second.birth_revision == rmm->second.birth_revision); - rid = lmm->second.birth_revision; - } - - load_and_cache_roster(db, rid, rosters, anc); - } - I(anc); -} - -void -content_merge_workspace_adaptor::get_version(file_id const & ident, - file_data & dat) const -{ - map::const_iterator i = temporary_store.find(ident); - if (i != temporary_store.end()) - dat = i->second; - else if (db.file_version_exists(ident)) - db.get_file_version(ident, dat); - else - { - data tmp; - file_id fid; - map::const_iterator i = content_paths.find(ident); - I(i != content_paths.end()); - - require_path_is_file(i->second, - F("file '%s' does not exist in workspace") % i->second, - F("'%s' in workspace is a directory, not a file") % i->second); - read_data(i->second, tmp); - calculate_ident(file_data(tmp), fid); - E(fid == ident, origin::system, - F("file %s in workspace has id %s, wanted %s") - % i->second - % fid - % ident); - dat = file_data(tmp); - } -} - - -/////////////////////////////////////////////////////////////////////////// -// content_merge_checkout_adaptor -/////////////////////////////////////////////////////////////////////////// - -void -content_merge_checkout_adaptor::record_merge(file_id const & left_ident, - file_id const & right_ident, - file_id const & merged_ident, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data) -{ - I(false); -} - -void -content_merge_checkout_adaptor::record_file(file_id const & parent_ident, - file_id const & merged_ident, - file_data const & parent_data, - file_data const & merged_data) -{ - I(false); -} - -void -content_merge_checkout_adaptor::get_ancestral_roster(node_id nid, - revision_id & rid, - shared_ptr & anc) -{ - I(false); -} - -void -content_merge_checkout_adaptor::get_version(file_id const & ident, - file_data & dat) const -{ - db.get_file_version(ident, dat); -} - - -/////////////////////////////////////////////////////////////////////////// -// content_merger -/////////////////////////////////////////////////////////////////////////// - -string -content_merger::get_file_encoding(file_path const & path, - roster_t const & ros) -{ - attr_value v; - if (ros.get_attr(path, attr_key(constants::encoding_attribute), v)) - return v(); - return constants::default_encoding; -} - -bool -content_merger::attribute_manual_merge(file_path const & path, - roster_t const & ros) -{ - attr_value v; - if (ros.get_attr(path, attr_key(constants::manual_merge_attribute), v) - && v() == "true") - return true; - return false; // default: enable auto merge -} - -bool -content_merger::attempt_auto_merge(file_path const & anc_path, // inputs - file_path const & left_path, - file_path const & right_path, - file_id const & ancestor_id, - file_id const & left_id, - file_id const & right_id, - file_data & left_data, // outputs - file_data & right_data, - file_data & merge_data) -{ - I(left_id != right_id); - - if (attribute_manual_merge(left_path, left_ros) || - attribute_manual_merge(right_path, right_ros)) - { - return false; - } - - // both files mergeable by monotone internal algorithm, try to merge - // note: the ancestor is not considered for manual merging. Forcing the - // user to merge manually just because of an ancestor mistakenly marked - // manual seems too harsh - - file_data ancestor_data; - - adaptor.get_version(left_id, left_data); - adaptor.get_version(ancestor_id, ancestor_data); - adaptor.get_version(right_id, right_data); - - data const left_unpacked = left_data.inner(); - data const ancestor_unpacked = ancestor_data.inner(); - data const right_unpacked = right_data.inner(); - - string const left_encoding(get_file_encoding(left_path, left_ros)); - string const anc_encoding(get_file_encoding(anc_path, anc_ros)); - string const right_encoding(get_file_encoding(right_path, right_ros)); - - vector left_lines, ancestor_lines, right_lines, merged_lines; - split_into_lines(left_unpacked(), left_encoding, left_lines); - split_into_lines(ancestor_unpacked(), anc_encoding, ancestor_lines); - split_into_lines(right_unpacked(), right_encoding, right_lines); - - if (merge3(ancestor_lines, left_lines, right_lines, merged_lines)) - { - string tmp; - - join_lines(merged_lines, tmp); - merge_data = file_data(tmp, origin::internal); - return true; - } - - return false; -} - -bool -content_merger::try_auto_merge(file_path const & anc_path, - file_path const & left_path, - file_path const & right_path, - file_path const & merged_path, - file_id const & ancestor_id, - file_id const & left_id, - file_id const & right_id, - file_id & merged_id) -{ - // This version of try_to_merge_files should only be called when there is a - // real merge3 to perform. - I(!null_id(ancestor_id)); - I(!null_id(left_id)); - I(!null_id(right_id)); - - L(FL("trying auto merge '%s' %s <-> %s (ancestor: %s)") - % merged_path - % left_id - % right_id - % ancestor_id); - - if (left_id == right_id) - { - L(FL("files are identical")); - merged_id = left_id; - return true; - } - - file_data left_data, right_data, merge_data; - - if (attempt_auto_merge(anc_path, left_path, right_path, - ancestor_id, left_id, right_id, - left_data, right_data, merge_data)) - { - L(FL("internal 3-way merged ok")); - calculate_ident(merge_data, merged_id); - - adaptor.record_merge(left_id, right_id, merged_id, - left_data, right_data, merge_data); - - return true; - } - - return false; -} - -bool -content_merger::try_user_merge(file_path const & anc_path, - file_path const & left_path, - file_path const & right_path, - file_path const & merged_path, - file_id const & ancestor_id, - file_id const & left_id, - file_id const & right_id, - file_id & merged_id) -{ - // This version of try_to_merge_files should only be called when there is a - // real merge3 to perform. - I(!null_id(ancestor_id)); - I(!null_id(left_id)); - I(!null_id(right_id)); - - L(FL("trying user merge '%s' %s <-> %s (ancestor: %s)") - % merged_path - % left_id - % right_id - % ancestor_id); - - if (left_id == right_id) - { - L(FL("files are identical")); - merged_id = left_id; - return true; - } - - file_data left_data, right_data, ancestor_data; - data left_unpacked, ancestor_unpacked, right_unpacked, merged_unpacked; - - adaptor.get_version(left_id, left_data); - adaptor.get_version(ancestor_id, ancestor_data); - adaptor.get_version(right_id, right_data); - - left_unpacked = left_data.inner(); - ancestor_unpacked = ancestor_data.inner(); - right_unpacked = right_data.inner(); - - P(F("help required for 3-way merge\n" - "[ancestor] %s\n" - "[ left] %s\n" - "[ right] %s\n" - "[ merged] %s") - % anc_path - % left_path - % right_path - % merged_path); - - if (lua.hook_merge3(anc_path, left_path, right_path, merged_path, - ancestor_unpacked, left_unpacked, - right_unpacked, merged_unpacked)) - { - file_data merge_data(merged_unpacked); - - L(FL("lua merge3 hook merged ok")); - calculate_ident(merge_data, merged_id); - - adaptor.record_merge(left_id, right_id, merged_id, - left_data, right_data, merge_data); - return true; - } - - return false; -} - // the remaining part of this file just handles printing out various // diff formats for the case where someone wants to *read* a diff // rather than apply it. ============================================================ --- diff_patch.hh 639f3d8ca629d87c430cd8039336b3e00fbfaea5 +++ diff_patch.hh d377b6a5fab0b570b44533383244b7f03cac4c13 @@ -11,19 +11,13 @@ // implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. -#include "rev_types.hh" -// XXX needed for gcc 3.3 which will otherwise complain that struct file_path -// is just a forward in rev_types.hh and therefor leads to an incomplete type -#include "paths.hh" +// this file is to contain some stripped down, in-process implementations +// of GNU-diffutils-like things (diff, diff3, maybe patch..) -class database; -class lua_hooks; +#include "vector.hh" +#include "vocab.hh" -struct conflict {}; -// this file is to contain some stripped down, in-process implementations -// of GNU-diffutils-like things (diff, diff3, maybe patch..) - void make_diff(std::string const & filename1, std::string const & filename2, file_id const & id1, @@ -39,208 +33,6 @@ bool merge3(std::vector con std::vector const & right, std::vector & merged); -struct -content_merge_adaptor -{ - virtual void record_merge(file_id const & left_ident, - file_id const & right_ident, - file_id const & merged_ident, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data) = 0; - - // For use when one side of the merge is dropped - virtual void record_file(file_id const & parent_ident, - file_id const & merged_ident, - file_data const & parent_data, - file_data const & merged_data) = 0; - - virtual void get_ancestral_roster(node_id nid, - revision_id & rid, - boost::shared_ptr & anc) = 0; - - virtual void get_version(file_id const & ident, - file_data & dat) const = 0; - - virtual ~content_merge_adaptor() {} -}; - -struct -content_merge_database_adaptor - : public content_merge_adaptor -{ - database & db; - revision_id lca; - revision_id left_rid; - revision_id right_rid; - marking_map const & left_mm; - marking_map const & right_mm; - std::map > rosters; - content_merge_database_adaptor(database & db, - revision_id const & left, - revision_id const & right, - marking_map const & left_mm, - marking_map const & right_mm); - void record_merge(file_id const & left_ident, - file_id const & right_ident, - file_id const & merged_ident, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data); - - void record_file(file_id const & parent_ident, - file_id const & merged_ident, - file_data const & parent_data, - file_data const & merged_data); - - void cache_roster(revision_id const & rid, - boost::shared_ptr roster); - - void get_ancestral_roster(node_id nid, - revision_id & rid, - boost::shared_ptr & anc); - - void get_version(file_id const & ident, - file_data & dat) const; -}; - -struct -content_merge_workspace_adaptor - : public content_merge_adaptor -{ - std::map temporary_store; - database & db; - revision_id const lca; - boost::shared_ptr base; - marking_map const & left_mm; - marking_map const & right_mm; - std::map > rosters; - std::map content_paths; - content_merge_workspace_adaptor(database & db, - revision_id const & lca, - boost::shared_ptr base, - marking_map const & left_mm, - marking_map const & right_mm, - std::map const & paths) - : db(db), lca(lca), base(base), - left_mm(left_mm), right_mm(right_mm), content_paths(paths) - {} - - void cache_roster(revision_id const & rid, - boost::shared_ptr roster); - - void record_merge(file_id const & left_ident, - file_id const & right_ident, - file_id const & merged_ident, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data); - - void record_file(file_id const & parent_ident, - file_id const & merged_ident, - file_data const & parent_data, - file_data const & merged_data); - - void get_ancestral_roster(node_id nid, - revision_id & rid, - boost::shared_ptr & anc); - - void get_version(file_id const & ident, - file_data & dat) const; -}; - -struct -content_merge_checkout_adaptor - : public content_merge_adaptor -{ - database & db; - content_merge_checkout_adaptor(database & db) - : db(db) - {} - - void record_merge(file_id const & left_ident, - file_id const & right_ident, - file_id const & merged_ident, - file_data const & left_data, - file_data const & right_data, - file_data const & merged_data); - - void record_file(file_id const & parent_ident, - file_id const & merged_ident, - file_data const & parent_data, - file_data const & merged_data); - - void get_ancestral_roster(node_id nid, - revision_id & rid, - boost::shared_ptr & anc); - - void get_version(file_id const & ident, - file_data & dat) const; - -}; - - -struct content_merger -{ - lua_hooks & lua; - roster_t const & anc_ros; - roster_t const & left_ros; - roster_t const & right_ros; - - content_merge_adaptor & adaptor; - - content_merger(lua_hooks & lua, - roster_t const & anc_ros, - roster_t const & left_ros, - roster_t const & right_ros, - content_merge_adaptor & adaptor) - : lua(lua), - anc_ros(anc_ros), - left_ros(left_ros), - right_ros(right_ros), - adaptor(adaptor) - {} - - // Attempt merge3 on a file (line by line). Return true and valid data if - // it would succeed; false and invalid data otherwise. - bool attempt_auto_merge(file_path const & anc_path, // inputs - file_path const & left_path, - file_path const & right_path, - file_id const & ancestor_id, - file_id const & left_id, - file_id const & right_id, - file_data & left_data, // outputs - file_data & right_data, - file_data & merge_data); - - // Attempt merge3 on a file (line by line). If it succeeded, store results - // in database and return true and valid merged_id; return false - // otherwise. - bool try_auto_merge(file_path const & anc_path, - file_path const & left_path, - file_path const & right_path, - file_path const & merged_path, - file_id const & ancestor_id, - file_id const & left_id, - file_id const & right, - file_id & merged_id); - - bool try_user_merge(file_path const & anc_path, - file_path const & left_path, - file_path const & right_path, - file_path const & merged_path, - file_id const & ancestor_id, - file_id const & left_id, - file_id const & right, - file_id & merged_id); - - std::string get_file_encoding(file_path const & path, - roster_t const & ros); - - bool attribute_manual_merge(file_path const & path, - roster_t const & ros); -}; - // Local Variables: // mode: C++ // fill-column: 76 ============================================================ --- merge.cc de79fac21bdff771d5c82c538cce1be1ade81668 +++ merge.cc f913e742c88967bed03438ba82c443015fefb335 @@ -9,113 +9,606 @@ // PURPOSE. #include "base.hh" -#include -#include +#include "merge.hh" -#include - +#include "constants.hh" +#include "database.hh" #include "diff_patch.hh" -#include "merge.hh" -#include "options.hh" +#include "file_io.hh" +#include "lua_hooks.hh" #include "revision.hh" #include "roster_merge.hh" +#include "simplestring_xform.hh" +#include "transforms.hh" +#include "xdelta.hh" + #include "safe_map.hh" -#include "transforms.hh" -#include "database.hh" +#include "vector.hh" +#include +#include using std::make_pair; using std::map; using std::set; +using std::string; using std::vector; +using boost::shared_ptr; -using boost::shared_ptr; +/////////////////////////////////////////////////////////////////////////// +// content_merge_database_adaptor +/////////////////////////////////////////////////////////////////////////// -namespace +content_merge_database_adaptor::content_merge_database_adaptor(database & db, + revision_id const & left, + revision_id const & right, + marking_map const & left_mm, + marking_map const & right_mm) + : db(db), left_rid (left), right_rid (right), left_mm(left_mm), right_mm(right_mm) { - enum merge_method { auto_merge, user_merge }; + // FIXME: possibly refactor to run this lazily, as we don't + // need to find common ancestors if we're never actually + // called on to do content merging. + find_common_ancestor_for_merge(db, left, right, lca); +} - void - try_to_merge_files(lua_hooks & lua, - roster_t const & left_roster, roster_t const & right_roster, - roster_merge_result & result, content_merge_adaptor & adaptor, - merge_method const method) - { - size_t cnt; - size_t total_conflicts = result.file_content_conflicts.size(); - std::vector::iterator it; +void +content_merge_database_adaptor::record_merge(file_id const & left_ident, + file_id const & right_ident, + file_id const & merged_ident, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data) +{ + L(FL("recording successful merge of %s <-> %s into %s") + % left_ident + % right_ident + % merged_ident); - for (cnt = 1, it = result.file_content_conflicts.begin(); - it != result.file_content_conflicts.end(); ++cnt) - { - file_content_conflict const & conflict = *it; + transaction_guard guard(db); - MM(conflict); + if (!(left_ident == merged_ident)) + { + delta left_delta; + diff(left_data.inner(), merged_data.inner(), left_delta); + db.put_file_version(left_ident, merged_ident, file_delta(left_delta)); + } + if (!(right_ident == merged_ident)) + { + delta right_delta; + diff(right_data.inner(), merged_data.inner(), right_delta); + db.put_file_version(right_ident, merged_ident, file_delta(right_delta)); + } + guard.commit(); +} - revision_id rid; - shared_ptr roster_for_file_lca; - adaptor.get_ancestral_roster(conflict.nid, rid, roster_for_file_lca); +void +content_merge_database_adaptor::record_file(file_id const & parent_ident, + file_id const & merged_ident, + file_data const & parent_data, + file_data const & merged_data) +{ + L(FL("recording file %s -> %s") + % parent_ident + % merged_ident); - // Now we should certainly have a roster, which has the node. - I(roster_for_file_lca); - I(roster_for_file_lca->has_node(conflict.nid)); + transaction_guard guard(db); - file_id anc_id, left_id, right_id; - file_path anc_path, left_path, right_path; - roster_for_file_lca->get_file_details(conflict.nid, anc_id, anc_path); - left_roster.get_file_details(conflict.nid, left_id, left_path); - right_roster.get_file_details(conflict.nid, right_id, right_path); + if (!(parent_ident == merged_ident)) + { + delta parent_delta; + diff(parent_data.inner(), merged_data.inner(), parent_delta); + db.put_file_version(parent_ident, merged_ident, file_delta(parent_delta)); + } + guard.commit(); +} - file_id merged_id; +void +content_merge_database_adaptor::cache_roster(revision_id const & rid, + boost::shared_ptr roster) +{ + safe_insert(rosters, make_pair(rid, roster)); +}; - content_merger cm(lua, *roster_for_file_lca, - left_roster, right_roster, - adaptor); +static void +load_and_cache_roster(database & db, revision_id const & rid, + map > & rmap, + shared_ptr & rout) +{ + map >::const_iterator i = rmap.find(rid); + if (i != rmap.end()) + rout = i->second; + else + { + cached_roster cr; + db.get_roster(rid, cr); + safe_insert(rmap, make_pair(rid, cr.first)); + rout = cr.first; + } +} - bool merged = false; +void +content_merge_database_adaptor::get_ancestral_roster(node_id nid, + revision_id & rid, + shared_ptr & anc) +{ + // Given a file, if the lca is nonzero and its roster contains the file, + // then we use its roster. Otherwise we use the roster at the file's + // birth revision, which is the "per-file worst case" lca. - switch (method) - { - case auto_merge: - merged = cm.try_auto_merge(anc_path, left_path, right_path, - right_path, anc_id, left_id, right_id, - merged_id); - break; + // Begin by loading any non-empty file lca roster + rid = lca; + if (!lca.inner()().empty()) + load_and_cache_roster(db, lca, rosters, anc); - case user_merge: - merged = cm.try_user_merge(anc_path, left_path, right_path, - right_path, anc_id, left_id, right_id, - merged_id); + // If there is no LCA, or the LCA's roster doesn't contain the file, + // then use the file's birth roster. + if (!anc || !anc->has_node(nid)) + { + marking_map::const_iterator lmm = left_mm.find(nid); + marking_map::const_iterator rmm = right_mm.find(nid); - // If the user merge has failed, there's no point - // trying to continue -- we'll only frustrate users by - // encouraging them to continue working with their merge - // tool on a merge that is now destined to fail. - if (!merged) - return; + MM(left_mm); + MM(right_mm); - break; - } + if (lmm == left_mm.end()) + { + I(rmm != right_mm.end()); + rid = rmm->second.birth_revision; + } + else if (rmm == right_mm.end()) + { + I(lmm != left_mm.end()); + rid = lmm->second.birth_revision; + } + else + { + I(lmm->second.birth_revision == rmm->second.birth_revision); + rid = lmm->second.birth_revision; + } - if (merged) - { - L(FL("resolved content conflict %d / %d on file '%s'") - % cnt % total_conflicts % right_path); - file_t f = downcast_to_file_t(result.roster.get_node(conflict.nid)); - f->content = merged_id; + load_and_cache_roster(db, rid, rosters, anc); + } + I(anc); +} - it = result.file_content_conflicts.erase(it); - } - else - { - ++it; - } - } - } +void +content_merge_database_adaptor::get_version(file_id const & ident, + file_data & dat) const +{ + db.get_file_version(ident, dat); +} +/////////////////////////////////////////////////////////////////////////// +// content_merge_workspace_adaptor +/////////////////////////////////////////////////////////////////////////// + +void +content_merge_workspace_adaptor::cache_roster(revision_id const & rid, + boost::shared_ptr roster) +{ + rosters.insert(std::make_pair(rid, roster)); } void +content_merge_workspace_adaptor::record_merge(file_id const & left_id, + file_id const & right_id, + file_id const & merged_id, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data) +{ + L(FL("temporarily recording merge of %s <-> %s into %s") + % left_id + % right_id + % merged_id); + // this is an insert instead of a safe_insert because it is perfectly + // legal (though rare) to have multiple merges resolve to the same file + // contents. + temporary_store.insert(make_pair(merged_id, merged_data)); +} + +void +content_merge_workspace_adaptor::record_file(file_id const & parent_id, + file_id const & merged_id, + file_data const & parent_data, + file_data const & merged_data) +{ + L(FL("temporarily recording file %s -> %s") + % parent_id + % merged_id); + // this is an insert instead of a safe_insert because it is perfectly + // legal (though rare) to have multiple merges resolve to the same file + // contents. + temporary_store.insert(make_pair(merged_id, merged_data)); +} + +void +content_merge_workspace_adaptor::get_ancestral_roster(node_id nid, + revision_id & rid, + shared_ptr & anc) +{ + // Begin by loading any non-empty file lca roster + if (base->has_node(nid)) + { + rid = lca; + anc = base; + } + else + { + marking_map::const_iterator lmm = left_mm.find(nid); + marking_map::const_iterator rmm = right_mm.find(nid); + + MM(left_mm); + MM(right_mm); + + if (lmm == left_mm.end()) + { + I(rmm != right_mm.end()); + rid = rmm->second.birth_revision; + } + else if (rmm == right_mm.end()) + { + I(lmm != left_mm.end()); + rid = lmm->second.birth_revision; + } + else + { + I(lmm->second.birth_revision == rmm->second.birth_revision); + rid = lmm->second.birth_revision; + } + + load_and_cache_roster(db, rid, rosters, anc); + } + I(anc); +} + +void +content_merge_workspace_adaptor::get_version(file_id const & ident, + file_data & dat) const +{ + map::const_iterator i = temporary_store.find(ident); + if (i != temporary_store.end()) + dat = i->second; + else if (db.file_version_exists(ident)) + db.get_file_version(ident, dat); + else + { + data tmp; + file_id fid; + map::const_iterator i = content_paths.find(ident); + I(i != content_paths.end()); + + require_path_is_file(i->second, + F("file '%s' does not exist in workspace") % i->second, + F("'%s' in workspace is a directory, not a file") % i->second); + read_data(i->second, tmp); + calculate_ident(file_data(tmp), fid); + E(fid == ident, origin::system, + F("file %s in workspace has id %s, wanted %s") + % i->second + % fid + % ident); + dat = file_data(tmp); + } +} + + +/////////////////////////////////////////////////////////////////////////// +// content_merge_checkout_adaptor +/////////////////////////////////////////////////////////////////////////// + +void +content_merge_checkout_adaptor::record_merge(file_id const & left_ident, + file_id const & right_ident, + file_id const & merged_ident, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data) +{ + I(false); +} + +void +content_merge_checkout_adaptor::record_file(file_id const & parent_ident, + file_id const & merged_ident, + file_data const & parent_data, + file_data const & merged_data) +{ + I(false); +} + +void +content_merge_checkout_adaptor::get_ancestral_roster(node_id nid, + revision_id & rid, + shared_ptr & anc) +{ + I(false); +} + +void +content_merge_checkout_adaptor::get_version(file_id const & ident, + file_data & dat) const +{ + db.get_file_version(ident, dat); +} + + +/////////////////////////////////////////////////////////////////////////// +// content_merger +/////////////////////////////////////////////////////////////////////////// + +string +content_merger::get_file_encoding(file_path const & path, + roster_t const & ros) +{ + attr_value v; + if (ros.get_attr(path, attr_key(constants::encoding_attribute), v)) + return v(); + return constants::default_encoding; +} + +bool +content_merger::attribute_manual_merge(file_path const & path, + roster_t const & ros) +{ + attr_value v; + if (ros.get_attr(path, attr_key(constants::manual_merge_attribute), v) + && v() == "true") + return true; + return false; // default: enable auto merge +} + +bool +content_merger::attempt_auto_merge(file_path const & anc_path, // inputs + file_path const & left_path, + file_path const & right_path, + file_id const & ancestor_id, + file_id const & left_id, + file_id const & right_id, + file_data & left_data, // outputs + file_data & right_data, + file_data & merge_data) +{ + I(left_id != right_id); + + if (attribute_manual_merge(left_path, left_ros) || + attribute_manual_merge(right_path, right_ros)) + { + return false; + } + + // both files mergeable by monotone internal algorithm, try to merge + // note: the ancestor is not considered for manual merging. Forcing the + // user to merge manually just because of an ancestor mistakenly marked + // manual seems too harsh + + file_data ancestor_data; + + adaptor.get_version(left_id, left_data); + adaptor.get_version(ancestor_id, ancestor_data); + adaptor.get_version(right_id, right_data); + + data const left_unpacked = left_data.inner(); + data const ancestor_unpacked = ancestor_data.inner(); + data const right_unpacked = right_data.inner(); + + string const left_encoding(get_file_encoding(left_path, left_ros)); + string const anc_encoding(get_file_encoding(anc_path, anc_ros)); + string const right_encoding(get_file_encoding(right_path, right_ros)); + + vector left_lines, ancestor_lines, right_lines, merged_lines; + split_into_lines(left_unpacked(), left_encoding, left_lines); + split_into_lines(ancestor_unpacked(), anc_encoding, ancestor_lines); + split_into_lines(right_unpacked(), right_encoding, right_lines); + + if (merge3(ancestor_lines, left_lines, right_lines, merged_lines)) + { + string tmp; + + join_lines(merged_lines, tmp); + merge_data = file_data(tmp, origin::internal); + return true; + } + + return false; +} + +bool +content_merger::try_auto_merge(file_path const & anc_path, + file_path const & left_path, + file_path const & right_path, + file_path const & merged_path, + file_id const & ancestor_id, + file_id const & left_id, + file_id const & right_id, + file_id & merged_id) +{ + // This version of try_to_merge_files should only be called when there is a + // real merge3 to perform. + I(!null_id(ancestor_id)); + I(!null_id(left_id)); + I(!null_id(right_id)); + + L(FL("trying auto merge '%s' %s <-> %s (ancestor: %s)") + % merged_path + % left_id + % right_id + % ancestor_id); + + if (left_id == right_id) + { + L(FL("files are identical")); + merged_id = left_id; + return true; + } + + file_data left_data, right_data, merge_data; + + if (attempt_auto_merge(anc_path, left_path, right_path, + ancestor_id, left_id, right_id, + left_data, right_data, merge_data)) + { + L(FL("internal 3-way merged ok")); + calculate_ident(merge_data, merged_id); + + adaptor.record_merge(left_id, right_id, merged_id, + left_data, right_data, merge_data); + + return true; + } + + return false; +} + +bool +content_merger::try_user_merge(file_path const & anc_path, + file_path const & left_path, + file_path const & right_path, + file_path const & merged_path, + file_id const & ancestor_id, + file_id const & left_id, + file_id const & right_id, + file_id & merged_id) +{ + // This version of try_to_merge_files should only be called when there is a + // real merge3 to perform. + I(!null_id(ancestor_id)); + I(!null_id(left_id)); + I(!null_id(right_id)); + + L(FL("trying user merge '%s' %s <-> %s (ancestor: %s)") + % merged_path + % left_id + % right_id + % ancestor_id); + + if (left_id == right_id) + { + L(FL("files are identical")); + merged_id = left_id; + return true; + } + + file_data left_data, right_data, ancestor_data; + data left_unpacked, ancestor_unpacked, right_unpacked, merged_unpacked; + + adaptor.get_version(left_id, left_data); + adaptor.get_version(ancestor_id, ancestor_data); + adaptor.get_version(right_id, right_data); + + left_unpacked = left_data.inner(); + ancestor_unpacked = ancestor_data.inner(); + right_unpacked = right_data.inner(); + + P(F("help required for 3-way merge\n" + "[ancestor] %s\n" + "[ left] %s\n" + "[ right] %s\n" + "[ merged] %s") + % anc_path + % left_path + % right_path + % merged_path); + + if (lua.hook_merge3(anc_path, left_path, right_path, merged_path, + ancestor_unpacked, left_unpacked, + right_unpacked, merged_unpacked)) + { + file_data merge_data(merged_unpacked); + + L(FL("lua merge3 hook merged ok")); + calculate_ident(merge_data, merged_id); + + adaptor.record_merge(left_id, right_id, merged_id, + left_data, right_data, merge_data); + return true; + } + + return false; +} + +enum merge_method { auto_merge, user_merge }; + +static void +try_to_merge_files(lua_hooks & lua, + roster_t const & left_roster, roster_t const & right_roster, + roster_merge_result & result, content_merge_adaptor & adaptor, + merge_method const method) +{ + size_t cnt; + size_t total_conflicts = result.file_content_conflicts.size(); + std::vector::iterator it; + + for (cnt = 1, it = result.file_content_conflicts.begin(); + it != result.file_content_conflicts.end(); ++cnt) + { + file_content_conflict const & conflict = *it; + + MM(conflict); + + revision_id rid; + shared_ptr roster_for_file_lca; + adaptor.get_ancestral_roster(conflict.nid, rid, roster_for_file_lca); + + // Now we should certainly have a roster, which has the node. + I(roster_for_file_lca); + I(roster_for_file_lca->has_node(conflict.nid)); + + file_id anc_id, left_id, right_id; + file_path anc_path, left_path, right_path; + roster_for_file_lca->get_file_details(conflict.nid, anc_id, anc_path); + left_roster.get_file_details(conflict.nid, left_id, left_path); + right_roster.get_file_details(conflict.nid, right_id, right_path); + + file_id merged_id; + + content_merger cm(lua, *roster_for_file_lca, + left_roster, right_roster, + adaptor); + + bool merged = false; + + switch (method) + { + case auto_merge: + merged = cm.try_auto_merge(anc_path, left_path, right_path, + right_path, anc_id, left_id, right_id, + merged_id); + break; + + case user_merge: + merged = cm.try_user_merge(anc_path, left_path, right_path, + right_path, anc_id, left_id, right_id, + merged_id); + + // If the user merge has failed, there's no point + // trying to continue -- we'll only frustrate users by + // encouraging them to continue working with their merge + // tool on a merge that is now destined to fail. + if (!merged) + return; + + break; + } + + if (merged) + { + L(FL("resolved content conflict %d / %d on file '%s'") + % cnt % total_conflicts % right_path); + file_t f = downcast_to_file_t(result.roster.get_node(conflict.nid)); + f->content = merged_id; + + it = result.file_content_conflicts.erase(it); + } + else + { + ++it; + } + } +} + +void resolve_merge_conflicts(lua_hooks & lua, roster_t const & left_roster, roster_t const & right_roster, ============================================================ --- merge.hh 8cd4bcbd4dfc7b481569c7725003e42bee951969 +++ merge.hh 837e0f0510935be055622c626378e289e6f5a59e @@ -12,20 +12,221 @@ // PURPOSE. #include "vocab.hh" +#include "rev_types.hh" class database; class lua_hooks; -class roster_t; +struct roster_merge_result; +struct options; +struct +content_merge_adaptor +{ + virtual void record_merge(file_id const & left_ident, + file_id const & right_ident, + file_id const & merged_ident, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data) = 0; + + // For use when one side of the merge is dropped + virtual void record_file(file_id const & parent_ident, + file_id const & merged_ident, + file_data const & parent_data, + file_data const & merged_data) = 0; + + virtual void get_ancestral_roster(node_id nid, + revision_id & rid, + boost::shared_ptr & anc) = 0; + + virtual void get_version(file_id const & ident, + file_data & dat) const = 0; + + virtual ~content_merge_adaptor() {} +}; + +struct +content_merge_database_adaptor + : public content_merge_adaptor +{ + database & db; + revision_id lca; + revision_id left_rid; + revision_id right_rid; + marking_map const & left_mm; + marking_map const & right_mm; + std::map > rosters; + content_merge_database_adaptor(database & db, + revision_id const & left, + revision_id const & right, + marking_map const & left_mm, + marking_map const & right_mm); + void record_merge(file_id const & left_ident, + file_id const & right_ident, + file_id const & merged_ident, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data); + + void record_file(file_id const & parent_ident, + file_id const & merged_ident, + file_data const & parent_data, + file_data const & merged_data); + + void cache_roster(revision_id const & rid, + boost::shared_ptr roster); + + void get_ancestral_roster(node_id nid, + revision_id & rid, + boost::shared_ptr & anc); + + void get_version(file_id const & ident, + file_data & dat) const; +}; + +struct +content_merge_workspace_adaptor + : public content_merge_adaptor +{ + std::map temporary_store; + database & db; + revision_id const lca; + boost::shared_ptr base; + marking_map const & left_mm; + marking_map const & right_mm; + std::map > rosters; + std::map content_paths; + content_merge_workspace_adaptor(database & db, + revision_id const & lca, + boost::shared_ptr base, + marking_map const & left_mm, + marking_map const & right_mm, + std::map const & paths) + : db(db), lca(lca), base(base), + left_mm(left_mm), right_mm(right_mm), content_paths(paths) + {} + + void cache_roster(revision_id const & rid, + boost::shared_ptr roster); + + void record_merge(file_id const & left_ident, + file_id const & right_ident, + file_id const & merged_ident, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data); + + void record_file(file_id const & parent_ident, + file_id const & merged_ident, + file_data const & parent_data, + file_data const & merged_data); + + void get_ancestral_roster(node_id nid, + revision_id & rid, + boost::shared_ptr & anc); + + void get_version(file_id const & ident, + file_data & dat) const; +}; + +struct +content_merge_checkout_adaptor + : public content_merge_adaptor +{ + database & db; + content_merge_checkout_adaptor(database & db) + : db(db) + {} + + void record_merge(file_id const & left_ident, + file_id const & right_ident, + file_id const & merged_ident, + file_data const & left_data, + file_data const & right_data, + file_data const & merged_data); + + void record_file(file_id const & parent_ident, + file_id const & merged_ident, + file_data const & parent_data, + file_data const & merged_data); + + void get_ancestral_roster(node_id nid, + revision_id & rid, + boost::shared_ptr & anc); + + void get_version(file_id const & ident, + file_data & dat) const; + +}; + + +struct content_merger +{ + lua_hooks & lua; + roster_t const & anc_ros; + roster_t const & left_ros; + roster_t const & right_ros; + + content_merge_adaptor & adaptor; + + content_merger(lua_hooks & lua, + roster_t const & anc_ros, + roster_t const & left_ros, + roster_t const & right_ros, + content_merge_adaptor & adaptor) + : lua(lua), + anc_ros(anc_ros), + left_ros(left_ros), + right_ros(right_ros), + adaptor(adaptor) + {} + + // Attempt merge3 on a file (line by line). Return true and valid data if + // it would succeed; false and invalid data otherwise. + bool attempt_auto_merge(file_path const & anc_path, // inputs + file_path const & left_path, + file_path const & right_path, + file_id const & ancestor_id, + file_id const & left_id, + file_id const & right_id, + file_data & left_data, // outputs + file_data & right_data, + file_data & merge_data); + + // Attempt merge3 on a file (line by line). If it succeeded, store results + // in database and return true and valid merged_id; return false + // otherwise. + bool try_auto_merge(file_path const & anc_path, + file_path const & left_path, + file_path const & right_path, + file_path const & merged_path, + file_id const & ancestor_id, + file_id const & left_id, + file_id const & right, + file_id & merged_id); + + bool try_user_merge(file_path const & anc_path, + file_path const & left_path, + file_path const & right_path, + file_path const & merged_path, + file_id const & ancestor_id, + file_id const & left_id, + file_id const & right, + file_id & merged_id); + + std::string get_file_encoding(file_path const & path, + roster_t const & ros); + + bool attribute_manual_merge(file_path const & path, + roster_t const & ros); +}; + + // Destructively alter a roster_merge_result to attempt to remove any // conflicts in it. Takes a content_merge_adaptor to pass on to the content // merger; used from both the merge-to-database code (below) and the // merge-to-workspace "update" code in commands.cc. -struct roster_merge_result; -struct content_merge_adaptor; -struct options; - void resolve_merge_conflicts(lua_hooks & lua, roster_t const & left_roster, ============================================================ --- roster_merge.hh 4fa9261ad66726a6d755d50ff54e6751c14a39dc +++ roster_merge.hh f530fb6fe1086d629f82d12b73d5019754615c48 @@ -15,7 +15,7 @@ #include "rev_types.hh" #include "database.hh" -#include "diff_patch.hh" +#include "merge.hh" #include "roster.hh" // needs full definition of roster_t available // interactions between conflict types: ============================================================ --- work.cc e2ed785cc9a7ec181639bf9c5ce989855997ab9b +++ work.cc 56a3a0f37a20d60df660b7d0cf00092ca3b6470b @@ -27,7 +27,7 @@ #include "simplestring_xform.hh" #include "revision.hh" #include "inodeprint.hh" -#include "diff_patch.hh" +#include "merge.hh" #include "charset.hh" #include "app_state.hh" #include "database.hh"