##// END OF EJS Templates
strip: factor out revset calculation for strip -B...
Ryan McElroy -
r26624:bcace0fb default
parent child Browse files
Show More
@@ -1,225 +1,222 b''
1 1 """strip changesets and their descendants from history
2 2
3 3 This extension allows you to strip changesets and all their descendants from the
4 4 repository. See the command help for details.
5 5 """
6 6 from mercurial.i18n import _
7 7 from mercurial.node import nullid
8 8 from mercurial.lock import release
9 9 from mercurial import cmdutil, hg, scmutil, util, error
10 10 from mercurial import repair, bookmarks, merge
11 11
12 12 cmdtable = {}
13 13 command = cmdutil.command(cmdtable)
14 14 # Note for extension authors: ONLY specify testedwith = 'internal' for
15 15 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
16 16 # be specifying the version(s) of Mercurial they are tested with, or
17 17 # leave the attribute unspecified.
18 18 testedwith = 'internal'
19 19
20 20 def checksubstate(repo, baserev=None):
21 21 '''return list of subrepos at a different revision than substate.
22 22 Abort if any subrepos have uncommitted changes.'''
23 23 inclsubs = []
24 24 wctx = repo[None]
25 25 if baserev:
26 26 bctx = repo[baserev]
27 27 else:
28 28 bctx = wctx.parents()[0]
29 29 for s in sorted(wctx.substate):
30 30 wctx.sub(s).bailifchanged(True)
31 31 if s not in bctx.substate or bctx.sub(s).dirty():
32 32 inclsubs.append(s)
33 33 return inclsubs
34 34
35 35 def checklocalchanges(repo, force=False, excsuffix=''):
36 36 cmdutil.checkunfinished(repo)
37 37 s = repo.status()
38 38 if not force:
39 39 if s.modified or s.added or s.removed or s.deleted:
40 40 _("local changes found") # i18n tool detection
41 41 raise error.Abort(_("local changes found" + excsuffix))
42 42 if checksubstate(repo):
43 43 _("local changed subrepos found") # i18n tool detection
44 44 raise error.Abort(_("local changed subrepos found" + excsuffix))
45 45 return s
46 46
47 47 def strip(ui, repo, revs, update=True, backup=True, force=None, bookmark=None):
48 48 wlock = lock = None
49 49 try:
50 50 wlock = repo.wlock()
51 51 lock = repo.lock()
52 52
53 53 if update:
54 54 checklocalchanges(repo, force=force)
55 55 urev, p2 = repo.changelog.parents(revs[0])
56 56 if (util.safehasattr(repo, 'mq') and
57 57 p2 != nullid
58 58 and p2 in [x.node for x in repo.mq.applied]):
59 59 urev = p2
60 60 hg.clean(repo, urev)
61 61 repo.dirstate.write()
62 62
63 63 repair.strip(ui, repo, revs, backup)
64 64
65 65 marks = repo._bookmarks
66 66 if bookmark:
67 67 if bookmark == repo._activebookmark:
68 68 bookmarks.deactivate(repo)
69 69 del marks[bookmark]
70 70 marks.write()
71 71 ui.write(_("bookmark '%s' deleted\n") % bookmark)
72 72 finally:
73 73 release(lock, wlock)
74 74
75 75
76 76 @command("strip",
77 77 [
78 78 ('r', 'rev', [], _('strip specified revision (optional, '
79 79 'can specify revisions without this '
80 80 'option)'), _('REV')),
81 81 ('f', 'force', None, _('force removal of changesets, discard '
82 82 'uncommitted changes (no backup)')),
83 83 ('', 'no-backup', None, _('no backups')),
84 84 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
85 85 ('n', '', None, _('ignored (DEPRECATED)')),
86 86 ('k', 'keep', None, _("do not modify working directory during "
87 87 "strip")),
88 88 ('B', 'bookmark', '', _("remove revs only reachable from given"
89 89 " bookmark"))],
90 90 _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
91 91 def stripcmd(ui, repo, *revs, **opts):
92 92 """strip changesets and all their descendants from the repository
93 93
94 94 The strip command removes the specified changesets and all their
95 95 descendants. If the working directory has uncommitted changes, the
96 96 operation is aborted unless the --force flag is supplied, in which
97 97 case changes will be discarded.
98 98
99 99 If a parent of the working directory is stripped, then the working
100 100 directory will automatically be updated to the most recent
101 101 available ancestor of the stripped parent after the operation
102 102 completes.
103 103
104 104 Any stripped changesets are stored in ``.hg/strip-backup`` as a
105 105 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
106 106 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
107 107 where BUNDLE is the bundle file created by the strip. Note that
108 108 the local revision numbers will in general be different after the
109 109 restore.
110 110
111 111 Use the --no-backup option to discard the backup bundle once the
112 112 operation completes.
113 113
114 114 Strip is not a history-rewriting operation and can be used on
115 115 changesets in the public phase. But if the stripped changesets have
116 116 been pushed to a remote repository you will likely pull them again.
117 117
118 118 Return 0 on success.
119 119 """
120 120 backup = True
121 121 if opts.get('no_backup') or opts.get('nobackup'):
122 122 backup = False
123 123
124 124 cl = repo.changelog
125 125 revs = list(revs) + opts.get('rev')
126 126 revs = set(scmutil.revrange(repo, revs))
127 127
128 128 wlock = repo.wlock()
129 129 try:
130 130 if opts.get('bookmark'):
131 131 mark = opts.get('bookmark')
132 132 marks = repo._bookmarks
133 133 if mark not in marks:
134 134 raise error.Abort(_("bookmark '%s' not found") % mark)
135 135
136 136 # If the requested bookmark is not the only one pointing to a
137 137 # a revision we have to only delete the bookmark and not strip
138 138 # anything. revsets cannot detect that case.
139 139 uniquebm = True
140 140 for m, n in marks.iteritems():
141 141 if m != mark and n == repo[mark].node():
142 142 uniquebm = False
143 143 break
144 144 if uniquebm:
145 rsrevs = repo.revs("ancestors(bookmark(%s)) - "
146 "ancestors(head() and not bookmark(%s)) - "
147 "ancestors(bookmark() and not bookmark(%s))",
148 mark, mark, mark)
145 rsrevs = repair.stripbmrevset(repo, mark)
149 146 revs.update(set(rsrevs))
150 147 if not revs:
151 148 del marks[mark]
152 149 marks.write()
153 150 ui.write(_("bookmark '%s' deleted\n") % mark)
154 151
155 152 if not revs:
156 153 raise error.Abort(_('empty revision set'))
157 154
158 155 descendants = set(cl.descendants(revs))
159 156 strippedrevs = revs.union(descendants)
160 157 roots = revs.difference(descendants)
161 158
162 159 update = False
163 160 # if one of the wdir parent is stripped we'll need
164 161 # to update away to an earlier revision
165 162 for p in repo.dirstate.parents():
166 163 if p != nullid and cl.rev(p) in strippedrevs:
167 164 update = True
168 165 break
169 166
170 167 rootnodes = set(cl.node(r) for r in roots)
171 168
172 169 q = getattr(repo, 'mq', None)
173 170 if q is not None and q.applied:
174 171 # refresh queue state if we're about to strip
175 172 # applied patches
176 173 if cl.rev(repo.lookup('qtip')) in strippedrevs:
177 174 q.applieddirty = True
178 175 start = 0
179 176 end = len(q.applied)
180 177 for i, statusentry in enumerate(q.applied):
181 178 if statusentry.node in rootnodes:
182 179 # if one of the stripped roots is an applied
183 180 # patch, only part of the queue is stripped
184 181 start = i
185 182 break
186 183 del q.applied[start:end]
187 184 q.savedirty()
188 185
189 186 revs = sorted(rootnodes)
190 187 if update and opts.get('keep'):
191 188 urev, p2 = repo.changelog.parents(revs[0])
192 189 if (util.safehasattr(repo, 'mq') and p2 != nullid
193 190 and p2 in [x.node for x in repo.mq.applied]):
194 191 urev = p2
195 192 uctx = repo[urev]
196 193
197 194 # only reset the dirstate for files that would actually change
198 195 # between the working context and uctx
199 196 descendantrevs = repo.revs("%s::." % uctx.rev())
200 197 changedfiles = []
201 198 for rev in descendantrevs:
202 199 # blindly reset the files, regardless of what actually changed
203 200 changedfiles.extend(repo[rev].files())
204 201
205 202 # reset files that only changed in the dirstate too
206 203 dirstate = repo.dirstate
207 204 dirchanges = [f for f in dirstate if dirstate[f] != 'n']
208 205 changedfiles.extend(dirchanges)
209 206
210 207 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
211 208 repo.dirstate.write()
212 209
213 210 # clear resolve state
214 211 ms = merge.mergestate(repo)
215 212 ms.reset(repo['.'].node())
216 213
217 214 update = False
218 215
219 216
220 217 strip(ui, repo, revs, backup=backup, update=update,
221 218 force=opts.get('force'), bookmark=opts.get('bookmark'))
222 219 finally:
223 220 wlock.release()
224 221
225 222 return 0
@@ -1,301 +1,313 b''
1 1 # repair.py - functions for repository repair for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 # Copyright 2007 Matt Mackall
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12
13 13 from .i18n import _
14 14 from .node import short
15 15 from . import (
16 16 bundle2,
17 17 changegroup,
18 18 error,
19 19 exchange,
20 20 util,
21 21 )
22 22
23 23 def _bundle(repo, bases, heads, node, suffix, compress=True):
24 24 """create a bundle with the specified revisions as a backup"""
25 25 cgversion = '01'
26 26 if 'generaldelta' in repo.requirements:
27 27 cgversion = '02'
28 28
29 29 cg = changegroup.changegroupsubset(repo, bases, heads, 'strip',
30 30 version=cgversion)
31 31 backupdir = "strip-backup"
32 32 vfs = repo.vfs
33 33 if not vfs.isdir(backupdir):
34 34 vfs.mkdir(backupdir)
35 35
36 36 # Include a hash of all the nodes in the filename for uniqueness
37 37 allcommits = repo.set('%ln::%ln', bases, heads)
38 38 allhashes = sorted(c.hex() for c in allcommits)
39 39 totalhash = util.sha1(''.join(allhashes)).hexdigest()
40 40 name = "%s/%s-%s-%s.hg" % (backupdir, short(node), totalhash[:8], suffix)
41 41
42 42 comp = None
43 43 if cgversion != '01':
44 44 bundletype = "HG20"
45 45 if compress:
46 46 comp = 'BZ'
47 47 elif compress:
48 48 bundletype = "HG10BZ"
49 49 else:
50 50 bundletype = "HG10UN"
51 51 return changegroup.writebundle(repo.ui, cg, name, bundletype, vfs,
52 52 compression=comp)
53 53
54 54 def _collectfiles(repo, striprev):
55 55 """find out the filelogs affected by the strip"""
56 56 files = set()
57 57
58 58 for x in xrange(striprev, len(repo)):
59 59 files.update(repo[x].files())
60 60
61 61 return sorted(files)
62 62
63 63 def _collectbrokencsets(repo, files, striprev):
64 64 """return the changesets which will be broken by the truncation"""
65 65 s = set()
66 66 def collectone(revlog):
67 67 _, brokenset = revlog.getstrippoint(striprev)
68 68 s.update([revlog.linkrev(r) for r in brokenset])
69 69
70 70 collectone(repo.manifest)
71 71 for fname in files:
72 72 collectone(repo.file(fname))
73 73
74 74 return s
75 75
76 76 def strip(ui, repo, nodelist, backup=True, topic='backup'):
77 77
78 78 # Simple way to maintain backwards compatibility for this
79 79 # argument.
80 80 if backup in ['none', 'strip']:
81 81 backup = False
82 82
83 83 repo = repo.unfiltered()
84 84 repo.destroying()
85 85
86 86 cl = repo.changelog
87 87 # TODO handle undo of merge sets
88 88 if isinstance(nodelist, str):
89 89 nodelist = [nodelist]
90 90 striplist = [cl.rev(node) for node in nodelist]
91 91 striprev = min(striplist)
92 92
93 93 # Some revisions with rev > striprev may not be descendants of striprev.
94 94 # We have to find these revisions and put them in a bundle, so that
95 95 # we can restore them after the truncations.
96 96 # To create the bundle we use repo.changegroupsubset which requires
97 97 # the list of heads and bases of the set of interesting revisions.
98 98 # (head = revision in the set that has no descendant in the set;
99 99 # base = revision in the set that has no ancestor in the set)
100 100 tostrip = set(striplist)
101 101 for rev in striplist:
102 102 for desc in cl.descendants([rev]):
103 103 tostrip.add(desc)
104 104
105 105 files = _collectfiles(repo, striprev)
106 106 saverevs = _collectbrokencsets(repo, files, striprev)
107 107
108 108 # compute heads
109 109 saveheads = set(saverevs)
110 110 for r in xrange(striprev + 1, len(cl)):
111 111 if r not in tostrip:
112 112 saverevs.add(r)
113 113 saveheads.difference_update(cl.parentrevs(r))
114 114 saveheads.add(r)
115 115 saveheads = [cl.node(r) for r in saveheads]
116 116
117 117 # compute base nodes
118 118 if saverevs:
119 119 descendants = set(cl.descendants(saverevs))
120 120 saverevs.difference_update(descendants)
121 121 savebases = [cl.node(r) for r in saverevs]
122 122 stripbases = [cl.node(r) for r in tostrip]
123 123
124 124 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)), but
125 125 # is much faster
126 126 newbmtarget = repo.revs('max(parents(%ld) - (%ld))', tostrip, tostrip)
127 127 if newbmtarget:
128 128 newbmtarget = repo[newbmtarget.first()].node()
129 129 else:
130 130 newbmtarget = '.'
131 131
132 132 bm = repo._bookmarks
133 133 updatebm = []
134 134 for m in bm:
135 135 rev = repo[bm[m]].rev()
136 136 if rev in tostrip:
137 137 updatebm.append(m)
138 138
139 139 # create a changegroup for all the branches we need to keep
140 140 backupfile = None
141 141 vfs = repo.vfs
142 142 node = nodelist[-1]
143 143 if backup:
144 144 backupfile = _bundle(repo, stripbases, cl.heads(), node, topic)
145 145 repo.ui.status(_("saved backup bundle to %s\n") %
146 146 vfs.join(backupfile))
147 147 repo.ui.log("backupbundle", "saved backup bundle to %s\n",
148 148 vfs.join(backupfile))
149 149 if saveheads or savebases:
150 150 # do not compress partial bundle if we remove it from disk later
151 151 chgrpfile = _bundle(repo, savebases, saveheads, node, 'temp',
152 152 compress=False)
153 153
154 154 mfst = repo.manifest
155 155
156 156 curtr = repo.currenttransaction()
157 157 if curtr is not None:
158 158 del curtr # avoid carrying reference to transaction for nothing
159 159 msg = _('programming error: cannot strip from inside a transaction')
160 160 raise error.Abort(msg, hint=_('contact your extension maintainer'))
161 161
162 162 tr = repo.transaction("strip")
163 163 offset = len(tr.entries)
164 164
165 165 try:
166 166 tr.startgroup()
167 167 cl.strip(striprev, tr)
168 168 mfst.strip(striprev, tr)
169 169 for fn in files:
170 170 repo.file(fn).strip(striprev, tr)
171 171 tr.endgroup()
172 172
173 173 try:
174 174 for i in xrange(offset, len(tr.entries)):
175 175 file, troffset, ignore = tr.entries[i]
176 176 repo.svfs(file, 'a').truncate(troffset)
177 177 if troffset == 0:
178 178 repo.store.markremoved(file)
179 179 tr.close()
180 180 finally:
181 181 tr.release()
182 182
183 183 if saveheads or savebases:
184 184 ui.note(_("adding branch\n"))
185 185 f = vfs.open(chgrpfile, "rb")
186 186 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
187 187 if not repo.ui.verbose:
188 188 # silence internal shuffling chatter
189 189 repo.ui.pushbuffer()
190 190 if isinstance(gen, bundle2.unbundle20):
191 191 tr = repo.transaction('strip')
192 192 tr.hookargs = {'source': 'strip',
193 193 'url': 'bundle:' + vfs.join(chgrpfile)}
194 194 try:
195 195 bundle2.processbundle(repo, gen, lambda: tr)
196 196 tr.close()
197 197 finally:
198 198 tr.release()
199 199 else:
200 200 changegroup.addchangegroup(repo, gen, 'strip',
201 201 'bundle:' + vfs.join(chgrpfile),
202 202 True)
203 203 if not repo.ui.verbose:
204 204 repo.ui.popbuffer()
205 205 f.close()
206 206
207 207 # remove undo files
208 208 for undovfs, undofile in repo.undofiles():
209 209 try:
210 210 undovfs.unlink(undofile)
211 211 except OSError as e:
212 212 if e.errno != errno.ENOENT:
213 213 ui.warn(_('error removing %s: %s\n') %
214 214 (undovfs.join(undofile), str(e)))
215 215
216 216 for m in updatebm:
217 217 bm[m] = repo[newbmtarget].node()
218 218 bm.write()
219 219 except: # re-raises
220 220 if backupfile:
221 221 ui.warn(_("strip failed, full bundle stored in '%s'\n")
222 222 % vfs.join(backupfile))
223 223 elif saveheads:
224 224 ui.warn(_("strip failed, partial bundle stored in '%s'\n")
225 225 % vfs.join(chgrpfile))
226 226 raise
227 227 else:
228 228 if saveheads or savebases:
229 229 # Remove partial backup only if there were no exceptions
230 230 vfs.unlink(chgrpfile)
231 231
232 232 repo.destroyed()
233 233
234 234 def rebuildfncache(ui, repo):
235 235 """Rebuilds the fncache file from repo history.
236 236
237 237 Missing entries will be added. Extra entries will be removed.
238 238 """
239 239 repo = repo.unfiltered()
240 240
241 241 if 'fncache' not in repo.requirements:
242 242 ui.warn(_('(not rebuilding fncache because repository does not '
243 243 'support fncache)\n'))
244 244 return
245 245
246 246 lock = repo.lock()
247 247 try:
248 248 fnc = repo.store.fncache
249 249 # Trigger load of fncache.
250 250 if 'irrelevant' in fnc:
251 251 pass
252 252
253 253 oldentries = set(fnc.entries)
254 254 newentries = set()
255 255 seenfiles = set()
256 256
257 257 repolen = len(repo)
258 258 for rev in repo:
259 259 ui.progress(_('changeset'), rev, total=repolen)
260 260
261 261 ctx = repo[rev]
262 262 for f in ctx.files():
263 263 # This is to minimize I/O.
264 264 if f in seenfiles:
265 265 continue
266 266 seenfiles.add(f)
267 267
268 268 i = 'data/%s.i' % f
269 269 d = 'data/%s.d' % f
270 270
271 271 if repo.store._exists(i):
272 272 newentries.add(i)
273 273 if repo.store._exists(d):
274 274 newentries.add(d)
275 275
276 276 ui.progress(_('changeset'), None)
277 277
278 278 addcount = len(newentries - oldentries)
279 279 removecount = len(oldentries - newentries)
280 280 for p in sorted(oldentries - newentries):
281 281 ui.write(_('removing %s\n') % p)
282 282 for p in sorted(newentries - oldentries):
283 283 ui.write(_('adding %s\n') % p)
284 284
285 285 if addcount or removecount:
286 286 ui.write(_('%d items added, %d removed from fncache\n') %
287 287 (addcount, removecount))
288 288 fnc.entries = newentries
289 289 fnc._dirty = True
290 290
291 291 tr = repo.transaction('fncache')
292 292 try:
293 293 fnc.write(tr)
294 294 tr.close()
295 295 finally:
296 296 tr.release()
297 297 else:
298 298 ui.write(_('fncache already up to date\n'))
299 299 finally:
300 300 lock.release()
301 301
302 def stripbmrevset(repo, mark):
303 """
304 The revset to strip when strip is called with -B mark
305
306 Needs to live here so extensions can use it and wrap it even when strip is
307 not enabled or not present on a box.
308 """
309 return repo.revs("ancestors(bookmark(%s)) - "
310 "ancestors(head() and not bookmark(%s)) - "
311 "ancestors(bookmark() and not bookmark(%s))",
312 mark, mark, mark)
313
General Comments 0
You need to be logged in to leave comments. Login now