sapling/backups.py
Laurent Charignon 25d0314a5b backups: make hg backups evolve aware
Summary:
backups: make hg backups evolve aware
With this patch: hg backups urges evolve user to look at hg reflog to recover
hidden commits. This is because evolve's commands are using obsolescence
markers to hide commits instead of stripping them.

Test Plan: Cross repo dependency, not sure how to make an automated test

Reviewers: durham, pyd

Differential Revision: https://phabricator.fb.com/D2088486

Tasks: 6534705
2015-05-20 12:29:09 -07:00

131 lines
5.0 KiB
Python

# backups.py
#
# Copyright 2013 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""display recently made backups to recover stripped commits"""
from mercurial import extensions, util, cmdutil, commands, error, bundlerepo
from mercurial import hg, changegroup, exchange
from mercurial.extensions import wrapfunction
from hgext import pager
from mercurial.node import hex, nullrev, nullid, short
from mercurial.i18n import _
import errno, os, re, glob, time
pager.attended.append('backups')
cmdtable = {}
command = cmdutil.command(cmdtable)
testedwith = 'internal'
msgwithevolve = """Evolve is enabled so no commit should be stripped unless you
explicitely called hg strip. hg backups will show you the stripped commits.
If you are trying to recover a commit hidden from a previous command, use hg
reflog to get its sha1 and you will be able to access it directly without
recovering a backup."""
@command('^backups', [
('', 'recover', '', 'brings the specified commit back into the repository')
] + commands.logopts, _('hg backups [--recover HASH]'))
def backups(ui, repo, *pats, **opts):
'''lists the commits available in backup bundles
Without any arguments, this command prints a list of the commits in each
backup bundle.
--recover takes a commit hash and unbundles the first bundle that contains
that commit hash, which puts that commit back in your repository.
--verbose will print the entire commit message and the bundle path for that
backup.
'''
try:
if extensions.find('evolve'):
# Warn evolve users that they probably don't want to use backups
# but reflog instead
ui.warn(msgwithevolve)
except KeyError:
pass
backuppath = repo.join("strip-backup")
backups = filter(os.path.isfile, glob.glob(backuppath + "/*.hg"))
backups.sort(key=lambda x: os.path.getmtime(x), reverse=True)
opts['bundle'] = ''
opts['force'] = None
def display(other, chlist, displayer):
limit = cmdutil.loglimit(opts)
if opts.get('newest_first'):
chlist.reverse()
count = 0
for n in chlist:
if limit is not None and count >= limit:
break
parents = [p for p in other.changelog.parents(n) if p != nullid]
if opts.get('no_merges') and len(parents) == 2:
continue
count += 1
displayer.show(other[n])
recovernode = opts.get('recover')
if recovernode:
if recovernode in repo:
ui.warn("%s already exists in the repo\n" % recovernode)
return
else:
ui.status("Recover commits using: hg backups --recover <commit hash>\n", label="status.removed")
for backup in backups:
# Much of this is copied from the hg incoming logic
source = os.path.relpath(backup, os.getcwd())
source = ui.expandpath(source)
source, branches = hg.parseurl(source, opts.get('branch'))
try:
other = hg.peer(repo, opts, source)
except error.LookupError as ex:
ui.status("\nwarning: unable to open bundle %s - missing parent rev %s\n" %
(source, short(ex.name)))
continue
revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev'))
if revs:
revs = [other.lookup(rev) for rev in revs]
quiet = ui.quiet
try:
ui.quiet = True
other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
revs, opts["bundle"], opts["force"])
except error.LookupError:
continue
finally:
ui.quiet = quiet
try:
if chlist:
if recovernode:
lock = repo.lock()
try:
if recovernode in other:
ui.status("Unbundling %s\n" % (recovernode))
f = hg.openpath(ui, source)
gen = exchange.readbundle(ui, f, source)
modheads = changegroup.addchangegroup(repo, gen, 'unbundle', 'bundle:' + source)
break
finally:
lock.release()
else:
backupdate = os.path.getmtime(source)
backupdate = time.strftime('%a %H:%M, %Y-%m-%d', time.localtime(backupdate))
ui.status("\n%s\n" % (backupdate.ljust(50)))
if not ui.verbose:
opts['template'] = "{label('status.modified', node|short)} {desc|firstline}\n"
else:
ui.status("%s%s\n" % ("bundle:".ljust(13), source))
displayer = cmdutil.show_changeset(ui, other, opts, False)
display(other, chlist, displayer)
displayer.close()
finally:
cleanupfn()