From b3d9562c54aca4483524d2934a04f211d3c3cc1f Mon Sep 17 00:00:00 2001 From: Thomas Lauf Date: Sat, 21 Jun 2025 13:34:18 +0200 Subject: [PATCH] Add import command The import command lets the user import time tracking data as produced by the export command, i.e. a JSON array of interval objects. Signed-off-by: Thomas Lauf --- ChangeLog | 1 + doc/man1/timew-import.1.adoc | 45 +++++++++++++ src/commands/CMakeLists.txt | 1 + src/commands/CmdImport.cpp | 120 +++++++++++++++++++++++++++++++++++ src/commands/commands.h | 1 + src/init.cpp | 2 + 6 files changed, 170 insertions(+) create mode 100644 doc/man1/timew-import.1.adoc create mode 100644 src/commands/CmdImport.cpp diff --git a/ChangeLog b/ChangeLog index 9e0bcdaa..f44ec591 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,4 @@ +- #676 Add import command - #677 Extension names starting with 'timew' cause problems (thanks to ftambara) - #661 Make display of ids and annotations the default in summary report for new users diff --git a/doc/man1/timew-import.1.adoc b/doc/man1/timew-import.1.adoc new file mode 100644 index 00000000..1898d919 --- /dev/null +++ b/doc/man1/timew-import.1.adoc @@ -0,0 +1,45 @@ += timew-import(1) + +== NAME +timew-import - import time-tracking data from files + +== SYNOPSIS +[verse] +*timew import* __**...** + +== DESCRIPTION +Import tracked time from `file`. + +The files have to be in JSON format, as exported by **timew-export**(1), i.e. a single array with interval objects. + +When importing, the intervals are checked for overlaps with existing intervals. +If an overlap is found, the import will abort at the first overlap and no more intervals are imported, unless the `:adjust` hint is specified. + +In general, it is recommended to create a backup of your data before importing. + +== HINTS + +**:adjust**:: +When given, the imported interval will overwrite any existing intervals that it overlaps with. + +== EXAMPLES + +*Import intervals from a file*:: ++ +Import intervals from a single file: ++ +[source] +---- +timew import intervals.json +---- ++ +Any file path that does not start with a `/` is interpreted as relative to the current working directory. + +*Import intervals from multiple files*:: ++ +One can also use shell wildcards and absolute paths: ++ +[source] +---- +timew import /path/to/intervals/*.json +---- diff --git a/src/commands/CMakeLists.txt b/src/commands/CMakeLists.txt index 9f0ced41..f0a752bb 100644 --- a/src/commands/CMakeLists.txt +++ b/src/commands/CMakeLists.txt @@ -30,6 +30,7 @@ set (commands_SRCS CmdAnnotate.cpp CmdGaps.cpp CmdGet.cpp CmdHelp.cpp + CmdImport.cpp CmdJoin.cpp CmdLengthen.cpp CmdModify.cpp diff --git a/src/commands/CmdImport.cpp b/src/commands/CmdImport.cpp new file mode 100644 index 00000000..d2c1f7d5 --- /dev/null +++ b/src/commands/CmdImport.cpp @@ -0,0 +1,120 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2025, Gothenburg Bit Factory. +// +// 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. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include +#include + +std::vector import_file (const std::string &file_name); + +//////////////////////////////////////////////////////////////////////////////// +int CmdImport( + CLI &cli, + Rules &rules, + Database &database, + Journal &journal) { + const bool verbose = rules.getBoolean("verbose"); + + auto fileNames = cli.getWords(); + + for (const auto& fileName: fileNames) + { + try + { + auto intervals = import_file (fileName); + + journal.startTransaction (); + for (auto& interval: intervals) + { + // Add each interval to the database + if (validate (cli, rules, database, interval)) { + database.addInterval (interval, verbose); + database.commit (); + } + } + journal.endTransaction (); + + if (verbose) + { + std::cout << "Imported " << intervals.size () << " interval(s) from '" << fileName << "'." << std::endl; + } + } + catch (const std::string &error) + { + throw format("Error importing '{1}': {2}", fileName, error); + } + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +std::vector import_file (const std::string& file_name) { + Path file_path; + + if (file_name.at (0) == '/') + { + file_path = file_name; + } + else + { + file_path = Directory::cwd() + "/" + file_name; + } + + std::string content; + + if (const bool exists = file_path.exists(); exists && File::read(file_path, content)) + { + std::unique_ptr json(json::parse(content)); + + if (content.empty() || (json == nullptr)) + { + throw std::string("Contents invalid."); + } + + if (json->type() != json::j_array) + { + throw std::string("Expected JSON array for import data."); + } + + std::vector intervals; + + for (auto item: dynamic_cast(json.get())->_data) + { + Interval new_interval = IntervalFactory::fromJson(item->dump()); + intervals.push_back(new_interval); + } + + return intervals; + } + + throw format("File {1} does not exist or cannot be read: ", file_name); +} diff --git a/src/commands/commands.h b/src/commands/commands.h index 8b156d17..4f9131de 100644 --- a/src/commands/commands.h +++ b/src/commands/commands.h @@ -47,6 +47,7 @@ int CmdGaps (CLI&, Rules&, Database& ); int CmdGet (CLI&, Rules&, Database& ); int CmdHelpUsage ( const Extensions&); int CmdHelp (CLI&, const Extensions&); +int CmdImport (CLI&, Rules&, Database&, Journal& ); int CmdJoin (CLI&, Rules&, Database&, Journal& ); int CmdLengthen (CLI&, Rules&, Database&, Journal& ); int CmdModify (CLI&, Rules&, Database&, Journal& ); diff --git a/src/init.cpp b/src/init.cpp index f8645ccb..1ea86956 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -63,6 +63,7 @@ void initializeEntities (CLI& cli) cli.entity ("command", "help"); cli.entity ("command", "--help"); cli.entity ("command", "-h"); + cli.entity ("command", "import"); cli.entity ("command", "join"); cli.entity ("command", "lengthen"); cli.entity ("command", "modify"); @@ -235,6 +236,7 @@ int dispatchCommand ( else if (command == "help" || command == "--help" || command == "-h") status = CmdHelp (cli, extensions); + else if (command == "import") status = CmdImport (cli, rules, database, journal ); else if (command == "join") status = CmdJoin (cli, rules, database, journal ); else if (command == "lengthen") status = CmdLengthen (cli, rules, database, journal ); else if (command == "modify") status = CmdModify (cli, rules, database, journal );