strip.py
221 lines
| 8.2 KiB
| text/x-python
|
PythonLexer
/ hgext / strip.py
Mads Kiilerich
|
r23139 | """strip changesets and their descendants from history | ||
Pierre-Yves David
|
r19826 | |||
Javi Merino
|
r19945 | This extension allows you to strip changesets and all their descendants from the | ||
Pierre-Yves David
|
r19826 | repository. See the command help for details. | ||
""" | ||||
Pierre-Yves David
|
r19823 | from mercurial.i18n import _ | ||
Pierre-Yves David
|
r19825 | from mercurial.node import nullid | ||
from mercurial.lock import release | ||||
Pierre-Yves David
|
r19826 | from mercurial import cmdutil, hg, scmutil, util | ||
Matt Mackall
|
r24709 | from mercurial import repair, bookmarks, merge | ||
Pierre-Yves David
|
r19822 | |||
cmdtable = {} | ||||
command = cmdutil.command(cmdtable) | ||||
testedwith = 'internal' | ||||
Pierre-Yves David
|
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): | ||||
FUJIWARA Katsunori
|
r24471 | wctx.sub(s).bailifchanged(True) | ||
if s not in bctx.substate or bctx.sub(s).dirty(): | ||||
Pierre-Yves David
|
r19823 | inclsubs.append(s) | ||
return inclsubs | ||||
Pierre-Yves David
|
r19824 | def checklocalchanges(repo, force=False, excsuffix=''): | ||
cmdutil.checkunfinished(repo) | ||||
Martin von Zweigbergk
|
r22925 | s = repo.status() | ||
Pierre-Yves David
|
r19824 | if not force: | ||
Martin von Zweigbergk
|
r22925 | if s.modified or s.added or s.removed or s.deleted: | ||
Pierre-Yves David
|
r19824 | _("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)) | ||||
Martin von Zweigbergk
|
r22925 | return s | ||
Pierre-Yves David
|
r19824 | |||
Jordi Gutiérrez Hermoso
|
r22057 | def strip(ui, repo, revs, update=True, backup=True, force=None, bookmark=None): | ||
Pierre-Yves David
|
r19825 | wlock = lock = None | ||
try: | ||||
wlock = repo.wlock() | ||||
lock = repo.lock() | ||||
if update: | ||||
checklocalchanges(repo, force=force) | ||||
urev, p2 = repo.changelog.parents(revs[0]) | ||||
Matt Mackall
|
r20009 | if (util.safehasattr(repo, 'mq') and | ||
p2 != nullid | ||||
and p2 in [x.node for x in repo.mq.applied]): | ||||
Pierre-Yves David
|
r19825 | urev = p2 | ||
hg.clean(repo, urev) | ||||
repo.dirstate.write() | ||||
repair.strip(ui, repo, revs, backup) | ||||
David Soria Parra
|
r21847 | |||
marks = repo._bookmarks | ||||
if bookmark: | ||||
if bookmark == repo._bookmarkcurrent: | ||||
bookmarks.unsetcurrent(repo) | ||||
del marks[bookmark] | ||||
marks.write() | ||||
ui.write(_("bookmark '%s' deleted\n") % bookmark) | ||||
Pierre-Yves David
|
r19825 | finally: | ||
release(lock, wlock) | ||||
Pierre-Yves David
|
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)')), | ||||
('', 'no-backup', None, _('no backups')), | ||||
('', 'nobackup', None, _('no backups (DEPRECATED)')), | ||||
('n', '', None, _('ignored (DEPRECATED)')), | ||||
Yuya Nishihara
|
r24364 | ('k', 'keep', None, _("do not modify working directory during " | ||
"strip")), | ||||
Pierre-Yves David
|
r19826 | ('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. | ||||
""" | ||||
Jordi Gutiérrez Hermoso
|
r22057 | backup = True | ||
if opts.get('no_backup') or opts.get('nobackup'): | ||||
backup = False | ||||
Pierre-Yves David
|
r19826 | |||
cl = repo.changelog | ||||
revs = list(revs) + opts.get('rev') | ||||
revs = set(scmutil.revrange(repo, revs)) | ||||
Siddharth Agarwal
|
r20096 | wlock = repo.wlock() | ||
try: | ||||
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) | ||||
Pierre-Yves David
|
r19826 | |||
Siddharth Agarwal
|
r20096 | 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'): | ||||
Siddharth Agarwal
|
r20102 | 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] | ||||
Siddharth Agarwal
|
r20096 | |||
Siddharth Agarwal
|
r20102 | # 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()) | ||||
Siddharth Agarwal
|
r20096 | |||
Siddharth Agarwal
|
r20102 | # 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) | ||||
Siddharth Agarwal
|
r20096 | |||
Siddharth Agarwal
|
r20102 | repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles) | ||
repo.dirstate.write() | ||||
Matt Mackall
|
r24709 | |||
# clear resolve state | ||||
ms = merge.mergestate(repo) | ||||
ms.reset(repo['.'].node()) | ||||
Siddharth Agarwal
|
r20102 | update = False | ||
Siddharth Agarwal
|
r20096 | |||
Pierre-Yves David
|
r19826 | |||
Siddharth Agarwal
|
r20096 | strip(ui, repo, revs, backup=backup, update=update, | ||
David Soria Parra
|
r21847 | force=opts.get('force'), bookmark=opts.get('bookmark')) | ||
Siddharth Agarwal
|
r20096 | finally: | ||
wlock.release() | ||||
Pierre-Yves David
|
r19826 | |||
return 0 | ||||