shelve: always backup shelves instead of deleting them
authorColin Chan <colinchan@fb.com>
Wed, 01 Jul 2015 13:13:02 -0700
changeset 25712 8a6264a2ee60
parent 25711 26579a91f4fb
child 25713 2ca116614cfc
shelve: always backup shelves instead of deleting them Instead of being deleted, shelve files are now moved into the .hg/shelve-backup directory. This is designed similarly to how strip saves backups into .ht/strip-backup. The goal is to prevent data loss especially when using unshelve. There are cases in which a user can complete an unshelve but lose some of the data that was shelved by, for example, resolving merge conflicts incorrectly. Storing backups will allow the user to recover the data that was shelved, at the expense of using more disk space over time.
hgext/shelve.py
tests/test-shelve.t
--- a/hgext/shelve.py	Tue Jun 30 22:02:40 2015 -0700
+++ b/hgext/shelve.py	Wed Jul 01 13:13:02 2015 -0700
@@ -22,6 +22,7 @@
 """
 
 import collections
+import itertools
 from mercurial.i18n import _
 from mercurial.node import nullid, nullrev, bin, hex
 from mercurial import changegroup, cmdutil, scmutil, phases, commands
@@ -48,6 +49,7 @@
         self.repo = repo
         self.name = name
         self.vfs = scmutil.vfs(repo.join('shelved'))
+        self.backupvfs = scmutil.vfs(repo.join('shelve-backup'))
         self.ui = self.repo.ui
         if filetype:
             self.fname = name + '.' + filetype
@@ -60,8 +62,22 @@
     def filename(self):
         return self.vfs.join(self.fname)
 
-    def unlink(self):
-        util.unlink(self.filename())
+    def backupfilename(self):
+        def gennames(base):
+            yield base
+            base, ext = base.rsplit('.', 1)
+            for i in itertools.count(1):
+                yield '%s-%d.%s' % (base, i, ext)
+
+        name = self.backupvfs.join(self.fname)
+        for n in gennames(name):
+            if not self.backupvfs.exists(n):
+                return n
+
+    def movetobackup(self):
+        if not self.backupvfs.isdir():
+            self.backupvfs.makedir()
+        util.rename(self.filename(), self.backupfilename())
 
     def stat(self):
         return self.vfs.stat(self.fname)
@@ -281,7 +297,7 @@
         for (name, _type) in repo.vfs.readdir('shelved'):
             suffix = name.rsplit('.', 1)[-1]
             if suffix in ('hg', 'patch'):
-                shelvedfile(repo, name).unlink()
+                shelvedfile(repo, name).movetobackup()
     finally:
         lockmod.release(wlock)
 
@@ -293,7 +309,7 @@
     try:
         for name in pats:
             for suffix in 'hg patch'.split():
-                shelvedfile(repo, name, suffix).unlink()
+                shelvedfile(repo, name, suffix).movetobackup()
     except OSError as err:
         if err.errno != errno.ENOENT:
             raise
@@ -442,7 +458,7 @@
     """remove related files after an unshelve"""
     if not opts['keep']:
         for filetype in 'hg patch'.split():
-            shelvedfile(repo, name, filetype).unlink()
+            shelvedfile(repo, name, filetype).movetobackup()
 
 def unshelvecontinue(ui, repo, state, opts):
     """subcommand to continue an in-progress unshelve"""
@@ -505,18 +521,19 @@
     restore. If none is given, the most recent shelved change is used.
 
     If a shelved change is applied successfully, the bundle that
-    contains the shelved changes is deleted afterwards.
+    contains the shelved changes is moved to a backup location
+    (.hg/shelve-backup).
 
     Since you can restore a shelved change on top of an arbitrary
     commit, it is possible that unshelving will result in a conflict
     between your changes and the commits you are unshelving onto. If
     this occurs, you must resolve the conflict, then use
     ``--continue`` to complete the unshelve operation. (The bundle
-    will not be deleted until you successfully complete the unshelve.)
+    will not be moved until you successfully complete the unshelve.)
 
     (Alternatively, you can use ``--abort`` to abandon an unshelve
     that causes a conflict. This reverts the unshelved changes, and
-    does not delete the bundle.)
+    leaves the bundle in place.)
     """
     abortf = opts['abort']
     continuef = opts['continue']
--- a/tests/test-shelve.t	Tue Jun 30 22:02:40 2015 -0700
+++ b/tests/test-shelve.t	Wed Jul 01 13:13:02 2015 -0700
@@ -85,6 +85,12 @@
   nothing changed
   [1]
 
+make sure shelve files were backed up
+
+  $ ls .hg/shelve-backup
+  default.hg
+  default.patch
+
 create an mq patch - shelving should work fine with a patch applied
 
   $ echo n > n
@@ -154,6 +160,14 @@
   $ hg shelve -d default
   $ hg qfinish -a -q
 
+ensure shelve backups aren't overwritten
+
+  $ ls .hg/shelve-backup/
+  default-1.hg
+  default-1.patch
+  default.hg
+  default.patch
+
 local edits should not prevent a shelved change from applying
 
   $ printf "z\na\n" > a/a