strip.py
217 lines
| 8.0 KiB
| text/x-python
|
PythonLexer
/ hgext / strip.py
|
r19826 | """This extension contains the strip commands. | ||
This extensions allows to strip changesets and all their descendants from the | ||||
repository. See the command help for details. | ||||
""" | ||||
|
r19823 | from mercurial.i18n import _ | ||
|
r19825 | from mercurial.node import nullid | ||
from mercurial.lock import release | ||||
|
r19826 | from mercurial import cmdutil, hg, scmutil, util | ||
from mercurial import repair, bookmarks | ||||
|
r19822 | |||
cmdtable = {} | ||||
command = cmdutil.command(cmdtable) | ||||
testedwith = 'internal' | ||||
|
r19823 | |||
def checksubstate(repo, baserev=None): | ||||
'''return list of subrepos at a different revision than substate. | ||||
Abort if any subrepos have uncommitted changes.''' | ||||
inclsubs = [] | ||||
wctx = repo[None] | ||||
if baserev: | ||||
bctx = repo[baserev] | ||||
else: | ||||
bctx = wctx.parents()[0] | ||||
for s in sorted(wctx.substate): | ||||
if wctx.sub(s).dirty(True): | ||||
raise util.Abort( | ||||
_("uncommitted changes in subrepository %s") % s) | ||||
elif s not in bctx.substate or bctx.sub(s).dirty(): | ||||
inclsubs.append(s) | ||||
return inclsubs | ||||
|
r19824 | def checklocalchanges(repo, force=False, excsuffix=''): | ||
cmdutil.checkunfinished(repo) | ||||
m, a, r, d = repo.status()[:4] | ||||
if not force: | ||||
if (m or a or r or d): | ||||
_("local changes found") # i18n tool detection | ||||
raise util.Abort(_("local changes found" + excsuffix)) | ||||
if checksubstate(repo): | ||||
_("local changed subrepos found") # i18n tool detection | ||||
raise util.Abort(_("local changed subrepos found" + excsuffix)) | ||||
return m, a, r, d | ||||
|
r19825 | def strip(ui, repo, revs, update=True, backup="all", force=None): | ||
wlock = lock = None | ||||
try: | ||||
wlock = repo.wlock() | ||||
lock = repo.lock() | ||||
if update: | ||||
checklocalchanges(repo, force=force) | ||||
urev, p2 = repo.changelog.parents(revs[0]) | ||||
if p2 != nullid and p2 in [x.node for x in repo.mq.applied]: | ||||
urev = p2 | ||||
hg.clean(repo, urev) | ||||
repo.dirstate.write() | ||||
repair.strip(ui, repo, revs, backup) | ||||
finally: | ||||
release(lock, wlock) | ||||
|
r19826 | |||
@command("strip", | ||||
[ | ||||
('r', 'rev', [], _('strip specified revision (optional, ' | ||||
'can specify revisions without this ' | ||||
'option)'), _('REV')), | ||||
('f', 'force', None, _('force removal of changesets, discard ' | ||||
'uncommitted changes (no backup)')), | ||||
('b', 'backup', None, _('bundle only changesets with local revision' | ||||
' number greater than REV which are not' | ||||
' descendants of REV (DEPRECATED)')), | ||||
('', 'no-backup', None, _('no backups')), | ||||
('', 'nobackup', None, _('no backups (DEPRECATED)')), | ||||
('n', '', None, _('ignored (DEPRECATED)')), | ||||
('k', 'keep', None, _("do not modify working copy during strip")), | ||||
('B', 'bookmark', '', _("remove revs only reachable from given" | ||||
" bookmark"))], | ||||
_('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...')) | ||||
def stripcmd(ui, repo, *revs, **opts): | ||||
"""strip changesets and all their descendants from the repository | ||||
The strip command removes the specified changesets and all their | ||||
descendants. If the working directory has uncommitted changes, the | ||||
operation is aborted unless the --force flag is supplied, in which | ||||
case changes will be discarded. | ||||
If a parent of the working directory is stripped, then the working | ||||
directory will automatically be updated to the most recent | ||||
available ancestor of the stripped parent after the operation | ||||
completes. | ||||
Any stripped changesets are stored in ``.hg/strip-backup`` as a | ||||
bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can | ||||
be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`, | ||||
where BUNDLE is the bundle file created by the strip. Note that | ||||
the local revision numbers will in general be different after the | ||||
restore. | ||||
Use the --no-backup option to discard the backup bundle once the | ||||
operation completes. | ||||
Strip is not a history-rewriting operation and can be used on | ||||
changesets in the public phase. But if the stripped changesets have | ||||
been pushed to a remote repository you will likely pull them again. | ||||
Return 0 on success. | ||||
""" | ||||
backup = 'all' | ||||
if opts.get('backup'): | ||||
backup = 'strip' | ||||
elif opts.get('no_backup') or opts.get('nobackup'): | ||||
backup = 'none' | ||||
cl = repo.changelog | ||||
revs = list(revs) + opts.get('rev') | ||||
revs = set(scmutil.revrange(repo, revs)) | ||||
if opts.get('bookmark'): | ||||
mark = opts.get('bookmark') | ||||
marks = repo._bookmarks | ||||
if mark not in marks: | ||||
raise util.Abort(_("bookmark '%s' not found") % mark) | ||||
# If the requested bookmark is not the only one pointing to a | ||||
# a revision we have to only delete the bookmark and not strip | ||||
# anything. revsets cannot detect that case. | ||||
uniquebm = True | ||||
for m, n in marks.iteritems(): | ||||
if m != mark and n == repo[mark].node(): | ||||
uniquebm = False | ||||
break | ||||
if uniquebm: | ||||
rsrevs = repo.revs("ancestors(bookmark(%s)) - " | ||||
"ancestors(head() and not bookmark(%s)) - " | ||||
"ancestors(bookmark() and not bookmark(%s))", | ||||
mark, mark, mark) | ||||
revs.update(set(rsrevs)) | ||||
if not revs: | ||||
del marks[mark] | ||||
marks.write() | ||||
ui.write(_("bookmark '%s' deleted\n") % mark) | ||||
if not revs: | ||||
raise util.Abort(_('empty revision set')) | ||||
descendants = set(cl.descendants(revs)) | ||||
strippedrevs = revs.union(descendants) | ||||
roots = revs.difference(descendants) | ||||
update = False | ||||
# if one of the wdir parent is stripped we'll need | ||||
# to update away to an earlier revision | ||||
for p in repo.dirstate.parents(): | ||||
if p != nullid and cl.rev(p) in strippedrevs: | ||||
update = True | ||||
break | ||||
rootnodes = set(cl.node(r) for r in roots) | ||||
q = getattr(repo, 'mq', None) | ||||
if q is not None and q.applied: | ||||
# refresh queue state if we're about to strip | ||||
# applied patches | ||||
if cl.rev(repo.lookup('qtip')) in strippedrevs: | ||||
q.applieddirty = True | ||||
start = 0 | ||||
end = len(q.applied) | ||||
for i, statusentry in enumerate(q.applied): | ||||
if statusentry.node in rootnodes: | ||||
# if one of the stripped roots is an applied | ||||
# patch, only part of the queue is stripped | ||||
start = i | ||||
break | ||||
del q.applied[start:end] | ||||
q.savedirty() | ||||
revs = sorted(rootnodes) | ||||
if update and opts.get('keep'): | ||||
wlock = repo.wlock() | ||||
try: | ||||
urev, p2 = repo.changelog.parents(revs[0]) | ||||
if (util.safehasattr(repo, 'mq') and p2 != nullid | ||||
and p2 in [x.node for x in repo.mq.applied]): | ||||
urev = p2 | ||||
uctx = repo[urev] | ||||
# only reset the dirstate for files that would actually change | ||||
# between the working context and uctx | ||||
descendantrevs = repo.revs("%s::." % uctx.rev()) | ||||
changedfiles = [] | ||||
for rev in descendantrevs: | ||||
# blindly reset the files, regardless of what actually changed | ||||
changedfiles.extend(repo[rev].files()) | ||||
# reset files that only changed in the dirstate too | ||||
dirstate = repo.dirstate | ||||
dirchanges = [f for f in dirstate if dirstate[f] != 'n'] | ||||
changedfiles.extend(dirchanges) | ||||
repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles) | ||||
repo.dirstate.write() | ||||
update = False | ||||
finally: | ||||
wlock.release() | ||||
if opts.get('bookmark'): | ||||
if mark == repo._bookmarkcurrent: | ||||
bookmarks.setcurrent(repo, None) | ||||
del marks[mark] | ||||
marks.write() | ||||
ui.write(_("bookmark '%s' deleted\n") % mark) | ||||
strip(ui, repo, revs, backup=backup, update=update, force=opts.get('force')) | ||||
return 0 | ||||