##// END OF EJS Templates
rebase: use fm.formatlist() and fm.formatdict() to support user template...
Pulkit Goyal -
r35123:f56a30b8 default
parent child Browse files
Show More
@@ -1,1706 +1,1709
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 __future__ import absolute_import
18 18
19 19 import errno
20 20 import os
21 21
22 22 from mercurial.i18n import _
23 23 from mercurial.node import (
24 hex,
25 24 nullid,
26 25 nullrev,
27 26 short,
28 27 )
29 28 from mercurial import (
30 29 bookmarks,
31 30 cmdutil,
32 31 commands,
33 32 copies,
34 33 destutil,
35 34 dirstateguard,
36 35 error,
37 36 extensions,
38 37 hg,
39 38 lock,
40 39 merge as mergemod,
41 40 mergeutil,
42 41 obsolete,
43 42 obsutil,
44 43 patch,
45 44 phases,
46 45 pycompat,
47 46 registrar,
48 47 repair,
49 48 revset,
50 49 revsetlang,
51 50 scmutil,
52 51 smartset,
53 52 util,
54 53 )
55 54
56 55 release = lock.release
57 56
58 57 # The following constants are used throughout the rebase module. The ordering of
59 58 # their values must be maintained.
60 59
61 60 # Indicates that a revision needs to be rebased
62 61 revtodo = -1
63 62 revtodostr = '-1'
64 63
65 64 # legacy revstates no longer needed in current code
66 65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
67 66 legacystates = {'-2', '-3', '-4', '-5'}
68 67
69 68 cmdtable = {}
70 69 command = registrar.command(cmdtable)
71 70 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
72 71 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
73 72 # be specifying the version(s) of Mercurial they are tested with, or
74 73 # leave the attribute unspecified.
75 74 testedwith = 'ships-with-hg-core'
76 75
77 76 def _nothingtorebase():
78 77 return 1
79 78
80 79 def _savegraft(ctx, extra):
81 80 s = ctx.extra().get('source', None)
82 81 if s is not None:
83 82 extra['source'] = s
84 83 s = ctx.extra().get('intermediate-source', None)
85 84 if s is not None:
86 85 extra['intermediate-source'] = s
87 86
88 87 def _savebranch(ctx, extra):
89 88 extra['branch'] = ctx.branch()
90 89
91 90 def _makeextrafn(copiers):
92 91 """make an extrafn out of the given copy-functions.
93 92
94 93 A copy function takes a context and an extra dict, and mutates the
95 94 extra dict as needed based on the given context.
96 95 """
97 96 def extrafn(ctx, extra):
98 97 for c in copiers:
99 98 c(ctx, extra)
100 99 return extrafn
101 100
102 101 def _destrebase(repo, sourceset, destspace=None):
103 102 """small wrapper around destmerge to pass the right extra args
104 103
105 104 Please wrap destutil.destmerge instead."""
106 105 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
107 106 onheadcheck=False, destspace=destspace)
108 107
109 108 revsetpredicate = registrar.revsetpredicate()
110 109
111 110 @revsetpredicate('_destrebase')
112 111 def _revsetdestrebase(repo, subset, x):
113 112 # ``_rebasedefaultdest()``
114 113
115 114 # default destination for rebase.
116 115 # # XXX: Currently private because I expect the signature to change.
117 116 # # XXX: - bailing out in case of ambiguity vs returning all data.
118 117 # i18n: "_rebasedefaultdest" is a keyword
119 118 sourceset = None
120 119 if x is not None:
121 120 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
122 121 return subset & smartset.baseset([_destrebase(repo, sourceset)])
123 122
124 123 def _ctxdesc(ctx):
125 124 """short description for a context"""
126 125 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
127 126 ctx.description().split('\n', 1)[0])
128 127 repo = ctx.repo()
129 128 names = []
130 129 for nsname, ns in repo.names.iteritems():
131 130 if nsname == 'branches':
132 131 continue
133 132 names.extend(ns.names(repo, ctx.node()))
134 133 if names:
135 134 desc += ' (%s)' % ' '.join(names)
136 135 return desc
137 136
138 137 class rebaseruntime(object):
139 138 """This class is a container for rebase runtime state"""
140 139 def __init__(self, repo, ui, opts=None):
141 140 if opts is None:
142 141 opts = {}
143 142
144 143 # prepared: whether we have rebasestate prepared or not. Currently it
145 144 # decides whether "self.repo" is unfiltered or not.
146 145 # The rebasestate has explicit hash to hash instructions not depending
147 146 # on visibility. If rebasestate exists (in-memory or on-disk), use
148 147 # unfiltered repo to avoid visibility issues.
149 148 # Before knowing rebasestate (i.e. when starting a new rebase (not
150 149 # --continue or --abort)), the original repo should be used so
151 150 # visibility-dependent revsets are correct.
152 151 self.prepared = False
153 152 self._repo = repo
154 153
155 154 self.ui = ui
156 155 self.opts = opts
157 156 self.originalwd = None
158 157 self.external = nullrev
159 158 # Mapping between the old revision id and either what is the new rebased
160 159 # revision or what needs to be done with the old revision. The state
161 160 # dict will be what contains most of the rebase progress state.
162 161 self.state = {}
163 162 self.activebookmark = None
164 163 self.destmap = {}
165 164 self.skipped = set()
166 165
167 166 self.collapsef = opts.get('collapse', False)
168 167 self.collapsemsg = cmdutil.logmessage(ui, opts)
169 168 self.date = opts.get('date', None)
170 169
171 170 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
172 171 self.extrafns = [_savegraft]
173 172 if e:
174 173 self.extrafns = [e]
175 174
176 175 self.keepf = opts.get('keep', False)
177 176 self.keepbranchesf = opts.get('keepbranches', False)
178 177 # keepopen is not meant for use on the command line, but by
179 178 # other extensions
180 179 self.keepopen = opts.get('keepopen', False)
181 180 self.obsoletenotrebased = {}
182 181 self.obsoletewithoutsuccessorindestination = set()
183 182
184 183 @property
185 184 def repo(self):
186 185 if self.prepared:
187 186 return self._repo.unfiltered()
188 187 else:
189 188 return self._repo
190 189
191 190 def storestatus(self, tr=None):
192 191 """Store the current status to allow recovery"""
193 192 if tr:
194 193 tr.addfilegenerator('rebasestate', ('rebasestate',),
195 194 self._writestatus, location='plain')
196 195 else:
197 196 with self.repo.vfs("rebasestate", "w") as f:
198 197 self._writestatus(f)
199 198
200 199 def _writestatus(self, f):
201 200 repo = self.repo
202 201 assert repo.filtername is None
203 202 f.write(repo[self.originalwd].hex() + '\n')
204 203 # was "dest". we now write dest per src root below.
205 204 f.write('\n')
206 205 f.write(repo[self.external].hex() + '\n')
207 206 f.write('%d\n' % int(self.collapsef))
208 207 f.write('%d\n' % int(self.keepf))
209 208 f.write('%d\n' % int(self.keepbranchesf))
210 209 f.write('%s\n' % (self.activebookmark or ''))
211 210 destmap = self.destmap
212 211 for d, v in self.state.iteritems():
213 212 oldrev = repo[d].hex()
214 213 if v >= 0:
215 214 newrev = repo[v].hex()
216 215 else:
217 216 newrev = v
218 217 destnode = repo[destmap[d]].hex()
219 218 f.write("%s:%s:%s\n" % (oldrev, newrev, destnode))
220 219 repo.ui.debug('rebase status stored\n')
221 220
222 221 def restorestatus(self):
223 222 """Restore a previously stored status"""
224 223 self.prepared = True
225 224 repo = self.repo
226 225 assert repo.filtername is None
227 226 keepbranches = None
228 227 legacydest = None
229 228 collapse = False
230 229 external = nullrev
231 230 activebookmark = None
232 231 state = {}
233 232 destmap = {}
234 233
235 234 try:
236 235 f = repo.vfs("rebasestate")
237 236 for i, l in enumerate(f.read().splitlines()):
238 237 if i == 0:
239 238 originalwd = repo[l].rev()
240 239 elif i == 1:
241 240 # this line should be empty in newer version. but legacy
242 241 # clients may still use it
243 242 if l:
244 243 legacydest = repo[l].rev()
245 244 elif i == 2:
246 245 external = repo[l].rev()
247 246 elif i == 3:
248 247 collapse = bool(int(l))
249 248 elif i == 4:
250 249 keep = bool(int(l))
251 250 elif i == 5:
252 251 keepbranches = bool(int(l))
253 252 elif i == 6 and not (len(l) == 81 and ':' in l):
254 253 # line 6 is a recent addition, so for backwards
255 254 # compatibility check that the line doesn't look like the
256 255 # oldrev:newrev lines
257 256 activebookmark = l
258 257 else:
259 258 args = l.split(':')
260 259 oldrev = args[0]
261 260 newrev = args[1]
262 261 if newrev in legacystates:
263 262 continue
264 263 if len(args) > 2:
265 264 destnode = args[2]
266 265 else:
267 266 destnode = legacydest
268 267 destmap[repo[oldrev].rev()] = repo[destnode].rev()
269 268 if newrev in (nullid, revtodostr):
270 269 state[repo[oldrev].rev()] = revtodo
271 270 # Legacy compat special case
272 271 else:
273 272 state[repo[oldrev].rev()] = repo[newrev].rev()
274 273
275 274 except IOError as err:
276 275 if err.errno != errno.ENOENT:
277 276 raise
278 277 cmdutil.wrongtooltocontinue(repo, _('rebase'))
279 278
280 279 if keepbranches is None:
281 280 raise error.Abort(_('.hg/rebasestate is incomplete'))
282 281
283 282 skipped = set()
284 283 # recompute the set of skipped revs
285 284 if not collapse:
286 285 seen = set(destmap.values())
287 286 for old, new in sorted(state.items()):
288 287 if new != revtodo and new in seen:
289 288 skipped.add(old)
290 289 seen.add(new)
291 290 repo.ui.debug('computed skipped revs: %s\n' %
292 291 (' '.join(str(r) for r in sorted(skipped)) or None))
293 292 repo.ui.debug('rebase status resumed\n')
294 293
295 294 self.originalwd = originalwd
296 295 self.destmap = destmap
297 296 self.state = state
298 297 self.skipped = skipped
299 298 self.collapsef = collapse
300 299 self.keepf = keep
301 300 self.keepbranchesf = keepbranches
302 301 self.external = external
303 302 self.activebookmark = activebookmark
304 303
305 304 def _handleskippingobsolete(self, obsoleterevs, destmap):
306 305 """Compute structures necessary for skipping obsolete revisions
307 306
308 307 obsoleterevs: iterable of all obsolete revisions in rebaseset
309 308 destmap: {srcrev: destrev} destination revisions
310 309 """
311 310 self.obsoletenotrebased = {}
312 311 if not self.ui.configbool('experimental', 'rebaseskipobsolete'):
313 312 return
314 313 obsoleteset = set(obsoleterevs)
315 314 self.obsoletenotrebased, self.obsoletewithoutsuccessorindestination = \
316 315 _computeobsoletenotrebased(self.repo, obsoleteset, destmap)
317 316 skippedset = set(self.obsoletenotrebased)
318 317 skippedset.update(self.obsoletewithoutsuccessorindestination)
319 318 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
320 319
321 320 def _prepareabortorcontinue(self, isabort):
322 321 try:
323 322 self.restorestatus()
324 323 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
325 324 except error.RepoLookupError:
326 325 if isabort:
327 326 clearstatus(self.repo)
328 327 clearcollapsemsg(self.repo)
329 328 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
330 329 ' only broken state is cleared)\n'))
331 330 return 0
332 331 else:
333 332 msg = _('cannot continue inconsistent rebase')
334 333 hint = _('use "hg rebase --abort" to clear broken state')
335 334 raise error.Abort(msg, hint=hint)
336 335 if isabort:
337 336 return abort(self.repo, self.originalwd, self.destmap,
338 337 self.state, activebookmark=self.activebookmark)
339 338
340 339 def _preparenewrebase(self, destmap):
341 340 if not destmap:
342 341 return _nothingtorebase()
343 342
344 343 rebaseset = destmap.keys()
345 344 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
346 345 if (not (self.keepf or allowunstable)
347 346 and self.repo.revs('first(children(%ld) - %ld)',
348 347 rebaseset, rebaseset)):
349 348 raise error.Abort(
350 349 _("can't remove original changesets with"
351 350 " unrebased descendants"),
352 351 hint=_('use --keep to keep original changesets'))
353 352
354 353 result = buildstate(self.repo, destmap, self.collapsef)
355 354
356 355 if not result:
357 356 # Empty state built, nothing to rebase
358 357 self.ui.status(_('nothing to rebase\n'))
359 358 return _nothingtorebase()
360 359
361 360 for root in self.repo.set('roots(%ld)', rebaseset):
362 361 if not self.keepf and not root.mutable():
363 362 raise error.Abort(_("can't rebase public changeset %s")
364 363 % root,
365 364 hint=_("see 'hg help phases' for details"))
366 365
367 366 (self.originalwd, self.destmap, self.state) = result
368 367 if self.collapsef:
369 368 dests = set(self.destmap.values())
370 369 if len(dests) != 1:
371 370 raise error.Abort(
372 371 _('--collapse does not work with multiple destinations'))
373 372 destrev = next(iter(dests))
374 373 destancestors = self.repo.changelog.ancestors([destrev],
375 374 inclusive=True)
376 375 self.external = externalparent(self.repo, self.state, destancestors)
377 376
378 377 for destrev in sorted(set(destmap.values())):
379 378 dest = self.repo[destrev]
380 379 if dest.closesbranch() and not self.keepbranchesf:
381 380 self.ui.status(_('reopening closed branch head %s\n') % dest)
382 381
383 382 self.prepared = True
384 383
385 384 def _performrebase(self, tr):
386 385 repo, ui = self.repo, self.ui
387 386 if self.keepbranchesf:
388 387 # insert _savebranch at the start of extrafns so if
389 388 # there's a user-provided extrafn it can clobber branch if
390 389 # desired
391 390 self.extrafns.insert(0, _savebranch)
392 391 if self.collapsef:
393 392 branches = set()
394 393 for rev in self.state:
395 394 branches.add(repo[rev].branch())
396 395 if len(branches) > 1:
397 396 raise error.Abort(_('cannot collapse multiple named '
398 397 'branches'))
399 398
400 399 # Calculate self.obsoletenotrebased
401 400 obsrevs = _filterobsoleterevs(self.repo, self.state)
402 401 self._handleskippingobsolete(obsrevs, self.destmap)
403 402
404 403 # Keep track of the active bookmarks in order to reset them later
405 404 self.activebookmark = self.activebookmark or repo._activebookmark
406 405 if self.activebookmark:
407 406 bookmarks.deactivate(repo)
408 407
409 408 # Store the state before we begin so users can run 'hg rebase --abort'
410 409 # if we fail before the transaction closes.
411 410 self.storestatus()
412 411
413 412 cands = [k for k, v in self.state.iteritems() if v == revtodo]
414 413 total = len(cands)
415 414 pos = 0
416 415 for subset in sortsource(self.destmap):
417 416 pos = self._performrebasesubset(tr, subset, pos, total)
418 417 ui.progress(_('rebasing'), None)
419 418 ui.note(_('rebase merging completed\n'))
420 419
421 420 def _performrebasesubset(self, tr, subset, pos, total):
422 421 repo, ui, opts = self.repo, self.ui, self.opts
423 422 sortedrevs = repo.revs('sort(%ld, -topo)', subset)
424 423 allowdivergence = self.ui.configbool(
425 424 'experimental', 'evolution.allowdivergence')
426 425 if not allowdivergence:
427 426 sortedrevs -= repo.revs(
428 427 'descendants(%ld) and not %ld',
429 428 self.obsoletewithoutsuccessorindestination,
430 429 self.obsoletewithoutsuccessorindestination,
431 430 )
432 431 for rev in sortedrevs:
433 432 dest = self.destmap[rev]
434 433 ctx = repo[rev]
435 434 desc = _ctxdesc(ctx)
436 435 if self.state[rev] == rev:
437 436 ui.status(_('already rebased %s\n') % desc)
438 437 elif (not allowdivergence
439 438 and rev in self.obsoletewithoutsuccessorindestination):
440 439 msg = _('note: not rebasing %s and its descendants as '
441 440 'this would cause divergence\n') % desc
442 441 repo.ui.status(msg)
443 442 self.skipped.add(rev)
444 443 elif rev in self.obsoletenotrebased:
445 444 succ = self.obsoletenotrebased[rev]
446 445 if succ is None:
447 446 msg = _('note: not rebasing %s, it has no '
448 447 'successor\n') % desc
449 448 else:
450 449 succdesc = _ctxdesc(repo[succ])
451 450 msg = (_('note: not rebasing %s, already in '
452 451 'destination as %s\n') % (desc, succdesc))
453 452 repo.ui.status(msg)
454 453 # Make clearrebased aware state[rev] is not a true successor
455 454 self.skipped.add(rev)
456 455 # Record rev as moved to its desired destination in self.state.
457 456 # This helps bookmark and working parent movement.
458 457 dest = max(adjustdest(repo, rev, self.destmap, self.state,
459 458 self.skipped))
460 459 self.state[rev] = dest
461 460 elif self.state[rev] == revtodo:
462 461 pos += 1
463 462 ui.status(_('rebasing %s\n') % desc)
464 463 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
465 464 _('changesets'), total)
466 465 p1, p2, base = defineparents(repo, rev, self.destmap,
467 466 self.state, self.skipped,
468 467 self.obsoletenotrebased)
469 468 self.storestatus(tr=tr)
470 469 storecollapsemsg(repo, self.collapsemsg)
471 470 if len(repo[None].parents()) == 2:
472 471 repo.ui.debug('resuming interrupted rebase\n')
473 472 else:
474 473 try:
475 474 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
476 475 'rebase')
477 476 stats = rebasenode(repo, rev, p1, base, self.state,
478 477 self.collapsef, dest)
479 478 if stats and stats[3] > 0:
480 479 raise error.InterventionRequired(
481 480 _('unresolved conflicts (see hg '
482 481 'resolve, then hg rebase --continue)'))
483 482 finally:
484 483 ui.setconfig('ui', 'forcemerge', '', 'rebase')
485 484 if not self.collapsef:
486 485 merging = p2 != nullrev
487 486 editform = cmdutil.mergeeditform(merging, 'rebase')
488 487 editor = cmdutil.getcommiteditor(editform=editform, **opts)
489 488 newnode = concludenode(repo, rev, p1, p2,
490 489 extrafn=_makeextrafn(self.extrafns),
491 490 editor=editor,
492 491 keepbranches=self.keepbranchesf,
493 492 date=self.date)
494 493 if newnode is None:
495 494 # If it ended up being a no-op commit, then the normal
496 495 # merge state clean-up path doesn't happen, so do it
497 496 # here. Fix issue5494
498 497 mergemod.mergestate.clean(repo)
499 498 else:
500 499 # Skip commit if we are collapsing
501 500 repo.setparents(repo[p1].node())
502 501 newnode = None
503 502 # Update the state
504 503 if newnode is not None:
505 504 self.state[rev] = repo[newnode].rev()
506 505 ui.debug('rebased as %s\n' % short(newnode))
507 506 else:
508 507 if not self.collapsef:
509 508 ui.warn(_('note: rebase of %d:%s created no changes '
510 509 'to commit\n') % (rev, ctx))
511 510 self.skipped.add(rev)
512 511 self.state[rev] = p1
513 512 ui.debug('next revision set to %s\n' % p1)
514 513 else:
515 514 ui.status(_('already rebased %s as %s\n') %
516 515 (desc, repo[self.state[rev]]))
517 516 return pos
518 517
519 518 def _finishrebase(self):
520 519 repo, ui, opts = self.repo, self.ui, self.opts
521 520 fm = ui.formatter('rebase', opts)
522 521 fm.startitem()
523 522 if self.collapsef and not self.keepopen:
524 523 p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
525 524 self.state, self.skipped,
526 525 self.obsoletenotrebased)
527 526 editopt = opts.get('edit')
528 527 editform = 'rebase.collapse'
529 528 if self.collapsemsg:
530 529 commitmsg = self.collapsemsg
531 530 else:
532 531 commitmsg = 'Collapsed revision'
533 532 for rebased in sorted(self.state):
534 533 if rebased not in self.skipped:
535 534 commitmsg += '\n* %s' % repo[rebased].description()
536 535 editopt = True
537 536 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
538 537 revtoreuse = max(self.state)
539 538
540 539 dsguard = None
541 540 if ui.configbool('rebase', 'singletransaction'):
542 541 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
543 542 with util.acceptintervention(dsguard):
544 543 newnode = concludenode(repo, revtoreuse, p1, self.external,
545 544 commitmsg=commitmsg,
546 545 extrafn=_makeextrafn(self.extrafns),
547 546 editor=editor,
548 547 keepbranches=self.keepbranchesf,
549 548 date=self.date)
550 549 if newnode is not None:
551 550 newrev = repo[newnode].rev()
552 551 for oldrev in self.state.iterkeys():
553 552 self.state[oldrev] = newrev
554 553
555 554 if 'qtip' in repo.tags():
556 555 updatemq(repo, self.state, self.skipped, **opts)
557 556
558 557 # restore original working directory
559 558 # (we do this before stripping)
560 559 newwd = self.state.get(self.originalwd, self.originalwd)
561 560 if newwd < 0:
562 561 # original directory is a parent of rebase set root or ignored
563 562 newwd = self.originalwd
564 563 if newwd not in [c.rev() for c in repo[None].parents()]:
565 564 ui.note(_("update back to initial working directory parent\n"))
566 565 hg.updaterepo(repo, newwd, False)
567 566
568 567 collapsedas = None
569 568 if not self.keepf:
570 569 if self.collapsef:
571 570 collapsedas = newnode
572 571 clearrebased(ui, repo, self.destmap, self.state, self.skipped,
573 572 collapsedas, self.keepf, fm=fm)
574 573
575 574 clearstatus(repo)
576 575 clearcollapsemsg(repo)
577 576
578 577 ui.note(_("rebase completed\n"))
579 578 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
580 579 if self.skipped:
581 580 skippedlen = len(self.skipped)
582 581 ui.note(_("%d revisions have been skipped\n") % skippedlen)
583 582 fm.end()
584 583
585 584 if (self.activebookmark and self.activebookmark in repo._bookmarks and
586 585 repo['.'].node() == repo._bookmarks[self.activebookmark]):
587 586 bookmarks.activate(repo, self.activebookmark)
588 587
589 588 @command('rebase',
590 589 [('s', 'source', '',
591 590 _('rebase the specified changeset and descendants'), _('REV')),
592 591 ('b', 'base', '',
593 592 _('rebase everything from branching point of specified changeset'),
594 593 _('REV')),
595 594 ('r', 'rev', [],
596 595 _('rebase these revisions'),
597 596 _('REV')),
598 597 ('d', 'dest', '',
599 598 _('rebase onto the specified changeset'), _('REV')),
600 599 ('', 'collapse', False, _('collapse the rebased changesets')),
601 600 ('m', 'message', '',
602 601 _('use text as collapse commit message'), _('TEXT')),
603 602 ('e', 'edit', False, _('invoke editor on commit messages')),
604 603 ('l', 'logfile', '',
605 604 _('read collapse commit message from file'), _('FILE')),
606 605 ('k', 'keep', False, _('keep original changesets')),
607 606 ('', 'keepbranches', False, _('keep original branch names')),
608 607 ('D', 'detach', False, _('(DEPRECATED)')),
609 608 ('i', 'interactive', False, _('(DEPRECATED)')),
610 609 ('t', 'tool', '', _('specify merge tool')),
611 610 ('c', 'continue', False, _('continue an interrupted rebase')),
612 611 ('a', 'abort', False, _('abort an interrupted rebase'))] +
613 612 cmdutil.formatteropts,
614 613 _('[-s REV | -b REV] [-d REV] [OPTION]'))
615 614 def rebase(ui, repo, **opts):
616 615 """move changeset (and descendants) to a different branch
617 616
618 617 Rebase uses repeated merging to graft changesets from one part of
619 618 history (the source) onto another (the destination). This can be
620 619 useful for linearizing *local* changes relative to a master
621 620 development tree.
622 621
623 622 Published commits cannot be rebased (see :hg:`help phases`).
624 623 To copy commits, see :hg:`help graft`.
625 624
626 625 If you don't specify a destination changeset (``-d/--dest``), rebase
627 626 will use the same logic as :hg:`merge` to pick a destination. if
628 627 the current branch contains exactly one other head, the other head
629 628 is merged with by default. Otherwise, an explicit revision with
630 629 which to merge with must be provided. (destination changeset is not
631 630 modified by rebasing, but new changesets are added as its
632 631 descendants.)
633 632
634 633 Here are the ways to select changesets:
635 634
636 635 1. Explicitly select them using ``--rev``.
637 636
638 637 2. Use ``--source`` to select a root changeset and include all of its
639 638 descendants.
640 639
641 640 3. Use ``--base`` to select a changeset; rebase will find ancestors
642 641 and their descendants which are not also ancestors of the destination.
643 642
644 643 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
645 644 rebase will use ``--base .`` as above.
646 645
647 646 Rebase will destroy original changesets unless you use ``--keep``.
648 647 It will also move your bookmarks (even if you do).
649 648
650 649 Some changesets may be dropped if they do not contribute changes
651 650 (e.g. merges from the destination branch).
652 651
653 652 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
654 653 a named branch with two heads. You will need to explicitly specify source
655 654 and/or destination.
656 655
657 656 If you need to use a tool to automate merge/conflict decisions, you
658 657 can specify one with ``--tool``, see :hg:`help merge-tools`.
659 658 As a caveat: the tool will not be used to mediate when a file was
660 659 deleted, there is no hook presently available for this.
661 660
662 661 If a rebase is interrupted to manually resolve a conflict, it can be
663 662 continued with --continue/-c or aborted with --abort/-a.
664 663
665 664 .. container:: verbose
666 665
667 666 Examples:
668 667
669 668 - move "local changes" (current commit back to branching point)
670 669 to the current branch tip after a pull::
671 670
672 671 hg rebase
673 672
674 673 - move a single changeset to the stable branch::
675 674
676 675 hg rebase -r 5f493448 -d stable
677 676
678 677 - splice a commit and all its descendants onto another part of history::
679 678
680 679 hg rebase --source c0c3 --dest 4cf9
681 680
682 681 - rebase everything on a branch marked by a bookmark onto the
683 682 default branch::
684 683
685 684 hg rebase --base myfeature --dest default
686 685
687 686 - collapse a sequence of changes into a single commit::
688 687
689 688 hg rebase --collapse -r 1520:1525 -d .
690 689
691 690 - move a named branch while preserving its name::
692 691
693 692 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
694 693
695 694 Configuration Options:
696 695
697 696 You can make rebase require a destination if you set the following config
698 697 option::
699 698
700 699 [commands]
701 700 rebase.requiredest = True
702 701
703 702 By default, rebase will close the transaction after each commit. For
704 703 performance purposes, you can configure rebase to use a single transaction
705 704 across the entire rebase. WARNING: This setting introduces a significant
706 705 risk of losing the work you've done in a rebase if the rebase aborts
707 706 unexpectedly::
708 707
709 708 [rebase]
710 709 singletransaction = True
711 710
712 711 Return Values:
713 712
714 713 Returns 0 on success, 1 if nothing to rebase or there are
715 714 unresolved conflicts.
716 715
717 716 """
718 717 opts = pycompat.byteskwargs(opts)
719 718 rbsrt = rebaseruntime(repo, ui, opts)
720 719
721 720 with repo.wlock(), repo.lock():
722 721 # Validate input and define rebasing points
723 722 destf = opts.get('dest', None)
724 723 srcf = opts.get('source', None)
725 724 basef = opts.get('base', None)
726 725 revf = opts.get('rev', [])
727 726 # search default destination in this space
728 727 # used in the 'hg pull --rebase' case, see issue 5214.
729 728 destspace = opts.get('_destspace')
730 729 contf = opts.get('continue')
731 730 abortf = opts.get('abort')
732 731 if opts.get('interactive'):
733 732 try:
734 733 if extensions.find('histedit'):
735 734 enablehistedit = ''
736 735 except KeyError:
737 736 enablehistedit = " --config extensions.histedit="
738 737 help = "hg%s help -e histedit" % enablehistedit
739 738 msg = _("interactive history editing is supported by the "
740 739 "'histedit' extension (see \"%s\")") % help
741 740 raise error.Abort(msg)
742 741
743 742 if rbsrt.collapsemsg and not rbsrt.collapsef:
744 743 raise error.Abort(
745 744 _('message can only be specified with collapse'))
746 745
747 746 if contf or abortf:
748 747 if contf and abortf:
749 748 raise error.Abort(_('cannot use both abort and continue'))
750 749 if rbsrt.collapsef:
751 750 raise error.Abort(
752 751 _('cannot use collapse with continue or abort'))
753 752 if srcf or basef or destf:
754 753 raise error.Abort(
755 754 _('abort and continue do not allow specifying revisions'))
756 755 if abortf and opts.get('tool', False):
757 756 ui.warn(_('tool option will be ignored\n'))
758 757 if contf:
759 758 ms = mergemod.mergestate.read(repo)
760 759 mergeutil.checkunresolved(ms)
761 760
762 761 retcode = rbsrt._prepareabortorcontinue(abortf)
763 762 if retcode is not None:
764 763 return retcode
765 764 else:
766 765 destmap = _definedestmap(ui, repo, destf, srcf, basef, revf,
767 766 destspace=destspace)
768 767 retcode = rbsrt._preparenewrebase(destmap)
769 768 if retcode is not None:
770 769 return retcode
771 770
772 771 tr = None
773 772 dsguard = None
774 773
775 774 singletr = ui.configbool('rebase', 'singletransaction')
776 775 if singletr:
777 776 tr = repo.transaction('rebase')
778 777 with util.acceptintervention(tr):
779 778 if singletr:
780 779 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
781 780 with util.acceptintervention(dsguard):
782 781 rbsrt._performrebase(tr)
783 782
784 783 rbsrt._finishrebase()
785 784
786 785 def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
787 786 destspace=None):
788 787 """use revisions argument to define destmap {srcrev: destrev}"""
789 788 if revf is None:
790 789 revf = []
791 790
792 791 # destspace is here to work around issues with `hg pull --rebase` see
793 792 # issue5214 for details
794 793 if srcf and basef:
795 794 raise error.Abort(_('cannot specify both a source and a base'))
796 795 if revf and basef:
797 796 raise error.Abort(_('cannot specify both a revision and a base'))
798 797 if revf and srcf:
799 798 raise error.Abort(_('cannot specify both a revision and a source'))
800 799
801 800 cmdutil.checkunfinished(repo)
802 801 cmdutil.bailifchanged(repo)
803 802
804 803 if ui.configbool('commands', 'rebase.requiredest') and not destf:
805 804 raise error.Abort(_('you must specify a destination'),
806 805 hint=_('use: hg rebase -d REV'))
807 806
808 807 dest = None
809 808
810 809 if revf:
811 810 rebaseset = scmutil.revrange(repo, revf)
812 811 if not rebaseset:
813 812 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
814 813 return None
815 814 elif srcf:
816 815 src = scmutil.revrange(repo, [srcf])
817 816 if not src:
818 817 ui.status(_('empty "source" revision set - nothing to rebase\n'))
819 818 return None
820 819 rebaseset = repo.revs('(%ld)::', src)
821 820 assert rebaseset
822 821 else:
823 822 base = scmutil.revrange(repo, [basef or '.'])
824 823 if not base:
825 824 ui.status(_('empty "base" revision set - '
826 825 "can't compute rebase set\n"))
827 826 return None
828 827 if destf:
829 828 # --base does not support multiple destinations
830 829 dest = scmutil.revsingle(repo, destf)
831 830 else:
832 831 dest = repo[_destrebase(repo, base, destspace=destspace)]
833 832 destf = str(dest)
834 833
835 834 roots = [] # selected children of branching points
836 835 bpbase = {} # {branchingpoint: [origbase]}
837 836 for b in base: # group bases by branching points
838 837 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
839 838 bpbase[bp] = bpbase.get(bp, []) + [b]
840 839 if None in bpbase:
841 840 # emulate the old behavior, showing "nothing to rebase" (a better
842 841 # behavior may be abort with "cannot find branching point" error)
843 842 bpbase.clear()
844 843 for bp, bs in bpbase.iteritems(): # calculate roots
845 844 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
846 845
847 846 rebaseset = repo.revs('%ld::', roots)
848 847
849 848 if not rebaseset:
850 849 # transform to list because smartsets are not comparable to
851 850 # lists. This should be improved to honor laziness of
852 851 # smartset.
853 852 if list(base) == [dest.rev()]:
854 853 if basef:
855 854 ui.status(_('nothing to rebase - %s is both "base"'
856 855 ' and destination\n') % dest)
857 856 else:
858 857 ui.status(_('nothing to rebase - working directory '
859 858 'parent is also destination\n'))
860 859 elif not repo.revs('%ld - ::%d', base, dest):
861 860 if basef:
862 861 ui.status(_('nothing to rebase - "base" %s is '
863 862 'already an ancestor of destination '
864 863 '%s\n') %
865 864 ('+'.join(str(repo[r]) for r in base),
866 865 dest))
867 866 else:
868 867 ui.status(_('nothing to rebase - working '
869 868 'directory parent is already an '
870 869 'ancestor of destination %s\n') % dest)
871 870 else: # can it happen?
872 871 ui.status(_('nothing to rebase from %s to %s\n') %
873 872 ('+'.join(str(repo[r]) for r in base), dest))
874 873 return None
875 874
876 875 if not destf:
877 876 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
878 877 destf = str(dest)
879 878
880 879 allsrc = revsetlang.formatspec('%ld', rebaseset)
881 880 alias = {'ALLSRC': allsrc}
882 881
883 882 if dest is None:
884 883 try:
885 884 # fast path: try to resolve dest without SRC alias
886 885 dest = scmutil.revsingle(repo, destf, localalias=alias)
887 886 except error.RepoLookupError:
888 887 if not ui.configbool('experimental', 'rebase.multidest'):
889 888 raise
890 889 # multi-dest path: resolve dest for each SRC separately
891 890 destmap = {}
892 891 for r in rebaseset:
893 892 alias['SRC'] = revsetlang.formatspec('%d', r)
894 893 # use repo.anyrevs instead of scmutil.revsingle because we
895 894 # don't want to abort if destset is empty.
896 895 destset = repo.anyrevs([destf], user=True, localalias=alias)
897 896 size = len(destset)
898 897 if size == 1:
899 898 destmap[r] = destset.first()
900 899 elif size == 0:
901 900 ui.note(_('skipping %s - empty destination\n') % repo[r])
902 901 else:
903 902 raise error.Abort(_('rebase destination for %s is not '
904 903 'unique') % repo[r])
905 904
906 905 if dest is not None:
907 906 # single-dest case: assign dest to each rev in rebaseset
908 907 destrev = dest.rev()
909 908 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
910 909
911 910 if not destmap:
912 911 ui.status(_('nothing to rebase - empty destination\n'))
913 912 return None
914 913
915 914 return destmap
916 915
917 916 def externalparent(repo, state, destancestors):
918 917 """Return the revision that should be used as the second parent
919 918 when the revisions in state is collapsed on top of destancestors.
920 919 Abort if there is more than one parent.
921 920 """
922 921 parents = set()
923 922 source = min(state)
924 923 for rev in state:
925 924 if rev == source:
926 925 continue
927 926 for p in repo[rev].parents():
928 927 if (p.rev() not in state
929 928 and p.rev() not in destancestors):
930 929 parents.add(p.rev())
931 930 if not parents:
932 931 return nullrev
933 932 if len(parents) == 1:
934 933 return parents.pop()
935 934 raise error.Abort(_('unable to collapse on top of %s, there is more '
936 935 'than one external parent: %s') %
937 936 (max(destancestors),
938 937 ', '.join(str(p) for p in sorted(parents))))
939 938
940 939 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
941 940 keepbranches=False, date=None):
942 941 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
943 942 but also store useful information in extra.
944 943 Return node of committed revision.'''
945 944 dsguard = util.nullcontextmanager()
946 945 if not repo.ui.configbool('rebase', 'singletransaction'):
947 946 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
948 947 with dsguard:
949 948 repo.setparents(repo[p1].node(), repo[p2].node())
950 949 ctx = repo[rev]
951 950 if commitmsg is None:
952 951 commitmsg = ctx.description()
953 952 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
954 953 extra = {'rebase_source': ctx.hex()}
955 954 if extrafn:
956 955 extrafn(ctx, extra)
957 956
958 957 destphase = max(ctx.phase(), phases.draft)
959 958 overrides = {('phases', 'new-commit'): destphase}
960 959 with repo.ui.configoverride(overrides, 'rebase'):
961 960 if keepbranch:
962 961 repo.ui.setconfig('ui', 'allowemptycommit', True)
963 962 # Commit might fail if unresolved files exist
964 963 if date is None:
965 964 date = ctx.date()
966 965 newnode = repo.commit(text=commitmsg, user=ctx.user(),
967 966 date=date, extra=extra, editor=editor)
968 967
969 968 repo.dirstate.setbranch(repo[newnode].branch())
970 969 return newnode
971 970
972 971 def rebasenode(repo, rev, p1, base, state, collapse, dest):
973 972 'Rebase a single revision rev on top of p1 using base as merge ancestor'
974 973 # Merge phase
975 974 # Update to destination and merge it with local
976 975 if repo['.'].rev() != p1:
977 976 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
978 977 mergemod.update(repo, p1, False, True)
979 978 else:
980 979 repo.ui.debug(" already in destination\n")
981 980 repo.dirstate.write(repo.currenttransaction())
982 981 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
983 982 if base is not None:
984 983 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
985 984 # When collapsing in-place, the parent is the common ancestor, we
986 985 # have to allow merging with it.
987 986 wctx = repo[None]
988 987 stats = mergemod.update(repo, rev, True, True, base, collapse,
989 988 labels=['dest', 'source'])
990 989 if collapse:
991 990 copies.duplicatecopies(repo, wctx, rev, dest)
992 991 else:
993 992 # If we're not using --collapse, we need to
994 993 # duplicate copies between the revision we're
995 994 # rebasing and its first parent, but *not*
996 995 # duplicate any copies that have already been
997 996 # performed in the destination.
998 997 p1rev = repo[rev].p1().rev()
999 998 copies.duplicatecopies(repo, wctx, rev, p1rev, skiprev=dest)
1000 999 return stats
1001 1000
1002 1001 def adjustdest(repo, rev, destmap, state, skipped):
1003 1002 """adjust rebase destination given the current rebase state
1004 1003
1005 1004 rev is what is being rebased. Return a list of two revs, which are the
1006 1005 adjusted destinations for rev's p1 and p2, respectively. If a parent is
1007 1006 nullrev, return dest without adjustment for it.
1008 1007
1009 1008 For example, when doing rebasing B+E to F, C to G, rebase will first move B
1010 1009 to B1, and E's destination will be adjusted from F to B1.
1011 1010
1012 1011 B1 <- written during rebasing B
1013 1012 |
1014 1013 F <- original destination of B, E
1015 1014 |
1016 1015 | E <- rev, which is being rebased
1017 1016 | |
1018 1017 | D <- prev, one parent of rev being checked
1019 1018 | |
1020 1019 | x <- skipped, ex. no successor or successor in (::dest)
1021 1020 | |
1022 1021 | C <- rebased as C', different destination
1023 1022 | |
1024 1023 | B <- rebased as B1 C'
1025 1024 |/ |
1026 1025 A G <- destination of C, different
1027 1026
1028 1027 Another example about merge changeset, rebase -r C+G+H -d K, rebase will
1029 1028 first move C to C1, G to G1, and when it's checking H, the adjusted
1030 1029 destinations will be [C1, G1].
1031 1030
1032 1031 H C1 G1
1033 1032 /| | /
1034 1033 F G |/
1035 1034 K | | -> K
1036 1035 | C D |
1037 1036 | |/ |
1038 1037 | B | ...
1039 1038 |/ |/
1040 1039 A A
1041 1040
1042 1041 Besides, adjust dest according to existing rebase information. For example,
1043 1042
1044 1043 B C D B needs to be rebased on top of C, C needs to be rebased on top
1045 1044 \|/ of D. We will rebase C first.
1046 1045 A
1047 1046
1048 1047 C' After rebasing C, when considering B's destination, use C'
1049 1048 | instead of the original C.
1050 1049 B D
1051 1050 \ /
1052 1051 A
1053 1052 """
1054 1053 # pick already rebased revs with same dest from state as interesting source
1055 1054 dest = destmap[rev]
1056 1055 source = [s for s, d in state.items()
1057 1056 if d > 0 and destmap[s] == dest and s not in skipped]
1058 1057
1059 1058 result = []
1060 1059 for prev in repo.changelog.parentrevs(rev):
1061 1060 adjusted = dest
1062 1061 if prev != nullrev:
1063 1062 candidate = repo.revs('max(%ld and (::%d))', source, prev).first()
1064 1063 if candidate is not None:
1065 1064 adjusted = state[candidate]
1066 1065 if adjusted == dest and dest in state:
1067 1066 adjusted = state[dest]
1068 1067 if adjusted == revtodo:
1069 1068 # sortsource should produce an order that makes this impossible
1070 1069 raise error.ProgrammingError(
1071 1070 'rev %d should be rebased already at this time' % dest)
1072 1071 result.append(adjusted)
1073 1072 return result
1074 1073
1075 1074 def _checkobsrebase(repo, ui, rebaseobsrevs, rebaseobsskipped):
1076 1075 """
1077 1076 Abort if rebase will create divergence or rebase is noop because of markers
1078 1077
1079 1078 `rebaseobsrevs`: set of obsolete revision in source
1080 1079 `rebaseobsskipped`: set of revisions from source skipped because they have
1081 1080 successors in destination
1082 1081 """
1083 1082 # Obsolete node with successors not in dest leads to divergence
1084 1083 divergenceok = ui.configbool('experimental',
1085 1084 'evolution.allowdivergence')
1086 1085 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
1087 1086
1088 1087 if divergencebasecandidates and not divergenceok:
1089 1088 divhashes = (str(repo[r])
1090 1089 for r in divergencebasecandidates)
1091 1090 msg = _("this rebase will cause "
1092 1091 "divergences from: %s")
1093 1092 h = _("to force the rebase please set "
1094 1093 "experimental.evolution.allowdivergence=True")
1095 1094 raise error.Abort(msg % (",".join(divhashes),), hint=h)
1096 1095
1097 1096 def successorrevs(unfi, rev):
1098 1097 """yield revision numbers for successors of rev"""
1099 1098 assert unfi.filtername is None
1100 1099 nodemap = unfi.changelog.nodemap
1101 1100 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
1102 1101 if s in nodemap:
1103 1102 yield nodemap[s]
1104 1103
1105 1104 def defineparents(repo, rev, destmap, state, skipped, obsskipped):
1106 1105 """Return new parents and optionally a merge base for rev being rebased
1107 1106
1108 1107 The destination specified by "dest" cannot always be used directly because
1109 1108 previously rebase result could affect destination. For example,
1110 1109
1111 1110 D E rebase -r C+D+E -d B
1112 1111 |/ C will be rebased to C'
1113 1112 B C D's new destination will be C' instead of B
1114 1113 |/ E's new destination will be C' instead of B
1115 1114 A
1116 1115
1117 1116 The new parents of a merge is slightly more complicated. See the comment
1118 1117 block below.
1119 1118 """
1120 1119 # use unfiltered changelog since successorrevs may return filtered nodes
1121 1120 assert repo.filtername is None
1122 1121 cl = repo.changelog
1123 1122 def isancestor(a, b):
1124 1123 # take revision numbers instead of nodes
1125 1124 if a == b:
1126 1125 return True
1127 1126 elif a > b:
1128 1127 return False
1129 1128 return cl.isancestor(cl.node(a), cl.node(b))
1130 1129
1131 1130 dest = destmap[rev]
1132 1131 oldps = repo.changelog.parentrevs(rev) # old parents
1133 1132 newps = [nullrev, nullrev] # new parents
1134 1133 dests = adjustdest(repo, rev, destmap, state, skipped)
1135 1134 bases = list(oldps) # merge base candidates, initially just old parents
1136 1135
1137 1136 if all(r == nullrev for r in oldps[1:]):
1138 1137 # For non-merge changeset, just move p to adjusted dest as requested.
1139 1138 newps[0] = dests[0]
1140 1139 else:
1141 1140 # For merge changeset, if we move p to dests[i] unconditionally, both
1142 1141 # parents may change and the end result looks like "the merge loses a
1143 1142 # parent", which is a surprise. This is a limit because "--dest" only
1144 1143 # accepts one dest per src.
1145 1144 #
1146 1145 # Therefore, only move p with reasonable conditions (in this order):
1147 1146 # 1. use dest, if dest is a descendent of (p or one of p's successors)
1148 1147 # 2. use p's rebased result, if p is rebased (state[p] > 0)
1149 1148 #
1150 1149 # Comparing with adjustdest, the logic here does some additional work:
1151 1150 # 1. decide which parents will not be moved towards dest
1152 1151 # 2. if the above decision is "no", should a parent still be moved
1153 1152 # because it was rebased?
1154 1153 #
1155 1154 # For example:
1156 1155 #
1157 1156 # C # "rebase -r C -d D" is an error since none of the parents
1158 1157 # /| # can be moved. "rebase -r B+C -d D" will move C's parent
1159 1158 # A B D # B (using rule "2."), since B will be rebased.
1160 1159 #
1161 1160 # The loop tries to be not rely on the fact that a Mercurial node has
1162 1161 # at most 2 parents.
1163 1162 for i, p in enumerate(oldps):
1164 1163 np = p # new parent
1165 1164 if any(isancestor(x, dests[i]) for x in successorrevs(repo, p)):
1166 1165 np = dests[i]
1167 1166 elif p in state and state[p] > 0:
1168 1167 np = state[p]
1169 1168
1170 1169 # "bases" only record "special" merge bases that cannot be
1171 1170 # calculated from changelog DAG (i.e. isancestor(p, np) is False).
1172 1171 # For example:
1173 1172 #
1174 1173 # B' # rebase -s B -d D, when B was rebased to B'. dest for C
1175 1174 # | C # is B', but merge base for C is B, instead of
1176 1175 # D | # changelog.ancestor(C, B') == A. If changelog DAG and
1177 1176 # | B # "state" edges are merged (so there will be an edge from
1178 1177 # |/ # B to B'), the merge base is still ancestor(C, B') in
1179 1178 # A # the merged graph.
1180 1179 #
1181 1180 # Also see https://bz.mercurial-scm.org/show_bug.cgi?id=1950#c8
1182 1181 # which uses "virtual null merge" to explain this situation.
1183 1182 if isancestor(p, np):
1184 1183 bases[i] = nullrev
1185 1184
1186 1185 # If one parent becomes an ancestor of the other, drop the ancestor
1187 1186 for j, x in enumerate(newps[:i]):
1188 1187 if x == nullrev:
1189 1188 continue
1190 1189 if isancestor(np, x): # CASE-1
1191 1190 np = nullrev
1192 1191 elif isancestor(x, np): # CASE-2
1193 1192 newps[j] = np
1194 1193 np = nullrev
1195 1194 # New parents forming an ancestor relationship does not
1196 1195 # mean the old parents have a similar relationship. Do not
1197 1196 # set bases[x] to nullrev.
1198 1197 bases[j], bases[i] = bases[i], bases[j]
1199 1198
1200 1199 newps[i] = np
1201 1200
1202 1201 # "rebasenode" updates to new p1, and the old p1 will be used as merge
1203 1202 # base. If only p2 changes, merging using unchanged p1 as merge base is
1204 1203 # suboptimal. Therefore swap parents to make the merge sane.
1205 1204 if newps[1] != nullrev and oldps[0] == newps[0]:
1206 1205 assert len(newps) == 2 and len(oldps) == 2
1207 1206 newps.reverse()
1208 1207 bases.reverse()
1209 1208
1210 1209 # No parent change might be an error because we fail to make rev a
1211 1210 # descendent of requested dest. This can happen, for example:
1212 1211 #
1213 1212 # C # rebase -r C -d D
1214 1213 # /| # None of A and B will be changed to D and rebase fails.
1215 1214 # A B D
1216 1215 if set(newps) == set(oldps) and dest not in newps:
1217 1216 raise error.Abort(_('cannot rebase %d:%s without '
1218 1217 'moving at least one of its parents')
1219 1218 % (rev, repo[rev]))
1220 1219
1221 1220 # Source should not be ancestor of dest. The check here guarantees it's
1222 1221 # impossible. With multi-dest, the initial check does not cover complex
1223 1222 # cases since we don't have abstractions to dry-run rebase cheaply.
1224 1223 if any(p != nullrev and isancestor(rev, p) for p in newps):
1225 1224 raise error.Abort(_('source is ancestor of destination'))
1226 1225
1227 1226 # "rebasenode" updates to new p1, use the corresponding merge base.
1228 1227 if bases[0] != nullrev:
1229 1228 base = bases[0]
1230 1229 else:
1231 1230 base = None
1232 1231
1233 1232 # Check if the merge will contain unwanted changes. That may happen if
1234 1233 # there are multiple special (non-changelog ancestor) merge bases, which
1235 1234 # cannot be handled well by the 3-way merge algorithm. For example:
1236 1235 #
1237 1236 # F
1238 1237 # /|
1239 1238 # D E # "rebase -r D+E+F -d Z", when rebasing F, if "D" was chosen
1240 1239 # | | # as merge base, the difference between D and F will include
1241 1240 # B C # C, so the rebased F will contain C surprisingly. If "E" was
1242 1241 # |/ # chosen, the rebased F will contain B.
1243 1242 # A Z
1244 1243 #
1245 1244 # But our merge base candidates (D and E in above case) could still be
1246 1245 # better than the default (ancestor(F, Z) == null). Therefore still
1247 1246 # pick one (so choose p1 above).
1248 1247 if sum(1 for b in bases if b != nullrev) > 1:
1249 1248 unwanted = [None, None] # unwanted[i]: unwanted revs if choose bases[i]
1250 1249 for i, base in enumerate(bases):
1251 1250 if base == nullrev:
1252 1251 continue
1253 1252 # Revisions in the side (not chosen as merge base) branch that
1254 1253 # might contain "surprising" contents
1255 1254 siderevs = list(repo.revs('((%ld-%d) %% (%d+%d))',
1256 1255 bases, base, base, dest))
1257 1256
1258 1257 # If those revisions are covered by rebaseset, the result is good.
1259 1258 # A merge in rebaseset would be considered to cover its ancestors.
1260 1259 if siderevs:
1261 1260 rebaseset = [r for r, d in state.items()
1262 1261 if d > 0 and r not in obsskipped]
1263 1262 merges = [r for r in rebaseset
1264 1263 if cl.parentrevs(r)[1] != nullrev]
1265 1264 unwanted[i] = list(repo.revs('%ld - (::%ld) - %ld',
1266 1265 siderevs, merges, rebaseset))
1267 1266
1268 1267 # Choose a merge base that has a minimal number of unwanted revs.
1269 1268 l, i = min((len(revs), i)
1270 1269 for i, revs in enumerate(unwanted) if revs is not None)
1271 1270 base = bases[i]
1272 1271
1273 1272 # newps[0] should match merge base if possible. Currently, if newps[i]
1274 1273 # is nullrev, the only case is newps[i] and newps[j] (j < i), one is
1275 1274 # the other's ancestor. In that case, it's fine to not swap newps here.
1276 1275 # (see CASE-1 and CASE-2 above)
1277 1276 if i != 0 and newps[i] != nullrev:
1278 1277 newps[0], newps[i] = newps[i], newps[0]
1279 1278
1280 1279 # The merge will include unwanted revisions. Abort now. Revisit this if
1281 1280 # we have a more advanced merge algorithm that handles multiple bases.
1282 1281 if l > 0:
1283 1282 unwanteddesc = _(' or ').join(
1284 1283 (', '.join('%d:%s' % (r, repo[r]) for r in revs)
1285 1284 for revs in unwanted if revs is not None))
1286 1285 raise error.Abort(
1287 1286 _('rebasing %d:%s will include unwanted changes from %s')
1288 1287 % (rev, repo[rev], unwanteddesc))
1289 1288
1290 1289 repo.ui.debug(" future parents are %d and %d\n" % tuple(newps))
1291 1290
1292 1291 return newps[0], newps[1], base
1293 1292
1294 1293 def isagitpatch(repo, patchname):
1295 1294 'Return true if the given patch is in git format'
1296 1295 mqpatch = os.path.join(repo.mq.path, patchname)
1297 1296 for line in patch.linereader(file(mqpatch, 'rb')):
1298 1297 if line.startswith('diff --git'):
1299 1298 return True
1300 1299 return False
1301 1300
1302 1301 def updatemq(repo, state, skipped, **opts):
1303 1302 'Update rebased mq patches - finalize and then import them'
1304 1303 mqrebase = {}
1305 1304 mq = repo.mq
1306 1305 original_series = mq.fullseries[:]
1307 1306 skippedpatches = set()
1308 1307
1309 1308 for p in mq.applied:
1310 1309 rev = repo[p.node].rev()
1311 1310 if rev in state:
1312 1311 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1313 1312 (rev, p.name))
1314 1313 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1315 1314 else:
1316 1315 # Applied but not rebased, not sure this should happen
1317 1316 skippedpatches.add(p.name)
1318 1317
1319 1318 if mqrebase:
1320 1319 mq.finish(repo, mqrebase.keys())
1321 1320
1322 1321 # We must start import from the newest revision
1323 1322 for rev in sorted(mqrebase, reverse=True):
1324 1323 if rev not in skipped:
1325 1324 name, isgit = mqrebase[rev]
1326 1325 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1327 1326 (name, state[rev], repo[state[rev]]))
1328 1327 mq.qimport(repo, (), patchname=name, git=isgit,
1329 1328 rev=[str(state[rev])])
1330 1329 else:
1331 1330 # Rebased and skipped
1332 1331 skippedpatches.add(mqrebase[rev][0])
1333 1332
1334 1333 # Patches were either applied and rebased and imported in
1335 1334 # order, applied and removed or unapplied. Discard the removed
1336 1335 # ones while preserving the original series order and guards.
1337 1336 newseries = [s for s in original_series
1338 1337 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1339 1338 mq.fullseries[:] = newseries
1340 1339 mq.seriesdirty = True
1341 1340 mq.savedirty()
1342 1341
1343 1342 def storecollapsemsg(repo, collapsemsg):
1344 1343 'Store the collapse message to allow recovery'
1345 1344 collapsemsg = collapsemsg or ''
1346 1345 f = repo.vfs("last-message.txt", "w")
1347 1346 f.write("%s\n" % collapsemsg)
1348 1347 f.close()
1349 1348
1350 1349 def clearcollapsemsg(repo):
1351 1350 'Remove collapse message file'
1352 1351 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1353 1352
1354 1353 def restorecollapsemsg(repo, isabort):
1355 1354 'Restore previously stored collapse message'
1356 1355 try:
1357 1356 f = repo.vfs("last-message.txt")
1358 1357 collapsemsg = f.readline().strip()
1359 1358 f.close()
1360 1359 except IOError as err:
1361 1360 if err.errno != errno.ENOENT:
1362 1361 raise
1363 1362 if isabort:
1364 1363 # Oh well, just abort like normal
1365 1364 collapsemsg = ''
1366 1365 else:
1367 1366 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1368 1367 return collapsemsg
1369 1368
1370 1369 def clearstatus(repo):
1371 1370 'Remove the status files'
1372 1371 # Make sure the active transaction won't write the state file
1373 1372 tr = repo.currenttransaction()
1374 1373 if tr:
1375 1374 tr.removefilegenerator('rebasestate')
1376 1375 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1377 1376
1378 1377 def needupdate(repo, state):
1379 1378 '''check whether we should `update --clean` away from a merge, or if
1380 1379 somehow the working dir got forcibly updated, e.g. by older hg'''
1381 1380 parents = [p.rev() for p in repo[None].parents()]
1382 1381
1383 1382 # Are we in a merge state at all?
1384 1383 if len(parents) < 2:
1385 1384 return False
1386 1385
1387 1386 # We should be standing on the first as-of-yet unrebased commit.
1388 1387 firstunrebased = min([old for old, new in state.iteritems()
1389 1388 if new == nullrev])
1390 1389 if firstunrebased in parents:
1391 1390 return True
1392 1391
1393 1392 return False
1394 1393
1395 1394 def abort(repo, originalwd, destmap, state, activebookmark=None):
1396 1395 '''Restore the repository to its original state. Additional args:
1397 1396
1398 1397 activebookmark: the name of the bookmark that should be active after the
1399 1398 restore'''
1400 1399
1401 1400 try:
1402 1401 # If the first commits in the rebased set get skipped during the rebase,
1403 1402 # their values within the state mapping will be the dest rev id. The
1404 1403 # dstates list must must not contain the dest rev (issue4896)
1405 1404 dstates = [s for r, s in state.items() if s >= 0 and s != destmap[r]]
1406 1405 immutable = [d for d in dstates if not repo[d].mutable()]
1407 1406 cleanup = True
1408 1407 if immutable:
1409 1408 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1410 1409 % ', '.join(str(repo[r]) for r in immutable),
1411 1410 hint=_("see 'hg help phases' for details"))
1412 1411 cleanup = False
1413 1412
1414 1413 descendants = set()
1415 1414 if dstates:
1416 1415 descendants = set(repo.changelog.descendants(dstates))
1417 1416 if descendants - set(dstates):
1418 1417 repo.ui.warn(_("warning: new changesets detected on destination "
1419 1418 "branch, can't strip\n"))
1420 1419 cleanup = False
1421 1420
1422 1421 if cleanup:
1423 1422 shouldupdate = False
1424 1423 rebased = [s for r, s in state.items()
1425 1424 if s >= 0 and s != destmap[r]]
1426 1425 if rebased:
1427 1426 strippoints = [
1428 1427 c.node() for c in repo.set('roots(%ld)', rebased)]
1429 1428
1430 1429 updateifonnodes = set(rebased)
1431 1430 updateifonnodes.update(destmap.values())
1432 1431 updateifonnodes.add(originalwd)
1433 1432 shouldupdate = repo['.'].rev() in updateifonnodes
1434 1433
1435 1434 # Update away from the rebase if necessary
1436 1435 if shouldupdate or needupdate(repo, state):
1437 1436 mergemod.update(repo, originalwd, False, True)
1438 1437
1439 1438 # Strip from the first rebased revision
1440 1439 if rebased:
1441 1440 # no backup of rebased cset versions needed
1442 1441 repair.strip(repo.ui, repo, strippoints)
1443 1442
1444 1443 if activebookmark and activebookmark in repo._bookmarks:
1445 1444 bookmarks.activate(repo, activebookmark)
1446 1445
1447 1446 finally:
1448 1447 clearstatus(repo)
1449 1448 clearcollapsemsg(repo)
1450 1449 repo.ui.warn(_('rebase aborted\n'))
1451 1450 return 0
1452 1451
1453 1452 def sortsource(destmap):
1454 1453 """yield source revisions in an order that we only rebase things once
1455 1454
1456 1455 If source and destination overlaps, we should filter out revisions
1457 1456 depending on other revisions which hasn't been rebased yet.
1458 1457
1459 1458 Yield a sorted list of revisions each time.
1460 1459
1461 1460 For example, when rebasing A to B, B to C. This function yields [B], then
1462 1461 [A], indicating B needs to be rebased first.
1463 1462
1464 1463 Raise if there is a cycle so the rebase is impossible.
1465 1464 """
1466 1465 srcset = set(destmap)
1467 1466 while srcset:
1468 1467 srclist = sorted(srcset)
1469 1468 result = []
1470 1469 for r in srclist:
1471 1470 if destmap[r] not in srcset:
1472 1471 result.append(r)
1473 1472 if not result:
1474 1473 raise error.Abort(_('source and destination form a cycle'))
1475 1474 srcset -= set(result)
1476 1475 yield result
1477 1476
1478 1477 def buildstate(repo, destmap, collapse):
1479 1478 '''Define which revisions are going to be rebased and where
1480 1479
1481 1480 repo: repo
1482 1481 destmap: {srcrev: destrev}
1483 1482 '''
1484 1483 rebaseset = destmap.keys()
1485 1484 originalwd = repo['.'].rev()
1486 1485
1487 1486 # This check isn't strictly necessary, since mq detects commits over an
1488 1487 # applied patch. But it prevents messing up the working directory when
1489 1488 # a partially completed rebase is blocked by mq.
1490 1489 if 'qtip' in repo.tags():
1491 1490 mqapplied = set(repo[s.node].rev() for s in repo.mq.applied)
1492 1491 if set(destmap.values()) & mqapplied:
1493 1492 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1494 1493
1495 1494 # Get "cycle" error early by exhausting the generator.
1496 1495 sortedsrc = list(sortsource(destmap)) # a list of sorted revs
1497 1496 if not sortedsrc:
1498 1497 raise error.Abort(_('no matching revisions'))
1499 1498
1500 1499 # Only check the first batch of revisions to rebase not depending on other
1501 1500 # rebaseset. This means "source is ancestor of destination" for the second
1502 1501 # (and following) batches of revisions are not checked here. We rely on
1503 1502 # "defineparents" to do that check.
1504 1503 roots = list(repo.set('roots(%ld)', sortedsrc[0]))
1505 1504 if not roots:
1506 1505 raise error.Abort(_('no matching revisions'))
1507 1506 roots.sort()
1508 1507 state = dict.fromkeys(rebaseset, revtodo)
1509 1508 emptyrebase = (len(sortedsrc) == 1)
1510 1509 for root in roots:
1511 1510 dest = repo[destmap[root.rev()]]
1512 1511 commonbase = root.ancestor(dest)
1513 1512 if commonbase == root:
1514 1513 raise error.Abort(_('source is ancestor of destination'))
1515 1514 if commonbase == dest:
1516 1515 wctx = repo[None]
1517 1516 if dest == wctx.p1():
1518 1517 # when rebasing to '.', it will use the current wd branch name
1519 1518 samebranch = root.branch() == wctx.branch()
1520 1519 else:
1521 1520 samebranch = root.branch() == dest.branch()
1522 1521 if not collapse and samebranch and dest in root.parents():
1523 1522 # mark the revision as done by setting its new revision
1524 1523 # equal to its old (current) revisions
1525 1524 state[root.rev()] = root.rev()
1526 1525 repo.ui.debug('source is a child of destination\n')
1527 1526 continue
1528 1527
1529 1528 emptyrebase = False
1530 1529 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1531 1530 if emptyrebase:
1532 1531 return None
1533 1532 for rev in sorted(state):
1534 1533 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1535 1534 # if all parents of this revision are done, then so is this revision
1536 1535 if parents and all((state.get(p) == p for p in parents)):
1537 1536 state[rev] = rev
1538 1537 return originalwd, destmap, state
1539 1538
1540 1539 def clearrebased(ui, repo, destmap, state, skipped, collapsedas=None,
1541 1540 keepf=False, fm=None):
1542 1541 """dispose of rebased revision at the end of the rebase
1543 1542
1544 1543 If `collapsedas` is not None, the rebase was a collapse whose result if the
1545 1544 `collapsedas` node.
1546 1545
1547 1546 If `keepf` is not True, the rebase has --keep set and no nodes should be
1548 1547 removed (but bookmarks still need to be moved).
1549 1548 """
1550 1549 tonode = repo.changelog.node
1551 1550 replacements = {}
1552 1551 moves = {}
1553 1552 for rev, newrev in sorted(state.items()):
1554 1553 if newrev >= 0 and newrev != rev:
1555 1554 oldnode = tonode(rev)
1556 1555 newnode = collapsedas or tonode(newrev)
1557 1556 moves[oldnode] = newnode
1558 1557 if not keepf:
1559 1558 if rev in skipped:
1560 1559 succs = ()
1561 1560 else:
1562 1561 succs = (newnode,)
1563 1562 replacements[oldnode] = succs
1564 1563 scmutil.cleanupnodes(repo, replacements, 'rebase', moves)
1565 1564 if fm:
1566 nodechanges = {hex(oldn): [hex(n) for n in newn]
1567 for oldn, newn in replacements.iteritems()}
1565 hf = fm.hexfunc
1566 fl = fm.formatlist
1567 fd = fm.formatdict
1568 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1569 for oldn, newn in replacements.iteritems()},
1570 key="oldnode", value="newnodes")
1568 1571 fm.data(nodechanges=nodechanges)
1569 1572
1570 1573 def pullrebase(orig, ui, repo, *args, **opts):
1571 1574 'Call rebase after pull if the latter has been invoked with --rebase'
1572 1575 ret = None
1573 1576 if opts.get(r'rebase'):
1574 1577 if ui.configbool('commands', 'rebase.requiredest'):
1575 1578 msg = _('rebase destination required by configuration')
1576 1579 hint = _('use hg pull followed by hg rebase -d DEST')
1577 1580 raise error.Abort(msg, hint=hint)
1578 1581
1579 1582 with repo.wlock(), repo.lock():
1580 1583 if opts.get(r'update'):
1581 1584 del opts[r'update']
1582 1585 ui.debug('--update and --rebase are not compatible, ignoring '
1583 1586 'the update flag\n')
1584 1587
1585 1588 cmdutil.checkunfinished(repo)
1586 1589 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1587 1590 'please commit or shelve your changes first'))
1588 1591
1589 1592 revsprepull = len(repo)
1590 1593 origpostincoming = commands.postincoming
1591 1594 def _dummy(*args, **kwargs):
1592 1595 pass
1593 1596 commands.postincoming = _dummy
1594 1597 try:
1595 1598 ret = orig(ui, repo, *args, **opts)
1596 1599 finally:
1597 1600 commands.postincoming = origpostincoming
1598 1601 revspostpull = len(repo)
1599 1602 if revspostpull > revsprepull:
1600 1603 # --rev option from pull conflict with rebase own --rev
1601 1604 # dropping it
1602 1605 if r'rev' in opts:
1603 1606 del opts[r'rev']
1604 1607 # positional argument from pull conflicts with rebase's own
1605 1608 # --source.
1606 1609 if r'source' in opts:
1607 1610 del opts[r'source']
1608 1611 # revsprepull is the len of the repo, not revnum of tip.
1609 1612 destspace = list(repo.changelog.revs(start=revsprepull))
1610 1613 opts[r'_destspace'] = destspace
1611 1614 try:
1612 1615 rebase(ui, repo, **opts)
1613 1616 except error.NoMergeDestAbort:
1614 1617 # we can maybe update instead
1615 1618 rev, _a, _b = destutil.destupdate(repo)
1616 1619 if rev == repo['.'].rev():
1617 1620 ui.status(_('nothing to rebase\n'))
1618 1621 else:
1619 1622 ui.status(_('nothing to rebase - updating instead\n'))
1620 1623 # not passing argument to get the bare update behavior
1621 1624 # with warning and trumpets
1622 1625 commands.update(ui, repo)
1623 1626 else:
1624 1627 if opts.get(r'tool'):
1625 1628 raise error.Abort(_('--tool can only be used with --rebase'))
1626 1629 ret = orig(ui, repo, *args, **opts)
1627 1630
1628 1631 return ret
1629 1632
1630 1633 def _filterobsoleterevs(repo, revs):
1631 1634 """returns a set of the obsolete revisions in revs"""
1632 1635 return set(r for r in revs if repo[r].obsolete())
1633 1636
1634 1637 def _computeobsoletenotrebased(repo, rebaseobsrevs, destmap):
1635 1638 """Return (obsoletenotrebased, obsoletewithoutsuccessorindestination).
1636 1639
1637 1640 `obsoletenotrebased` is a mapping mapping obsolete => successor for all
1638 1641 obsolete nodes to be rebased given in `rebaseobsrevs`.
1639 1642
1640 1643 `obsoletewithoutsuccessorindestination` is a set with obsolete revisions
1641 1644 without a successor in destination.
1642 1645 """
1643 1646 obsoletenotrebased = {}
1644 1647 obsoletewithoutsuccessorindestination = set([])
1645 1648
1646 1649 assert repo.filtername is None
1647 1650 cl = repo.changelog
1648 1651 nodemap = cl.nodemap
1649 1652 for srcrev in rebaseobsrevs:
1650 1653 srcnode = cl.node(srcrev)
1651 1654 destnode = cl.node(destmap[srcrev])
1652 1655 # XXX: more advanced APIs are required to handle split correctly
1653 1656 successors = list(obsutil.allsuccessors(repo.obsstore, [srcnode]))
1654 1657 if len(successors) == 1:
1655 1658 # obsutil.allsuccessors includes node itself. When the list only
1656 1659 # contains one element, it means there are no successors.
1657 1660 obsoletenotrebased[srcrev] = None
1658 1661 else:
1659 1662 for succnode in successors:
1660 1663 if succnode == srcnode or succnode not in nodemap:
1661 1664 continue
1662 1665 if cl.isancestor(succnode, destnode):
1663 1666 obsoletenotrebased[srcrev] = nodemap[succnode]
1664 1667 break
1665 1668 else:
1666 1669 # If 'srcrev' has a successor in rebase set but none in
1667 1670 # destination (which would be catched above), we shall skip it
1668 1671 # and its descendants to avoid divergence.
1669 1672 if any(nodemap[s] in destmap
1670 1673 for s in successors if s != srcnode):
1671 1674 obsoletewithoutsuccessorindestination.add(srcrev)
1672 1675
1673 1676 return obsoletenotrebased, obsoletewithoutsuccessorindestination
1674 1677
1675 1678 def summaryhook(ui, repo):
1676 1679 if not repo.vfs.exists('rebasestate'):
1677 1680 return
1678 1681 try:
1679 1682 rbsrt = rebaseruntime(repo, ui, {})
1680 1683 rbsrt.restorestatus()
1681 1684 state = rbsrt.state
1682 1685 except error.RepoLookupError:
1683 1686 # i18n: column positioning for "hg summary"
1684 1687 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1685 1688 ui.write(msg)
1686 1689 return
1687 1690 numrebased = len([i for i in state.itervalues() if i >= 0])
1688 1691 # i18n: column positioning for "hg summary"
1689 1692 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1690 1693 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1691 1694 ui.label(_('%d remaining'), 'rebase.remaining') %
1692 1695 (len(state) - numrebased)))
1693 1696
1694 1697 def uisetup(ui):
1695 1698 #Replace pull with a decorator to provide --rebase option
1696 1699 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1697 1700 entry[1].append(('', 'rebase', None,
1698 1701 _("rebase working directory to branch head")))
1699 1702 entry[1].append(('t', 'tool', '',
1700 1703 _("specify merge tool for rebase")))
1701 1704 cmdutil.summaryhooks.add('rebase', summaryhook)
1702 1705 cmdutil.unfinishedstates.append(
1703 1706 ['rebasestate', False, False, _('rebase in progress'),
1704 1707 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1705 1708 cmdutil.afterresolvedstates.append(
1706 1709 ['rebasestate', _('hg rebase --continue')])
@@ -1,58 +1,57
1 1 Testing templating for rebase command
2 2
3 3 Setup
4 4
5 5 $ cat >> $HGRCPATH <<EOF
6 6 > [extensions]
7 7 > rebase=
8 8 > [experimental]
9 9 > evolution=createmarkers
10 10 > EOF
11 11
12 12 $ hg init repo
13 13 $ cd repo
14 14 $ for ch in a b c d; do echo foo > $ch; hg commit -Aqm "Added "$ch; done
15 15
16 16 $ hg log -G -T "{rev}:{node|short} {desc}"
17 17 @ 3:62615734edd5 Added d
18 18 |
19 19 o 2:28ad74487de9 Added c
20 20 |
21 21 o 1:29becc82797a Added b
22 22 |
23 23 o 0:18d04c59bb5d Added a
24 24
25 25 Getting the JSON output for nodechanges
26 26
27 27 $ hg rebase -s 2 -d 0 -q -Tjson
28 28 [
29 29 {
30 30 "nodechanges": {"28ad74487de9599d00d81085be739c61fc340652": ["849767420fd5519cf0026232411a943ed03cc9fb"], "62615734edd52f06b6fb9c2beb429e4fe30d57b8": ["df21b32134ba85d86bca590cbe9b8b7cbc346c53"]}
31 31 }
32 32 ]
33 33
34 34 $ hg log -G -T "{rev}:{node|short} {desc}"
35 35 @ 5:df21b32134ba Added d
36 36 |
37 37 o 4:849767420fd5 Added c
38 38 |
39 39 | o 1:29becc82797a Added b
40 40 |/
41 41 o 0:18d04c59bb5d Added a
42 42
43 43 $ hg rebase -s 1 -d 5 -q -T "{nodechanges|json}"
44 44 {"29becc82797a4bc11ec8880b58eaecd2ab3e7760": ["d9d6773efc831c274eace04bc13e8e6412517139"]} (no-eol)
45 45
46 46 $ hg log -G -T "{rev}:{node|short} {desc}"
47 47 o 6:d9d6773efc83 Added b
48 48 |
49 49 @ 5:df21b32134ba Added d
50 50 |
51 51 o 4:849767420fd5 Added c
52 52 |
53 53 o 0:18d04c59bb5d Added a
54 54
55 55
56 56 $ hg rebase -s 6 -d 4 -q -T "{nodechanges % '{oldnode}:{newnodes % ' {node} '}'}"
57 d9d6773efc831c274eace04bc13e8e6412517139: f48cd65c6dc3d2acb55da54402a5b029546e546f (no-eol) (false !)
58 d9d6773efc831c274eace04bc13e8e6412517139 (no-eol)
57 d9d6773efc831c274eace04bc13e8e6412517139: f48cd65c6dc3d2acb55da54402a5b029546e546f (no-eol)
General Comments 0
You need to be logged in to leave comments. Login now