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