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