##// END OF EJS Templates
rebase: change internal format to support destination map...
Jun Wu -
r34006:af609bb3 default
parent child Browse files
Show More
@@ -0,0 +1,76 b''
1 Test rebase --continue with rebasestate written by legacy client
2
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
5 > rebase=
6 > drawdag=$TESTDIR/drawdag.py
7 > EOF
8
9 $ hg init
10 $ hg debugdrawdag <<'EOF'
11 > D H
12 > | |
13 > C G
14 > | |
15 > B F
16 > | |
17 > Z A E
18 > \|/
19 > R
20 > EOF
21
22 rebasestate generated by a legacy client running "hg rebase -r B+D+E+G+H -d Z"
23
24 $ touch .hg/last-message.txt
25 $ cat > .hg/rebasestate <<EOF
26 > 0000000000000000000000000000000000000000
27 > f424eb6a8c01c4a0c0fba9f863f79b3eb5b4b69f
28 > 0000000000000000000000000000000000000000
29 > 0
30 > 0
31 > 0
32 >
33 > 21a6c45028857f500f56ae84fbf40689c429305b:-2
34 > de008c61a447fcfd93f808ef527d933a84048ce7:0000000000000000000000000000000000000000
35 > c1e6b162678d07d0b204e5c8267d51b4e03b633c:0000000000000000000000000000000000000000
36 > aeba276fcb7df8e10153a07ee728d5540693f5aa:-3
37 > bd5548558fcf354d37613005737a143871bf3723:-3
38 > d2fa1c02b2401b0e32867f26cce50818a4bd796a:0000000000000000000000000000000000000000
39 > 6f7a236de6852570cd54649ab62b1012bb78abc8:0000000000000000000000000000000000000000
40 > 6582e6951a9c48c236f746f186378e36f59f4928:0000000000000000000000000000000000000000
41 > EOF
42
43 $ hg rebase --continue
44 rebasing 4:c1e6b162678d "B" (B)
45 rebasing 8:6f7a236de685 "D" (D)
46 rebasing 2:de008c61a447 "E" (E)
47 rebasing 7:d2fa1c02b240 "G" (G)
48 rebasing 9:6582e6951a9c "H" (H tip)
49 warning: orphaned descendants detected, not stripping c1e6b162678d, de008c61a447
50 saved backup bundle to $TESTTMP/.hg/strip-backup/6f7a236de685-9880a3dc-rebase.hg (glob)
51
52 $ hg log -G -T '{rev}:{node|short} {desc}\n'
53 o 11:721b8da0a708 H
54 |
55 o 10:9d65695ec3c2 G
56 |
57 o 9:21c8397a5d68 E
58 |
59 | o 8:fc52970345e8 D
60 | |
61 | o 7:eac96551b107 B
62 |/
63 | o 6:bd5548558fcf C
64 | |
65 | | o 5:aeba276fcb7d F
66 | | |
67 | o | 4:c1e6b162678d B
68 | | |
69 o | | 3:f424eb6a8c01 Z
70 | | |
71 +---o 2:de008c61a447 E
72 | |
73 | o 1:21a6c4502885 A
74 |/
75 o 0:b41ce7760717 R
76
@@ -1,1541 +1,1570 b''
1 1 # rebase.py - rebasing feature for mercurial
2 2 #
3 3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to move sets of revisions to a different ancestor
9 9
10 10 This extension lets you rebase changesets in an existing Mercurial
11 11 repository.
12 12
13 13 For more information:
14 14 https://mercurial-scm.org/wiki/RebaseExtension
15 15 '''
16 16
17 17 from __future__ import absolute_import
18 18
19 19 import errno
20 20 import os
21 21
22 22 from mercurial.i18n import _
23 23 from mercurial.node import (
24 hex,
25 24 nullid,
26 25 nullrev,
27 26 short,
28 27 )
29 28 from mercurial import (
30 29 bookmarks,
31 30 cmdutil,
32 31 commands,
33 32 copies,
34 33 destutil,
35 34 dirstateguard,
36 35 error,
37 36 extensions,
38 37 hg,
39 38 lock,
40 39 merge as mergemod,
41 40 mergeutil,
42 41 obsolete,
43 42 obsutil,
44 43 patch,
45 44 phases,
46 45 registrar,
47 46 repair,
48 47 repoview,
49 48 revset,
50 49 scmutil,
51 50 smartset,
52 51 util,
53 52 )
54 53
55 54 release = lock.release
56 55 templateopts = cmdutil.templateopts
57 56
58 57 # The following constants are used throughout the rebase module. The ordering of
59 58 # their values must be maintained.
60 59
61 60 # Indicates that a revision needs to be rebased
62 61 revtodo = -1
62 revtodostr = '-1'
63 63
64 64 # legacy revstates no longer needed in current code
65 65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
66 66 legacystates = {'-2', '-3', '-4', '-5'}
67 67
68 68 cmdtable = {}
69 69 command = registrar.command(cmdtable)
70 70 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
71 71 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
72 72 # be specifying the version(s) of Mercurial they are tested with, or
73 73 # leave the attribute unspecified.
74 74 testedwith = 'ships-with-hg-core'
75 75
76 76 def _nothingtorebase():
77 77 return 1
78 78
79 79 def _savegraft(ctx, extra):
80 80 s = ctx.extra().get('source', None)
81 81 if s is not None:
82 82 extra['source'] = s
83 83 s = ctx.extra().get('intermediate-source', None)
84 84 if s is not None:
85 85 extra['intermediate-source'] = s
86 86
87 87 def _savebranch(ctx, extra):
88 88 extra['branch'] = ctx.branch()
89 89
90 90 def _makeextrafn(copiers):
91 91 """make an extrafn out of the given copy-functions.
92 92
93 93 A copy function takes a context and an extra dict, and mutates the
94 94 extra dict as needed based on the given context.
95 95 """
96 96 def extrafn(ctx, extra):
97 97 for c in copiers:
98 98 c(ctx, extra)
99 99 return extrafn
100 100
101 101 def _destrebase(repo, sourceset, destspace=None):
102 102 """small wrapper around destmerge to pass the right extra args
103 103
104 104 Please wrap destutil.destmerge instead."""
105 105 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
106 106 onheadcheck=False, destspace=destspace)
107 107
108 108 revsetpredicate = registrar.revsetpredicate()
109 109
110 110 @revsetpredicate('_destrebase')
111 111 def _revsetdestrebase(repo, subset, x):
112 112 # ``_rebasedefaultdest()``
113 113
114 114 # default destination for rebase.
115 115 # # XXX: Currently private because I expect the signature to change.
116 116 # # XXX: - bailing out in case of ambiguity vs returning all data.
117 117 # i18n: "_rebasedefaultdest" is a keyword
118 118 sourceset = None
119 119 if x is not None:
120 120 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
121 121 return subset & smartset.baseset([_destrebase(repo, sourceset)])
122 122
123 123 def _ctxdesc(ctx):
124 124 """short description for a context"""
125 125 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
126 126 ctx.description().split('\n', 1)[0])
127 127 repo = ctx.repo()
128 128 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
129 129 if names:
130 130 desc += ' (%s)' % ' '.join(names)
131 131 return desc
132 132
133 133 class rebaseruntime(object):
134 134 """This class is a container for rebase runtime state"""
135 135 def __init__(self, repo, ui, opts=None):
136 136 if opts is None:
137 137 opts = {}
138 138
139 139 self.repo = repo
140 140 self.ui = ui
141 141 self.opts = opts
142 142 self.originalwd = None
143 143 self.external = nullrev
144 144 # Mapping between the old revision id and either what is the new rebased
145 145 # revision or what needs to be done with the old revision. The state
146 146 # dict will be what contains most of the rebase progress state.
147 147 self.state = {}
148 148 self.activebookmark = None
149 self.dest = None
149 self.destmap = {}
150 150 self.skipped = set()
151 151
152 152 self.collapsef = opts.get('collapse', False)
153 153 self.collapsemsg = cmdutil.logmessage(ui, opts)
154 154 self.date = opts.get('date', None)
155 155
156 156 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
157 157 self.extrafns = [_savegraft]
158 158 if e:
159 159 self.extrafns = [e]
160 160
161 161 self.keepf = opts.get('keep', False)
162 162 self.keepbranchesf = opts.get('keepbranches', False)
163 163 # keepopen is not meant for use on the command line, but by
164 164 # other extensions
165 165 self.keepopen = opts.get('keepopen', False)
166 166 self.obsoletenotrebased = {}
167 167
168 168 def storestatus(self, tr=None):
169 169 """Store the current status to allow recovery"""
170 170 if tr:
171 171 tr.addfilegenerator('rebasestate', ('rebasestate',),
172 172 self._writestatus, location='plain')
173 173 else:
174 174 with self.repo.vfs("rebasestate", "w") as f:
175 175 self._writestatus(f)
176 176
177 177 def _writestatus(self, f):
178 178 repo = self.repo.unfiltered()
179 179 f.write(repo[self.originalwd].hex() + '\n')
180 f.write(repo[self.dest].hex() + '\n')
180 # was "dest". we now write dest per src root below.
181 f.write('\n')
181 182 f.write(repo[self.external].hex() + '\n')
182 183 f.write('%d\n' % int(self.collapsef))
183 184 f.write('%d\n' % int(self.keepf))
184 185 f.write('%d\n' % int(self.keepbranchesf))
185 186 f.write('%s\n' % (self.activebookmark or ''))
187 destmap = self.destmap
186 188 for d, v in self.state.iteritems():
187 189 oldrev = repo[d].hex()
188 190 if v >= 0:
189 191 newrev = repo[v].hex()
190 elif v == revtodo:
191 # To maintain format compatibility, we have to use nullid.
192 # Please do remove this special case when upgrading the format.
193 newrev = hex(nullid)
194 192 else:
195 193 newrev = v
196 f.write("%s:%s\n" % (oldrev, newrev))
194 destnode = repo[destmap[d]].hex()
195 f.write("%s:%s:%s\n" % (oldrev, newrev, destnode))
197 196 repo.ui.debug('rebase status stored\n')
198 197
199 198 def restorestatus(self):
200 199 """Restore a previously stored status"""
201 200 repo = self.repo
202 201 keepbranches = None
203 dest = None
202 legacydest = None
204 203 collapse = False
205 204 external = nullrev
206 205 activebookmark = None
207 206 state = {}
207 destmap = {}
208 208
209 209 try:
210 210 f = repo.vfs("rebasestate")
211 211 for i, l in enumerate(f.read().splitlines()):
212 212 if i == 0:
213 213 originalwd = repo[l].rev()
214 214 elif i == 1:
215 dest = repo[l].rev()
215 # this line should be empty in newer version. but legacy
216 # clients may still use it
217 if l:
218 legacydest = repo[l].rev()
216 219 elif i == 2:
217 220 external = repo[l].rev()
218 221 elif i == 3:
219 222 collapse = bool(int(l))
220 223 elif i == 4:
221 224 keep = bool(int(l))
222 225 elif i == 5:
223 226 keepbranches = bool(int(l))
224 227 elif i == 6 and not (len(l) == 81 and ':' in l):
225 228 # line 6 is a recent addition, so for backwards
226 229 # compatibility check that the line doesn't look like the
227 230 # oldrev:newrev lines
228 231 activebookmark = l
229 232 else:
230 oldrev, newrev = l.split(':')
233 args = l.split(':')
234 oldrev = args[0]
235 newrev = args[1]
231 236 if newrev in legacystates:
232 237 continue
233 elif newrev == nullid:
238 if len(args) > 2:
239 destnode = args[2]
240 else:
241 destnode = legacydest
242 destmap[repo[oldrev].rev()] = repo[destnode].rev()
243 if newrev in (nullid, revtodostr):
234 244 state[repo[oldrev].rev()] = revtodo
235 245 # Legacy compat special case
236 246 else:
237 247 state[repo[oldrev].rev()] = repo[newrev].rev()
238 248
239 249 except IOError as err:
240 250 if err.errno != errno.ENOENT:
241 251 raise
242 252 cmdutil.wrongtooltocontinue(repo, _('rebase'))
243 253
244 254 if keepbranches is None:
245 255 raise error.Abort(_('.hg/rebasestate is incomplete'))
246 256
247 257 skipped = set()
248 258 # recompute the set of skipped revs
249 259 if not collapse:
250 seen = {dest}
260 seen = set(destmap.values())
251 261 for old, new in sorted(state.items()):
252 262 if new != revtodo and new in seen:
253 263 skipped.add(old)
254 264 seen.add(new)
255 265 repo.ui.debug('computed skipped revs: %s\n' %
256 266 (' '.join(str(r) for r in sorted(skipped)) or None))
257 267 repo.ui.debug('rebase status resumed\n')
258 268 _setrebasesetvisibility(repo, set(state.keys()) | {originalwd})
259 269
260 270 self.originalwd = originalwd
261 self.dest = dest
271 self.destmap = destmap
262 272 self.state = state
263 273 self.skipped = skipped
264 274 self.collapsef = collapse
265 275 self.keepf = keep
266 276 self.keepbranchesf = keepbranches
267 277 self.external = external
268 278 self.activebookmark = activebookmark
269 279
270 def _handleskippingobsolete(self, rebaserevs, obsoleterevs, dest):
280 def _handleskippingobsolete(self, obsoleterevs, destmap):
271 281 """Compute structures necessary for skipping obsolete revisions
272 282
273 rebaserevs: iterable of all revisions that are to be rebased
274 283 obsoleterevs: iterable of all obsolete revisions in rebaseset
275 dest: a destination revision for the rebase operation
284 destmap: {srcrev: destrev} destination revisions
276 285 """
277 286 self.obsoletenotrebased = {}
278 287 if not self.ui.configbool('experimental', 'rebaseskipobsolete',
279 288 default=True):
280 289 return
281 290 obsoleteset = set(obsoleterevs)
282 291 self.obsoletenotrebased = _computeobsoletenotrebased(self.repo,
283 obsoleteset, dest)
292 obsoleteset, destmap)
284 293 skippedset = set(self.obsoletenotrebased)
285 294 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
286 295
287 296 def _prepareabortorcontinue(self, isabort):
288 297 try:
289 298 self.restorestatus()
290 299 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
291 300 except error.RepoLookupError:
292 301 if isabort:
293 302 clearstatus(self.repo)
294 303 clearcollapsemsg(self.repo)
295 304 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
296 305 ' only broken state is cleared)\n'))
297 306 return 0
298 307 else:
299 308 msg = _('cannot continue inconsistent rebase')
300 309 hint = _('use "hg rebase --abort" to clear broken state')
301 310 raise error.Abort(msg, hint=hint)
302 311 if isabort:
303 return abort(self.repo, self.originalwd, self.dest,
312 return abort(self.repo, self.originalwd, self.destmap,
304 313 self.state, activebookmark=self.activebookmark)
305 314
306 def _preparenewrebase(self, dest, rebaseset):
307 if dest is None:
315 def _preparenewrebase(self, destmap):
316 if not destmap:
308 317 return _nothingtorebase()
309 318
319 rebaseset = destmap.keys()
310 320 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
311 321 if (not (self.keepf or allowunstable)
312 322 and self.repo.revs('first(children(%ld) - %ld)',
313 323 rebaseset, rebaseset)):
314 324 raise error.Abort(
315 325 _("can't remove original changesets with"
316 326 " unrebased descendants"),
317 327 hint=_('use --keep to keep original changesets'))
318 328
319 obsrevs = _filterobsoleterevs(self.repo, set(rebaseset))
320 self._handleskippingobsolete(rebaseset, obsrevs, dest.rev())
329 obsrevs = _filterobsoleterevs(self.repo, rebaseset)
330 self._handleskippingobsolete(obsrevs, destmap)
321 331
322 result = buildstate(self.repo, dest, rebaseset, self.collapsef,
332 result = buildstate(self.repo, destmap, self.collapsef,
323 333 self.obsoletenotrebased)
324 334
325 335 if not result:
326 336 # Empty state built, nothing to rebase
327 337 self.ui.status(_('nothing to rebase\n'))
328 338 return _nothingtorebase()
329 339
330 340 for root in self.repo.set('roots(%ld)', rebaseset):
331 341 if not self.keepf and not root.mutable():
332 342 raise error.Abort(_("can't rebase public changeset %s")
333 343 % root,
334 344 hint=_("see 'hg help phases' for details"))
335 345
336 (self.originalwd, self.dest, self.state) = result
346 (self.originalwd, self.destmap, self.state) = result
337 347 if self.collapsef:
338 destancestors = self.repo.changelog.ancestors([self.dest],
348 dests = set(self.destmap.values())
349 if len(dests) != 1:
350 raise error.Abort(
351 _('--collapse does not work with multiple destinations'))
352 destrev = next(iter(dests))
353 destancestors = self.repo.changelog.ancestors([destrev],
339 354 inclusive=True)
340 355 self.external = externalparent(self.repo, self.state, destancestors)
341 356
342 if dest.closesbranch() and not self.keepbranchesf:
343 self.ui.status(_('reopening closed branch head %s\n') % dest)
357 for destrev in sorted(set(destmap.values())):
358 dest = self.repo[destrev]
359 if dest.closesbranch() and not self.keepbranchesf:
360 self.ui.status(_('reopening closed branch head %s\n') % dest)
344 361
345 362 def _performrebase(self, tr):
346 363 repo, ui, opts = self.repo, self.ui, self.opts
347 364 if self.keepbranchesf:
348 365 # insert _savebranch at the start of extrafns so if
349 366 # there's a user-provided extrafn it can clobber branch if
350 367 # desired
351 368 self.extrafns.insert(0, _savebranch)
352 369 if self.collapsef:
353 370 branches = set()
354 371 for rev in self.state:
355 372 branches.add(repo[rev].branch())
356 373 if len(branches) > 1:
357 374 raise error.Abort(_('cannot collapse multiple named '
358 375 'branches'))
359 376
360 377 # Keep track of the active bookmarks in order to reset them later
361 378 self.activebookmark = self.activebookmark or repo._activebookmark
362 379 if self.activebookmark:
363 380 bookmarks.deactivate(repo)
364 381
365 382 # Store the state before we begin so users can run 'hg rebase --abort'
366 383 # if we fail before the transaction closes.
367 384 self.storestatus()
368 385
369 386 sortedrevs = repo.revs('sort(%ld, -topo)', self.state)
370 387 cands = [k for k, v in self.state.iteritems() if v == revtodo]
371 388 total = len(cands)
372 389 pos = 0
373 390 for rev in sortedrevs:
391 dest = self.destmap[rev]
374 392 ctx = repo[rev]
375 393 desc = _ctxdesc(ctx)
376 394 if self.state[rev] == rev:
377 395 ui.status(_('already rebased %s\n') % desc)
378 396 elif self.state[rev] == revtodo:
379 397 pos += 1
380 398 ui.status(_('rebasing %s\n') % desc)
381 399 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
382 400 _('changesets'), total)
383 p1, p2, base = defineparents(repo, rev, self.dest, self.state)
401 p1, p2, base = defineparents(repo, rev, self.destmap,
402 self.state)
384 403 self.storestatus(tr=tr)
385 404 storecollapsemsg(repo, self.collapsemsg)
386 405 if len(repo[None].parents()) == 2:
387 406 repo.ui.debug('resuming interrupted rebase\n')
388 407 else:
389 408 try:
390 409 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
391 410 'rebase')
392 411 stats = rebasenode(repo, rev, p1, base, self.state,
393 self.collapsef, self.dest)
412 self.collapsef, dest)
394 413 if stats and stats[3] > 0:
395 414 raise error.InterventionRequired(
396 415 _('unresolved conflicts (see hg '
397 416 'resolve, then hg rebase --continue)'))
398 417 finally:
399 418 ui.setconfig('ui', 'forcemerge', '', 'rebase')
400 419 if not self.collapsef:
401 420 merging = p2 != nullrev
402 421 editform = cmdutil.mergeeditform(merging, 'rebase')
403 422 editor = cmdutil.getcommiteditor(editform=editform, **opts)
404 423 newnode = concludenode(repo, rev, p1, p2,
405 424 extrafn=_makeextrafn(self.extrafns),
406 425 editor=editor,
407 426 keepbranches=self.keepbranchesf,
408 427 date=self.date)
409 428 if newnode is None:
410 429 # If it ended up being a no-op commit, then the normal
411 430 # merge state clean-up path doesn't happen, so do it
412 431 # here. Fix issue5494
413 432 mergemod.mergestate.clean(repo)
414 433 else:
415 434 # Skip commit if we are collapsing
416 435 repo.setparents(repo[p1].node())
417 436 newnode = None
418 437 # Update the state
419 438 if newnode is not None:
420 439 self.state[rev] = repo[newnode].rev()
421 440 ui.debug('rebased as %s\n' % short(newnode))
422 441 else:
423 442 if not self.collapsef:
424 443 ui.warn(_('note: rebase of %d:%s created no changes '
425 444 'to commit\n') % (rev, ctx))
426 445 self.skipped.add(rev)
427 446 self.state[rev] = p1
428 447 ui.debug('next revision set to %s\n' % p1)
429 448 else:
430 449 ui.status(_('already rebased %s as %s\n') %
431 450 (desc, repo[self.state[rev]]))
432 451
433 452 ui.progress(_('rebasing'), None)
434 453 ui.note(_('rebase merging completed\n'))
435 454
436 455 def _finishrebase(self):
437 456 repo, ui, opts = self.repo, self.ui, self.opts
438 457 if self.collapsef and not self.keepopen:
439 p1, p2, _base = defineparents(repo, min(self.state),
440 self.dest, self.state)
458 p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
459 self.state)
441 460 editopt = opts.get('edit')
442 461 editform = 'rebase.collapse'
443 462 if self.collapsemsg:
444 463 commitmsg = self.collapsemsg
445 464 else:
446 465 commitmsg = 'Collapsed revision'
447 466 for rebased in sorted(self.state):
448 467 if rebased not in self.skipped:
449 468 commitmsg += '\n* %s' % repo[rebased].description()
450 469 editopt = True
451 470 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
452 471 revtoreuse = max(self.state)
453 472
454 473 dsguard = None
455 474 if ui.configbool('rebase', 'singletransaction'):
456 475 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
457 476 with util.acceptintervention(dsguard):
458 477 newnode = concludenode(repo, revtoreuse, p1, self.external,
459 478 commitmsg=commitmsg,
460 479 extrafn=_makeextrafn(self.extrafns),
461 480 editor=editor,
462 481 keepbranches=self.keepbranchesf,
463 482 date=self.date)
464 483 if newnode is not None:
465 484 newrev = repo[newnode].rev()
466 485 for oldrev in self.state.iterkeys():
467 486 self.state[oldrev] = newrev
468 487
469 488 if 'qtip' in repo.tags():
470 489 updatemq(repo, self.state, self.skipped, **opts)
471 490
472 491 # restore original working directory
473 492 # (we do this before stripping)
474 493 newwd = self.state.get(self.originalwd, self.originalwd)
475 494 if newwd < 0:
476 495 # original directory is a parent of rebase set root or ignored
477 496 newwd = self.originalwd
478 497 if newwd not in [c.rev() for c in repo[None].parents()]:
479 498 ui.note(_("update back to initial working directory parent\n"))
480 499 hg.updaterepo(repo, newwd, False)
481 500
482 501 if not self.keepf:
483 502 collapsedas = None
484 503 if self.collapsef:
485 504 collapsedas = newnode
486 clearrebased(ui, repo, self.dest, self.state, self.skipped,
505 clearrebased(ui, repo, self.destmap, self.state, self.skipped,
487 506 collapsedas)
488 507
489 508 clearstatus(repo)
490 509 clearcollapsemsg(repo)
491 510
492 511 ui.note(_("rebase completed\n"))
493 512 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
494 513 if self.skipped:
495 514 skippedlen = len(self.skipped)
496 515 ui.note(_("%d revisions have been skipped\n") % skippedlen)
497 516
498 517 if (self.activebookmark and self.activebookmark in repo._bookmarks and
499 518 repo['.'].node() == repo._bookmarks[self.activebookmark]):
500 519 bookmarks.activate(repo, self.activebookmark)
501 520
502 521 @command('rebase',
503 522 [('s', 'source', '',
504 523 _('rebase the specified changeset and descendants'), _('REV')),
505 524 ('b', 'base', '',
506 525 _('rebase everything from branching point of specified changeset'),
507 526 _('REV')),
508 527 ('r', 'rev', [],
509 528 _('rebase these revisions'),
510 529 _('REV')),
511 530 ('d', 'dest', '',
512 531 _('rebase onto the specified changeset'), _('REV')),
513 532 ('', 'collapse', False, _('collapse the rebased changesets')),
514 533 ('m', 'message', '',
515 534 _('use text as collapse commit message'), _('TEXT')),
516 535 ('e', 'edit', False, _('invoke editor on commit messages')),
517 536 ('l', 'logfile', '',
518 537 _('read collapse commit message from file'), _('FILE')),
519 538 ('k', 'keep', False, _('keep original changesets')),
520 539 ('', 'keepbranches', False, _('keep original branch names')),
521 540 ('D', 'detach', False, _('(DEPRECATED)')),
522 541 ('i', 'interactive', False, _('(DEPRECATED)')),
523 542 ('t', 'tool', '', _('specify merge tool')),
524 543 ('c', 'continue', False, _('continue an interrupted rebase')),
525 544 ('a', 'abort', False, _('abort an interrupted rebase'))] +
526 545 templateopts,
527 546 _('[-s REV | -b REV] [-d REV] [OPTION]'))
528 547 def rebase(ui, repo, **opts):
529 548 """move changeset (and descendants) to a different branch
530 549
531 550 Rebase uses repeated merging to graft changesets from one part of
532 551 history (the source) onto another (the destination). This can be
533 552 useful for linearizing *local* changes relative to a master
534 553 development tree.
535 554
536 555 Published commits cannot be rebased (see :hg:`help phases`).
537 556 To copy commits, see :hg:`help graft`.
538 557
539 558 If you don't specify a destination changeset (``-d/--dest``), rebase
540 559 will use the same logic as :hg:`merge` to pick a destination. if
541 560 the current branch contains exactly one other head, the other head
542 561 is merged with by default. Otherwise, an explicit revision with
543 562 which to merge with must be provided. (destination changeset is not
544 563 modified by rebasing, but new changesets are added as its
545 564 descendants.)
546 565
547 566 Here are the ways to select changesets:
548 567
549 568 1. Explicitly select them using ``--rev``.
550 569
551 570 2. Use ``--source`` to select a root changeset and include all of its
552 571 descendants.
553 572
554 573 3. Use ``--base`` to select a changeset; rebase will find ancestors
555 574 and their descendants which are not also ancestors of the destination.
556 575
557 576 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
558 577 rebase will use ``--base .`` as above.
559 578
560 579 Rebase will destroy original changesets unless you use ``--keep``.
561 580 It will also move your bookmarks (even if you do).
562 581
563 582 Some changesets may be dropped if they do not contribute changes
564 583 (e.g. merges from the destination branch).
565 584
566 585 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
567 586 a named branch with two heads. You will need to explicitly specify source
568 587 and/or destination.
569 588
570 589 If you need to use a tool to automate merge/conflict decisions, you
571 590 can specify one with ``--tool``, see :hg:`help merge-tools`.
572 591 As a caveat: the tool will not be used to mediate when a file was
573 592 deleted, there is no hook presently available for this.
574 593
575 594 If a rebase is interrupted to manually resolve a conflict, it can be
576 595 continued with --continue/-c or aborted with --abort/-a.
577 596
578 597 .. container:: verbose
579 598
580 599 Examples:
581 600
582 601 - move "local changes" (current commit back to branching point)
583 602 to the current branch tip after a pull::
584 603
585 604 hg rebase
586 605
587 606 - move a single changeset to the stable branch::
588 607
589 608 hg rebase -r 5f493448 -d stable
590 609
591 610 - splice a commit and all its descendants onto another part of history::
592 611
593 612 hg rebase --source c0c3 --dest 4cf9
594 613
595 614 - rebase everything on a branch marked by a bookmark onto the
596 615 default branch::
597 616
598 617 hg rebase --base myfeature --dest default
599 618
600 619 - collapse a sequence of changes into a single commit::
601 620
602 621 hg rebase --collapse -r 1520:1525 -d .
603 622
604 623 - move a named branch while preserving its name::
605 624
606 625 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
607 626
608 627 Configuration Options:
609 628
610 629 You can make rebase require a destination if you set the following config
611 630 option::
612 631
613 632 [commands]
614 633 rebase.requiredest = True
615 634
616 635 By default, rebase will close the transaction after each commit. For
617 636 performance purposes, you can configure rebase to use a single transaction
618 637 across the entire rebase. WARNING: This setting introduces a significant
619 638 risk of losing the work you've done in a rebase if the rebase aborts
620 639 unexpectedly::
621 640
622 641 [rebase]
623 642 singletransaction = True
624 643
625 644 Return Values:
626 645
627 646 Returns 0 on success, 1 if nothing to rebase or there are
628 647 unresolved conflicts.
629 648
630 649 """
631 650 rbsrt = rebaseruntime(repo, ui, opts)
632 651
633 652 with repo.wlock(), repo.lock():
634 653 # Validate input and define rebasing points
635 654 destf = opts.get('dest', None)
636 655 srcf = opts.get('source', None)
637 656 basef = opts.get('base', None)
638 657 revf = opts.get('rev', [])
639 658 # search default destination in this space
640 659 # used in the 'hg pull --rebase' case, see issue 5214.
641 660 destspace = opts.get('_destspace')
642 661 contf = opts.get('continue')
643 662 abortf = opts.get('abort')
644 663 if opts.get('interactive'):
645 664 try:
646 665 if extensions.find('histedit'):
647 666 enablehistedit = ''
648 667 except KeyError:
649 668 enablehistedit = " --config extensions.histedit="
650 669 help = "hg%s help -e histedit" % enablehistedit
651 670 msg = _("interactive history editing is supported by the "
652 671 "'histedit' extension (see \"%s\")") % help
653 672 raise error.Abort(msg)
654 673
655 674 if rbsrt.collapsemsg and not rbsrt.collapsef:
656 675 raise error.Abort(
657 676 _('message can only be specified with collapse'))
658 677
659 678 if contf or abortf:
660 679 if contf and abortf:
661 680 raise error.Abort(_('cannot use both abort and continue'))
662 681 if rbsrt.collapsef:
663 682 raise error.Abort(
664 683 _('cannot use collapse with continue or abort'))
665 684 if srcf or basef or destf:
666 685 raise error.Abort(
667 686 _('abort and continue do not allow specifying revisions'))
668 687 if abortf and opts.get('tool', False):
669 688 ui.warn(_('tool option will be ignored\n'))
670 689 if contf:
671 690 ms = mergemod.mergestate.read(repo)
672 691 mergeutil.checkunresolved(ms)
673 692
674 693 retcode = rbsrt._prepareabortorcontinue(abortf)
675 694 if retcode is not None:
676 695 return retcode
677 696 else:
678 dest, rebaseset = _definesets(ui, repo, destf, srcf, basef, revf,
679 destspace=destspace)
680 retcode = rbsrt._preparenewrebase(dest, rebaseset)
697 destmap = _definedestmap(ui, repo, destf, srcf, basef, revf,
698 destspace=destspace)
699 retcode = rbsrt._preparenewrebase(destmap)
681 700 if retcode is not None:
682 701 return retcode
683 702
684 703 tr = None
685 704 dsguard = None
686 705
687 706 singletr = ui.configbool('rebase', 'singletransaction')
688 707 if singletr:
689 708 tr = repo.transaction('rebase')
690 709 with util.acceptintervention(tr):
691 710 if singletr:
692 711 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
693 712 with util.acceptintervention(dsguard):
694 713 rbsrt._performrebase(tr)
695 714
696 715 rbsrt._finishrebase()
697 716
698 def _definesets(ui, repo, destf=None, srcf=None, basef=None, revf=None,
699 destspace=None):
700 """use revisions argument to define destination and rebase set
701 """
717 def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
718 destspace=None):
719 """use revisions argument to define destmap {srcrev: destrev}"""
702 720 if revf is None:
703 721 revf = []
704 722
705 723 # destspace is here to work around issues with `hg pull --rebase` see
706 724 # issue5214 for details
707 725 if srcf and basef:
708 726 raise error.Abort(_('cannot specify both a source and a base'))
709 727 if revf and basef:
710 728 raise error.Abort(_('cannot specify both a revision and a base'))
711 729 if revf and srcf:
712 730 raise error.Abort(_('cannot specify both a revision and a source'))
713 731
714 732 cmdutil.checkunfinished(repo)
715 733 cmdutil.bailifchanged(repo)
716 734
717 735 if ui.configbool('commands', 'rebase.requiredest') and not destf:
718 736 raise error.Abort(_('you must specify a destination'),
719 737 hint=_('use: hg rebase -d REV'))
720 738
721 739 if destf:
722 740 dest = scmutil.revsingle(repo, destf)
723 741
724 742 if revf:
725 743 rebaseset = scmutil.revrange(repo, revf)
726 744 if not rebaseset:
727 745 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
728 return None, None
746 return None
729 747 elif srcf:
730 748 src = scmutil.revrange(repo, [srcf])
731 749 if not src:
732 750 ui.status(_('empty "source" revision set - nothing to rebase\n'))
733 return None, None
751 return None
734 752 rebaseset = repo.revs('(%ld)::', src)
735 753 assert rebaseset
736 754 else:
737 755 base = scmutil.revrange(repo, [basef or '.'])
738 756 if not base:
739 757 ui.status(_('empty "base" revision set - '
740 758 "can't compute rebase set\n"))
741 return None, None
759 return None
742 760 if not destf:
743 761 dest = repo[_destrebase(repo, base, destspace=destspace)]
744 762 destf = str(dest)
745 763
746 764 roots = [] # selected children of branching points
747 765 bpbase = {} # {branchingpoint: [origbase]}
748 766 for b in base: # group bases by branching points
749 767 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
750 768 bpbase[bp] = bpbase.get(bp, []) + [b]
751 769 if None in bpbase:
752 770 # emulate the old behavior, showing "nothing to rebase" (a better
753 771 # behavior may be abort with "cannot find branching point" error)
754 772 bpbase.clear()
755 773 for bp, bs in bpbase.iteritems(): # calculate roots
756 774 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
757 775
758 776 rebaseset = repo.revs('%ld::', roots)
759 777
760 778 if not rebaseset:
761 779 # transform to list because smartsets are not comparable to
762 780 # lists. This should be improved to honor laziness of
763 781 # smartset.
764 782 if list(base) == [dest.rev()]:
765 783 if basef:
766 784 ui.status(_('nothing to rebase - %s is both "base"'
767 785 ' and destination\n') % dest)
768 786 else:
769 787 ui.status(_('nothing to rebase - working directory '
770 788 'parent is also destination\n'))
771 789 elif not repo.revs('%ld - ::%d', base, dest):
772 790 if basef:
773 791 ui.status(_('nothing to rebase - "base" %s is '
774 792 'already an ancestor of destination '
775 793 '%s\n') %
776 794 ('+'.join(str(repo[r]) for r in base),
777 795 dest))
778 796 else:
779 797 ui.status(_('nothing to rebase - working '
780 798 'directory parent is already an '
781 799 'ancestor of destination %s\n') % dest)
782 800 else: # can it happen?
783 801 ui.status(_('nothing to rebase from %s to %s\n') %
784 802 ('+'.join(str(repo[r]) for r in base), dest))
785 return None, None
803 return None
786 804
787 805 if not destf:
788 806 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
789 807 destf = str(dest)
790 808
791 return dest, rebaseset
809 # assign dest to each rev in rebaseset
810 destrev = dest.rev()
811 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
812
813 return destmap
792 814
793 815 def externalparent(repo, state, destancestors):
794 816 """Return the revision that should be used as the second parent
795 817 when the revisions in state is collapsed on top of destancestors.
796 818 Abort if there is more than one parent.
797 819 """
798 820 parents = set()
799 821 source = min(state)
800 822 for rev in state:
801 823 if rev == source:
802 824 continue
803 825 for p in repo[rev].parents():
804 826 if (p.rev() not in state
805 827 and p.rev() not in destancestors):
806 828 parents.add(p.rev())
807 829 if not parents:
808 830 return nullrev
809 831 if len(parents) == 1:
810 832 return parents.pop()
811 833 raise error.Abort(_('unable to collapse on top of %s, there is more '
812 834 'than one external parent: %s') %
813 835 (max(destancestors),
814 836 ', '.join(str(p) for p in sorted(parents))))
815 837
816 838 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
817 839 keepbranches=False, date=None):
818 840 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
819 841 but also store useful information in extra.
820 842 Return node of committed revision.'''
821 843 dsguard = util.nullcontextmanager()
822 844 if not repo.ui.configbool('rebase', 'singletransaction'):
823 845 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
824 846 with dsguard:
825 847 repo.setparents(repo[p1].node(), repo[p2].node())
826 848 ctx = repo[rev]
827 849 if commitmsg is None:
828 850 commitmsg = ctx.description()
829 851 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
830 852 extra = {'rebase_source': ctx.hex()}
831 853 if extrafn:
832 854 extrafn(ctx, extra)
833 855
834 856 destphase = max(ctx.phase(), phases.draft)
835 857 overrides = {('phases', 'new-commit'): destphase}
836 858 with repo.ui.configoverride(overrides, 'rebase'):
837 859 if keepbranch:
838 860 repo.ui.setconfig('ui', 'allowemptycommit', True)
839 861 # Commit might fail if unresolved files exist
840 862 if date is None:
841 863 date = ctx.date()
842 864 newnode = repo.commit(text=commitmsg, user=ctx.user(),
843 865 date=date, extra=extra, editor=editor)
844 866
845 867 repo.dirstate.setbranch(repo[newnode].branch())
846 868 return newnode
847 869
848 870 def rebasenode(repo, rev, p1, base, state, collapse, dest):
849 871 'Rebase a single revision rev on top of p1 using base as merge ancestor'
850 872 # Merge phase
851 873 # Update to destination and merge it with local
852 874 if repo['.'].rev() != p1:
853 875 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
854 876 mergemod.update(repo, p1, False, True)
855 877 else:
856 878 repo.ui.debug(" already in destination\n")
857 879 repo.dirstate.write(repo.currenttransaction())
858 880 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
859 881 if base is not None:
860 882 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
861 883 # When collapsing in-place, the parent is the common ancestor, we
862 884 # have to allow merging with it.
863 885 stats = mergemod.update(repo, rev, True, True, base, collapse,
864 886 labels=['dest', 'source'])
865 887 if collapse:
866 888 copies.duplicatecopies(repo, rev, dest)
867 889 else:
868 890 # If we're not using --collapse, we need to
869 891 # duplicate copies between the revision we're
870 892 # rebasing and its first parent, but *not*
871 893 # duplicate any copies that have already been
872 894 # performed in the destination.
873 895 p1rev = repo[rev].p1().rev()
874 896 copies.duplicatecopies(repo, rev, p1rev, skiprev=dest)
875 897 return stats
876 898
877 def adjustdest(repo, rev, dest, state):
899 def adjustdest(repo, rev, destmap, state):
878 900 """adjust rebase destination given the current rebase state
879 901
880 902 rev is what is being rebased. Return a list of two revs, which are the
881 903 adjusted destinations for rev's p1 and p2, respectively. If a parent is
882 904 nullrev, return dest without adjustment for it.
883 905
884 906 For example, when doing rebase -r B+E -d F, rebase will first move B to B1,
885 907 and E's destination will be adjusted from F to B1.
886 908
887 909 B1 <- written during rebasing B
888 910 |
889 911 F <- original destination of B, E
890 912 |
891 913 | E <- rev, which is being rebased
892 914 | |
893 915 | D <- prev, one parent of rev being checked
894 916 | |
895 917 | x <- skipped, ex. no successor or successor in (::dest)
896 918 | |
897 919 | C
898 920 | |
899 921 | B <- rebased as B1
900 922 |/
901 923 A
902 924
903 925 Another example about merge changeset, rebase -r C+G+H -d K, rebase will
904 926 first move C to C1, G to G1, and when it's checking H, the adjusted
905 927 destinations will be [C1, G1].
906 928
907 929 H C1 G1
908 930 /| | /
909 931 F G |/
910 932 K | | -> K
911 933 | C D |
912 934 | |/ |
913 935 | B | ...
914 936 |/ |/
915 937 A A
916 938 """
917 # pick already rebased revs from state
918 source = [s for s, d in state.items() if d > 0]
939 # pick already rebased revs with same dest from state as interesting source
940 dest = destmap[rev]
941 source = [s for s, d in state.items() if d > 0 and destmap[s] == dest]
919 942
920 943 result = []
921 944 for prev in repo.changelog.parentrevs(rev):
922 945 adjusted = dest
923 946 if prev != nullrev:
924 947 candidate = repo.revs('max(%ld and (::%d))', source, prev).first()
925 948 if candidate is not None:
926 949 adjusted = state[candidate]
927 950 result.append(adjusted)
928 951 return result
929 952
930 953 def _checkobsrebase(repo, ui, rebaseobsrevs, rebaseobsskipped):
931 954 """
932 955 Abort if rebase will create divergence or rebase is noop because of markers
933 956
934 957 `rebaseobsrevs`: set of obsolete revision in source
935 958 `rebaseobsskipped`: set of revisions from source skipped because they have
936 959 successors in destination
937 960 """
938 961 # Obsolete node with successors not in dest leads to divergence
939 962 divergenceok = ui.configbool('experimental',
940 963 'allowdivergence')
941 964 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
942 965
943 966 if divergencebasecandidates and not divergenceok:
944 967 divhashes = (str(repo[r])
945 968 for r in divergencebasecandidates)
946 969 msg = _("this rebase will cause "
947 970 "divergences from: %s")
948 971 h = _("to force the rebase please set "
949 972 "experimental.allowdivergence=True")
950 973 raise error.Abort(msg % (",".join(divhashes),), hint=h)
951 974
952 975 def successorrevs(repo, rev):
953 976 """yield revision numbers for successors of rev"""
954 977 unfi = repo.unfiltered()
955 978 nodemap = unfi.changelog.nodemap
956 979 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
957 980 if s in nodemap:
958 981 yield nodemap[s]
959 982
960 def defineparents(repo, rev, dest, state):
983 def defineparents(repo, rev, destmap, state):
961 984 """Return new parents and optionally a merge base for rev being rebased
962 985
963 986 The destination specified by "dest" cannot always be used directly because
964 987 previously rebase result could affect destination. For example,
965 988
966 989 D E rebase -r C+D+E -d B
967 990 |/ C will be rebased to C'
968 991 B C D's new destination will be C' instead of B
969 992 |/ E's new destination will be C' instead of B
970 993 A
971 994
972 995 The new parents of a merge is slightly more complicated. See the comment
973 996 block below.
974 997 """
975 998 cl = repo.changelog
976 999 def isancestor(a, b):
977 1000 # take revision numbers instead of nodes
978 1001 if a == b:
979 1002 return True
980 1003 elif a > b:
981 1004 return False
982 1005 return cl.isancestor(cl.node(a), cl.node(b))
983 1006
1007 dest = destmap[rev]
984 1008 oldps = repo.changelog.parentrevs(rev) # old parents
985 1009 newps = [nullrev, nullrev] # new parents
986 dests = adjustdest(repo, rev, dest, state) # adjusted destinations
1010 dests = adjustdest(repo, rev, destmap, state) # adjusted destinations
987 1011 bases = list(oldps) # merge base candidates, initially just old parents
988 1012
989 1013 if all(r == nullrev for r in oldps[1:]):
990 1014 # For non-merge changeset, just move p to adjusted dest as requested.
991 1015 newps[0] = dests[0]
992 1016 else:
993 1017 # For merge changeset, if we move p to dests[i] unconditionally, both
994 1018 # parents may change and the end result looks like "the merge loses a
995 1019 # parent", which is a surprise. This is a limit because "--dest" only
996 1020 # accepts one dest per src.
997 1021 #
998 1022 # Therefore, only move p with reasonable conditions (in this order):
999 1023 # 1. use dest, if dest is a descendent of (p or one of p's successors)
1000 1024 # 2. use p's rebased result, if p is rebased (state[p] > 0)
1001 1025 #
1002 1026 # Comparing with adjustdest, the logic here does some additional work:
1003 1027 # 1. decide which parents will not be moved towards dest
1004 1028 # 2. if the above decision is "no", should a parent still be moved
1005 1029 # because it was rebased?
1006 1030 #
1007 1031 # For example:
1008 1032 #
1009 1033 # C # "rebase -r C -d D" is an error since none of the parents
1010 1034 # /| # can be moved. "rebase -r B+C -d D" will move C's parent
1011 1035 # A B D # B (using rule "2."), since B will be rebased.
1012 1036 #
1013 1037 # The loop tries to be not rely on the fact that a Mercurial node has
1014 1038 # at most 2 parents.
1015 1039 for i, p in enumerate(oldps):
1016 1040 np = p # new parent
1017 1041 if any(isancestor(x, dests[i]) for x in successorrevs(repo, p)):
1018 1042 np = dests[i]
1019 1043 elif p in state and state[p] > 0:
1020 1044 np = state[p]
1021 1045
1022 1046 # "bases" only record "special" merge bases that cannot be
1023 1047 # calculated from changelog DAG (i.e. isancestor(p, np) is False).
1024 1048 # For example:
1025 1049 #
1026 1050 # B' # rebase -s B -d D, when B was rebased to B'. dest for C
1027 1051 # | C # is B', but merge base for C is B, instead of
1028 1052 # D | # changelog.ancestor(C, B') == A. If changelog DAG and
1029 1053 # | B # "state" edges are merged (so there will be an edge from
1030 1054 # |/ # B to B'), the merge base is still ancestor(C, B') in
1031 1055 # A # the merged graph.
1032 1056 #
1033 1057 # Also see https://bz.mercurial-scm.org/show_bug.cgi?id=1950#c8
1034 1058 # which uses "virtual null merge" to explain this situation.
1035 1059 if isancestor(p, np):
1036 1060 bases[i] = nullrev
1037 1061
1038 1062 # If one parent becomes an ancestor of the other, drop the ancestor
1039 1063 for j, x in enumerate(newps[:i]):
1040 1064 if x == nullrev:
1041 1065 continue
1042 1066 if isancestor(np, x): # CASE-1
1043 1067 np = nullrev
1044 1068 elif isancestor(x, np): # CASE-2
1045 1069 newps[j] = np
1046 1070 np = nullrev
1047 1071 # New parents forming an ancestor relationship does not
1048 1072 # mean the old parents have a similar relationship. Do not
1049 1073 # set bases[x] to nullrev.
1050 1074 bases[j], bases[i] = bases[i], bases[j]
1051 1075
1052 1076 newps[i] = np
1053 1077
1054 1078 # "rebasenode" updates to new p1, and the old p1 will be used as merge
1055 1079 # base. If only p2 changes, merging using unchanged p1 as merge base is
1056 1080 # suboptimal. Therefore swap parents to make the merge sane.
1057 1081 if newps[1] != nullrev and oldps[0] == newps[0]:
1058 1082 assert len(newps) == 2 and len(oldps) == 2
1059 1083 newps.reverse()
1060 1084 bases.reverse()
1061 1085
1062 1086 # No parent change might be an error because we fail to make rev a
1063 1087 # descendent of requested dest. This can happen, for example:
1064 1088 #
1065 1089 # C # rebase -r C -d D
1066 1090 # /| # None of A and B will be changed to D and rebase fails.
1067 1091 # A B D
1068 1092 if set(newps) == set(oldps) and dest not in newps:
1069 1093 raise error.Abort(_('cannot rebase %d:%s without '
1070 1094 'moving at least one of its parents')
1071 1095 % (rev, repo[rev]))
1072 1096
1073 1097 # "rebasenode" updates to new p1, use the corresponding merge base.
1074 1098 if bases[0] != nullrev:
1075 1099 base = bases[0]
1076 1100 else:
1077 1101 base = None
1078 1102
1079 1103 # Check if the merge will contain unwanted changes. That may happen if
1080 1104 # there are multiple special (non-changelog ancestor) merge bases, which
1081 1105 # cannot be handled well by the 3-way merge algorithm. For example:
1082 1106 #
1083 1107 # F
1084 1108 # /|
1085 1109 # D E # "rebase -r D+E+F -d Z", when rebasing F, if "D" was chosen
1086 1110 # | | # as merge base, the difference between D and F will include
1087 1111 # B C # C, so the rebased F will contain C surprisingly. If "E" was
1088 1112 # |/ # chosen, the rebased F will contain B.
1089 1113 # A Z
1090 1114 #
1091 1115 # But our merge base candidates (D and E in above case) could still be
1092 1116 # better than the default (ancestor(F, Z) == null). Therefore still
1093 1117 # pick one (so choose p1 above).
1094 1118 if sum(1 for b in bases if b != nullrev) > 1:
1095 1119 unwanted = [None, None] # unwanted[i]: unwanted revs if choose bases[i]
1096 1120 for i, base in enumerate(bases):
1097 1121 if base == nullrev:
1098 1122 continue
1099 1123 # Revisions in the side (not chosen as merge base) branch that
1100 1124 # might contain "surprising" contents
1101 1125 siderevs = list(repo.revs('((%ld-%d) %% (%d+%d))',
1102 1126 bases, base, base, dest))
1103 1127
1104 1128 # If those revisions are covered by rebaseset, the result is good.
1105 1129 # A merge in rebaseset would be considered to cover its ancestors.
1106 1130 if siderevs:
1107 1131 rebaseset = [r for r, d in state.items() if d > 0]
1108 1132 merges = [r for r in rebaseset
1109 1133 if cl.parentrevs(r)[1] != nullrev]
1110 1134 unwanted[i] = list(repo.revs('%ld - (::%ld) - %ld',
1111 1135 siderevs, merges, rebaseset))
1112 1136
1113 1137 # Choose a merge base that has a minimal number of unwanted revs.
1114 1138 l, i = min((len(revs), i)
1115 1139 for i, revs in enumerate(unwanted) if revs is not None)
1116 1140 base = bases[i]
1117 1141
1118 1142 # newps[0] should match merge base if possible. Currently, if newps[i]
1119 1143 # is nullrev, the only case is newps[i] and newps[j] (j < i), one is
1120 1144 # the other's ancestor. In that case, it's fine to not swap newps here.
1121 1145 # (see CASE-1 and CASE-2 above)
1122 1146 if i != 0 and newps[i] != nullrev:
1123 1147 newps[0], newps[i] = newps[i], newps[0]
1124 1148
1125 1149 # The merge will include unwanted revisions. Abort now. Revisit this if
1126 1150 # we have a more advanced merge algorithm that handles multiple bases.
1127 1151 if l > 0:
1128 1152 unwanteddesc = _(' or ').join(
1129 1153 (', '.join('%d:%s' % (r, repo[r]) for r in revs)
1130 1154 for revs in unwanted if revs is not None))
1131 1155 raise error.Abort(
1132 1156 _('rebasing %d:%s will include unwanted changes from %s')
1133 1157 % (rev, repo[rev], unwanteddesc))
1134 1158
1135 1159 repo.ui.debug(" future parents are %d and %d\n" % tuple(newps))
1136 1160
1137 1161 return newps[0], newps[1], base
1138 1162
1139 1163 def isagitpatch(repo, patchname):
1140 1164 'Return true if the given patch is in git format'
1141 1165 mqpatch = os.path.join(repo.mq.path, patchname)
1142 1166 for line in patch.linereader(file(mqpatch, 'rb')):
1143 1167 if line.startswith('diff --git'):
1144 1168 return True
1145 1169 return False
1146 1170
1147 1171 def updatemq(repo, state, skipped, **opts):
1148 1172 'Update rebased mq patches - finalize and then import them'
1149 1173 mqrebase = {}
1150 1174 mq = repo.mq
1151 1175 original_series = mq.fullseries[:]
1152 1176 skippedpatches = set()
1153 1177
1154 1178 for p in mq.applied:
1155 1179 rev = repo[p.node].rev()
1156 1180 if rev in state:
1157 1181 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1158 1182 (rev, p.name))
1159 1183 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1160 1184 else:
1161 1185 # Applied but not rebased, not sure this should happen
1162 1186 skippedpatches.add(p.name)
1163 1187
1164 1188 if mqrebase:
1165 1189 mq.finish(repo, mqrebase.keys())
1166 1190
1167 1191 # We must start import from the newest revision
1168 1192 for rev in sorted(mqrebase, reverse=True):
1169 1193 if rev not in skipped:
1170 1194 name, isgit = mqrebase[rev]
1171 1195 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1172 1196 (name, state[rev], repo[state[rev]]))
1173 1197 mq.qimport(repo, (), patchname=name, git=isgit,
1174 1198 rev=[str(state[rev])])
1175 1199 else:
1176 1200 # Rebased and skipped
1177 1201 skippedpatches.add(mqrebase[rev][0])
1178 1202
1179 1203 # Patches were either applied and rebased and imported in
1180 1204 # order, applied and removed or unapplied. Discard the removed
1181 1205 # ones while preserving the original series order and guards.
1182 1206 newseries = [s for s in original_series
1183 1207 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1184 1208 mq.fullseries[:] = newseries
1185 1209 mq.seriesdirty = True
1186 1210 mq.savedirty()
1187 1211
1188 1212 def storecollapsemsg(repo, collapsemsg):
1189 1213 'Store the collapse message to allow recovery'
1190 1214 collapsemsg = collapsemsg or ''
1191 1215 f = repo.vfs("last-message.txt", "w")
1192 1216 f.write("%s\n" % collapsemsg)
1193 1217 f.close()
1194 1218
1195 1219 def clearcollapsemsg(repo):
1196 1220 'Remove collapse message file'
1197 1221 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1198 1222
1199 1223 def restorecollapsemsg(repo, isabort):
1200 1224 'Restore previously stored collapse message'
1201 1225 try:
1202 1226 f = repo.vfs("last-message.txt")
1203 1227 collapsemsg = f.readline().strip()
1204 1228 f.close()
1205 1229 except IOError as err:
1206 1230 if err.errno != errno.ENOENT:
1207 1231 raise
1208 1232 if isabort:
1209 1233 # Oh well, just abort like normal
1210 1234 collapsemsg = ''
1211 1235 else:
1212 1236 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1213 1237 return collapsemsg
1214 1238
1215 1239 def clearstatus(repo):
1216 1240 'Remove the status files'
1217 1241 _clearrebasesetvisibiliy(repo)
1218 1242 # Make sure the active transaction won't write the state file
1219 1243 tr = repo.currenttransaction()
1220 1244 if tr:
1221 1245 tr.removefilegenerator('rebasestate')
1222 1246 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1223 1247
1224 1248 def needupdate(repo, state):
1225 1249 '''check whether we should `update --clean` away from a merge, or if
1226 1250 somehow the working dir got forcibly updated, e.g. by older hg'''
1227 1251 parents = [p.rev() for p in repo[None].parents()]
1228 1252
1229 1253 # Are we in a merge state at all?
1230 1254 if len(parents) < 2:
1231 1255 return False
1232 1256
1233 1257 # We should be standing on the first as-of-yet unrebased commit.
1234 1258 firstunrebased = min([old for old, new in state.iteritems()
1235 1259 if new == nullrev])
1236 1260 if firstunrebased in parents:
1237 1261 return True
1238 1262
1239 1263 return False
1240 1264
1241 def abort(repo, originalwd, dest, state, activebookmark=None):
1265 def abort(repo, originalwd, destmap, state, activebookmark=None):
1242 1266 '''Restore the repository to its original state. Additional args:
1243 1267
1244 1268 activebookmark: the name of the bookmark that should be active after the
1245 1269 restore'''
1246 1270
1247 1271 try:
1248 1272 # If the first commits in the rebased set get skipped during the rebase,
1249 1273 # their values within the state mapping will be the dest rev id. The
1250 1274 # dstates list must must not contain the dest rev (issue4896)
1251 dstates = [s for s in state.values() if s >= 0 and s != dest]
1275 dstates = [s for r, s in state.items() if s >= 0 and s != destmap[r]]
1252 1276 immutable = [d for d in dstates if not repo[d].mutable()]
1253 1277 cleanup = True
1254 1278 if immutable:
1255 1279 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1256 1280 % ', '.join(str(repo[r]) for r in immutable),
1257 1281 hint=_("see 'hg help phases' for details"))
1258 1282 cleanup = False
1259 1283
1260 1284 descendants = set()
1261 1285 if dstates:
1262 1286 descendants = set(repo.changelog.descendants(dstates))
1263 1287 if descendants - set(dstates):
1264 1288 repo.ui.warn(_("warning: new changesets detected on destination "
1265 1289 "branch, can't strip\n"))
1266 1290 cleanup = False
1267 1291
1268 1292 if cleanup:
1269 1293 shouldupdate = False
1270 rebased = filter(lambda x: x >= 0 and x != dest, state.values())
1294 rebased = [s for r, s in state.items()
1295 if s >= 0 and s != destmap[r]]
1271 1296 if rebased:
1272 1297 strippoints = [
1273 1298 c.node() for c in repo.set('roots(%ld)', rebased)]
1274 1299
1275 1300 updateifonnodes = set(rebased)
1276 updateifonnodes.add(dest)
1301 updateifonnodes.update(destmap.values())
1277 1302 updateifonnodes.add(originalwd)
1278 1303 shouldupdate = repo['.'].rev() in updateifonnodes
1279 1304
1280 1305 # Update away from the rebase if necessary
1281 1306 if shouldupdate or needupdate(repo, state):
1282 1307 mergemod.update(repo, originalwd, False, True)
1283 1308
1284 1309 # Strip from the first rebased revision
1285 1310 if rebased:
1286 1311 # no backup of rebased cset versions needed
1287 1312 repair.strip(repo.ui, repo, strippoints)
1288 1313
1289 1314 if activebookmark and activebookmark in repo._bookmarks:
1290 1315 bookmarks.activate(repo, activebookmark)
1291 1316
1292 1317 finally:
1293 1318 clearstatus(repo)
1294 1319 clearcollapsemsg(repo)
1295 1320 repo.ui.warn(_('rebase aborted\n'))
1296 1321 return 0
1297 1322
1298 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased):
1323 def buildstate(repo, destmap, collapse, obsoletenotrebased):
1299 1324 '''Define which revisions are going to be rebased and where
1300 1325
1301 1326 repo: repo
1302 dest: context
1303 rebaseset: set of rev
1327 destmap: {srcrev: destrev}
1304 1328 '''
1329 rebaseset = destmap.keys()
1305 1330 originalwd = repo['.'].rev()
1306 1331 _setrebasesetvisibility(repo, set(rebaseset) | {originalwd})
1307 1332
1308 1333 # This check isn't strictly necessary, since mq detects commits over an
1309 1334 # applied patch. But it prevents messing up the working directory when
1310 1335 # a partially completed rebase is blocked by mq.
1311 if 'qtip' in repo.tags() and (dest.node() in
1312 [s.node for s in repo.mq.applied]):
1313 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1336 if 'qtip' in repo.tags():
1337 mqapplied = set(repo[s.node].rev() for s in repo.mq.applied)
1338 if set(destmap.values()) & mqapplied:
1339 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1314 1340
1315 1341 roots = list(repo.set('roots(%ld)', rebaseset))
1316 1342 if not roots:
1317 1343 raise error.Abort(_('no matching revisions'))
1318 1344 roots.sort()
1319 1345 state = dict.fromkeys(rebaseset, revtodo)
1320 1346 emptyrebase = True
1321 1347 for root in roots:
1348 dest = repo[destmap[root.rev()]]
1322 1349 commonbase = root.ancestor(dest)
1323 1350 if commonbase == root:
1324 1351 raise error.Abort(_('source is ancestor of destination'))
1325 1352 if commonbase == dest:
1326 1353 wctx = repo[None]
1327 1354 if dest == wctx.p1():
1328 1355 # when rebasing to '.', it will use the current wd branch name
1329 1356 samebranch = root.branch() == wctx.branch()
1330 1357 else:
1331 1358 samebranch = root.branch() == dest.branch()
1332 1359 if not collapse and samebranch and dest in root.parents():
1333 1360 # mark the revision as done by setting its new revision
1334 1361 # equal to its old (current) revisions
1335 1362 state[root.rev()] = root.rev()
1336 1363 repo.ui.debug('source is a child of destination\n')
1337 1364 continue
1338 1365
1339 1366 emptyrebase = False
1340 1367 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1341 1368 if emptyrebase:
1342 1369 return None
1343 1370 for rev in sorted(state):
1344 1371 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1345 1372 # if all parents of this revision are done, then so is this revision
1346 1373 if parents and all((state.get(p) == p for p in parents)):
1347 1374 state[rev] = rev
1348 1375 unfi = repo.unfiltered()
1349 1376 for r in obsoletenotrebased:
1350 1377 desc = _ctxdesc(unfi[r])
1351 1378 succ = obsoletenotrebased[r]
1352 1379 if succ is None:
1353 1380 msg = _('note: not rebasing %s, it has no successor\n') % desc
1354 1381 del state[r]
1382 del destmap[r]
1355 1383 else:
1356 1384 destctx = unfi[succ]
1357 1385 destdesc = '%d:%s "%s"' % (destctx.rev(), destctx,
1358 1386 destctx.description().split('\n', 1)[0])
1359 1387 msg = (_('note: not rebasing %s, already in destination as %s\n')
1360 1388 % (desc, destdesc))
1361 1389 del state[r]
1390 del destmap[r]
1362 1391 repo.ui.status(msg)
1363 return originalwd, dest.rev(), state
1392 return originalwd, destmap, state
1364 1393
1365 def clearrebased(ui, repo, dest, state, skipped, collapsedas=None):
1394 def clearrebased(ui, repo, destmap, state, skipped, collapsedas=None):
1366 1395 """dispose of rebased revision at the end of the rebase
1367 1396
1368 1397 If `collapsedas` is not None, the rebase was a collapse whose result if the
1369 1398 `collapsedas` node."""
1370 1399 tonode = repo.changelog.node
1371 1400 # Move bookmark of skipped nodes to destination. This cannot be handled
1372 1401 # by scmutil.cleanupnodes since it will treat rev as removed (no successor)
1373 1402 # and move bookmark backwards.
1374 bmchanges = [(name, tonode(max(adjustdest(repo, rev, dest, state))))
1403 bmchanges = [(name, tonode(max(adjustdest(repo, rev, destmap, state))))
1375 1404 for rev in skipped
1376 1405 for name in repo.nodebookmarks(tonode(rev))]
1377 1406 if bmchanges:
1378 1407 with repo.transaction('rebase') as tr:
1379 1408 repo._bookmarks.applychanges(repo, tr, bmchanges)
1380 1409 mapping = {}
1381 1410 for rev, newrev in sorted(state.items()):
1382 1411 if newrev >= 0 and newrev != rev:
1383 1412 if rev in skipped:
1384 1413 succs = ()
1385 1414 elif collapsedas is not None:
1386 1415 succs = (collapsedas,)
1387 1416 else:
1388 1417 succs = (tonode(newrev),)
1389 1418 mapping[tonode(rev)] = succs
1390 1419 scmutil.cleanupnodes(repo, mapping, 'rebase')
1391 1420
1392 1421 def pullrebase(orig, ui, repo, *args, **opts):
1393 1422 'Call rebase after pull if the latter has been invoked with --rebase'
1394 1423 ret = None
1395 1424 if opts.get('rebase'):
1396 1425 if ui.configbool('commands', 'rebase.requiredest'):
1397 1426 msg = _('rebase destination required by configuration')
1398 1427 hint = _('use hg pull followed by hg rebase -d DEST')
1399 1428 raise error.Abort(msg, hint=hint)
1400 1429
1401 1430 with repo.wlock(), repo.lock():
1402 1431 if opts.get('update'):
1403 1432 del opts['update']
1404 1433 ui.debug('--update and --rebase are not compatible, ignoring '
1405 1434 'the update flag\n')
1406 1435
1407 1436 cmdutil.checkunfinished(repo)
1408 1437 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1409 1438 'please commit or shelve your changes first'))
1410 1439
1411 1440 revsprepull = len(repo)
1412 1441 origpostincoming = commands.postincoming
1413 1442 def _dummy(*args, **kwargs):
1414 1443 pass
1415 1444 commands.postincoming = _dummy
1416 1445 try:
1417 1446 ret = orig(ui, repo, *args, **opts)
1418 1447 finally:
1419 1448 commands.postincoming = origpostincoming
1420 1449 revspostpull = len(repo)
1421 1450 if revspostpull > revsprepull:
1422 1451 # --rev option from pull conflict with rebase own --rev
1423 1452 # dropping it
1424 1453 if 'rev' in opts:
1425 1454 del opts['rev']
1426 1455 # positional argument from pull conflicts with rebase's own
1427 1456 # --source.
1428 1457 if 'source' in opts:
1429 1458 del opts['source']
1430 1459 # revsprepull is the len of the repo, not revnum of tip.
1431 1460 destspace = list(repo.changelog.revs(start=revsprepull))
1432 1461 opts['_destspace'] = destspace
1433 1462 try:
1434 1463 rebase(ui, repo, **opts)
1435 1464 except error.NoMergeDestAbort:
1436 1465 # we can maybe update instead
1437 1466 rev, _a, _b = destutil.destupdate(repo)
1438 1467 if rev == repo['.'].rev():
1439 1468 ui.status(_('nothing to rebase\n'))
1440 1469 else:
1441 1470 ui.status(_('nothing to rebase - updating instead\n'))
1442 1471 # not passing argument to get the bare update behavior
1443 1472 # with warning and trumpets
1444 1473 commands.update(ui, repo)
1445 1474 else:
1446 1475 if opts.get('tool'):
1447 1476 raise error.Abort(_('--tool can only be used with --rebase'))
1448 1477 ret = orig(ui, repo, *args, **opts)
1449 1478
1450 1479 return ret
1451 1480
1452 1481 def _setrebasesetvisibility(repo, revs):
1453 1482 """store the currently rebased set on the repo object
1454 1483
1455 1484 This is used by another function to prevent rebased revision to because
1456 1485 hidden (see issue4504)"""
1457 1486 repo = repo.unfiltered()
1458 1487 repo._rebaseset = revs
1459 1488 # invalidate cache if visibility changes
1460 1489 hiddens = repo.filteredrevcache.get('visible', set())
1461 1490 if revs & hiddens:
1462 1491 repo.invalidatevolatilesets()
1463 1492
1464 1493 def _clearrebasesetvisibiliy(repo):
1465 1494 """remove rebaseset data from the repo"""
1466 1495 repo = repo.unfiltered()
1467 1496 if '_rebaseset' in vars(repo):
1468 1497 del repo._rebaseset
1469 1498
1470 1499 def _rebasedvisible(orig, repo):
1471 1500 """ensure rebased revs stay visible (see issue4504)"""
1472 1501 blockers = orig(repo)
1473 1502 blockers.update(getattr(repo, '_rebaseset', ()))
1474 1503 return blockers
1475 1504
1476 1505 def _filterobsoleterevs(repo, revs):
1477 1506 """returns a set of the obsolete revisions in revs"""
1478 1507 return set(r for r in revs if repo[r].obsolete())
1479 1508
1480 def _computeobsoletenotrebased(repo, rebaseobsrevs, dest):
1509 def _computeobsoletenotrebased(repo, rebaseobsrevs, destmap):
1481 1510 """return a mapping obsolete => successor for all obsolete nodes to be
1482 1511 rebased that have a successors in the destination
1483 1512
1484 1513 obsolete => None entries in the mapping indicate nodes with no successor"""
1485 1514 obsoletenotrebased = {}
1486 1515
1487 1516 cl = repo.unfiltered().changelog
1488 1517 nodemap = cl.nodemap
1489 destnode = cl.node(dest)
1490 1518 for srcrev in rebaseobsrevs:
1491 1519 srcnode = cl.node(srcrev)
1520 destnode = cl.node(destmap[srcrev])
1492 1521 # XXX: more advanced APIs are required to handle split correctly
1493 1522 successors = list(obsutil.allsuccessors(repo.obsstore, [srcnode]))
1494 1523 if len(successors) == 1:
1495 1524 # obsutil.allsuccessors includes node itself. When the list only
1496 1525 # contains one element, it means there are no successors.
1497 1526 obsoletenotrebased[srcrev] = None
1498 1527 else:
1499 1528 for succnode in successors:
1500 1529 if succnode == srcnode or succnode not in nodemap:
1501 1530 continue
1502 1531 if cl.isancestor(succnode, destnode):
1503 1532 obsoletenotrebased[srcrev] = nodemap[succnode]
1504 1533 break
1505 1534
1506 1535 return obsoletenotrebased
1507 1536
1508 1537 def summaryhook(ui, repo):
1509 1538 if not repo.vfs.exists('rebasestate'):
1510 1539 return
1511 1540 try:
1512 1541 rbsrt = rebaseruntime(repo, ui, {})
1513 1542 rbsrt.restorestatus()
1514 1543 state = rbsrt.state
1515 1544 except error.RepoLookupError:
1516 1545 # i18n: column positioning for "hg summary"
1517 1546 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1518 1547 ui.write(msg)
1519 1548 return
1520 1549 numrebased = len([i for i in state.itervalues() if i >= 0])
1521 1550 # i18n: column positioning for "hg summary"
1522 1551 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1523 1552 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1524 1553 ui.label(_('%d remaining'), 'rebase.remaining') %
1525 1554 (len(state) - numrebased)))
1526 1555
1527 1556 def uisetup(ui):
1528 1557 #Replace pull with a decorator to provide --rebase option
1529 1558 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1530 1559 entry[1].append(('', 'rebase', None,
1531 1560 _("rebase working directory to branch head")))
1532 1561 entry[1].append(('t', 'tool', '',
1533 1562 _("specify merge tool for rebase")))
1534 1563 cmdutil.summaryhooks.add('rebase', summaryhook)
1535 1564 cmdutil.unfinishedstates.append(
1536 1565 ['rebasestate', False, False, _('rebase in progress'),
1537 1566 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1538 1567 cmdutil.afterresolvedstates.append(
1539 1568 ['rebasestate', _('hg rebase --continue')])
1540 1569 # ensure rebased rev are not hidden
1541 1570 extensions.wrapfunction(repoview, 'pinnedrevs', _rebasedvisible)
General Comments 0
You need to be logged in to leave comments. Login now