Commits

Jason Harris committed 2d9ce92

- Move all the multi undo files into the repo/.hg/multiundo/ directory.
- Have two trees. An undo root and a redo root. So it's really clear what is replacing
what.
- Use _copyTree to copy the undo / redo trees around
- Switch to the in memory undo list being dominant.
- Prune old undo states according to the [multiundo] undocount = nnn

Comments (0)

Files changed (2)

 '''Support for multi-level undo and redo using file system snapshots'''
 
-# TODO: non-0-based index
-# TODO: removing elements from the undo state rather than everything at once
-
 import os
 import shutil
+import string
 
 from mercurial import dispatch
 from mercurial import extensions
         self.repo = repo
         self.filecache = {}
         try:
-            self.undolst = open(self.repo.join('multiundo.lst')).read().splitlines()
-        except IOError:
+            self.undolst = open(self._undoListPath()).read().splitlines()
+            data = open(self._undoIndexPath()).read()
+            self.currentidx = int(data)
+            if (self.currentidx + 1 < self._firstIndex()) or (self._lastIndex() < self.currentidx):
+                raise ValueError('currentIndex is outside first to last index')
+        except (IOError, ValueError):
             self.undolst = []
-        try:
-            data = open(self.repo.join('multiundo.idx')).read()
-            self.index = int(data)
-        except (IOError, ValueError):
-            self.index = -1
+            self.currentidx = -1
 
-    def _write_index(self):
-        open(self.repo.join('multiundo.idx'), 'w').write(str(self.index) + '\n')
+    def _firstIndex(self):       return int(self.undolst[ 0].split()[0]) if self.undolst else -1
+    def _lastIndex(self):        return int(self.undolst[-1].split()[0]) if self.undolst else -1
+    def _undoListPath(self):     return self.repo.join(os.path.join('multiundo','multiundo.lst'))
+    def _undoIndexPath(self):    return self.repo.join(os.path.join('multiundo','multiundo.idx'))
+    def _ithUndoRoot(self, idx): return self.repo.join(os.path.join('multiundo','undo', str(idx)))
+    def _ithRedoRoot(self, idx): return self.repo.join(os.path.join('multiundo','redo', str(idx)))
+    
+    def _write_currentidx(self):
+        _ensureDirectoryExistance(os.path.dirname(self._undoIndexPath()))
+        open(self._undoIndexPath(), 'w').write(str(self.currentidx) + '\n')
+
+    def _write_undoList(self):
+        if not self.undolst:
+            _deletePath(self._undoListPath())
+        else:
+            try:
+                path = self._undoListPath()
+                _ensureDirectoryExistance(os.path.dirname(path))
+                f = open(path, 'w')
+                f.write(string.join(self.undolst,"\n"))
+            finally:
+                f.close()
+        
 
     def _delete_stale(self):
-        for i in xrange(self.index + 1, len(self.undolst)):
-            shutil.rmtree(self.repo.join(os.path.join('multiundo', str(i))), True, True)
-        if self.index == -1:
-            if os.path.exists(self.repo.join('multiundo.lst')):
-                os.unlink(self.repo.join('multiundo.lst'))
-        else:
-            f = open(self.repo.join('multiundo.lst'), 'w')
-            try:
-                for i in xrange(0, self.index + 1):
-                    e = self.undolst[i]
-                    f.write('%s\n' % (e,))
-            finally:
-                f.close()
-
-    def _write_undo(self):
+        for i in xrange(self.currentidx + 1, self._lastIndex() + 1):
+            _deletePath(self._ithUndoRoot(i))
+        _deletePath(self.repo.join(os.path.join('multiundo','redo')))
+        self.undolst = self.undolst[:(self.currentidx + 1 - self._firstIndex())] if self.currentidx != -1 else []
+    
+    def _pruneOldUndos(self):
+        keepCount = int(self.repo.ui.config('multiundo', 'undocount', default=5))
+        keepCount = max(keepCount, self._lastIndex() - self.currentidx)
+        currentUndoCount = len(self.undolst)
+        if currentUndoCount > keepCount:
+            newFirst = self._lastIndex() - keepCount
+            for i in xrange(self._firstIndex(), newFirst + 1):
+                _deletePath(self._ithRedoRoot(i))
+                _deletePath(self._ithUndoRoot(i))
+            self.undolst = self.undolst[-keepCount:]
+    
+    def _addNewUndoState(self):
         global command
