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.
--- 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