Add more notes and comments to RevisionHistory

This commit is contained in:
David Vierra 2016-09-16 03:51:33 -10:00
parent f9df465874
commit 773f1ab1f9

View File

@ -50,7 +50,7 @@ log = logging.getLogger(__name__)
#
# How to preserve undo history after writing changes?
#
# Problem: the root folder now reflects all changes and cannot be the root folder any more, and must be
# Problem: the root folder now reflects all changes and cannot be the tail node any more, and must be
# moved to the head of the history. All chunks and files in the root folder that are overwritten will be lost
# from the history completely.
#
@ -60,6 +60,60 @@ log = logging.getLogger(__name__)
# then replace the written revision. The reverse revision must also store delete commands for any files/chunks added
# in the written revision. The reverse revision's previousNode should then point forward in the history,
# toward the world folder's new position. In fact, previousNode may not even be needed.
#
#
# ---
# Each node has a 'parentNode' pointer which points to the node that its revision is
# based on. The pointer will point in one of three directions:
#
# Backward if the root node is earlier in the history
# Forward if `writeAllChanges` was previously called (this node is now a "revert" node)
# To an "orphaned" node not in the history if the root node is no longer in the history
# A node will be "orphaned" if, after `writeAllChanges` is called, a new revision is
# created before the root node's current position in the history.
#
# Initial State:
#
# root
# ^-head
#
# After adding several revisions with changes:
#
# root <- 1 <- 2 <- 3
# ^-head
#
# After calling writeAllChanges:
#
# 1' -> 2' -> 3' -> root
# ^-head
#
# After undoing twice:
#
# 1' -> 2' -> 3' -> root
# ^-head
#
#
# Adding revisions to 2' will create an orphan chain starting with 2':
#
# orphanChainIndex -> 2'
#
# -> 3' -> root
# |
# 1' -> 2' <- 4 <- 5
# ^-head
#
# Here, calling writeAllChanges must re-revert the changes in 3' and then 2' before
# applying the changes in 4 and 5. orphanChainIndex points to 2'. We walk this chain
# until it hits root and collect all nodes, including 2', and then apply them to root
# in reverse order. After collapsing the orphan chain, we should have this:
#
# 1' -> root <- 4 <- 5
# ^-head
#
# Then we can call the rest of writeAllChanges as usual:
#
# 1' -> 4' -> 5' -> root
# ^-head
class RevisionChanges(object):
@ -70,6 +124,7 @@ class RevisionChanges(object):
def __repr__(self):
return "RevisionChanges(chunks=%r, files=%r)" % (self.chunks, self.files)
class UndoFolderExists(IOError):
"""
Raised when initializing a RevisionHistory and an existing undo folder is found, but no resume mode is given.
@ -97,16 +152,17 @@ class RevisionHistory(object):
When the RevisionHistory is deleted, all partial folders are removed from disk.
"""
def __init__(self, filename, resume=None):
"""
:param resume: Whether to resume editing if an undo folder already exists. Pass True to resume editing,
Parameters
----------
resume : bool | None
Whether to resume editing if an undo folder already exists. Pass True to resume editing,
False to delete the old world folder, or None to raise an exception if the folder exists.
:type resume: bool | None
:param filename: Path to the world folder to open
:type filename: str | unicode
:rtype: RevisionHistory
filename : str | unicode
Path to the world folder to open
"""
# Create undo folder in the folder containing the root folder.
@ -186,17 +242,28 @@ class RevisionHistory(object):
self.rootNodeIndex = revisionIndex
self.orphanChainIndex = revisionIndex
# Root folder is about to be orphaned. Revisions from revisionIndex on to the root folder won't show up
# in the history, but the world folder needs to stay as is. The node at revisionIndex will point
# forward to the chain leading to the root folder, and the revision about to be created will point
# backward to the node at revisionIndex. When the root folder is saved, it will need to move backwards
# along its subchain until it reaches revisionIndex, deleting revisions as it goes instead of
# reversing them, then move forward to the new revision.
# Root folder won't be orphaned in more than one position in the history. When it needs to be moved
# again because createRevision is called on a previous revision, the intervening revisions are made part
# of the new orphan chain and the newly created revision added as before. When it needs to be moved
# because of a call to writeAllChanges, the orphan chain is collapsed onto the root folder before
# writing changes as usual.
# The root folder is ahead of the requested revision. We want this newly
# created revision to point to the current state of the world folder, but
# we also don't want to modify the root folder right now.
#
# The node at `revisionIndex` captures the current state of the world folder,
# since its chain of `previousNode` pointers will point along a list of
# "reversion" revisions that eventually points to the root folder.
# We allow it to keep this chain while all of the nodes in that chain
# are removed from the revision history, and we store the index
# of this node in `orphanChainIndex` so we can later collapse the orphaned
# chain back onto the root folder during `writeAllChanges()`
#
# Oddly, `rootNodeIndex` now points to this node, which is not the original
# root node. However, it is the "effective" root node because when `createRevision`
# is called again with a revision that comes before the "root node", the
# intervening revisions will be added onto the tail of the orphan chain
# and the `orphanChainIndex` will be updated accordingly.
#
# This also means there is no possibility of having multiple orphaned chains.
# Either the new revision is created after the "effective" root node and is
# created normally, or it is created before this node and the orphan chain
# is extended.
newFolder = self._createRevisionFolder()
@ -305,6 +372,7 @@ class RevisionHistory(object):
orphanNodes.append(orphanChainNode)
orphanChainNode = orphanChainNode.parentNode
# Apply each orphaned node onto the root node.
for progress, orphanChainNode in enumProgress(orphanNodes[::-1], 0, 20):
yield (progress, maxprogress, "Collapsing orphaned chain")
@ -313,17 +381,24 @@ class RevisionHistory(object):
for current, _, status in copyTask:
yield current, maxprogress, status
# Root node now replaces the orphan chain's tail in the history.
# (the nodes ahead and behind of the root node should now point to this node)
self.nodes[self.orphanChainIndex] = self.rootNode
self.orphanChainIndex = None
if requestedIndex == self.rootNodeIndex:
return # nothing to do
elif requestedIndex < self.rootNodeIndex:
# Nodes behind the root node in the history will be re-reverted and replaced
# with plain nodes
direction = -1
indexes = xrange(self.rootNodeIndex-1, requestedIndex-1, -1)
else:
# Nodes ahead of the root node will be reverted and replaced with
# "reversion" nodes.
direction = 1
indexes = xrange(self.rootNodeIndex+1, requestedIndex+1)
log.info("writeAllChanges: moving %s", "forwards" if direction == 1 else "backwards")
for progress, currentIndex in enumProgress(indexes, 20, 80):