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.

Data can be either provided as one or more files, or via stdin.
The latter enables synchronization between remote machines via ssh:
timew export | ssh <server> 'timew import'

Signed-off-by: Thomas Lauf <thomas.lauf@tngtech.com>
This commit is contained in:
Thomas Lauf 2025-06-21 13:34:18 +02:00 committed by Thomas Lauf
parent d925553116
commit 5276a9f4bd
6 changed files with 230 additions and 0 deletions

View file

@ -1,3 +1,5 @@
- #676 Add import command
(thanks to Shaun Ruffel)
- #677 Extension names starting with 'timew' cause problems - #677 Extension names starting with 'timew' cause problems
(thanks to ftambara) (thanks to ftambara)
- #661 Make display of ids and annotations the default in summary report for new users - #661 Make display of ids and annotations the default in summary report for new users

View file

@ -0,0 +1,57 @@
= timew-import(1)
== NAME
timew-import - import time-tracking data from files
== SYNOPSIS
[verse]
*timew import* [_<file>_**...**]
== DESCRIPTION
Import tracked time from `file`.
If no files are specified, the command will read from standard input.
The data to import has 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
----
*Import intervals from standard input*::
+
Import intervals from stdin:
+
[source]
----
timew export | ssh <server> 'timew import'
----
This is especially useful for synchronizing intervals between different machines.
+

View file

@ -30,6 +30,7 @@ set (commands_SRCS CmdAnnotate.cpp
CmdGaps.cpp CmdGaps.cpp
CmdGet.cpp CmdGet.cpp
CmdHelp.cpp CmdHelp.cpp
CmdImport.cpp
CmdJoin.cpp CmdJoin.cpp
CmdLengthen.cpp CmdLengthen.cpp
CmdModify.cpp CmdModify.cpp

167
src/commands/CmdImport.cpp Normal file
View file

@ -0,0 +1,167 @@
////////////////////////////////////////////////////////////////////////////////
//
// 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 <commands.h>
#include <format.h>
#include <iostream>
#include <timew.h>
#include <IntervalFactory.h>
#include <JSON.h>
////////////////////////////////////////////////////////////////////////////////
std::vector<Interval> parse_content (const std::string& content)
{
const std::unique_ptr<json::value> 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<Interval> intervals;
for (const auto item: dynamic_cast<json::array *> (json.get ())->_data)
{
Interval new_interval = IntervalFactory::fromJson (item->dump ());
intervals.push_back (new_interval);
}
return intervals;
}
////////////////////////////////////////////////////////////////////////////////
std::string read_input ()
{
std::string content;
std::string line;
while (std::getline (std::cin, line))
{
content += line;
}
return content;
}
////////////////////////////////////////////////////////////////////////////////
std::vector<Interval> import_file (const std::string& file_name)
{
Path file_path;
if (file_name.empty ())
{
throw format ("Attempted to import from empty file name!", file_name);
}
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))
{
return parse_content (content);
}
throw format ("File {1} does not exist or cannot be read!", file_name);
}
void import_intervals (
const CLI& cli,
const Rules& rules,
Database& database,
Journal& journal,
const bool verbose,
std::vector<Interval>& intervals)
{
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 ();
}
////////////////////////////////////////////////////////////////////////////////
int CmdImport (
CLI& cli,
Rules& rules,
Database &database,
Journal &journal)
{
const bool verbose = rules.getBoolean ("verbose");
if (const auto fileNames = cli.getWords (); fileNames.empty ())
{
const auto content = read_input ();
auto intervals = parse_content (content);
import_intervals (cli, rules, database, journal, verbose, intervals);
if (verbose)
{
std::cout << "Imported " << intervals.size () << " interval(s)." << std::endl;
}
}
else
{
for (const auto& fileName: fileNames)
{
try
{
auto intervals = import_file (fileName);
import_intervals (cli, rules, database, journal, verbose, intervals);
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;
}

View file

@ -47,6 +47,7 @@ int CmdGaps (CLI&, Rules&, Database& );
int CmdGet (CLI&, Rules&, Database& ); int CmdGet (CLI&, Rules&, Database& );
int CmdHelpUsage ( const Extensions&); int CmdHelpUsage ( const Extensions&);
int CmdHelp (CLI&, const Extensions&); int CmdHelp (CLI&, const Extensions&);
int CmdImport (CLI&, Rules&, Database&, Journal& );
int CmdJoin (CLI&, Rules&, Database&, Journal& ); int CmdJoin (CLI&, Rules&, Database&, Journal& );
int CmdLengthen (CLI&, Rules&, Database&, Journal& ); int CmdLengthen (CLI&, Rules&, Database&, Journal& );
int CmdModify (CLI&, Rules&, Database&, Journal& ); int CmdModify (CLI&, Rules&, Database&, Journal& );

View file

@ -63,6 +63,7 @@ void initializeEntities (CLI& cli)
cli.entity ("command", "help"); cli.entity ("command", "help");
cli.entity ("command", "--help"); cli.entity ("command", "--help");
cli.entity ("command", "-h"); cli.entity ("command", "-h");
cli.entity ("command", "import");
cli.entity ("command", "join"); cli.entity ("command", "join");
cli.entity ("command", "lengthen"); cli.entity ("command", "lengthen");
cli.entity ("command", "modify"); cli.entity ("command", "modify");
@ -235,6 +236,7 @@ int dispatchCommand (
else if (command == "help" || else if (command == "help" ||
command == "--help" || command == "--help" ||
command == "-h") status = CmdHelp (cli, extensions); 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 == "join") status = CmdJoin (cli, rules, database, journal );
else if (command == "lengthen") status = CmdLengthen (cli, rules, database, journal ); else if (command == "lengthen") status = CmdLengthen (cli, rules, database, journal );
else if (command == "modify") status = CmdModify (cli, rules, database, journal ); else if (command == "modify") status = CmdModify (cli, rules, database, journal );