Commit 8ca31100 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge remote-tracking branch 'mruwek/revert-subcmds' into style-fixes

parents 0ffb6869 bbd03ee4
This diff is collapsed.
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <bram@topydo.org>
# Copyright (C) 2014 - 2017 Bram Schoenmakers <bram@topydo.org>
#
# 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
......@@ -14,44 +14,119 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import arrow
from topydo.lib import TodoFile, TodoList
from topydo.lib.ChangeSet import ChangeSet
from topydo.lib.Command import Command
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.Config import config
class RevertCommand(Command):
def __init__(self, p_args, p_todolist, #pragma: no branch
def __init__(self, p_args, p_todolist, # pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super().__init__(p_args, p_todolist, p_out, p_err,
p_prompt)
super().__init__(p_args, p_todolist, p_out, p_err, p_prompt)
self._backup = None
self._archive_file = None
self._archive = None
def execute(self):
if not super().execute():
return False
archive_file = TodoFile.TodoFile(config().archive())
archive = TodoList.TodoList(archive_file.read())
self._backup = ChangeSet()
archive_path = config().archive()
if archive_path:
self._archive_file = TodoFile.TodoFile(config().archive())
self._archive = TodoList.TodoList(self._archive_file.read())
if len(self.args) > 1:
self.error(self.usage())
else:
try:
arg = self.argument(0)
self._handle_args(arg)
except InvalidCommandArgument:
try:
self._revert_last()
except (ValueError, KeyError):
self.error('No backup was found for the current state of '
+ config().todotxt())
self._backup.close()
def _revert(self, p_timestamp=None):
self._backup.read_backup(self.todolist, p_timestamp)
self._backup.apply(self.todolist, self._archive)
if self._archive:
self._archive_file.write(self._archive.print_todos())
last_change = ChangeSet()
self.out("Reverted to state before: " + self._backup.label)
def _revert_last(self):
self._revert()
self._backup.delete()
def _revert_to_specific(self, p_position):
timestamps = [timestamp for timestamp, _ in self._backup]
position = int(p_position) - 1 # numbering in UI starts with 1
try:
last_change.get_backup(self.todolist)
last_change.apply(self.todolist, archive)
archive_file.write(archive.print_todos())
last_change.delete()
timestamp = timestamps[position]
self._revert(timestamp)
for timestamp in timestamps[:position + 1]:
self._backup.read_backup(p_timestamp=timestamp)
self._backup.delete()
except IndexError:
self.error('Specified index is out range')
self.out("Successfully reverted: " + last_change.label)
except (ValueError, KeyError):
self.error('No backup was found for the current state of ' + config().todotxt())
def _handle_args(self, p_arg):
try:
if p_arg == 'ls':
self._handle_ls()
elif p_arg.isdigit():
self._revert_to_specific(p_arg)
else:
raise InvalidCommandArgument
except InvalidCommandArgument:
self.error(self.usage())
last_change.close()
def _handle_ls(self):
num = 1
for timestamp, change in self._backup:
label = change[2]
time = arrow.get(timestamp).format('YYYY-MM-DD HH:mm:ss')
self.out('{0: >3}| {1} | {2}'.format(str(num), time, label))
num += 1
def usage(self):
return """Synopsis: revert"""
return """Synopsis:
revert [ls]
revert [NUMBER]"""
def help(self):
return """Reverts the last command."""
return """\
Reverts last commands.
* ls : Lists all backups ordered and numbered chronologically (starting
with 1 for latest backup).
* [NUMBER] : revert to specific point in history specified by NUMBER.
Output example for `revert ls`:
1 | 1970-01-01 00:00:02 | add Baz
2 | 1970-01-01 00:00:01 | add Bar
3 | 1970-01-01 00:00:00 | add Foo
In such example executing `revert 2` will revert todo and archive files to the
state before execution of `add Bar`.
* `revert` without any further arguments will revert to the latest backup
available, provided that this backup matches current state of the todo file.
Topydo will refuse to revert, if any changes to todo file were made by
external application after the latest backup. To force a `revert` action use
it with a NUMBER.\
"""
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <bram@topydo.org>
# Copyright (C) 2014 - 2017 Bram Schoenmakers <bram@topydo.org>
#
# 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
......@@ -46,7 +46,7 @@ class ChangeSet(object):
def __init__(self, p_todolist=None, p_archive=None, p_label=None):
self.todolist = deepcopy(p_todolist)
self.archive = deepcopy(p_archive)
self.timestamp = str(int(time.time()))
self.timestamp = str(time.time())
self.label = ' '.join(p_label if p_label else [])
try:
......@@ -56,6 +56,11 @@ class ChangeSet(object):
self._read()
def __iter__(self):
items = {key: self.backup_dict[key]
for key in self.backup_dict if key != 'index'}.items()
return iter(sorted(items, reverse=True))
def _read(self):
"""
Reads backup file from json_file property and sets backup_dict property
......@@ -158,15 +163,18 @@ class ChangeSet(object):
for changeset in index[backup_limit:]:
self.delete(changeset[0], p_write=False)
def get_backup(self, p_todolist):
def read_backup(self, p_todolist=None, p_timestamp=None):
"""
Retrieves a backup for p_todolist from backup file and sets todolist,
archive and label attributes to appropriate data from it.
Retrieves a backup for p_timestamp or p_todolist (if p_timestamp is not
specified) from backup file and sets timestamp, todolist, archive and
label attributes to appropriate data from it.
"""
change_hash = hash_todolist(p_todolist)
index = self._get_index()
self.timestamp = index[[change[1] for change in index].index(change_hash)][0]
if not p_timestamp:
change_hash = hash_todolist(p_todolist)
index = self._get_index()
self.timestamp = index[[change[1] for change in index].index(change_hash)][0]
else:
self.timestamp = p_timestamp
d = self.backup_dict[self.timestamp]
......@@ -176,10 +184,10 @@ class ChangeSet(object):
def apply(self, p_todolist, p_archive):
""" Applies backup on supplied p_todolist. """
if self.todolist:
if self.todolist and p_todolist:
p_todolist.replace(self.todolist.todos())
if self.archive:
if self.archive and p_archive:
p_archive.replace(self.archive.todos())
def close(self):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment