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