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