##// END OF EJS Templates
rebase: remove bailifchanged check from pullrebase (BC)...
Siddharth Agarwal -
r19774:7805cb80 default
parent child Browse files
Show More
@@ -1,823 +1,822 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 http://mercurial.selenic.com/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.commands import templateopts
20 20 from mercurial.node import nullrev
21 21 from mercurial.lock import release
22 22 from mercurial.i18n import _
23 23 import os, errno
24 24
25 25 nullmerge = -2
26 26 revignored = -3
27 27
28 28 cmdtable = {}
29 29 command = cmdutil.command(cmdtable)
30 30 testedwith = 'internal'
31 31
32 32 @command('rebase',
33 33 [('s', 'source', '',
34 34 _('rebase from the specified changeset'), _('REV')),
35 35 ('b', 'base', '',
36 36 _('rebase from the base of the specified changeset '
37 37 '(up to greatest common ancestor of base and dest)'),
38 38 _('REV')),
39 39 ('r', 'rev', [],
40 40 _('rebase these revisions'),
41 41 _('REV')),
42 42 ('d', 'dest', '',
43 43 _('rebase onto the specified changeset'), _('REV')),
44 44 ('', 'collapse', False, _('collapse the rebased changesets')),
45 45 ('m', 'message', '',
46 46 _('use text as collapse commit message'), _('TEXT')),
47 47 ('e', 'edit', False, _('invoke editor on commit messages')),
48 48 ('l', 'logfile', '',
49 49 _('read collapse commit message from file'), _('FILE')),
50 50 ('', 'keep', False, _('keep original changesets')),
51 51 ('', 'keepbranches', False, _('keep original branch names')),
52 52 ('D', 'detach', False, _('(DEPRECATED)')),
53 53 ('t', 'tool', '', _('specify merge tool')),
54 54 ('c', 'continue', False, _('continue an interrupted rebase')),
55 55 ('a', 'abort', False, _('abort an interrupted rebase'))] +
56 56 templateopts,
57 57 _('[-s REV | -b REV] [-d REV] [OPTION]'))
58 58 def rebase(ui, repo, **opts):
59 59 """move changeset (and descendants) to a different branch
60 60
61 61 Rebase uses repeated merging to graft changesets from one part of
62 62 history (the source) onto another (the destination). This can be
63 63 useful for linearizing *local* changes relative to a master
64 64 development tree.
65 65
66 66 You should not rebase changesets that have already been shared
67 67 with others. Doing so will force everybody else to perform the
68 68 same rebase or they will end up with duplicated changesets after
69 69 pulling in your rebased changesets.
70 70
71 71 In its default configuration, Mercurial will prevent you from
72 72 rebasing published changes. See :hg:`help phases` for details.
73 73
74 74 If you don't specify a destination changeset (``-d/--dest``),
75 75 rebase uses the current branch tip as the destination. (The
76 76 destination changeset is not modified by rebasing, but new
77 77 changesets are added as its descendants.)
78 78
79 79 You can specify which changesets to rebase in two ways: as a
80 80 "source" changeset or as a "base" changeset. Both are shorthand
81 81 for a topologically related set of changesets (the "source
82 82 branch"). If you specify source (``-s/--source``), rebase will
83 83 rebase that changeset and all of its descendants onto dest. If you
84 84 specify base (``-b/--base``), rebase will select ancestors of base
85 85 back to but not including the common ancestor with dest. Thus,
86 86 ``-b`` is less precise but more convenient than ``-s``: you can
87 87 specify any changeset in the source branch, and rebase will select
88 88 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
89 89 uses the parent of the working directory as the base.
90 90
91 91 For advanced usage, a third way is available through the ``--rev``
92 92 option. It allows you to specify an arbitrary set of changesets to
93 93 rebase. Descendants of revs you specify with this option are not
94 94 automatically included in the rebase.
95 95
96 96 By default, rebase recreates the changesets in the source branch
97 97 as descendants of dest and then destroys the originals. Use
98 98 ``--keep`` to preserve the original source changesets. Some
99 99 changesets in the source branch (e.g. merges from the destination
100 100 branch) may be dropped if they no longer contribute any change.
101 101
102 102 One result of the rules for selecting the destination changeset
103 103 and source branch is that, unlike ``merge``, rebase will do
104 104 nothing if you are at the branch tip of a named branch
105 105 with two heads. You need to explicitly specify source and/or
106 106 destination (or ``update`` to the other head, if it's the head of
107 107 the intended source branch).
108 108
109 109 If a rebase is interrupted to manually resolve a merge, it can be
110 110 continued with --continue/-c or aborted with --abort/-a.
111 111
112 112 Returns 0 on success, 1 if nothing to rebase.
113 113 """
114 114 originalwd = target = None
115 115 activebookmark = None
116 116 external = nullrev
117 117 state = {}
118 118 skipped = set()
119 119 targetancestors = set()
120 120
121 121 editor = None
122 122 if opts.get('edit'):
123 123 editor = cmdutil.commitforceeditor
124 124
125 125 lock = wlock = None
126 126 try:
127 127 wlock = repo.wlock()
128 128 lock = repo.lock()
129 129
130 130 # Validate input and define rebasing points
131 131 destf = opts.get('dest', None)
132 132 srcf = opts.get('source', None)
133 133 basef = opts.get('base', None)
134 134 revf = opts.get('rev', [])
135 135 contf = opts.get('continue')
136 136 abortf = opts.get('abort')
137 137 collapsef = opts.get('collapse', False)
138 138 collapsemsg = cmdutil.logmessage(ui, opts)
139 139 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
140 140 keepf = opts.get('keep', False)
141 141 keepbranchesf = opts.get('keepbranches', False)
142 142 # keepopen is not meant for use on the command line, but by
143 143 # other extensions
144 144 keepopen = opts.get('keepopen', False)
145 145
146 146 if collapsemsg and not collapsef:
147 147 raise util.Abort(
148 148 _('message can only be specified with collapse'))
149 149
150 150 if contf or abortf:
151 151 if contf and abortf:
152 152 raise util.Abort(_('cannot use both abort and continue'))
153 153 if collapsef:
154 154 raise util.Abort(
155 155 _('cannot use collapse with continue or abort'))
156 156 if srcf or basef or destf:
157 157 raise util.Abort(
158 158 _('abort and continue do not allow specifying revisions'))
159 159 if opts.get('tool', False):
160 160 ui.warn(_('tool option will be ignored\n'))
161 161
162 162 (originalwd, target, state, skipped, collapsef, keepf,
163 163 keepbranchesf, external, activebookmark) = restorestatus(repo)
164 164 if abortf:
165 165 return abort(repo, originalwd, target, state)
166 166 else:
167 167 if srcf and basef:
168 168 raise util.Abort(_('cannot specify both a '
169 169 'source and a base'))
170 170 if revf and basef:
171 171 raise util.Abort(_('cannot specify both a '
172 172 'revision and a base'))
173 173 if revf and srcf:
174 174 raise util.Abort(_('cannot specify both a '
175 175 'revision and a source'))
176 176
177 177 cmdutil.checkunfinished(repo)
178 178 cmdutil.bailifchanged(repo)
179 179
180 180 if not destf:
181 181 # Destination defaults to the latest revision in the
182 182 # current branch
183 183 branch = repo[None].branch()
184 184 dest = repo[branch]
185 185 else:
186 186 dest = scmutil.revsingle(repo, destf)
187 187
188 188 if revf:
189 189 rebaseset = scmutil.revrange(repo, revf)
190 190 elif srcf:
191 191 src = scmutil.revrange(repo, [srcf])
192 192 rebaseset = repo.revs('(%ld)::', src)
193 193 else:
194 194 base = scmutil.revrange(repo, [basef or '.'])
195 195 rebaseset = repo.revs(
196 196 '(children(ancestor(%ld, %d)) and ::(%ld))::',
197 197 base, dest, base)
198 198 if rebaseset:
199 199 root = min(rebaseset)
200 200 else:
201 201 root = None
202 202
203 203 if not rebaseset:
204 204 repo.ui.debug('base is ancestor of destination\n')
205 205 result = None
206 206 elif (not (keepf or obsolete._enabled)
207 207 and repo.revs('first(children(%ld) - %ld)',
208 208 rebaseset, rebaseset)):
209 209 raise util.Abort(
210 210 _("can't remove original changesets with"
211 211 " unrebased descendants"),
212 212 hint=_('use --keep to keep original changesets'))
213 213 else:
214 214 result = buildstate(repo, dest, rebaseset, collapsef)
215 215
216 216 if not result:
217 217 # Empty state built, nothing to rebase
218 218 ui.status(_('nothing to rebase\n'))
219 219 return 1
220 220 elif not keepf and not repo[root].mutable():
221 221 raise util.Abort(_("can't rebase immutable changeset %s")
222 222 % repo[root],
223 223 hint=_('see hg help phases for details'))
224 224 else:
225 225 originalwd, target, state = result
226 226 if collapsef:
227 227 targetancestors = repo.changelog.ancestors([target],
228 228 inclusive=True)
229 229 external = checkexternal(repo, state, targetancestors)
230 230
231 231 if keepbranchesf:
232 232 assert not extrafn, 'cannot use both keepbranches and extrafn'
233 233 def extrafn(ctx, extra):
234 234 extra['branch'] = ctx.branch()
235 235 if collapsef:
236 236 branches = set()
237 237 for rev in state:
238 238 branches.add(repo[rev].branch())
239 239 if len(branches) > 1:
240 240 raise util.Abort(_('cannot collapse multiple named '
241 241 'branches'))
242 242
243 243
244 244 # Rebase
245 245 if not targetancestors:
246 246 targetancestors = repo.changelog.ancestors([target], inclusive=True)
247 247
248 248 # Keep track of the current bookmarks in order to reset them later
249 249 currentbookmarks = repo._bookmarks.copy()
250 250 activebookmark = activebookmark or repo._bookmarkcurrent
251 251 if activebookmark:
252 252 bookmarks.unsetcurrent(repo)
253 253
254 254 sortedstate = sorted(state)
255 255 total = len(sortedstate)
256 256 pos = 0
257 257 for rev in sortedstate:
258 258 pos += 1
259 259 if state[rev] == -1:
260 260 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
261 261 _('changesets'), total)
262 262 p1, p2 = defineparents(repo, rev, target, state,
263 263 targetancestors)
264 264 storestatus(repo, originalwd, target, state, collapsef, keepf,
265 265 keepbranchesf, external, activebookmark)
266 266 if len(repo.parents()) == 2:
267 267 repo.ui.debug('resuming interrupted rebase\n')
268 268 else:
269 269 try:
270 270 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
271 271 stats = rebasenode(repo, rev, p1, state, collapsef)
272 272 if stats and stats[3] > 0:
273 273 raise error.InterventionRequired(
274 274 _('unresolved conflicts (see hg '
275 275 'resolve, then hg rebase --continue)'))
276 276 finally:
277 277 ui.setconfig('ui', 'forcemerge', '')
278 278 cmdutil.duplicatecopies(repo, rev, target)
279 279 if not collapsef:
280 280 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
281 281 editor=editor)
282 282 else:
283 283 # Skip commit if we are collapsing
284 284 repo.setparents(repo[p1].node())
285 285 newrev = None
286 286 # Update the state
287 287 if newrev is not None:
288 288 state[rev] = repo[newrev].rev()
289 289 else:
290 290 if not collapsef:
291 291 ui.note(_('no changes, revision %d skipped\n') % rev)
292 292 ui.debug('next revision set to %s\n' % p1)
293 293 skipped.add(rev)
294 294 state[rev] = p1
295 295
296 296 ui.progress(_('rebasing'), None)
297 297 ui.note(_('rebase merging completed\n'))
298 298
299 299 if collapsef and not keepopen:
300 300 p1, p2 = defineparents(repo, min(state), target,
301 301 state, targetancestors)
302 302 if collapsemsg:
303 303 commitmsg = collapsemsg
304 304 else:
305 305 commitmsg = 'Collapsed revision'
306 306 for rebased in state:
307 307 if rebased not in skipped and state[rebased] > nullmerge:
308 308 commitmsg += '\n* %s' % repo[rebased].description()
309 309 commitmsg = ui.edit(commitmsg, repo.ui.username())
310 310 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
311 311 extrafn=extrafn, editor=editor)
312 312
313 313 if 'qtip' in repo.tags():
314 314 updatemq(repo, state, skipped, **opts)
315 315
316 316 if currentbookmarks:
317 317 # Nodeids are needed to reset bookmarks
318 318 nstate = {}
319 319 for k, v in state.iteritems():
320 320 if v > nullmerge:
321 321 nstate[repo[k].node()] = repo[v].node()
322 322 # XXX this is the same as dest.node() for the non-continue path --
323 323 # this should probably be cleaned up
324 324 targetnode = repo[target].node()
325 325
326 326 if not keepf:
327 327 collapsedas = None
328 328 if collapsef:
329 329 collapsedas = newrev
330 330 clearrebased(ui, repo, state, skipped, collapsedas)
331 331
332 332 if currentbookmarks:
333 333 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
334 334
335 335 clearstatus(repo)
336 336 ui.note(_("rebase completed\n"))
337 337 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
338 338 if skipped:
339 339 ui.note(_("%d revisions have been skipped\n") % len(skipped))
340 340
341 341 if (activebookmark and
342 342 repo['tip'].node() == repo._bookmarks[activebookmark]):
343 343 bookmarks.setcurrent(repo, activebookmark)
344 344
345 345 finally:
346 346 release(lock, wlock)
347 347
348 348 def checkexternal(repo, state, targetancestors):
349 349 """Check whether one or more external revisions need to be taken in
350 350 consideration. In the latter case, abort.
351 351 """
352 352 external = nullrev
353 353 source = min(state)
354 354 for rev in state:
355 355 if rev == source:
356 356 continue
357 357 # Check externals and fail if there are more than one
358 358 for p in repo[rev].parents():
359 359 if (p.rev() not in state
360 360 and p.rev() not in targetancestors):
361 361 if external != nullrev:
362 362 raise util.Abort(_('unable to collapse, there is more '
363 363 'than one external parent'))
364 364 external = p.rev()
365 365 return external
366 366
367 367 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
368 368 'Commit the changes and store useful information in extra'
369 369 try:
370 370 repo.setparents(repo[p1].node(), repo[p2].node())
371 371 ctx = repo[rev]
372 372 if commitmsg is None:
373 373 commitmsg = ctx.description()
374 374 extra = {'rebase_source': ctx.hex()}
375 375 if extrafn:
376 376 extrafn(ctx, extra)
377 377 # Commit might fail if unresolved files exist
378 378 newrev = repo.commit(text=commitmsg, user=ctx.user(),
379 379 date=ctx.date(), extra=extra, editor=editor)
380 380 repo.dirstate.setbranch(repo[newrev].branch())
381 381 targetphase = max(ctx.phase(), phases.draft)
382 382 # retractboundary doesn't overwrite upper phase inherited from parent
383 383 newnode = repo[newrev].node()
384 384 if newnode:
385 385 phases.retractboundary(repo, targetphase, [newnode])
386 386 return newrev
387 387 except util.Abort:
388 388 # Invalidate the previous setparents
389 389 repo.dirstate.invalidate()
390 390 raise
391 391
392 392 def rebasenode(repo, rev, p1, state, collapse):
393 393 'Rebase a single revision'
394 394 # Merge phase
395 395 # Update to target and merge it with local
396 396 if repo['.'].rev() != repo[p1].rev():
397 397 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
398 398 merge.update(repo, p1, False, True, False)
399 399 else:
400 400 repo.ui.debug(" already in target\n")
401 401 repo.dirstate.write()
402 402 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
403 403 base = None
404 404 if repo[rev].rev() != repo[min(state)].rev():
405 405 base = repo[rev].p1().node()
406 406 # When collapsing in-place, the parent is the common ancestor, we
407 407 # have to allow merging with it.
408 408 return merge.update(repo, rev, True, True, False, base, collapse)
409 409
410 410 def nearestrebased(repo, rev, state):
411 411 """return the nearest ancestors of rev in the rebase result"""
412 412 rebased = [r for r in state if state[r] > nullmerge]
413 413 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
414 414 if candidates:
415 415 return state[candidates[0]]
416 416 else:
417 417 return None
418 418
419 419 def defineparents(repo, rev, target, state, targetancestors):
420 420 'Return the new parent relationship of the revision that will be rebased'
421 421 parents = repo[rev].parents()
422 422 p1 = p2 = nullrev
423 423
424 424 P1n = parents[0].rev()
425 425 if P1n in targetancestors:
426 426 p1 = target
427 427 elif P1n in state:
428 428 if state[P1n] == nullmerge:
429 429 p1 = target
430 430 elif state[P1n] == revignored:
431 431 p1 = nearestrebased(repo, P1n, state)
432 432 if p1 is None:
433 433 p1 = target
434 434 else:
435 435 p1 = state[P1n]
436 436 else: # P1n external
437 437 p1 = target
438 438 p2 = P1n
439 439
440 440 if len(parents) == 2 and parents[1].rev() not in targetancestors:
441 441 P2n = parents[1].rev()
442 442 # interesting second parent
443 443 if P2n in state:
444 444 if p1 == target: # P1n in targetancestors or external
445 445 p1 = state[P2n]
446 446 elif state[P2n] == revignored:
447 447 p2 = nearestrebased(repo, P2n, state)
448 448 if p2 is None:
449 449 # no ancestors rebased yet, detach
450 450 p2 = target
451 451 else:
452 452 p2 = state[P2n]
453 453 else: # P2n external
454 454 if p2 != nullrev: # P1n external too => rev is a merged revision
455 455 raise util.Abort(_('cannot use revision %d as base, result '
456 456 'would have 3 parents') % rev)
457 457 p2 = P2n
458 458 repo.ui.debug(" future parents are %d and %d\n" %
459 459 (repo[p1].rev(), repo[p2].rev()))
460 460 return p1, p2
461 461
462 462 def isagitpatch(repo, patchname):
463 463 'Return true if the given patch is in git format'
464 464 mqpatch = os.path.join(repo.mq.path, patchname)
465 465 for line in patch.linereader(file(mqpatch, 'rb')):
466 466 if line.startswith('diff --git'):
467 467 return True
468 468 return False
469 469
470 470 def updatemq(repo, state, skipped, **opts):
471 471 'Update rebased mq patches - finalize and then import them'
472 472 mqrebase = {}
473 473 mq = repo.mq
474 474 original_series = mq.fullseries[:]
475 475 skippedpatches = set()
476 476
477 477 for p in mq.applied:
478 478 rev = repo[p.node].rev()
479 479 if rev in state:
480 480 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
481 481 (rev, p.name))
482 482 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
483 483 else:
484 484 # Applied but not rebased, not sure this should happen
485 485 skippedpatches.add(p.name)
486 486
487 487 if mqrebase:
488 488 mq.finish(repo, mqrebase.keys())
489 489
490 490 # We must start import from the newest revision
491 491 for rev in sorted(mqrebase, reverse=True):
492 492 if rev not in skipped:
493 493 name, isgit = mqrebase[rev]
494 494 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
495 495 mq.qimport(repo, (), patchname=name, git=isgit,
496 496 rev=[str(state[rev])])
497 497 else:
498 498 # Rebased and skipped
499 499 skippedpatches.add(mqrebase[rev][0])
500 500
501 501 # Patches were either applied and rebased and imported in
502 502 # order, applied and removed or unapplied. Discard the removed
503 503 # ones while preserving the original series order and guards.
504 504 newseries = [s for s in original_series
505 505 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
506 506 mq.fullseries[:] = newseries
507 507 mq.seriesdirty = True
508 508 mq.savedirty()
509 509
510 510 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
511 511 'Move bookmarks to their correct changesets, and delete divergent ones'
512 512 marks = repo._bookmarks
513 513 for k, v in originalbookmarks.iteritems():
514 514 if v in nstate:
515 515 # update the bookmarks for revs that have moved
516 516 marks[k] = nstate[v]
517 517 bookmarks.deletedivergent(repo, [targetnode], k)
518 518
519 519 marks.write()
520 520
521 521 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
522 522 external, activebookmark):
523 523 'Store the current status to allow recovery'
524 524 f = repo.opener("rebasestate", "w")
525 525 f.write(repo[originalwd].hex() + '\n')
526 526 f.write(repo[target].hex() + '\n')
527 527 f.write(repo[external].hex() + '\n')
528 528 f.write('%d\n' % int(collapse))
529 529 f.write('%d\n' % int(keep))
530 530 f.write('%d\n' % int(keepbranches))
531 531 f.write('%s\n' % (activebookmark or ''))
532 532 for d, v in state.iteritems():
533 533 oldrev = repo[d].hex()
534 534 if v > nullmerge:
535 535 newrev = repo[v].hex()
536 536 else:
537 537 newrev = v
538 538 f.write("%s:%s\n" % (oldrev, newrev))
539 539 f.close()
540 540 repo.ui.debug('rebase status stored\n')
541 541
542 542 def clearstatus(repo):
543 543 'Remove the status files'
544 544 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
545 545
546 546 def restorestatus(repo):
547 547 'Restore a previously stored status'
548 548 try:
549 549 target = None
550 550 collapse = False
551 551 external = nullrev
552 552 activebookmark = None
553 553 state = {}
554 554 f = repo.opener("rebasestate")
555 555 for i, l in enumerate(f.read().splitlines()):
556 556 if i == 0:
557 557 originalwd = repo[l].rev()
558 558 elif i == 1:
559 559 target = repo[l].rev()
560 560 elif i == 2:
561 561 external = repo[l].rev()
562 562 elif i == 3:
563 563 collapse = bool(int(l))
564 564 elif i == 4:
565 565 keep = bool(int(l))
566 566 elif i == 5:
567 567 keepbranches = bool(int(l))
568 568 elif i == 6 and not (len(l) == 81 and ':' in l):
569 569 # line 6 is a recent addition, so for backwards compatibility
570 570 # check that the line doesn't look like the oldrev:newrev lines
571 571 activebookmark = l
572 572 else:
573 573 oldrev, newrev = l.split(':')
574 574 if newrev in (str(nullmerge), str(revignored)):
575 575 state[repo[oldrev].rev()] = int(newrev)
576 576 else:
577 577 state[repo[oldrev].rev()] = repo[newrev].rev()
578 578 skipped = set()
579 579 # recompute the set of skipped revs
580 580 if not collapse:
581 581 seen = set([target])
582 582 for old, new in sorted(state.items()):
583 583 if new != nullrev and new in seen:
584 584 skipped.add(old)
585 585 seen.add(new)
586 586 repo.ui.debug('computed skipped revs: %s\n' % skipped)
587 587 repo.ui.debug('rebase status resumed\n')
588 588 return (originalwd, target, state, skipped,
589 589 collapse, keep, keepbranches, external, activebookmark)
590 590 except IOError, err:
591 591 if err.errno != errno.ENOENT:
592 592 raise
593 593 raise util.Abort(_('no rebase in progress'))
594 594
595 595 def inrebase(repo, originalwd, state):
596 596 '''check whether the workdir is in an interrupted rebase'''
597 597 parents = [p.rev() for p in repo.parents()]
598 598 if originalwd in parents:
599 599 return True
600 600
601 601 for newrev in state.itervalues():
602 602 if newrev in parents:
603 603 return True
604 604
605 605 return False
606 606
607 607 def abort(repo, originalwd, target, state):
608 608 'Restore the repository to its original state'
609 609 dstates = [s for s in state.values() if s != nullrev]
610 610 immutable = [d for d in dstates if not repo[d].mutable()]
611 611 cleanup = True
612 612 if immutable:
613 613 repo.ui.warn(_("warning: can't clean up immutable changesets %s\n")
614 614 % ', '.join(str(repo[r]) for r in immutable),
615 615 hint=_('see hg help phases for details'))
616 616 cleanup = False
617 617
618 618 descendants = set()
619 619 if dstates:
620 620 descendants = set(repo.changelog.descendants(dstates))
621 621 if descendants - set(dstates):
622 622 repo.ui.warn(_("warning: new changesets detected on target branch, "
623 623 "can't strip\n"))
624 624 cleanup = False
625 625
626 626 if cleanup:
627 627 # Update away from the rebase if necessary
628 628 if inrebase(repo, originalwd, state):
629 629 merge.update(repo, repo[originalwd].rev(), False, True, False)
630 630
631 631 # Strip from the first rebased revision
632 632 rebased = filter(lambda x: x > -1 and x != target, state.values())
633 633 if rebased:
634 634 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
635 635 # no backup of rebased cset versions needed
636 636 repair.strip(repo.ui, repo, strippoints)
637 637
638 638 clearstatus(repo)
639 639 repo.ui.warn(_('rebase aborted\n'))
640 640 return 0
641 641
642 642 def buildstate(repo, dest, rebaseset, collapse):
643 643 '''Define which revisions are going to be rebased and where
644 644
645 645 repo: repo
646 646 dest: context
647 647 rebaseset: set of rev
648 648 '''
649 649
650 650 # This check isn't strictly necessary, since mq detects commits over an
651 651 # applied patch. But it prevents messing up the working directory when
652 652 # a partially completed rebase is blocked by mq.
653 653 if 'qtip' in repo.tags() and (dest.node() in
654 654 [s.node for s in repo.mq.applied]):
655 655 raise util.Abort(_('cannot rebase onto an applied mq patch'))
656 656
657 657 roots = list(repo.set('roots(%ld)', rebaseset))
658 658 if not roots:
659 659 raise util.Abort(_('no matching revisions'))
660 660 roots.sort()
661 661 state = {}
662 662 detachset = set()
663 663 for root in roots:
664 664 commonbase = root.ancestor(dest)
665 665 if commonbase == root:
666 666 raise util.Abort(_('source is ancestor of destination'))
667 667 if commonbase == dest:
668 668 samebranch = root.branch() == dest.branch()
669 669 if not collapse and samebranch and root in dest.children():
670 670 repo.ui.debug('source is a child of destination\n')
671 671 return None
672 672
673 673 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
674 674 state.update(dict.fromkeys(rebaseset, nullrev))
675 675 # Rebase tries to turn <dest> into a parent of <root> while
676 676 # preserving the number of parents of rebased changesets:
677 677 #
678 678 # - A changeset with a single parent will always be rebased as a
679 679 # changeset with a single parent.
680 680 #
681 681 # - A merge will be rebased as merge unless its parents are both
682 682 # ancestors of <dest> or are themselves in the rebased set and
683 683 # pruned while rebased.
684 684 #
685 685 # If one parent of <root> is an ancestor of <dest>, the rebased
686 686 # version of this parent will be <dest>. This is always true with
687 687 # --base option.
688 688 #
689 689 # Otherwise, we need to *replace* the original parents with
690 690 # <dest>. This "detaches" the rebased set from its former location
691 691 # and rebases it onto <dest>. Changes introduced by ancestors of
692 692 # <root> not common with <dest> (the detachset, marked as
693 693 # nullmerge) are "removed" from the rebased changesets.
694 694 #
695 695 # - If <root> has a single parent, set it to <dest>.
696 696 #
697 697 # - If <root> is a merge, we cannot decide which parent to
698 698 # replace, the rebase operation is not clearly defined.
699 699 #
700 700 # The table below sums up this behavior:
701 701 #
702 702 # +------------------+----------------------+-------------------------+
703 703 # | | one parent | merge |
704 704 # +------------------+----------------------+-------------------------+
705 705 # | parent in | new parent is <dest> | parents in ::<dest> are |
706 706 # | ::<dest> | | remapped to <dest> |
707 707 # +------------------+----------------------+-------------------------+
708 708 # | unrelated source | new parent is <dest> | ambiguous, abort |
709 709 # +------------------+----------------------+-------------------------+
710 710 #
711 711 # The actual abort is handled by `defineparents`
712 712 if len(root.parents()) <= 1:
713 713 # ancestors of <root> not ancestors of <dest>
714 714 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
715 715 [root.rev()]))
716 716 for r in detachset:
717 717 if r not in state:
718 718 state[r] = nullmerge
719 719 if len(roots) > 1:
720 720 # If we have multiple roots, we may have "hole" in the rebase set.
721 721 # Rebase roots that descend from those "hole" should not be detached as
722 722 # other root are. We use the special `revignored` to inform rebase that
723 723 # the revision should be ignored but that `defineparents` should search
724 724 # a rebase destination that make sense regarding rebased topology.
725 725 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
726 726 for ignored in set(rebasedomain) - set(rebaseset):
727 727 state[ignored] = revignored
728 728 return repo['.'].rev(), dest.rev(), state
729 729
730 730 def clearrebased(ui, repo, state, skipped, collapsedas=None):
731 731 """dispose of rebased revision at the end of the rebase
732 732
733 733 If `collapsedas` is not None, the rebase was a collapse whose result if the
734 734 `collapsedas` node."""
735 735 if obsolete._enabled:
736 736 markers = []
737 737 for rev, newrev in sorted(state.items()):
738 738 if newrev >= 0:
739 739 if rev in skipped:
740 740 succs = ()
741 741 elif collapsedas is not None:
742 742 succs = (repo[collapsedas],)
743 743 else:
744 744 succs = (repo[newrev],)
745 745 markers.append((repo[rev], succs))
746 746 if markers:
747 747 obsolete.createmarkers(repo, markers)
748 748 else:
749 749 rebased = [rev for rev in state if state[rev] > nullmerge]
750 750 if rebased:
751 751 stripped = []
752 752 for root in repo.set('roots(%ld)', rebased):
753 753 if set(repo.changelog.descendants([root.rev()])) - set(state):
754 754 ui.warn(_("warning: new changesets detected "
755 755 "on source branch, not stripping\n"))
756 756 else:
757 757 stripped.append(root.node())
758 758 if stripped:
759 759 # backup the old csets by default
760 760 repair.strip(ui, repo, stripped, "all")
761 761
762 762
763 763 def pullrebase(orig, ui, repo, *args, **opts):
764 764 'Call rebase after pull if the latter has been invoked with --rebase'
765 765 if opts.get('rebase'):
766 766 if opts.get('update'):
767 767 del opts['update']
768 768 ui.debug('--update and --rebase are not compatible, ignoring '
769 769 'the update flag\n')
770 770
771 771 movemarkfrom = repo['.'].node()
772 cmdutil.bailifchanged(repo)
773 772 revsprepull = len(repo)
774 773 origpostincoming = commands.postincoming
775 774 def _dummy(*args, **kwargs):
776 775 pass
777 776 commands.postincoming = _dummy
778 777 try:
779 778 orig(ui, repo, *args, **opts)
780 779 finally:
781 780 commands.postincoming = origpostincoming
782 781 revspostpull = len(repo)
783 782 if revspostpull > revsprepull:
784 783 # --rev option from pull conflict with rebase own --rev
785 784 # dropping it
786 785 if 'rev' in opts:
787 786 del opts['rev']
788 787 rebase(ui, repo, **opts)
789 788 branch = repo[None].branch()
790 789 dest = repo[branch].rev()
791 790 if dest != repo['.'].rev():
792 791 # there was nothing to rebase we force an update
793 792 hg.update(repo, dest)
794 793 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
795 794 ui.status(_("updating bookmark %s\n")
796 795 % repo._bookmarkcurrent)
797 796 else:
798 797 if opts.get('tool'):
799 798 raise util.Abort(_('--tool can only be used with --rebase'))
800 799 orig(ui, repo, *args, **opts)
801 800
802 801 def summaryhook(ui, repo):
803 802 if not os.path.exists(repo.join('rebasestate')):
804 803 return
805 804 state = restorestatus(repo)[2]
806 805 numrebased = len([i for i in state.itervalues() if i != -1])
807 806 # i18n: column positioning for "hg summary"
808 807 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
809 808 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
810 809 ui.label(_('%d remaining'), 'rebase.remaining') %
811 810 (len(state) - numrebased)))
812 811
813 812 def uisetup(ui):
814 813 'Replace pull with a decorator to provide --rebase option'
815 814 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
816 815 entry[1].append(('', 'rebase', None,
817 816 _("rebase working directory to branch head")))
818 817 entry[1].append(('t', 'tool', '',
819 818 _("specify merge tool for rebase")))
820 819 cmdutil.summaryhooks.add('rebase', summaryhook)
821 820 cmdutil.unfinishedstates.append(
822 821 ['rebasestate', False, False, _('rebase in progress'),
823 822 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now