From 9b3f565e908030b05454ccfc7fa5bd2b56f34f88 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Tue, 12 Apr 2011 23:47:38 -0400 Subject: [PATCH] Enhancements - Added stubbed DOM code. - Added stubbed TDB2 code. --- src/CMakeLists.txt | 30 +- src/Context.cpp | 2 + src/Context.h | 4 + src/DOM.cpp | 96 +++ src/DOM.h | 55 ++ src/TDB2.cpp | 1696 +++++++++++++++++++++++++++++++++++++++++++ src/TDB2.h | 112 +++ test/.gitignore | 1 + test/CMakeLists.txt | 1 + test/tdb2.t.cpp | 175 +++++ 10 files changed, 2157 insertions(+), 15 deletions(-) create mode 100644 src/DOM.cpp create mode 100644 src/DOM.h create mode 100644 src/TDB2.cpp create mode 100644 src/TDB2.h create mode 100644 test/tdb2.t.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 62a58fd87..bf745560b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,21 +3,21 @@ include_directories (${CMAKE_SOURCE_DIR}/src set (task_SRCS API.cpp API.h Att.cpp Att.h Cmd.cpp Cmd.h Color.cpp Color.h Config.cpp Config.h Context.cpp Context.h Date.cpp Date.h - Directory.cpp Directory.h Duration.cpp Duration.h File.cpp - File.h Filter.cpp Filter.h feedback.cpp Grid.cpp Grid.h Hooks.cpp - Hooks.h JSON.cpp JSON.h Keymap.cpp Keymap.h Location.cpp - Location.h Nibbler.cpp Nibbler.h Path.cpp Path.h Permission.cpp - Permission.h Record.cpp Record.h Rectangle.cpp Rectangle.h - Sequence.cpp Sequence.h Subst.cpp Subst.h TDB.cpp TDB.h Table.cpp - Table.h Task.cpp Task.h Taskmod.cpp Taskmod.h Thread.cpp Thread.h - Timer.cpp Timer.h Transport.cpp Transport.h TransportSSH.cpp - TransportSSH.h TransportRSYNC.cpp TransportRSYNC.h - TransportCurl.cpp TransportCurl.h Tree.cpp Tree.h burndown.cpp - command.cpp custom.cpp dependency.cpp diag.cpp edit.cpp - export.cpp history.cpp i18n.h import.cpp interactive.cpp - recur.cpp report.cpp rules.cpp rx.cpp rx.h text.cpp text.h - utf8.cpp utf8.h util.cpp util.h Uri.cpp Uri.h Variant.cpp - Variant.h) + Directory.cpp Directory.h DOM.cpp DOM.h Duration.cpp Duration.h + File.cpp File.h Filter.cpp Filter.h feedback.cpp Grid.cpp Grid.h + Hooks.cpp Hooks.h JSON.cpp JSON.h Keymap.cpp Keymap.h + Location.cpp Location.h Nibbler.cpp Nibbler.h Path.cpp Path.h + Permission.cpp Permission.h Record.cpp Record.h Rectangle.cpp + Rectangle.h Sequence.cpp Sequence.h Subst.cpp Subst.h TDB.cpp + TDB.h Table.cpp TDB2.cpp TDB2.h Table.h Task.cpp Task.h + Taskmod.cpp Taskmod.h Thread.cpp Thread.h Timer.cpp Timer.h + Transport.cpp Transport.h TransportSSH.cpp TransportSSH.h + TransportRSYNC.cpp TransportRSYNC.h TransportCurl.cpp + TransportCurl.h Tree.cpp Tree.h burndown.cpp command.cpp + custom.cpp dependency.cpp diag.cpp edit.cpp export.cpp + history.cpp i18n.h import.cpp interactive.cpp recur.cpp + report.cpp rules.cpp rx.cpp rx.h text.cpp text.h utf8.cpp utf8.h + util.cpp util.h Uri.cpp Uri.h Variant.cpp Variant.h) add_library (task STATIC ${task_SRCS}) add_executable (task_executable main.cpp) diff --git a/src/Context.cpp b/src/Context.cpp index 04ae0cdfd..5a7292ebb 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -51,10 +51,12 @@ Context::Context () , subst () , task () , tdb () +, tdb2 () , program ("") , file_override ("") , var_overrides ("") , cmd () +, dom () , inShadow (false) , terminal_width (0) , terminal_height (0) diff --git a/src/Context.h b/src/Context.h index b44433a47..ab49b729b 100644 --- a/src/Context.h +++ b/src/Context.h @@ -35,7 +35,9 @@ #include "Cmd.h" #include "Task.h" #include "TDB.h" +#include "TDB2.h" #include "Hooks.h" +#include "DOM.h" class Context { @@ -83,6 +85,7 @@ public: Subst subst; Task task; TDB tdb; + TDB2 tdb2; std::string program; std::vector args; std::string file_override; @@ -92,6 +95,7 @@ public: std::vector tagAdditions; std::vector tagRemovals; Hooks hooks; + DOM dom; std::vector headers; std::vector footnotes; diff --git a/src/DOM.cpp b/src/DOM.cpp new file mode 100644 index 000000000..b400ad16c --- /dev/null +++ b/src/DOM.cpp @@ -0,0 +1,96 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2011, Paul Beckingham, Federico Hernandez. +// 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 + +//////////////////////////////////////////////////////////////////////////////// +DOM::DOM () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +DOM::~DOM () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +const int DOM::getInteger (const std::string& name) +{ + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +const double DOM::getReal (const std::string& name) +{ + return 0.0; +} + +//////////////////////////////////////////////////////////////////////////////// +const bool DOM::getBoolean (const std::string& name) +{ + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +const time_t DOM::getDate (const std::string& name) +{ + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +const std::string DOM::get (const std::string& name) +{ + return ""; +} + +//////////////////////////////////////////////////////////////////////////////// +void DOM::set (const std::string& name, const bool value) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +void DOM::set (const std::string& name, const int value) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +void DOM::set (const std::string& name, const double value) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +void DOM::set (const std::string& name, const time_t value) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +void DOM::set (const std::string& name, const std::string& value) +{ +} + +//////////////////////////////////////////////////////////////////////////////// + diff --git a/src/DOM.h b/src/DOM.h new file mode 100644 index 000000000..6c4da7c1f --- /dev/null +++ b/src/DOM.h @@ -0,0 +1,55 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2011, Paul Beckingham, Federico Hernandez. +// 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 +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_DOM +#define INCLUDED_DOM + +#include +#include + +class DOM +{ +public: + DOM (); + ~DOM (); + + const int getInteger (const std::string&); + const double getReal (const std::string&); + const bool getBoolean (const std::string&); + const time_t getDate (const std::string&); + const std::string get (const std::string&); + + void set (const std::string&, const bool); + void set (const std::string&, const int); + void set (const std::string&, const double); + void set (const std::string&, const time_t); + void set (const std::string&, const std::string&); + +private: +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/TDB2.cpp b/src/TDB2.cpp new file mode 100644 index 000000000..f6cd261d1 --- /dev/null +++ b/src/TDB2.cpp @@ -0,0 +1,1696 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// 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 + +#if 0 +#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 << "\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. +// 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) +{ + // 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; + } + } + + loadPending (tasks, filter); + + if (numberStatusClauses == 0 || + numberStatusClauses != numberSimpleStatusClauses) + loadCompleted (tasks, filter); + else + context.debug ("load optimization short circuit"); + + 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::status status = task.getStatus (); + 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) +{ + std::string unique; + Task t (task); + if (task.get ("uuid") == "") + unique = ::uuid (); + else + unique = task.get ("uuid"); + + t.set ("uuid", unique); + + // If the tasks are loaded, then verify that this uuid is not already in + // the file. + if (uuidAlreadyUsed (unique, mNew) || + uuidAlreadyUsed (unique, mModified) || + uuidAlreadyUsed (unique, mPending) || + uuidAlreadyUsed (unique, mCompleted)) + throw std::string ("Cannot add task because the uuid '") + unique + "' is not unique."; + + mNew.push_back (t); + mI2U[task.id] = unique; + mU2I[task.get ("uuid")] = t.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. +// Now cleans up dangling dependencies. +int TDB::gc () +{ + Timer t ("TDB::gc"); + + // Allowed as a temporary override. + if (!context.config.getBoolean ("gc")) + return 0; + + 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); + + // Search for dangling dependencies. These are dependencies whose uuid cannot + // be converted to an id by TDB. + std::vector deps; + foreach (task, mPending) + { + if (task->has ("depends")) + { + deps.clear (); + task->getDependencies (deps); + foreach (dep, deps) + { + if (id (*dep) == 0) + { + task->removeDependency (*dep); + ++count_pending_changes; + context.debug ("GC: Removed dangling dependency " + + task->get ("uuid") + + " -> " + + *dep); + } + } + } + } + + // 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 + + context.debug (std::string ("TDB::gc waiting -> pending for ") + + task->get ("uuid")); + } + + 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. + bool useColor = context.config.getBoolean ("color") || + context.config.getBoolean ("_forcecolor") ? true : false; + + Color color_red (useColor ? context.config.get ("color.undo.before") : ""); + Color color_green (useColor ? 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. + Table table; + table.setTableWidth (context.getWidth ()); + table.setTableIntraPadding (2); + table.addColumn (" "); + table.addColumn ("Prior Values"); + table.addColumn ("Current Values"); + + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (1); + table.setColumnUnderline (2); + } + else + table.setTableDashedUnderline (); + + 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 << "\n" + << table.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. + 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 << "\n" + << table.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"; + 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.\n"; + } + else + { + p.erase (task); + std::cout << "Task removed.\n"; + } + + // 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.\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"; + 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.\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 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. + bool useColor = (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + ? true : false; + 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 " + << (useColor ? 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 " + << (useColor ? 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"); + + 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 " + << (useColor ? 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 " + << (useColor ? 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 + + // 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 + + // 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 " + << (useColor ? 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 " + << (useColor ? 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 " + << (useColor ? 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 " + << (useColor ? 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 + "'."; + + // 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 + "'."; + } + + // 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...\n"; + 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 ()); +} + +//////////////////////////////////////////////////////////////////////////////// +bool TDB::uuidAlreadyUsed ( + const std::string& uuid, + const std::vector & all) +{ + std::vector ::const_iterator it; + for (it = all.begin (); it != all.end (); ++it) + if (it->get ("uuid") == uuid) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +#endif + diff --git a/src/TDB2.h b/src/TDB2.h new file mode 100644 index 000000000..f419b5cd1 --- /dev/null +++ b/src/TDB2.h @@ -0,0 +1,112 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// 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 +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_TDB2 +#define INCLUDED_TDB2 + +#include +#include +#include + +class TDB2 +{ +public: + +private: +}; + + + + + +/* +#include "Location.h" +#include "Filter.h" +#include "Task.h" + +// Length of longest line. +#define T_LINE_MAX 32768 + +class TDB +{ +public: + TDB (); // Default constructor + ~TDB (); // Destructor + + TDB (const TDB&); + TDB& operator= (const TDB&); + + void clear (); + void location (const std::string&); + + void lock (bool lockFile = true); + void unlock (); + + int load (std::vector &, Filter&); + int loadPending (std::vector &, Filter&); + int loadCompleted (std::vector &, Filter&); + + const std::vector & getAllPending (); + const std::vector & getAllNew (); + const std::vector & getAllCompleted (); + const std::vector & getAllModified (); + + void add (const Task&); // Single task add to pending + void update (const Task&); // Single task update to pending + int commit (); // Write out all tasks + int gc (); // Clean up pending + int nextId (); + void undo (); + void merge (const std::string&); + + std::string uuid (int) const; + int id (const std::string&) const; + +private: + FILE* openAndLock (const std::string&); + void writeUndo (const Task&, FILE*); + void writeUndo (const Task&, const Task&, FILE*); + bool uuidAlreadyUsed (const std::string&); + bool uuidAlreadyUsed (const std::string&, const std::vector &); + +private: + std::vector mLocations; + bool mLock; + bool mAllOpenAndLocked; + int mId; + + std::vector mPending; // Contents of pending.data + std::vector mCompleted; // Contents of pending.data + std::vector mNew; // Uncommitted new tasks + std::vector mModified; // Uncommitted modified tasks + + std::map mI2U; // ID -> UUID map + std::map mU2I; // UUID -> ID map +}; +*/ + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/test/.gitignore b/test/.gitignore index 894dc81af..b512d39c0 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -27,6 +27,7 @@ t.benchmark.t t.t taskmod.t tdb.t +tdb2.t text.t transport.t tree.t diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5d66dc250..1fcbdfc68 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -4,6 +4,7 @@ include_directories (${CMAKE_SOURCE_DIR}/src ${TASK_INCLUDE_DIRS}) set (test_SRCS date.t t.t tdb.t duration.t t.benchmark.t text.t autocomplete.t + tdb2.t seq.t record.t att.t subst.t nibbler.t filt.t cmd.t config.t util.t color.t list.t path.t file.t grid.t directory.t rx.t taskmod.t rectangle.t tree.t tree2.t uri.t json.t variant.t) diff --git a/test/tdb2.t.cpp b/test/tdb2.t.cpp new file mode 100644 index 000000000..a9715bb37 --- /dev/null +++ b/test/tdb2.t.cpp @@ -0,0 +1,175 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// 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 "main.h" +#include "test.h" + +Context context; + +//////////////////////////////////////////////////////////////////////////////// +void get (std::vector & pending, std::vector & completed) +{ + TDB tdb; + tdb.location ("."); + tdb.lock (); + tdb.loadPending (pending, context.filter); + tdb.loadCompleted (completed, context.filter); + tdb.unlock (); +} + +//////////////////////////////////////////////////////////////////////////////// +int main (int argc, char** argv) +{ + UnitTest t (1); + + try + { + t.pass ("sample"); +/* + // Remove any residual test file. + unlink ("./pending.data"); + unlink ("./completed.data"); + unlink ("./undo.data"); + + // Set the context to allow GC. + context.config.set ("gc", "on"); + + // Try reading an empty database. + Filter filter; + std::vector all; + std::vector pending; + std::vector completed; + get (pending, completed); + t.ok (pending.size () == 0, "TDB Read empty pending"); + t.ok (completed.size () == 0, "TDB Read empty completed"); + + // Add without commit. + TDB tdb; + tdb.location ("."); + tdb.lock (); + Task task ("[name:\"value\"]"); + tdb.add (task); // P0 C0 N1 M0 + tdb.unlock (); + + pending.clear (); + completed.clear (); + get (pending, completed); + + t.ok (pending.size () == 0, "TDB add -> no commit -> empty"); + t.ok (completed.size () == 0, "TDB add -> no commit -> empty"); + + // Add with commit. + tdb.lock (); + tdb.add (task); // P0 C0 N1 M0 + tdb.commit (); // P1 C0 N0 M0 + tdb.unlock (); + + get (pending, completed); + t.ok (pending.size () == 1, "TDB add -> commit -> saved"); + t.is (pending[0].get ("name"), "value", "TDB load name=value"); + t.is (pending[0].id, 1, "TDB load verification id=1"); + t.ok (completed.size () == 0, "TDB add -> commit -> saved"); + + // Update with commit. + pending.clear (); + completed.clear (); + + tdb.lock (); + tdb.load (all, context.filter); + all[0].set ("name", "value2"); + tdb.update (all[0]); // P1 C0 N0 M1 + tdb.commit (); // P1 C0 N0 M0 + tdb.unlock (); + + pending.clear (); + completed.clear (); + get (pending, completed); + + t.ok (all.size () == 1, "TDB update -> commit -> saved"); + t.is (all[0].get ("name"), "value2", "TDB load name=value2"); + t.is (all[0].id, 1, "TDB load verification id=1"); + + // GC. + all.clear (); + + tdb.lock (); + tdb.loadPending (all, context.filter); + all[0].setStatus (Task::completed); + tdb.update (all[0]); // P1 C0 N0 M1 + Task t2 ("[foo:\"bar\" status:\"pending\"]"); + tdb.add (t2); // P1 C0 N1 M1 + tdb.commit (); + tdb.unlock (); + + pending.clear (); + completed.clear (); + get (pending, completed); + + t.is (pending.size (), (size_t)2, "TDB before gc pending #2"); + t.is (pending[0].id, 1, "TDB before gc pending id 1"); + t.is (pending[0].getStatus (), Task::completed, "TDB before gc pending status completed"); + t.is (pending[1].id, 2, "TDB before gc pending id 2"); + t.is (pending[1].getStatus (), Task::pending, "TDB before gc pending status pending"); + t.is (completed.size (), (size_t)0, "TDB before gc completed 0"); + + tdb.gc (); // P1 C1 N0 M0 + + pending.clear (); + completed.clear (); + get (pending, completed); + + t.is (pending.size (), (size_t)1, "TDB after gc pending #1"); + t.is (pending[0].id, 1, "TDB after gc pending id 2"); + t.is (pending[0].getStatus (), Task::pending, "TDB after gc pending status pending"); + t.is (completed.size (), (size_t)1, "TDB after gc completed #1"); + t.is (completed[0].getStatus (), Task::completed, "TDB after gc completed status completed"); +*/ + } + + catch (std::string& error) + { + t.diag (error); + return -1; + } + + catch (...) + { + t.diag ("Unknown error."); + return -2; + } + + unlink ("./pending.data"); + unlink ("./completed.data"); + unlink ("./undo.data"); + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +