mirror of
https://github.com/GothenburgBitFactory/timewarrior.git
synced 2025-07-27 21:23:26 +02:00
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:
parent
d925553116
commit
5276a9f4bd
6 changed files with 230 additions and 0 deletions
|
@ -1,3 +1,5 @@
|
|||
- #676 Add import command
|
||||
(thanks to Shaun Ruffel)
|
||||
- #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
|
||||
|
|
57
doc/man1/timew-import.1.adoc
Normal file
57
doc/man1/timew-import.1.adoc
Normal 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.
|
||||
+
|
|
@ -30,6 +30,7 @@ set (commands_SRCS CmdAnnotate.cpp
|
|||
CmdGaps.cpp
|
||||
CmdGet.cpp
|
||||
CmdHelp.cpp
|
||||
CmdImport.cpp
|
||||
CmdJoin.cpp
|
||||
CmdLengthen.cpp
|
||||
CmdModify.cpp
|
||||
|
|
167
src/commands/CmdImport.cpp
Normal file
167
src/commands/CmdImport.cpp
Normal 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;
|
||||
}
|
|
@ -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& );
|
||||
|
|
|
@ -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 );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue