##// END OF EJS Templates
strip: strip a list of bookmarks...
Shubhanshu Agrawal -
r27029:8279c5d1 default
parent child Browse files
Show More
@@ -1,221 +1,227
1 """strip changesets and their descendants from history
1 """strip changesets and their descendants from history
2
2
3 This extension allows you to strip changesets and all their descendants from the
3 This extension allows you to strip changesets and all their descendants from the
4 repository. See the command help for details.
4 repository. See the command help for details.
5 """
5 """
6 from mercurial.i18n import _
6 from mercurial.i18n import _
7 from mercurial.node import nullid
7 from mercurial.node import nullid
8 from mercurial.lock import release
8 from mercurial.lock import release
9 from mercurial import cmdutil, hg, scmutil, util, error
9 from mercurial import cmdutil, hg, scmutil, util, error
10 from mercurial import repair, bookmarks as bookmarksmod , merge
10 from mercurial import repair, bookmarks as bookmarksmod , merge
11
11
12 cmdtable = {}
12 cmdtable = {}
13 command = cmdutil.command(cmdtable)
13 command = cmdutil.command(cmdtable)
14 # Note for extension authors: ONLY specify testedwith = 'internal' for
14 # Note for extension authors: ONLY specify testedwith = 'internal' for
15 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
15 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
16 # be specifying the version(s) of Mercurial they are tested with, or
16 # be specifying the version(s) of Mercurial they are tested with, or
17 # leave the attribute unspecified.
17 # leave the attribute unspecified.
18 testedwith = 'internal'
18 testedwith = 'internal'
19
19
20 def checksubstate(repo, baserev=None):
20 def checksubstate(repo, baserev=None):
21 '''return list of subrepos at a different revision than substate.
21 '''return list of subrepos at a different revision than substate.
22 Abort if any subrepos have uncommitted changes.'''
22 Abort if any subrepos have uncommitted changes.'''
23 inclsubs = []
23 inclsubs = []
24 wctx = repo[None]
24 wctx = repo[None]
25 if baserev:
25 if baserev:
26 bctx = repo[baserev]
26 bctx = repo[baserev]
27 else:
27 else:
28 bctx = wctx.parents()[0]
28 bctx = wctx.parents()[0]
29 for s in sorted(wctx.substate):
29 for s in sorted(wctx.substate):
30 wctx.sub(s).bailifchanged(True)
30 wctx.sub(s).bailifchanged(True)
31 if s not in bctx.substate or bctx.sub(s).dirty():
31 if s not in bctx.substate or bctx.sub(s).dirty():
32 inclsubs.append(s)
32 inclsubs.append(s)
33 return inclsubs
33 return inclsubs
34
34
35 def checklocalchanges(repo, force=False, excsuffix=''):
35 def checklocalchanges(repo, force=False, excsuffix=''):
36 cmdutil.checkunfinished(repo)
36 cmdutil.checkunfinished(repo)
37 s = repo.status()
37 s = repo.status()
38 if not force:
38 if not force:
39 if s.modified or s.added or s.removed or s.deleted:
39 if s.modified or s.added or s.removed or s.deleted:
40 _("local changes found") # i18n tool detection
40 _("local changes found") # i18n tool detection
41 raise error.Abort(_("local changes found" + excsuffix))
41 raise error.Abort(_("local changes found" + excsuffix))
42 if checksubstate(repo):
42 if checksubstate(repo):
43 _("local changed subrepos found") # i18n tool detection
43 _("local changed subrepos found") # i18n tool detection
44 raise error.Abort(_("local changed subrepos found" + excsuffix))
44 raise error.Abort(_("local changed subrepos found" + excsuffix))
45 return s
45 return s
46
46
47 def strip(ui, repo, revs, update=True, backup=True, force=None, bookmark=None):
47 def strip(ui, repo, revs, update=True, backup=True, force=None, bookmarks=None):
48 wlock = lock = None
48 wlock = lock = None
49 try:
49 try:
50 wlock = repo.wlock()
50 wlock = repo.wlock()
51 lock = repo.lock()
51 lock = repo.lock()
52
52
53 if update:
53 if update:
54 checklocalchanges(repo, force=force)
54 checklocalchanges(repo, force=force)
55 urev, p2 = repo.changelog.parents(revs[0])
55 urev, p2 = repo.changelog.parents(revs[0])
56 if (util.safehasattr(repo, 'mq') and
56 if (util.safehasattr(repo, 'mq') and
57 p2 != nullid
57 p2 != nullid
58 and p2 in [x.node for x in repo.mq.applied]):
58 and p2 in [x.node for x in repo.mq.applied]):
59 urev = p2
59 urev = p2
60 hg.clean(repo, urev)
60 hg.clean(repo, urev)
61 repo.dirstate.write(repo.currenttransaction())
61 repo.dirstate.write(repo.currenttransaction())
62
62
63 repair.strip(ui, repo, revs, backup)
63 repair.strip(ui, repo, revs, backup)
64
64
65 repomarks = repo._bookmarks
65 repomarks = repo._bookmarks
66 if bookmark:
66 if bookmarks:
67 if bookmark == repo._activebookmark:
67 if repo._activebookmark in bookmarks:
68 bookmarksmod.deactivate(repo)
68 bookmarksmod.deactivate(repo)
69 for bookmark in bookmarks:
69 del repomarks[bookmark]
70 del repomarks[bookmark]
70 repomarks.write()
71 repomarks.write()
72 for bookmark in sorted(bookmarks):
71 ui.write(_("bookmark '%s' deleted\n") % bookmark)
73 ui.write(_("bookmark '%s' deleted\n") % bookmark)
72 finally:
74 finally:
73 release(lock, wlock)
75 release(lock, wlock)
74
76
75
77
76 @command("strip",
78 @command("strip",
77 [
79 [
78 ('r', 'rev', [], _('strip specified revision (optional, '
80 ('r', 'rev', [], _('strip specified revision (optional, '
79 'can specify revisions without this '
81 'can specify revisions without this '
80 'option)'), _('REV')),
82 'option)'), _('REV')),
81 ('f', 'force', None, _('force removal of changesets, discard '
83 ('f', 'force', None, _('force removal of changesets, discard '
82 'uncommitted changes (no backup)')),
84 'uncommitted changes (no backup)')),
83 ('', 'no-backup', None, _('no backups')),
85 ('', 'no-backup', None, _('no backups')),
84 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
86 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
85 ('n', '', None, _('ignored (DEPRECATED)')),
87 ('n', '', None, _('ignored (DEPRECATED)')),
86 ('k', 'keep', None, _("do not modify working directory during "
88 ('k', 'keep', None, _("do not modify working directory during "
87 "strip")),
89 "strip")),
88 ('B', 'bookmark', '', _("remove revs only reachable from given"
90 ('B', 'bookmark', '', _("remove revs only reachable from given"
89 " bookmark"))],
91 " bookmark"))],
90 _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
92 _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
91 def stripcmd(ui, repo, *revs, **opts):
93 def stripcmd(ui, repo, *revs, **opts):
92 """strip changesets and all their descendants from the repository
94 """strip changesets and all their descendants from the repository
93
95
94 The strip command removes the specified changesets and all their
96 The strip command removes the specified changesets and all their
95 descendants. If the working directory has uncommitted changes, the
97 descendants. If the working directory has uncommitted changes, the
96 operation is aborted unless the --force flag is supplied, in which
98 operation is aborted unless the --force flag is supplied, in which
97 case changes will be discarded.
99 case changes will be discarded.
98
100
99 If a parent of the working directory is stripped, then the working
101 If a parent of the working directory is stripped, then the working
100 directory will automatically be updated to the most recent
102 directory will automatically be updated to the most recent
101 available ancestor of the stripped parent after the operation
103 available ancestor of the stripped parent after the operation
102 completes.
104 completes.
103
105
104 Any stripped changesets are stored in ``.hg/strip-backup`` as a
106 Any stripped changesets are stored in ``.hg/strip-backup`` as a
105 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
107 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
106 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
108 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
107 where BUNDLE is the bundle file created by the strip. Note that
109 where BUNDLE is the bundle file created by the strip. Note that
108 the local revision numbers will in general be different after the
110 the local revision numbers will in general be different after the
109 restore.
111 restore.
110
112
111 Use the --no-backup option to discard the backup bundle once the
113 Use the --no-backup option to discard the backup bundle once the
112 operation completes.
114 operation completes.
113
115
114 Strip is not a history-rewriting operation and can be used on
116 Strip is not a history-rewriting operation and can be used on
115 changesets in the public phase. But if the stripped changesets have
117 changesets in the public phase. But if the stripped changesets have
116 been pushed to a remote repository you will likely pull them again.
118 been pushed to a remote repository you will likely pull them again.
117
119
118 Return 0 on success.
120 Return 0 on success.
119 """
121 """
120 backup = True
122 backup = True
121 if opts.get('no_backup') or opts.get('nobackup'):
123 if opts.get('no_backup') or opts.get('nobackup'):
122 backup = False
124 backup = False
123
125
124 cl = repo.changelog
126 cl = repo.changelog
125 revs = list(revs) + opts.get('rev')
127 revs = list(revs) + opts.get('rev')
126 revs = set(scmutil.revrange(repo, revs))
128 revs = set(scmutil.revrange(repo, revs))
127
129
128 wlock = repo.wlock()
130 wlock = repo.wlock()
129 try:
131 try:
130 bookmark = opts.get('bookmark')
132 bookmarks = None
131 if bookmark:
133 if opts.get('bookmark'):
134 bookmarks = set([opts.get('bookmark')])
135 if bookmarks:
132 repomarks = repo._bookmarks
136 repomarks = repo._bookmarks
133 if bookmark not in repomarks:
137 if not bookmarks.issubset(repomarks):
134 raise error.Abort(_("bookmark '%s' not found") % bookmark)
138 raise error.Abort(_("bookmark '%s' not found") %
139 ','.join(sorted(bookmarks - set(repomarks.keys()))))
135
140
136 # If the requested bookmark is not the only one pointing to a
141 # If the requested bookmark is not the only one pointing to a
137 # a revision we have to only delete the bookmark and not strip
142 # a revision we have to only delete the bookmark and not strip
138 # anything. revsets cannot detect that case.
143 # anything. revsets cannot detect that case.
139 uniquebm = True
144 nodetobookmarks = {}
140 for m, n in repomarks.iteritems():
145 for mark, node in repomarks.iteritems():
141 if m != bookmark and n == repo[bookmark].node():
146 nodetobookmarks.setdefault(node, []).append(mark)
142 uniquebm = False
147 for marks in nodetobookmarks.values():
143 break
148 if bookmarks.issuperset(marks):
144 if uniquebm:
149 rsrevs = repair.stripbmrevset(repo, marks[0])
145 rsrevs = repair.stripbmrevset(repo, bookmark)
146 revs.update(set(rsrevs))
150 revs.update(set(rsrevs))
147 if not revs:
151 if not revs:
152 for bookmark in bookmarks:
148 del repomarks[bookmark]
153 del repomarks[bookmark]
149 repomarks.write()
154 repomarks.write()
155 for bookmark in sorted(bookmarks):
150 ui.write(_("bookmark '%s' deleted\n") % bookmark)
156 ui.write(_("bookmark '%s' deleted\n") % bookmark)
151
157
152 if not revs:
158 if not revs:
153 raise error.Abort(_('empty revision set'))
159 raise error.Abort(_('empty revision set'))
154
160
155 descendants = set(cl.descendants(revs))
161 descendants = set(cl.descendants(revs))
156 strippedrevs = revs.union(descendants)
162 strippedrevs = revs.union(descendants)
157 roots = revs.difference(descendants)
163 roots = revs.difference(descendants)
158
164
159 update = False
165 update = False
160 # if one of the wdir parent is stripped we'll need
166 # if one of the wdir parent is stripped we'll need
161 # to update away to an earlier revision
167 # to update away to an earlier revision
162 for p in repo.dirstate.parents():
168 for p in repo.dirstate.parents():
163 if p != nullid and cl.rev(p) in strippedrevs:
169 if p != nullid and cl.rev(p) in strippedrevs:
164 update = True
170 update = True
165 break
171 break
166
172
167 rootnodes = set(cl.node(r) for r in roots)
173 rootnodes = set(cl.node(r) for r in roots)
168
174
169 q = getattr(repo, 'mq', None)
175 q = getattr(repo, 'mq', None)
170 if q is not None and q.applied:
176 if q is not None and q.applied:
171 # refresh queue state if we're about to strip
177 # refresh queue state if we're about to strip
172 # applied patches
178 # applied patches
173 if cl.rev(repo.lookup('qtip')) in strippedrevs:
179 if cl.rev(repo.lookup('qtip')) in strippedrevs:
174 q.applieddirty = True
180 q.applieddirty = True
175 start = 0
181 start = 0
176 end = len(q.applied)
182 end = len(q.applied)
177 for i, statusentry in enumerate(q.applied):
183 for i, statusentry in enumerate(q.applied):
178 if statusentry.node in rootnodes:
184 if statusentry.node in rootnodes:
179 # if one of the stripped roots is an applied
185 # if one of the stripped roots is an applied
180 # patch, only part of the queue is stripped
186 # patch, only part of the queue is stripped
181 start = i
187 start = i
182 break
188 break
183 del q.applied[start:end]
189 del q.applied[start:end]
184 q.savedirty()
190 q.savedirty()
185
191
186 revs = sorted(rootnodes)
192 revs = sorted(rootnodes)
187 if update and opts.get('keep'):
193 if update and opts.get('keep'):
188 urev, p2 = repo.changelog.parents(revs[0])
194 urev, p2 = repo.changelog.parents(revs[0])
189 if (util.safehasattr(repo, 'mq') and p2 != nullid
195 if (util.safehasattr(repo, 'mq') and p2 != nullid
190 and p2 in [x.node for x in repo.mq.applied]):
196 and p2 in [x.node for x in repo.mq.applied]):
191 urev = p2
197 urev = p2
192 uctx = repo[urev]
198 uctx = repo[urev]
193
199
194 # only reset the dirstate for files that would actually change
200 # only reset the dirstate for files that would actually change
195 # between the working context and uctx
201 # between the working context and uctx
196 descendantrevs = repo.revs("%s::." % uctx.rev())
202 descendantrevs = repo.revs("%s::." % uctx.rev())
197 changedfiles = []
203 changedfiles = []
198 for rev in descendantrevs:
204 for rev in descendantrevs:
199 # blindly reset the files, regardless of what actually changed
205 # blindly reset the files, regardless of what actually changed
200 changedfiles.extend(repo[rev].files())
206 changedfiles.extend(repo[rev].files())
201
207
202 # reset files that only changed in the dirstate too
208 # reset files that only changed in the dirstate too
203 dirstate = repo.dirstate
209 dirstate = repo.dirstate
204 dirchanges = [f for f in dirstate if dirstate[f] != 'n']
210 dirchanges = [f for f in dirstate if dirstate[f] != 'n']
205 changedfiles.extend(dirchanges)
211 changedfiles.extend(dirchanges)
206
212
207 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
213 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
208 repo.dirstate.write(repo.currenttransaction())
214 repo.dirstate.write(repo.currenttransaction())
209
215
210 # clear resolve state
216 # clear resolve state
211 merge.mergestate.clean(repo, repo['.'].node())
217 merge.mergestate.clean(repo, repo['.'].node())
212
218
213 update = False
219 update = False
214
220
215
221
216 strip(ui, repo, revs, backup=backup, update=update,
222 strip(ui, repo, revs, backup=backup, update=update,
217 force=opts.get('force'), bookmark=bookmark)
223 force=opts.get('force'), bookmarks=bookmarks)
218 finally:
224 finally:
219 wlock.release()
225 wlock.release()
220
226
221 return 0
227 return 0
General Comments 0
You need to be logged in to leave comments. Login now