-        open(self.repo.join('multiundo.lst'), 'a').write(str(self.index).rjust(3) + ' ' + command + '\n')
+        self.currentidx += 1
+        self.undolst.append(str(self.currentidx).rjust(3) + ' ' + command + '\n')
 
     def addfile(self, base, fname, hardlink=False):
         global written
         if not written:
             written = True
             self._delete_stale()
-            self.index += 1
-            self._write_index()
-            self._write_undo()
+            self._addNewUndoState()
+            self._pruneOldUndos()
+            self._write_currentidx()
+            self._write_undoList()
 
-        target = self.repo.join(os.path.join('multiundo', str(self.index), sourcebase))
-        if not os.path.exists(os.path.dirname(target)):
-            os.makedirs(os.path.dirname(target))
+        undoRoot = self._ithUndoRoot(self.currentidx)
+        target = os.path.join(undoRoot, sourcebase)
+        _ensureDirectoryExistance(os.path.dirname(target))
 
         self.repo.ui.debug('  Backing up %s to %s\n' % (source, target))
 
 
     def liststates(self, ui):
         ui.write('Available states:\n')
-        if self.index == -1:
+        if self.currentidx == -1:
             ui.write('  (current)\n')
-        for i in xrange(len(self.undolst)):
-            entry = self.undolst[i]
-            ui.write('  %s\n' % (entry,))
-            if self.index == i:
-                ui.write('  (current)\n')
-        if self.index >= len(self.undolst):
+        if self.undolst:
+            for entry in self.undolst:
+                ui.write('  %s\n' % entry)
+                if self.currentidx == int(entry.split()[0]):
+                    ui.write('  (current)\n')
+        if self.currentidx > self._lastIndex():
             ui.write('  (current)\n')
 
-    def _replay(self, ui, swap=False):
-        index = self.index
-        undoroot = self.repo.join(os.path.join('multiundo', str(self.index)))
+    def _undo_replay(self, ui):
+        undoroot = self._ithUndoRoot(self.currentidx)
+        redoRoot = self._ithRedoRoot(self.currentidx)
+        _deletePath(redoRoot)
+        _copyTree(undoroot, self.repo.root, redoRoot)
+        _copyTree(undoroot, undoroot, self.repo.root)
 
-        for root, d, files in os.walk(undoroot):
-            for f in files:
-                source = os.path.join(root, f)
-                target = os.path.join(self.repo.root, source[len(undoroot)+1:])
-                ui.debug('  Copying %s to %s\n' % (source, target))
-                if not os.path.exists(os.path.dirname(target)):
-                    os.path.makedirs(os.path.dirname(target))
-                if swap:
-                    atf = util.atomictempfile(target)
-                    atf.write(open(source, 'rb').read())
-                    shutil.copyfile(target, source)
-                    atf.close()
-                else:
-                    shutil.copyfile(source, target)
-
+    def _redo_replay(self, ui):
+        redoRoot = self._ithRedoRoot(self.currentidx)
+        _copyTree(redoRoot, redoRoot, self.repo.root)
 
     def undo(self, ui):
-        ui.write('Undoing state %d\n' % (self.index,))
-
-        self._replay(ui, swap=self.index + 1 == len(self.undolst))
-
+        ui.write('Undoing state %d\n' % (self.currentidx,))
+        self._undo_replay(ui)
+        self.currentidx -= 1
+        self._write_currentidx()
         ui.write('Done\n')
-        self.index -= 1
-        self._write_index()
 
     def redo(self, ui):
-        self.index += 1
-        ui.write('Redoing state %d\n' % (self.index,))
-
-        if self.index > len(self.undolst):
-            raise util.Abort('nothing to redo')
-
-        self._replay(ui, swap=self.index + 1 == len(self.undolst))
+        self.currentidx += 1
+        ui.write('Redoing state %d\n' % (self.currentidx,))
+        self._redo_replay(ui)
+        self._write_currentidx()
         ui.write('Done\n')
-        self._write_index()
 
 def _list(ui, repo):
     undoer.liststates(ui)
 
 def _clear(ui, repo):
-    lst = repo.join('multiundo.lst')
-    idx = repo.join('multiundo.idx')
-    d = repo.join('multiundo')
+    _deletePath(repo.join('multiundo'))
 
