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