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. # 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 # 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 # it under the terms of the GNU General Public License as published by
...@@ -14,8 +14,10 @@ ...@@ -14,8 +14,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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.ChangeSet import ChangeSet
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib import TodoFile from topydo.lib import TodoFile
from topydo.lib import TodoList from topydo.lib import TodoList
from topydo.lib.Config import config from topydo.lib.Config import config
...@@ -28,36 +30,104 @@ class RevertCommand(Command): ...@@ -28,36 +30,104 @@ class RevertCommand(Command):
p_prompt=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._archive = None self._backup = None
self._archive_file = None self._archive_file = None
self._archive = None
def execute(self): def execute(self):
if not super().execute(): if not super().execute():
return False return False
self._backup = ChangeSet()
archive_path = config().archive() archive_path = config().archive()
if archive_path: 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()) 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: try:
last_change.get_backup(self.todolist) if p_arg == 'ls':
last_change.apply(self.todolist, self._archive) self._handle_ls()
if self._archive: elif p_arg.isdigit():
self._archive_file.write(self._archive.print_todos()) self._revert_to_specific(p_arg)
last_change.delete() else:
raise InvalidCommandArgument
except InvalidCommandArgument:
self.error(self.usage())
self.out("Successfully reverted: " + last_change.label) def _handle_ls(self):
except (ValueError, KeyError): num = 1
self.error('No backup was found for the current state of ' for timestamp, change in self._backup:
+ config().todotxt()) 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): def usage(self):
return """Synopsis: revert""" return """Synopsis:
revert [ls]
revert [NUMBER]"""
def help(self): 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. # 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 # 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 # it under the terms of the GNU General Public License as published by
...@@ -46,7 +46,7 @@ class ChangeSet(object): ...@@ -46,7 +46,7 @@ class ChangeSet(object):
def __init__(self, p_todolist=None, p_archive=None, p_label=[]): def __init__(self, p_todolist=None, p_archive=None, p_label=[]):
self.todolist = deepcopy(p_todolist) self.todolist = deepcopy(p_todolist)
self.archive = deepcopy(p_archive) self.archive = deepcopy(p_archive)
self.timestamp = str(int(time.time())) self.timestamp = str(time.time())
self.label = ' '.join(p_label) self.label = ' '.join(p_label)
try: try:
...@@ -56,6 +56,11 @@ class ChangeSet(object): ...@@ -56,6 +56,11 @@ class ChangeSet(object):
self._read() 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): def _read(self):
""" """
Reads backup file from json_file property and sets backup_dict property Reads backup file from json_file property and sets backup_dict property
...@@ -158,15 +163,18 @@ class ChangeSet(object): ...@@ -158,15 +163,18 @@ class ChangeSet(object):
for changeset in index[backup_limit:]: for changeset in index[backup_limit:]:
self.delete(changeset[0], p_write=False) 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, Retrieves a backup for p_timestamp or p_todolist (if p_timestamp is not
archive and label attributes to appropriate data from it. specified) from backup file and sets timestamp, todolist, archive and
label attributes to appropriate data from it.
""" """
change_hash = hash_todolist(p_todolist) if not p_timestamp:
change_hash = hash_todolist(p_todolist)
index = self._get_index() index = self._get_index()
self.timestamp = index[[change[1] for change in index].index(change_hash)][0] self.timestamp = index[[change[1] for change in index].index(change_hash)][0]
else:
self.timestamp = p_timestamp
d = self.backup_dict[self.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