[PATCH 1 of 2 V2] shelve: always backup shelves instead of deleting them

Colin Chan colinchan at fb.com
Thu Jul 2 11:50:14 CDT 2015


# HG changeset patch
# User Colin Chan <colinchan at fb.com>
# Date 1435781582 25200
#      Wed Jul 01 13:13:02 2015 -0700
# Node ID 2fb801a45897eaa5f7cb45225b2f9db4c7ec73d6
# Parent  c76e8d14383a44a740d986d87db6f58276fb57e8
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.

diff --git a/hgext/shelve.py b/hgext/shelve.py
--- a/hgext/shelve.py
+++ b/hgext/shelve.py
@@ -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']
diff --git a/tests/test-shelve.t b/tests/test-shelve.t
--- a/tests/test-shelve.t
+++ b/tests/test-shelve.t
@@ -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


More information about the Mercurial-devel mailing list