Add import command
Some checks failed
tests / tests (alpine-edge, Alpine Edge, ubuntu-latest) (push) Has been cancelled
tests / tests (alpine-latest, Alpine Latest, ubuntu-latest) (push) Has been cancelled
tests / tests (archlinux, Archlinux Base, ubuntu-latest) (push) Has been cancelled
tests / tests (centos-stream9, Centos Stream9, ubuntu-latest) (push) Has been cancelled
tests / tests (debianstable, Debian Stable, ubuntu-latest) (push) Has been cancelled
tests / tests (debiantesting, Debian Testing, ubuntu-latest) (push) Has been cancelled
tests / tests (fedora41, Fedora 41, ubuntu-latest) (push) Has been cancelled
tests / tests (fedora42, Fedora 42, ubuntu-latest) (push) Has been cancelled
tests / tests (opensuseleap, OpenSUSE Leap, ubuntu-latest) (push) Has been cancelled
tests / tests (opensusetumbleweed, OpenSUSE Tumbleweed, ubuntu-latest) (push) Has been cancelled
tests / tests (osx-13, macOS 13, macos-13) (push) Has been cancelled
tests / tests (osx-14, macOS 14, macos-14) (push) Has been cancelled
tests / tests (osx-15, macOS 15, macos-15) (push) Has been cancelled
tests / tests (ubuntu2204, Ubuntu 22.04, ubuntu-latest) (push) Has been cancelled
tests / tests (ubuntu2204, Ubuntu 24.04, ubuntu-latest) (push) Has been cancelled

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 <thomas.lauf@tngtech.com>
This commit is contained in:
Thomas Lauf 2025-06-21 13:34:18 +02:00
parent d925553116
commit b3d9562c54
6 changed files with 170 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,45 @@
= timew-import(1)
== NAME
timew-import - import time-tracking data from files
== SYNOPSIS
[verse]
*timew import* _<file>_**...**
== 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
----

View file

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

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

@ -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 <commands.h>
#include <format.h>
#include <fstream>
#include <iostream>
#include <timew.h>
#include <IntervalFactory.h>
#include <JSON.h>
std::vector <Interval> 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<Interval> 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::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 (auto item: dynamic_cast<json::array *>(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);
}

View file

@ -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& );

View file

@ -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 );