diff --git a/src/TDB.cpp b/src/TDB.cpp index feeec44ad..d4a4aacd4 100644 --- a/src/TDB.cpp +++ b/src/TDB.cpp @@ -44,74 +44,8 @@ #include #include -#define NDEBUG -#include -#include - -#define DEBUG_OUTPUT 0 - -#if DEBUG_OUTPUT > 0 - #define DEBUG_STR(str) std::cout << "DEBUG: " << str << "\n"; std::cout.flush() - #define DEBUG_STR_PART(str) std::cout << "DEBUG: " << str; std::cout.flush() - #define DEBUG_STR_END(str) std::cout << str << "\n"; std::cout.flush() -#else - #define DEBUG_STR(str) - #define DEBUG_STR_PART(str) - #define DEBUG_STR_END(str) -#endif - extern Context context; -//////////////////////////////////////////////////////////////////////////////// -// Helper function for TDB::merge -void readTaskmods (std::vector &input, - std::vector ::iterator &start, - std::list &list) -{ - std::string line; - Taskmod tmod_tmp; - - DEBUG_STR ("reading taskmods from file: "); - - for ( ; start != input.end (); ++start) - { - line = *start; - - if (line.substr (0, 4) == "time") - { - std::stringstream stream (line.substr (5)); - long ts; - stream >> ts; - - if (stream.fail ()) - throw std::string ("There was a problem reading the timestamp from the undo.data file."); - - // 'time' is the first line of a modification - // thus we will (re)set the taskmod object - tmod_tmp.reset (ts); - - } - else if (line.substr (0, 3) == "old") - { - tmod_tmp.setBefore (Task (line.substr (4))); - - } - else if (line.substr (0, 3) == "new") - { - tmod_tmp.setAfter (Task (line.substr (4))); - - // 'new' is the last line of a modification, - // thus we can push to the list - list.push_back (tmod_tmp); - - assert (tmod_tmp.isValid ()); - DEBUG_STR (" taskmod complete"); - } - } - - DEBUG_STR ("DONE"); -} - //////////////////////////////////////////////////////////////////////////////// // The ctor/dtor do nothing. // The lock/unlock methods hold the file open. @@ -242,20 +176,6 @@ int TDB::load (std::vector & tasks) // and no other 'status' filters, then loadCompleted can be skipped. int numberStatusClauses = 0; int numberSimpleStatusClauses = 0; -/* - foreach (att, filter) - { - if (att->name () == "status") - { - ++numberStatusClauses; - - if (att->mod () == "" && - (att->value () == "pending" || - att->value () == "waiting")) - ++numberSimpleStatusClauses; - } - } -*/ loadPending (tasks); @@ -675,885 +595,11 @@ int TDB::nextId () //////////////////////////////////////////////////////////////////////////////// void TDB::undo () { - Directory location (context.config.get ("data.location")); - - std::string undoFile = location._data + "/undo.data"; - std::string pendingFile = location._data + "/pending.data"; - std::string completedFile = location._data + "/completed.data"; - - // load undo.data - std::vector u; - File::read (undoFile, u); - - if (u.size () < 3) - throw std::string ("There are no recorded transactions to undo."); - - // pop last tx - u.pop_back (); // separator. - - std::string current = u.back ().substr (4); - u.pop_back (); - - std::string prior; - std::string when; - if (u.back ().substr (0, 5) == "time ") - { - when = u.back ().substr (5); - u.pop_back (); - prior = ""; - } - else - { - prior = u.back ().substr (4); - u.pop_back (); - when = u.back ().substr (5); - u.pop_back (); - } - - Date lastChange (atoi (when.c_str ())); - - // Set the colors. - Color color_red (context.color () ? context.config.get ("color.undo.before") : ""); - Color color_green (context.color () ? context.config.get ("color.undo.after") : ""); - - if (context.config.get ("undo.style") == "side") - { - std::cout << "\n" - << "The last modification was made " - << lastChange.toString () - << "\n"; - - // Attributes are all there is, so figure the different attribute names - // between before and after. - ViewText view; - view.width (context.getWidth ()); - view.intraPadding (2); - view.add (Column::factory ("string", "")); - view.add (Column::factory ("string", "Prior Values")); - view.add (Column::factory ("string", "Current Values")); - - Task after (current); - - if (prior != "") - { - Task before (prior); - - std::vector beforeAtts; - foreach (att, before) - beforeAtts.push_back (att->first); - - std::vector afterAtts; - foreach (att, after) - afterAtts.push_back (att->first); - - std::vector beforeOnly; - std::vector afterOnly; - listDiff (beforeAtts, afterAtts, beforeOnly, afterOnly); - - int row; - foreach (name, beforeOnly) - { - row = view.addRow (); - view.set (row, 0, *name); - view.set (row, 1, renderAttribute (*name, before.get (*name)), color_red); - } - - foreach (name, before) - { - std::string priorValue = before.get (name->first); - std::string currentValue = after.get (name->first); - - if (currentValue != "") - { - row = view.addRow (); - view.set (row, 0, name->first); - view.set (row, 1, renderAttribute (name->first, priorValue), - (priorValue != currentValue ? color_red : Color ())); - view.set (row, 2, renderAttribute (name->first, currentValue), - (priorValue != currentValue ? color_green : Color ())); - } - } - - foreach (name, afterOnly) - { - row = view.addRow (); - view.set (row, 0, *name); - view.set (row, 2, renderAttribute (*name, after.get (*name)), color_green); - } - } - else - { - int row; - foreach (name, after) - { - row = view.addRow (); - view.set (row, 0, name->first); - view.set (row, 2, renderAttribute (name->first, after.get (name->first)), color_green); - } - } - - std::cout << "\n" - << view.render () - << "\n"; - } - - // This style looks like this: - // --- before 2009-07-04 00:00:25.000000000 +0200 - // +++ after 2009-07-04 00:00:45.000000000 +0200 - // - // - name: old // att deleted - // + name: - // - // - name: old // att changed - // + name: new - // - // - name: - // + name: new // att added - // - else if (context.config.get ("undo.style") == "diff") - { - // Create reference tasks. - Task before; - if (prior != "") - before.parse (prior); - - Task after (current); - - // Generate table header. - ViewText view; - view.width (context.getWidth ()); - view.intraPadding (2); - view.add (Column::factory ("string", "")); - view.add (Column::factory ("string", "")); - - int row = view.addRow (); - view.set (row, 0, "--- previous state", color_red); - view.set (row, 1, "Undo will restore this state", color_red); - - row = view.addRow (); - view.set (row, 0, "+++ current state ", color_green); // Note trailing space. - view.set (row, 1, "Change made " + lastChange.toString (context.config.get ("dateformat")), color_green); - - view.addRow (); - - // Add rows to table showing diffs. - std::vector all = context.getColumns (); - - // Now factor in the annotation attributes. - Task::iterator it; - for (it = before.begin (); it != before.end (); ++it) - if (it->first.substr (0, 11) == "annotation_") - all.push_back (it->first); - - for (it = after.begin (); it != after.end (); ++it) - if (it->first.substr (0, 11) == "annotation_") - all.push_back (it->first); - - // Now render all the attributes. - std::sort (all.begin (), all.end ()); - - std::string before_att; - std::string after_att; - std::string last_att; - foreach (a, all) - { - if (*a != last_att) // Skip duplicates. - { - last_att = *a; - - before_att = before.get (*a); - after_att = after.get (*a); - - // Don't report different uuid. - // Show nothing if values are the unchanged. - if (*a == "uuid" || - before_att == after_att) - { - // Show nothing - no point displaying that which did not change. - - // row = view.addRow (); - // view.set (row, 0, *a + ":"); - // view.set (row, 1, before_att); - } - - // Attribute deleted. - else if (before_att != "" && after_att == "") - { - row = view.addRow (); - view.set (row, 0, "-" + *a + ":", color_red); - view.set (row, 1, before_att, color_red); - - row = view.addRow (); - view.set (row, 0, "+" + *a + ":", color_green); - } - - // Attribute added. - else if (before_att == "" && after_att != "") - { - row = view.addRow (); - view.set (row, 0, "-" + *a + ":", color_red); - - row = view.addRow (); - view.set (row, 0, "+" + *a + ":", color_green); - view.set (row, 1, after_att, color_green); - } - - // Attribute changed. - else - { - row = view.addRow (); - view.set (row, 0, "-" + *a + ":", color_red); - view.set (row, 1, before_att, color_red); - - row = view.addRow (); - view.set (row, 0, "+" + *a + ":", color_green); - view.set (row, 1, after_att, color_green); - } - } - } - - std::cout << "\n" - << view.render () - << "\n"; - } - - // Output displayed, now confirm. - if (context.config.getBoolean ("confirmation") && - !confirm ("The undo command is not reversible. Are you sure you want to revert to the previous state?")) - { - std::cout << "No changes made.\n"; - return; - } - - // Extract identifying uuid. - std::string uuid; - std::string::size_type uuidAtt = current.find ("uuid:\""); - if (uuidAtt != std::string::npos) - uuid = current.substr (uuidAtt, 43); // 43 = uuid:"..." - else - throw std::string ("Cannot locate UUID in task to undo."); - - // load pending.data - std::vector p; - File::read (pendingFile, p); - - // is 'current' in pending? - foreach (task, p) - { - if (task->find (uuid) != std::string::npos) - { - context.debug ("TDB::undo - task found in pending.data"); - - // Either revert if there was a prior state, or remove the task. - if (prior != "") - { - *task = prior; - std::cout << "Modified task reverted.\n"; - } - else - { - p.erase (task); - std::cout << "Task removed.\n"; - } - - // Rewrite files. - File::write (pendingFile, p); - File::write (undoFile, u); - return; - } - } - - // load completed.data - std::vector c; - File::read (completedFile, c); - - // is 'current' in completed? - foreach (task, c) - { - if (task->find (uuid) != std::string::npos) - { - context.debug ("TDB::undo - task found in completed.data"); - - // If task now belongs back in pending.data - if (prior.find ("status:\"pending\"") != std::string::npos || - prior.find ("status:\"waiting\"") != std::string::npos || - prior.find ("status:\"recurring\"") != std::string::npos) - { - c.erase (task); - p.push_back (prior); - File::write (completedFile, c); - File::write (pendingFile, p); - File::write (undoFile, u); - std::cout << "Modified task reverted.\n"; - context.debug ("TDB::undo - task belongs in pending.data"); - } - else - { - *task = prior; - File::write (completedFile, c); - File::write (undoFile, u); - std::cout << "Modified task reverted.\n"; - context.debug ("TDB::undo - task belongs in completed.data"); - } - - std::cout << "Undo complete.\n"; - return; - } - } - - // Perhaps user hand-edited the data files? - // Perhaps the task was in completed.data, which was still in file format 3? - std::cout << "Task with UUID " - << uuid.substr (6, 36) - << " not found in data.\n" - << "No undo possible.\n"; } //////////////////////////////////////////////////////////////////////////////// void TDB::merge (const std::string& mergeFile) { - /////////////////////////////////////// - // Copyright 2010 - 2011, Johannes Schlatow. - /////////////////////////////////////// - - // list of modifications that we want to add to the local database - std::list mods; - - // list of modifications that we want to add to the local history - std::list mods_history; - - // list of modifications on the local database - // has to be merged with mods to create the new undo.data - std::list lmods; - - // will contain the NEW undo.data - std::vector undo; - - /////////////////////////////////////// - // initialize the files: - - // load merge file (undo file of right/remote branch) - std::vector r; - if (! File::read (mergeFile, r)) - throw std::string ("Could not read '") + mergeFile + "'."; - - // file has to contain at least one entry - if (r.size () < 3) - throw std::string ("There are no changes to merge."); - - // load undo file (left/local branch) - Directory location (context.config.get ("data.location")); - std::string undoFile = location._data + "/undo.data"; - - std::vector l; - if (! File::read (undoFile, l)) - throw std::string ("Could not read '") + undoFile + "'."; - - std::string rline, lline; - std::vector ::iterator rit, lit; - - // read first line - rit = r.begin (); - lit = l.begin (); - - if (rit != r.end()) - rline = *rit; - if (lit != l.end()) - lline = *lit; - - /////////////////////////////////////// - // find the branch-off point: - - // first lines are not equal => assuming mergeFile starts at a - // later point in time - if (lline.compare (rline) != 0) - { - // iterate in local file to find rline - for ( ; lit != l.end (); ++lit) - { - lline = *lit; - - // push the line to the new undo.data - undo.push_back (lline + "\n"); - - // found first matching lines? - if (lline.compare (rline) == 0) - break; - } - } - - // Add some color. - Color colorAdded (context.config.get ("color.sync.added")); - Color colorChanged (context.config.get ("color.sync.changed")); - Color colorRejected (context.config.get ("color.sync.rejected")); - - // at this point we can assume: (lline==rline) || (lit == l.end()) - // thus we search for the first non-equal lines or the EOF - bool found = false; - for ( ; (lit != l.end ()) && (rit != r.end ()); ++lit, ++rit) - { - lline = *lit; - rline = *rit; - - // found first non-matching lines? - if (lline.compare (rline) != 0) - { - found = true; - break; - } - else - { - // push the line to the new undo.data - undo.push_back (lline + "\n"); - } - } - - std::cout << "\n"; - - /////////////////////////////////////// - // branch-off point found: - if (found) - { - DEBUG_STR_PART ("Branch-off point found at: "); - DEBUG_STR_END (lline); - - std::list rmods; - - // helper lists - std::set uuid_new, uuid_left; - - // 1. read taskmods out of the remaining lines - readTaskmods (l, lit, lmods); - readTaskmods (r, rit, rmods); - - // 2. move new uuids into mods - DEBUG_STR_PART ("adding new uuids (left) to skip list..."); - - // modifications on the left side are already in the database - // we just need them to merge conflicts, so we add new the mods for - // new uuids to the skip-list 'uuid_left' - std::list::iterator lmod_it; - for (lmod_it = lmods.begin (); lmod_it != lmods.end (); lmod_it++) - { - if (lmod_it->isNew ()) - { -/* - std::cout << "New local task " - << (context.color () ? colorAdded.colorize (lmod_it->getUuid ()) : lmod_it->getUuid ()) - << "\n"; -*/ - - uuid_left.insert (lmod_it->getUuid ()); - } - } - - DEBUG_STR_END ("done"); - DEBUG_STR_PART ("move new uuids (right) to redo list..."); - - // new items on the right side need to be inserted into the - // local database - std::list::iterator rmod_it; - for (rmod_it = rmods.begin (); rmod_it != rmods.end (); ) - { - // we have to save and increment the iterator because we may want to delete - // the object from the list - std::list::iterator current = rmod_it++; - Taskmod tmod = *current; - - // new uuid? - if (tmod.isNew ()) - { -/* - std::cout << "Adding new remote task " - << (context.color () ? colorAdded.colorize (tmod.getUuid ()) : tmod.getUuid ()) - << "\n"; -*/ - - uuid_new.insert (tmod.getUuid ()); - mods.push_back (tmod); - rmods.erase (current); - } - else if (uuid_new.find (tmod.getUuid ()) != uuid_new.end ()) - { - // uuid of modification was new - mods.push_back (tmod); - rmods.erase (current); - } - } - - DEBUG_STR_END ("done"); - - /////////////////////////////////////// - // merge modifications: - DEBUG_STR ("Merging modifications:"); - - // we iterate backwards to resolve conflicts by timestamps (newest one wins) - std::list::reverse_iterator lmod_rit; - std::list::reverse_iterator rmod_rit; - for (lmod_rit = lmods.rbegin (); lmod_rit != lmods.rend (); ++lmod_rit) - { - Taskmod tmod_l = *lmod_rit; - std::string uuid = tmod_l.getUuid (); - - DEBUG_STR (" left uuid: " + uuid); - - // skip if uuid had already been merged - if (uuid_left.find (uuid) == uuid_left.end ()) - { - bool rwin = false; - bool lwin = false; - for (rmod_rit = rmods.rbegin (); rmod_rit != rmods.rend (); rmod_rit++) - { - Taskmod tmod_r = *rmod_rit; - - DEBUG_STR (" right uuid: " + tmod_r.getUuid ()); - if (tmod_r.getUuid () == uuid) - { - DEBUG_STR (" uuid match found for " + uuid); - - // we already decided to take the mods from the right side - // but we have to find the first modification newer than - // the one on the left side to merge the history too - if (rwin) - { - DEBUG_STR (" scanning right side"); - if (tmod_r > tmod_l) - mods.push_front (tmod_r); - - std::list::iterator tmp_it = rmod_rit.base (); - rmods.erase (--tmp_it); - rmod_rit--; - } - else if (lwin) - { - DEBUG_STR (" cleaning up right side"); - - // add tmod_r to local history - mods_history.push_front (tmod_r); - - std::list::iterator tmp_it = rmod_rit.base (); - rmods.erase (--tmp_it); - rmod_rit--; - } - else - { - // which one is newer? - if (tmod_r > tmod_l) - { - std::cout << "Found remote change to " - << (context.color () ? colorChanged.colorize (uuid) : uuid) - << " \"" << cutOff (tmod_r.getBefore ().get ("description"), 10) << "\"" - << "\n"; - - mods.push_front(tmod_r); - - // delete tmod from right side - std::list::iterator tmp_it = rmod_rit.base (); - rmods.erase (--tmp_it); - rmod_rit--; - - rwin = true; - } - else - { - std::cout << "Retaining local changes to " - << (context.color () ? colorRejected.colorize (uuid) : uuid) - << " \"" << cutOff (tmod_l.getBefore ().get ("description"), 10) << "\"" - << "\n"; - - // inserting right mod into history of local database - // so that it can be restored later - // AND more important: create a history that looks the same - // as if we switched the roles 'remote' and 'local' - - // thus we have to find the oldest change on the local branch that is not on remote branch - std::list::iterator lmod_it; - std::list::iterator last = lmod_it; - for (lmod_it = lmods.begin (); lmod_it != lmods.end (); ++lmod_it) { - if ((*lmod_it).getUuid () == uuid) { - last = lmod_it; - } - } - - if (tmod_l > tmod_r) { // local change is newer - last->setBefore(tmod_r.getAfter ()); - - // add tmod_r to local history - lmods.push_back(tmod_r); - } - else { // both mods have equal timestamps - // in this case the local branch wins as above, but the remote change with the - // same timestamp will be discarded - - // find next (i.e. older) mod of this uuid on remote side - std::list::reverse_iterator rmod_rit2; - for (rmod_rit2 = rmod_rit, ++rmod_rit2; rmod_rit2 != rmods.rend (); ++rmod_rit2) { - Taskmod tmp_mod = *rmod_rit2; - if (tmp_mod.getUuid () == uuid) { - last->setBefore (tmp_mod.getAfter ()); - break; - } - } - } - - // TODO feature: restore command? We would have to add a marker to the undo.file. - - // delete tmod from right side - std::list::iterator tmp_it = rmod_rit.base (); - rmods.erase (--tmp_it); - rmod_rit--; - - // mark this uuid as merged - uuid_left.insert (uuid); - lwin = true; - } - } - } - } // for - - if (rwin) - { - DEBUG_STR (" concat the first match to left branch"); - // concat the oldest (but still newer) modification on the right - // to the endpoint on the left - mods.front ().setBefore(tmod_l.getAfter ()); - } - } - } // for - - DEBUG_STR ("adding non-conflicting changes from the right branch"); - mods.splice (mods.begin (), rmods); - - DEBUG_STR ("sorting taskmod list"); - mods.sort (); - mods_history.sort (); - } - else if (rit == r.end ()) - { - // nothing happend on the remote branch - // local branch is up-to-date - - // nothing happend on the local branch either - - // break, to suppress autopush - if (lit == l.end ()) - { - mods.clear (); - lmods.clear (); - throw std::string ("Database is up-to-date, no merge required."); - } - - } - else // lit == l.end () - { - // nothing happend on the local branch -/* - std::cout << "No local changes detected.\n"; -*/ - - // add remaining lines (remote branch) to the list of modifications -/* - std::cout << "Remote changes detected.\n"; -*/ - readTaskmods (r, rit, mods); - } - - /////////////////////////////////////// - // Now apply the changes. - // redo command: - - if (!mods.empty ()) - { - std::string pendingFile = location._data + "/pending.data"; - std::vector pending; - - std::string completedFile = location._data + "/completed.data"; - std::vector completed; - - if (! File::read (pendingFile, pending)) - throw std::string ("Could not read '") + pendingFile + "'."; - - if (! File::read (completedFile, completed)) - throw std::string ("Could not read '") + completedFile + "'."; - - // iterate over taskmod list - std::list::iterator it; - for (it = mods.begin (); it != mods.end (); ) - { - std::list::iterator current = it++; - Taskmod tmod = *current; - - // Modification to an existing task. - if (!tmod.isNew ()) - { - std::string uuid = tmod.getUuid (); - Task::status statusBefore = tmod.getBefore().getStatus (); - Task::status statusAfter = tmod.getAfter().getStatus (); - - std::vector ::iterator it; - - bool found = false; - if ( (statusBefore == Task::completed) - || (statusBefore == Task::deleted) ) - { - // Find the same uuid in completed data - for (it = completed.begin (); it != completed.end (); ++it) - { - if (it->find ("uuid:\"" + uuid) != std::string::npos) - { - // Update the completed record. -/* - std::cout << "Modifying " - << (context.color () ? colorChanged.colorize (uuid) : uuid) - << "\n"; -*/ - - // remove the \n from composeF4() string - std::string newline = tmod.getAfter ().composeF4 (); - newline = newline.substr (0, newline.length ()-1); - - // does the tasks move to pending data? - // this taskmod will not arise from - // normal usage of task, but those kinds of - // taskmods may be constructed to merge databases - if ( (statusAfter != Task::completed) - && (statusAfter != Task::deleted) ) - { - // insert task into pending data - pending.push_back (newline); - - // remove task from completed data - completed.erase (it); - - } - else - { - // replace the current line - *it = newline; - } - - found = true; - break; - } - } - } - else - { - // Find the same uuid in the pending data. - for (it = pending.begin (); it != pending.end (); ++it) - { - if (it->find ("uuid:\"" + uuid) != std::string::npos) - { - // Update the pending record. - std::cout << "Found remote change to " - << (context.color () ? colorChanged.colorize (uuid) : uuid) - << " \"" << cutOff (tmod.getBefore ().get ("description"), 10) << "\"" - << "\n"; - - // remove the \n from composeF4() string - // which will replace the current line - std::string newline = tmod.getAfter ().composeF4 (); - newline = newline.substr (0, newline.length ()-1); - - // does the tasks move to completed data - if ( (statusAfter == Task::completed) - || (statusAfter == Task::deleted) ) - { - // insert task into completed data - completed.push_back (newline); - - // remove task from pending data - pending.erase (it); - - } - else - { - // replace the current line - *it = newline; - } - - found = true; - break; - } - } - } - - if (!found) - { - std::cout << "Missing " - << (context.color () ? colorRejected.colorize (uuid) : uuid) - << " \"" << cutOff (tmod.getBefore ().get ("description"), 10) << "\"" - << "\n"; - mods.erase (current); - } - } - else - { - // Check for dups. - std::string uuid = tmod.getAfter ().get ("uuid"); - - // Find the same uuid in the pending data. - bool found = false; - std::vector ::iterator pit; - for (pit = pending.begin (); pit != pending.end (); ++pit) - { - if (pit->find ("uuid:\"" + uuid) != std::string::npos) - { - found = true; - break; - } - } - - if (!found) - { - std::cout << "Merging new remote task " - << (context.color () ? colorAdded.colorize (uuid) : uuid) - << " \"" << cutOff (tmod.getAfter ().get ("description"), 10) << "\"" - << "\n"; - - // remove the \n from composeF4() string - std::string newline = tmod.getAfter ().composeF4 (); - newline = newline.substr (0, newline.length ()-1); - pending.push_back (newline); - } - else - { - mods.erase (current); - } - } - } - - // write pending file - if (! File::write (pendingFile, pending)) - throw std::string ("Could not write '") + pendingFile + "'."; - - // write completed file - if (! File::write (completedFile, completed)) - throw std::string ("Could not write '") + completedFile + "'."; - } - - if (!mods.empty() || !lmods.empty() || !mods_history.empty()) { - // at this point undo contains the lines up to the branch-off point - // now we merge mods (new modifications from mergefile) - // with lmods (part of old undo.data) - lmods.sort(); - mods.merge (lmods); - mods.merge (mods_history); - - // generate undo.data format - std::list::iterator it; - for (it = mods.begin (); it != mods.end (); it++) - undo.push_back(it->toString ()); - - // write undo file - if (! File::write (undoFile, undo, false)) - throw std::string ("Could not write '") + undoFile + "'."; - } - - // delete objects - lmods.clear (); - mods.clear (); - mods_history.clear (); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 29f08c0c5..dc94def2f 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -26,7 +26,10 @@ //////////////////////////////////////////////////////////////////////////////// #include // TODO Remove. +#include #include +#include +#include #include #include #include @@ -38,6 +41,22 @@ extern Context context; +#define NDEBUG +#include +#include + +#define DEBUG_OUTPUT 0 + +#if DEBUG_OUTPUT > 0 + #define DEBUG_STR(str) std::cout << "DEBUG: " << str << "\n"; std::cout.flush() + #define DEBUG_STR_PART(str) std::cout << "DEBUG: " << str; std::cout.flush() + #define DEBUG_STR_END(str) std::cout << str << "\n"; std::cout.flush() +#else + #define DEBUG_STR(str) + #define DEBUG_STR_PART(str) + #define DEBUG_STR_END(str) +#endif + //////////////////////////////////////////////////////////////////////////////// TF2::TF2 () : _read_only (false) @@ -577,6 +596,598 @@ void TDB2::synch () context.timer_synch.stop (); } +//////////////////////////////////////////////////////////////////////////////// +// Helper function for TDB::merge +void readTaskmods (std::vector &input, + std::vector ::iterator &start, + std::list &list) +{ + std::string line; + Taskmod tmod_tmp; + + DEBUG_STR ("reading taskmods from file: "); + + for ( ; start != input.end (); ++start) + { + line = *start; + + if (line.substr (0, 4) == "time") + { + std::stringstream stream (line.substr (5)); + long ts; + stream >> ts; + + if (stream.fail ()) + throw std::string ("There was a problem reading the timestamp from the undo.data file."); + + // 'time' is the first line of a modification + // thus we will (re)set the taskmod object + tmod_tmp.reset (ts); + + } + else if (line.substr (0, 3) == "old") + { + tmod_tmp.setBefore (Task (line.substr (4))); + + } + else if (line.substr (0, 3) == "new") + { + tmod_tmp.setAfter (Task (line.substr (4))); + + // 'new' is the last line of a modification, + // thus we can push to the list + list.push_back (tmod_tmp); + + assert (tmod_tmp.isValid ()); + DEBUG_STR (" taskmod complete"); + } + } + + DEBUG_STR ("DONE"); +} + +//////////////////////////////////////////////////////////////////////////////// +void TDB2::merge (const std::string& mergeFile) +{ + /////////////////////////////////////// + // Copyright 2010 - 2011, Johannes Schlatow. + /////////////////////////////////////// + + // list of modifications that we want to add to the local database + std::list mods; + + // list of modifications that we want to add to the local history + std::list mods_history; + + // list of modifications on the local database + // has to be merged with mods to create the new undo.data + std::list lmods; + + // will contain the NEW undo.data + std::vector undo_lines; + + /////////////////////////////////////// + // initialize the files: + + // load merge file (undo file of right/remote branch) + std::vector r; + if (! File::read (mergeFile, r)) + throw std::string ("Could not read '") + mergeFile + "'."; + + // file has to contain at least one entry + if (r.size () < 3) + throw std::string ("There are no changes to merge."); + + // load undo file (left/local branch) + std::vector l; + if (! File::read (undo._file._data, l)) + throw std::string ("Could not read '") + undo._file._data + "'."; + + std::string rline, lline; + std::vector ::iterator rit, lit; + + // read first line + rit = r.begin (); + lit = l.begin (); + + if (rit != r.end()) + rline = *rit; + if (lit != l.end()) + lline = *lit; + + /////////////////////////////////////// + // find the branch-off point: + + // first lines are not equal => assuming mergeFile starts at a + // later point in time + if (lline.compare (rline) != 0) + { + // iterate in local file to find rline + for ( ; lit != l.end (); ++lit) + { + lline = *lit; + + // push the line to the new undo.data + undo_lines.push_back (lline + "\n"); + + // found first matching lines? + if (lline.compare (rline) == 0) + break; + } + } + + // Add some color. + Color colorAdded (context.config.get ("color.sync.added")); + Color colorChanged (context.config.get ("color.sync.changed")); + Color colorRejected (context.config.get ("color.sync.rejected")); + + // at this point we can assume: (lline==rline) || (lit == l.end()) + // thus we search for the first non-equal lines or the EOF + bool found = false; + for ( ; (lit != l.end ()) && (rit != r.end ()); ++lit, ++rit) + { + lline = *lit; + rline = *rit; + + // found first non-matching lines? + if (lline.compare (rline) != 0) + { + found = true; + break; + } + else + { + // push the line to the new undo.data + undo_lines.push_back (lline + "\n"); + } + } + + std::cout << "\n"; + + /////////////////////////////////////// + // branch-off point found: + if (found) + { + DEBUG_STR_PART ("Branch-off point found at: "); + DEBUG_STR_END (lline); + + std::list rmods; + + // helper lists + std::set uuid_new, uuid_left; + + // 1. read taskmods out of the remaining lines + readTaskmods (l, lit, lmods); + readTaskmods (r, rit, rmods); + + // 2. move new uuids into mods + DEBUG_STR_PART ("adding new uuids (left) to skip list..."); + + // modifications on the left side are already in the database + // we just need them to merge conflicts, so we add new the mods for + // new uuids to the skip-list 'uuid_left' + std::list::iterator lmod_it; + for (lmod_it = lmods.begin (); lmod_it != lmods.end (); lmod_it++) + { + if (lmod_it->isNew ()) + { +/* + std::cout << "New local task " + << (context.color () ? colorAdded.colorize (lmod_it->getUuid ()) : lmod_it->getUuid ()) + << "\n"; +*/ + + uuid_left.insert (lmod_it->getUuid ()); + } + } + + DEBUG_STR_END ("done"); + DEBUG_STR_PART ("move new uuids (right) to redo list..."); + + // new items on the right side need to be inserted into the + // local database + std::list::iterator rmod_it; + for (rmod_it = rmods.begin (); rmod_it != rmods.end (); ) + { + // we have to save and increment the iterator because we may want to delete + // the object from the list + std::list::iterator current = rmod_it++; + Taskmod tmod = *current; + + // new uuid? + if (tmod.isNew ()) + { +/* + std::cout << "Adding new remote task " + << (context.color () ? colorAdded.colorize (tmod.getUuid ()) : tmod.getUuid ()) + << "\n"; +*/ + + uuid_new.insert (tmod.getUuid ()); + mods.push_back (tmod); + rmods.erase (current); + } + else if (uuid_new.find (tmod.getUuid ()) != uuid_new.end ()) + { + // uuid of modification was new + mods.push_back (tmod); + rmods.erase (current); + } + } + + DEBUG_STR_END ("done"); + + /////////////////////////////////////// + // merge modifications: + DEBUG_STR ("Merging modifications:"); + + // we iterate backwards to resolve conflicts by timestamps (newest one wins) + std::list::reverse_iterator lmod_rit; + std::list::reverse_iterator rmod_rit; + for (lmod_rit = lmods.rbegin (); lmod_rit != lmods.rend (); ++lmod_rit) + { + Taskmod tmod_l = *lmod_rit; + std::string uuid = tmod_l.getUuid (); + + DEBUG_STR (" left uuid: " + uuid); + + // skip if uuid had already been merged + if (uuid_left.find (uuid) == uuid_left.end ()) + { + bool rwin = false; + bool lwin = false; + for (rmod_rit = rmods.rbegin (); rmod_rit != rmods.rend (); rmod_rit++) + { + Taskmod tmod_r = *rmod_rit; + + DEBUG_STR (" right uuid: " + tmod_r.getUuid ()); + if (tmod_r.getUuid () == uuid) + { + DEBUG_STR (" uuid match found for " + uuid); + + // we already decided to take the mods from the right side + // but we have to find the first modification newer than + // the one on the left side to merge the history too + if (rwin) + { + DEBUG_STR (" scanning right side"); + if (tmod_r > tmod_l) + mods.push_front (tmod_r); + + std::list::iterator tmp_it = rmod_rit.base (); + rmods.erase (--tmp_it); + rmod_rit--; + } + else if (lwin) + { + DEBUG_STR (" cleaning up right side"); + + // add tmod_r to local history + mods_history.push_front (tmod_r); + + std::list::iterator tmp_it = rmod_rit.base (); + rmods.erase (--tmp_it); + rmod_rit--; + } + else + { + // which one is newer? + if (tmod_r > tmod_l) + { + std::cout << "Found remote change to " + << (context.color () ? colorChanged.colorize (uuid) : uuid) + << " \"" << cutOff (tmod_r.getBefore ().get ("description"), 10) << "\"" + << "\n"; + + mods.push_front(tmod_r); + + // delete tmod from right side + std::list::iterator tmp_it = rmod_rit.base (); + rmods.erase (--tmp_it); + rmod_rit--; + + rwin = true; + } + else + { + std::cout << "Retaining local changes to " + << (context.color () ? colorRejected.colorize (uuid) : uuid) + << " \"" << cutOff (tmod_l.getBefore ().get ("description"), 10) << "\"" + << "\n"; + + // inserting right mod into history of local database + // so that it can be restored later + // AND more important: create a history that looks the same + // as if we switched the roles 'remote' and 'local' + + // thus we have to find the oldest change on the local branch that is not on remote branch + std::list::iterator lmod_it; + std::list::iterator last = lmod_it; + for (lmod_it = lmods.begin (); lmod_it != lmods.end (); ++lmod_it) { + if ((*lmod_it).getUuid () == uuid) { + last = lmod_it; + } + } + + if (tmod_l > tmod_r) { // local change is newer + last->setBefore(tmod_r.getAfter ()); + + // add tmod_r to local history + lmods.push_back(tmod_r); + } + else { // both mods have equal timestamps + // in this case the local branch wins as above, but the remote change with the + // same timestamp will be discarded + + // find next (i.e. older) mod of this uuid on remote side + std::list::reverse_iterator rmod_rit2; + for (rmod_rit2 = rmod_rit, ++rmod_rit2; rmod_rit2 != rmods.rend (); ++rmod_rit2) { + Taskmod tmp_mod = *rmod_rit2; + if (tmp_mod.getUuid () == uuid) { + last->setBefore (tmp_mod.getAfter ()); + break; + } + } + } + + // TODO feature: restore command? We would have to add a marker to the undo.file. + + // delete tmod from right side + std::list::iterator tmp_it = rmod_rit.base (); + rmods.erase (--tmp_it); + rmod_rit--; + + // mark this uuid as merged + uuid_left.insert (uuid); + lwin = true; + } + } + } + } // for + + if (rwin) + { + DEBUG_STR (" concat the first match to left branch"); + // concat the oldest (but still newer) modification on the right + // to the endpoint on the left + mods.front ().setBefore(tmod_l.getAfter ()); + } + } + } // for + + DEBUG_STR ("adding non-conflicting changes from the right branch"); + mods.splice (mods.begin (), rmods); + + DEBUG_STR ("sorting taskmod list"); + mods.sort (); + mods_history.sort (); + } + else if (rit == r.end ()) + { + // nothing happend on the remote branch + // local branch is up-to-date + + // nothing happend on the local branch either + + // break, to suppress autopush + if (lit == l.end ()) + { + mods.clear (); + lmods.clear (); + throw std::string ("Database is up-to-date, no merge required."); + } + + } + else // lit == l.end () + { + // nothing happend on the local branch +/* + std::cout << "No local changes detected.\n"; +*/ + + // add remaining lines (remote branch) to the list of modifications +/* + std::cout << "Remote changes detected.\n"; +*/ + readTaskmods (r, rit, mods); + } + + /////////////////////////////////////// + // Now apply the changes. + // redo command: + + if (!mods.empty ()) + { + std::vector pending_lines; + + std::vector completed_lines; + + if (! File::read (pending._file._data, pending_lines)) + throw std::string ("Could not read '") + pending._file._data + "'."; + + if (! File::read (completed._file._data, completed_lines)) + throw std::string ("Could not read '") + completed._file._data + "'."; + + // iterate over taskmod list + std::list::iterator it; + for (it = mods.begin (); it != mods.end (); ) + { + std::list::iterator current = it++; + Taskmod tmod = *current; + + // Modification to an existing task. + if (!tmod.isNew ()) + { + std::string uuid = tmod.getUuid (); + Task::status statusBefore = tmod.getBefore().getStatus (); + Task::status statusAfter = tmod.getAfter().getStatus (); + + std::vector ::iterator it; + + bool found = false; + if ( (statusBefore == Task::completed) + || (statusBefore == Task::deleted) ) + { + // Find the same uuid in completed data + for (it = completed_lines.begin (); it != completed_lines.end (); ++it) + { + if (it->find ("uuid:\"" + uuid) != std::string::npos) + { + // Update the completed record. +/* + std::cout << "Modifying " + << (context.color () ? colorChanged.colorize (uuid) : uuid) + << "\n"; +*/ + + // remove the \n from composeF4() string + std::string newline = tmod.getAfter ().composeF4 (); + newline = newline.substr (0, newline.length ()-1); + + // does the tasks move to pending data? + // this taskmod will not arise from + // normal usage of task, but those kinds of + // taskmods may be constructed to merge databases + if ( (statusAfter != Task::completed) + && (statusAfter != Task::deleted) ) + { + // insert task into pending data + pending_lines.push_back (newline); + + // remove task from completed data + completed_lines.erase (it); + + } + else + { + // replace the current line + *it = newline; + } + + found = true; + break; + } + } + } + else + { + // Find the same uuid in the pending data. + for (it = pending_lines.begin (); it != pending_lines.end (); ++it) + { + if (it->find ("uuid:\"" + uuid) != std::string::npos) + { + // Update the pending record. + std::cout << "Found remote change to " + << (context.color () ? colorChanged.colorize (uuid) : uuid) + << " \"" << cutOff (tmod.getBefore ().get ("description"), 10) << "\"" + << "\n"; + + // remove the \n from composeF4() string + // which will replace the current line + std::string newline = tmod.getAfter ().composeF4 (); + newline = newline.substr (0, newline.length ()-1); + + // does the tasks move to completed data + if ( (statusAfter == Task::completed) + || (statusAfter == Task::deleted) ) + { + // insert task into completed data + completed_lines.push_back (newline); + + // remove task from pending data + pending_lines.erase (it); + + } + else + { + // replace the current line + *it = newline; + } + + found = true; + break; + } + } + } + + if (!found) + { + std::cout << "Missing " + << (context.color () ? colorRejected.colorize (uuid) : uuid) + << " \"" << cutOff (tmod.getBefore ().get ("description"), 10) << "\"" + << "\n"; + mods.erase (current); + } + } + else + { + // Check for dups. + std::string uuid = tmod.getAfter ().get ("uuid"); + + // Find the same uuid in the pending data. + bool found = false; + std::vector ::iterator pit; + for (pit = pending_lines.begin (); pit != pending_lines.end (); ++pit) + { + if (pit->find ("uuid:\"" + uuid) != std::string::npos) + { + found = true; + break; + } + } + + if (!found) + { + std::cout << "Merging new remote task " + << (context.color () ? colorAdded.colorize (uuid) : uuid) + << " \"" << cutOff (tmod.getAfter ().get ("description"), 10) << "\"" + << "\n"; + + // remove the \n from composeF4() string + std::string newline = tmod.getAfter ().composeF4 (); + newline = newline.substr (0, newline.length ()-1); + pending_lines.push_back (newline); + } + else + { + mods.erase (current); + } + } + } + + // write pending file + if (! File::write (pending._file._data, pending_lines)) + throw std::string ("Could not write '") + pending._file._data + "'."; + + // write completed file + if (! File::write (completed._file._data, completed_lines)) + throw std::string ("Could not write '") + completed._file._data + "'."; + } + + if (!mods.empty() || !lmods.empty() || !mods_history.empty()) { + // at this point undo contains the lines up to the branch-off point + // now we merge mods (new modifications from mergefile) + // with lmods (part of old undo.data) + lmods.sort(); + mods.merge (lmods); + mods.merge (mods_history); + + // generate undo.data format + std::list::iterator it; + for (it = mods.begin (); it != mods.end (); it++) + undo_lines.push_back(it->toString ()); + + // write undo file + if (! File::write (undo._file._data, undo_lines, false)) + throw std::string ("Could not write '") + undo._file._data + "'."; + } + + // delete objects + lmods.clear (); + mods.clear (); + mods_history.clear (); +} + //////////////////////////////////////////////////////////////////////////////// void TDB2::revert () { diff --git a/src/TDB2.h b/src/TDB2.h index c849664bd..df216d73f 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -97,6 +97,7 @@ public: void modify (Task&); void commit (); void synch (); + void merge (const std::string&); void revert (); int gc (); int next_id (); diff --git a/src/commands/CmdAdd.cpp b/src/commands/CmdAdd.cpp index a98b008d0..8543118f6 100644 --- a/src/commands/CmdAdd.cpp +++ b/src/commands/CmdAdd.cpp @@ -59,9 +59,9 @@ int CmdAdd::execute (std::string& output) // TODO This should be a call in to feedback.cpp. if (context.verbose ("new-id")) output = format (STRING_CMD_ADD_FEEDBACK, context.tdb2.next_id ()) + "\n"; -/* - context.footnote (onProjectChange (task)); -*/ + +// context.footnote (onProjectChange (task)); + context.tdb2.commit (); return rc; } diff --git a/src/commands/CmdMerge.cpp b/src/commands/CmdMerge.cpp index 682e1c9cf..f3dea0b8a 100644 --- a/src/commands/CmdMerge.cpp +++ b/src/commands/CmdMerge.cpp @@ -82,9 +82,7 @@ int CmdMerge::execute (std::string& output) else file = uri._path; - context.tdb.lock (context.config.getBoolean ("locking")); - context.tdb.merge (file); - context.tdb.unlock (); + context.tdb2.merge (file); output += "Merge complete.\n"; diff --git a/test/merge.duplicates.t b/test/merge.duplicates.t index c939b4a96..810369fba 100755 --- a/test/merge.duplicates.t +++ b/test/merge.duplicates.t @@ -28,8 +28,9 @@ use strict; use warnings; -use Test::More tests => 53; +use Test::More tests => 31; use File::Copy; +use File::Path; use constant false => 0; use constant true => 1; @@ -175,65 +176,50 @@ like ($output, qr/up-to-date/, "res2: up-to-date"); unlike ($output, qr/Missing/, "no missing entry"); # Cleanup. -unlink 'data1/pending.data'; -ok (!-r 'data1/pending.data', 'Removed data1/pending.data'); -unlink 'data1/completed.data'; -ok (!-r 'data1/completed.data', 'Removed data1/completed.data'); -unlink 'data1/undo.data'; -ok (!-r 'data1/undo.data', 'Removed data1/undo.data'); -unlink 'data1/backlog.data'; -ok (!-r 'data1/backlog.data', 'Removed data1/backlog.data'); -unlink 'data1/synch.key'; -ok (!-r 'data1/synch.key', 'Removed data1/synch.key'); +unlink qw(data1/pending.data data1/completed.data data1/undo.data data1/undo.save data1/backlog.data data1/synch.key 1.rc); +ok (! -r 'data1/pending.data' && + ! -r 'data1/completed.data' && + ! -r 'data1/undo.data' && + ! -r 'data1/undo.save' && + ! -r 'data1/backlog.data' && + ! -r 'data1/synch_key.data' && + ! -r '1.rc', 'data1 Cleanup'); -unlink 'data2/pending.data'; -ok (!-r 'data2/pending.data', 'Removed data2/pending.data'); -unlink 'data2/completed.data'; -ok (!-r 'data2/completed.data', 'Removed data2/completed.data'); -unlink 'data2/undo.data'; -ok (!-r 'data2/undo.data', 'Removed data2/undo.data'); -unlink 'data2/backlog.data'; -ok (!-r 'data2/backlog.data', 'Removed data2/backlog.data'); -unlink 'data2/synch.key'; -ok (!-r 'data2/synch.key', 'Removed data2/synch.key'); +unlink qw(data2/pending.data data2/completed.data data2/undo.data data2/undo.save data2/backlog.data data2/synch.key 2.rc); +ok (! -r 'data2/pending.data' && + ! -r 'data2/completed.data' && + ! -r 'data2/undo.data' && + ! -r 'data2/undo.save' && + ! -r 'data2/backlog.data' && + ! -r 'data2/synch_key.data' && + ! -r '2.rc', 'data2 Cleanup'); -unlink 'data3/pending.data'; -ok (!-r 'data3/pending.data', 'Removed data3/pending.data'); -unlink 'data3/completed.data'; -ok (!-r 'data3/completed.data', 'Removed data3/completed.data'); -unlink 'data3/undo.data'; -ok (!-r 'data3/undo.data', 'Removed data3/undo.data'); -unlink 'data3/backlog.data'; -ok (!-r 'data3/backlog.data', 'Removed data3/backlog.data'); -unlink 'data3/synch.key'; -ok (!-r 'data3/synch.key', 'Removed data3/synch.key'); +unlink qw(data3/pending.data data3/completed.data data3/undo.data data3/undo.save data3/backlog.data data3/synch.key 3.rc); +ok (! -r 'data3/pending.data' && + ! -r 'data3/completed.data' && + ! -r 'data3/undo.data' && + ! -r 'data3/undo.save' && + ! -r 'data3/backlog.data' && + ! -r 'data3/synch_key.data' && + ! -r '3.rc', 'data3 Cleanup'); -unlink 'backup/pending.data'; -ok (!-r 'backup/pending.data', 'Removed backup/pending.data'); -unlink 'backup/completed.data'; -ok (!-r 'backup/completed.data', 'Removed backup/completed.data'); -unlink 'backup/undo.data'; -ok (!-r 'backup/undo.data', 'Removed backup/undo.data'); -unlink 'backup/backlog.data'; -ok (!-r 'backup/backlog.data', 'Removed backup/backlog.data'); -unlink 'backup/synch.key'; -ok (!-r 'backup/synch.key', 'Removed backup/synch.key'); +unlink qw(backup/pending.data backup/completed.data backup/undo.data backup/undo.save backup/backlog.data backup/synch.key); +ok (! -r 'backup/pending.data' && + ! -r 'backup/completed.data' && + ! -r 'backup/undo.data' && + ! -r 'backup/undo.save' && + ! -r 'backup/backlog.data' && + ! -r 'backup/synch_key.data', 'backup Cleanup'); -unlink '1.rc'; -ok (!-r '1.rc', 'Removed 1.rc'); -unlink '2.rc'; -ok (!-r '2.rc', 'Removed 2.rc'); -unlink '3.rc'; -ok (!-r '3.rc', 'Removed 3.rc'); - -rmdir("data1"); -ok (!-e "data1", "Removed dir data1"); -rmdir("data2"); -ok (!-e "data2", "Removed dir data2"); -rmdir("data3"); -ok (!-e "data3", "Removed dir data3"); -rmdir("backup"); -ok (!-e "backup", "Removed dir backup"); +rmtree (['data1/extensions', 'data1', 'data2/extensions', 'data2', 'data3/extensions', 'data3', 'backup/extensions', 'backup'], 0, 1); +ok (! -e 'data1/extensions' && + ! -e 'data1' && + ! -e 'data2/extensions' && + ! -e 'data2' && + ! -e 'data3/extensions' && + ! -e 'data3' && + ! -e 'backup/extensions' && + ! -e 'backup', 'Removed dir local'); exit 0; diff --git a/test/merge.t b/test/merge.t index b95602125..bd8ab0d41 100755 --- a/test/merge.t +++ b/test/merge.t @@ -28,8 +28,9 @@ use strict; use warnings; -use Test::More tests => 45; +use Test::More tests => 33; use File::Copy; +use File::Path; use constant false => 0; use constant true => 1; @@ -45,12 +46,12 @@ if (open my $fh, '>', 'local.rc') { print $fh "data.location=./local\n", "confirmation=no\n", - "merge.autopush=no\n", + "merge.autopush=no\n", "report.list.description=DESC\n", - "report.list.columns=id,project,active,priority,description,tags\n", - "report.list.labels=id,pro,a,pri,d,t\n", - "report.list.sort=id+\n", - "report.list.filter=status:pending\n"; + "report.list.columns=id,project,active,priority,description,tags\n", + "report.list.labels=id,pro,a,pri,d,t\n", + "report.list.sort=id+\n", + "report.list.filter=status:pending\n"; close $fh; ok (-r 'local.rc', 'Created local.rc'); } @@ -59,12 +60,12 @@ if (open my $fh, '>', 'remote.rc') { print $fh "data.location=./remote\n", "confirmation=no\n", - "merge.autopush=no\n", + "merge.autopush=no\n", "report.list.description=DESC\n", - "report.list.columns=id,project,active,priority,description,tags\n", - "report.list.labels=id,pro,a,pri,d,t\n", - "report.list.sort=id+\n", - "report.list.filter=status:pending\n"; + "report.list.columns=id,project,active,priority,description,tags\n", + "report.list.labels=id,pro,a,pri,d,t\n", + "report.list.sort=id+\n", + "report.list.filter=status:pending\n"; close $fh; ok (-r 'remote.rc', 'Created remote.rc'); } @@ -72,71 +73,71 @@ if (open my $fh, '>', 'remote.rc') # Create some basic tasks on both sides qx{../src/task rc:local.rc add left_modified}; diag ("25 second delay"); -sleep(1); +sleep 1; qx{../src/task rc:local.rc add right_modified}; -sleep(1); +sleep 1; qx{../src/task rc:local.rc add left_newer}; -sleep(1); +sleep 1; qx{../src/task rc:local.rc add right_newer}; -sleep(1); +sleep 1; qx{../src/task rc:local.rc add left_deleted}; -sleep(1); +sleep 1; qx{../src/task rc:local.rc add right_deleted}; -sleep(1); +sleep 1; qx{../src/task rc:local.rc add left_completed}; -sleep(1); +sleep 1; qx{../src/task rc:local.rc add right_completed}; -sleep(1); +sleep 1; -copy("local/undo.data", "remote/undo.data") or fail("copy local/undo.data to remote/undo.data"); -copy("local/pending.data", "remote/pending.data") or fail("copy local/undo.data to remote/undo.data"); -copy("local/completed.data", "remote/completed.data") or fail("copy local/undo.data to remote/undo.data"); +copy ("local/undo.data", "remote/undo.data") or fail ("copy local/undo.data to remote/undo.data"); +copy ("local/pending.data", "remote/pending.data") or fail ("copy local/undo.data to remote/undo.data"); +copy ("local/completed.data", "remote/completed.data") or fail ("copy local/undo.data to remote/undo.data"); # make local modifications qx{../src/task rc:local.rc add left_added}; #left_added -sleep(1); +sleep 1; qx{../src/task rc:local.rc 1 modify prio:H}; #left_modified -sleep(1); +sleep 1; qx{../src/task rc:local.rc 3 modify +stay}; #left_newer -sleep(1); +sleep 1; qx{../src/task rc:local.rc 4 modify project:test}; #right_newer -sleep(1); +sleep 1; qx{../src/task rc:local.rc 6 modify +delete}; #right_deleted -sleep(1); +sleep 1; # make remote modifications qx{../src/task rc:remote.rc add right_added}; #right_added -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 2 modify prio:L}; #right_modified -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 2 modify wait:tomorrow}; #right_modified -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 4 modify proj:realProject}; #right_newer -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 5 modify project:deletion}; #left_deleted -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 8 done}; #right_completed -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 6 del}; #right_deleted -sleep(1); +sleep 1; qx{../src/task rc:remote.rc 3 done}; #left_newer -sleep(1); +sleep 1; # make new local modifications qx{../src/task rc:local.rc 3 start}; #left_newer -sleep(1); +sleep 1; qx{../src/task rc:local.rc 4 modify +car}; #right_newer -sleep(1); +sleep 1; qx{../src/task rc:local.rc 7 done}; #left_completed -sleep(1); +sleep 1; qx{../src/task rc:local.rc 5 del}; #left_deleted -sleep(1); +sleep 1; # make new remote modifications qx{../src/task rc:remote.rc 4 modify +gym}; # right_newer # merge remote into local -copy("local/undo.data", "local/undo.save") or fail("copy local/undo.data to local/undo.save"); +copy ("local/undo.data", "local/undo.save") or fail ("copy local/undo.data to local/undo.save"); my $output_l = qx{../src/task rc:local.rc merge remote/}; #check output @@ -151,37 +152,37 @@ unlike ($output_r, qr/Missing/, "remote-merge: no missing entry") unlike ($output_r, qr/Not adding duplicate/, "remote-merge: no duplicates"); # check reports -my $report_l = qx{../src/task rc:local.rc}; -my $report_r = qx{../src/task rc:remote.rc}; +my $report_l = qx{../src/task rc:local.rc list}; +my $report_r = qx{../src/task rc:remote.rc list}; # local-merge -like ($report_l, qr/left_added/, "local-merge: left_added is present"); -like ($report_l, qr/right_added/, "local-merge: right_added is present"); -like ($report_l, qr/H.*left_modified/, "local-merge: left_modified ok"); -like ($report_l, qr/\*.*left_newer.*stay/, "local-merge: left_newer ok"); +like ($report_l, qr/left_added/, "local-merge: left_added is present"); +like ($report_l, qr/right_added/, "local-merge: right_added is present"); +like ($report_l, qr/H.*left_modified/, "local-merge: left_modified ok"); +like ($report_l, qr/\*.*left_newer.*stay/, "local-merge: left_newer ok"); like ($report_l, qr/realProject.*right_newer.*gym/, "local-merge: right_newer ok"); -$report_l = qx{../src/task rc:local.rc export.csv}; -like ($report_l, qr/deleted.*left_deleted/, "local-merge: left_deleted ok"); -like ($report_l, qr/deleted.*right_deleted/, "local-merge: right_deleted ok"); -like ($report_l, qr/completed.*left_completed/, "local-merge: left_completed ok"); -like ($report_l, qr/completed.*right_completed/, "local-merge: right_completed ok"); +$report_l = qx{../src/task rc:local.rc export}; +like ($report_l, qr/deleted.*left_deleted/, "local-merge: left_deleted ok"); +like ($report_l, qr/deleted.*right_deleted/, "local-merge: right_deleted ok"); +like ($report_l, qr/completed.*left_completed/, "local-merge: left_completed ok"); +like ($report_l, qr/completed.*right_completed/, "local-merge: right_completed ok"); $report_l = qx(../src/task rc:local.rc waiting); -like ($report_l, qr/L.*right_modified/, "local-merge: right_modified ok"); +like ($report_l, qr/L.*right_modified/, "local-merge: right_modified ok"); # remote-merge -like ($report_r, qr/left_added/, "remote-merge: left_added is present"); -like ($report_r, qr/right_added/, "remote-merge: right_added is present"); -like ($report_r, qr/H.*left_modified/, "remote-merge: left_modified ok"); -like ($report_r, qr/\*.*left_newer.*stay/, "remote-merge: left_newer ok"); +like ($report_r, qr/left_added/, "remote-merge: left_added is present"); +like ($report_r, qr/right_added/, "remote-merge: right_added is present"); +like ($report_r, qr/H.*left_modified/, "remote-merge: left_modified ok"); +like ($report_r, qr/\*.*left_newer.*stay/, "remote-merge: left_newer ok"); like ($report_r, qr/realProject.*right_newer.*gym/, "remote-merge: right_newer ok"); -$report_r = qx{../src/task rc:remote.rc export.csv}; -like ($report_r, qr/deleted.*left_deleted/, "remote-merge: left_deleted ok"); -like ($report_r, qr/deleted.*right_deleted/, "remote-merge: right_deleted ok"); -like ($report_r, qr/completed.*left_completed/, "remote-merge: left_completed ok"); -like ($report_r, qr/completed.*right_completed/, "remote-merge: right_completed ok"); +$report_r = qx{../src/task rc:remote.rc export}; +like ($report_r, qr/deleted.*left_deleted/, "remote-merge: left_deleted ok"); +like ($report_r, qr/deleted.*right_deleted/, "remote-merge: right_deleted ok"); +like ($report_r, qr/completed.*left_completed/, "remote-merge: left_completed ok"); +like ($report_r, qr/completed.*right_completed/, "remote-merge: right_completed ok"); $report_r = qx(../src/task rc:remote.rc waiting); like ($report_r, qr/L.*right_modified/, "remote-merge: right_modified ok"); @@ -228,49 +229,28 @@ if (open my $fh, 'remote/undo.data') { ok ($good, "remote-merge: timestamps ok"); # Cleanup. -unlink 'local/pending.data'; -ok (!-r 'local/pending.data', 'Removed local/pending.data'); +unlink qw(local/pending.data local/completed.data local/undo.data local/undo.save local/backlog.data local/synch.key local.rc); +ok (! -r 'local/pending.data' && + ! -r 'local/completed.data' && + ! -r 'local/undo.data' && + ! -r 'local/undo.save' && + ! -r 'local/backlog.data' && + ! -r 'local/synch_key.data' && + ! -r 'local.rc', 'Local Cleanup'); -unlink 'local/completed.data'; -ok (!-r 'local/completed.data', 'Removed local/completed.data'); +unlink qw(remote/pending.data remote/completed.data remote/undo.data remote/backlog.data remote/synch.key remote.rc); +ok (! -r 'remote/pending.data' && + ! -r 'remote/completed.data' && + ! -r 'remote/undo.data' && + ! -r 'remote/backlog.data' && + ! -r 'remote/synch_key.data' && + ! -r 'remote.rc', 'Remove Cleanup'); -unlink 'local/undo.data'; -ok (!-r 'local/undo.data', 'Removed local/undo.data'); - -unlink 'local/backlog.data'; -ok (!-r 'local/backlog.data', 'Removed local/backlog.data'); - -unlink 'local/synch.key'; -ok (!-r 'local/synch.key', 'Removed local/synch.key'); - -unlink 'local/undo.save'; -ok (!-r 'local/undo.save', 'Removed local/undo.save'); - -unlink 'local.rc'; -ok (!-r 'local.rc', 'Removed local.rc'); - -unlink 'remote/pending.data'; -ok (!-r 'remote/pending.data', 'Removed remote/pending.data'); - -unlink 'remote/completed.data'; -ok (!-r 'remote/completed.data', 'Removed remote/completed.data'); - -unlink 'remote/undo.data'; -ok (!-r 'remote/undo.data', 'Removed remote/undo.data'); - -unlink 'remote/backlog.data'; -ok (!-r 'remote/backlog.data', 'Removed remote/backlog.data'); - -unlink 'remote/synch.key'; -ok (!-r 'remote/synch.key', 'Removed remote/synch.key'); - -unlink 'remote.rc'; -ok (!-r 'remote.rc', 'Removed remote.rc'); - -rmdir("remote"); -ok (!-e "remote", "Removed dir remote"); -rmdir("local"); -ok (!-e "local", "Removed dir local"); +rmtree (['remote/extensions', 'remote', 'local/extensions', 'local'], 0, 1); +ok (! -e 'remote/extensions' && + ! -e 'remote' && + ! -e 'local/extensions' && + ! -e 'local', 'Removed dir local'); exit 0;