##// END OF EJS Templates
rebase: block collapse with keepbranches on multiple named branches (issue2112)...
Stefano Tortarolo -
r14897:b9daa5b7 stable
parent child Browse files
Show More
@@ -1,610 +1,618 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, copies, patch
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
27 27 cmdtable = {}
28 28 command = cmdutil.command(cmdtable)
29 29
30 30 @command('rebase',
31 31 [('s', 'source', '',
32 32 _('rebase from the specified changeset'), _('REV')),
33 33 ('b', 'base', '',
34 34 _('rebase from the base of the specified changeset '
35 35 '(up to greatest common ancestor of base and dest)'),
36 36 _('REV')),
37 37 ('d', 'dest', '',
38 38 _('rebase onto the specified changeset'), _('REV')),
39 39 ('', 'collapse', False, _('collapse the rebased changesets')),
40 40 ('m', 'message', '',
41 41 _('use text as collapse commit message'), _('TEXT')),
42 42 ('l', 'logfile', '',
43 43 _('read collapse commit message from file'), _('FILE')),
44 44 ('', 'keep', False, _('keep original changesets')),
45 45 ('', 'keepbranches', False, _('keep original branch names')),
46 46 ('', 'detach', False, _('force detaching of source from its original '
47 47 'branch')),
48 48 ('t', 'tool', '', _('specify merge tool')),
49 49 ('c', 'continue', False, _('continue an interrupted rebase')),
50 50 ('a', 'abort', False, _('abort an interrupted rebase'))] +
51 51 templateopts,
52 52 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
53 53 'hg rebase {-a|-c}'))
54 54 def rebase(ui, repo, **opts):
55 55 """move changeset (and descendants) to a different branch
56 56
57 57 Rebase uses repeated merging to graft changesets from one part of
58 58 history (the source) onto another (the destination). This can be
59 59 useful for linearizing *local* changes relative to a master
60 60 development tree.
61 61
62 62 You should not rebase changesets that have already been shared
63 63 with others. Doing so will force everybody else to perform the
64 64 same rebase or they will end up with duplicated changesets after
65 65 pulling in your rebased changesets.
66 66
67 67 If you don't specify a destination changeset (``-d/--dest``),
68 68 rebase uses the tipmost head of the current named branch as the
69 69 destination. (The destination changeset is not modified by
70 70 rebasing, but new changesets are added as its descendants.)
71 71
72 72 You can specify which changesets to rebase in two ways: as a
73 73 "source" changeset or as a "base" changeset. Both are shorthand
74 74 for a topologically related set of changesets (the "source
75 75 branch"). If you specify source (``-s/--source``), rebase will
76 76 rebase that changeset and all of its descendants onto dest. If you
77 77 specify base (``-b/--base``), rebase will select ancestors of base
78 78 back to but not including the common ancestor with dest. Thus,
79 79 ``-b`` is less precise but more convenient than ``-s``: you can
80 80 specify any changeset in the source branch, and rebase will select
81 81 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
82 82 uses the parent of the working directory as the base.
83 83
84 84 By default, rebase recreates the changesets in the source branch
85 85 as descendants of dest and then destroys the originals. Use
86 86 ``--keep`` to preserve the original source changesets. Some
87 87 changesets in the source branch (e.g. merges from the destination
88 88 branch) may be dropped if they no longer contribute any change.
89 89
90 90 One result of the rules for selecting the destination changeset
91 91 and source branch is that, unlike ``merge``, rebase will do
92 92 nothing if you are at the latest (tipmost) head of a named branch
93 93 with two heads. You need to explicitly specify source and/or
94 94 destination (or ``update`` to the other head, if it's the head of
95 95 the intended source branch).
96 96
97 97 If a rebase is interrupted to manually resolve a merge, it can be
98 98 continued with --continue/-c or aborted with --abort/-a.
99 99
100 100 Returns 0 on success, 1 if nothing to rebase.
101 101 """
102 102 originalwd = target = None
103 103 external = nullrev
104 104 state = {}
105 105 skipped = set()
106 106 targetancestors = set()
107 107
108 108 lock = wlock = None
109 109 try:
110 110 lock = repo.lock()
111 111 wlock = repo.wlock()
112 112
113 113 # Validate input and define rebasing points
114 114 destf = opts.get('dest', None)
115 115 srcf = opts.get('source', None)
116 116 basef = opts.get('base', None)
117 117 contf = opts.get('continue')
118 118 abortf = opts.get('abort')
119 119 collapsef = opts.get('collapse', False)
120 120 collapsemsg = cmdutil.logmessage(ui, opts)
121 121 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
122 122 keepf = opts.get('keep', False)
123 123 keepbranchesf = opts.get('keepbranches', False)
124 124 detachf = opts.get('detach', False)
125 125 # keepopen is not meant for use on the command line, but by
126 126 # other extensions
127 127 keepopen = opts.get('keepopen', False)
128 128
129 129 if collapsemsg and not collapsef:
130 130 raise util.Abort(
131 131 _('message can only be specified with collapse'))
132 132
133 133 if contf or abortf:
134 134 if contf and abortf:
135 135 raise util.Abort(_('cannot use both abort and continue'))
136 136 if collapsef:
137 137 raise util.Abort(
138 138 _('cannot use collapse with continue or abort'))
139 139 if detachf:
140 140 raise util.Abort(_('cannot use detach with continue or abort'))
141 141 if srcf or basef or destf:
142 142 raise util.Abort(
143 143 _('abort and continue do not allow specifying revisions'))
144 144 if opts.get('tool', False):
145 145 ui.warn(_('tool option will be ignored\n'))
146 146
147 147 (originalwd, target, state, skipped, collapsef, keepf,
148 148 keepbranchesf, external) = restorestatus(repo)
149 149 if abortf:
150 150 return abort(repo, originalwd, target, state)
151 151 else:
152 152 if srcf and basef:
153 153 raise util.Abort(_('cannot specify both a '
154 154 'revision and a base'))
155 155 if detachf:
156 156 if not srcf:
157 157 raise util.Abort(
158 158 _('detach requires a revision to be specified'))
159 159 if basef:
160 160 raise util.Abort(_('cannot specify a base with detach'))
161 161
162 162 cmdutil.bailifchanged(repo)
163 163 result = buildstate(repo, destf, srcf, basef, detachf)
164 164 if not result:
165 165 # Empty state built, nothing to rebase
166 166 ui.status(_('nothing to rebase\n'))
167 167 return 1
168 168 else:
169 169 originalwd, target, state = result
170 170 if collapsef:
171 171 targetancestors = set(repo.changelog.ancestors(target))
172 172 external = checkexternal(repo, state, targetancestors)
173 173
174 174 if keepbranchesf:
175 175 assert not extrafn, 'cannot use both keepbranches and extrafn'
176 176 def extrafn(ctx, extra):
177 177 extra['branch'] = ctx.branch()
178 if collapsef:
179 branches = set()
180 for rev in state:
181 branches.add(repo[rev].branch())
182 if len(branches) > 1:
183 raise util.Abort(_('cannot collapse multiple named '
184 'branches'))
185
178 186
179 187 # Rebase
180 188 if not targetancestors:
181 189 targetancestors = set(repo.changelog.ancestors(target))
182 190 targetancestors.add(target)
183 191
184 192 # Keep track of the current bookmarks in order to reset them later
185 193 currentbookmarks = repo._bookmarks.copy()
186 194
187 195 sortedstate = sorted(state)
188 196 total = len(sortedstate)
189 197 pos = 0
190 198 for rev in sortedstate:
191 199 pos += 1
192 200 if state[rev] == -1:
193 201 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
194 202 _('changesets'), total)
195 203 storestatus(repo, originalwd, target, state, collapsef, keepf,
196 204 keepbranchesf, external)
197 205 p1, p2 = defineparents(repo, rev, target, state,
198 206 targetancestors)
199 207 if len(repo.parents()) == 2:
200 208 repo.ui.debug('resuming interrupted rebase\n')
201 209 else:
202 210 try:
203 211 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
204 212 stats = rebasenode(repo, rev, p1, state)
205 213 if stats and stats[3] > 0:
206 214 raise util.Abort(_('unresolved conflicts (see hg '
207 215 'resolve, then hg rebase --continue)'))
208 216 finally:
209 217 ui.setconfig('ui', 'forcemerge', '')
210 218 updatedirstate(repo, rev, target, p2)
211 219 if not collapsef:
212 220 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn)
213 221 else:
214 222 # Skip commit if we are collapsing
215 223 repo.dirstate.setparents(repo[p1].node())
216 224 newrev = None
217 225 # Update the state
218 226 if newrev is not None:
219 227 state[rev] = repo[newrev].rev()
220 228 else:
221 229 if not collapsef:
222 230 ui.note(_('no changes, revision %d skipped\n') % rev)
223 231 ui.debug('next revision set to %s\n' % p1)
224 232 skipped.add(rev)
225 233 state[rev] = p1
226 234
227 235 ui.progress(_('rebasing'), None)
228 236 ui.note(_('rebase merging completed\n'))
229 237
230 238 if collapsef and not keepopen:
231 239 p1, p2 = defineparents(repo, min(state), target,
232 240 state, targetancestors)
233 241 if collapsemsg:
234 242 commitmsg = collapsemsg
235 243 else:
236 244 commitmsg = 'Collapsed revision'
237 245 for rebased in state:
238 246 if rebased not in skipped and state[rebased] != nullmerge:
239 247 commitmsg += '\n* %s' % repo[rebased].description()
240 248 commitmsg = ui.edit(commitmsg, repo.ui.username())
241 249 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
242 250 extrafn=extrafn)
243 251
244 252 if 'qtip' in repo.tags():
245 253 updatemq(repo, state, skipped, **opts)
246 254
247 255 if currentbookmarks:
248 256 # Nodeids are needed to reset bookmarks
249 257 nstate = {}
250 258 for k, v in state.iteritems():
251 259 if v != nullmerge:
252 260 nstate[repo[k].node()] = repo[v].node()
253 261
254 262 if not keepf:
255 263 # Remove no more useful revisions
256 264 rebased = [rev for rev in state if state[rev] != nullmerge]
257 265 if rebased:
258 266 if set(repo.changelog.descendants(min(rebased))) - set(state):
259 267 ui.warn(_("warning: new changesets detected "
260 268 "on source branch, not stripping\n"))
261 269 else:
262 270 # backup the old csets by default
263 271 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
264 272
265 273 if currentbookmarks:
266 274 updatebookmarks(repo, nstate, currentbookmarks, **opts)
267 275
268 276 clearstatus(repo)
269 277 ui.note(_("rebase completed\n"))
270 278 if os.path.exists(repo.sjoin('undo')):
271 279 util.unlinkpath(repo.sjoin('undo'))
272 280 if skipped:
273 281 ui.note(_("%d revisions have been skipped\n") % len(skipped))
274 282 finally:
275 283 release(lock, wlock)
276 284
277 285 def checkexternal(repo, state, targetancestors):
278 286 """Check whether one or more external revisions need to be taken in
279 287 consideration. In the latter case, abort.
280 288 """
281 289 external = nullrev
282 290 source = min(state)
283 291 for rev in state:
284 292 if rev == source:
285 293 continue
286 294 # Check externals and fail if there are more than one
287 295 for p in repo[rev].parents():
288 296 if (p.rev() not in state
289 297 and p.rev() not in targetancestors):
290 298 if external != nullrev:
291 299 raise util.Abort(_('unable to collapse, there is more '
292 300 'than one external parent'))
293 301 external = p.rev()
294 302 return external
295 303
296 304 def updatedirstate(repo, rev, p1, p2):
297 305 """Keep track of renamed files in the revision that is going to be rebased
298 306 """
299 307 # Here we simulate the copies and renames in the source changeset
300 308 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
301 309 m1 = repo[rev].manifest()
302 310 m2 = repo[p1].manifest()
303 311 for k, v in cop.iteritems():
304 312 if k in m1:
305 313 if v in m1 or v in m2:
306 314 repo.dirstate.copy(v, k)
307 315 if v in m2 and v not in m1 and k in m2:
308 316 repo.dirstate.remove(v)
309 317
310 318 def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None):
311 319 'Commit the changes and store useful information in extra'
312 320 try:
313 321 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
314 322 ctx = repo[rev]
315 323 if commitmsg is None:
316 324 commitmsg = ctx.description()
317 325 extra = {'rebase_source': ctx.hex()}
318 326 if extrafn:
319 327 extrafn(ctx, extra)
320 328 # Commit might fail if unresolved files exist
321 329 newrev = repo.commit(text=commitmsg, user=ctx.user(),
322 330 date=ctx.date(), extra=extra)
323 331 repo.dirstate.setbranch(repo[newrev].branch())
324 332 return newrev
325 333 except util.Abort:
326 334 # Invalidate the previous setparents
327 335 repo.dirstate.invalidate()
328 336 raise
329 337
330 338 def rebasenode(repo, rev, p1, state):
331 339 'Rebase a single revision'
332 340 # Merge phase
333 341 # Update to target and merge it with local
334 342 if repo['.'].rev() != repo[p1].rev():
335 343 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
336 344 merge.update(repo, p1, False, True, False)
337 345 else:
338 346 repo.ui.debug(" already in target\n")
339 347 repo.dirstate.write()
340 348 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
341 349 base = None
342 350 if repo[rev].rev() != repo[min(state)].rev():
343 351 base = repo[rev].p1().node()
344 352 return merge.update(repo, rev, True, True, False, base)
345 353
346 354 def defineparents(repo, rev, target, state, targetancestors):
347 355 'Return the new parent relationship of the revision that will be rebased'
348 356 parents = repo[rev].parents()
349 357 p1 = p2 = nullrev
350 358
351 359 P1n = parents[0].rev()
352 360 if P1n in targetancestors:
353 361 p1 = target
354 362 elif P1n in state:
355 363 if state[P1n] == nullmerge:
356 364 p1 = target
357 365 else:
358 366 p1 = state[P1n]
359 367 else: # P1n external
360 368 p1 = target
361 369 p2 = P1n
362 370
363 371 if len(parents) == 2 and parents[1].rev() not in targetancestors:
364 372 P2n = parents[1].rev()
365 373 # interesting second parent
366 374 if P2n in state:
367 375 if p1 == target: # P1n in targetancestors or external
368 376 p1 = state[P2n]
369 377 else:
370 378 p2 = state[P2n]
371 379 else: # P2n external
372 380 if p2 != nullrev: # P1n external too => rev is a merged revision
373 381 raise util.Abort(_('cannot use revision %d as base, result '
374 382 'would have 3 parents') % rev)
375 383 p2 = P2n
376 384 repo.ui.debug(" future parents are %d and %d\n" %
377 385 (repo[p1].rev(), repo[p2].rev()))
378 386 return p1, p2
379 387
380 388 def isagitpatch(repo, patchname):
381 389 'Return true if the given patch is in git format'
382 390 mqpatch = os.path.join(repo.mq.path, patchname)
383 391 for line in patch.linereader(file(mqpatch, 'rb')):
384 392 if line.startswith('diff --git'):
385 393 return True
386 394 return False
387 395
388 396 def updatemq(repo, state, skipped, **opts):
389 397 'Update rebased mq patches - finalize and then import them'
390 398 mqrebase = {}
391 399 mq = repo.mq
392 400 original_series = mq.fullseries[:]
393 401
394 402 for p in mq.applied:
395 403 rev = repo[p.node].rev()
396 404 if rev in state:
397 405 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
398 406 (rev, p.name))
399 407 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
400 408
401 409 if mqrebase:
402 410 mq.finish(repo, mqrebase.keys())
403 411
404 412 # We must start import from the newest revision
405 413 for rev in sorted(mqrebase, reverse=True):
406 414 if rev not in skipped:
407 415 name, isgit = mqrebase[rev]
408 416 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
409 417 mq.qimport(repo, (), patchname=name, git=isgit,
410 418 rev=[str(state[rev])])
411 419
412 420 # restore old series to preserve guards
413 421 mq.fullseries = original_series
414 422 mq.series_dirty = True
415 423 mq.savedirty()
416 424
417 425 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
418 426 'Move bookmarks to their correct changesets'
419 427 current = repo._bookmarkcurrent
420 428 for k, v in originalbookmarks.iteritems():
421 429 if v in nstate:
422 430 if nstate[v] != nullmerge:
423 431 # reset the pointer if the bookmark was moved incorrectly
424 432 if k != current:
425 433 repo._bookmarks[k] = nstate[v]
426 434
427 435 bookmarks.write(repo)
428 436
429 437 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
430 438 external):
431 439 'Store the current status to allow recovery'
432 440 f = repo.opener("rebasestate", "w")
433 441 f.write(repo[originalwd].hex() + '\n')
434 442 f.write(repo[target].hex() + '\n')
435 443 f.write(repo[external].hex() + '\n')
436 444 f.write('%d\n' % int(collapse))
437 445 f.write('%d\n' % int(keep))
438 446 f.write('%d\n' % int(keepbranches))
439 447 for d, v in state.iteritems():
440 448 oldrev = repo[d].hex()
441 449 newrev = repo[v].hex()
442 450 f.write("%s:%s\n" % (oldrev, newrev))
443 451 f.close()
444 452 repo.ui.debug('rebase status stored\n')
445 453
446 454 def clearstatus(repo):
447 455 'Remove the status files'
448 456 if os.path.exists(repo.join("rebasestate")):
449 457 util.unlinkpath(repo.join("rebasestate"))
450 458
451 459 def restorestatus(repo):
452 460 'Restore a previously stored status'
453 461 try:
454 462 target = None
455 463 collapse = False
456 464 external = nullrev
457 465 state = {}
458 466 f = repo.opener("rebasestate")
459 467 for i, l in enumerate(f.read().splitlines()):
460 468 if i == 0:
461 469 originalwd = repo[l].rev()
462 470 elif i == 1:
463 471 target = repo[l].rev()
464 472 elif i == 2:
465 473 external = repo[l].rev()
466 474 elif i == 3:
467 475 collapse = bool(int(l))
468 476 elif i == 4:
469 477 keep = bool(int(l))
470 478 elif i == 5:
471 479 keepbranches = bool(int(l))
472 480 else:
473 481 oldrev, newrev = l.split(':')
474 482 state[repo[oldrev].rev()] = repo[newrev].rev()
475 483 skipped = set()
476 484 # recompute the set of skipped revs
477 485 if not collapse:
478 486 seen = set([target])
479 487 for old, new in sorted(state.items()):
480 488 if new != nullrev and new in seen:
481 489 skipped.add(old)
482 490 seen.add(new)
483 491 repo.ui.debug('computed skipped revs: %s\n' % skipped)
484 492 repo.ui.debug('rebase status resumed\n')
485 493 return (originalwd, target, state, skipped,
486 494 collapse, keep, keepbranches, external)
487 495 except IOError, err:
488 496 if err.errno != errno.ENOENT:
489 497 raise
490 498 raise util.Abort(_('no rebase in progress'))
491 499
492 500 def abort(repo, originalwd, target, state):
493 501 'Restore the repository to its original state'
494 502 if set(repo.changelog.descendants(target)) - set(state.values()):
495 503 repo.ui.warn(_("warning: new changesets detected on target branch, "
496 504 "can't abort\n"))
497 505 return -1
498 506 else:
499 507 # Strip from the first rebased revision
500 508 merge.update(repo, repo[originalwd].rev(), False, True, False)
501 509 rebased = filter(lambda x: x > -1 and x != target, state.values())
502 510 if rebased:
503 511 strippoint = min(rebased)
504 512 # no backup of rebased cset versions needed
505 513 repair.strip(repo.ui, repo, repo[strippoint].node())
506 514 clearstatus(repo)
507 515 repo.ui.warn(_('rebase aborted\n'))
508 516 return 0
509 517
510 518 def buildstate(repo, dest, src, base, detach):
511 519 'Define which revisions are going to be rebased and where'
512 520 targetancestors = set()
513 521 detachset = set()
514 522
515 523 if not dest:
516 524 # Destination defaults to the latest revision in the current branch
517 525 branch = repo[None].branch()
518 526 dest = repo[branch].rev()
519 527 else:
520 528 dest = repo[dest].rev()
521 529
522 530 # This check isn't strictly necessary, since mq detects commits over an
523 531 # applied patch. But it prevents messing up the working directory when
524 532 # a partially completed rebase is blocked by mq.
525 533 if 'qtip' in repo.tags() and (repo[dest].node() in
526 534 [s.node for s in repo.mq.applied]):
527 535 raise util.Abort(_('cannot rebase onto an applied mq patch'))
528 536
529 537 if src:
530 538 commonbase = repo[src].ancestor(repo[dest])
531 539 samebranch = repo[src].branch() == repo[dest].branch()
532 540 if commonbase == repo[src]:
533 541 raise util.Abort(_('source is ancestor of destination'))
534 542 if samebranch and commonbase == repo[dest]:
535 543 raise util.Abort(_('source is descendant of destination'))
536 544 source = repo[src].rev()
537 545 if detach:
538 546 # We need to keep track of source's ancestors up to the common base
539 547 srcancestors = set(repo.changelog.ancestors(source))
540 548 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
541 549 detachset = srcancestors - baseancestors
542 550 detachset.discard(commonbase.rev())
543 551 else:
544 552 if base:
545 553 cwd = repo[base].rev()
546 554 else:
547 555 cwd = repo['.'].rev()
548 556
549 557 if cwd == dest:
550 558 repo.ui.debug('source and destination are the same\n')
551 559 return None
552 560
553 561 targetancestors = set(repo.changelog.ancestors(dest))
554 562 if cwd in targetancestors:
555 563 repo.ui.debug('source is ancestor of destination\n')
556 564 return None
557 565
558 566 cwdancestors = set(repo.changelog.ancestors(cwd))
559 567 if dest in cwdancestors:
560 568 repo.ui.debug('source is descendant of destination\n')
561 569 return None
562 570
563 571 cwdancestors.add(cwd)
564 572 rebasingbranch = cwdancestors - targetancestors
565 573 source = min(rebasingbranch)
566 574
567 575 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
568 576 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
569 577 state.update(dict.fromkeys(detachset, nullmerge))
570 578 state[source] = nullrev
571 579 return repo['.'].rev(), repo[dest].rev(), state
572 580
573 581 def pullrebase(orig, ui, repo, *args, **opts):
574 582 'Call rebase after pull if the latter has been invoked with --rebase'
575 583 if opts.get('rebase'):
576 584 if opts.get('update'):
577 585 del opts['update']
578 586 ui.debug('--update and --rebase are not compatible, ignoring '
579 587 'the update flag\n')
580 588
581 589 cmdutil.bailifchanged(repo)
582 590 revsprepull = len(repo)
583 591 origpostincoming = commands.postincoming
584 592 def _dummy(*args, **kwargs):
585 593 pass
586 594 commands.postincoming = _dummy
587 595 try:
588 596 orig(ui, repo, *args, **opts)
589 597 finally:
590 598 commands.postincoming = origpostincoming
591 599 revspostpull = len(repo)
592 600 if revspostpull > revsprepull:
593 601 rebase(ui, repo, **opts)
594 602 branch = repo[None].branch()
595 603 dest = repo[branch].rev()
596 604 if dest != repo['.'].rev():
597 605 # there was nothing to rebase we force an update
598 606 hg.update(repo, dest)
599 607 else:
600 608 if opts.get('tool'):
601 609 raise util.Abort(_('--tool can only be used with --rebase'))
602 610 orig(ui, repo, *args, **opts)
603 611
604 612 def uisetup(ui):
605 613 'Replace pull with a decorator to provide --rebase option'
606 614 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
607 615 entry[1].append(('', 'rebase', None,
608 616 _("rebase working directory to branch head")))
609 617 entry[1].append(('t', 'tool', '',
610 618 _("specify merge tool for rebase")))
@@ -1,444 +1,482 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > graphlog=
4 4 > rebase=
5 5 >
6 6 > [alias]
7 7 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
8 8 > EOF
9 9
10 10 Create repo a:
11 11
12 12 $ hg init a
13 13 $ cd a
14 14 $ hg unbundle $TESTDIR/bundles/rebase.hg
15 15 adding changesets
16 16 adding manifests
17 17 adding file changes
18 18 added 8 changesets with 7 changes to 7 files (+2 heads)
19 19 (run 'hg heads' to see heads, 'hg merge' to merge)
20 20 $ hg up tip
21 21 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
22 22
23 23 $ hg tglog
24 24 @ 7: 'H'
25 25 |
26 26 | o 6: 'G'
27 27 |/|
28 28 o | 5: 'F'
29 29 | |
30 30 | o 4: 'E'
31 31 |/
32 32 | o 3: 'D'
33 33 | |
34 34 | o 2: 'C'
35 35 | |
36 36 | o 1: 'B'
37 37 |/
38 38 o 0: 'A'
39 39
40 40 $ cd ..
41 41
42 42
43 43 Rebasing B onto H:
44 44
45 45 $ hg clone -q -u 3 a a1
46 46 $ cd a1
47 47
48 48 $ hg rebase --collapse --keepbranches
49 49 saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
50 50
51 51 $ hg tglog
52 52 @ 5: 'Collapsed revision
53 53 | * B
54 54 | * C
55 55 | * D'
56 56 o 4: 'H'
57 57 |
58 58 | o 3: 'G'
59 59 |/|
60 60 o | 2: 'F'
61 61 | |
62 62 | o 1: 'E'
63 63 |/
64 64 o 0: 'A'
65 65
66 66 $ hg manifest
67 67 A
68 68 B
69 69 C
70 70 D
71 71 F
72 72 H
73 73
74 74 $ cd ..
75 75
76 76
77 77 Rebasing G onto H:
78 78
79 79 $ hg clone -q -u . a a2
80 80 $ cd a2
81 81
82 82 $ hg rebase --base 6 --collapse
83 83 saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
84 84
85 85 $ hg tglog
86 86 @ 6: 'Collapsed revision
87 87 | * E
88 88 | * G'
89 89 o 5: 'H'
90 90 |
91 91 o 4: 'F'
92 92 |
93 93 | o 3: 'D'
94 94 | |
95 95 | o 2: 'C'
96 96 | |
97 97 | o 1: 'B'
98 98 |/
99 99 o 0: 'A'
100 100
101 101 $ hg manifest
102 102 A
103 103 E
104 104 F
105 105 H
106 106
107 107 $ cd ..
108 108
109 109 Rebasing G onto H with custom message:
110 110
111 111 $ hg clone -q -u . a a3
112 112 $ cd a3
113 113
114 114 $ hg rebase --base 6 -m 'custom message'
115 115 abort: message can only be specified with collapse
116 116 [255]
117 117
118 118 $ hg rebase --base 6 --collapse -m 'custom message'
119 119 saved backup bundle to $TESTTMP/a3/.hg/strip-backup/*-backup.hg (glob)
120 120
121 121 $ hg tglog
122 122 @ 6: 'custom message'
123 123 |
124 124 o 5: 'H'
125 125 |
126 126 o 4: 'F'
127 127 |
128 128 | o 3: 'D'
129 129 | |
130 130 | o 2: 'C'
131 131 | |
132 132 | o 1: 'B'
133 133 |/
134 134 o 0: 'A'
135 135
136 136 $ hg manifest
137 137 A
138 138 E
139 139 F
140 140 H
141 141
142 142 $ cd ..
143 143
144 144 Create repo b:
145 145
146 146 $ hg init b
147 147 $ cd b
148 148
149 149 $ echo A > A
150 150 $ hg ci -Am A
151 151 adding A
152 152 $ echo B > B
153 153 $ hg ci -Am B
154 154 adding B
155 155
156 156 $ hg up -q 0
157 157
158 158 $ echo C > C
159 159 $ hg ci -Am C
160 160 adding C
161 161 created new head
162 162
163 163 $ hg merge
164 164 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
165 165 (branch merge, don't forget to commit)
166 166
167 167 $ echo D > D
168 168 $ hg ci -Am D
169 169 adding D
170 170
171 171 $ hg up -q 1
172 172
173 173 $ echo E > E
174 174 $ hg ci -Am E
175 175 adding E
176 176 created new head
177 177
178 178 $ echo F > F
179 179 $ hg ci -Am F
180 180 adding F
181 181
182 182 $ hg merge
183 183 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
184 184 (branch merge, don't forget to commit)
185 185 $ hg ci -m G
186 186
187 187 $ hg up -q 0
188 188
189 189 $ echo H > H
190 190 $ hg ci -Am H
191 191 adding H
192 192 created new head
193 193
194 194 $ hg tglog
195 195 @ 7: 'H'
196 196 |
197 197 | o 6: 'G'
198 198 | |\
199 199 | | o 5: 'F'
200 200 | | |
201 201 | | o 4: 'E'
202 202 | | |
203 203 | o | 3: 'D'
204 204 | |\|
205 205 | o | 2: 'C'
206 206 |/ /
207 207 | o 1: 'B'
208 208 |/
209 209 o 0: 'A'
210 210
211 211 $ cd ..
212 212
213 213
214 214 Rebase and collapse - more than one external (fail):
215 215
216 216 $ hg clone -q -u . b b1
217 217 $ cd b1
218 218
219 219 $ hg rebase -s 2 --collapse
220 220 abort: unable to collapse, there is more than one external parent
221 221 [255]
222 222
223 223 Rebase and collapse - E onto H:
224 224
225 225 $ hg rebase -s 4 --collapse
226 226 saved backup bundle to $TESTTMP/b1/.hg/strip-backup/*-backup.hg (glob)
227 227
228 228 $ hg tglog
229 229 @ 5: 'Collapsed revision
230 230 |\ * E
231 231 | | * F
232 232 | | * G'
233 233 | o 4: 'H'
234 234 | |
235 235 o | 3: 'D'
236 236 |\ \
237 237 | o | 2: 'C'
238 238 | |/
239 239 o / 1: 'B'
240 240 |/
241 241 o 0: 'A'
242 242
243 243 $ hg manifest
244 244 A
245 245 B
246 246 C
247 247 D
248 248 E
249 249 F
250 250 H
251 251
252 252 $ cd ..
253 253
254 254
255 255 Create repo c:
256 256
257 257 $ hg init c
258 258 $ cd c
259 259
260 260 $ echo A > A
261 261 $ hg ci -Am A
262 262 adding A
263 263 $ echo B > B
264 264 $ hg ci -Am B
265 265 adding B
266 266
267 267 $ hg up -q 0
268 268
269 269 $ echo C > C
270 270 $ hg ci -Am C
271 271 adding C
272 272 created new head
273 273
274 274 $ hg merge
275 275 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
276 276 (branch merge, don't forget to commit)
277 277
278 278 $ echo D > D
279 279 $ hg ci -Am D
280 280 adding D
281 281
282 282 $ hg up -q 1
283 283
284 284 $ echo E > E
285 285 $ hg ci -Am E
286 286 adding E
287 287 created new head
288 288 $ echo F > E
289 289 $ hg ci -m 'F'
290 290
291 291 $ echo G > G
292 292 $ hg ci -Am G
293 293 adding G
294 294
295 295 $ hg merge
296 296 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
297 297 (branch merge, don't forget to commit)
298 298
299 299 $ hg ci -m H
300 300
301 301 $ hg up -q 0
302 302
303 303 $ echo I > I
304 304 $ hg ci -Am I
305 305 adding I
306 306 created new head
307 307
308 308 $ hg tglog
309 309 @ 8: 'I'
310 310 |
311 311 | o 7: 'H'
312 312 | |\
313 313 | | o 6: 'G'
314 314 | | |
315 315 | | o 5: 'F'
316 316 | | |
317 317 | | o 4: 'E'
318 318 | | |
319 319 | o | 3: 'D'
320 320 | |\|
321 321 | o | 2: 'C'
322 322 |/ /
323 323 | o 1: 'B'
324 324 |/
325 325 o 0: 'A'
326 326
327 327 $ cd ..
328 328
329 329
330 330 Rebase and collapse - E onto I:
331 331
332 332 $ hg clone -q -u . c c1
333 333 $ cd c1
334 334
335 335 $ hg rebase -s 4 --collapse
336 336 merging E
337 337 saved backup bundle to $TESTTMP/c1/.hg/strip-backup/*-backup.hg (glob)
338 338
339 339 $ hg tglog
340 340 @ 5: 'Collapsed revision
341 341 |\ * E
342 342 | | * F
343 343 | | * G
344 344 | | * H'
345 345 | o 4: 'I'
346 346 | |
347 347 o | 3: 'D'
348 348 |\ \
349 349 | o | 2: 'C'
350 350 | |/
351 351 o / 1: 'B'
352 352 |/
353 353 o 0: 'A'
354 354
355 355 $ hg manifest
356 356 A
357 357 B
358 358 C
359 359 D
360 360 E
361 361 G
362 362 I
363 363
364 364 $ cat E
365 365 F
366 366
367 367 $ cd ..
368 368
369 369
370 370 Create repo d:
371 371
372 372 $ hg init d
373 373 $ cd d
374 374
375 375 $ echo A > A
376 376 $ hg ci -Am A
377 377 adding A
378 378 $ echo B > B
379 379 $ hg ci -Am B
380 380 adding B
381 381 $ echo C > C
382 382 $ hg ci -Am C
383 383 adding C
384 384
385 385 $ hg up -q 1
386 386
387 387 $ echo D > D
388 388 $ hg ci -Am D
389 389 adding D
390 390 created new head
391 391 $ hg merge
392 392 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
393 393 (branch merge, don't forget to commit)
394 394
395 395 $ hg ci -m E
396 396
397 397 $ hg up -q 0
398 398
399 399 $ echo F > F
400 400 $ hg ci -Am F
401 401 adding F
402 402 created new head
403 403
404 404 $ hg tglog
405 405 @ 5: 'F'
406 406 |
407 407 | o 4: 'E'
408 408 | |\
409 409 | | o 3: 'D'
410 410 | | |
411 411 | o | 2: 'C'
412 412 | |/
413 413 | o 1: 'B'
414 414 |/
415 415 o 0: 'A'
416 416
417 417 $ cd ..
418 418
419 419
420 420 Rebase and collapse - B onto F:
421 421
422 422 $ hg clone -q -u . d d1
423 423 $ cd d1
424 424
425 425 $ hg rebase -s 1 --collapse
426 426 saved backup bundle to $TESTTMP/d1/.hg/strip-backup/*-backup.hg (glob)
427 427
428 428 $ hg tglog
429 429 @ 2: 'Collapsed revision
430 430 | * B
431 431 | * C
432 432 | * D
433 433 | * E'
434 434 o 1: 'F'
435 435 |
436 436 o 0: 'A'
437 437
438 438 $ hg manifest
439 439 A
440 440 B
441 441 C
442 442 D
443 443 F
444 444
445 Interactions between collapse and keepbranches
446 $ cd ..
447 $ hg init e
448 $ cd e
449 $ echo 'a' > a
450 $ hg ci -Am 'A'
451 adding a
452
453 $ hg branch '1'
454 marked working directory as branch 1
455 $ echo 'b' > b
456 $ hg ci -Am 'B'
457 adding b
458
459 $ hg branch '2'
460 marked working directory as branch 2
461 $ echo 'c' > c
462 $ hg ci -Am 'C'
463 adding c
464
465 $ hg up -q 0
466 $ echo 'd' > d
467 $ hg ci -Am 'D'
468 adding d
469
470 $ hg tglog
471 @ 3: 'D'
472 |
473 | o 2: 'C' 2
474 | |
475 | o 1: 'B' 1
476 |/
477 o 0: 'A'
478
479 $ hg rebase --keepbranches --collapse -s 1 -d 3
480 abort: cannot collapse multiple named branches
481 [255]
482
General Comments 0
You need to be logged in to leave comments. Login now