1
"""strip changesets and their descendents from history
3
This extension allows you to strip changesets and all their descendants from the
4
repository. See the command help for details.
6
from mercurial.i18n import _
7
from mercurial.node import nullid
8
from mercurial.lock import release
9
from mercurial import cmdutil, hg, scmutil, util
10
from mercurial import repair, bookmarks
13
command = cmdutil.command(cmdtable)
14
testedwith = 'internal'
16
def checksubstate(repo, baserev=None):
17
'''return list of subrepos at a different revision than substate.
18
Abort if any subrepos have uncommitted changes.'''
24
bctx = wctx.parents()[0]
25
for s in sorted(wctx.substate):
26
if wctx.sub(s).dirty(True):
28
_("uncommitted changes in subrepository %s") % s)
29
elif s not in bctx.substate or bctx.sub(s).dirty():
33
def checklocalchanges(repo, force=False, excsuffix=''):
34
cmdutil.checkunfinished(repo)
35
m, a, r, d = repo.status()[:4]
37
if (m or a or r or d):
38
_("local changes found") # i18n tool detection
39
raise util.Abort(_("local changes found" + excsuffix))
40
if checksubstate(repo):
41
_("local changed subrepos found") # i18n tool detection
42
raise util.Abort(_("local changed subrepos found" + excsuffix))
45
def strip(ui, repo, revs, update=True, backup="all", force=None):
52
checklocalchanges(repo, force=force)
53
urev, p2 = repo.changelog.parents(revs[0])
54
if p2 != nullid and p2 in [x.node for x in repo.mq.applied]:
59
repair.strip(ui, repo, revs, backup)
66
('r', 'rev', [], _('strip specified revision (optional, '
67
'can specify revisions without this '
68
'option)'), _('REV')),
69
('f', 'force', None, _('force removal of changesets, discard '
70
'uncommitted changes (no backup)')),
71
('b', 'backup', None, _('bundle only changesets with local revision'
72
' number greater than REV which are not'
73
' descendants of REV (DEPRECATED)')),
74
('', 'no-backup', None, _('no backups')),
75
('', 'nobackup', None, _('no backups (DEPRECATED)')),
76
('n', '', None, _('ignored (DEPRECATED)')),
77
('k', 'keep', None, _("do not modify working copy during strip")),
78
('B', 'bookmark', '', _("remove revs only reachable from given"
80
_('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
81
def stripcmd(ui, repo, *revs, **opts):
82
"""strip changesets and all their descendants from the repository
84
The strip command removes the specified changesets and all their
85
descendants. If the working directory has uncommitted changes, the
86
operation is aborted unless the --force flag is supplied, in which
87
case changes will be discarded.
89
If a parent of the working directory is stripped, then the working
90
directory will automatically be updated to the most recent
91
available ancestor of the stripped parent after the operation
94
Any stripped changesets are stored in ``.hg/strip-backup`` as a
95
bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
96
be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
97
where BUNDLE is the bundle file created by the strip. Note that
98
the local revision numbers will in general be different after the
101
Use the --no-backup option to discard the backup bundle once the
104
Strip is not a history-rewriting operation and can be used on
105
changesets in the public phase. But if the stripped changesets have
106
been pushed to a remote repository you will likely pull them again.
111
if opts.get('backup'):
113
elif opts.get('no_backup') or opts.get('nobackup'):
117
revs = list(revs) + opts.get('rev')
118
revs = set(scmutil.revrange(repo, revs))
120
if opts.get('bookmark'):
121
mark = opts.get('bookmark')
122
marks = repo._bookmarks
123
if mark not in marks:
124
raise util.Abort(_("bookmark '%s' not found") % mark)
126
# If the requested bookmark is not the only one pointing to a
127
# a revision we have to only delete the bookmark and not strip
128
# anything. revsets cannot detect that case.
130
for m, n in marks.iteritems():
131
if m != mark and n == repo[mark].node():
135
rsrevs = repo.revs("ancestors(bookmark(%s)) - "
136
"ancestors(head() and not bookmark(%s)) - "
137
"ancestors(bookmark() and not bookmark(%s))",
139
revs.update(set(rsrevs))
143
ui.write(_("bookmark '%s' deleted\n") % mark)
146
raise util.Abort(_('empty revision set'))
148
descendants = set(cl.descendants(revs))
149
strippedrevs = revs.union(descendants)
150
roots = revs.difference(descendants)
153
# if one of the wdir parent is stripped we'll need
154
# to update away to an earlier revision
155
for p in repo.dirstate.parents():
156
if p != nullid and cl.rev(p) in strippedrevs:
160
rootnodes = set(cl.node(r) for r in roots)
162
q = getattr(repo, 'mq', None)
163
if q is not None and q.applied:
164
# refresh queue state if we're about to strip
166
if cl.rev(repo.lookup('qtip')) in strippedrevs:
167
q.applieddirty = True
170
for i, statusentry in enumerate(q.applied):
171
if statusentry.node in rootnodes:
172
# if one of the stripped roots is an applied
173
# patch, only part of the queue is stripped
176
del q.applied[start:end]
179
revs = sorted(rootnodes)
180
if update and opts.get('keep'):
183
urev, p2 = repo.changelog.parents(revs[0])
184
if (util.safehasattr(repo, 'mq') and p2 != nullid
185
and p2 in [x.node for x in repo.mq.applied]):
189
# only reset the dirstate for files that would actually change
190
# between the working context and uctx
191
descendantrevs = repo.revs("%s::." % uctx.rev())
193
for rev in descendantrevs:
194
# blindly reset the files, regardless of what actually changed
195
changedfiles.extend(repo[rev].files())
197
# reset files that only changed in the dirstate too
198
dirstate = repo.dirstate
199
dirchanges = [f for f in dirstate if dirstate[f] != 'n']
200
changedfiles.extend(dirchanges)
202
repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
203
repo.dirstate.write()
208
if opts.get('bookmark'):
209
if mark == repo._bookmarkcurrent:
210
bookmarks.setcurrent(repo, None)
213
ui.write(_("bookmark '%s' deleted\n") % mark)
215
strip(ui, repo, revs, backup=backup, update=update, force=opts.get('force'))