//////////////////////////////////////////////////////////////////////////////// // task - a command line task list manager. // // Copyright 2006 - 2010, Paul Beckingham. // All rights reserved. // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software // Foundation; either version 2 of the License, or (at your option) any later // version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more // details. // // You should have received a copy of the GNU General Public License along with // this program; if not, write to the // // Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, // Boston, MA // 02110-1301 // USA // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include "text.h" #include "util.h" #include "TDB.h" #include "Directory.h" #include "File.h" #include "Table.h" #include "Timer.h" #include "Color.h" #include "main.h" #define NDEBUG #include "assert.h" #include "Taskmod.h" #define DEBUG_OUTPUT 0 #if DEBUG_OUTPUT > 0 #define DEBUG_STR(str) std::cout << "DEBUG: " << str << std::endl; std::cout.flush() #define DEBUG_STR_PART(str) std::cout << "DEBUG: " << str; std::cout.flush() #define DEBUG_STR_END(str) std::cout << str << std::endl; 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 ("Failed to convert \"" + stream.str() + "\" to integer: " + tmod_tmp.getTimeStr() + "."); } // '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. // There should be only one commit. // // +- TDB::TDB // | // | +- TDB::lock // | | open // | | [lock] // | | // | | +- TDB::load (Filter) // | | | read all // | | | apply filter // | | | return subset // | | | // | | +- TDB::add (T) // | | | // | | +- TDB::update (T) // | | | // | | +- TDB::commit // | | | write all // | | | // | | +- TDB::undo // | | // | +- TDB::unlock // | [unlock] // | close // | // +- TDB::~TDB // [TDB::unlock] // TDB::TDB () : mLock (true) , mAllOpenAndLocked (false) , mId (1) { } //////////////////////////////////////////////////////////////////////////////// TDB::~TDB () { if (mAllOpenAndLocked) unlock (); } //////////////////////////////////////////////////////////////////////////////// void TDB::clear () { mLocations.clear (); mLock = true; if (mAllOpenAndLocked) unlock (); mAllOpenAndLocked = false; mId = 1; mPending.clear (); mNew.clear (); mCompleted.clear (); mModified.clear (); } //////////////////////////////////////////////////////////////////////////////// void TDB::location (const std::string& path) { Directory d (path); if (!d.exists ()) throw std::string ("Data location '") + path + "' does not exist, or is not readable and writable."; mLocations.push_back (Location (d)); } //////////////////////////////////////////////////////////////////////////////// void TDB::lock (bool lockFile /* = true */) { if (context.hooks.trigger ("pre-file-lock")) { mLock = lockFile; mPending.clear (); mNew.clear (); mCompleted.clear (); mModified.clear (); foreach (location, mLocations) { location->pending = openAndLock (location->path + "/pending.data"); location->completed = openAndLock (location->path + "/completed.data"); location->undo = openAndLock (location->path + "/undo.data"); } mAllOpenAndLocked = true; context.hooks.trigger ("post-file-lock"); } } //////////////////////////////////////////////////////////////////////////////// void TDB::unlock () { // Do not clear out these items, as they may be used in a read-only fashion. // mPending.clear (); // mNew.clear (); // mModified.clear (); foreach (location, mLocations) { fflush (location->pending); fclose (location->pending); location->pending = NULL; fflush (location->completed); fclose (location->completed); location->completed = NULL; fflush (location->undo); fclose (location->undo); location->completed = NULL; } mAllOpenAndLocked = false; } //////////////////////////////////////////////////////////////////////////////// // Returns number of filtered tasks. // Note: tasks.clear () is deliberately not called, to allow the combination of // multiple files. int TDB::load (std::vector & tasks, Filter& filter) { #ifdef FEATURE_TDB_OPT // Special optimization: if the filter contains Att ('status', '', 'pending'), // 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; } } #endif loadPending (tasks, filter); #ifdef FEATURE_TDB_OPT if (numberStatusClauses == 0 || numberStatusClauses != numberSimpleStatusClauses) loadCompleted (tasks, filter); else context.debug ("load optimization short circuit"); #else loadCompleted (tasks, filter); #endif return tasks.size (); } //////////////////////////////////////////////////////////////////////////////// // Returns number of filtered tasks. // Note: tasks.clear () is deliberately not called, to allow the combination of // multiple files. int TDB::loadPending (std::vector & tasks, Filter& filter) { Timer t ("TDB::loadPending"); std::string file; int line_number = 1; try { // Only load if not already loaded. if (mPending.size () == 0) { mId = 1; char line[T_LINE_MAX]; foreach (location, mLocations) { line_number = 1; file = location->path + "/pending.data"; fseek (location->pending, 0, SEEK_SET); while (fgets (line, T_LINE_MAX, location->pending)) { int length = strlen (line); if (length > 3) // []\n { // TODO Add hidden attribute indicating source? Task task (line); task.id = mId++; mPending.push_back (task); // Maintain mapping for ease of link/dependency resolution. // Note that this mapping is not restricted by the filter, and is // therefore a complete set. mI2U[task.id] = task.get ("uuid"); mU2I[task.get ("uuid")] = task.id; } ++line_number; } } } // Now filter and return. if (filter.size ()) { foreach (task, mPending) if (filter.pass (*task)) tasks.push_back (*task); } else { foreach (task, mPending) tasks.push_back (*task); } // Hand back any accumulated additions, if TDB::loadPending is being called // repeatedly. int fakeId = mId; if (filter.size ()) { foreach (task, mNew) { task->id = fakeId++; if (filter.pass (*task)) tasks.push_back (*task); } } else { foreach (task, mNew) { task->id = fakeId++; tasks.push_back (*task); } } } catch (std::string& e) { std::stringstream s; s << " in " << file << " at line " << line_number; throw e + s.str (); } return tasks.size (); } //////////////////////////////////////////////////////////////////////////////// // Returns number of filtered tasks. // Note: tasks.clear () is deliberately not called, to allow the combination of // multiple files. int TDB::loadCompleted (std::vector & tasks, Filter& filter) { Timer t ("TDB::loadCompleted"); std::string file; int line_number = 1; try { if (mCompleted.size () == 0) { char line[T_LINE_MAX]; foreach (location, mLocations) { line_number = 1; file = location->path + "/completed.data"; fseek (location->completed, 0, SEEK_SET); while (fgets (line, T_LINE_MAX, location->completed)) { int length = strlen (line); if (length > 3) // []\n { // TODO Add hidden attribute indicating source? Task task (line); task.id = 0; // Need a value, just not a valid value. mCompleted.push_back (task); } ++line_number; } } } // Now filter and return. if (filter.size ()) { foreach (task, mCompleted) if (filter.pass (*task)) tasks.push_back (*task); } else { foreach (task, mCompleted) tasks.push_back (*task); } } catch (std::string& e) { std::stringstream s; s << " in " << file << " at line " << line_number; throw e + s.str (); } return tasks.size (); } //////////////////////////////////////////////////////////////////////////////// const std::vector & TDB::getAllPending () { return mPending; } //////////////////////////////////////////////////////////////////////////////// const std::vector & TDB::getAllNew () { return mNew; } //////////////////////////////////////////////////////////////////////////////// const std::vector & TDB::getAllCompleted () { return mCompleted; } //////////////////////////////////////////////////////////////////////////////// const std::vector & TDB::getAllModified () { return mModified; } //////////////////////////////////////////////////////////////////////////////// // Note: mLocations[0] is where all tasks are written. void TDB::add (const Task& task) { mNew.push_back (task); mI2U[task.id] = task.get ("uuid"); mU2I[task.get ("uuid")] = task.id; } //////////////////////////////////////////////////////////////////////////////// void TDB::update (const Task& task) { mModified.push_back (task); } //////////////////////////////////////////////////////////////////////////////// // Interestingly, only the pending file gets written to. The completed file is // only modified by TDB::gc. int TDB::commit () { Timer t ("TDB::commit"); context.hooks.trigger ("pre-commit"); int quantity = mNew.size () + mModified.size (); // This is an optimization. If there are only new tasks, and none were // modified, simply seek to the end of pending and write. if (mNew.size () && ! mModified.size ()) { fseek (mLocations[0].pending, 0, SEEK_END); foreach (task, mNew) { mPending.push_back (*task); fputs (task->composeF4 ().c_str (), mLocations[0].pending); } fseek (mLocations[0].undo, 0, SEEK_END); foreach (task, mNew) writeUndo (*task, mLocations[0].undo); mNew.clear (); context.hooks.trigger ("post-commit"); return quantity; } // The alternative is to potentially rewrite both files. else if (mNew.size () || mModified.size ()) { // allPending is a copy of mPending, with all modifications included, and // new tasks appended. std::vector allPending; allPending = mPending; foreach (mtask, mModified) { foreach (task, allPending) { if (task->id == mtask->id) { *task = *mtask; goto next_mod; } } next_mod: ; } foreach (task, mNew) allPending.push_back (*task); // Write out all pending. if (fseek (mLocations[0].pending, 0, SEEK_SET) == 0) { if (ftruncate (fileno (mLocations[0].pending), 0)) throw std::string ("Failed to truncate pending.data file "); foreach (task, allPending) fputs (task->composeF4 ().c_str (), mLocations[0].pending); } // Update the undo log. if (fseek (mLocations[0].undo, 0, SEEK_END) == 0) { foreach (mtask, mModified) { foreach (task, mPending) { if (task->id == mtask->id) { writeUndo (*task, *mtask, mLocations[0].undo); goto next_mod2; } } next_mod2: ; } foreach (task, mNew) writeUndo (*task, mLocations[0].undo); } mPending = allPending; mModified.clear (); mNew.clear (); } context.hooks.trigger ("post-commit"); return quantity; } //////////////////////////////////////////////////////////////////////////////// // Scans the pending tasks for any that are completed or deleted, and if so, // moves them to the completed.data file. Returns a count of tasks moved. // Now reverts expired waiting tasks to pending. int TDB::gc () { Timer t ("TDB::gc"); int count_pending_changes = 0; int count_completed_changes = 0; Date now; if (mNew.size ()) throw std::string ("Unexpected new tasks found during gc."); if (mModified.size ()) throw std::string ("Unexpected modified tasks found during gc."); lock (); Filter filter; std::vector ignore; loadPending (ignore, filter); // Now move completed and deleted tasks from the pending list to the // completed list. Isn't garbage collection easy? std::vector still_pending; std::vector newly_completed; foreach (task, mPending) { Task::status s = task->getStatus (); if (s == Task::completed || s == Task::deleted) { newly_completed.push_back (*task); ++count_pending_changes; // removal ++count_completed_changes; // addition } else if (s == Task::waiting) { // Wake up tasks that need to be woken. Date wait_date (atoi (task->get ("wait").c_str ())); if (now > wait_date) { task->setStatus (Task::pending); task->remove ("wait"); ++count_pending_changes; // modification } still_pending.push_back (*task); } else still_pending.push_back (*task); } // No commit - all updates performed manually. if (count_pending_changes > 0) { if (fseek (mLocations[0].pending, 0, SEEK_SET) == 0) { if (ftruncate (fileno (mLocations[0].pending), 0)) throw std::string ("Failed to truncate pending.data file "); foreach (task, still_pending) fputs (task->composeF4 ().c_str (), mLocations[0].pending); // Update cached copy. mPending = still_pending; } } // Append the new_completed tasks to completed.data. No need to write out the // whole list. if (count_completed_changes > 0) { fseek (mLocations[0].completed, 0, SEEK_END); foreach (task, newly_completed) fputs (task->composeF4 ().c_str (), mLocations[0].completed); } // Close files. unlock (); std::stringstream s; s << "gc " << (count_pending_changes + count_completed_changes) << " tasks"; context.debug (s.str ()); return count_pending_changes + count_completed_changes; } //////////////////////////////////////////////////////////////////////////////// int TDB::nextId () { return mId++; } //////////////////////////////////////////////////////////////////////////////// void TDB::undo () { context.hooks.trigger ("pre-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.config.get ("color.undo.before")); Color color_green (context.config.get ("color.undo.after")); if (context.config.get ("undo.style") == "side") { std::cout << std::endl << "The last modification was made " << lastChange.toString () << std::endl; // Attributes are all there is, so figure the different attribute names // between before and after. Table table; table.setTableWidth (context.getWidth ()); table.setTableIntraPadding (2); table.addColumn (" "); table.addColumn ("Prior Values"); table.addColumn ("Current Values"); table.setColumnUnderline (1); table.setColumnUnderline (2); table.setColumnWidth (0, Table::minimum); table.setColumnWidth (1, Table::flexible); table.setColumnWidth (2, Table::flexible); 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 = table.addRow (); table.addCell (row, 0, *name); table.addCell (row, 1, renderAttribute (*name, before.get (*name))); table.setCellColor (row, 1, color_red); } foreach (name, before) { std::string priorValue = before.get (name->first); std::string currentValue = after.get (name->first); if (currentValue != "") { row = table.addRow (); table.addCell (row, 0, name->first); table.addCell (row, 1, renderAttribute (name->first, priorValue)); table.addCell (row, 2, renderAttribute (name->first, currentValue)); if (priorValue != currentValue) { table.setCellColor (row, 1, color_red); table.setCellColor (row, 2, color_green); } } } foreach (name, afterOnly) { row = table.addRow (); table.addCell (row, 0, *name); table.addCell (row, 2, renderAttribute (*name, after.get (*name))); table.setCellColor (row, 2, color_green); } } else { int row; foreach (name, after) { row = table.addRow (); table.addCell (row, 0, name->first); table.addCell (row, 2, renderAttribute (name->first, after.get (name->first))); table.setCellColor (row, 2, color_green); } } std::cout << std::endl << table.render () << std::endl; } // 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. Table table; table.setTableWidth (context.getWidth ()); table.setTableIntraPadding (2); table.addColumn (" "); table.addColumn (" "); table.setColumnWidth (0, Table::minimum); table.setColumnWidth (1, Table::flexible); table.setColumnJustification (0, Table::right); table.setColumnJustification (1, Table::left); int row = table.addRow (); table.addCell (row, 0, "--- previous state"); table.addCell (row, 1, "Undo will restore this state"); table.setRowColor (row, color_red); row = table.addRow (); table.addCell (row, 0, "+++ current state "); // Note trailing space. table.addCell (row, 1, "Change made " + lastChange.toString (context.config.get ("dateformat"))); table.setRowColor (row, color_green); table.addRow (); // Add rows to table showing diffs. std::vector all; Att::allNames (all); // 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 = table.addRow (); // table.addCell (row, 0, *a + ":"); // table.addCell (row, 1, before_att); } // Attribute deleted. else if (before_att != "" && after_att == "") { row = table.addRow (); table.addCell (row, 0, "-" + *a + ":"); table.addCell (row, 1, before_att); table.setRowColor (row, color_red); row = table.addRow (); table.addCell (row, 0, "+" + *a + ":"); table.setRowColor (row, color_green); } // Attribute added. else if (before_att == "" && after_att != "") { row = table.addRow (); table.addCell (row, 0, "-" + *a + ":"); table.setRowColor (row, color_red); row = table.addRow (); table.addCell (row, 0, "+" + *a + ":"); table.addCell (row, 1, after_att); table.setRowColor (row, color_green); } // Attribute changed. else { row = table.addRow (); table.addCell (row, 0, "-" + *a + ":"); table.addCell (row, 1, before_att); table.setRowColor (row, color_red); row = table.addRow (); table.addCell (row, 0, "+" + *a + ":"); table.addCell (row, 1, after_att); table.setRowColor (row, color_green); } } } std::cout << std::endl << table.render () << std::endl; } // Output displayed, now confirm. if (!confirm ("The undo command is not reversible. Are you sure you want to revert to the previous state?")) { std::cout << "No changes made." << std::endl; context.hooks.trigger ("post-undo"); 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." << std::endl; } else { p.erase (task); std::cout << "Task removed." << std::endl; } // Rewrite files. File::write (pendingFile, p); File::write (undoFile, u); context.hooks.trigger ("post-undo"); 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." << std::endl; 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." << std::endl; context.debug ("TDB::undo - task belongs in completed.data"); } std::cout << "Undo complete." << std::endl; context.hooks.trigger ("post-undo"); 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." << std::endl << "No undo possible." << std::endl; } //////////////////////////////////////////////////////////////////////////////// void TDB::merge (const std::string& mergeFile) { /////////////////////////////////////// // Copyright 2010, Johannes Schlatow. /////////////////////////////////////// // list of modifications that we want to add to the local database std::list mods; // 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 transactions to apply."); // 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(); rline = *rit; 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); // found first matching lines? if (lline.compare(rline) == 0) break; } } // 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"); } } ///////////////////////////////////////////////////////////////////////////////////////// // 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 tasmods 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 << "Skipping the new local task " << (*lmod_it).getUuid() << std::endl; 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 the new remote task " << tmod.getUuid() << std::endl; 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"); DEBUG_STR("Merging modifications:"); // ------ 3. merge 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"); 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 << "Applying remote changes for uuid " << uuid << std::endl; 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 << "Rejecting remote changes for uuid " << uuid << std::endl; // inserting right mod into history of local database // so that it can be restored later // TODO feature: make rejected changes on the remote branch restorable // Taskmod reverse_tmod; // // tmod_r.setBefore((*lmod_rit).getAfter()); // tmod_r.setTimestamp((*lmod_rit).getTimestamp()+1); // // reverse_tmod.setAfter(tmod_r.getBefore()); // reverse_tmod.setBefore(tmod_r.getAfter()); // reverse_tmod.setTimestamp(tmod_r.getTimestamp()); // // mods.push_back(tmod_r); // mods.push_back(reverse_tmod); // 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(); } else if (rit == r.end()) { // nothing happend on the remote branch // local branch is up-to-date throw std::string ("Database is already up-to-date."); } else // lit == undo.end() { // nothing happend on the local branch std::cout << "No changes were made on the local database. Appending changes..." << std::endl; // add remaining lines (remote branch) to the list of modifications readTaskmods(r, rit, mods); } //////////////////////////////////////////////////////////// // redo command //////////////////////////////////////////////////////////// if (!mods.empty()) { std::cout << "Running redo routine..." << std::endl; 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) != std::string::npos) { // Update the completed record. std::cout << "Modifying " << uuid << std::endl; // 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) != std::string::npos) { // Update the pending record. std::cout << "Modifying " << uuid << std::endl; // 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 " << uuid << std::endl; 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) != std::string::npos) { found = true; break; } } if (!found) { std::cout << "Adding " << uuid << std::endl; pending.push_back (tmod.getAfter().composeF4()); } else std::cout << "Not adding duplicate " << uuid << std::endl; 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 + "'."; // 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) mods.merge(lmods); // generate undo.data format 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 + "'."; } else // nothing to be done { std::cout << "Nothing to be done." << std::endl; } // delete objects lmods.clear(); mods.clear(); } //////////////////////////////////////////////////////////////////////////////// std::string TDB::uuid (int id) const { std::map ::const_iterator i; if ((i = mI2U.find (id)) != mI2U.end ()) return i->second; return ""; } //////////////////////////////////////////////////////////////////////////////// int TDB::id (const std::string& uuid) const { std::map ::const_iterator i; if ((i = mU2I.find (uuid)) != mU2I.end ()) return i->second; return 0; } //////////////////////////////////////////////////////////////////////////////// FILE* TDB::openAndLock (const std::string& file) { // TODO Need provision here for read-only locations. // Check for access. File f (file); bool exists = f.exists (); if (exists) if (!f.readable () || !f.writable ()) throw std::string ("Task does not have the correct permissions for '") + file + "'."; // Open the file. FILE* in = fopen (file.c_str (), (exists ? "r+" : "w+")); if (!in) throw std::string ("Could not open '") + file + "'."; // Lock if desired. Try three times before failing. int retry = 0; if (mLock) while (flock (fileno (in), LOCK_NB | LOCK_EX) && ++retry <= 3) { std::cout << "Waiting for file lock..." << std::endl; while (flock (fileno (in), LOCK_NB | LOCK_EX) && ++retry <= 3) delay (0.2); } if (retry > 3) throw std::string ("Could not lock '") + file + "'."; return in; } //////////////////////////////////////////////////////////////////////////////// void TDB::writeUndo (const Task& after, FILE* file) { fprintf (file, "time %u\nnew %s---\n", (unsigned int) time (NULL), after.composeF4 ().c_str ()); } //////////////////////////////////////////////////////////////////////////////// void TDB::writeUndo (const Task& before, const Task& after, FILE* file) { fprintf (file, "time %u\nold %snew %s---\n", (unsigned int) time (NULL), before.composeF4 ().c_str (), after.composeF4 ().c_str ()); } ////////////////////////////////////////////////////////////////////////////////