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