# # # add_dir "tests/resolve_duplicate_name_conflict_drop_vs_suture" # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/__driver__.lua" # content [f87d5a753d24516c35db3e468c085b2ede9fcf47] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_a_b-conflicts-resolve" # content [fd8a5bc6aa764873c85b65cb677aa2cc054deaf7] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts" # content [ec2604699eb4a24ef6f101c6969ae638b9131b46] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts-resolve-1" # content [b1c2df24b910d9d4dedcfbd4498a9ec26ee2b8eb] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts-resolve-2" # content [091ef902f8e76456dcbc6583b8e2c3c95d2c9d2a] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-1" # content [33ab8b27af438df4115248bdbb01d0ef3e05743d] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-2" # content [8bdba2d3318c1af0971a5b82320e1800ec3527f0] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-3" # content [5811894d18f56aeb7cb8f0fe1ae8748154eeebc7] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_g_d-message-1" # content [a9df3392a16b1486a1c1f02011b35dd5d4558f13] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_g_d-message-2" # content [2f2b77fef092a5212d7af70295583b5897cbecef] # # add_file "tests/resolve_duplicate_name_conflict_drop_vs_suture/update-message-1" # content [58f9ab361d833fe8439fbca562ee37bebc50e0be] # # patch "cmd_merging.cc" # from [473026835519b620a06b47b9386b4bd739a68350] # to [83cf17ff88126ef9cf541b1ce8851f8ce32228cd] # # patch "merge.cc" # from [81b4bfcb4523d510efedd8c5758b4decc10e6095] # to [48d7db2994ac47a9b4d0d2a4a5c3bd4c5ef7083d] # # patch "monotone.texi" # from [53a2aa982f8fbc875b31ce1db576838753880da9] # to [8aefbc696e12aad56800a6b17f0c54438386544f] # # patch "roster_merge.cc" # from [49b49de7d0d89f40cc774d6237e7d4a6ff0ec999] # to [5714092f9912cf902207f1bfd6272ff59271d19c] # # patch "roster_merge.hh" # from [7e40959f06f4dc24ebec108a2a9f2cdfe1e6eb4c] # to [4617dc0cfec57a20195f0655214acfcc1acad33d] # # patch "ss-existence-merge.text" # from [84169b7caf7da082a6393fc1ecd4c83a16f154da] # to [26afb7b1f0e0ad519cf3873c319394fb9cddc9bd] # # patch "testlib.lua" # from [01d860a7d8781f38ea776b60e3b4fd875e2766aa] # to [36e8a3f33665718abff5bdab3f309879d48f8974] # ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/__driver__.lua f87d5a753d24516c35db3e468c085b2ede9fcf47 +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/__driver__.lua f87d5a753d24516c35db3e468c085b2ede9fcf47 @@ -0,0 +1,155 @@ +-- Test/demonstrate handling of inconsistent resolutions of a duplicate name conflict. +-- +-- We have this revision history graph: +-- +-- FIXME: add more files to show other resolution choices at each step +-- +-- o +-- / \ +-- A1a B2b +-- /| \ /| +-- / | X | +-- / C / \| +-- / |/ D3d +-- E1e F2b +-- +-- 'o' is the base revision. In 'A' and 'B', Abe and Beth each add a +-- file with the same name 'checkout.sh'. +-- +-- in 'D', Beth resolves the duplicate name conflict by suturing. +-- +-- in 'C', Abe prepares to resolve the duplicate name conflict by +-- droppng his version; he merges in F, keeping Beth's (Abe hasn't +-- learned about suture yet). +-- +-- in 'E', Jim edits checkout.sh. +-- +-- Then we consider the two possible merge orders for D, E, and F, and +-- show that they produce consistent results. +-- +-- Merging E, F to G encounters a content/drop conflict, resolved by suture. +-- Merging G, D to H encounters a content conflict, resolved by keeping Jim's content. +-- +-- o +-- / \ +-- A1a B2b +-- /| \ /| +-- / | X | +-- / C / \| +-- / |/ D3d +-- E1e F2b / +-- \ / / +-- G4e / +-- \ / +-- H5e +-- +-- Merging D, F to G first gives one drop/suture conflict, resolved by ignore_drop. +-- Merging E and G to H is then a content conflict, resolved by keeping +-- Jim's content: +-- +-- o +-- / \ +-- A1a B2b +-- /| \ /| +-- / | X | +-- / C / \| +-- / |/ D3d +-- E1e F2b / +-- \ \ / +-- \ G3d +-- \ / +-- H3e + +mtn_setup() + +-- Get a non-empty base revision +addfile("randomfile", "blah blah blah") +commit() +base = base_revision() + +-- Abe adds his file +addfile("checkout.sh", "checkout.sh abe 1") +commit("testbranch", "rev_A") +rev_A = base_revision() + +revert_to(base) + +-- Beth adds her file and merges to D via suture +addfile("checkout.sh", "checkout.sh beth 1") +commit("testbranch", "rev_B") +rev_B = base_revision() + +-- Beth sutures +check(mtn("automate", "show_conflicts"), 0, true, nil) +writefile ("checkout.sh", "checkout.sh merged") +get ("merge_a_b-conflicts-resolve", "_MTN/conflicts") +check(mtn("merge", "--resolve-conflicts-file=_MTN/conflicts"), 0, nil, false) +check(mtn("update"), 0, nil, false) +rev_D = base_revision() + +-- Abe merges to F via drop +revert_to(rev_A) + +check(mtn("drop", "checkout.sh"), 0, nil, false) +commit("testbranch", "rev_C") +rev_C = base_revision() + +check(mtn("explicit_merge", rev_C, rev_B, "testbranch"), 0, nil, false) +check(mtn("update"), 0, nil, false) +check("checkout.sh beth 1" == readfile("checkout.sh")) +rev_F = base_revision() + +-- Jim edits to get E +revert_to(rev_A) + +writefile("checkout.sh", "checkout.sh jim 1") + +commit("testbranch", "rev_E") +rev_E = base_revision() + +-- plain 'merge' chooses to merge E, F first; that gives duplicate name and content/drop conflicts +-- which just shows that 'drop' is _not_ a good way to resolve duplicate name conflicts. +check(mtn("merge"), 1, nil, true) +canonicalize("stderr") +check(samefilestd("merge_e_f-message-1", "stderr")) + +check(mtn("automate", "show_conflicts"), 0, true, nil) +canonicalize("stdout") +check(samefilestd("merge_e_f-conflicts", "stdout")) + +-- first resolution attempt fails +get("merge_e_f-conflicts-resolve-1", "_MTN/conflicts") +check(mtn("merge", "--resolve-conflicts-file=_MTN/conflicts"), 1, nil, true) +canonicalize("stderr") +check(samefilestd("merge_e_f-message-2", "stderr")) + +-- second resolution attempt succeeds +get("merge_e_f-conflicts-resolve-2", "_MTN/conflicts") +check(mtn("merge", "--resolve-conflicts-file=_MTN/conflicts"), 0, nil, true) +canonicalize("stderr") +check(samefilestd("merge_e_f-message-3", "stderr")) + +-- Now merge G, D +-- This fails with duplicate name conflict +check(mtn("merge"), 1, nil, true) +canonicalize("stderr") +check(samefilestd("merge_g_d-message-1", "stderr")) + +check(mtn("merge", "--resolve-conflicts", 'resolved_suture "checkout.sh"'), 0, nil, true) +canonicalize("stderr") +check(samefilestd("merge_g_d-message-2", "stderr")) + +check(mtn("update"), 0, nil, true) +canonicalize("stderr") +check(samefilestd("update-message-1", "stderr")) +rev_H = base_revision() + +-- go back and try other merge order +revert_to(rev_E) +check(mtn("db", "kill_rev_locally", rev_H), 0, nil, false) +check(mtn("db", "kill_rev_locally", "98997255d9ff7355d9e5ee2287aba2e8e8fe33e0"), 0) -- rev_G + +-- This gives a drop/suture conflict +check(mtn("explicit_merge", rev_D, rev_F, "testbranch"), 1, nil, true) +check(mtn("update", "--revision=59d830bd65520e2a961aae0d31afd9bd24799b5e"), 0, nil, false) +-- end of file ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_a_b-conflicts-resolve fd8a5bc6aa764873c85b65cb677aa2cc054deaf7 +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_a_b-conflicts-resolve fd8a5bc6aa764873c85b65cb677aa2cc054deaf7 @@ -0,0 +1,12 @@ + left [ae94e6677b8e31692c67d98744dccf5fa9ccffe5] + right [dfdf50b19fb971f502671b0cfa6d15d69a0d04bb] +ancestor [b3ac8a77cee78263b66800c635441ecb1f259a42] + + conflict duplicate_name + left_type "added file" + left_name "checkout.sh" + left_file_id [61b8d4fb0e5d78be111f691b955d523c782fa92e] + right_type "added file" + right_name "checkout.sh" + right_file_id [dd6805ae36432d6edcbdff6ea578ea981ffa2144] +resolved_suture "checkout.sh" ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts ec2604699eb4a24ef6f101c6969ae638b9131b46 +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts ec2604699eb4a24ef6f101c6969ae638b9131b46 @@ -0,0 +1,16 @@ + left [38fee2fc132b056f10476fff55f10ccaa52b5452] + right [f214512c044555f3519597c19e6a50399ec81241] +ancestor [ae94e6677b8e31692c67d98744dccf5fa9ccffe5] + + conflict duplicate_name + left_type "added file" + left_name "checkout.sh" + left_file_id [dd6805ae36432d6edcbdff6ea578ea981ffa2144] + right_type "added file" + right_name "checkout.sh" +right_file_id [7b4836e9931cf1a3adfc8c432a3e407b2fef84e9] + + conflict content_drop + right_type "file" + right_name "checkout.sh" +right_file_id [7b4836e9931cf1a3adfc8c432a3e407b2fef84e9] ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts-resolve-1 b1c2df24b910d9d4dedcfbd4498a9ec26ee2b8eb +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts-resolve-1 b1c2df24b910d9d4dedcfbd4498a9ec26ee2b8eb @@ -0,0 +1,18 @@ + left [38fee2fc132b056f10476fff55f10ccaa52b5452] + right [f214512c044555f3519597c19e6a50399ec81241] +ancestor [ae94e6677b8e31692c67d98744dccf5fa9ccffe5] + + conflict duplicate_name + left_type "added file" + left_name "checkout.sh" + left_file_id [dd6805ae36432d6edcbdff6ea578ea981ffa2144] + right_type "added file" + right_name "checkout.sh" + right_file_id [7b4836e9931cf1a3adfc8c432a3e407b2fef84e9] +resolved_suture "checkout.sh" + + conflict content_drop + right_type "file" + right_name "checkout.sh" + right_file_id [7b4836e9931cf1a3adfc8c432a3e407b2fef84e9] +resolved_ignore_drop "checkout.sh" ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts-resolve-2 091ef902f8e76456dcbc6583b8e2c3c95d2c9d2a +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-conflicts-resolve-2 091ef902f8e76456dcbc6583b8e2c3c95d2c9d2a @@ -0,0 +1,18 @@ + left [38fee2fc132b056f10476fff55f10ccaa52b5452] + right [f214512c044555f3519597c19e6a50399ec81241] +ancestor [ae94e6677b8e31692c67d98744dccf5fa9ccffe5] + + conflict duplicate_name + left_type "added file" + left_name "checkout.sh" + left_file_id [dd6805ae36432d6edcbdff6ea578ea981ffa2144] + right_type "added file" + right_name "checkout.sh" + right_file_id [7b4836e9931cf1a3adfc8c432a3e407b2fef84e9] +resolved_suture "checkout.sh" + + conflict content_drop + right_type "file" + right_name "checkout.sh" + right_file_id [7b4836e9931cf1a3adfc8c432a3e407b2fef84e9] +resolved_suture "checkout.sh" ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-1 33ab8b27af438df4115248bdbb01d0ef3e05743d +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-1 33ab8b27af438df4115248bdbb01d0ef3e05743d @@ -0,0 +1,10 @@ +mtn: 3 heads on branch 'testbranch' +mtn: merge 1 / 2: +mtn: calculating best pair of heads to merge next +mtn: [left] 38fee2fc132b056f10476fff55f10ccaa52b5452 +mtn: [right] f214512c044555f3519597c19e6a50399ec81241 +mtn: conflict: duplicate name 'checkout.sh' for the directory '' +mtn: added as a new file on the left +mtn: added as a new file on the right +mtn: conflict: file 'checkout.sh' dropped on the right, changed on the left +mtn: error: merge failed due to unresolved conflicts ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-2 8bdba2d3318c1af0971a5b82320e1800ec3527f0 +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-2 8bdba2d3318c1af0971a5b82320e1800ec3527f0 @@ -0,0 +1,7 @@ +mtn: 3 heads on branch 'testbranch' +mtn: merge 1 / 1: +mtn: calculating best pair of heads to merge next +mtn: [left] 38fee2fc132b056f10476fff55f10ccaa52b5452 +mtn: [right] f214512c044555f3519597c19e6a50399ec81241 +mtn: suturing checkout.sh, checkout.sh into checkout.sh +mtn: misuse: checkout.sh was sutured in this merge; resolution must be 'resolved_suture' ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-3 5811894d18f56aeb7cb8f0fe1ae8748154eeebc7 +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_e_f-message-3 5811894d18f56aeb7cb8f0fe1ae8748154eeebc7 @@ -0,0 +1,9 @@ +mtn: 3 heads on branch 'testbranch' +mtn: merge 1 / 1: +mtn: calculating best pair of heads to merge next +mtn: [left] 38fee2fc132b056f10476fff55f10ccaa52b5452 +mtn: [right] f214512c044555f3519597c19e6a50399ec81241 +mtn: suturing checkout.sh, checkout.sh into checkout.sh +mtn: [merged] 98997255d9ff7355d9e5ee2287aba2e8e8fe33e0 +mtn: note: branch 'testbranch' still has 2 heads; run merge again +mtn: note: your workspaces have not been updated ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_g_d-message-1 a9df3392a16b1486a1c1f02011b35dd5d4558f13 +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_g_d-message-1 a9df3392a16b1486a1c1f02011b35dd5d4558f13 @@ -0,0 +1,9 @@ +mtn: 2 heads on branch 'testbranch' +mtn: merge 1 / 1: +mtn: calculating best pair of heads to merge next +mtn: [left] 75354508f1be7980da4cfbd0edeabf58492a2d49 +mtn: [right] 98997255d9ff7355d9e5ee2287aba2e8e8fe33e0 +mtn: conflict: duplicate name 'checkout.sh' for the directory '' +mtn: added as a new file on the left +mtn: added as a new file on the right +mtn: error: merge failed due to unresolved conflicts ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_g_d-message-2 2f2b77fef092a5212d7af70295583b5897cbecef +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/merge_g_d-message-2 2f2b77fef092a5212d7af70295583b5897cbecef @@ -0,0 +1,8 @@ +mtn: 2 heads on branch 'testbranch' +mtn: merge 1 / 1: +mtn: calculating best pair of heads to merge next +mtn: [left] 75354508f1be7980da4cfbd0edeabf58492a2d49 +mtn: [right] 98997255d9ff7355d9e5ee2287aba2e8e8fe33e0 +mtn: suturing checkout.sh, checkout.sh into checkout.sh +mtn: [merged] 9c7bb44261ee498d6516fe39df5164b5674610a1 +mtn: note: your workspaces have not been updated ============================================================ --- tests/resolve_duplicate_name_conflict_drop_vs_suture/update-message-1 58f9ab361d833fe8439fbca562ee37bebc50e0be +++ tests/resolve_duplicate_name_conflict_drop_vs_suture/update-message-1 58f9ab361d833fe8439fbca562ee37bebc50e0be @@ -0,0 +1,5 @@ +mtn: updating along branch 'testbranch' +mtn: selected update target 9c7bb44261ee498d6516fe39df5164b5674610a1 +mtn: adding checkout.sh +mtn: dropping checkout.sh +mtn: updated to base revision 9c7bb44261ee498d6516fe39df5164b5674610a1 ============================================================ --- cmd_merging.cc 473026835519b620a06b47b9386b4bd739a68350 +++ cmd_merging.cc 83cf17ff88126ef9cf541b1ce8851f8ce32228cd @@ -405,7 +405,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; @@ -479,6 +488,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 @@ -491,7 +506,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")); @@ -507,19 +522,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")); } @@ -949,6 +954,7 @@ show_conflicts_core (database & db, result.report_orphaned_node_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); result.report_multiple_name_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); result.report_duplicate_name_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); + result.report_content_drop_conflicts(*l_roster, *r_roster, basic_io, output); result.report_attribute_conflicts(*l_roster, *r_roster, adaptor, basic_io, output); result.report_file_content_conflicts(lua, *l_roster, *r_roster, adaptor, basic_io, output); @@ -1012,19 +1018,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) { ============================================================ --- merge.cc 81b4bfcb4523d510efedd8c5758b4decc10e6095 +++ merge.cc 48d7db2994ac47a9b4d0d2a4a5c3bd4c5ef7083d @@ -140,6 +140,7 @@ resolve_merge_conflicts(lua_hooks & lua, // resolve the ones we can. result.resolve_duplicate_name_conflicts(lua, left_roster, right_roster, adaptor); + result.resolve_content_drop_conflicts(left_roster, right_roster); result.resolve_file_content_conflicts(lua, left_roster, right_roster, adaptor); } } @@ -153,6 +154,7 @@ resolve_merge_conflicts(lua_hooks & lua, result.report_orphaned_node_conflicts(left_roster, right_roster, adaptor, false, std::cout); result.report_multiple_name_conflicts(left_roster, right_roster, adaptor, false, std::cout); result.report_duplicate_name_conflicts(left_roster, right_roster, adaptor, false, std::cout); + result.report_content_drop_conflicts(left_roster, right_roster, false, std::cout); result.report_attribute_conflicts(left_roster, right_roster, adaptor, false, std::cout); result.report_file_content_conflicts(lua, left_roster, right_roster, adaptor, false, std::cout); ============================================================ --- monotone.texi 53a2aa982f8fbc875b31ce1db576838753880da9 +++ monotone.texi 8aefbc696e12aad56800a6b17f0c54438386544f @@ -3024,16 +3024,24 @@ @section Merge Conflicts @item address@hidden @end table -For @var{--resolve-conflicts-file}, the file must contain the output +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 @var{--resolve-conflicts}, a single conflict resolution is given +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. ============================================================ --- roster_merge.cc 49b49de7d0d89f40cc774d6237e7d4a6ff0ec999 +++ roster_merge.cc 5714092f9912cf902207f1bfd6272ff59271d19c @@ -28,6 +28,45 @@ using std::string; using std::set; using std::string; +static char * const +image(resolve_conflicts::side_t side) +{ + switch (side) + { + case resolve_conflicts::left_side: + return "left"; + case resolve_conflicts::right_side: + return "right"; + + default: + I(false); + } +} + +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::ignore_drop: + return "ignore_drop"; + case resolve_conflicts::rename: + return "rename"; + case resolve_conflicts::respect_drop: + return "respect_drop"; + case resolve_conflicts::suture: + return "suture"; + default: + I(false); + } +} + template <> void dump(invalid_name_conflict const & conflict, string & out) { @@ -77,11 +116,44 @@ 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(); } template <> void +dump(content_drop_conflict const & conflict, string & out) +{ + ostringstream oss; + oss << "content_drop_conflict: " + << "node: "<< conflict.nid << " " + << "content: " << conflict.fid << " " + << "parent_side: " << image(conflict.parent_side); + + if (conflict.resolution.first != resolve_conflicts::none) + { + oss << " resolution: " << image(conflict.resolution.first); + if (conflict.resolution.first != resolve_conflicts::ignore_drop) + { + oss << " new_name: " << conflict.resolution.second; + } + } + oss << "\n"; + out = oss.str(); +} + +template <> void dump(attribute_conflict const & conflict, string & out) { ostringstream oss; @@ -127,6 +199,7 @@ roster_merge_result::has_non_content_con || !orphaned_node_conflicts.empty() || !multiple_name_conflicts.empty() || !duplicate_name_conflicts.empty() + || !content_drop_conflicts.empty() || !attribute_conflicts.empty(); } static void @@ -141,6 +214,7 @@ dump_conflicts(roster_merge_result const dump(result.orphaned_node_conflicts, out); dump(result.multiple_name_conflicts, out); dump(result.duplicate_name_conflicts, out); + dump(result.content_drop_conflicts, out); dump(result.attribute_conflicts, out); dump(result.file_content_conflicts, out); @@ -190,6 +264,7 @@ namespace symbol const attribute("attribute"); symbol const conflict("conflict"); symbol const content("content"); + symbol const content_drop("content_drop"); symbol const directory_loop_created("directory_loop_created"); symbol const duplicate_name("duplicate_name"); symbol const invalid_name("invalid_name"); @@ -203,9 +278,11 @@ namespace symbol const node_type("node_type"); symbol const orphaned_directory("orphaned_directory"); symbol const orphaned_file("orphaned_file"); + symbol const resolved_ignore_drop("resolved_ignore_drop"); 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_respect_drop("resolved_respect_drop"); symbol const resolved_suture ("resolved_suture"); symbol const resolved_user("resolved_user"); symbol const right_attr_state("right_attr_state"); @@ -486,6 +563,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); + } } } @@ -1174,6 +1261,85 @@ void } void +roster_merge_result::report_content_drop_conflicts(roster_t const & left_roster, + roster_t const & right_roster, + bool const basic_io, + std::ostream & output) const +{ + for (size_t i = 0; i < content_drop_conflicts.size(); ++i) + { + content_drop_conflict const & conflict = content_drop_conflicts[i]; + + I(!roster.is_attached(conflict.nid)); + + basic_io::stanza st; + + file_path name; + + switch (conflict.parent_side) + { + case resolve_conflicts::left_side: + left_roster.get_name (conflict.nid, name); + I(file_type == get_type (left_roster, conflict.nid)); + + if (basic_io) + { + st.push_str_pair(syms::conflict, syms::content_drop); + st.push_str_pair(syms::left_type, "file"); + st.push_file_pair(syms::left_name, name); + st.push_binary_pair(syms::left_file_id, conflict.fid.inner()); + } + else + { + P(F("conflict: file '%s' dropped on the right, changed on the left") % name); + } + + break; + + case resolve_conflicts::right_side: + right_roster.get_name (conflict.nid, name); + I(file_type == get_type (right_roster, conflict.nid)); + + if (basic_io) + { + st.push_str_pair(syms::conflict, syms::content_drop); + st.push_str_pair(syms::right_type, "file"); + st.push_file_pair(syms::right_name, name); + st.push_binary_pair(syms::right_file_id, conflict.fid.inner()); + } + else + { + P(F("conflict: file '%s' dropped on the right, changed on the left") % name); + } + + break; + } + + if (basic_io) + { + switch (conflict.resolution.first) + { + case resolve_conflicts::none: + break; + + case resolve_conflicts::ignore_drop: + st.push_file_pair(syms::resolved_ignore_drop, conflict.resolution.second); + break; + + case resolve_conflicts::respect_drop: + st.push_symbol(syms::resolved_respect_drop); + break; + + default: + I(false); + } + + put_stanza(st, output); + } + } +} + +void roster_merge_result::report_attribute_conflicts(roster_t const & left_roster, roster_t const & right_roster, content_merge_adaptor & adaptor, @@ -1422,30 +1588,6 @@ namespace resolve_conflicts namespace resolve_conflicts { - char* image (resolution_t resolution) - { - switch (resolution) - { - case none: - return "none"; - - case content_internal: - return "content_internal"; - - case content_user: - return "content_user"; - - case suture: - return "suture"; - - case rename: - return "rename"; - - } - - return ""; // suppress bogus compiler warning - } - bool do_auto_merge(lua_hooks & lua, file_content_conflict const & conflict, @@ -1549,6 +1691,88 @@ static void } // parse_duplicate_name_conflicts static void +parse_content_drop_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; + string name; + resolve_conflicts::side_t parent_side; + node_id nid; + string hex_fid; + + content_drop_conflict & conflict = *i; + + pars.esym(syms::content_drop); + + if (pars.symp (syms::left_type)) + { + parent_side = resolve_conflicts::left_side; + pars.sym(); + pars.str(tmp); + I(tmp == "file"); + pars.esym(syms::left_name); + pars.str(name); + pars.esym(syms::left_file_id); pars.hex(hex_fid); + nid = left_roster.get_node (file_path_internal (name))->self; + } + else + { + parent_side = resolve_conflicts::right_side; + pars.sym(); + pars.str(tmp); + I(tmp == "file"); + pars.esym (syms::right_name); pars.str(name); + pars.esym(syms::right_file_id); pars.hex(hex_fid); + nid = right_roster.get_node (file_path_internal (name))->self; + } + + N(parent_side == conflict.parent_side && nid == conflict.nid && hex_fid == encode_hexenc(conflict.fid.inner()()), + F("conflicts file does not match current conflicts: content_drop, name %s") + % name); + + // check for a resolution + if ((!pars.symp (syms::conflict)) && pars.tok.in.lookahead != EOF) + { + if (pars.symp (syms::resolved_suture)) + { + conflict.resolution.first = resolve_conflicts::suture; + pars.sym(); + pars.str(tmp); + conflict.resolution.second = file_path_internal(tmp); + } + else if (pars.symp (syms::resolved_ignore_drop)) + { + conflict.resolution.first = resolve_conflicts::ignore_drop; + pars.sym(); + pars.str(tmp); + conflict.resolution.second = file_path_internal(tmp); + } + else if (pars.symp (syms::resolved_respect_drop)) + { + conflict.resolution.first = resolve_conflicts::respect_drop; + pars.sym(); + } + else + N(false, F("%s is not a supported conflict resolution for %s") % pars.token % "content_drop"); + } + + 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_content_drop_conflicts + +static void parse_file_content_conflicts(basic_io::parser & pars, std::vector & conflicts, roster_t const & left_roster, @@ -1618,27 +1842,32 @@ parse_resolve_conflicts_str(basic_io::pa static void parse_resolve_conflicts_str(basic_io::parser & pars, roster_merge_result & result) { - char const * error_message = "can't specify a %s conflict resolution for more than one conflict"; + 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 applies only to %s conflicts"; + char const * error_message_3 = "conflict resolution %s is not appropriate for current conflicts"; while (pars.tok.in.lookahead != EOF) { - if (pars.symp (syms::resolved_suture)) + // resolution alphabetical order + if (pars.symp (syms::resolved_ignore_drop)) { - N(result.duplicate_name_conflicts.size() == 1, - F(error_message) % syms::resolved_suture); + pars.sym(); - duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); + N(result.content_drop_conflicts.size() > 0, + F(error_message_2) % syms::resolved_ignore_drop % syms::content_drop); - conflict.left_resolution.first = resolve_conflicts::suture; - conflict.right_resolution.first = resolve_conflicts::suture; - pars.sym(); - conflict.left_resolution.second = file_path_internal (pars.token); - pars.str(); + N(result.content_drop_conflicts.size() == 1, + F(error_message_1) % syms::resolved_ignore_drop); + + content_drop_conflict & conflict = *result.content_drop_conflicts.begin(); + string tmp; + pars.str(tmp); + conflict.resolution = make_pair(resolve_conflicts::ignore_drop, file_path_internal(tmp)); } else if (pars.symp (syms::resolved_rename_left)) { N(result.duplicate_name_conflicts.size() == 1, - F(error_message) % syms::resolved_rename_left); + F(error_message_1) % syms::resolved_rename_left); duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); @@ -1650,7 +1879,7 @@ parse_resolve_conflicts_str(basic_io::pa else if (pars.symp (syms::resolved_rename_right)) { N(result.duplicate_name_conflicts.size() == 1, - F(error_message) % syms::resolved_rename_right); + F(error_message_1) % syms::resolved_rename_right); duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); @@ -1659,6 +1888,61 @@ parse_resolve_conflicts_str(basic_io::pa conflict.right_resolution.second = file_path_internal (pars.token); pars.str(); } + else if (pars.symp (syms::resolved_respect_drop)) + { + pars.sym(); + + N(result.content_drop_conflicts.size() > 0, + F(error_message_2) % syms::resolved_respect_drop % syms::content_drop); + + N(result.content_drop_conflicts.size() == 1, + F(error_message_1) % syms::resolved_ignore_drop); + + content_drop_conflict & conflict = *result.content_drop_conflicts.begin(); + conflict.resolution = make_pair(resolve_conflicts::respect_drop, file_path()); + } + else if (pars.symp (syms::resolved_suture)) + { + if (result.duplicate_name_conflicts.size() == 1 && + result.content_drop_conflicts.size() == 1) + { + duplicate_name_conflict & dn_conflict = *result.duplicate_name_conflicts.begin(); + content_drop_conflict & cd_conflict = *result.content_drop_conflicts.begin(); + + dn_conflict.left_resolution.first = resolve_conflicts::suture; + dn_conflict.right_resolution.first = resolve_conflicts::suture; + pars.sym(); + dn_conflict.left_resolution.second = file_path_internal (pars.token); + dn_conflict.right_resolution.second = dn_conflict.left_resolution.second; + pars.str(); + + cd_conflict.resolution.first = resolve_conflicts::suture; + cd_conflict.resolution.second = dn_conflict.left_resolution.second; + } + else if (result.duplicate_name_conflicts.size() == 1 && + result.content_drop_conflicts.size() == 0) + { + duplicate_name_conflict & conflict = *result.duplicate_name_conflicts.begin(); + + conflict.left_resolution.first = resolve_conflicts::suture; + conflict.right_resolution.first = resolve_conflicts::suture; + pars.sym(); + conflict.left_resolution.second = file_path_internal (pars.token); + pars.str(); + } + else if (result.duplicate_name_conflicts.size() == 0 && + result.content_drop_conflicts.size() == 1) + { + content_drop_conflict & cd_conflict = *result.content_drop_conflicts.begin(); + + cd_conflict.resolution.first = resolve_conflicts::suture; + pars.sym(); + cd_conflict.resolution.second = file_path_internal (pars.token); + pars.str(); + } + else + N(false, F(error_message_3) % syms::resolved_suture); + } else N(false, F("%s is not a supported conflict resolution") % pars.token); @@ -1730,6 +2014,7 @@ parse_resolve_conflicts_opts (options co // These are the ones we know how to resolve. parse_duplicate_name_conflicts(pars, result.duplicate_name_conflicts, left_roster, right_roster); + parse_content_drop_conflicts(pars, result.content_drop_conflicts, left_roster, right_roster); parse_file_content_conflicts(pars, result.file_content_conflicts, left_roster, right_roster); if (src.lookahead != EOF) @@ -1849,8 +2134,7 @@ roster_merge_result::resolve_duplicate_n break; case resolve_conflicts::none: - // Just keep current name - this->roster.attach_node (left_nid, left_name); + N(false, F("no resolution provided for duplicate_name %s") % left_name); break; default: @@ -1883,6 +2167,98 @@ void } void +roster_merge_result::resolve_content_drop_conflicts(roster_t const & left_roster, + roster_t const & right_roster) +{ + MM(left_roster); + MM(right_roster); + MM(this->roster); // New roster + + // Conflict node is present but unattached in the new roster, with null + // file content id. The resolution is to fill in or delete the node. + + for (std::vector::const_iterator i = content_drop_conflicts.begin(); + i != content_drop_conflicts.end(); + ++i) + { + content_drop_conflict const & conflict = *i; + MM(conflict); + + file_path name; + node_t old_n; + + switch (conflict.parent_side) + { + case resolve_conflicts::left_side: + left_roster.get_name(conflict.nid, name); + old_n = left_roster.get_node (conflict.nid); + break; + + case resolve_conflicts::right_side: + right_roster.get_name(conflict.nid, name); + old_n = right_roster.get_node (conflict.nid); + break; + } + + switch (conflict.resolution.first) + { + case resolve_conflicts::none: + N(false, F("no resolution specified for conflict: content_drop %s") % name); + break; + + case resolve_conflicts::suture: + { + // Verify that conflict.nid was sutured in this merge + node_t new_n = roster.get_node(conflict.resolution.second); + + N((new_n->ancestors.first == conflict.nid || new_n->ancestors.second == conflict.nid), + F("%s was not sutured to %s in this merge") % name % conflict.resolution.second); + } + break; + + case resolve_conflicts::ignore_drop: + { + file_path dirname; + path_component basename; + + N(roster.has_node(conflict.nid), + F("%s was sutured in this merge; resolution must be 'resolved_suture'") % name); + + node_t new_n = roster.get_node(conflict.nid); + + P(F("ignoring drop of %s; new name %s") % name % conflict.resolution.second); + N(!roster.has_node(conflict.resolution.second), + F("%s already exists") % conflict.resolution.second); + + name.dirname_basename(dirname, basename); + + node_t dir_n = roster.get_node(dirname); + + N(dir_n != 0, F("%s directory does not exist") % dirname); + + // fill in node in result roster + new_n->attrs = old_n->attrs; + I(is_file_t(new_n)); + downcast_to_file_t(new_n)->content = downcast_to_file_t(old_n)->content; + roster.attach_node(conflict.nid, dir_n->self, basename); + } + break; + + case resolve_conflicts::respect_drop: + P(F("keeping drop of %s") % old_n->name); + roster.drop_detached_node(conflict.nid); + break; + + default: + I(false); + } + + } // end for + + content_drop_conflicts.clear(); +} + +void roster_merge_result::resolve_file_content_conflicts(lua_hooks & lua, roster_t const & left_roster, roster_t const & right_roster, @@ -1968,6 +2344,7 @@ roster_merge_result::clear() orphaned_node_conflicts.clear(); multiple_name_conflicts.clear(); duplicate_name_conflicts.clear(); + content_drop_conflicts.clear(); attribute_conflicts.clear(); file_content_conflicts.clear(); @@ -2069,7 +2446,8 @@ namespace set const & uncommon_ancestors, roster_t const & parent_roster, roster_t const & other_parent_roster, - roster_t & new_roster, + resolve_conflicts::side_t parent_side, + roster_merge_result & result, set & already_handled) { MM(markings); @@ -2098,7 +2476,7 @@ namespace { case marking_t::add: // case ii, iv; if ii, conflict will be discovered in next phase - create_node_for(n, new_roster); + create_node_for(n, result.roster); break; case marking_t::split: @@ -2120,19 +2498,19 @@ namespace // Set ancestors so mark-merge step will know how to check for // conflicts. We can't tell whether n is in left or right, so // we always put it in ancestors.first. - create_node_for(n, make_pair (n->self, birth_ancestors.first), new_roster); + create_node_for(n, make_pair (n->self, birth_ancestors.first), result.roster); already_handled.insert(birth_ancestors.first); } else if (right_anc_in_other) { I(!left_anc_in_other); - create_node_for(n, make_pair (n->self, birth_ancestors.second), new_roster); + create_node_for(n, make_pair (n->self, birth_ancestors.second), result.roster); already_handled.insert(birth_ancestors.second); } else { // both ancestors deleted in other parent - create_node_for(n, new_roster); + create_node_for(n, result.roster); } } break; @@ -2140,26 +2518,19 @@ namespace } else { - // In this branch we are NOT inserting the node into the new roster as it - // has been deleted from the other side of the merge. - // In this case, output a warning if there are changes to the file on the - // side of the merge where it still exists. + // case v + set const & content_marks = safe_get(markings, n->self).file_content; - bool found_one_ignored_content = false; for (set::const_iterator it = content_marks.begin(); it != content_marks.end(); it++) { if (uncommon_ancestors.find(*it) != uncommon_ancestors.end()) { - if (!found_one_ignored_content) - { - file_path fp; - parent_roster.get_name(n->self, fp); - W(F("Content changes to the file '%s'\n" - "will be ignored during this merge as the file has been\n" - "removed on one side of the merge. Affected revisions include:") % fp); - } - found_one_ignored_content = true; - W(F("Revision: %s") % encode_hexenc(it->inner()())); + result.content_drop_conflicts.push_back + (content_drop_conflict(n->self, + downcast_to_file_t(parent_roster.get_node(n->self))->content, + parent_side)); + create_node_for(n, result.roster); + break; } } } @@ -2181,11 +2552,9 @@ namespace return false; } - enum side_t { left_side, right_side }; - void assign_name(roster_merge_result & result, node_id nid, - node_id parent, path_component name, side_t side) + node_id parent, path_component name, resolve_conflicts::side_t side) { // this function is reponsible for detecting structural conflicts. by the // time we've gotten here, we have a node that's unambiguously decided on @@ -2211,11 +2580,11 @@ namespace // conflicted nodes detached. switch (side) { - case left_side: + case resolve_conflicts::left_side: c.left_nid = nid; c.right_nid = result.roster.root()->self; break; - case right_side: + case resolve_conflicts::right_side: c.left_nid = result.roster.root()->self; c.right_nid = nid; break; @@ -2259,11 +2628,11 @@ namespace // conflicted nodes detached. switch (side) { - case left_side: + case resolve_conflicts::left_side: c.left_nid = nid; c.right_nid = p->get_child(name)->self; break; - case right_side: + case resolve_conflicts::right_side: c.left_nid = p->get_child(name)->self; c.right_nid = nid; break; @@ -2289,7 +2658,7 @@ namespace void copy_node_forward(roster_merge_result & result, node_t const & n, - node_t const & old_n, side_t const & side) + node_t const & old_n, resolve_conflicts::side_t const & side) { I(n->self == old_n->self); n->attrs = old_n->attrs; @@ -2321,12 +2690,12 @@ namespace right_uncommon_ancestors, new_name, conflict)) { - side_t winning_side; + resolve_conflicts::side_t winning_side; if (new_name == left_name) - winning_side = left_side; + winning_side = resolve_conflicts::left_side; else if (new_name == right_name) - winning_side = right_side; + winning_side = resolve_conflicts::right_side; else I(false); @@ -2450,17 +2819,17 @@ roster_merge(roster_t const & left_paren I(false); case parallel::in_left: - // case ii, iii, iva, or va + // case ii, iii, iva, va, vc insert_if_unborn_or_sutured(i.left_data(), left_markings, left_uncommon_ancestors, left_parent, - right_parent, result.roster, already_handled); + right_parent, resolve_conflicts::left_side, result, already_handled); break; case parallel::in_right: - // case ii, iii, ivb, or vb + // case ii, iii, ivb, vb, vd insert_if_unborn_or_sutured(i.right_data(), right_markings, right_uncommon_ancestors, right_parent, - left_parent, result.roster, already_handled); + left_parent, resolve_conflicts::right_side, result, already_handled); break; case parallel::in_both: @@ -2521,9 +2890,9 @@ roster_merge(roster_t const & left_paren // Not sutured. // // Attach this node from the left roster. This may cause - // a name collision with the previously attached node from + // a name collision with a previously attached node from // the other side of the merge. - copy_node_forward(result, new_i->second, left_n, left_side); + copy_node_forward(result, new_i->second, left_n, resolve_conflicts::left_side); ++new_i; } } @@ -2567,9 +2936,9 @@ roster_merge(roster_t const & left_paren // Not sutured. // // Attach this node from the right roster. This may - // cause a name collision with the previously attached + // cause a name collision with a previously attached // node from the other side of the merge. - copy_node_forward(result, new_i->second, right_n, right_side); + copy_node_forward(result, new_i->second, right_n, resolve_conflicts::right_side); ++new_i; } } ============================================================ --- roster_merge.hh 7e40959f06f4dc24ebec108a2a9f2cdfe1e6eb4c +++ roster_merge.hh 4617dc0cfec57a20195f0655214acfcc1acad33d @@ -37,7 +37,9 @@ namespace resolve_conflicts namespace resolve_conflicts { - enum resolution_t {none, content_user, content_internal, rename, suture}; + enum resolution_t {none, content_user, content_internal, ignore_drop, rename, respect_drop, suture}; + + enum side_t {left_side, right_side}; } // renaming the root dir allows these: @@ -105,6 +107,25 @@ struct duplicate_name_conflict right_resolution.first = resolve_conflicts::none;}; }; +// files with content_drop conflicts are left attached in result roster +// (unless unattached for another reason), with parent content hash. +struct content_drop_conflict +{ + node_id nid; + file_id fid; + resolve_conflicts::side_t parent_side; // node is in parent_side roster, not in other roster + + // resolution is one of none, ignore_drop, respect_drop. If ignore_drop, + // provide new name to allow avoiding name conflicts. + std::pair resolution; + + content_drop_conflict () : + nid(the_null_node), parent_side(resolve_conflicts::left_side) {resolution.first = resolve_conflicts::none;}; + + content_drop_conflict(node_id nid, file_id fid, resolve_conflicts::side_t parent_side) : + nid(nid), fid(fid), parent_side(parent_side){resolution.first = resolve_conflicts::none;}; +}; + // nodes with attribute conflicts are left attached in the resulting tree (unless // detached for some other reason), but with the given attribute left out of // their full_attr_map_t. Note that this doesn't actually leave the resulting @@ -123,7 +144,7 @@ struct file_content_conflict // detached for some other reason), but with a null content hash. struct file_content_conflict { - node_id left_nid, right_nid, result_nid; + node_id left_nid, right_nid, result_nid; // node ids can be different due to suture file_id left, right; std::pair resolution; @@ -152,6 +173,7 @@ template <> void dump(file_content_confl template <> void dump(attribute_conflict const & conflict, std::string & out); template <> void dump(file_content_conflict const & conflict, std::string & out); +template <> void dump(content_drop_conflict const & conflict, std::string & out); struct roster_merge_result { @@ -163,6 +185,7 @@ struct roster_merge_result // - orphaned node conflicts // - multiple name conflicts // - directory loop conflicts + // - content_drop conflicts // - attribute conflicts // - file content conflicts @@ -173,6 +196,7 @@ struct roster_merge_result std::vector orphaned_node_conflicts; std::vector multiple_name_conflicts; std::vector duplicate_name_conflicts; + std::vector content_drop_conflicts; std::vector attribute_conflicts; std::vector file_content_conflicts; @@ -222,6 +246,13 @@ struct roster_merge_result roster_t const & right_roster, content_merge_adaptor & adaptor); + void report_content_drop_conflicts(roster_t const & left_roster, + roster_t const & right_roster, + bool const basic_io, + std::ostream & output) const; + void resolve_content_drop_conflicts(roster_t const & left_roster, + roster_t const & right_roster); + void report_attribute_conflicts(roster_t const & left, roster_t const & right, content_merge_adaptor & adaptor, ============================================================ --- ss-existence-merge.text 84169b7caf7da082a6393fc1ecd4c83a16f154da +++ ss-existence-merge.text 26afb7b1f0e0ad519cf3873c319394fb9cddc9bd @@ -1,16 +1,23 @@ doc current die-die-die-merge for existe doc current die-die-die-merge for existence http://revctrl.org/DieDieDieMerge implemented in roster_merge.cc roster_merge -In a given revision, a node either exists or does not exist. +In a given revision, a node either exists or does not exist. In +addition, a scalar for the node may have different values in different +revisions. Notation: - exists A1 - does not exist A + A1a exists, with node id '1', scalar value 'a' + A does not exist -In a child revision with two parents, there are several cases: +In a child revision with two parents, there are nominally eight cases +depending on where the node exists. We group them into five cases; the +scalar value matters only in the case where the node is dropped on one +side, since changing the scalar value expresses a user intent that the +node exist, which conflicts with the user intent expressed by the +drop. A1 B1 i) \ / @@ -66,21 +73,36 @@ vb) \ / The node was deleted in A's uncommon ancestors + D1a E1b + | | +vc) A B1b + \ / + C? -In monotone 0.4 and earlier, cases ii and iii did not exist. + The node was deleted in A's uncommon ancestors, and modified in B's. + D1b E1a + | | +vd) A1b B + \ / + C? + + The node was deleted in B's uncommon ancestors, and modified in A's. + + +In monotone 0.4 and earlier, cases ii, iii, vc and vd did not exist. + Case ii can only happen if we support sutures as user operations; not yet. -In case v, deletion wins over any other change; it might be better to -have deletion conflict with any other change. But that's left for -another time. +In cases vc and vd, two users have expressed different intent, so we +declare a conflict. To process all nodes, we loop thru the union of the parent node ids, deciding which case each node belongs in. We create nodes in the -result roster for cases i thru iv; we don't create a node for case v. -The mark-merge step only considers nodes that are in the result -roster. +result roster for cases i thru iv and vc, vd; we don't create a node +for cases va, vb. The mark-merge step only considers nodes that are in +the result roster. To distinguish cases iii and iv, we need to store information about how a node was born. This can be done in the marking map, by adding @@ -89,10 +111,14 @@ ancestor. indicate the ancestors. If split, the first node_id indicates the ancestor. -To distinguish case iiia from va, we search B_uncommon for a node that -is a suture or split and has 1 as an ancestor. Similarly for case iiib -and vb. +To distinguish case iiia from va and vc, we search B_uncommon for a +node that is a suture or split and has 1 as an ancestor. If found, it +is case iii. Similarly for iiib and vb; search A_uncommon. +To distinguish case va from vc, we search B_uncommon for the scalar +marks; if found, it is case vc. Similarly for vb, vd; search +A_uncommon. + case iii will show up twice, once for node id 1 and once for node id 3. We want to create only one node in the result roster, with node id 3 (the sutured node id). Since we handle both nodes when we encounter ============================================================ --- testlib.lua 01d860a7d8781f38ea776b60e3b4fd875e2766aa +++ testlib.lua 36e8a3f33665718abff5bdab3f309879d48f8974 @@ -395,7 +395,7 @@ function runcmd(cmd, prefix, bgnd) else prepare_redirect(prefix.."stdin", prefix.."stdout", prefix.."stderr") end - + local result if cmd.logline ~= nil then L(locheader(), cmd.logline, "\n") @@ -417,7 +417,7 @@ function runcmd(cmd, prefix, bgnd) "(first entry is a " .. type(cmd[1]) ..")") end execute = oldexec - + if local_redir then files.stdin:close() files.stdout:close() @@ -427,8 +427,8 @@ function samefile(left, right) end function samefile(left, right) - if left == "-" or right == "-" then - err("tests may not rely on standard input") + if left == "-" or right == "-" then + err("tests may not rely on standard input") end if fsize(left) ~= fsize(right) then return false @@ -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 @@ -683,7 +687,7 @@ function bg(torun, ret, stdout, stderr, mt.__index = mt mt.finish = function(obj, timeout) if obj.retval ~= nil then return end - + if timeout == nil then timeout = 0 end if type(timeout) ~= "number" then err("Bad timeout of type "..type(timeout)) @@ -701,7 +705,7 @@ function bg(torun, ret, stdout, stderr, obj.retval, res = timed_wait(obj.pid, 2) end end - + test.bglist[obj.id] = nil L(locheader(), "checking background command from ", out.locstr, cmd_as_str(out.cmd), "\n") @@ -932,7 +936,7 @@ function run_tests(debugging, list_only, local s = prepare_to_run_tests(P) if s ~= 0 then P("Test suite preparation failed.\n") - return s + return s end P("Running tests...\n") @@ -1108,7 +1112,7 @@ function run_one_test(tname) end end test.log:close() - + -- record the short status where report_one_test can find it local s = io.open(test.root .. "/STATUS", "w") if r then