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