TEST_F(ThreeWayMergeTest, CascadingParentsCauseNoCascadingKeys) { Key root("/", KEY_END); MergeResult result = merger.mergeKeySet(MergeTask(BaseMergeKeys(base, Key("/parentb", KEY_END)), OurMergeKeys(ours, Key("/parento", KEY_END)), TheirMergeKeys (theirs, Key("/parentt", KEY_END)), root)); EXPECT_FALSE(result.hasConflicts()) << "Invalid conflict detected"; Key current; KeySet merged = result.getMergedKeys (); merged.rewind(); while ((current = merged.next ())) { EXPECT_FALSE(current.getNamespace() == "/"); } }
TEST_F(ThreeWayMergeTest, EqualKeySetsWontCauseSync) { unsyncKeys(ours); unsyncKeys(theirs); unsyncKeys(base); MergeResult result = merger.mergeKeySet (base, ours, theirs, ourParent); EXPECT_FALSE(result.hasConflicts()) << "Invalid conflict detected"; KeySet merged = result.getMergedKeys(); Key current; merged.rewind(); while ((current = merged.next ())) { EXPECT_FALSE(current.needSync()); } }
TEST_F(ThreeWayMergeTest, SameAddedDifferentValueConflict) { ours.append (Key ("user/parento/config/key5", KEY_VALUE, "newvalueours", KEY_END)); theirs.append (Key ("user/parentt/config/key5", KEY_VALUE, "newvaluetheirs", KEY_END)); MergeResult result = merger.mergeKeySet (base, ours, theirs, mergeParent); ASSERT_TRUE (result.hasConflicts())<< "No conflict detected although conflicts should exist"; KeySet conflicts = result.getConflictSet (); EXPECT_EQ(1, conflicts.size ()); testConflictMeta (conflicts.at (0), CONFLICT_ADD, CONFLICT_ADD); KeySet merged = result.getMergedKeys (); EXPECT_EQ(5, merged.size ()); compareAllKeys (merged); }
int MergingKDB::synchronize (KeySet & returned, Key & parentKey, ThreeWayMerge & merger) { try { // write our config int ret = KDB::set (returned, parentKey); // update our config (if no conflict) KDB::get (returned, parentKey); return ret; } catch (KDBException const &) { // a conflict occurred, see if we can solve it with the merger // refresh the key database KeySet theirs = returned.dup (); KDB::get (theirs, parentKey); // try to merge MergeResult result = merger.mergeKeySet (MergeTask (BaseMergeKeys (base, parentKey), OurMergeKeys (returned, parentKey), TheirMergeKeys (theirs, parentKey), parentKey)); if (!result.hasConflicts ()) { // hurray, we solved the issue KeySet resultKeys = result.getMergedKeys (); int ret = KDB::set (resultKeys, parentKey); base = resultKeys; return ret; } else { // nothing we can do anymore KeySet conflictSet = result.getConflictSet (); throw MergingKDBException (parentKey, conflictSet); } } }
void OneSideValueStrategy::resolveConflict(const MergeTask& task, Key& conflictKey, MergeResult& result) { ConflictOperation ourOperation = getOurConflictOperation (conflictKey); ConflictOperation theirOperation = getTheirConflictOperation (conflictKey); string ourLookup = rebasePath (conflictKey, task.mergeRoot, task.ourParent); string theirLookup = rebasePath (conflictKey, task.mergeRoot, task.theirParent); // TODO: this is a subset of the onesidestrategy // the onesidestrategy could be split up into several smaller strategies if ((ourOperation == CONFLICT_SAME && theirOperation == CONFLICT_MODIFY) || (ourOperation == CONFLICT_MODIFY && theirOperation == CONFLICT_SAME)) { string lookupPath; Key winningKey; switch (winningSide) { case BASE: lookupPath = rebasePath (conflictKey, task.mergeRoot, task.baseParent); winningKey = task.base.lookup (lookupPath); break; case OURS: lookupPath = rebasePath (conflictKey, task.mergeRoot, task.ourParent); winningKey = task.ours.lookup (lookupPath); break; case THEIRS: lookupPath = rebasePath (conflictKey, task.mergeRoot, task.theirParent); winningKey = task.theirs.lookup (lookupPath); break; } if (winningKey) { conflictKey.setString (winningKey.getString ()); result.resolveConflict (conflictKey); result.addMergeKey (conflictKey); } } }
TEST (MergeResult, CountsEqualKeysCorrectly) { Key mergedKey1 = Key ("user/test/config/key1", KEY_END); Key mergedKey2 = Key ("user/test/config/key2", KEY_END); Key mergedKey3 = Key ("user/test/config/key3", KEY_END); Key conflictKey1 = Key ("user/test/config/key4", KEY_END); KeySet conflicts; conflicts.append (conflictKey1); KeySet merged; merged.append (mergedKey1); merged.append (mergedKey2); MergeResult result (conflicts, merged); EXPECT_EQ (2, result.getNumberOfEqualKeys ()) << "Initially merged keys not counted"; result.resolveConflict (conflictKey1); result.addMergeKey (conflictKey1); EXPECT_EQ (2, result.getNumberOfEqualKeys ()) << "Resolved key is counted as equal key"; result.addMergeKey (mergedKey3); EXPECT_EQ (3, result.getNumberOfEqualKeys ()) << "Merged key is not counted as equal key"; }
void AutoMergeStrategy::resolveConflict(const MergeTask& task, Key& conflictKey, MergeResult& result) { ConflictOperation ourOperation = getOurConflictOperation(conflictKey); ConflictOperation theirOperation = getTheirConflictOperation(conflictKey); string ourLookup = rebasePath (conflictKey, task.mergeRoot, task.ourParent); string theirLookup = rebasePath (conflictKey, task.mergeRoot, task.theirParent); switch (ourOperation) { case SAME: if (theirOperation == MODIFY || theirOperation == ADD) { Key source = task.theirs.lookup(theirLookup); conflictKey.setString(source.getString()); result.resolveConflict(conflictKey); result.addMergeKey(conflictKey); } if (theirOperation == DELETE) { result.resolveConflict(conflictKey); } break; case MODIFY: case ADD: if (theirOperation == SAME) { Key source = task.ours.lookup(ourLookup); conflictKey.setString(source.getString()); result.resolveConflict(conflictKey); result.addMergeKey(conflictKey); } break; case DELETE: if (theirOperation == SAME) { result.resolveConflict(conflictKey); } break; case META: break; } }
void ThreeWayMerge::detectConflicts (const MergeTask & task, MergeResult & mergeResult, bool reverseConflictMeta = false) { Key our; cursor_t savedCursor = task.ours.getCursor (); task.ours.rewind (); while ((our = task.ours.next ())) { string theirLookup = rebasePath (our, task.ourParent, task.theirParent); Key theirLookupResult = task.theirs.lookup (theirLookup); // we have to copy it to obtain owner etc... Key mergeKey = rebaseKey (our, task.ourParent, task.mergeRoot); if (keyDataEqual (our, theirLookupResult)) { // keydata matches, see if metakeys match if (keyMetaEqual (our, theirLookupResult)) { if (task.ourParent.getFullName () == task.mergeRoot.getFullName ()) { // the key was not rebased, we can reuse our (prevents that the key is rewritten) mergeResult.addMergeKey (our); } else { // the key causes no merge conflict, but the merge result is below a new parent mergeResult.addMergeKey (mergeKey); } } else { // metakeys are different mergeResult.addConflict (mergeKey, CONFLICT_META, CONFLICT_META); } } else { string baseLookup = rebasePath (our, task.ourParent, task.baseParent); Key baseLookupResult = task.base.lookup (baseLookup); // check if the keys was newly added in ours if (baseLookupResult) { // the key exists in base, check if the key still exists in theirs if (theirLookupResult) { // check if only they modified it if (!keyDataEqual (our, baseLookupResult) && keyDataEqual (theirLookupResult, baseLookupResult)) { // the key was only modified in ours addAsymmetricConflict (mergeResult, mergeKey, CONFLICT_MODIFY, CONFLICT_SAME, reverseConflictMeta); } else { // check if both modified it if (!keyDataEqual (our, baseLookupResult) && !keyDataEqual (theirLookupResult, baseLookupResult)) { // the key was modified on both sides mergeResult.addConflict (mergeKey, CONFLICT_MODIFY, CONFLICT_MODIFY); } } } else { // the key does not exist in theirs anymore, check if ours has modified it if (keyDataEqual (our, baseLookupResult)) { // the key was deleted in theirs, and not modified in ours addAsymmetricConflict (mergeResult, mergeKey, CONFLICT_SAME, CONFLICT_DELETE, reverseConflictMeta); } else { // the key was deleted in theirs, but modified in ours addAsymmetricConflict (mergeResult, mergeKey, CONFLICT_MODIFY, CONFLICT_DELETE, reverseConflictMeta); } } } else { // the key does not exist in base, check if the key was added in theirs if (theirLookupResult) { // check if the key was added with the same value in theirs if (keyDataEqual (mergeKey, theirLookupResult)) { if (keyMetaEqual (our, theirLookupResult)) { // the key was added on both sides with the same value if (task.ourParent.getFullName () == task.mergeRoot.getFullName ()) { // the key was not rebased, we can reuse our and prevent the sync flag being // set mergeResult.addMergeKey (our); } else { // the key causes no merge conflict, but the merge result is below a new // parent mergeResult.addMergeKey (mergeKey); } } else { // metakeys are different mergeResult.addConflict (mergeKey, CONFLICT_META, CONFLICT_META); } } else { // the key was added on both sides with different values mergeResult.addConflict (mergeKey, CONFLICT_ADD, CONFLICT_ADD); } } else { // the key was only added to ours addAsymmetricConflict (mergeResult, mergeKey, CONFLICT_ADD, CONFLICT_SAME, reverseConflictMeta); } } } } task.ours.setCursor (savedCursor); }
void ThreeWayMerge::detectConflicts(const MergeTask& task, MergeResult& mergeResult, bool reverseConflictMeta = false) { Key our; cursor_t savedCursor = task.ours.getCursor (); task.ours.rewind (); while ((our = task.ours.next ())) { if (our.getName() == task.ourParent.getName()) continue; string theirLookup = rebasePath (our, task.ourParent, task.theirParent); Key theirLookupResult = task.theirs.lookup (theirLookup); // we have to copy it to obtain owner etc... Key mergeKey = rebaseKey (our, task.ourParent, task.mergeRoot); if (keyDataEqual (our, theirLookupResult)) { // keydata matches, see if metakeys match if (keyMetaEqual (our, theirLookupResult)) { mergeResult.addMergeKey (mergeKey); } else { // metakeys are different mergeResult.addConflict (mergeKey, META, META); } } else { string baseLookup = rebasePath (our, task.ourParent, task.baseParent); Key baseLookupResult = task.base.lookup (baseLookup); // check if the keys was newly added in ours if (baseLookupResult) { // the key exists in base, check if the key still exists in theirs if (theirLookupResult) { // check if only they modified it if (!keyDataEqual (our, baseLookupResult) && keyDataEqual (theirLookupResult, baseLookupResult)) { // the key was only modified in ours addAsymmetricConflict (mergeResult, mergeKey, MODIFY, SAME, reverseConflictMeta); } else { // check if both modified it if (!keyDataEqual (our, baseLookupResult) && !keyDataEqual (theirLookupResult, baseLookupResult)) { // the key was modified on both sides mergeResult.addConflict (mergeKey, MODIFY, MODIFY); } } } else { // the key does not exist in theirs anymore, check if ours has modified it if (keyDataEqual (our, baseLookupResult)) { // the key was deleted in theirs, and not modified in ours addAsymmetricConflict (mergeResult, mergeKey, SAME, DELETE, reverseConflictMeta); } else { // the key was deleted in theirs, but modified in ours addAsymmetricConflict (mergeResult, mergeKey, MODIFY, DELETE, reverseConflictMeta); } } } else { // the key does not exist in base, check if the key was added in theirs if (theirLookupResult) { // check if the key was added with the same value in theirs if (keyDataEqual (mergeKey, theirLookupResult)) { if (keyMetaEqual (our, theirLookupResult)) { // the key was added on both sides with the same value mergeResult.addMergeKey (mergeKey); } else { // metakeys are different mergeResult.addConflict (mergeKey, META, META); } } else { // the key was added on both sides with different values mergeResult.addConflict (mergeKey, ADD, ADD); } } else { // the key was only added to ours addAsymmetricConflict (mergeResult, mergeKey, ADD, SAME, reverseConflictMeta); } } } } task.ours.setCursor (savedCursor); }
int ImportCommand::execute (Cmdline const & cl) { size_t argc = cl.arguments.size (); if (argc != 1 && argc != 2 && argc != 3) { throw invalid_argument ("need 1 to 3 arguments"); } Key root = cl.createKey (0); if (!root.isValid ()) { throw invalid_argument ("root key \"" + cl.arguments[0] + "\" is not a valid key name"); } KeySet originalKeys; kdb.get (originalKeys, root); KeySet base = originalKeys.cut (root); printWarnings (cerr, root); string format = cl.format; if (argc > 1) format = cl.arguments[1]; string file = "/dev/stdin"; if (argc > 2 && cl.arguments[2] != "-") file = cl.arguments[2]; Modules modules; PluginPtr plugin = modules.load (format, cl.getPluginsConfig ()); Key errorKey (root); errorKey.setString (file); KeySet importedKeys; plugin->get (importedKeys, errorKey); importedKeys = importedKeys.cut (root); printWarnings (cerr, errorKey); printError (cerr, errorKey); ThreeWayMerge merger; MergeHelper helper; helper.configureMerger (cl, merger); MergeResult result = merger.mergeKeySet ( MergeTask (BaseMergeKeys (base, root), OurMergeKeys (base, root), TheirMergeKeys (importedKeys, root), root)); helper.reportResult (cl, result, cout, cerr); int ret = -1; if (!result.hasConflicts ()) { if (cl.verbose) { cout << "The merged keyset with strategy " << cl.strategy << " is:" << endl; cout << result.getMergedKeys (); } KeySet resultKeys = result.getMergedKeys (); originalKeys.append (resultKeys); kdb.set (originalKeys, root); ret = 0; } return ret; }
int main() { KeySet ours; KeySet theirs; KeySet base; // the root of the subtree containing our keys (i.e. our side of the merge) Key oursRoot ("user/ours", KEY_END); // the root of the subtree containing their keys (i.e. their side of the merge) Key theirsRoot ("user/theirs", KEY_END); // the root of the subtree containing the base keys (i.e. the common ancestor of the merge) Key baseRoot ("user/base", KEY_END); // the root of the subtree that will contain the merge result Key resultRoot ("user/result", KEY_END); // Step 1: retrieve clean KeySets containing only those // keys that should be merged. This is a bit trickier than // it seems at first. Have a look at the documentation of kdbGet // for detailed information // things to note: // * use blocks with local KDB instances so we don't have to worry about // writing the keys back // * remove the root key itself from the result KeySet because it usually // contains the mounted filename and cannot be merged anyway // Also have a look at the documentation of kdbSet() // (http://doc.libelektra.org/api/latest/html/group__kdb.html#ga11436b058408f83d303ca5e996832bcf). // The merging framework can also be used to resolve conflicts resulting from // concurrent calls to kdbSet() as described in the example of kdbSet(). { KDB lkdb; lkdb.get (ours, oursRoot); ours = ours.cut (oursRoot); ours.lookup(oursRoot, KDB_O_POP); lkdb.get (theirs, theirsRoot); theirs = theirs.cut (theirsRoot); theirs.lookup(theirsRoot, KDB_O_POP); lkdb.get (base, baseRoot); base = base.cut (baseRoot); base.lookup(baseRoot, KDB_O_POP); } // Step 2: Make sure that no keys reside below the intended merge result root // Usually the merge can be either aborted if this is the case or the existing // keys can be overwritten. KeySet resultKeys; kdb::KDB kdb; kdb.get (resultKeys, resultRoot); KeySet discard = resultKeys.cut (resultRoot); if (discard.size () != 0) { // handle existing keys below the result root return -1; } ThreeWayMerge merger; // Step 3: Decide which resolution strategies to use. The strategies are registered // with the merge and applied in order as soon as a conflict is detected. If a strategy // marks a conflict as resolved, no further strategies are consulted. Therefore the order // in which they are registered is absolutely crucial. With this chaining the strategies // remain simple, but can be combined to powerful resolution strategies. // Have a look at the strategy documentation for further details on what they do and how they work. // The unit tests also provide insight into how the strategies work. // In order to simplify the initialization, predefined merge configurations exist. // in this example we first resolve all the keys that can be automatically // resolved (e.g. only one side was modified). This is exactly the use case of the // AutoMergeConfiguration. AutoMergeConfiguration configuration; configuration.configureMerger(merger); // Step 4: Perform the actual merge MergeResult result = merger.mergeKeySet ( MergeTask (BaseMergeKeys (base, baseRoot), OurMergeKeys (ours, oursRoot), TheirMergeKeys (theirs, theirsRoot), resultRoot)); // Step 5: work with the result. The merger will return an object containing information // about the merge result. if (!result.hasConflicts ()) { // output some statistical information cout << result.getMergedKeys().size() << " keys in the result" << endl; cout << result.getNumberOfEqualKeys() << " keys were equal" << endl; cout << result.getNumberOfResolvedKeys() << " keys were resolved" << endl; // write the result resultKeys.append(result.getMergedKeys()); kdb.set (resultKeys, resultRoot); return 0; } else { KeySet conflicts = result.getConflictSet(); cerr << conflicts.size() << " conflicts were detected that could not be resolved automatically:" << endl; conflicts.rewind(); Key current; while ((current = conflicts.next())) { // For each unresolved conflict there is a conflict key in the merge result. // This conflict key contains meta information about the reason of the conflict. // In particular the metakeys conflict/operation/our and conflict/operation/their contain // the operations done on our version of the key and their version of the key relative to // the base version of the key. string ourConflict = current.getMeta<string> ("conflict/operation/our"); string theirConflict = current.getMeta<string> ("conflict/operation/their"); cerr << current << endl; cerr << "ours: " << ourConflict << ", theirs: " << theirConflict << endl; cerr << endl; } cerr << "Merge unsuccessful." << endl; return -1; } }
int EditorCommand::execute(Cmdline const& cl) { #ifdef _WIN32 throw EditorNotAvailable(); #endif int argc = cl.arguments.size (); if (argc < 1) { throw invalid_argument ("wrong number of arguments, 1 needed"); } Key root = cl.createKey(0); KeySet ours; KDB kdb; kdb.get (ours, root); KeySet oursToEdit = ours.cut (root); // export it to file string format = cl.format; if (argc > 1) format = cl.arguments[1]; Modules modules; PluginPtr plugin = modules.load(format); tmpFile(); if (cl.verbose) std::cout << "filename set to " << filename << std::endl; Key errorKey(root); errorKey.setString(filename); if (plugin->set(oursToEdit, errorKey) == -1) { printWarnings(cerr, errorKey); printError(cerr, errorKey); return 11; } printWarnings(cerr, errorKey); // start editor if (cl.verbose) std::cout << "running editor with " << filename << std::endl; if (!cl.editor.empty()) { if (!runEditor (cl.editor, filename)) { std::cerr << "Could not run editor " << cl.editor << std::endl; return 12; } } else { if (!runAllEditors(filename)) { std::cerr << "Could not run any editor, please change /sw/elektra/kdb/#0/current/editor" << std::endl; return 12; } } // import from the file KeySet importedKeys; plugin->get(importedKeys, errorKey); importedKeys = importedKeys.cut(root); printWarnings (cerr, errorKey); printError (cerr, errorKey); ThreeWayMerge merger; MergeHelper helper; helper.configureMerger (cl, merger); MergeResult result = merger.mergeKeySet ( MergeTask (BaseMergeKeys (oursToEdit, root), OurMergeKeys (oursToEdit, root), TheirMergeKeys (importedKeys, root), root)); helper.reportResult (cl, result, cout, cerr); int ret = 13; if (!result.hasConflicts ()) { if (cl.verbose) { cout << "The merged keyset with strategy " << cl.strategy << " is:" << endl; cout << result.getMergedKeys(); } KeySet resultKeys = result.getMergedKeys(); if (cl.verbose) std::cout << "about to write result keys " << resultKeys << std::endl; ours.append(resultKeys); kdb.set (ours, root); if (cl.verbose) std::cout << "successful, cleaning up " << filename << std::endl; unlink(filename.c_str()); ret = 0; } else { std::cout << "Import not successful, please import and remove \"" << filename << '"' << std::endl; } return ret; }
int MergeCommand::execute(Cmdline const& cl) { if (cl.arguments.size () < 4) { throw invalid_argument ("wrong number of arguments, 4 needed"); } Key oursRoot = cl.createKey(0); Key theirsRoot = cl.createKey(1); Key baseRoot = cl.createKey(2); Key resultRoot = cl.createKey(3); KeySet ours; KeySet theirs; KeySet base; { KDB lkdb; lkdb.get (ours, oursRoot); ours = ours.cut (oursRoot); ours.lookup(oursRoot, KDB_O_POP); if (cl.verbose) std::cout << "we got ours: " << oursRoot << " with keys " << ours << std::endl; } { KDB lkdb; lkdb.get (theirs, theirsRoot); theirs = theirs.cut (theirsRoot); ours.lookup(oursRoot, KDB_O_POP); if (cl.verbose) std::cout << "we got theirs: " << theirsRoot << " with keys " << theirs << std::endl; } { KDB lkdb; lkdb.get (base, baseRoot); base = base.cut (baseRoot); ours.lookup(oursRoot, KDB_O_POP); if (cl.verbose) std::cout << "we got base: " << baseRoot << " with keys " << base << std::endl; } KeySet resultKeys; kdb.get (resultKeys, resultRoot); KeySet discard = resultKeys.cut (resultRoot); if (discard.size () != 0) { if (cl.force) { if (cl.verbose) { std::cout << "will remove " << discard.size () << " keys, because -f was given" << std::endl; } } else { std::cerr << discard.size () << " keys exist in merge resultroot, will quit. Use -f to override the keys there." << std::endl; } } MergeHelper helper; ThreeWayMerge merger; helper.configureMerger (cl, merger); MergeResult result = merger.mergeKeySet ( MergeTask (BaseMergeKeys (base, baseRoot), OurMergeKeys (ours, oursRoot), TheirMergeKeys (theirs, theirsRoot), resultRoot)); helper.reportResult (cl, result, cout, cerr); int ret = 0; if (!result.hasConflicts ()) { resultKeys.append(result.getMergedKeys()); kdb.set (resultKeys, resultRoot); } else { ret = -1; } return ret; }