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