mercurial/strip.py
changeset 45865 d7a508a75d72
parent 45514 93a0f3ba36bb
child 45942 89a2afe31e82
equal deleted inserted replaced
45864:11842aad3195 45865:d7a508a75d72
       
     1 from __future__ import absolute_import
       
     2 
       
     3 from .i18n import _
       
     4 from .pycompat import getattr
       
     5 from . import (
       
     6     bookmarks as bookmarksmod,
       
     7     cmdutil,
       
     8     error,
       
     9     hg,
       
    10     lock as lockmod,
       
    11     mergestate as mergestatemod,
       
    12     node as nodemod,
       
    13     pycompat,
       
    14     registrar,
       
    15     repair,
       
    16     scmutil,
       
    17     util,
       
    18 )
       
    19 
       
    20 nullid = nodemod.nullid
       
    21 release = lockmod.release
       
    22 
       
    23 cmdtable = {}
       
    24 command = registrar.command(cmdtable)
       
    25 
       
    26 
       
    27 def checklocalchanges(repo, force=False):
       
    28     s = repo.status()
       
    29     if not force:
       
    30         cmdutil.checkunfinished(repo)
       
    31         cmdutil.bailifchanged(repo)
       
    32     else:
       
    33         cmdutil.checkunfinished(repo, skipmerge=True)
       
    34     return s
       
    35 
       
    36 
       
    37 def _findupdatetarget(repo, nodes):
       
    38     unode, p2 = repo.changelog.parents(nodes[0])
       
    39     currentbranch = repo[None].branch()
       
    40 
       
    41     if (
       
    42         util.safehasattr(repo, b'mq')
       
    43         and p2 != nullid
       
    44         and p2 in [x.node for x in repo.mq.applied]
       
    45     ):
       
    46         unode = p2
       
    47     elif currentbranch != repo[unode].branch():
       
    48         pwdir = b'parents(wdir())'
       
    49         revset = b'max(((parents(%ln::%r) + %r) - %ln::%r) and branch(%s))'
       
    50         branchtarget = repo.revs(
       
    51             revset, nodes, pwdir, pwdir, nodes, pwdir, currentbranch
       
    52         )
       
    53         if branchtarget:
       
    54             cl = repo.changelog
       
    55             unode = cl.node(branchtarget.first())
       
    56 
       
    57     return unode
       
    58 
       
    59 
       
    60 def strip(
       
    61     ui,
       
    62     repo,
       
    63     revs,
       
    64     update=True,
       
    65     backup=True,
       
    66     force=None,
       
    67     bookmarks=None,
       
    68     soft=False,
       
    69 ):
       
    70     with repo.wlock(), repo.lock():
       
    71 
       
    72         if update:
       
    73             checklocalchanges(repo, force=force)
       
    74             urev = _findupdatetarget(repo, revs)
       
    75             hg.clean(repo, urev)
       
    76             repo.dirstate.write(repo.currenttransaction())
       
    77 
       
    78         if soft:
       
    79             repair.softstrip(ui, repo, revs, backup)
       
    80         else:
       
    81             repair.strip(ui, repo, revs, backup)
       
    82 
       
    83         repomarks = repo._bookmarks
       
    84         if bookmarks:
       
    85             with repo.transaction(b'strip') as tr:
       
    86                 if repo._activebookmark in bookmarks:
       
    87                     bookmarksmod.deactivate(repo)
       
    88                 repomarks.applychanges(repo, tr, [(b, None) for b in bookmarks])
       
    89             for bookmark in sorted(bookmarks):
       
    90                 ui.write(_(b"bookmark '%s' deleted\n") % bookmark)
       
    91 
       
    92 
       
    93 @command(
       
    94     b"debugstrip",
       
    95     [
       
    96         (
       
    97             b'r',
       
    98             b'rev',
       
    99             [],
       
   100             _(
       
   101                 b'strip specified revision (optional, '
       
   102                 b'can specify revisions without this '
       
   103                 b'option)'
       
   104             ),
       
   105             _(b'REV'),
       
   106         ),
       
   107         (
       
   108             b'f',
       
   109             b'force',
       
   110             None,
       
   111             _(
       
   112                 b'force removal of changesets, discard '
       
   113                 b'uncommitted changes (no backup)'
       
   114             ),
       
   115         ),
       
   116         (b'', b'no-backup', None, _(b'do not save backup bundle')),
       
   117         (b'', b'nobackup', None, _(b'do not save backup bundle (DEPRECATED)'),),
       
   118         (b'n', b'', None, _(b'ignored  (DEPRECATED)')),
       
   119         (
       
   120             b'k',
       
   121             b'keep',
       
   122             None,
       
   123             _(b"do not modify working directory during strip"),
       
   124         ),
       
   125         (
       
   126             b'B',
       
   127             b'bookmark',
       
   128             [],
       
   129             _(b"remove revs only reachable from given bookmark"),
       
   130             _(b'BOOKMARK'),
       
   131         ),
       
   132         (
       
   133             b'',
       
   134             b'soft',
       
   135             None,
       
   136             _(b"simply drop changesets from visible history (EXPERIMENTAL)"),
       
   137         ),
       
   138     ],
       
   139     _(b'hg debugstrip [-k] [-f] [-B bookmark] [-r] REV...'),
       
   140     helpcategory=command.CATEGORY_MAINTENANCE,
       
   141 )
       
   142 def debugstrip(ui, repo, *revs, **opts):
       
   143     """strip changesets and all their descendants from the repository
       
   144 
       
   145     The strip command removes the specified changesets and all their
       
   146     descendants. If the working directory has uncommitted changes, the
       
   147     operation is aborted unless the --force flag is supplied, in which
       
   148     case changes will be discarded.
       
   149 
       
   150     If a parent of the working directory is stripped, then the working
       
   151     directory will automatically be updated to the most recent
       
   152     available ancestor of the stripped parent after the operation
       
   153     completes.
       
   154 
       
   155     Any stripped changesets are stored in ``.hg/strip-backup`` as a
       
   156     bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
       
   157     be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
       
   158     where BUNDLE is the bundle file created by the strip. Note that
       
   159     the local revision numbers will in general be different after the
       
   160     restore.
       
   161 
       
   162     Use the --no-backup option to discard the backup bundle once the
       
   163     operation completes.
       
   164 
       
   165     Strip is not a history-rewriting operation and can be used on
       
   166     changesets in the public phase. But if the stripped changesets have
       
   167     been pushed to a remote repository you will likely pull them again.
       
   168 
       
   169     Return 0 on success.
       
   170     """
       
   171     opts = pycompat.byteskwargs(opts)
       
   172     backup = True
       
   173     if opts.get(b'no_backup') or opts.get(b'nobackup'):
       
   174         backup = False
       
   175 
       
   176     cl = repo.changelog
       
   177     revs = list(revs) + opts.get(b'rev')
       
   178     revs = set(scmutil.revrange(repo, revs))
       
   179 
       
   180     with repo.wlock():
       
   181         bookmarks = set(opts.get(b'bookmark'))
       
   182         if bookmarks:
       
   183             repomarks = repo._bookmarks
       
   184             if not bookmarks.issubset(repomarks):
       
   185                 raise error.Abort(
       
   186                     _(b"bookmark '%s' not found")
       
   187                     % b','.join(sorted(bookmarks - set(repomarks.keys())))
       
   188                 )
       
   189 
       
   190             # If the requested bookmark is not the only one pointing to a
       
   191             # a revision we have to only delete the bookmark and not strip
       
   192             # anything. revsets cannot detect that case.
       
   193             nodetobookmarks = {}
       
   194             for mark, node in pycompat.iteritems(repomarks):
       
   195                 nodetobookmarks.setdefault(node, []).append(mark)
       
   196             for marks in nodetobookmarks.values():
       
   197                 if bookmarks.issuperset(marks):
       
   198                     rsrevs = scmutil.bookmarkrevs(repo, marks[0])
       
   199                     revs.update(set(rsrevs))
       
   200             if not revs:
       
   201                 with repo.lock(), repo.transaction(b'bookmark') as tr:
       
   202                     bmchanges = [(b, None) for b in bookmarks]
       
   203                     repomarks.applychanges(repo, tr, bmchanges)
       
   204                 for bookmark in sorted(bookmarks):
       
   205                     ui.write(_(b"bookmark '%s' deleted\n") % bookmark)
       
   206 
       
   207         if not revs:
       
   208             raise error.Abort(_(b'empty revision set'))
       
   209 
       
   210         descendants = set(cl.descendants(revs))
       
   211         strippedrevs = revs.union(descendants)
       
   212         roots = revs.difference(descendants)
       
   213 
       
   214         # if one of the wdir parent is stripped we'll need
       
   215         # to update away to an earlier revision
       
   216         update = any(
       
   217             p != nullid and cl.rev(p) in strippedrevs
       
   218             for p in repo.dirstate.parents()
       
   219         )
       
   220 
       
   221         rootnodes = {cl.node(r) for r in roots}
       
   222 
       
   223         q = getattr(repo, 'mq', None)
       
   224         if q is not None and q.applied:
       
   225             # refresh queue state if we're about to strip
       
   226             # applied patches
       
   227             if cl.rev(repo.lookup(b'qtip')) in strippedrevs:
       
   228                 q.applieddirty = True
       
   229                 start = 0
       
   230                 end = len(q.applied)
       
   231                 for i, statusentry in enumerate(q.applied):
       
   232                     if statusentry.node in rootnodes:
       
   233                         # if one of the stripped roots is an applied
       
   234                         # patch, only part of the queue is stripped
       
   235                         start = i
       
   236                         break
       
   237                 del q.applied[start:end]
       
   238                 q.savedirty()
       
   239 
       
   240         revs = sorted(rootnodes)
       
   241         if update and opts.get(b'keep'):
       
   242             urev = _findupdatetarget(repo, revs)
       
   243             uctx = repo[urev]
       
   244 
       
   245             # only reset the dirstate for files that would actually change
       
   246             # between the working context and uctx
       
   247             descendantrevs = repo.revs(b"only(., %d)", uctx.rev())
       
   248             changedfiles = []
       
   249             for rev in descendantrevs:
       
   250                 # blindly reset the files, regardless of what actually changed
       
   251                 changedfiles.extend(repo[rev].files())
       
   252 
       
   253             # reset files that only changed in the dirstate too
       
   254             dirstate = repo.dirstate
       
   255             dirchanges = [f for f in dirstate if dirstate[f] != b'n']
       
   256             changedfiles.extend(dirchanges)
       
   257 
       
   258             repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
       
   259             repo.dirstate.write(repo.currenttransaction())
       
   260 
       
   261             # clear resolve state
       
   262             mergestatemod.mergestate.clean(repo)
       
   263 
       
   264             update = False
       
   265 
       
   266         strip(
       
   267             ui,
       
   268             repo,
       
   269             revs,
       
   270             backup=backup,
       
   271             update=update,
       
   272             force=opts.get(b'force'),
       
   273             bookmarks=bookmarks,
       
   274             soft=opts[b'soft'],
       
   275         )
       
   276 
       
   277     return 0