Commit 2bbe8bb0 authored by Jacek Sowiński's avatar Jacek Sowiński

List backup points and revert to specific ones

`topydo revert ls` will now show all saved backup points with position
number (starting with 1), date and time of relevant action, and its
label.

`topydo revert 2` will revert to a point **before** the backup point
marked as 2 in `topydo revert ls` (i.e. it will go 2 changes backwards
or phrasing more naturally: revert last 2 changes).

Warning: changes between between current state and desired backup point
are also deleted to avoid false assumption that our backups can be
treated as some sort of atomic "patches".
parent 6cb00ce0
# 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,8 +14,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command
import arrow
from topydo.lib.ChangeSet import ChangeSet
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib import TodoFile
from topydo.lib import TodoList
from topydo.lib.Config import config
......@@ -28,36 +30,104 @@ class RevertCommand(Command):
p_prompt=lambda a: None):
super().__init__(p_args, p_todolist, p_out, p_err, p_prompt)
self._archive = None
self._backup = None
self._archive_file = None
self._archive = None
def execute(self):
if not super().execute():
return False
self._backup = ChangeSet()
archive_path = config().archive()
if archive_path:
self._archive_file = TodoFile.TodoFile(archive_path)
self._archive_file = TodoFile.TodoFile(config().archive())
self._archive = TodoList.TodoList(self._archive_file.read())
last_change = ChangeSet()
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())
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:
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')
def _handle_args(self, p_arg):
try:
last_change.get_backup(self.todolist)
last_change.apply(self.todolist, self._archive)
if self._archive:
self._archive_file.write(self._archive.print_todos())
last_change.delete()
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())
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_ls(self):
num = 1
for timestamp, change in self._backup:
label = change[2]
time = arrow.get(timestamp).format('YYYY-MM-DD HH:mm:ss')
last_change.close()
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=[]):
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)
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]
......
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