# # # add_dir "tests/resolve_content_conflict" # # add_file "tests/resolve_content_conflict/__driver__.lua" # content [96bf801b85023fa84035c19133bd3f6fc339a7e1] # # add_file "tests/resolve_content_conflict/conflicts-1" # content [d0d2e52fdf8329d3cf0ba5644454a931a30cacf2] # # add_file "tests/resolve_content_conflict/merge-1" # content [8ab8aad3487a1e247c5edcb01f5f50813f01fa07] # # add_file "tests/resolve_content_conflict/resolve-conflicts-1" # content [8651ee547db93b1ee41e0ee6309144d4e955b9cc] # # add_file "tests/resolve_content_conflict/update-1" # content [4ee9b94a8bc3f20abd7191ea65cbb7cf85ab46c6] # # patch "basic_io.cc" # from [5aacaa8dcf17e4113dbaf2e91b476a9b795ccbf5] # to [8537b617f85273a7c80736695a983d8a6104c205] # # patch "basic_io.hh" # from [6b126a5b42e3eba99b5aa01b73d3b2f7ffce5b1c] # to [212d1bb1bb8fed7cb5d123eb9b09f60e993d1018] # # patch "cmd_merging.cc" # from [c00d7dd2a1dd4dda7da812ae6216b6ee391238b9] # to [88fd8e589c2fbbf1d0eed179260bdafbdf12f77b] # # patch "diff_patch.cc" # from [211424b28fdc3a5dcdae5a781feea97e1e915326] # to [adf43e9fcc6b0a828e7deb3280ad76b5ae1489e4] # # patch "diff_patch.hh" # from [8386bfd9dc62a30e7173fca60d27adab6d02167b] # to [1fdf49ab99f9d3a6ca15c08f5c5e7828533e5fa2] # # patch "merge.cc" # from [9059000487b5a7e6e6390175c376b45ca4f251f0] # to [46e286382febebbed4f099acb5e83b17146cffb8] # # patch "merge.hh" # from [f3613e195ad378f7666d003b959b322abbce97d0] # to [8cd4bcbd4dfc7b481569c7725003e42bee951969] # # patch "monotone.texi" # from [6a8534e5e1e4045df89160f4d49b69a0fed06077] # to [2f6f14a68447e9b3418c328cd350db9c54c0d616] # # patch "options_list.hh" # from [cd0ff5a0d051b4644c02a5f81ed262a512c65360] # to [154daf8d9f4ab4673441dc8e30bf7531fb7fe3eb] # # patch "roster.cc" # from [9cebdc581039f2d117abc2c90c4230b2c9635166] # to [04ac3afba58ec4c48d56f080d46ed924e51df79c] # # patch "roster.hh" # from [0da49ea4d1bda34fe2875b0e45add9b6918f2df4] # to [c83fc2636e82ac77a1446c42df169b44bb47197f] # # patch "roster_merge.cc" # from [9bb39c2c87f92180b0a5db5697ff6ea15b82d25b] # to [2904442eb8835e76182b01daed84ac42f16bc258] # # patch "roster_merge.hh" # from [48d9aa743811732826e3f855a5759c1d61416984] # to [8f0603774befb2458b9e6a2660a90f31104a9a22] # # patch "testlib.lua" # from [0e8c0415f85924dfb385a6e6a0de8b0f787a611d] # to [785c976d13cafe3d472fa9d7121ddb7f75a4bae9] # ============================================================ --- tests/resolve_content_conflict/__driver__.lua 96bf801b85023fa84035c19133bd3f6fc339a7e1 +++ tests/resolve_content_conflict/__driver__.lua 96bf801b85023fa84035c19133bd3f6fc339a7e1 @@ -0,0 +1,42 @@ +-- Demonstrate content conflict resolutions +mtn_setup() + +addfile("foo", "foo") +addfile("bar", "bar\none\ntwo\nthree") +addfile("baz", "baz\naaa\nbbb\nccc") +commit("testbranch", "base") +base = base_revision() + +writefile("foo", "foo\nfirst\nrevision") +writefile("bar", "bar\nzero\none\ntwo\nthree") +writefile("baz", "baz\nAAA\nbbb\nccc") +commit("testbranch", "first") +first = base_revision() + +revert_to(base) + +writefile("foo", "foo\nsecond\nrevision") +writefile("bar", "bar\none\ntwo\nthree\nfour") +writefile("baz", "baz\naaa\nbbb\nCCC") +commit("testbranch", "second") +second = base_revision() + +check(mtn("automate", "show_conflicts", first, second), 0, true, nil) +canonicalize("stdout") +check(samefilestd("conflicts-1", "stdout")) + +writefile("foo", "foo\nmerged\nrevision") + +check(get("resolve-conflicts-1", "_MTN/conflicts")) +check(mtn("merge", "--resolve-conflicts-file=_MTN/conflicts"), 0, nil, true) +canonicalize("stderr") +check(samefilestd("merge-1", "stderr")) + +check(mtn("update"), 0, nil, true) +canonicalize("stderr") +check(samefilestd("update-1", "stderr")) + +check(readfile("foo") == "foo\nmerged\nrevision") +check(readfile("bar") == "bar\nzero\none\ntwo\nthree\nfour\n") +check(readfile("baz") == "baz\nAAA\nbbb\nCCC\n") +-- end of file ============================================================ --- tests/resolve_content_conflict/conflicts-1 d0d2e52fdf8329d3cf0ba5644454a931a30cacf2 +++ tests/resolve_content_conflict/conflicts-1 d0d2e52fdf8329d3cf0ba5644454a931a30cacf2 @@ -0,0 +1,32 @@ + left [c28c3c0f5a7f9cde848be91f79d86a6dd63e88ca] + right [9340b41fa26b20293292cb37449cc722b1b55a5a] +ancestor [4c059d3a8bbe8e624e46a50b298b4f301caf380d] + + conflict content + node_type "file" + ancestor_name "bar" + ancestor_file_id [fc8a40f0b775e86503c7522399f309f6ac298348] + left_name "bar" + left_file_id [bf227d19bccee7740bb58219910ff0930b6200c1] + right_name "bar" + right_file_id [5fd4e3cf64e24e969cfcd2380cf244aee9e52d5d] +resolved_internal + + conflict content + node_type "file" + ancestor_name "baz" + ancestor_file_id [ae708173915e11248629c18d16c96c3a34f87d16] + left_name "baz" + left_file_id [c438704db55b0d6f819e7e79c1622e5d757b926b] + right_name "baz" + right_file_id [f0ef173c31428d85dcca6399de12c6352f8b72b3] +resolved_internal + + conflict content + node_type "file" + ancestor_name "foo" +ancestor_file_id [0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33] + left_name "foo" + left_file_id [841e3d2ada1a56123f9efe9db0d0c045ff9e6d8f] + right_name "foo" + right_file_id [3506995775abf41d3cdeb1e0417bdd3bcf059395] ============================================================ --- tests/resolve_content_conflict/merge-1 8ab8aad3487a1e247c5edcb01f5f50813f01fa07 +++ tests/resolve_content_conflict/merge-1 8ab8aad3487a1e247c5edcb01f5f50813f01fa07 @@ -0,0 +1,10 @@ +mtn: 2 heads on branch 'testbranch' +mtn: merge 1 / 1: +mtn: calculating best pair of heads to merge next +mtn: [left] 9340b41fa26b20293292cb37449cc722b1b55a5a +mtn: [right] c28c3c0f5a7f9cde848be91f79d86a6dd63e88ca +mtn: merged bar, bar +mtn: merged baz, baz +mtn: replacing content of foo, foo with foo +mtn: [merged] c63027af0230a9bf587088ec08fd60318308730d +mtn: note: your workspaces have not been updated ============================================================ --- tests/resolve_content_conflict/resolve-conflicts-1 8651ee547db93b1ee41e0ee6309144d4e955b9cc +++ tests/resolve_content_conflict/resolve-conflicts-1 8651ee547db93b1ee41e0ee6309144d4e955b9cc @@ -0,0 +1,33 @@ + left [c28c3c0f5a7f9cde848be91f79d86a6dd63e88ca] + right [9340b41fa26b20293292cb37449cc722b1b55a5a] +ancestor [4c059d3a8bbe8e624e46a50b298b4f301caf380d] + + conflict content + node_type "file" + ancestor_name "bar" + ancestor_file_id [fc8a40f0b775e86503c7522399f309f6ac298348] + left_name "bar" + left_file_id [bf227d19bccee7740bb58219910ff0930b6200c1] + right_name "bar" + right_file_id [5fd4e3cf64e24e969cfcd2380cf244aee9e52d5d] +resolved_internal + + conflict content + node_type "file" + ancestor_name "baz" + ancestor_file_id [ae708173915e11248629c18d16c96c3a34f87d16] + left_name "baz" + left_file_id [c438704db55b0d6f819e7e79c1622e5d757b926b] + right_name "baz" + right_file_id [f0ef173c31428d85dcca6399de12c6352f8b72b3] +resolved_internal + + conflict content + node_type "file" + ancestor_name "foo" +ancestor_file_id [0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33] + left_name "foo" + left_file_id [841e3d2ada1a56123f9efe9db0d0c045ff9e6d8f] + right_name "foo" + right_file_id [3506995775abf41d3cdeb1e0417bdd3bcf059395] +resolved_user "foo" ============================================================ --- tests/resolve_content_conflict/update-1 4ee9b94a8bc3f20abd7191ea65cbb7cf85ab46c6 +++ tests/resolve_content_conflict/update-1 4ee9b94a8bc3f20abd7191ea65cbb7cf85ab46c6 @@ -0,0 +1,7 @@ +mtn: updating along branch 'testbranch' +mtn: selected update target c63027af0230a9bf587088ec08fd60318308730d +mtn: [left] 834637991b7d5e28d893a4e29285b20442d87225 +mtn: [right] c63027af0230a9bf587088ec08fd60318308730d +mtn: modifying bar +mtn: modifying baz +mtn: updated to base revision c63027af0230a9bf587088ec08fd60318308730d ============================================================ --- basic_io.cc 5aacaa8dcf17e4113dbaf2e91b476a9b795ccbf5 +++ basic_io.cc 8537b617f85273a7c80736695a983d8a6104c205 @@ -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 @@ -68,6 +69,14 @@ void basic_io::stanza::push_binary_pair( push_hex_pair(k, hexenc(encode_hexenc(v()))); } +void +basic_io::stanza::push_symbol(symbol const & k) +{ + entries.push_back(make_pair(k, "")); + if (k().size() > indent) + indent = k().size(); +} + void basic_io::stanza::push_hex_pair(symbol const & k, hexenc const & v) { entries.push_back(make_pair(k, "")); @@ -129,6 +138,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 6b126a5b42e3eba99b5aa01b73d3b2f7ffce5b1c +++ basic_io.hh 212d1bb1bb8fed7cb5d123eb9b09f60e993d1018 @@ -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 @@ -29,29 +30,21 @@ namespace basic_io namespace { - namespace syms + namespace syms { // 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"); } } - + typedef enum { TOK_SYMBOL, @@ -77,7 +70,7 @@ namespace basic_io inline void peek() { if (LIKELY(curr != in.end())) - // we do want to distinguish between EOF and '\xff', + // we do want to distinguish between EOF and '\xff', // so we translate '\xff' to 255u lookahead = widen(*curr); else @@ -162,7 +155,7 @@ namespace basic_io in.err("non-hex character in hex string"); advance(); } - + store(val); if (UNLIKELY(static_cast(in.lookahead) != ']')) @@ -230,13 +223,13 @@ namespace basic_io } advance(); } - + store(val); if (UNLIKELY(static_cast(in.lookahead) != '"')) in.err("string did not end with '\"'"); in.advance(); - + return basic_io::TOK_STRING; } else @@ -253,6 +246,7 @@ namespace basic_io stanza(); size_t indent; std::vector > entries; + void push_symbol(symbol const & k); void push_hex_pair(symbol const & k, hexenc const & v); void push_binary_pair(symbol const & k, id const & v); void push_binary_triple(symbol const & k, std::string const & n, @@ -264,10 +258,13 @@ 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); }; - // Note: printer uses a static buffer; thus only one buffer + // Note: printer uses a static buffer; thus only one buffer // may be referenced (globally). An invariant will be triggered // if more than one basic_io::printer is instantiated. struct ============================================================ --- cmd_merging.cc c00d7dd2a1dd4dda7da812ae6216b6ee391238b9 +++ cmd_merging.cc 88fd8e589c2fbbf1d0eed179260bdafbdf12f77b @@ -143,7 +143,8 @@ CMD(update, "update", "", CMD_REF(worksp "different revision, preserving uncommitted changes as it does so. " "If a revision is given, update the workspace to that revision. " "If not, update the workspace to the head of the branch."), - options::opts::branch | options::opts::revision) + options::opts::branch | options::opts::revision | + options::opts::resolve_conflicts_opts) { if (args.size() > 0) throw usage(execid); @@ -281,8 +282,9 @@ CMD(update, "update", "", CMD_REF(worksp content_merge_workspace_adaptor wca(db, old_rid, old_roster, left_markings, right_markings, paths); wca.cache_roster(working_rid, working_roster); + // FIXME_RESOLVE_CONFLICTS: add conflict resolutions here resolve_merge_conflicts(app.lua, *working_roster, chosen_roster, - result, wca); + result, wca, false); // Make sure it worked... I(result.is_clean()); @@ -297,7 +299,7 @@ CMD(update, "update", "", CMD_REF(worksp make_revision_for_workspace(chosen_rid, chosen_roster, merged_roster, remaining); - // small race condition here... + // small race condition here... FIXME: what is it? work.put_work_rev(remaining); work.update_any_attrs(db); work.maybe_update_inodeprints(db); @@ -355,7 +357,7 @@ merge_two(options & opts, lua_hooks & lu revision_id merged; transaction_guard guard(project.db); - interactive_merge_and_store(lua, project.db, left, right, merged); + interactive_merge_and_store(lua, project.db, opts, left, right, merged); project.put_standard_certs_from_options(opts, lua, keys, merged, branch, utf8(log.str())); @@ -375,7 +377,16 @@ find_heads_to_merge(database & db, set const heads) { - I(heads.size() > 2); + I(heads.size() >= 2); + + if (heads.size() == 2) + { + rid_set_iter i = heads.begin(); + revision_id left = *i++; + revision_id right = *i++; + return revpair(left, right); + }; + map heads_for_ancestor; set ancestors; @@ -417,7 +428,8 @@ CMD(merge, "merge", "", CMD_REF(tree), " CMD(merge, "merge", "", CMD_REF(tree), "", N_("Merges unmerged heads of a branch"), "", - options::opts::branch | options::opts::date | options::opts::author) + options::opts::branch | options::opts::date | options::opts::author | + options::opts::resolve_conflicts_opts) { database db(app); key_store keys(app); @@ -448,6 +460,12 @@ CMD(merge, "merge", "", CMD_REF(tree), " size_t pass = 1, todo = heads.size() - 1; + if (app.opts.resolve_conflicts_given || app.opts.resolve_conflicts_file_given) + { + // conflicts and resolutions only apply to first merge, so only do that one. + todo = 1; + } + // If there are more than two heads to be merged, on each iteration we // merge a pair whose least common ancestor is not an ancestor of any // other pair's least common ancestor. For example, if the history graph @@ -460,7 +478,7 @@ CMD(merge, "merge", "", CMD_REF(tree), " // A B // // A and B will be merged first, and then the result will be merged with C. - while (heads.size() > 2) + while (pass <= todo) { P(F("merge %d / %d:") % pass % todo); P(F("calculating best pair of heads to merge next")); @@ -476,19 +494,9 @@ CMD(merge, "merge", "", CMD_REF(tree), " pass++; } - // Last one. - I(pass == todo); - if (todo > 1) - P(F("merge %d / %d:") % pass % todo); + if (heads.size() > 1) + P(F("note: branch '%s' still has %s heads; run merge again") % app.opts.branchname % heads.size()); - rid_set_iter i = heads.begin(); - revision_id left = *i++; - revision_id right = *i++; - I(i == heads.end()); - - merge_two(app.opts, app.lua, project, keys, - left, right, app.opts.branchname, string("merge"), - std::cout, false); P(F("note: your workspaces have not been updated")); } @@ -496,7 +504,8 @@ CMD(propagate, "propagate", "", CMD_REF( N_("SOURCE-BRANCH DEST-BRANCH"), N_("Merges from one branch to another asymmetrically"), "", - options::opts::date | options::opts::author | options::opts::message | options::opts::msgfile) + options::opts::date | options::opts::author | options::opts::message | options::opts::msgfile | + options::opts::resolve_conflicts_opts) { if (args.size() != 2) throw usage(execid); @@ -535,7 +544,8 @@ CMD(merge_into_dir, "merge_into_dir", "" N_("SOURCE-BRANCH DEST-BRANCH DIR"), N_("Merges one branch into a subdirectory in another branch"), "", - options::opts::date | options::opts::author | options::opts::message | options::opts::msgfile) + options::opts::date | options::opts::author | options::opts::message | options::opts::msgfile | + options::opts::resolve_conflicts_opts) { database db(app); key_store keys(app); @@ -601,8 +611,8 @@ CMD(merge_into_dir, "merge_into_dir", "" db.get_roster(left_rid, left_roster, left_marking_map); db.get_roster(right_rid, right_roster, right_marking_map); db.get_uncommon_ancestors(left_rid, right_rid, - left_uncommon_ancestors, - right_uncommon_ancestors); + left_uncommon_ancestors, + right_uncommon_ancestors); if (!idx(args,2)().empty()) { @@ -637,8 +647,12 @@ CMD(merge_into_dir, "merge_into_dir", "" content_merge_database_adaptor dba(db, left_rid, right_rid, left_marking_map, right_marking_map); + bool resolutions_given; + + parse_resolve_conflicts_opts (app.opts, left_roster, right_roster, result, resolutions_given); + resolve_merge_conflicts(app.lua, left_roster, right_roster, - result, dba); + result, dba, false); { dir_t moved_root = left_roster.root(); @@ -681,7 +695,8 @@ CMD(merge_into_workspace, "merge_into_wo "pending changes in the current workspace. Both OTHER-REVISION and " "the workspace's base revision will be recorded as parents on commit. " "The workspace's selected branch is not changed."), - options::opts::none) + options::opts::none | + options::opts::resolve_conflicts_opts) { revision_id left_id, right_id; cached_roster left, right; @@ -752,7 +767,8 @@ CMD(merge_into_workspace, "merge_into_wo content_merge_workspace_adaptor wca(db, lca_id, lca.first, *left.second, *right.second, paths); wca.cache_roster(working_rid, working_roster); - resolve_merge_conflicts(app.lua, *left.first, *right.first, merge_result, wca); + // FIXME_RESOLVE_CONFLICTS: add conflict resolutions here + resolve_merge_conflicts(app.lua, *left.first, *right.first, merge_result, wca, false); // Make sure it worked... I(merge_result.is_clean()); @@ -789,7 +805,8 @@ CMD(explicit_merge, "explicit_merge", "" N_("Merges two explicitly given revisions"), N_("The results of the merge are placed on the branch specified by " "DEST-BRANCH."), - options::opts::date | options::opts::author) + options::opts::date | options::opts::author | + options::opts::resolve_conflicts_opts) { database db(app); key_store keys(app); @@ -832,7 +849,12 @@ static void } static void -show_conflicts_core (database & db, revision_id const & l_id, revision_id const & r_id, bool const basic_io, std::ostream & output) +show_conflicts_core (database & db, + lua_hooks & lua, + revision_id const & l_id, + revision_id const & r_id, + bool const basic_io, + std::ostream & output) { N(!is_ancestor(db, l_id, r_id), F("%s is an ancestor of %s; no merge is needed.") @@ -907,7 +929,7 @@ show_conflicts_core (database & db, revi result.report_duplicate_name_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); result.report_attribute_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); - result.report_file_content_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); + result.report_file_content_conflicts(lua, *l_roster, *r_roster, adaptor, basic_io, output); } } @@ -926,7 +948,7 @@ CMD(show_conflicts, "show_conflicts", "" complete(app.opts, app.lua, project, idx(args,0)(), l_id); complete(app.opts, app.lua, project, idx(args,1)(), r_id); - show_conflicts_core(db, l_id, r_id, false, std::cout); + show_conflicts_core(db, app.lua, l_id, r_id, false, std::cout); } // Name: show_conflicts @@ -968,19 +990,9 @@ CMD_AUTOMATE(show_conflicts, N_("[LEFT_R N(heads.size() >= 2, F("branch '%s' has %d heads; must be at least 2 for show_conflicts") % app.opts.branchname % heads.size()); - if (heads.size() == 2) - { - set::const_iterator i = heads.begin(); - l_id = *i; - ++i; - r_id = *i; - } - else - { - revpair p = find_heads_to_merge (db, heads); - l_id = p.first; - r_id = p.second; - } + revpair p = find_heads_to_merge (db, heads); + l_id = p.first; + r_id = p.second; } else if (args.size() == 2) { @@ -991,7 +1003,7 @@ CMD_AUTOMATE(show_conflicts, N_("[LEFT_R else N(false, F("wrong argument count")); - show_conflicts_core(db, l_id, r_id, true, output); + show_conflicts_core(db, app.lua, l_id, r_id, true, output); } CMD(pluck, "pluck", "", CMD_REF(workspace), N_("[-r FROM] -r TO [PATH...]"), @@ -1005,7 +1017,8 @@ CMD(pluck, "pluck", "", CMD_REF(workspac "compared to its parent.\n" "If two revisions are given, applies the changes made to get from the " "first revision to the second."), - options::opts::revision | options::opts::depth | options::opts::exclude) + options::opts::revision | options::opts::depth | options::opts::exclude | + options::opts::resolve_conflicts_opts) { database db(app); workspace work(app); @@ -1135,8 +1148,9 @@ CMD(pluck, "pluck", "", CMD_REF(workspac // to_roster is not fetched from the db which does not have temporary nids wca.cache_roster(to_rid, to_roster); + // FIXME_RESOLVE_CONFLICTS: add conflict resolutions here resolve_merge_conflicts(app.lua, *working_roster, *to_roster, - result, wca); + result, wca, false); I(result.is_clean()); // temporary node ids may appear ============================================================ --- diff_patch.cc 211424b28fdc3a5dcdae5a781feea97e1e915326 +++ diff_patch.cc adf43e9fcc6b0a828e7deb3280ad76b5ae1489e4 @@ -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 @@ -767,6 +768,61 @@ bool } 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); + 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, @@ -795,50 +851,19 @@ content_merger::try_auto_merge(file_path return true; } - file_data left_data, right_data, ancestor_data; - data left_unpacked, ancestor_unpacked, right_unpacked, merged_unpacked; + file_data left_data, right_data, merge_data; - 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(); - - if (!attribute_manual_merge(left_path, left_ros) && - !attribute_manual_merge(right_path, right_ros)) + if (attempt_auto_merge(anc_path, left_path, right_path, + ancestor_id, left_id, right_id, + left_data, right_data, merge_data)) { - // 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 - string left_encoding, anc_encoding, right_encoding; - left_encoding = this->get_file_encoding(left_path, left_ros); - anc_encoding = this->get_file_encoding(anc_path, anc_ros); - right_encoding = this->get_file_encoding(right_path, right_ros); + L(FL("internal 3-way merged ok")); + calculate_ident(merge_data, merged_id); - 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); + adaptor.record_merge(left_id, right_id, merged_id, + left_data, right_data, merge_data); - if (merge3(ancestor_lines, left_lines, right_lines, merged_lines)) - { - file_id tmp_id; - file_data merge_data; - string tmp; - - L(FL("internal 3-way merged ok")); - join_lines(merged_lines, tmp); - merge_data = file_data(tmp); - calculate_ident(merge_data, merged_id); - - adaptor.record_merge(left_id, right_id, merged_id, - left_data, right_data, merge_data); - - return true; - } + return true; } return false; ============================================================ --- diff_patch.hh 8386bfd9dc62a30e7173fca60d27adab6d02167b +++ diff_patch.hh 1fdf49ab99f9d3a6ca15c08f5c5e7828533e5fa2 @@ -1,6 +1,7 @@ #ifndef __DIFF_PATCH_HH__ #define __DIFF_PATCH_HH__ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2002 Graydon Hoare // // This program is made available under the GNU GPL version 2.0 or @@ -179,7 +180,21 @@ struct content_merger adaptor(adaptor) {} - // merge3 on a file (line by line) + // 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, ============================================================ --- merge.cc 9059000487b5a7e6e6390175c376b45ca4f251f0 +++ merge.cc 46e286382febebbed4f099acb5e83b17146cffb8 @@ -16,6 +16,7 @@ #include "diff_patch.hh" #include "merge.hh" +#include "options.hh" #include "revision.hh" #include "roster_merge.hh" #include "safe_map.hh" @@ -34,17 +35,6 @@ namespace enum merge_method { auto_merge, user_merge }; void - get_file_details(roster_t const & ros, node_id nid, - file_id & fid, - file_path & pth) - { - I(ros.has_node(nid)); - file_t f = downcast_to_file_t(ros.get_node(nid)); - fid = f->content; - ros.get_name(nid, pth); - } - - 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, @@ -71,9 +61,9 @@ namespace file_id anc_id, left_id, right_id; file_path anc_path, left_path, right_path; - get_file_details(*roster_for_file_lca, conflict.nid, anc_id, anc_path); - get_file_details(left_roster, conflict.nid, left_id, left_path); - get_file_details(right_roster, conflict.nid, right_id, 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; @@ -130,17 +120,32 @@ resolve_merge_conflicts(lua_hooks & lua, roster_t const & left_roster, roster_t const & right_roster, roster_merge_result & result, - content_merge_adaptor & adaptor) + content_merge_adaptor & adaptor, + bool resolutions_given) { - // FIXME_ROSTERS: we only have code (below) to invoke the - // line-merger on content conflicts. Other classes of conflict will - // cause an invariant to trip below. Probably just a bunch of lua - // hooks for remaining conflict types will be ok. - if (!result.is_clean()) - result.log_conflicts(); + { + result.log_conflicts(); + if (resolutions_given) + { + // If there are any conflicts for which we don't currently support + // resolutions, give a nice error message. + char const * const msg = "conflict resolution for %s not yet supported"; + N(!result.missing_root_dir, F(msg) % "missing_root_dir"); + N(result.invalid_name_conflicts.size() == 0, F(msg) % "invalid_name_conflicts"); + N(result.directory_loop_conflicts.size() == 0, F(msg) % "directory_loop_conflicts"); + N(result.orphaned_node_conflicts.size() == 0, F(msg) % "orphaned_node_conflicts"); + N(result.multiple_name_conflicts.size() == 0, F(msg) % "multiple_name_conflicts"); + N(result.attribute_conflicts.size() == 0, F(msg) % "attribute_conflicts"); + + // resolve the ones we can. + result.resolve_duplicate_name_conflicts(lua, left_roster, right_roster, adaptor); + result.resolve_file_content_conflicts(lua, left_roster, right_roster, adaptor); + } + } + if (result.has_non_content_conflicts()) { result.report_missing_root_conflicts(left_roster, right_roster, adaptor, false, std::cout); @@ -152,7 +157,7 @@ resolve_merge_conflicts(lua_hooks & lua, result.report_duplicate_name_conflicts(left_roster, right_roster, adaptor, false, std::cout); result.report_attribute_conflicts(left_roster, right_roster, adaptor, false, std::cout); - result.report_file_content_conflicts(left_roster, right_roster, adaptor, false, std::cout); + result.report_file_content_conflicts(lua, left_roster, right_roster, adaptor, false, std::cout); } else if (result.has_content_conflicts()) { @@ -170,7 +175,7 @@ resolve_merge_conflicts(lua_hooks & lua, P(FP("%d content conflict requires user intervention", "%d content conflicts require user intervention", remaining) % remaining); - result.report_file_content_conflicts(left_roster, right_roster, adaptor, false, std::cout); + result.report_file_content_conflicts(lua, left_roster, right_roster, adaptor, false, std::cout); try_to_merge_files(lua, left_roster, right_roster, result, adaptor, user_merge); @@ -182,7 +187,9 @@ void } void -interactive_merge_and_store(lua_hooks & lua, database & db, +interactive_merge_and_store(lua_hooks & lua, + database & db, + options const & opts, revision_id const & left_rid, revision_id const & right_rid, revision_id & merged_rid) @@ -202,11 +209,14 @@ interactive_merge_and_store(lua_hooks & right_roster, right_marking_map, right_uncommon_ancestors, result); + bool resolutions_given; content_merge_database_adaptor dba(db, left_rid, right_rid, left_marking_map, right_marking_map); - resolve_merge_conflicts(lua, left_roster, right_roster, - result, dba); + parse_resolve_conflicts_opts (opts, left_roster, right_roster, result, resolutions_given); + + resolve_merge_conflicts(lua, left_roster, right_roster, result, dba, resolutions_given); + // write new files into the db store_roster_merge_result(db, left_roster, right_roster, result, ============================================================ --- merge.hh f3613e195ad378f7666d003b959b322abbce97d0 +++ merge.hh 8cd4bcbd4dfc7b481569c7725003e42bee951969 @@ -1,6 +1,7 @@ #ifndef __MERGE_HH__ #define __MERGE_HH__ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2005 Nathaniel Smith // // This program is made available under the GNU GPL version 2.0 or @@ -23,13 +24,15 @@ struct content_merge_adaptor; struct roster_merge_result; struct content_merge_adaptor; +struct options; void resolve_merge_conflicts(lua_hooks & lua, roster_t const & left_roster, roster_t const & right_roster, roster_merge_result & result, - content_merge_adaptor & adaptor); + content_merge_adaptor & adaptor, + bool const resolutions_given); // traditional resolve-all-conflicts-as-you-go style merging with 3-way merge // for file texts @@ -43,7 +46,9 @@ void // around the revision and its files not being in the db, and the resulting // revision and its merged files not being written back to the db void -interactive_merge_and_store(lua_hooks & lua, database & db, +interactive_merge_and_store(lua_hooks & lua, + database & db, + options const & opts, revision_id const & left, revision_id const & right, revision_id & merged); ============================================================ --- monotone.texi 6a8534e5e1e4045df89160f4d49b69a0fed06077 +++ monotone.texi 2f6f14a68447e9b3418c328cd350db9c54c0d616 @@ -3019,6 +3019,33 @@ @section Merge Conflicts Unfortunately, these commands can't yet list conflicts between a database revision and the current workspace. +All merging commands accept options that specify conflict resolutions: address@hidden @command address@hidden address@hidden resolution} address@hidden address@hidden address@hidden table + +For @command{--resolve-conflicts-file}, the file must contain the output +of @command{automate show_conflicts}, with conflict resolutions +appended to each stanza. @file{_MTN/conflicts} is a good place to put +the file. + +For @command{--resolve-conflicts}, a single conflict resolution is given +in the string; it has the same format as the conflict resolutions in +the file, and must be applicable to all conflicts in the merge. This +is most usefull when there is a single conflict. + +The @command{merge} command normally will perform as many merges as +necessary to merge all current heads of a branch. However, when address@hidden or @command{--resolve-conflicts-file} is given, +the conflicts and their resolutions apply only to the first merge, so +the subsequent merges are not done; the @command{merge} command must +be repeated, possibly with new conflicts and resolutions, to merge the +remaining heads. + +The possible conflict resolutions are discussed with each conflict in +the following sections. + @subsection Conflict Types Monotone versions both files and directories explicitly and it tracks @@ -8606,9 +8633,14 @@ @section Automation workspace is present, the branch may be given by the @var{--branch} option. address@hidden Added in: address@hidden Changes: -7.1 address@hidden address@hidden +FIXME_RESOLVE_CONFLICTS: -- added default resolution for file content conflicts address@hidden +7.1 -- initial address@hidden itemize @item Purpose: @@ -8732,14 +8764,15 @@ @section Automation File content changed (this may be resolvable by the internal line merger), file also renamed: @verbatim - conflict content - node_type "file" - ancestor_name "bar" -ancestor_file_id [f0ef49fe92167fe2a086588019ffcff7ea561786] - left_name "bar" - left_file_id [08cd878106a93ce2ef036a32499c1432adb3ee0d] - right_name "bar" - right_file_id [0cf419dd93d38b2daaaf1f5e0f3ec647745b9690] + conflict content + node_type "file" + ancestor_name "bar" + ancestor_file_id [f0ef49fe92167fe2a086588019ffcff7ea561786] + left_name "bar" + left_file_id [08cd878106a93ce2ef036a32499c1432adb3ee0d] + right_name "bar" + right_file_id [0cf419dd93d38b2daaaf1f5e0f3ec647745b9690] +resolved_internal conflict content node_type "file" @@ -8750,6 +8783,10 @@ @section Automation right_name "baz" right_file_id [b966b2d35b99e456cb0c55e4573ef0b1b155b4a9] @end verbatim address@hidden is a conflict resolution; see @ref{Merge +Conflicts}. If the file contents in the two revs can be successfully +merged by the internal line merger, @code{resolved_internal} is +output. File added and/or renamed: @verbatim ============================================================ --- options_list.hh cd0ff5a0d051b4644c02a5f81ed262a512c65360 +++ options_list.hh 154daf8d9f4ab4673441dc8e30bf7531fb7fe3eb @@ -1,3 +1,4 @@ +// Copyright 2008 Stephen Leake // Copyright 2006 Timothy Brownawell // This is made available under the GNU GPL v2 or later. @@ -635,6 +636,30 @@ OPTION(automate_inventory_opts, no_corre } #endif +OPTSET(resolve_conflicts_opts) +OPTVAR(resolve_conflicts_opts, utf8, resolve_conflicts_file, ) +OPTVAR(resolve_conflicts_opts, std::string, resolve_conflicts, ) + +OPTION(resolve_conflicts_opts, resolve_conflicts_file, true, "resolve-conflicts-file", + gettext_noop("use file to resolve conflicts")) +#ifdef option_bodies +{ + N(!resolve_conflicts_given, + F("only one of --resolve-conflicts or --resolve-conflicts-file may be given")); + resolve_conflicts_file = utf8(arg); +} +#endif + +OPTION(resolve_conflicts_opts, resolve_conflicts, true, "resolve-conflicts", + gettext_noop("use argument to resolve conflicts")) +#ifdef option_bodies +{ + N(!resolve_conflicts_file_given, + F("only one of --resolve-conflicts or --resolve-conflicts-file may be given")); + resolve_conflicts = arg; +} +#endif + // Local Variables: // mode: C++ // fill-column: 76 ============================================================ --- roster.cc 9cebdc581039f2d117abc2c90c4230b2c9635166 +++ roster.cc 04ac3afba58ec4c48d56f080d46ed924e51df79c @@ -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 @@ -46,6 +47,19 @@ using boost::lexical_cast; /////////////////////////////////////////////////////////////////// +namespace +{ + namespace syms + { + symbol const birth("birth"); + symbol const dormant_attr("dormant_attr"); + symbol const ident("ident"); + + symbol const path_mark("path_mark"); + symbol const attr_mark("attr_mark"); + } +} + template <> void dump(node_id const & val, string & out) { @@ -517,9 +531,8 @@ shallow_equal(node_t a, node_t b, } -// FIXME_ROSTERS: why does this do two loops? why does it pass 'true' to -// shallow_equal? -// -- njs +// FIXME_ROSTERS: why does this do two loops? why does it pass 'true' for +// shallow_compare_dir_children to shallow_equal? -- njs bool roster_t::operator==(roster_t const & other) const { @@ -847,6 +860,7 @@ roster_t::create_file_node(file_id const create_file_node(content, nid); return nid; } + void roster_t::create_file_node(file_id const & content, node_id nid) { @@ -1858,7 +1872,6 @@ namespace { left_cs.apply_to(from_left_er); right_cs.apply_to(from_right_er); - set new_ids; unify_rosters(new_roster, from_left_er.new_nodes, from_right_r, from_right_er.new_nodes, nis); @@ -2430,6 +2443,17 @@ void } void +roster_t::get_file_details(node_id nid, + file_id & fid, + file_path & pth) const +{ + I(has_node(nid)); + file_t f = downcast_to_file_t(get_node(nid)); + fid = f->content; + get_name(nid, pth); +} + +void roster_t::extract_path_set(set & paths) const { paths.clear(); @@ -2473,11 +2497,11 @@ 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()); 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) { @@ -2493,7 +2517,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()); } } @@ -2505,13 +2529,13 @@ 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::path_mark)) { pa.sym(); pa.hex(rev); @@ -2523,7 +2547,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(); @@ -2573,7 +2597,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 @@ -2596,7 +2620,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()); } } @@ -2662,7 +2686,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)))); @@ -2671,7 +2695,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))); } @@ -2704,7 +2728,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; ============================================================ --- roster.hh 0da49ea4d1bda34fe2875b0e45add9b6918f2df4 +++ roster.hh c83fc2636e82ac77a1446c42df169b44bb47197f @@ -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 @@ -202,6 +203,10 @@ public: attr_key const & key, attr_value & val) const; + void get_file_details(node_id nid, + file_id & fid, + file_path & pth) const; + void extract_path_set(std::set & paths) const; node_map const & all_nodes() const ============================================================ --- roster_merge.cc 9bb39c2c87f92180b0a5db5697ff6ea15b82d25b +++ roster_merge.cc 2904442eb8835e76182b01daed84ac42f16bc258 @@ -11,14 +11,15 @@ #include "base.hh" #include -#include - #include "basic_io.hh" -#include "vocab.hh" +#include "file_io.hh" +#include "lua_hooks.hh" +#include "options.hh" +#include "parallel_iter.hh" #include "roster_merge.hh" -#include "parallel_iter.hh" #include "safe_map.hh" #include "transforms.hh" +#include "vocab.hh" using boost::shared_ptr; @@ -28,6 +29,24 @@ using std::string; using std::set; using std::string; +static char * const +image(resolve_conflicts::resolution_t resolution) +{ + switch (resolution) + { + case resolve_conflicts::none: + return "none"; + case resolve_conflicts::content_user: + return "content_user"; + case resolve_conflicts::content_internal: + return "content_internal"; + case resolve_conflicts::rename: + return "rename"; + default: + I(false); + } +} + template <> void dump(invalid_name_conflict const & conflict, string & out) { @@ -77,7 +96,19 @@ dump(duplicate_name_conflict const & con oss << "duplicate_name_conflict between left node: " << conflict.left_nid << " " << "and right node: " << conflict.right_nid << " " << "parent: " << conflict.parent_name.first << " " - << "basename: " << conflict.parent_name.second << "\n"; + << "basename: " << conflict.parent_name.second; + + if (conflict.left_resolution.first != resolve_conflicts::none) + { + oss << " left_resolution: " << image(conflict.left_resolution.first); + oss << " left_name: " << conflict.left_resolution.second; + } + if (conflict.right_resolution.first != resolve_conflicts::none) + { + oss << " right_resolution: " << image(conflict.right_resolution.first); + oss << " right_name: " << conflict.right_resolution.second; + } + oss << "\n"; out = oss.str(); } @@ -96,9 +127,14 @@ dump(file_content_conflict const & confl dump(file_content_conflict const & conflict, string & out) { ostringstream oss; - oss << "file_content_conflict on node: " << conflict.nid << " " - << "left: " << conflict.left << " " - << "right: " << conflict.right << "\n"; + oss << "file_content_conflict on node: " << conflict.nid; + + if (conflict.resolution.first != resolve_conflicts::none) + { + oss << " resolution: " << image(conflict.resolution.first); + oss << " name: " << conflict.resolution.second; + } + oss << "\n"; out = oss.str(); } @@ -178,10 +214,7 @@ namespace else I(false); } -} -namespace -{ namespace syms { symbol const ancestor_file_id("ancestor_file_id"); @@ -203,6 +236,10 @@ namespace symbol const node_type("node_type"); symbol const orphaned_directory("orphaned_directory"); symbol const orphaned_file("orphaned_file"); + symbol const resolved_internal("resolved_internal"); + symbol const resolved_rename_left("resolved_rename_left"); + symbol const resolved_rename_right("resolved_rename_right"); + symbol const resolved_user("resolved_user"); symbol const right_attr_state("right_attr_state"); symbol const right_attr_value("right_attr_value"); symbol const right_file_id("right_file_id"); @@ -398,7 +435,6 @@ put_attr_conflict (basic_io::stanza & st db_adaptor.db.get_file_content (db_adaptor.lca, conflict.nid, ancestor_fid); st.push_str_pair(syms::ancestor_name, ancestor_name.as_external()); st.push_binary_pair(syms::ancestor_file_id, ancestor_fid.inner()); - // FIXME: don't have this. st.push_str_pair(syms::ancestor_attr_value, ???); file_id left_fid; db_adaptor.db.get_file_content (db_adaptor.left_rid, conflict.nid, left_fid); st.push_file_pair(syms::left_name, left_name); @@ -415,7 +451,6 @@ put_attr_conflict (basic_io::stanza & st st.push_str_pair(syms::node_type, "directory"); st.push_str_pair(syms::attr_name, conflict.key()); st.push_str_pair(syms::ancestor_name, ancestor_name.as_external()); - // FIXME: don't have this. st.push_str_pair(syms::ancestor_attr_value, ???); st.push_file_pair(syms::left_name, left_name); put_attr_state_left (st, conflict); st.push_file_pair(syms::right_name, right_name); @@ -425,6 +460,8 @@ put_content_conflict (basic_io::stanza & static void put_content_conflict (basic_io::stanza & st, + roster_t const & left_roster, + roster_t const & right_roster, content_merge_adaptor & adaptor, file_content_conflict const & conflict) { @@ -437,18 +474,13 @@ put_content_conflict (basic_io::stanza & revision_id ancestor_rid; db_adaptor.get_ancestral_roster (conflict.nid, ancestor_rid, ancestor_roster); - boost::shared_ptr left_roster(db_adaptor.rosters[db_adaptor.left_rid]); - I(0 != left_roster); - boost::shared_ptr right_roster(db_adaptor.rosters[db_adaptor.right_rid]); - I(0 != right_roster); - file_path ancestor_name; file_path left_name; file_path right_name; ancestor_roster->get_name (conflict.nid, ancestor_name); - left_roster->get_name (conflict.nid, left_name); - right_roster->get_name (conflict.nid, right_name); + left_roster.get_name (conflict.nid, left_name); + right_roster.get_name (conflict.nid, right_name); if (file_type == get_type (*ancestor_roster, conflict.nid)) { @@ -457,14 +489,30 @@ put_content_conflict (basic_io::stanza & db_adaptor.db.get_file_content (db_adaptor.lca, conflict.nid, ancestor_fid); st.push_str_pair(syms::ancestor_name, ancestor_name.as_external()); st.push_binary_pair(syms::ancestor_file_id, ancestor_fid.inner()); + st.push_file_pair(syms::left_name, left_name); file_id left_fid; db_adaptor.db.get_file_content (db_adaptor.left_rid, conflict.nid, left_fid); - st.push_file_pair(syms::left_name, left_name); st.push_binary_pair(syms::left_file_id, left_fid.inner()); + st.push_file_pair(syms::right_name, right_name); file_id right_fid; db_adaptor.db.get_file_content (db_adaptor.right_rid, conflict.nid, right_fid); - st.push_file_pair(syms::right_name, right_name); st.push_binary_pair(syms::right_file_id, right_fid.inner()); + switch (conflict.resolution.first) + { + case resolve_conflicts::none: + break; + + case resolve_conflicts::content_internal: + st.push_symbol(syms::resolved_internal); + break; + + case resolve_conflicts::content_user: + st.push_file_pair(syms::resolved_user, conflict.resolution.second); + break; + + default: + I(false); + } } else { @@ -472,6 +520,16 @@ put_content_conflict (basic_io::stanza & st.push_str_pair(syms::ancestor_name, ancestor_name.as_external()); st.push_file_pair(syms::left_name, left_name); st.push_file_pair(syms::right_name, right_name); + + switch (conflict.resolution.first) + { + case resolve_conflicts::none: + break; + + default: + // not implemented yet + I(false); + } } } @@ -1274,28 +1332,63 @@ roster_merge_result::report_attribute_co } } +namespace +{ + bool + auto_merge_succeeds(lua_hooks & lua, + file_content_conflict conflict, + content_merge_adaptor & adaptor, + roster_t const & left_roster, + roster_t const & right_roster) + { + revision_id ancestor_rid; + shared_ptr ancestor_roster; + adaptor.get_ancestral_roster(conflict.nid, ancestor_rid, ancestor_roster); + + I(ancestor_roster); + I(ancestor_roster->has_node(conflict.nid)); // this fails if there is no least common ancestor + + file_id anc_id, left_id, right_id; + file_path anc_path, left_path, right_path; + ancestor_roster->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); + + content_merger cm(lua, *ancestor_roster, left_roster, right_roster, adaptor); + + file_data left_data, right_data, merge_data; + + return cm.attempt_auto_merge(anc_path, left_path, right_path, + anc_id, left_id, right_id, + left_data, right_data, merge_data); + } +} + void -roster_merge_result::report_file_content_conflicts(roster_t const & left_roster, +roster_merge_result::report_file_content_conflicts(lua_hooks & lua, + roster_t const & left_roster, roster_t const & right_roster, content_merge_adaptor & adaptor, bool basic_io, - std::ostream & output) const + std::ostream & output) { MM(left_roster); MM(right_roster); for (size_t i = 0; i < file_content_conflicts.size(); ++i) { - file_content_conflict const & conflict = file_content_conflicts[i]; + file_content_conflict & conflict = file_content_conflicts[i]; MM(conflict); - if (basic_io) { basic_io::stanza st; + if (auto_merge_succeeds(lua, conflict, adaptor, left_roster, right_roster)) + conflict.resolution = make_pair(resolve_conflicts::content_internal, file_path()); + st.push_str_pair(syms::conflict, syms::content); - put_content_conflict (st, adaptor, conflict); + put_content_conflict (st, left_roster, right_roster, adaptor, conflict); put_stanza (st, output); } else @@ -1338,7 +1431,467 @@ roster_merge_result::report_file_content } } +// Resolving non-content conflicts + +namespace resolve_conflicts +{ + bool + do_auto_merge(lua_hooks & lua, + file_content_conflict const & conflict, + content_merge_adaptor & adaptor, + roster_t const & left_roster, + roster_t const & right_roster, + roster_t const & result_roster, + file_id & merged_id) + { + revision_id ancestor_rid; + shared_ptr ancestor_roster; + adaptor.get_ancestral_roster(conflict.nid, ancestor_rid, ancestor_roster); + + I(ancestor_roster); + I(ancestor_roster->has_node(conflict.nid)); // this fails if there is no least common ancestor + + file_id anc_id, left_id, right_id; + file_path anc_path, left_path, right_path, merged_path; + ancestor_roster->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); + result_roster.get_file_details(conflict.nid, merged_id, merged_path); + + content_merger cm(lua, *ancestor_roster, left_roster, right_roster, adaptor); + + return cm.try_auto_merge(anc_path, left_path, right_path, merged_path, + anc_id, left_id, right_id, merged_id); + } +} + +static void +parse_duplicate_name_conflicts(basic_io::parser & pars, + std::vector & conflicts, + roster_t const & left_roster, + roster_t const & right_roster) +{ + for (std::vector::iterator i = conflicts.begin(); + i != conflicts.end(); + ++i) + { + duplicate_name_conflict & conflict = *i; + + pars.esym(syms::duplicate_name); + + node_id left_nid, right_nid; + string left_name, right_name; + + pars.esym(syms::left_type); pars.str(); + pars.esym (syms::left_name); pars.str(left_name); + pars.esym(syms::left_file_id); pars.hex(); + + pars.esym(syms::right_type); pars.str(); + pars.esym (syms::right_name); pars.str(right_name); + pars.esym(syms::right_file_id); pars.hex(); + + left_nid = left_roster.get_node (file_path_internal (left_name))->self; + right_nid = right_roster.get_node (file_path_internal (right_name))->self; + + // Note that we cannot confirm the file ids. + N(left_nid == conflict.left_nid && right_nid == conflict.right_nid, + F("conflicts file does not match current conflicts: (duplicate_name, left %s, right %s") + % left_name % right_name); + + // check for a resolution + while ((!pars.symp (syms::conflict)) && pars.tok.in.lookahead != EOF) + { + if (pars.symp (syms::resolved_rename_left)) + { + conflict.left_resolution.first = resolve_conflicts::rename; + pars.sym(); + conflict.left_resolution.second = file_path_internal (pars.token); + pars.str(); + } + else if (pars.symp (syms::resolved_rename_right)) + { + conflict.right_resolution.first = resolve_conflicts::rename; + pars.sym(); + conflict.right_resolution.second = file_path_internal (pars.token); + pars.str(); + } + else + N(false, F("%s is not a supported conflict resolution for %s") % pars.token % "duplicate_name"); + } + + if (pars.tok.in.lookahead != EOF) + pars.esym (syms::conflict); + else + { + std::vector::iterator tmp = i; + N(++tmp == conflicts.end(), F("conflicts file does not match current conflicts")); + } + } +} // parse_duplicate_name_conflicts + +static void +parse_file_content_conflicts(basic_io::parser & pars, + std::vector & conflicts, + roster_t const & left_roster, + roster_t const & right_roster) +{ + for (std::vector::iterator i = conflicts.begin(); + i != conflicts.end(); + ++i) + { + string tmp; + node_id left_nid, right_nid; + string left_name, right_name, result_name; + + file_content_conflict & conflict = *i; + + pars.esym(syms::content); + + pars.esym(syms::node_type); + pars.str(tmp); + I(tmp == "file"); + + pars.esym (syms::ancestor_name); pars.str(); + pars.esym (syms::ancestor_file_id); pars.hex(); + + pars.esym (syms::left_name); pars.str(left_name); + pars.esym(syms::left_file_id); pars.hex(); + + pars.esym (syms::right_name); pars.str(right_name); + pars.esym(syms::right_file_id); pars.hex(); + + left_nid = left_roster.get_node (file_path_internal (left_name))->self; + right_nid = right_roster.get_node (file_path_internal (right_name))->self; + + N(left_nid == conflict.nid & right_nid == conflict.nid, + F("conflicts file does not match current conflicts: (file_content, left %s, right %s") + % left_name % right_name); + + // check for a resolution + if ((!pars.symp (syms::conflict)) && pars.tok.in.lookahead != EOF) + { + if (pars.symp (syms::resolved_internal)) + { + conflict.resolution.first = resolve_conflicts::content_internal; + pars.sym(); + } + else if (pars.symp (syms::resolved_user)) + { + conflict.resolution.first = resolve_conflicts::content_user; + pars.sym(); + conflict.resolution.second = file_path_internal (pars.token); + pars.str(); + } + else + N(false, F("%s is not a supported conflict resolution for %s") % pars.token % "file_content"); + } + + if (pars.tok.in.lookahead != EOF) + pars.esym (syms::conflict); + else + { + std::vector::iterator tmp = i; + N(++tmp == conflicts.end(), F("conflicts file does not match current conflicts")); + } + } +} // parse_file_content_conflicts + +static void +parse_resolve_conflicts_str(basic_io::parser & pars, roster_merge_result & result) +{ + char const * error_message_1 = "can't specify a %s conflict resolution for more than one conflict"; + char const * error_message_2 = "conflict resolution %s is not appropriate for current conflicts"; + + // We don't detect all cases of inappropriate resolutions here; that would + // be too hard to maintain as more conflicts and/or resolutions are added. + // If the single resolution specified is not appropriate for some + // conflict, that conflict will not be resolved, which will be reported + // later. Then the user will need to use a conflict resolution file. + while (pars.tok.in.lookahead != EOF) + { + // resolution alphabetical order + if (pars.symp (syms::resolved_rename_left)) + { + N(result.duplicate_name_conflicts.size() == 1, + F(error_message_1) % syms::resolved_rename_left); + + duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); + + conflict.left_resolution.first = resolve_conflicts::rename; + pars.sym(); + conflict.left_resolution.second = file_path_internal (pars.token); + pars.str(); + } + else if (pars.symp (syms::resolved_rename_right)) + { + N(result.duplicate_name_conflicts.size() == 1, + F(error_message_1) % syms::resolved_rename_right); + + duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); + + conflict.right_resolution.first = resolve_conflicts::rename; + pars.sym(); + conflict.right_resolution.second = file_path_internal (pars.token); + pars.str(); + } + else if (pars.symp (syms::resolved_user)) + { + N(result.file_content_conflicts.size() == 1, + F(error_message_1) % syms::resolved_user); + + file_content_conflict & conflict = *result.file_content_conflicts.begin(); + + conflict.resolution.first = resolve_conflicts::content_user; + pars.sym(); + conflict.resolution.second = file_path_internal (pars.token); + pars.str(); + } + else + N(false, F("%s is not a supported conflict resolution") % pars.token); + + } // while +} + void +parse_resolve_conflicts_opts (options const & opts, + roster_t const & left_roster, + roster_t const & right_roster, + roster_merge_result & result, + bool & resolutions_given) +{ + if (opts.resolve_conflicts_given) + { + resolutions_given = true; + + basic_io::input_source src(opts.resolve_conflicts, "resolve_conflicts string"); + basic_io::tokenizer tok(src); + basic_io::parser pars(tok); + + parse_resolve_conflicts_str(pars, result); + + if (src.lookahead != EOF) + pars.err("invalid conflict resolution syntax"); + } + else if (opts.resolve_conflicts_file_given) + { + resolutions_given = true; + + data dat; + + if (opts.resolve_conflicts_file().substr(0, 4) == "_MTN") + read_data (bookkeeping_path(opts.resolve_conflicts_file()), dat); + else + read_data (file_path_external(opts.resolve_conflicts_file), dat); + + basic_io::input_source src(dat(), opts.resolve_conflicts_file()); + basic_io::tokenizer tok(src); + basic_io::parser pars(tok); + + // Skip left, right, ancestor. FIXME_SUTURE: should check these! But don't + // see how to access them right now. + for (int i = 1; i <= 3; i++) + { + pars.sym(); + pars.hex(); + } + + // Get into the first conflict + pars.esym (syms::conflict); + + // There must be one stanza in the file for each conflict; otherwise + // something has changed since the file was regenerated. So we go thru + // the conflicts in the same order they are generated; see merge.cc + // resolve_merge_conflicts. + + // resolve_merge_conflicts should not call us if there are any + // conflicts for which we don't currently support resolutions; assert + // that + + I(!result.missing_root_dir); + I(result.invalid_name_conflicts.size() == 0); + I(result.directory_loop_conflicts.size() == 0); + I(result.orphaned_node_conflicts.size() == 0); + I(result.multiple_name_conflicts.size() == 0); + I(result.attribute_conflicts.size() == 0); + + // These are the ones we know how to resolve. + + parse_duplicate_name_conflicts(pars, result.duplicate_name_conflicts, left_roster, right_roster); + parse_file_content_conflicts(pars, result.file_content_conflicts, left_roster, right_roster); + + if (src.lookahead != EOF) + pars.err("extra conflicts in file"); + } + else + resolutions_given = false; + +} // parse_resolve_conflicts_opts + +static void +attach_node (lua_hooks & lua, + roster_t & new_roster, + node_id nid, + file_path const target_path) +{ + // Simplified from workspace::perform_rename in work.cc + + I(!target_path.empty()); + + N(!new_roster.has_node(target_path), F("%s already exists") % target_path.as_external()); + N(new_roster.has_node(target_path.dirname()), + F("directory %s does not exist or is unknown") % target_path.dirname()); + + new_roster.attach_node (nid, target_path); + + node_t node = new_roster.get_node (nid); + for (full_attr_map_t::const_iterator attr = node->attrs.begin(); + attr != node->attrs.end(); + ++attr) + lua.hook_apply_attribute (attr->first(), target_path, attr->second.second()); + +} // attach_node + +void +roster_merge_result::resolve_duplicate_name_conflicts(lua_hooks & lua, + roster_t const & left_roster, + roster_t const & right_roster, + content_merge_adaptor & adaptor) +{ + MM(left_roster); + MM(right_roster); + MM(this->roster); // New roster + + // Conflict nodes are present but detached (without filenames) in the new + // roster. The resolution is either to suture the two files together, or to + // rename one or both. + + for (std::vector::const_iterator i = duplicate_name_conflicts.begin(); + i != duplicate_name_conflicts.end(); + ++i) + { + duplicate_name_conflict const & conflict = *i; + MM(conflict); + + node_id left_nid = conflict.left_nid; + node_id right_nid= conflict.right_nid; + + file_path left_name, right_name; + + left_roster.get_name(left_nid, left_name); + right_roster.get_name(right_nid, right_name); + + switch (conflict.left_resolution.first) + { + case resolve_conflicts::rename: + P(F("renaming %s to %s") % left_name % conflict.left_resolution.second); + attach_node (lua, this->roster, left_nid, conflict.left_resolution.second); + break; + + case resolve_conflicts::none: + N(false, F("no resolution provided for duplicate_name %s") % left_name); + break; + + default: + N(false, F("%s: invalid resolution for this conflict") % image (conflict.left_resolution.first)); + } + + switch (conflict.right_resolution.first) + { + case resolve_conflicts::rename: + P(F("renaming %s to %s") % right_name % conflict.right_resolution.second); + attach_node (lua, this->roster, right_nid, conflict.right_resolution.second); + break; + + case resolve_conflicts::none: + // Just keep current name + this->roster.attach_node (right_nid, right_name); + break; + + default: + N(false, F("%s: invalid resolution for this conflict") % image (conflict.right_resolution.first)); + } + } // end for + + duplicate_name_conflicts.clear(); +} + +void +roster_merge_result::resolve_file_content_conflicts(lua_hooks & lua, + roster_t const & left_roster, + roster_t const & right_roster, + content_merge_adaptor & adaptor) +{ + MM(left_roster); + MM(right_roster); + MM(this->roster); // New roster + + // Conflict node is present and attached in the new roster, with a null + // file content id. The resolution is to enter the user specified file + // content in the database and roster, or let the internal line merger + // handle it. + + for (std::vector::const_iterator i = file_content_conflicts.begin(); + i != file_content_conflicts.end(); + ++i) + { + file_content_conflict const & conflict = *i; + MM(conflict); + + file_path left_name, right_name; + + left_roster.get_name(conflict.nid, left_name); + right_roster.get_name(conflict.nid, right_name); + + switch (conflict.resolution.first) + { + case resolve_conflicts::content_internal: + case resolve_conflicts::none: + { + file_id merged_id; + + N(resolve_conflicts::do_auto_merge(lua, conflict, adaptor, left_roster, + right_roster, this->roster, merged_id), + F("merge of %s, %s failed") % left_name % right_name); + + P(F("merged %s, %s") % left_name % right_name); + + file_t result_node = downcast_to_file_t(roster.get_node(conflict.nid)); + result_node->content = merged_id; + } + break; + + case resolve_conflicts::content_user: + { + P(F("replacing content of %s, %s with %s") % left_name % right_name % conflict.resolution.second); + + file_id result_id; + file_data left_data, right_data, result_data; + data result_raw_data; + adaptor.get_version(conflict.left, left_data); + adaptor.get_version(conflict.right, right_data); + read_data(conflict.resolution.second, result_raw_data); + result_data = file_data(result_raw_data); + calculate_ident(result_data, result_id); + + file_t result_node = downcast_to_file_t(roster.get_node(conflict.nid)); + result_node->content = result_id; + + adaptor.record_merge(conflict.left, conflict.right, result_id, + left_data, right_data, result_data); + + } + break; + + default: + I(false); + } + + } // end for + + file_content_conflicts.clear(); +} + +void roster_merge_result::clear() { missing_root_dir = false; @@ -1396,22 +1949,22 @@ namespace bool left_wins = a_wins(right_marks, right_uncommon_ancestors); bool right_wins = a_wins(left_marks, left_uncommon_ancestors); // two bools means 4 cases: - // left_wins && right_wins + // this is ambiguous clean merge, which is theoretically impossible. I(!(left_wins && right_wins)); - // left_wins && !right_wins + if (left_wins && !right_wins) { result = left; return true; } - // !left_wins && right_wins + if (!left_wins && right_wins) { result = right; return true; } - // !left_wins && !right_wins + if (!left_wins && !right_wins) { conflict_descriptor.left = left; ============================================================ --- roster_merge.hh 48d9aa743811732826e3f855a5759c1d61416984 +++ roster_merge.hh 8f0603774befb2458b9e6a2660a90f31104a9a22 @@ -1,6 +1,7 @@ #ifndef __ROSTER_MERGE_HH__ #define __ROSTER_MERGE_HH__ +// Copyright (C) 2008 Stephen Leake // Copyright (C) 2005 Nathaniel Smith // // This program is made available under the GNU GPL version 2.0 or @@ -10,6 +11,8 @@ // implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. +#include + #include "rev_types.hh" #include "database.hh" #include "diff_patch.hh" @@ -24,6 +27,11 @@ // manifest.) // +namespace resolve_conflicts +{ + enum resolution_t {none, content_user, content_internal, rename}; +} + // renaming the root dir allows these: // -- _MTN in root // -- missing root directory @@ -86,6 +94,11 @@ struct duplicate_name_conflict { node_id left_nid, right_nid; std::pair parent_name; + std::pair left_resolution, right_resolution; + + duplicate_name_conflict () + {left_resolution.first = resolve_conflicts::none; + right_resolution.first = resolve_conflicts::none;}; }; // nodes with attribute conflicts are left attached in the resulting tree (unless @@ -107,11 +120,17 @@ struct file_content_conflict struct file_content_conflict { node_id nid; - file_content_conflict(node_id nid) : nid(nid) {} file_id left, right; -}; + std::pair resolution; + file_content_conflict () : + nid(the_null_node), + resolution(std::make_pair(resolve_conflicts::none, file_path())) {}; + file_content_conflict(node_id nid) : + nid(nid), resolution(std::make_pair(resolve_conflicts::none, file_path())) {}; +}; + template <> void dump(invalid_name_conflict const & conflict, std::string & out); template <> void dump(directory_loop_conflict const & conflict, std::string & out); @@ -180,23 +199,35 @@ struct roster_merge_result content_merge_adaptor & adaptor, bool const basic_io, std::ostream & output) const; + void report_duplicate_name_conflicts(roster_t const & left, roster_t const & right, content_merge_adaptor & adaptor, bool const basic_io, std::ostream & output) const; + void resolve_duplicate_name_conflicts(lua_hooks & lua, + roster_t const & left_roster, + roster_t const & right_roster, + content_merge_adaptor & adaptor); void report_attribute_conflicts(roster_t const & left, roster_t const & right, content_merge_adaptor & adaptor, bool const basic_io, std::ostream & output) const; - void report_file_content_conflicts(roster_t const & left, - roster_t const & right, + + // not 'const' because this sets resolution to 'resolved_internal' if the + // internal merger would succeed. + void report_file_content_conflicts(lua_hooks & lua, + roster_t const & left_roster, + roster_t const & right_roster, content_merge_adaptor & adaptor, bool const basic_io, - std::ostream & output) const; - + std::ostream & output); + void resolve_file_content_conflicts(lua_hooks & lua, + roster_t const & left_roster, + roster_t const & right_roster, + content_merge_adaptor & adaptor); void clear(); }; @@ -211,6 +242,12 @@ roster_merge(roster_t const & left_paren std::set const & right_uncommon_ancestors, roster_merge_result & result); +void +parse_resolve_conflicts_opts (options const & opts, + roster_t const & left_roster, + roster_t const & right_roster, + roster_merge_result & result, + bool & resolutions_given); // Local Variables: // mode: C++ ============================================================ --- testlib.lua 0e8c0415f85924dfb385a6e6a0de8b0f787a611d +++ testlib.lua 785c976d13cafe3d472fa9d7121ddb7f75a4bae9 @@ -439,6 +439,10 @@ end end end +function samefilestd(left, right) + return samefile(testdir .. "/" .. test.name .. "/" .. left, right) +end + function samelines(f, t) local fl = {} for l in io.lines(f) do table.insert(fl, l) end