mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 01:07:15 +03:00
9771b5695b
Summary: . Test Plan: test-reset.t is passing now Reviewers: #sourcecontrol, ttung Differential Revision: https://phabricator.fb.com/D2925005
251 lines
8.1 KiB
Python
251 lines
8.1 KiB
Python
# Copyright 2014 Facebook Inc.
|
|
#
|
|
"""reset the active bookmark and working copy to a desired revision"""
|
|
|
|
from mercurial.i18n import _
|
|
from mercurial.node import short, hex
|
|
from mercurial import extensions, merge, scmutil, hg
|
|
from mercurial import cmdutil, obsolete, repair, util, bundlerepo, error
|
|
from mercurial import exchange, phases
|
|
from mercurial import lock as lockmod
|
|
import struct, os, glob, binascii
|
|
|
|
cmdtable = {}
|
|
command = cmdutil.command(cmdtable)
|
|
testedwith = 'internal'
|
|
|
|
def _isevolverepo(repo):
|
|
try:
|
|
return extensions.find('evolve')
|
|
except KeyError:
|
|
return None
|
|
|
|
def _isahash(rev):
|
|
try:
|
|
binascii.unhexlify(rev)
|
|
return True
|
|
except TypeError:
|
|
return False
|
|
|
|
@command("reset", [
|
|
('C', 'clean', None, _('wipe the working copy clean when resetting')),
|
|
('k', 'keep', None, _('keeps the old commits the bookmark pointed to')),
|
|
], _('hg reset [REV]'))
|
|
def reset(ui, repo, *args, **opts):
|
|
"""moves the active bookmark and working copy parent to the desired rev
|
|
|
|
The reset command is for moving your active bookmark and working copy to a
|
|
different location. This is useful for undoing commits, amends, etc.
|
|
|
|
By default, the working copy content is not touched, so you will have
|
|
pending changes after the reset. If --clean/-C is specified, the working
|
|
copy contents will be overwritten to match the destination revision, and you
|
|
will not have any pending changes.
|
|
|
|
After your bookmark and working copy have been moved, the command will
|
|
delete any commits that belonged only to that bookmark. Use --keep/-k to
|
|
avoid deleting any commits.
|
|
"""
|
|
rev = args[0] if args else '.'
|
|
oldctx = repo['.']
|
|
|
|
wlock = None
|
|
try:
|
|
wlock = repo.wlock()
|
|
# Ensure we have an active bookmark
|
|
bookmark = bmactive(repo)
|
|
if not bookmark:
|
|
ui.warn(_('resetting without an active bookmark\n'))
|
|
|
|
ctx = _revive(repo, rev)
|
|
_moveto(repo, bookmark, ctx, clean=opts.get('clean'))
|
|
if not opts.get('keep'):
|
|
_deleteunreachable(repo, oldctx)
|
|
finally:
|
|
wlock.release()
|
|
|
|
def _revive(repo, rev):
|
|
"""Brings the given rev back into the repository. Finding it in backup
|
|
bundles if necessary.
|
|
"""
|
|
if _isahash(rev):
|
|
# If it appears to be a hash, just read it directly.
|
|
try:
|
|
rev = scmutil.revsingle(repo, rev).node()
|
|
return repo[rev]
|
|
except error.FilteredRepoLookupError:
|
|
return _touch(repo, repo.unfiltered()[rev])
|
|
except error.RepoLookupError:
|
|
# It could either be a revset or a stripped commit.
|
|
pass
|
|
|
|
try:
|
|
revs = scmutil.revrange(repo, [rev])
|
|
if len(revs) > 1:
|
|
raise error.Abort(_('exactly one revision must be specified'))
|
|
if len(revs) == 1:
|
|
return repo[revs.first()]
|
|
except error.RepoLookupError:
|
|
revs = []
|
|
|
|
return _pullbundle(repo, rev)
|
|
|
|
def _touch(repo, rev):
|
|
"""Touch the given rev and any of its ancestors to bring it back into the
|
|
repository.
|
|
"""
|
|
unfi = repo.unfiltered()
|
|
evolve = _isevolverepo(repo.ui)
|
|
if evolve:
|
|
needtouch = unfi.revs('::%d & obsolete()', rev)
|
|
opts = {
|
|
# Use 'duplicate' to avoid divergence. We may want something better
|
|
# later.
|
|
'allowdivergence': False,
|
|
'duplicate': True,
|
|
'rev': [],
|
|
}
|
|
evolve.touch(repo.ui, unfi, *needtouch, **opts)
|
|
# Return the newly revived version of rev.
|
|
# Use tip, since it's guaranteed to be the latest commit in the
|
|
# repo, since it will have been the last commit revived.
|
|
return repo['tip']
|
|
else:
|
|
raise error.Abort("unable to revive '%s' because it is hidden and "
|
|
"evolve is disabled" % rev)
|
|
|
|
def _pullbundle(repo, rev):
|
|
"""Find the given rev in a backup bundle and pull it back into the
|
|
repository.
|
|
"""
|
|
other, rev = _findbundle(repo, rev)
|
|
if not other:
|
|
raise error.Abort("could not find '%s' in the repo or the backup"
|
|
" bundles" % rev)
|
|
lock = repo.lock()
|
|
try:
|
|
oldtip = len(repo)
|
|
exchange.pull(repo, other, heads=[rev])
|
|
|
|
tr = repo.transaction("phase")
|
|
nodes = (c.node() for c in repo.set('%d:', oldtip))
|
|
phases.retractboundary(repo, tr, 1, nodes)
|
|
tr.close()
|
|
finally:
|
|
lock.release()
|
|
|
|
if rev not in repo:
|
|
raise error.Abort("unable to get rev %s from repo" % rev)
|
|
|
|
return repo[rev]
|
|
|
|
def _findbundle(repo, rev):
|
|
"""Returns the backup bundle that contains the given rev. If found, it
|
|
returns the bundle peer and the full rev hash. If not found, it return None
|
|
and the given rev value.
|
|
"""
|
|
ui = repo.ui
|
|
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)
|
|
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)
|
|
other = hg.peer(repo, {}, source)
|
|
|
|
quiet = ui.quiet
|
|
try:
|
|
ui.quiet = True
|
|
ret = bundlerepo.getremotechanges(ui, repo, other, None, None, None)
|
|
localother, chlist, cleanupfn = ret
|
|
for node in chlist:
|
|
if hex(node).startswith(rev):
|
|
return other, node
|
|
except error.LookupError:
|
|
continue
|
|
finally:
|
|
ui.quiet = quiet
|
|
|
|
return None, rev
|
|
|
|
def _moveto(repo, bookmark, ctx, clean=False):
|
|
"""Moves the given bookmark and the working copy to the given revision.
|
|
By default it does not overwrite the working copy contents unless clean is
|
|
True.
|
|
|
|
Assumes the wlock is already taken.
|
|
"""
|
|
# Move working copy over
|
|
if clean:
|
|
merge.update(repo, ctx.node(),
|
|
False, # not a branchmerge
|
|
True, # force overwriting files
|
|
None) # not a partial update
|
|
else:
|
|
# Mark any files that are different between the two as normal-lookup
|
|
# so they show up correctly in hg status afterwards.
|
|
wctx = repo[None]
|
|
m1 = wctx.manifest()
|
|
m2 = ctx.manifest()
|
|
diff = m1.diff(m2)
|
|
|
|
changedfiles = []
|
|
changedfiles.extend(diff.iterkeys())
|
|
|
|
dirstate = repo.dirstate
|
|
dirchanges = [f for f in dirstate if dirstate[f] != 'n']
|
|
changedfiles.extend(dirchanges)
|
|
|
|
if changedfiles or ctx.node() != repo['.'].node():
|
|
dirstate.beginparentchange()
|
|
dirstate.rebuild(ctx.node(), m2, changedfiles)
|
|
dirstate.endparentchange()
|
|
|
|
# Move bookmark over
|
|
if bookmark:
|
|
lock = tr = None
|
|
try:
|
|
lock = repo.lock()
|
|
tr = repo.transaction('reset')
|
|
repo._bookmarks[bookmark] = ctx.node()
|
|
repo._bookmarks.recordchange(tr)
|
|
tr.close()
|
|
finally:
|
|
lockmod.release(lock, tr)
|
|
|
|
def _deleteunreachable(repo, ctx):
|
|
"""Deletes all ancestor and descendant commits of the given revision that
|
|
aren't reachable from another bookmark.
|
|
"""
|
|
keepheads = "bookmark() + ."
|
|
try:
|
|
extensions.find('remotenames')
|
|
keepheads += " + remotenames()"
|
|
except KeyError:
|
|
pass
|
|
hiderevs = repo.revs('::%s - ::(%r)', ctx.rev(), keepheads)
|
|
if hiderevs:
|
|
lock = None
|
|
try:
|
|
lock = repo.lock()
|
|
if _isevolverepo(repo.ui):
|
|
markers = []
|
|
for rev in hiderevs:
|
|
markers.append((repo[rev], ()))
|
|
obsolete.createmarkers(repo, markers)
|
|
repo.ui.status(_("%d changesets pruned\n") % len(hiderevs))
|
|
else:
|
|
repair.strip(repo.ui, repo,
|
|
[repo.changelog.node(r) for r in hiderevs])
|
|
finally:
|
|
lockmod.release(lock)
|
|
|
|
### bookmarks api compatibility layer ###
|
|
def bmactive(repo):
|
|
try:
|
|
return repo._activebookmark
|
|
except AttributeError:
|
|
return repo._bookmarkcurrent
|