strip.py
287 lines
| 9.1 KiB
| text/x-python
|
PythonLexer
/ hgext / strip.py
|
r23139 | """strip changesets and their descendants from history | ||
|
r19826 | |||
|
r19945 | This extension allows you to strip changesets and all their descendants from the | ||
|
r19826 | repository. See the command help for details. | ||
""" | ||||
|
r28377 | from __future__ import absolute_import | ||
|
r29205 | from mercurial.i18n import _ | ||
|
r43359 | from mercurial.pycompat import getattr | ||
|
r28377 | from mercurial import ( | ||
bookmarks as bookmarksmod, | ||||
cmdutil, | ||||
error, | ||||
hg, | ||||
lock as lockmod, | ||||
|
r45383 | mergestate as mergestatemod, | ||
|
r28377 | node as nodemod, | ||
|
r32897 | pycompat, | ||
|
r32337 | registrar, | ||
|
r28377 | repair, | ||
scmutil, | ||||
util, | ||||
) | ||||
|
r43346 | |||
|
r28377 | nullid = nodemod.nullid | ||
release = lockmod.release | ||||
|
r19822 | |||
cmdtable = {} | ||||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r19823 | |||
|
r43346 | |||
|
r42690 | def checklocalchanges(repo, force=False): | ||
|
r22925 | s = repo.status() | ||
|
r19824 | if not force: | ||
|
r42732 | cmdutil.checkunfinished(repo) | ||
|
r42691 | cmdutil.bailifchanged(repo) | ||
|
r42732 | else: | ||
cmdutil.checkunfinished(repo, skipmerge=True) | ||||
|
r22925 | return s | ||
|
r19824 | |||
|
r43346 | |||
|
r34575 | def _findupdatetarget(repo, nodes): | ||
unode, p2 = repo.changelog.parents(nodes[0]) | ||||
|
r34622 | currentbranch = repo[None].branch() | ||
|
r34575 | |||
|
r43346 | if ( | ||
|
r43347 | util.safehasattr(repo, b'mq') | ||
|
r43346 | and p2 != nullid | ||
and p2 in [x.node for x in repo.mq.applied] | ||||
): | ||||
|
r34575 | unode = p2 | ||
|
r34622 | elif currentbranch != repo[unode].branch(): | ||
|
r43347 | pwdir = b'parents(wdir())' | ||
revset = b'max(((parents(%ln::%r) + %r) - %ln::%r) and branch(%s))' | ||||
|
r43346 | branchtarget = repo.revs( | ||
revset, nodes, pwdir, pwdir, nodes, pwdir, currentbranch | ||||
) | ||||
|
r34622 | if branchtarget: | ||
cl = repo.changelog | ||||
unode = cl.node(branchtarget.first()) | ||||
|
r34575 | |||
return unode | ||||
|
r43346 | |||
def strip( | ||||
ui, | ||||
repo, | ||||
revs, | ||||
update=True, | ||||
backup=True, | ||||
force=None, | ||||
bookmarks=None, | ||||
soft=False, | ||||
): | ||||
|
r32919 | with repo.wlock(), repo.lock(): | ||
|
r19825 | |||
if update: | ||||
checklocalchanges(repo, force=force) | ||||
|
r34575 | urev = _findupdatetarget(repo, revs) | ||
|
r19825 | hg.clean(repo, urev) | ||
|
r26748 | repo.dirstate.write(repo.currenttransaction()) | ||
|
r19825 | |||
|
r41960 | if soft: | ||
repair.softstrip(ui, repo, revs, backup) | ||||
else: | ||||
repair.strip(ui, repo, revs, backup) | ||||
|
r21847 | |||
|
r26972 | repomarks = repo._bookmarks | ||
|
r27029 | if bookmarks: | ||
|
r43347 | with repo.transaction(b'strip') as tr: | ||
|
r27052 | if repo._activebookmark in bookmarks: | ||
bookmarksmod.deactivate(repo) | ||||
|
r33488 | repomarks.applychanges(repo, tr, [(b, None) for b in bookmarks]) | ||
|
r27872 | for bookmark in sorted(bookmarks): | ||
|
r43347 | ui.write(_(b"bookmark '%s' deleted\n") % bookmark) | ||
|
r19826 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b"strip", | ||
|
r43346 | [ | ||
( | ||||
|
r43347 | b'r', | ||
b'rev', | ||||
|
r43346 | [], | ||
_( | ||||
|
r43347 | b'strip specified revision (optional, ' | ||
b'can specify revisions without this ' | ||||
b'option)' | ||||
|
r43346 | ), | ||
|
r43347 | _(b'REV'), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'f', | ||
b'force', | ||||
|
r43346 | None, | ||
_( | ||||
|
r43347 | b'force removal of changesets, discard ' | ||
b'uncommitted changes (no backup)' | ||||
|
r43346 | ), | ||
), | ||||
|
r43347 | (b'', b'no-backup', None, _(b'do not save backup bundle')), | ||
|
r43387 | (b'', b'nobackup', None, _(b'do not save backup bundle (DEPRECATED)'),), | ||
|
r43347 | (b'n', b'', None, _(b'ignored (DEPRECATED)')), | ||
( | ||||
b'k', | ||||
b'keep', | ||||
None, | ||||
|
r43387 | _(b"do not modify working directory during strip"), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'B', | ||
b'bookmark', | ||||
|
r43346 | [], | ||
|
r43387 | _(b"remove revs only reachable from given bookmark"), | ||
|
r43347 | _(b'BOOKMARK'), | ||
|
r43346 | ), | ||
( | ||||
|
r43347 | b'', | ||
b'soft', | ||||
|
r43346 | None, | ||
|
r43347 | _(b"simply drop changesets from visible history (EXPERIMENTAL)"), | ||
|
r43346 | ), | ||
], | ||||
|
r43347 | _(b'hg strip [-k] [-f] [-B bookmark] [-r] REV...'), | ||
|
r43346 | helpcategory=command.CATEGORY_MAINTENANCE, | ||
) | ||||
|
r19826 | 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. | ||||
""" | ||||
|
r32897 | opts = pycompat.byteskwargs(opts) | ||
|
r22057 | backup = True | ||
|
r43347 | if opts.get(b'no_backup') or opts.get(b'nobackup'): | ||
|
r22057 | backup = False | ||
|
r19826 | |||
cl = repo.changelog | ||||
|
r43347 | revs = list(revs) + opts.get(b'rev') | ||
|
r19826 | revs = set(scmutil.revrange(repo, revs)) | ||
|
r27839 | with repo.wlock(): | ||
|
r43347 | bookmarks = set(opts.get(b'bookmark')) | ||
|
r27029 | if bookmarks: | ||
|
r26972 | repomarks = repo._bookmarks | ||
|
r27029 | if not bookmarks.issubset(repomarks): | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"bookmark '%s' not found") | ||
% b','.join(sorted(bookmarks - set(repomarks.keys()))) | ||||
|
r43346 | ) | ||
|
r20096 | |||
# 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. | ||||
|
r27029 | nodetobookmarks = {} | ||
|
r43375 | for mark, node in pycompat.iteritems(repomarks): | ||
|
r27029 | nodetobookmarks.setdefault(node, []).append(mark) | ||
for marks in nodetobookmarks.values(): | ||||
if bookmarks.issuperset(marks): | ||||
|
r38146 | rsrevs = scmutil.bookmarkrevs(repo, marks[0]) | ||
|
r27030 | revs.update(set(rsrevs)) | ||
|
r20096 | if not revs: | ||
|
r43347 | with repo.lock(), repo.transaction(b'bookmark') as tr: | ||
|
r33488 | bmchanges = [(b, None) for b in bookmarks] | ||
repomarks.applychanges(repo, tr, bmchanges) | ||||
|
r32920 | for bookmark in sorted(bookmarks): | ||
|
r43347 | ui.write(_(b"bookmark '%s' deleted\n") % bookmark) | ||
|
r20096 | |||
if not revs: | ||||
|
r43347 | raise error.Abort(_(b'empty revision set')) | ||
|
r20096 | |||
descendants = set(cl.descendants(revs)) | ||||
strippedrevs = revs.union(descendants) | ||||
roots = revs.difference(descendants) | ||||
# if one of the wdir parent is stripped we'll need | ||||
# to update away to an earlier revision | ||||
|
r43346 | update = any( | ||
p != nullid and cl.rev(p) in strippedrevs | ||||
for p in repo.dirstate.parents() | ||||
) | ||||
|
r20096 | |||
|
r44937 | rootnodes = {cl.node(r) for r in roots} | ||
|
r19826 | |||
|
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 | ||||
|
r43347 | if cl.rev(repo.lookup(b'qtip')) in strippedrevs: | ||
|
r20096 | 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) | ||||
|
r43347 | if update and opts.get(b'keep'): | ||
|
r34575 | urev = _findupdatetarget(repo, revs) | ||
|
r20102 | uctx = repo[urev] | ||
|
r20096 | |||
|
r20102 | # only reset the dirstate for files that would actually change | ||
# between the working context and uctx | ||||
r46081 | descendantrevs = repo.revs(b"only(., %d)", uctx.rev()) | |||
|
r20102 | changedfiles = [] | ||
for rev in descendantrevs: | ||||
# blindly reset the files, regardless of what actually changed | ||||
changedfiles.extend(repo[rev].files()) | ||||
|
r20096 | |||
|
r20102 | # reset files that only changed in the dirstate too | ||
dirstate = repo.dirstate | ||||
|
r43347 | dirchanges = [f for f in dirstate if dirstate[f] != b'n'] | ||
|
r20102 | changedfiles.extend(dirchanges) | ||
|
r20096 | |||
|
r20102 | repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles) | ||
|
r26748 | repo.dirstate.write(repo.currenttransaction()) | ||
|
r24709 | |||
# clear resolve state | ||||
|
r46062 | mergestatemod.mergestate.clean(repo) | ||
|
r24709 | |||
|
r20102 | update = False | ||
|
r20096 | |||
|
r43346 | strip( | ||
ui, | ||||
repo, | ||||
revs, | ||||
backup=backup, | ||||
update=update, | ||||
|
r43347 | force=opts.get(b'force'), | ||
|
r43346 | bookmarks=bookmarks, | ||
|
r43347 | soft=opts[b'soft'], | ||
|
r43346 | ) | ||
|
r19826 | |||
return 0 | ||||