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