diff --git a/src/Att.cpp b/src/Att.cpp index 3a7e36d68..224c44bce 100644 --- a/src/Att.cpp +++ b/src/Att.cpp @@ -154,6 +154,14 @@ Att& Att::operator= (const Att& other) return *this; } +//////////////////////////////////////////////////////////////////////////////// +bool Att::operator== (const Att& other) const +{ + return mName == other.mName && + mMod == other.mMod && + mValue == other.mValue; +} + //////////////////////////////////////////////////////////////////////////////// Att::~Att () { diff --git a/src/Att.h b/src/Att.h index cc5cf4ee7..29f23b361 100644 --- a/src/Att.h +++ b/src/Att.h @@ -41,6 +41,7 @@ public: Att (const std::string&, int); Att (const Att&); Att& operator= (const Att&); + bool operator== (const Att&) const; ~Att (); bool valid (const std::string&) const; diff --git a/src/Cmd.cpp b/src/Cmd.cpp index c55860890..eddc745fc 100644 --- a/src/Cmd.cpp +++ b/src/Cmd.cpp @@ -112,6 +112,7 @@ void Cmd::load () commands.push_back ("_ids"); commands.push_back ("_config"); commands.push_back ("_version"); + commands.push_back ("_merge"); commands.push_back ("export.csv"); commands.push_back ("export.ical"); commands.push_back ("history.monthly"); @@ -233,7 +234,8 @@ bool Cmd::isReadOnlyCommand () // Commands that directly modify the data files. bool Cmd::isWriteCommand () { - if (command == context.stringtable.get (CMD_ADD, "add") || + if (command == "_merge" || + command == context.stringtable.get (CMD_ADD, "add") || command == context.stringtable.get (CMD_APPEND, "append") || command == context.stringtable.get (CMD_ANNOTATE, "annotate") || command == context.stringtable.get (CMD_DENOTATE, "denotate") || diff --git a/src/Context.cpp b/src/Context.cpp index 8cd22b6ba..af6e83dca 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -240,6 +240,7 @@ int Context::dispatch (std::string &out) else if (cmd.command == "shell") { handleShell ( ); } #endif else if (cmd.command == "undo") { handleUndo ( ); } + else if (cmd.command == "_merge") { handleMerge (out); } else if (cmd.command == "_projects") { rc = handleCompletionProjects (out); } else if (cmd.command == "_tags") { rc = handleCompletionTags (out); } else if (cmd.command == "_commands") { rc = handleCompletionCommands (out); } diff --git a/src/Makefile.am b/src/Makefile.am index 4ad43461f..94c67049f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -4,13 +4,14 @@ task_SOURCES = API.cpp Att.cpp Cmd.cpp Color.cpp Config.cpp Context.cpp \ Date.cpp Directory.cpp Duration.cpp File.cpp Filter.cpp \ Grid.cpp Hooks.cpp Keymap.cpp Location.cpp Nibbler.cpp \ Path.cpp Permission.cpp Record.cpp Sequence.cpp \ - StringTable.cpp Subst.cpp TDB.cpp Table.cpp Task.cpp Timer.cpp \ - command.cpp custom.cpp edit.cpp export.cpp import.cpp \ - interactive.cpp main.cpp recur.cpp report.cpp rules.cpp rx.cpp \ - text.cpp util.cpp API.h Att.h Cmd.h Color.h Config.h Context.h \ - Date.h Directory.h Duration.h File.h Filter.h Grid.h Hooks.h \ - Keymap.h Location.h Nibbler.h Path.h Permission.h Record.h \ - Sequence.h StringTable.h Subst.h TDB.h Table.h Task.h Timer.h \ - i18n.h main.h text.h util.h rx.h + StringTable.cpp Subst.cpp TDB.cpp Table.cpp Task.cpp \ + Taskmod.cpp Timer.cpp command.cpp custom.cpp edit.cpp \ + export.cpp import.cpp interactive.cpp main.cpp recur.cpp \ + report.cpp rules.cpp rx.cpp text.cpp util.cpp API.h Att.h \ + Cmd.h Color.h Config.h Context.h Date.h Directory.h Duration.h \ + File.h Filter.h Grid.h Hooks.h Keymap.h Location.h Nibbler.h \ + Path.h Permission.h Record.h Sequence.h StringTable.h Subst.h \ + TDB.h Table.h Task.h Taskmod.h Timer.h i18n.h main.h text.h \ + util.h rx.h task_CPPFLAGS=$(LUA_CFLAGS) task_LDFLAGS=$(LUA_LFLAGS) diff --git a/src/TDB.cpp b/src/TDB.cpp index 2fa2f20dc..42e50a9de 100644 --- a/src/TDB.cpp +++ b/src/TDB.cpp @@ -27,6 +27,8 @@ #include #include +#include +#include #include #include #include @@ -42,8 +44,69 @@ #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. @@ -783,6 +846,439 @@ void TDB::undo () << std::endl; } +//////////////////////////////////////////////////////////////////////////////// +void TDB::merge (const std::string& mergeFile) +{ + // 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.getBefore()); + } + } + } // 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(); +} + //////////////////////////////////////////////////////////////////////////////// FILE* TDB::openAndLock (const std::string& file) { diff --git a/src/TDB.h b/src/TDB.h index eab2ab350..c65ad0570 100644 --- a/src/TDB.h +++ b/src/TDB.h @@ -62,6 +62,7 @@ public: int gc (); // Clean up pending int nextId (); void undo (); + void merge (const std::string&); private: FILE* openAndLock (const std::string&); diff --git a/src/Taskmod.cpp b/src/Taskmod.cpp new file mode 100644 index 000000000..c66fa6d43 --- /dev/null +++ b/src/Taskmod.cpp @@ -0,0 +1,183 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 "Taskmod.h" + +Taskmod::Taskmod() +{ + timestamp = 0; + bAfterSet = false; + bBeforeSet = false; +} + +Taskmod::Taskmod(const Taskmod& other) +{ + this->before = other.before; + this->after = other.after; + this->timestamp = other.timestamp; + this->bAfterSet = other.bAfterSet; + this->bBeforeSet = other.bBeforeSet; +} + +Taskmod::~Taskmod() +{ +} + +// OPERATORS +bool Taskmod::operator< (const Taskmod &compare) +{ + return (timestamp < compare.getTimestamp()); +} + +bool Taskmod::operator> (const Taskmod &compare) +{ + return (timestamp > compare.getTimestamp()); +} + +bool Taskmod::operator== (const Taskmod& compare) +{ + return ( (compare.after == this->after) + && (compare.before == this->before) + && (compare.timestamp == this->timestamp) ); +} + +bool Taskmod::operator!= (const Taskmod& compare) +{ + return !this->operator ==(compare); +} + +Taskmod& Taskmod::operator= (const Taskmod& other) +{ + if (this != &other) { + this->before = other.before; + this->after = other.after; + this->timestamp = other.timestamp; + this->bAfterSet = other.bAfterSet; + this->bBeforeSet = other.bBeforeSet; + } + + return *this; +} + +// HELPER +void Taskmod::reset(long timestamp) +{ + this->bAfterSet = false; + this->bBeforeSet = false; + this->timestamp = timestamp; +} + +bool Taskmod::isNew() +{ + return !bBeforeSet; +} + +bool Taskmod::issetAfter() +{ + return bAfterSet; +} + +bool Taskmod::issetBefore() +{ + return bBeforeSet; +} + +bool Taskmod::isValid() +{ + return (timestamp > 0) && (bAfterSet); +} + +std::string Taskmod::getUuid() +{ + if (!bAfterSet) { + throw std::string("Taskmod::getUuid(): Task object not initialized"); + } + + return after.get("uuid"); +} + +std::string Taskmod::toString() +{ + assert(bAfterSet); + + std::stringstream stream; + stream << "time " << timestamp << "\n"; + + if (bBeforeSet) { + stream << "old " << before.composeF4(); + } + + stream << "new " << after.composeF4(); + stream << "---\n"; + + return stream.str(); +} + +// SETTER +void Taskmod::setAfter(const Task& after) +{ + this->after = after; + bAfterSet = true; +} + +void Taskmod::setBefore(const Task& before) +{ + this->before = before; + bBeforeSet = true; +} + +void Taskmod::setTimestamp(long timestamp) +{ + this->timestamp = timestamp; +} + +// GETTER +Task& Taskmod::getAfter() +{ + return after; +} + +Task& Taskmod::getBefore() +{ + return before; +} + +long Taskmod::getTimestamp() const +{ + return timestamp; +} + +std::string Taskmod::getTimeStr() const +{ + std::stringstream sstream; + sstream << timestamp; + return sstream.str(); +} + diff --git a/src/Taskmod.h b/src/Taskmod.h new file mode 100644 index 000000000..0296783cc --- /dev/null +++ b/src/Taskmod.h @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_TASKMOD +#define INCLUDED_TASKMOD + +#include +#include + +class Taskmod { + +public: + Taskmod(); + Taskmod(const Taskmod& other); + ~Taskmod(); + + // operators + bool operator< (const Taskmod& compare); + bool operator> (const Taskmod& compare); + bool operator== (const Taskmod& compare); + bool operator!= (const Taskmod& compare); + Taskmod& operator= (const Taskmod& other); + + // helper + void reset(long timestamp=0); + bool isNew(); + bool issetBefore(); + bool issetAfter(); + bool isValid(); + + std::string getUuid(); + std::string toString(); + + // setter + void setAfter(const Task& after); + void setBefore(const Task& before); + void setTimestamp(long timestamp); + + // getter + Task& getAfter(); + Task& getBefore(); + long getTimestamp() const; + std::string getTimeStr() const; + +protected: + Task after; + Task before; + long timestamp; + bool bAfterSet; + bool bBeforeSet; +}; + +#endif + diff --git a/src/command.cpp b/src/command.cpp index b71ae225c..90c397dac 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -516,6 +516,25 @@ void handleUndo () } } +//////////////////////////////////////////////////////////////////////////////// +void handleMerge (std::string& outs) +{ + if (context.hooks.trigger ("pre-merge-command")) + { + std::string file = trim (context.task.get ("description")); + if (file.length () > 0) + { + context.tdb.lock (context.config.getBoolean ("locking")); + context.tdb.merge (file); + context.tdb.unlock (); + + context.hooks.trigger ("post-merge-command"); + } + else + throw std::string ("You must specify a file to merge."); + } +} + //////////////////////////////////////////////////////////////////////////////// int handleVersion (std::string &outs) { diff --git a/src/main.h b/src/main.h index 79423e5e6..b137ea838 100644 --- a/src/main.h +++ b/src/main.h @@ -80,6 +80,7 @@ int handleAnnotate (std::string &); int handleDenotate (std::string &); int handleDuplicate (std::string &); void handleUndo (); +void handleMerge (std::string&); #ifdef FEATURE_SHELL void handleShell (); #endif diff --git a/src/tests/Makefile b/src/tests/Makefile index 3a3b801c6..542a8576c 100644 --- a/src/tests/Makefile +++ b/src/tests/Makefile @@ -11,7 +11,7 @@ OBJECTS = ../t-TDB.o ../t-Task.o ../t-text.o ../t-Date.o ../t-Table.o \ ../t-Grid.o ../t-Color.o ../t-rules.o ../t-recur.o ../t-custom.o \ ../t-export.o ../t-import.o ../t-edit.o ../t-Timer.o \ ../t-Permission.o ../t-Path.o ../t-File.o ../t-Directory.o \ - ../t-Hooks.o ../t-API.o ../t-rx.o + ../t-Hooks.o ../t-API.o ../t-rx.o ../t-Taskmod.o all: $(PROJECT)