//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2014, Paul Beckingham, Federico Hernandez. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // http://www.opensource.org/licenses/mit-license.php // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern Context context; //////////////////////////////////////////////////////////////////////////////// TF2::TF2 () : _read_only (false) , _dirty (false) , _loaded_tasks (false) , _loaded_lines (false) , _has_ids (false) , _auto_dep_scan (false) { } //////////////////////////////////////////////////////////////////////////////// TF2::~TF2 () { if (_dirty && context.verbose ("debug")) std::cout << format (STRING_TDB2_DIRTY_EXIT, _file) << "\n"; } //////////////////////////////////////////////////////////////////////////////// void TF2::target (const std::string& f) { _file = File (f); // A missing file is not considered unwritable. _read_only = false; if (_file.exists () && ! _file.writable ()) _read_only = true; } //////////////////////////////////////////////////////////////////////////////// const std::vector & TF2::get_tasks () { if (! _loaded_tasks) load_tasks (); return _tasks; } //////////////////////////////////////////////////////////////////////////////// const std::vector & TF2::get_lines () { if (! _loaded_lines) load_lines (); return _lines; } //////////////////////////////////////////////////////////////////////////////// // Locate task by id. bool TF2::get (int id, Task& task) { if (! _loaded_tasks) load_tasks (); // This is an optimization. Since the 'id' is based on the line number of // pending.data file, the task in question cannot appear earlier than line // (id - 1) in the file. It can, however, appear significantly later because // it is not known how recent a GC operation was run. for (int i = id - 1; i < _tasks.size (); ++i) { if (_tasks[i].id == id) { task = _tasks[i]; return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// // Locate task by uuid. bool TF2::get (const std::string& uuid, Task& task) { if (! _loaded_tasks) load_tasks (); std::vector ::iterator i; for (i = _tasks.begin (); i != _tasks.end (); ++i) { if (i->get ("uuid") == uuid) { task = *i; return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// bool TF2::has (const std::string& uuid) { if (! _loaded_tasks) load_tasks (); std::vector ::iterator i; for (i = _tasks.begin (); i != _tasks.end (); ++i) if (i->get ("uuid") == uuid) return true; return false; } //////////////////////////////////////////////////////////////////////////////// void TF2::add_task (Task& task) { _tasks.push_back (task); // For subsequent queries _added_tasks.push_back (task); // For commit/synch Task::status status = task.getStatus (); if (task.id == 0 && (status == Task::pending || status == Task::recurring || status == Task::waiting)) { task.id = context.tdb2.next_id (); } _I2U[task.id] = task.get ("uuid"); _U2I[task.get ("uuid")] = task.id; _dirty = true; } //////////////////////////////////////////////////////////////////////////////// bool TF2::modify_task (const Task& task) { // Modify in-place. std::string uuid = task.get ("uuid"); std::vector ::iterator i; for (i = _tasks.begin (); i != _tasks.end (); ++i) { if (i->get ("uuid") == uuid) { *i = task; _modified_tasks.push_back (task); _dirty = true; return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// void TF2::add_line (const std::string& line) { _lines.push_back (line); _added_lines.push_back (line); _dirty = true; } //////////////////////////////////////////////////////////////////////////////// void TF2::clear_tasks () { _tasks.clear (); _dirty = true; } //////////////////////////////////////////////////////////////////////////////// void TF2::clear_lines () { _lines.clear (); _dirty = true; } //////////////////////////////////////////////////////////////////////////////// // Top-down recomposition. void TF2::commit () { // The _dirty flag indicates that the file needs to be written. if (_dirty) { // Special case: added but no modified means just append to the file. if (!_modified_tasks.size () && (_added_tasks.size () || _added_lines.size ())) { if (_file.open ()) { if (context.config.getBoolean ("locking")) _file.waitForLock (); // Write out all the added tasks. std::vector ::iterator task; for (task = _added_tasks.begin (); task != _added_tasks.end (); ++task) { _file.append (task->composeF4 () + "\n"); } _added_tasks.clear (); // Write out all the added lines. std::vector ::iterator line; for (line = _added_lines.begin (); line != _added_lines.end (); ++line) { _file.append (*line); } _added_lines.clear (); _file.close (); _dirty = false; } } else { if (_file.open ()) { if (context.config.getBoolean ("locking")) _file.waitForLock (); // Truncate the file and rewrite. _file.truncate (); // Only write out _tasks, because any deltas have already been applied. std::vector ::iterator task; for (task = _tasks.begin (); task != _tasks.end (); ++task) { _file.append (task->composeF4 () + "\n"); } // Write out all the added lines. std::vector ::iterator line; for (line = _added_lines.begin (); line != _added_lines.end (); ++line) { _file.append (*line); } _added_lines.clear (); _file.close (); _dirty = false; } } } } //////////////////////////////////////////////////////////////////////////////// void TF2::load_tasks () { context.timer_load.start (); if (! _loaded_lines) { load_lines (); // Apply previously added lines. std::vector ::iterator i; for (i = _added_lines.begin (); i != _added_lines.end (); ++i) _lines.push_back (*i); } int line_number = 0; try { // Reduce unnecessary allocations/copies. _tasks.reserve (_lines.size ()); std::vector ::iterator i; for (i = _lines.begin (); i != _lines.end (); ++i) { ++line_number; Task task (*i); // Some tasks gets an ID. if (_has_ids) task.id = context.tdb2.next_id (); _tasks.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. if (task.id) { _I2U[task.id] = task.get ("uuid"); _U2I[task.get ("uuid")] = task.id; } } if (_auto_dep_scan) dependency_scan (); _loaded_tasks = true; } catch (const std::string& e) { throw e + format (STRING_TDB2_PARSE_ERROR, _file._data, line_number); } context.timer_load.stop (); } //////////////////////////////////////////////////////////////////////////////// void TF2::load_lines () { if (_file.open ()) { if (context.config.getBoolean ("locking")) _file.waitForLock (); _file.read (_lines); _file.close (); _loaded_lines = true; } } //////////////////////////////////////////////////////////////////////////////// std::string TF2::uuid (int id) { if (! _loaded_tasks) { load_tasks (); // Apply previously added tasks. std::vector ::iterator i; for (i = _added_tasks.begin (); i != _added_tasks.end (); ++i) _tasks.push_back (*i); } std::map ::const_iterator i; if ((i = _I2U.find (id)) != _I2U.end ()) return i->second; return ""; } //////////////////////////////////////////////////////////////////////////////// int TF2::id (const std::string& uuid) { if (! _loaded_tasks) { load_tasks (); // Apply previously added tasks. std::vector ::iterator i; for (i = _added_tasks.begin (); i != _added_tasks.end (); ++i) _tasks.push_back (*i); } std::map ::const_iterator i; if ((i = _U2I.find (uuid)) != _U2I.end ()) return i->second; return 0; } //////////////////////////////////////////////////////////////////////////////// void TF2::has_ids () { _has_ids = true; } //////////////////////////////////////////////////////////////////////////////// void TF2::auto_dep_scan () { _auto_dep_scan = true; } //////////////////////////////////////////////////////////////////////////////// // Completely wipe it all clean. void TF2::clear () { _read_only = false; _dirty = false; _loaded_tasks = false; _loaded_lines = false; // Note that these are deliberately not cleared. //_file._data = ""; //_has_ids = false; //_auto_dep_scan = false; _tasks.clear (); _added_tasks.clear (); _modified_tasks.clear (); _lines.clear (); _added_lines.clear (); _I2U.clear (); _U2I.clear (); } //////////////////////////////////////////////////////////////////////////////// // For any task that has depenencies, follow the chain of dependencies until the // end. Along the way, update the Task::is_blocked and Task::is_blocking data // cache. void TF2::dependency_scan () { // Iterate and modify TDB2 in-place. Don't do this at home. std::vector ::iterator left; for (left = _tasks.begin (); left != _tasks.end (); ++left) { if (left->has ("depends")) { std::vector deps; left->getDependencies (deps); std::vector ::iterator d; for (d = deps.begin (); d != deps.end (); ++d) { std::vector ::iterator right; for (right = _tasks.begin (); right != _tasks.end (); ++right) { if (right->get ("uuid") == *d) { Task::status status = right->getStatus (); if (status != Task::completed && status != Task::deleted) { left->is_blocked = true; right->is_blocking = true; } break; } } } } } } //////////////////////////////////////////////////////////////////////////////// const std::string TF2::dump () { Color red ("rgb500 on rgb100"); Color yellow ("rgb550 on rgb220"); Color green ("rgb050 on rgb010"); // File label. std::string label; std::string::size_type slash = _file._data.rfind ('/'); if (slash != std::string::npos) label = rightJustify (_file._data.substr (slash + 1), 14); // File mode. std::string mode = std::string (_file.readable () ? "r" : "-") + std::string (_file.writable () ? "w" : "-"); if (mode == "r-") mode = red.colorize (mode); else if (mode == "rw") mode = green.colorize (mode); else mode = yellow.colorize (mode); // Hygiene. std::string hygiene = _dirty ? red.colorize ("O") : green.colorize ("-"); std::string tasks = green.colorize (rightJustifyZero ((int) _tasks.size (), 4)); std::string tasks_added = red.colorize (rightJustifyZero ((int) _added_tasks.size (), 3)); std::string tasks_modified = yellow.colorize (rightJustifyZero ((int) _modified_tasks.size (), 3)); std::string lines = green.colorize (rightJustifyZero ((int) _lines.size (), 4)); std::string lines_added = red.colorize (rightJustifyZero ((int) _added_lines.size (), 3)); char buffer[256]; // Composed string is actually 246 bytes. Yikes. snprintf (buffer, 256, "%14s %s %s T%s+%s~%s L%s+%s", label.c_str (), mode.c_str (), hygiene.c_str (), tasks.c_str (), tasks_added.c_str (), tasks_modified.c_str (), lines.c_str (), lines_added.c_str ()); return std::string (buffer); } //////////////////////////////////////////////////////////////////////////////// TDB2::TDB2 () : _location ("") , _id (1) { // Mark the pending file as the only one that has ID numbers. pending.has_ids (); // Indicate that dependencies should be automatically scanned on startup, // setting Task::is_blocked and Task::is_blocking accordingly. pending.auto_dep_scan (); } //////////////////////////////////////////////////////////////////////////////// // Deliberately no file writes on destruct. Commit should have been already // called, if data is to be preserved. TDB2::~TDB2 () { } //////////////////////////////////////////////////////////////////////////////// // Once a location is known, the files can be set up. Note that they are not // read. void TDB2::set_location (const std::string& location) { _location = location; pending.target (location + "/pending.data"); completed.target (location + "/completed.data"); undo.target (location + "/undo.data"); backlog.target (location + "/backlog.data"); } //////////////////////////////////////////////////////////////////////////////// // Add the new task to the appropriate file. void TDB2::add (Task& task, bool add_to_backlog /* = true */) { // Ensure the task is consistent, and provide defaults if necessary. task.validate (); // If the tasks are loaded, then verify that this uuid is not already in // the file. if (!verifyUniqueUUID (task.get ("uuid"))) throw format (STRING_TDB2_UUID_NOT_UNIQUE, task.get ("uuid")); // Add new task to either pending or completed. std::string status = task.get ("status"); if (status == "completed" || status == "deleted") completed.add_task (task); else pending.add_task (task); // Add undo data lines: // time