-    for f in [lst, idx, d]:
-        if os.path.exists(f):
-            if os.path.isdir(f):
-                shutil.rmtree(f, True, True)
-            else:
-                os.unlink(f)
+def _deletePath(p):
+    if os.path.exists(p):
+        if os.path.isdir(p):
+            shutil.rmtree(p, True, True)
+        else:
+            os.unlink(p)
+
+def _ensureDirectoryExistance(d):
+    if not os.path.exists(d):
+        os.makedirs(d)
+
+def _copyTree(namesRoot, sourceRoot, targetRoot):
+    for root, d, files in os.walk(namesRoot):
+        for f in files:
+            name = os.path.join(root,f)
+            source = os.path.join(sourceRoot, name[len(namesRoot)+1:])
+            target = os.path.join(targetRoot, name[len(namesRoot)+1:])
+            _copyFile(source, target)
+
+
+def _copyFile(source, target):
+    #ui.debug('  Copying %s to %s\n' % (source, target))
+    if not os.path.exists(os.path.dirname(target)):
+        os.makedirs(os.path.dirname(target))
+    shutil.copyfile(source, target)
 
 def undo(ui, repo, *args, **opts):
     global undoer
     if opts.get('clear'):
         return _clear(ui, repo)
 
-    if undoer.index == -1:
+    if undoer.currentidx < undoer._firstIndex() or undoer.currentidx == -1:
         raise util.Abort('nothing to undo')
 
     u = undoer
     if opts.get('clear'):
         return _clear(ui, repo)
 
-    if undoer.index >= len(undoer.undolst):
+    if undoer.currentidx >= undoer._lastIndex():
         raise util.Abort('nothing to redo')
 
     u = undoer
 
 cmdtable = {
         'undo': (undo, [
-            ('l', 'list', False, ''),
-            ('C', 'clear', False, ''),
+            ('l', 'list', False, 'list the undo states`'),
+            ('C', 'clear', False, 'clear the undo states'),
             ], ''),
         'redo': (redo, [
-            ('l', 'list', False, ''),
-            ('C', 'clear', False, '')
-            ], ''),
+            ('l', 'list', False, 'list the redo states'),
+            ('C', 'clear', False, 'clear the redo states')
+            ], '')
         }
+Test with something like 'python /Development/Mercurial/mercurial-crew/tests/run-tests.py -i --tmpdir=testRepo test-multiundo.t'
 
-  $ echo "[ui]" >> $HGRCPATH
-  $ echo 'logtemplate = {rev}:{node|short}: {desc}\\n' >> $HGRCPATH
-  $ echo "[extensions]" >> $HGRCPATH
-  $ echo "mq =" >> $HGRCPATH
-  $ echo "multiundo = $TESTDIR/multiundo.py" >> $HGRCPATH
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > mq=
+  > multiundo = $TESTDIR/multiundo.py
+  > 
+  > [multiundo]
+  > undocount = 4
+  > [alias]
+  > slog  = log  --template "{rev}:{node|short}: {desc|firstline} \n"
+  > EOF
+
 
 Test with clean repository:
 
   $ hg redo
   Redoing state 0
   Done
-  $ hg log
-  0:9092f1db7931: added a
+  $ hg slog
+  0:9092f1db7931: added a 
 
 Test revert:
 
 
   $ cat a
   a
-
   $ hg undo -l
   Available states:
       0 000000000000 commit -m added a
   $ cat a
   ccc
 
