//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2015, 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; bool TDB2::debug_mode = false; //////////////////////////////////////////////////////////////////////////////// TF2::TF2 () : _read_only (false) , _dirty (false) , _loaded_tasks (false) , _loaded_lines (false) , _has_ids (false) , _auto_dep_scan (false) { } //////////////////////////////////////////////////////////////////////////////// TF2::~TF2 () { if (_dirty && TDB2::debug_mode) std::cout << format (STRING_TDB2_DIRTY_EXIT, std::string (_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 (unsigned int i = id - 1; i < _tasks.size (); ++i) { if (_tasks[i].id == id) { task = _tasks[i]; return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// // Locate task by uuid, which may be a partial UUID. bool TF2::get (const std::string& uuid, Task& task) { if (! _loaded_tasks) load_tasks (); if (_tasks_map.size () > 0 && uuid.size () == 36) { // Fast lookup, same result as below. Only used during "task import". auto i = _tasks_map.find (uuid); if (i != _tasks_map.end ()) { task = i->second; return true; } } else { // Slow lookup, same result as above. for (auto& i : _tasks) { if (closeEnough (i.get ("uuid"), uuid, uuid.length ())) { task = i; return true; } } } return false; } //////////////////////////////////////////////////////////////////////////////// bool TF2::has (const std::string& uuid) { if (! _loaded_tasks) load_tasks (); for (auto& i : _tasks) 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 // For faster lookup if (context.cli2.getCommand () == "import") _tasks_map.insert (std::pair (task.get("uuid"), task)); 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) { std::string uuid = task.get ("uuid"); if (context.cli2.getCommand () == "import") { // Update map used for faster lookup auto i = _tasks_map.find (uuid); if (i != _tasks_map.end ()) { i->second = task; } } for (auto& i : _tasks) { if (i.get ("uuid") == uuid) { // Modify in-place. 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.lock (); // Write out all the added tasks. for (auto& task : _added_tasks) _file.append (task.composeF4 () + "\n"); _added_tasks.clear (); // Write out all the added lines. for (auto& line : _added_lines) _file.append (line); _added_lines.clear (); _file.close (); _dirty = false; } } else { if (_file.open ()) { if (context.config.getBoolean ("locking")) _file.lock (); // Truncate the file and rewrite. _file.truncate (); // Only write out _tasks, because any deltas have already been applied. for (auto& task : _tasks) _file.append (task.composeF4 () + "\n"); // Write out all the added lines. for (auto& line : _added_lines) _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. for (auto& line : _added_lines) _lines.push_back (line); } int line_number = 0; try { // Reduce unnecessary allocations/copies. _tasks.reserve (_lines.size ()); for (auto& line : _lines) { ++line_number; Task task (line); // Some tasks get an ID. if (_has_ids) { Task::status status = task.getStatus (); // Completed / deleted tasks in pending.data get an ID if GC is off. if (! context.run_gc || (status != Task::completed && status != Task::deleted)) task.id = context.tdb2.next_id (); } _tasks.push_back (task); if (context.cli2.getCommand () == "import") // For faster lookup only _tasks_map.insert (std::pair (task.get("uuid"), 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.lock (); _file.read (_lines); _file.close (); _loaded_lines = true; } } //////////////////////////////////////////////////////////////////////////////// std::string TF2::uuid (int id) { if (! _loaded_tasks) { load_tasks (); // Apply previously added tasks. for (auto& task : _added_tasks) _tasks.push_back (task); } auto i = _I2U.find (id); if (i != _I2U.end ()) return i->second; return ""; } //////////////////////////////////////////////////////////////////////////////// int TF2::id (const std::string& uuid) { if (! _loaded_tasks) { load_tasks (); // Apply previously added tasks. for (auto& task : _added_tasks) _tasks.push_back (task); } auto i = _U2I.find (uuid); if (i != _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. for (auto& left : _tasks) { if (left.has ("depends")) { std::vector deps; left.getDependencies (deps); for (auto& dep : deps) { for (auto& right : _tasks) { if (right.get ("uuid") == dep) { // GC hasn't run yet, check both tasks for their current status Task::status lstatus = left.getStatus (); Task::status rstatus = right.getStatus (); if (lstatus != Task::completed && lstatus != Task::deleted && rstatus != Task::completed && rstatus != Task::deleted) { left.is_blocked = true; right.is_blocking = true; } // Only want to break out of the "right" loop. 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; auto 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. // bool argument to validate() is "applyDefault". Pass add_to_backlog through // in order to not apply defaults to synchronized tasks. task.validate (add_to_backlog); std::string uuid = task.get ("uuid"); // If the tasks are loaded, then verify that this uuid is not already in // the file. if (!verifyUniqueUUID (uuid)) throw format (STRING_TDB2_UUID_NOT_UNIQUE, uuid); // Only locally-added tasks trigger hooks. This means that tasks introduced // via 'sync' do not trigger hooks. if (add_to_backlog) context.hooks.onAdd (task); update (task, add_to_backlog, true); } //////////////////////////////////////////////////////////////////////////////// void TDB2::modify (Task& task, bool add_to_backlog /* = true */) { // Ensure the task is consistent. task.validate (false); std::string uuid = task.get ("uuid"); // Get the unmodified task as reference, so the hook can compare. if (add_to_backlog) { Task original; get (uuid, original); context.hooks.onModify (original, task); } update (task, add_to_backlog); } //////////////////////////////////////////////////////////////////////////////// void TDB2::update ( Task& task, const bool add_to_backlog, const bool addition /* = false */) { // Validate to add metadata. task.validate (false); // If the task already exists, it is a modification, else addition. Task original; if (not addition && get (task.get ("uuid"), original)) { // Update only if the tasks differ if (task == original) return; if (add_to_backlog) { // All locally modified tasks are timestamped, implicitly overwriting any // changes the user or hooks tried to apply to the "modified" attribute. task.setAsNow ("modified"); } // Update the task, wherever it is. if (!pending.modify_task (task)) completed.modify_task (task); // time