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