-  $ hg log
-  1:fb120e7f4d23: change a
-  0:9092f1db7931: added a
+  $ hg slog
+  1:fb120e7f4d23: change a 
+  0:9092f1db7931: added a 
+
+
+Second test to exercise the undo count and multiple undos
+
+Initilize the repository
+  $ cd ..
+  $ hg init fish  
+  $ cd fish
+  $ echo carp > carp
+  $ hg commit -Aqm "first fish carp"
+  $ echo cod > cod
+  $ hg commit -Aqm "second fish cod"
+  $ echo tuna > tuna
+  $ hg commit -Aqm "third fish tuna"
+  $ echo bass > bass
+  $ hg commit -Aqm "fourth fish bass"
+  $ echo trout > trout
+  $ hg commit -Aqm "fifth fish trout"
+  $ hg slog
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+  $ hg undo --list
+  Available states:
+      1 bcd55252497e commit -Aqm second fish cod
+      2 1fdc414e7d05 commit -Aqm third fish tuna
+      3 97513b5a89da commit -Aqm fourth fish bass
+      4 c7db66635584 commit -Aqm fifth fish trout
+    (current)
+ 
+Remove the recorded undoes
+  $ hg undo --clear
+  $ echo pike > pike
+  $ hg commit -Am "sixth fish pike"
+  adding pike
+  $ echo ling > ling
+  $ hg commit -Am "seventh fish ling"
+  adding ling
+  $ more carp
+  carp
+  $ hg slog
+  6:35c6905cc353: seventh fish ling 
+  5:3d874f7d61c5: sixth fish pike 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+Do a basic undo
+  $ hg undo --list
+  Available states:
+      0 a2d62ae9b7cd commit -Am sixth fish pike
+      1 3d874f7d61c5 commit -Am seventh fish ling
+    (current)
+  $ hg undo
+  Undoing state 1
+  Done
+  $ hg slog
+  5:3d874f7d61c5: sixth fish pike 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+Redo the operation
+  $ hg undo --list
+  Available states:
+      0 a2d62ae9b7cd commit -Am sixth fish pike
+    (current)
+      1 3d874f7d61c5 commit -Am seventh fish ling
+  $ hg redo
+  Redoing state 1
+  Done
+  $ hg slog
+  6:35c6905cc353: seventh fish ling 
+  5:3d874f7d61c5: sixth fish pike 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+  $ hg undo --list
+  Available states:
+      0 a2d62ae9b7cd commit -Am sixth fish pike
+      1 3d874f7d61c5 commit -Am seventh fish ling
+    (current)
+
+Go back and do a commit
+  $ hg undo
+  Undoing state 1
+  Done
+  $ hg undo
+  Undoing state 0
+  Done
+  $ hg slog
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+  $ echo talapia > talapia
+  $ hg add talapia
+  $ hg commit -m "eighth fish talapia"
+  $ hg undo --list
+  Available states:
+      0 a2d62ae9b7cd add talapia
+      1 a2d62ae9b7cd commit -m eighth fish talapia
+    (current)
+  $ hg slog
+  5:143c6eb00f29: eighth fish talapia 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+
+Change the cotent and see that it is restored even though the content hasn't been committed
+  $ hg undo --clear
+  $ echo tasty > cod
+  $ hg update --clean --rev 5
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat cod
+  cod
+  $ echo nice > cod
+  $ hg update --clean --rev 4
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ cat cod
+  cod
+  $ hg undo --list
+  Available states:
+      0 143c6eb00f29 update --clean --rev 5
+      1 143c6eb00f29 update --clean --rev 4
+    (current)
+
+Get back to cod -> nice
+  $ hg undo
+  Undoing state 1
+  Done
+  $ cat cod
+  nice
+  $ hg slog
+  5:143c6eb00f29: eighth fish talapia 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+Get back to cod -> tasty
+  $ hg undo
+  Undoing state 0
+  Done
+  $ cat cod
+  tasty
+  $ hg slog
+  5:143c6eb00f29: eighth fish talapia 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+We can't go further back than -1
+  $ hg undo
+  abort: nothing to undo
+  [255]
+
+Redo to get cod -> nice
+  $ hg redo
+  Redoing state 0
+  Done
+  $ cat cod
+  nice
+  $ hg slog
+  5:143c6eb00f29: eighth fish talapia 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+Redo to get cod -> cod
+  $ hg redo
+  Redoing state 1
+  Done
+  $ cat cod
+  cod
+  $ hg slog
+  5:143c6eb00f29: eighth fish talapia 
+  4:a2d62ae9b7cd: fifth fish trout 
+  3:c7db66635584: fourth fish bass 
+  2:97513b5a89da: third fish tuna 
+  1:1fdc414e7d05: second fish cod 
+  0:bcd55252497e: first fish carp 
+
+Try to redo when we can't
+  $ hg redo
+  abort: nothing to redo
+  [255]
+
+Clear the undo history
+  $ hg undo --clear
+  $ hg undo
+  abort: nothing to undo
+  [255]
+
+Get the status
+  $ hg status
+  ? ling
+  ? pike