##// END OF EJS Templates
rebase: clean up rebasestate from active transaction...
Jun Wu -
r33056:2312e70c default
parent child Browse files
Show More
@@ -1,1528 +1,1532
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 24 hex,
25 25 nullid,
26 26 nullrev,
27 27 short,
28 28 )
29 29 from mercurial import (
30 30 bookmarks,
31 31 cmdutil,
32 32 commands,
33 33 copies,
34 34 destutil,
35 35 dirstateguard,
36 36 error,
37 37 extensions,
38 38 hg,
39 39 lock,
40 40 merge as mergemod,
41 41 mergeutil,
42 42 obsolete,
43 43 patch,
44 44 phases,
45 45 registrar,
46 46 repair,
47 47 repoview,
48 48 revset,
49 49 scmutil,
50 50 smartset,
51 51 util,
52 52 )
53 53
54 54 release = lock.release
55 55 templateopts = cmdutil.templateopts
56 56
57 57 # The following constants are used throughout the rebase module. The ordering of
58 58 # their values must be maintained.
59 59
60 60 # Indicates that a revision needs to be rebased
61 61 revtodo = -1
62 62 nullmerge = -2
63 63 revignored = -3
64 64 # successor in rebase destination
65 65 revprecursor = -4
66 66 # plain prune (no successor)
67 67 revpruned = -5
68 68 revskipped = (revignored, revprecursor, revpruned)
69 69
70 70 cmdtable = {}
71 71 command = registrar.command(cmdtable)
72 72 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
73 73 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
74 74 # be specifying the version(s) of Mercurial they are tested with, or
75 75 # leave the attribute unspecified.
76 76 testedwith = 'ships-with-hg-core'
77 77
78 78 def _nothingtorebase():
79 79 return 1
80 80
81 81 def _savegraft(ctx, extra):
82 82 s = ctx.extra().get('source', None)
83 83 if s is not None:
84 84 extra['source'] = s
85 85 s = ctx.extra().get('intermediate-source', None)
86 86 if s is not None:
87 87 extra['intermediate-source'] = s
88 88
89 89 def _savebranch(ctx, extra):
90 90 extra['branch'] = ctx.branch()
91 91
92 92 def _makeextrafn(copiers):
93 93 """make an extrafn out of the given copy-functions.
94 94
95 95 A copy function takes a context and an extra dict, and mutates the
96 96 extra dict as needed based on the given context.
97 97 """
98 98 def extrafn(ctx, extra):
99 99 for c in copiers:
100 100 c(ctx, extra)
101 101 return extrafn
102 102
103 103 def _destrebase(repo, sourceset, destspace=None):
104 104 """small wrapper around destmerge to pass the right extra args
105 105
106 106 Please wrap destutil.destmerge instead."""
107 107 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
108 108 onheadcheck=False, destspace=destspace)
109 109
110 110 revsetpredicate = registrar.revsetpredicate()
111 111
112 112 @revsetpredicate('_destrebase')
113 113 def _revsetdestrebase(repo, subset, x):
114 114 # ``_rebasedefaultdest()``
115 115
116 116 # default destination for rebase.
117 117 # # XXX: Currently private because I expect the signature to change.
118 118 # # XXX: - bailing out in case of ambiguity vs returning all data.
119 119 # i18n: "_rebasedefaultdest" is a keyword
120 120 sourceset = None
121 121 if x is not None:
122 122 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
123 123 return subset & smartset.baseset([_destrebase(repo, sourceset)])
124 124
125 125 class rebaseruntime(object):
126 126 """This class is a container for rebase runtime state"""
127 127 def __init__(self, repo, ui, opts=None):
128 128 if opts is None:
129 129 opts = {}
130 130
131 131 self.repo = repo
132 132 self.ui = ui
133 133 self.opts = opts
134 134 self.originalwd = None
135 135 self.external = nullrev
136 136 # Mapping between the old revision id and either what is the new rebased
137 137 # revision or what needs to be done with the old revision. The state
138 138 # dict will be what contains most of the rebase progress state.
139 139 self.state = {}
140 140 self.activebookmark = None
141 141 self.currentbookmarks = None
142 142 self.dest = None
143 143 self.skipped = set()
144 144 self.destancestors = set()
145 145
146 146 self.collapsef = opts.get('collapse', False)
147 147 self.collapsemsg = cmdutil.logmessage(ui, opts)
148 148 self.date = opts.get('date', None)
149 149
150 150 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
151 151 self.extrafns = [_savegraft]
152 152 if e:
153 153 self.extrafns = [e]
154 154
155 155 self.keepf = opts.get('keep', False)
156 156 self.keepbranchesf = opts.get('keepbranches', False)
157 157 # keepopen is not meant for use on the command line, but by
158 158 # other extensions
159 159 self.keepopen = opts.get('keepopen', False)
160 160 self.obsoletenotrebased = {}
161 161
162 162 def storestatus(self, tr=None):
163 163 """Store the current status to allow recovery"""
164 164 if tr:
165 165 tr.addfilegenerator('rebasestate', ('rebasestate',),
166 166 self._writestatus, location='plain')
167 167 else:
168 168 with self.repo.vfs("rebasestate", "w") as f:
169 169 self._writestatus(f)
170 170
171 171 def _writestatus(self, f):
172 172 repo = self.repo.unfiltered()
173 173 f.write(repo[self.originalwd].hex() + '\n')
174 174 f.write(repo[self.dest].hex() + '\n')
175 175 f.write(repo[self.external].hex() + '\n')
176 176 f.write('%d\n' % int(self.collapsef))
177 177 f.write('%d\n' % int(self.keepf))
178 178 f.write('%d\n' % int(self.keepbranchesf))
179 179 f.write('%s\n' % (self.activebookmark or ''))
180 180 for d, v in self.state.iteritems():
181 181 oldrev = repo[d].hex()
182 182 if v >= 0:
183 183 newrev = repo[v].hex()
184 184 elif v == revtodo:
185 185 # To maintain format compatibility, we have to use nullid.
186 186 # Please do remove this special case when upgrading the format.
187 187 newrev = hex(nullid)
188 188 else:
189 189 newrev = v
190 190 f.write("%s:%s\n" % (oldrev, newrev))
191 191 repo.ui.debug('rebase status stored\n')
192 192
193 193 def restorestatus(self):
194 194 """Restore a previously stored status"""
195 195 repo = self.repo
196 196 keepbranches = None
197 197 dest = None
198 198 collapse = False
199 199 external = nullrev
200 200 activebookmark = None
201 201 state = {}
202 202
203 203 try:
204 204 f = repo.vfs("rebasestate")
205 205 for i, l in enumerate(f.read().splitlines()):
206 206 if i == 0:
207 207 originalwd = repo[l].rev()
208 208 elif i == 1:
209 209 dest = repo[l].rev()
210 210 elif i == 2:
211 211 external = repo[l].rev()
212 212 elif i == 3:
213 213 collapse = bool(int(l))
214 214 elif i == 4:
215 215 keep = bool(int(l))
216 216 elif i == 5:
217 217 keepbranches = bool(int(l))
218 218 elif i == 6 and not (len(l) == 81 and ':' in l):
219 219 # line 6 is a recent addition, so for backwards
220 220 # compatibility check that the line doesn't look like the
221 221 # oldrev:newrev lines
222 222 activebookmark = l
223 223 else:
224 224 oldrev, newrev = l.split(':')
225 225 if newrev in (str(nullmerge), str(revignored),
226 226 str(revprecursor), str(revpruned)):
227 227 state[repo[oldrev].rev()] = int(newrev)
228 228 elif newrev == nullid:
229 229 state[repo[oldrev].rev()] = revtodo
230 230 # Legacy compat special case
231 231 else:
232 232 state[repo[oldrev].rev()] = repo[newrev].rev()
233 233
234 234 except IOError as err:
235 235 if err.errno != errno.ENOENT:
236 236 raise
237 237 cmdutil.wrongtooltocontinue(repo, _('rebase'))
238 238
239 239 if keepbranches is None:
240 240 raise error.Abort(_('.hg/rebasestate is incomplete'))
241 241
242 242 skipped = set()
243 243 # recompute the set of skipped revs
244 244 if not collapse:
245 245 seen = {dest}
246 246 for old, new in sorted(state.items()):
247 247 if new != revtodo and new in seen:
248 248 skipped.add(old)
249 249 seen.add(new)
250 250 repo.ui.debug('computed skipped revs: %s\n' %
251 251 (' '.join(str(r) for r in sorted(skipped)) or None))
252 252 repo.ui.debug('rebase status resumed\n')
253 253 _setrebasesetvisibility(repo, set(state.keys()) | {originalwd})
254 254
255 255 self.originalwd = originalwd
256 256 self.dest = dest
257 257 self.state = state
258 258 self.skipped = skipped
259 259 self.collapsef = collapse
260 260 self.keepf = keep
261 261 self.keepbranchesf = keepbranches
262 262 self.external = external
263 263 self.activebookmark = activebookmark
264 264
265 265 def _handleskippingobsolete(self, rebaserevs, obsoleterevs, dest):
266 266 """Compute structures necessary for skipping obsolete revisions
267 267
268 268 rebaserevs: iterable of all revisions that are to be rebased
269 269 obsoleterevs: iterable of all obsolete revisions in rebaseset
270 270 dest: a destination revision for the rebase operation
271 271 """
272 272 self.obsoletenotrebased = {}
273 273 if not self.ui.configbool('experimental', 'rebaseskipobsolete',
274 274 default=True):
275 275 return
276 276 rebaseset = set(rebaserevs)
277 277 obsoleteset = set(obsoleterevs)
278 278 self.obsoletenotrebased = _computeobsoletenotrebased(self.repo,
279 279 obsoleteset, dest)
280 280 skippedset = set(self.obsoletenotrebased)
281 281 _checkobsrebase(self.repo, self.ui, obsoleteset, rebaseset, skippedset)
282 282
283 283 def _prepareabortorcontinue(self, isabort):
284 284 try:
285 285 self.restorestatus()
286 286 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
287 287 except error.RepoLookupError:
288 288 if isabort:
289 289 clearstatus(self.repo)
290 290 clearcollapsemsg(self.repo)
291 291 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
292 292 ' only broken state is cleared)\n'))
293 293 return 0
294 294 else:
295 295 msg = _('cannot continue inconsistent rebase')
296 296 hint = _('use "hg rebase --abort" to clear broken state')
297 297 raise error.Abort(msg, hint=hint)
298 298 if isabort:
299 299 return abort(self.repo, self.originalwd, self.dest,
300 300 self.state, activebookmark=self.activebookmark)
301 301
302 302 obsrevs = (r for r, st in self.state.items() if st == revprecursor)
303 303 self._handleskippingobsolete(self.state.keys(), obsrevs, self.dest)
304 304
305 305 def _preparenewrebase(self, dest, rebaseset):
306 306 if dest is None:
307 307 return _nothingtorebase()
308 308
309 309 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
310 310 if (not (self.keepf or allowunstable)
311 311 and self.repo.revs('first(children(%ld) - %ld)',
312 312 rebaseset, rebaseset)):
313 313 raise error.Abort(
314 314 _("can't remove original changesets with"
315 315 " unrebased descendants"),
316 316 hint=_('use --keep to keep original changesets'))
317 317
318 318 obsrevs = _filterobsoleterevs(self.repo, set(rebaseset))
319 319 self._handleskippingobsolete(rebaseset, obsrevs, dest)
320 320
321 321 result = buildstate(self.repo, dest, rebaseset, self.collapsef,
322 322 self.obsoletenotrebased)
323 323
324 324 if not result:
325 325 # Empty state built, nothing to rebase
326 326 self.ui.status(_('nothing to rebase\n'))
327 327 return _nothingtorebase()
328 328
329 329 for root in self.repo.set('roots(%ld)', rebaseset):
330 330 if not self.keepf and not root.mutable():
331 331 raise error.Abort(_("can't rebase public changeset %s")
332 332 % root,
333 333 hint=_("see 'hg help phases' for details"))
334 334
335 335 (self.originalwd, self.dest, self.state) = result
336 336 if self.collapsef:
337 337 self.destancestors = self.repo.changelog.ancestors(
338 338 [self.dest],
339 339 inclusive=True)
340 340 self.external = externalparent(self.repo, self.state,
341 341 self.destancestors)
342 342
343 343 if dest.closesbranch() and not self.keepbranchesf:
344 344 self.ui.status(_('reopening closed branch head %s\n') % dest)
345 345
346 346 def _performrebase(self, tr):
347 347 repo, ui, opts = self.repo, self.ui, self.opts
348 348 if self.keepbranchesf:
349 349 # insert _savebranch at the start of extrafns so if
350 350 # there's a user-provided extrafn it can clobber branch if
351 351 # desired
352 352 self.extrafns.insert(0, _savebranch)
353 353 if self.collapsef:
354 354 branches = set()
355 355 for rev in self.state:
356 356 branches.add(repo[rev].branch())
357 357 if len(branches) > 1:
358 358 raise error.Abort(_('cannot collapse multiple named '
359 359 'branches'))
360 360
361 361 # Rebase
362 362 if not self.destancestors:
363 363 self.destancestors = repo.changelog.ancestors([self.dest],
364 364 inclusive=True)
365 365
366 366 # Keep track of the current bookmarks in order to reset them later
367 367 self.currentbookmarks = repo._bookmarks.copy()
368 368 self.activebookmark = self.activebookmark or repo._activebookmark
369 369 if self.activebookmark:
370 370 bookmarks.deactivate(repo)
371 371
372 372 # Store the state before we begin so users can run 'hg rebase --abort'
373 373 # if we fail before the transaction closes.
374 374 self.storestatus()
375 375
376 376 sortedrevs = repo.revs('sort(%ld, -topo)', self.state)
377 377 cands = [k for k, v in self.state.iteritems() if v == revtodo]
378 378 total = len(cands)
379 379 pos = 0
380 380 for rev in sortedrevs:
381 381 ctx = repo[rev]
382 382 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
383 383 ctx.description().split('\n', 1)[0])
384 384 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
385 385 if names:
386 386 desc += ' (%s)' % ' '.join(names)
387 387 if self.state[rev] == rev:
388 388 ui.status(_('already rebased %s\n') % desc)
389 389 elif self.state[rev] == revtodo:
390 390 pos += 1
391 391 ui.status(_('rebasing %s\n') % desc)
392 392 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
393 393 _('changesets'), total)
394 394 p1, p2, base = defineparents(repo, rev, self.dest,
395 395 self.state,
396 396 self.destancestors,
397 397 self.obsoletenotrebased)
398 398 self.storestatus(tr=tr)
399 399 storecollapsemsg(repo, self.collapsemsg)
400 400 if len(repo[None].parents()) == 2:
401 401 repo.ui.debug('resuming interrupted rebase\n')
402 402 else:
403 403 try:
404 404 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
405 405 'rebase')
406 406 stats = rebasenode(repo, rev, p1, base, self.state,
407 407 self.collapsef, self.dest)
408 408 if stats and stats[3] > 0:
409 409 raise error.InterventionRequired(
410 410 _('unresolved conflicts (see hg '
411 411 'resolve, then hg rebase --continue)'))
412 412 finally:
413 413 ui.setconfig('ui', 'forcemerge', '', 'rebase')
414 414 if not self.collapsef:
415 415 merging = p2 != nullrev
416 416 editform = cmdutil.mergeeditform(merging, 'rebase')
417 417 editor = cmdutil.getcommiteditor(editform=editform, **opts)
418 418 newnode = concludenode(repo, rev, p1, p2,
419 419 extrafn=_makeextrafn(self.extrafns),
420 420 editor=editor,
421 421 keepbranches=self.keepbranchesf,
422 422 date=self.date)
423 423 if newnode is None:
424 424 # If it ended up being a no-op commit, then the normal
425 425 # merge state clean-up path doesn't happen, so do it
426 426 # here. Fix issue5494
427 427 mergemod.mergestate.clean(repo)
428 428 else:
429 429 # Skip commit if we are collapsing
430 430 repo.setparents(repo[p1].node())
431 431 newnode = None
432 432 # Update the state
433 433 if newnode is not None:
434 434 self.state[rev] = repo[newnode].rev()
435 435 ui.debug('rebased as %s\n' % short(newnode))
436 436 else:
437 437 if not self.collapsef:
438 438 ui.warn(_('note: rebase of %d:%s created no changes '
439 439 'to commit\n') % (rev, ctx))
440 440 self.skipped.add(rev)
441 441 self.state[rev] = p1
442 442 ui.debug('next revision set to %s\n' % p1)
443 443 elif self.state[rev] == nullmerge:
444 444 ui.debug('ignoring null merge rebase of %s\n' % rev)
445 445 elif self.state[rev] == revignored:
446 446 ui.status(_('not rebasing ignored %s\n') % desc)
447 447 elif self.state[rev] == revprecursor:
448 448 destctx = repo[self.obsoletenotrebased[rev]]
449 449 descdest = '%d:%s "%s"' % (destctx.rev(), destctx,
450 450 destctx.description().split('\n', 1)[0])
451 451 msg = _('note: not rebasing %s, already in destination as %s\n')
452 452 ui.status(msg % (desc, descdest))
453 453 elif self.state[rev] == revpruned:
454 454 msg = _('note: not rebasing %s, it has no successor\n')
455 455 ui.status(msg % desc)
456 456 else:
457 457 ui.status(_('already rebased %s as %s\n') %
458 458 (desc, repo[self.state[rev]]))
459 459
460 460 ui.progress(_('rebasing'), None)
461 461 ui.note(_('rebase merging completed\n'))
462 462
463 463 def _finishrebase(self):
464 464 repo, ui, opts = self.repo, self.ui, self.opts
465 465 if self.collapsef and not self.keepopen:
466 466 p1, p2, _base = defineparents(repo, min(self.state),
467 467 self.dest, self.state,
468 468 self.destancestors,
469 469 self.obsoletenotrebased)
470 470 editopt = opts.get('edit')
471 471 editform = 'rebase.collapse'
472 472 if self.collapsemsg:
473 473 commitmsg = self.collapsemsg
474 474 else:
475 475 commitmsg = 'Collapsed revision'
476 476 for rebased in self.state:
477 477 if rebased not in self.skipped and\
478 478 self.state[rebased] > nullmerge:
479 479 commitmsg += '\n* %s' % repo[rebased].description()
480 480 editopt = True
481 481 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
482 482 revtoreuse = max(self.state)
483 483 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
484 484 try:
485 485 newnode = concludenode(repo, revtoreuse, p1, self.external,
486 486 commitmsg=commitmsg,
487 487 extrafn=_makeextrafn(self.extrafns),
488 488 editor=editor,
489 489 keepbranches=self.keepbranchesf,
490 490 date=self.date)
491 491 dsguard.close()
492 492 release(dsguard)
493 493 except error.InterventionRequired:
494 494 dsguard.close()
495 495 release(dsguard)
496 496 raise
497 497 except Exception:
498 498 release(dsguard)
499 499 raise
500 500
501 501 if newnode is None:
502 502 newrev = self.dest
503 503 else:
504 504 newrev = repo[newnode].rev()
505 505 for oldrev in self.state.iterkeys():
506 506 if self.state[oldrev] > nullmerge:
507 507 self.state[oldrev] = newrev
508 508
509 509 if 'qtip' in repo.tags():
510 510 updatemq(repo, self.state, self.skipped, **opts)
511 511
512 512 if self.currentbookmarks:
513 513 # Nodeids are needed to reset bookmarks
514 514 nstate = {}
515 515 for k, v in self.state.iteritems():
516 516 if v > nullmerge and v != k:
517 517 nstate[repo[k].node()] = repo[v].node()
518 518 elif v == revprecursor:
519 519 succ = self.obsoletenotrebased[k]
520 520 nstate[repo[k].node()] = repo[succ].node()
521 521 # XXX this is the same as dest.node() for the non-continue path --
522 522 # this should probably be cleaned up
523 523 destnode = repo[self.dest].node()
524 524
525 525 # restore original working directory
526 526 # (we do this before stripping)
527 527 newwd = self.state.get(self.originalwd, self.originalwd)
528 528 if newwd == revprecursor:
529 529 newwd = self.obsoletenotrebased[self.originalwd]
530 530 elif newwd < 0:
531 531 # original directory is a parent of rebase set root or ignored
532 532 newwd = self.originalwd
533 533 if newwd not in [c.rev() for c in repo[None].parents()]:
534 534 ui.note(_("update back to initial working directory parent\n"))
535 535 hg.updaterepo(repo, newwd, False)
536 536
537 537 if self.currentbookmarks:
538 538 with repo.transaction('bookmark') as tr:
539 539 updatebookmarks(repo, destnode, nstate,
540 540 self.currentbookmarks, tr)
541 541 if self.activebookmark not in repo._bookmarks:
542 542 # active bookmark was divergent one and has been deleted
543 543 self.activebookmark = None
544 544
545 545 if not self.keepf:
546 546 collapsedas = None
547 547 if self.collapsef:
548 548 collapsedas = newnode
549 549 clearrebased(ui, repo, self.state, self.skipped, collapsedas)
550 550
551 551 clearstatus(repo)
552 552 clearcollapsemsg(repo)
553 553
554 554 ui.note(_("rebase completed\n"))
555 555 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
556 556 if self.skipped:
557 557 skippedlen = len(self.skipped)
558 558 ui.note(_("%d revisions have been skipped\n") % skippedlen)
559 559
560 560 if (self.activebookmark and
561 561 repo['.'].node() == repo._bookmarks[self.activebookmark]):
562 562 bookmarks.activate(repo, self.activebookmark)
563 563
564 564 @command('rebase',
565 565 [('s', 'source', '',
566 566 _('rebase the specified changeset and descendants'), _('REV')),
567 567 ('b', 'base', '',
568 568 _('rebase everything from branching point of specified changeset'),
569 569 _('REV')),
570 570 ('r', 'rev', [],
571 571 _('rebase these revisions'),
572 572 _('REV')),
573 573 ('d', 'dest', '',
574 574 _('rebase onto the specified changeset'), _('REV')),
575 575 ('', 'collapse', False, _('collapse the rebased changesets')),
576 576 ('m', 'message', '',
577 577 _('use text as collapse commit message'), _('TEXT')),
578 578 ('e', 'edit', False, _('invoke editor on commit messages')),
579 579 ('l', 'logfile', '',
580 580 _('read collapse commit message from file'), _('FILE')),
581 581 ('k', 'keep', False, _('keep original changesets')),
582 582 ('', 'keepbranches', False, _('keep original branch names')),
583 583 ('D', 'detach', False, _('(DEPRECATED)')),
584 584 ('i', 'interactive', False, _('(DEPRECATED)')),
585 585 ('t', 'tool', '', _('specify merge tool')),
586 586 ('c', 'continue', False, _('continue an interrupted rebase')),
587 587 ('a', 'abort', False, _('abort an interrupted rebase'))] +
588 588 templateopts,
589 589 _('[-s REV | -b REV] [-d REV] [OPTION]'))
590 590 def rebase(ui, repo, **opts):
591 591 """move changeset (and descendants) to a different branch
592 592
593 593 Rebase uses repeated merging to graft changesets from one part of
594 594 history (the source) onto another (the destination). This can be
595 595 useful for linearizing *local* changes relative to a master
596 596 development tree.
597 597
598 598 Published commits cannot be rebased (see :hg:`help phases`).
599 599 To copy commits, see :hg:`help graft`.
600 600
601 601 If you don't specify a destination changeset (``-d/--dest``), rebase
602 602 will use the same logic as :hg:`merge` to pick a destination. if
603 603 the current branch contains exactly one other head, the other head
604 604 is merged with by default. Otherwise, an explicit revision with
605 605 which to merge with must be provided. (destination changeset is not
606 606 modified by rebasing, but new changesets are added as its
607 607 descendants.)
608 608
609 609 Here are the ways to select changesets:
610 610
611 611 1. Explicitly select them using ``--rev``.
612 612
613 613 2. Use ``--source`` to select a root changeset and include all of its
614 614 descendants.
615 615
616 616 3. Use ``--base`` to select a changeset; rebase will find ancestors
617 617 and their descendants which are not also ancestors of the destination.
618 618
619 619 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
620 620 rebase will use ``--base .`` as above.
621 621
622 622 Rebase will destroy original changesets unless you use ``--keep``.
623 623 It will also move your bookmarks (even if you do).
624 624
625 625 Some changesets may be dropped if they do not contribute changes
626 626 (e.g. merges from the destination branch).
627 627
628 628 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
629 629 a named branch with two heads. You will need to explicitly specify source
630 630 and/or destination.
631 631
632 632 If you need to use a tool to automate merge/conflict decisions, you
633 633 can specify one with ``--tool``, see :hg:`help merge-tools`.
634 634 As a caveat: the tool will not be used to mediate when a file was
635 635 deleted, there is no hook presently available for this.
636 636
637 637 If a rebase is interrupted to manually resolve a conflict, it can be
638 638 continued with --continue/-c or aborted with --abort/-a.
639 639
640 640 .. container:: verbose
641 641
642 642 Examples:
643 643
644 644 - move "local changes" (current commit back to branching point)
645 645 to the current branch tip after a pull::
646 646
647 647 hg rebase
648 648
649 649 - move a single changeset to the stable branch::
650 650
651 651 hg rebase -r 5f493448 -d stable
652 652
653 653 - splice a commit and all its descendants onto another part of history::
654 654
655 655 hg rebase --source c0c3 --dest 4cf9
656 656
657 657 - rebase everything on a branch marked by a bookmark onto the
658 658 default branch::
659 659
660 660 hg rebase --base myfeature --dest default
661 661
662 662 - collapse a sequence of changes into a single commit::
663 663
664 664 hg rebase --collapse -r 1520:1525 -d .
665 665
666 666 - move a named branch while preserving its name::
667 667
668 668 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
669 669
670 670 Configuration Options:
671 671
672 672 You can make rebase require a destination if you set the following config
673 673 option::
674 674
675 675 [commands]
676 676 rebase.requiredest = True
677 677
678 678 Return Values:
679 679
680 680 Returns 0 on success, 1 if nothing to rebase or there are
681 681 unresolved conflicts.
682 682
683 683 """
684 684 rbsrt = rebaseruntime(repo, ui, opts)
685 685
686 686 with repo.wlock(), repo.lock():
687 687 # Validate input and define rebasing points
688 688 destf = opts.get('dest', None)
689 689 srcf = opts.get('source', None)
690 690 basef = opts.get('base', None)
691 691 revf = opts.get('rev', [])
692 692 # search default destination in this space
693 693 # used in the 'hg pull --rebase' case, see issue 5214.
694 694 destspace = opts.get('_destspace')
695 695 contf = opts.get('continue')
696 696 abortf = opts.get('abort')
697 697 if opts.get('interactive'):
698 698 try:
699 699 if extensions.find('histedit'):
700 700 enablehistedit = ''
701 701 except KeyError:
702 702 enablehistedit = " --config extensions.histedit="
703 703 help = "hg%s help -e histedit" % enablehistedit
704 704 msg = _("interactive history editing is supported by the "
705 705 "'histedit' extension (see \"%s\")") % help
706 706 raise error.Abort(msg)
707 707
708 708 if rbsrt.collapsemsg and not rbsrt.collapsef:
709 709 raise error.Abort(
710 710 _('message can only be specified with collapse'))
711 711
712 712 if contf or abortf:
713 713 if contf and abortf:
714 714 raise error.Abort(_('cannot use both abort and continue'))
715 715 if rbsrt.collapsef:
716 716 raise error.Abort(
717 717 _('cannot use collapse with continue or abort'))
718 718 if srcf or basef or destf:
719 719 raise error.Abort(
720 720 _('abort and continue do not allow specifying revisions'))
721 721 if abortf and opts.get('tool', False):
722 722 ui.warn(_('tool option will be ignored\n'))
723 723 if contf:
724 724 ms = mergemod.mergestate.read(repo)
725 725 mergeutil.checkunresolved(ms)
726 726
727 727 retcode = rbsrt._prepareabortorcontinue(abortf)
728 728 if retcode is not None:
729 729 return retcode
730 730 else:
731 731 dest, rebaseset = _definesets(ui, repo, destf, srcf, basef, revf,
732 732 destspace=destspace)
733 733 retcode = rbsrt._preparenewrebase(dest, rebaseset)
734 734 if retcode is not None:
735 735 return retcode
736 736
737 737 with repo.transaction('rebase') as tr:
738 738 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
739 739 try:
740 740 rbsrt._performrebase(tr)
741 741 dsguard.close()
742 742 release(dsguard)
743 743 except error.InterventionRequired:
744 744 dsguard.close()
745 745 release(dsguard)
746 746 tr.close()
747 747 raise
748 748 except Exception:
749 749 release(dsguard)
750 750 raise
751 751 rbsrt._finishrebase()
752 752
753 753 def _definesets(ui, repo, destf=None, srcf=None, basef=None, revf=None,
754 754 destspace=None):
755 755 """use revisions argument to define destination and rebase set
756 756 """
757 757 if revf is None:
758 758 revf = []
759 759
760 760 # destspace is here to work around issues with `hg pull --rebase` see
761 761 # issue5214 for details
762 762 if srcf and basef:
763 763 raise error.Abort(_('cannot specify both a source and a base'))
764 764 if revf and basef:
765 765 raise error.Abort(_('cannot specify both a revision and a base'))
766 766 if revf and srcf:
767 767 raise error.Abort(_('cannot specify both a revision and a source'))
768 768
769 769 cmdutil.checkunfinished(repo)
770 770 cmdutil.bailifchanged(repo)
771 771
772 772 if ui.configbool('commands', 'rebase.requiredest') and not destf:
773 773 raise error.Abort(_('you must specify a destination'),
774 774 hint=_('use: hg rebase -d REV'))
775 775
776 776 if destf:
777 777 dest = scmutil.revsingle(repo, destf)
778 778
779 779 if revf:
780 780 rebaseset = scmutil.revrange(repo, revf)
781 781 if not rebaseset:
782 782 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
783 783 return None, None
784 784 elif srcf:
785 785 src = scmutil.revrange(repo, [srcf])
786 786 if not src:
787 787 ui.status(_('empty "source" revision set - nothing to rebase\n'))
788 788 return None, None
789 789 rebaseset = repo.revs('(%ld)::', src)
790 790 assert rebaseset
791 791 else:
792 792 base = scmutil.revrange(repo, [basef or '.'])
793 793 if not base:
794 794 ui.status(_('empty "base" revision set - '
795 795 "can't compute rebase set\n"))
796 796 return None, None
797 797 if not destf:
798 798 dest = repo[_destrebase(repo, base, destspace=destspace)]
799 799 destf = str(dest)
800 800
801 801 roots = [] # selected children of branching points
802 802 bpbase = {} # {branchingpoint: [origbase]}
803 803 for b in base: # group bases by branching points
804 804 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
805 805 bpbase[bp] = bpbase.get(bp, []) + [b]
806 806 if None in bpbase:
807 807 # emulate the old behavior, showing "nothing to rebase" (a better
808 808 # behavior may be abort with "cannot find branching point" error)
809 809 bpbase.clear()
810 810 for bp, bs in bpbase.iteritems(): # calculate roots
811 811 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
812 812
813 813 rebaseset = repo.revs('%ld::', roots)
814 814
815 815 if not rebaseset:
816 816 # transform to list because smartsets are not comparable to
817 817 # lists. This should be improved to honor laziness of
818 818 # smartset.
819 819 if list(base) == [dest.rev()]:
820 820 if basef:
821 821 ui.status(_('nothing to rebase - %s is both "base"'
822 822 ' and destination\n') % dest)
823 823 else:
824 824 ui.status(_('nothing to rebase - working directory '
825 825 'parent is also destination\n'))
826 826 elif not repo.revs('%ld - ::%d', base, dest):
827 827 if basef:
828 828 ui.status(_('nothing to rebase - "base" %s is '
829 829 'already an ancestor of destination '
830 830 '%s\n') %
831 831 ('+'.join(str(repo[r]) for r in base),
832 832 dest))
833 833 else:
834 834 ui.status(_('nothing to rebase - working '
835 835 'directory parent is already an '
836 836 'ancestor of destination %s\n') % dest)
837 837 else: # can it happen?
838 838 ui.status(_('nothing to rebase from %s to %s\n') %
839 839 ('+'.join(str(repo[r]) for r in base), dest))
840 840 return None, None
841 841
842 842 if not destf:
843 843 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
844 844 destf = str(dest)
845 845
846 846 return dest, rebaseset
847 847
848 848 def externalparent(repo, state, destancestors):
849 849 """Return the revision that should be used as the second parent
850 850 when the revisions in state is collapsed on top of destancestors.
851 851 Abort if there is more than one parent.
852 852 """
853 853 parents = set()
854 854 source = min(state)
855 855 for rev in state:
856 856 if rev == source:
857 857 continue
858 858 for p in repo[rev].parents():
859 859 if (p.rev() not in state
860 860 and p.rev() not in destancestors):
861 861 parents.add(p.rev())
862 862 if not parents:
863 863 return nullrev
864 864 if len(parents) == 1:
865 865 return parents.pop()
866 866 raise error.Abort(_('unable to collapse on top of %s, there is more '
867 867 'than one external parent: %s') %
868 868 (max(destancestors),
869 869 ', '.join(str(p) for p in sorted(parents))))
870 870
871 871 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
872 872 keepbranches=False, date=None):
873 873 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
874 874 but also store useful information in extra.
875 875 Return node of committed revision.'''
876 876 repo.setparents(repo[p1].node(), repo[p2].node())
877 877 ctx = repo[rev]
878 878 if commitmsg is None:
879 879 commitmsg = ctx.description()
880 880 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
881 881 extra = {'rebase_source': ctx.hex()}
882 882 if extrafn:
883 883 extrafn(ctx, extra)
884 884
885 885 destphase = max(ctx.phase(), phases.draft)
886 886 overrides = {('phases', 'new-commit'): destphase}
887 887 with repo.ui.configoverride(overrides, 'rebase'):
888 888 if keepbranch:
889 889 repo.ui.setconfig('ui', 'allowemptycommit', True)
890 890 # Commit might fail if unresolved files exist
891 891 if date is None:
892 892 date = ctx.date()
893 893 newnode = repo.commit(text=commitmsg, user=ctx.user(),
894 894 date=date, extra=extra, editor=editor)
895 895
896 896 repo.dirstate.setbranch(repo[newnode].branch())
897 897 return newnode
898 898
899 899 def rebasenode(repo, rev, p1, base, state, collapse, dest):
900 900 'Rebase a single revision rev on top of p1 using base as merge ancestor'
901 901 # Merge phase
902 902 # Update to destination and merge it with local
903 903 if repo['.'].rev() != p1:
904 904 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
905 905 mergemod.update(repo, p1, False, True)
906 906 else:
907 907 repo.ui.debug(" already in destination\n")
908 908 repo.dirstate.write(repo.currenttransaction())
909 909 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
910 910 if base is not None:
911 911 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
912 912 # When collapsing in-place, the parent is the common ancestor, we
913 913 # have to allow merging with it.
914 914 stats = mergemod.update(repo, rev, True, True, base, collapse,
915 915 labels=['dest', 'source'])
916 916 if collapse:
917 917 copies.duplicatecopies(repo, rev, dest)
918 918 else:
919 919 # If we're not using --collapse, we need to
920 920 # duplicate copies between the revision we're
921 921 # rebasing and its first parent, but *not*
922 922 # duplicate any copies that have already been
923 923 # performed in the destination.
924 924 p1rev = repo[rev].p1().rev()
925 925 copies.duplicatecopies(repo, rev, p1rev, skiprev=dest)
926 926 return stats
927 927
928 928 def nearestrebased(repo, rev, state):
929 929 """return the nearest ancestors of rev in the rebase result"""
930 930 rebased = [r for r in state if state[r] > nullmerge]
931 931 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
932 932 if candidates:
933 933 return state[candidates.first()]
934 934 else:
935 935 return None
936 936
937 937 def _checkobsrebase(repo, ui, rebaseobsrevs, rebasesetrevs, rebaseobsskipped):
938 938 """
939 939 Abort if rebase will create divergence or rebase is noop because of markers
940 940
941 941 `rebaseobsrevs`: set of obsolete revision in source
942 942 `rebasesetrevs`: set of revisions to be rebased from source
943 943 `rebaseobsskipped`: set of revisions from source skipped because they have
944 944 successors in destination
945 945 """
946 946 # Obsolete node with successors not in dest leads to divergence
947 947 divergenceok = ui.configbool('experimental',
948 948 'allowdivergence')
949 949 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
950 950
951 951 if divergencebasecandidates and not divergenceok:
952 952 divhashes = (str(repo[r])
953 953 for r in divergencebasecandidates)
954 954 msg = _("this rebase will cause "
955 955 "divergences from: %s")
956 956 h = _("to force the rebase please set "
957 957 "experimental.allowdivergence=True")
958 958 raise error.Abort(msg % (",".join(divhashes),), hint=h)
959 959
960 960 def defineparents(repo, rev, dest, state, destancestors,
961 961 obsoletenotrebased):
962 962 'Return the new parent relationship of the revision that will be rebased'
963 963 parents = repo[rev].parents()
964 964 p1 = p2 = nullrev
965 965 rp1 = None
966 966
967 967 p1n = parents[0].rev()
968 968 if p1n in destancestors:
969 969 p1 = dest
970 970 elif p1n in state:
971 971 if state[p1n] == nullmerge:
972 972 p1 = dest
973 973 elif state[p1n] in revskipped:
974 974 p1 = nearestrebased(repo, p1n, state)
975 975 if p1 is None:
976 976 p1 = dest
977 977 else:
978 978 p1 = state[p1n]
979 979 else: # p1n external
980 980 p1 = dest
981 981 p2 = p1n
982 982
983 983 if len(parents) == 2 and parents[1].rev() not in destancestors:
984 984 p2n = parents[1].rev()
985 985 # interesting second parent
986 986 if p2n in state:
987 987 if p1 == dest: # p1n in destancestors or external
988 988 p1 = state[p2n]
989 989 if p1 == revprecursor:
990 990 rp1 = obsoletenotrebased[p2n]
991 991 elif state[p2n] in revskipped:
992 992 p2 = nearestrebased(repo, p2n, state)
993 993 if p2 is None:
994 994 # no ancestors rebased yet, detach
995 995 p2 = dest
996 996 else:
997 997 p2 = state[p2n]
998 998 else: # p2n external
999 999 if p2 != nullrev: # p1n external too => rev is a merged revision
1000 1000 raise error.Abort(_('cannot use revision %d as base, result '
1001 1001 'would have 3 parents') % rev)
1002 1002 p2 = p2n
1003 1003 repo.ui.debug(" future parents are %d and %d\n" %
1004 1004 (repo[rp1 or p1].rev(), repo[p2].rev()))
1005 1005
1006 1006 if not any(p.rev() in state for p in parents):
1007 1007 # Case (1) root changeset of a non-detaching rebase set.
1008 1008 # Let the merge mechanism find the base itself.
1009 1009 base = None
1010 1010 elif not repo[rev].p2():
1011 1011 # Case (2) detaching the node with a single parent, use this parent
1012 1012 base = repo[rev].p1().rev()
1013 1013 else:
1014 1014 # Assuming there is a p1, this is the case where there also is a p2.
1015 1015 # We are thus rebasing a merge and need to pick the right merge base.
1016 1016 #
1017 1017 # Imagine we have:
1018 1018 # - M: current rebase revision in this step
1019 1019 # - A: one parent of M
1020 1020 # - B: other parent of M
1021 1021 # - D: destination of this merge step (p1 var)
1022 1022 #
1023 1023 # Consider the case where D is a descendant of A or B and the other is
1024 1024 # 'outside'. In this case, the right merge base is the D ancestor.
1025 1025 #
1026 1026 # An informal proof, assuming A is 'outside' and B is the D ancestor:
1027 1027 #
1028 1028 # If we pick B as the base, the merge involves:
1029 1029 # - changes from B to M (actual changeset payload)
1030 1030 # - changes from B to D (induced by rebase) as D is a rebased
1031 1031 # version of B)
1032 1032 # Which exactly represent the rebase operation.
1033 1033 #
1034 1034 # If we pick A as the base, the merge involves:
1035 1035 # - changes from A to M (actual changeset payload)
1036 1036 # - changes from A to D (with include changes between unrelated A and B
1037 1037 # plus changes induced by rebase)
1038 1038 # Which does not represent anything sensible and creates a lot of
1039 1039 # conflicts. A is thus not the right choice - B is.
1040 1040 #
1041 1041 # Note: The base found in this 'proof' is only correct in the specified
1042 1042 # case. This base does not make sense if is not D a descendant of A or B
1043 1043 # or if the other is not parent 'outside' (especially not if the other
1044 1044 # parent has been rebased). The current implementation does not
1045 1045 # make it feasible to consider different cases separately. In these
1046 1046 # other cases we currently just leave it to the user to correctly
1047 1047 # resolve an impossible merge using a wrong ancestor.
1048 1048 #
1049 1049 # xx, p1 could be -4, and both parents could probably be -4...
1050 1050 for p in repo[rev].parents():
1051 1051 if state.get(p.rev()) == p1:
1052 1052 base = p.rev()
1053 1053 break
1054 1054 else: # fallback when base not found
1055 1055 base = None
1056 1056
1057 1057 # Raise because this function is called wrong (see issue 4106)
1058 1058 raise AssertionError('no base found to rebase on '
1059 1059 '(defineparents called wrong)')
1060 1060 return rp1 or p1, p2, base
1061 1061
1062 1062 def isagitpatch(repo, patchname):
1063 1063 'Return true if the given patch is in git format'
1064 1064 mqpatch = os.path.join(repo.mq.path, patchname)
1065 1065 for line in patch.linereader(file(mqpatch, 'rb')):
1066 1066 if line.startswith('diff --git'):
1067 1067 return True
1068 1068 return False
1069 1069
1070 1070 def updatemq(repo, state, skipped, **opts):
1071 1071 'Update rebased mq patches - finalize and then import them'
1072 1072 mqrebase = {}
1073 1073 mq = repo.mq
1074 1074 original_series = mq.fullseries[:]
1075 1075 skippedpatches = set()
1076 1076
1077 1077 for p in mq.applied:
1078 1078 rev = repo[p.node].rev()
1079 1079 if rev in state:
1080 1080 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1081 1081 (rev, p.name))
1082 1082 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1083 1083 else:
1084 1084 # Applied but not rebased, not sure this should happen
1085 1085 skippedpatches.add(p.name)
1086 1086
1087 1087 if mqrebase:
1088 1088 mq.finish(repo, mqrebase.keys())
1089 1089
1090 1090 # We must start import from the newest revision
1091 1091 for rev in sorted(mqrebase, reverse=True):
1092 1092 if rev not in skipped:
1093 1093 name, isgit = mqrebase[rev]
1094 1094 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1095 1095 (name, state[rev], repo[state[rev]]))
1096 1096 mq.qimport(repo, (), patchname=name, git=isgit,
1097 1097 rev=[str(state[rev])])
1098 1098 else:
1099 1099 # Rebased and skipped
1100 1100 skippedpatches.add(mqrebase[rev][0])
1101 1101
1102 1102 # Patches were either applied and rebased and imported in
1103 1103 # order, applied and removed or unapplied. Discard the removed
1104 1104 # ones while preserving the original series order and guards.
1105 1105 newseries = [s for s in original_series
1106 1106 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1107 1107 mq.fullseries[:] = newseries
1108 1108 mq.seriesdirty = True
1109 1109 mq.savedirty()
1110 1110
1111 1111 def updatebookmarks(repo, destnode, nstate, originalbookmarks, tr):
1112 1112 'Move bookmarks to their correct changesets, and delete divergent ones'
1113 1113 marks = repo._bookmarks
1114 1114 for k, v in originalbookmarks.iteritems():
1115 1115 if v in nstate:
1116 1116 # update the bookmarks for revs that have moved
1117 1117 marks[k] = nstate[v]
1118 1118 bookmarks.deletedivergent(repo, [destnode], k)
1119 1119 marks.recordchange(tr)
1120 1120
1121 1121 def storecollapsemsg(repo, collapsemsg):
1122 1122 'Store the collapse message to allow recovery'
1123 1123 collapsemsg = collapsemsg or ''
1124 1124 f = repo.vfs("last-message.txt", "w")
1125 1125 f.write("%s\n" % collapsemsg)
1126 1126 f.close()
1127 1127
1128 1128 def clearcollapsemsg(repo):
1129 1129 'Remove collapse message file'
1130 1130 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1131 1131
1132 1132 def restorecollapsemsg(repo, isabort):
1133 1133 'Restore previously stored collapse message'
1134 1134 try:
1135 1135 f = repo.vfs("last-message.txt")
1136 1136 collapsemsg = f.readline().strip()
1137 1137 f.close()
1138 1138 except IOError as err:
1139 1139 if err.errno != errno.ENOENT:
1140 1140 raise
1141 1141 if isabort:
1142 1142 # Oh well, just abort like normal
1143 1143 collapsemsg = ''
1144 1144 else:
1145 1145 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1146 1146 return collapsemsg
1147 1147
1148 1148 def clearstatus(repo):
1149 1149 'Remove the status files'
1150 1150 _clearrebasesetvisibiliy(repo)
1151 # Make sure the active transaction won't write the state file
1152 tr = repo.currenttransaction()
1153 if tr:
1154 tr.removefilegenerator('rebasestate')
1151 1155 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1152 1156
1153 1157 def needupdate(repo, state):
1154 1158 '''check whether we should `update --clean` away from a merge, or if
1155 1159 somehow the working dir got forcibly updated, e.g. by older hg'''
1156 1160 parents = [p.rev() for p in repo[None].parents()]
1157 1161
1158 1162 # Are we in a merge state at all?
1159 1163 if len(parents) < 2:
1160 1164 return False
1161 1165
1162 1166 # We should be standing on the first as-of-yet unrebased commit.
1163 1167 firstunrebased = min([old for old, new in state.iteritems()
1164 1168 if new == nullrev])
1165 1169 if firstunrebased in parents:
1166 1170 return True
1167 1171
1168 1172 return False
1169 1173
1170 1174 def abort(repo, originalwd, dest, state, activebookmark=None):
1171 1175 '''Restore the repository to its original state. Additional args:
1172 1176
1173 1177 activebookmark: the name of the bookmark that should be active after the
1174 1178 restore'''
1175 1179
1176 1180 try:
1177 1181 # If the first commits in the rebased set get skipped during the rebase,
1178 1182 # their values within the state mapping will be the dest rev id. The
1179 1183 # dstates list must must not contain the dest rev (issue4896)
1180 1184 dstates = [s for s in state.values() if s >= 0 and s != dest]
1181 1185 immutable = [d for d in dstates if not repo[d].mutable()]
1182 1186 cleanup = True
1183 1187 if immutable:
1184 1188 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1185 1189 % ', '.join(str(repo[r]) for r in immutable),
1186 1190 hint=_("see 'hg help phases' for details"))
1187 1191 cleanup = False
1188 1192
1189 1193 descendants = set()
1190 1194 if dstates:
1191 1195 descendants = set(repo.changelog.descendants(dstates))
1192 1196 if descendants - set(dstates):
1193 1197 repo.ui.warn(_("warning: new changesets detected on destination "
1194 1198 "branch, can't strip\n"))
1195 1199 cleanup = False
1196 1200
1197 1201 if cleanup:
1198 1202 shouldupdate = False
1199 1203 rebased = filter(lambda x: x >= 0 and x != dest, state.values())
1200 1204 if rebased:
1201 1205 strippoints = [
1202 1206 c.node() for c in repo.set('roots(%ld)', rebased)]
1203 1207
1204 1208 updateifonnodes = set(rebased)
1205 1209 updateifonnodes.add(dest)
1206 1210 updateifonnodes.add(originalwd)
1207 1211 shouldupdate = repo['.'].rev() in updateifonnodes
1208 1212
1209 1213 # Update away from the rebase if necessary
1210 1214 if shouldupdate or needupdate(repo, state):
1211 1215 mergemod.update(repo, originalwd, False, True)
1212 1216
1213 1217 # Strip from the first rebased revision
1214 1218 if rebased:
1215 1219 # no backup of rebased cset versions needed
1216 1220 repair.strip(repo.ui, repo, strippoints)
1217 1221
1218 1222 if activebookmark and activebookmark in repo._bookmarks:
1219 1223 bookmarks.activate(repo, activebookmark)
1220 1224
1221 1225 finally:
1222 1226 clearstatus(repo)
1223 1227 clearcollapsemsg(repo)
1224 1228 repo.ui.warn(_('rebase aborted\n'))
1225 1229 return 0
1226 1230
1227 1231 def buildstate(repo, dest, rebaseset, collapse, obsoletenotrebased):
1228 1232 '''Define which revisions are going to be rebased and where
1229 1233
1230 1234 repo: repo
1231 1235 dest: context
1232 1236 rebaseset: set of rev
1233 1237 '''
1234 1238 originalwd = repo['.'].rev()
1235 1239 _setrebasesetvisibility(repo, set(rebaseset) | {originalwd})
1236 1240
1237 1241 # This check isn't strictly necessary, since mq detects commits over an
1238 1242 # applied patch. But it prevents messing up the working directory when
1239 1243 # a partially completed rebase is blocked by mq.
1240 1244 if 'qtip' in repo.tags() and (dest.node() in
1241 1245 [s.node for s in repo.mq.applied]):
1242 1246 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1243 1247
1244 1248 roots = list(repo.set('roots(%ld)', rebaseset))
1245 1249 if not roots:
1246 1250 raise error.Abort(_('no matching revisions'))
1247 1251 roots.sort()
1248 1252 state = dict.fromkeys(rebaseset, revtodo)
1249 1253 detachset = set()
1250 1254 emptyrebase = True
1251 1255 for root in roots:
1252 1256 commonbase = root.ancestor(dest)
1253 1257 if commonbase == root:
1254 1258 raise error.Abort(_('source is ancestor of destination'))
1255 1259 if commonbase == dest:
1256 1260 wctx = repo[None]
1257 1261 if dest == wctx.p1():
1258 1262 # when rebasing to '.', it will use the current wd branch name
1259 1263 samebranch = root.branch() == wctx.branch()
1260 1264 else:
1261 1265 samebranch = root.branch() == dest.branch()
1262 1266 if not collapse and samebranch and dest in root.parents():
1263 1267 # mark the revision as done by setting its new revision
1264 1268 # equal to its old (current) revisions
1265 1269 state[root.rev()] = root.rev()
1266 1270 repo.ui.debug('source is a child of destination\n')
1267 1271 continue
1268 1272
1269 1273 emptyrebase = False
1270 1274 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1271 1275 # Rebase tries to turn <dest> into a parent of <root> while
1272 1276 # preserving the number of parents of rebased changesets:
1273 1277 #
1274 1278 # - A changeset with a single parent will always be rebased as a
1275 1279 # changeset with a single parent.
1276 1280 #
1277 1281 # - A merge will be rebased as merge unless its parents are both
1278 1282 # ancestors of <dest> or are themselves in the rebased set and
1279 1283 # pruned while rebased.
1280 1284 #
1281 1285 # If one parent of <root> is an ancestor of <dest>, the rebased
1282 1286 # version of this parent will be <dest>. This is always true with
1283 1287 # --base option.
1284 1288 #
1285 1289 # Otherwise, we need to *replace* the original parents with
1286 1290 # <dest>. This "detaches" the rebased set from its former location
1287 1291 # and rebases it onto <dest>. Changes introduced by ancestors of
1288 1292 # <root> not common with <dest> (the detachset, marked as
1289 1293 # nullmerge) are "removed" from the rebased changesets.
1290 1294 #
1291 1295 # - If <root> has a single parent, set it to <dest>.
1292 1296 #
1293 1297 # - If <root> is a merge, we cannot decide which parent to
1294 1298 # replace, the rebase operation is not clearly defined.
1295 1299 #
1296 1300 # The table below sums up this behavior:
1297 1301 #
1298 1302 # +------------------+----------------------+-------------------------+
1299 1303 # | | one parent | merge |
1300 1304 # +------------------+----------------------+-------------------------+
1301 1305 # | parent in | new parent is <dest> | parents in ::<dest> are |
1302 1306 # | ::<dest> | | remapped to <dest> |
1303 1307 # +------------------+----------------------+-------------------------+
1304 1308 # | unrelated source | new parent is <dest> | ambiguous, abort |
1305 1309 # +------------------+----------------------+-------------------------+
1306 1310 #
1307 1311 # The actual abort is handled by `defineparents`
1308 1312 if len(root.parents()) <= 1:
1309 1313 # ancestors of <root> not ancestors of <dest>
1310 1314 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
1311 1315 [root.rev()]))
1312 1316 if emptyrebase:
1313 1317 return None
1314 1318 for rev in sorted(state):
1315 1319 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1316 1320 # if all parents of this revision are done, then so is this revision
1317 1321 if parents and all((state.get(p) == p for p in parents)):
1318 1322 state[rev] = rev
1319 1323 for r in detachset:
1320 1324 if r not in state:
1321 1325 state[r] = nullmerge
1322 1326 if len(roots) > 1:
1323 1327 # If we have multiple roots, we may have "hole" in the rebase set.
1324 1328 # Rebase roots that descend from those "hole" should not be detached as
1325 1329 # other root are. We use the special `revignored` to inform rebase that
1326 1330 # the revision should be ignored but that `defineparents` should search
1327 1331 # a rebase destination that make sense regarding rebased topology.
1328 1332 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
1329 1333 for ignored in set(rebasedomain) - set(rebaseset):
1330 1334 state[ignored] = revignored
1331 1335 for r in obsoletenotrebased:
1332 1336 if obsoletenotrebased[r] is None:
1333 1337 state[r] = revpruned
1334 1338 else:
1335 1339 state[r] = revprecursor
1336 1340 return originalwd, dest.rev(), state
1337 1341
1338 1342 def clearrebased(ui, repo, state, skipped, collapsedas=None):
1339 1343 """dispose of rebased revision at the end of the rebase
1340 1344
1341 1345 If `collapsedas` is not None, the rebase was a collapse whose result if the
1342 1346 `collapsedas` node."""
1343 1347 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1344 1348 markers = []
1345 1349 for rev, newrev in sorted(state.items()):
1346 1350 if newrev >= 0 and newrev != rev:
1347 1351 if rev in skipped:
1348 1352 succs = ()
1349 1353 elif collapsedas is not None:
1350 1354 succs = (repo[collapsedas],)
1351 1355 else:
1352 1356 succs = (repo[newrev],)
1353 1357 markers.append((repo[rev], succs))
1354 1358 if markers:
1355 1359 obsolete.createmarkers(repo, markers, operation='rebase')
1356 1360 else:
1357 1361 rebased = [rev for rev in state
1358 1362 if state[rev] > nullmerge and state[rev] != rev]
1359 1363 if rebased:
1360 1364 stripped = []
1361 1365 for root in repo.set('roots(%ld)', rebased):
1362 1366 if set(repo.changelog.descendants([root.rev()])) - set(state):
1363 1367 ui.warn(_("warning: new changesets detected "
1364 1368 "on source branch, not stripping\n"))
1365 1369 else:
1366 1370 stripped.append(root.node())
1367 1371 if stripped:
1368 1372 # backup the old csets by default
1369 1373 repair.strip(ui, repo, stripped, "all")
1370 1374
1371 1375
1372 1376 def pullrebase(orig, ui, repo, *args, **opts):
1373 1377 'Call rebase after pull if the latter has been invoked with --rebase'
1374 1378 ret = None
1375 1379 if opts.get('rebase'):
1376 1380 if ui.configbool('commands', 'rebase.requiredest'):
1377 1381 msg = _('rebase destination required by configuration')
1378 1382 hint = _('use hg pull followed by hg rebase -d DEST')
1379 1383 raise error.Abort(msg, hint=hint)
1380 1384
1381 1385 with repo.wlock(), repo.lock():
1382 1386 if opts.get('update'):
1383 1387 del opts['update']
1384 1388 ui.debug('--update and --rebase are not compatible, ignoring '
1385 1389 'the update flag\n')
1386 1390
1387 1391 cmdutil.checkunfinished(repo)
1388 1392 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1389 1393 'please commit or shelve your changes first'))
1390 1394
1391 1395 revsprepull = len(repo)
1392 1396 origpostincoming = commands.postincoming
1393 1397 def _dummy(*args, **kwargs):
1394 1398 pass
1395 1399 commands.postincoming = _dummy
1396 1400 try:
1397 1401 ret = orig(ui, repo, *args, **opts)
1398 1402 finally:
1399 1403 commands.postincoming = origpostincoming
1400 1404 revspostpull = len(repo)
1401 1405 if revspostpull > revsprepull:
1402 1406 # --rev option from pull conflict with rebase own --rev
1403 1407 # dropping it
1404 1408 if 'rev' in opts:
1405 1409 del opts['rev']
1406 1410 # positional argument from pull conflicts with rebase's own
1407 1411 # --source.
1408 1412 if 'source' in opts:
1409 1413 del opts['source']
1410 1414 # revsprepull is the len of the repo, not revnum of tip.
1411 1415 destspace = list(repo.changelog.revs(start=revsprepull))
1412 1416 opts['_destspace'] = destspace
1413 1417 try:
1414 1418 rebase(ui, repo, **opts)
1415 1419 except error.NoMergeDestAbort:
1416 1420 # we can maybe update instead
1417 1421 rev, _a, _b = destutil.destupdate(repo)
1418 1422 if rev == repo['.'].rev():
1419 1423 ui.status(_('nothing to rebase\n'))
1420 1424 else:
1421 1425 ui.status(_('nothing to rebase - updating instead\n'))
1422 1426 # not passing argument to get the bare update behavior
1423 1427 # with warning and trumpets
1424 1428 commands.update(ui, repo)
1425 1429 else:
1426 1430 if opts.get('tool'):
1427 1431 raise error.Abort(_('--tool can only be used with --rebase'))
1428 1432 ret = orig(ui, repo, *args, **opts)
1429 1433
1430 1434 return ret
1431 1435
1432 1436 def _setrebasesetvisibility(repo, revs):
1433 1437 """store the currently rebased set on the repo object
1434 1438
1435 1439 This is used by another function to prevent rebased revision to because
1436 1440 hidden (see issue4504)"""
1437 1441 repo = repo.unfiltered()
1438 1442 repo._rebaseset = revs
1439 1443 # invalidate cache if visibility changes
1440 1444 hiddens = repo.filteredrevcache.get('visible', set())
1441 1445 if revs & hiddens:
1442 1446 repo.invalidatevolatilesets()
1443 1447
1444 1448 def _clearrebasesetvisibiliy(repo):
1445 1449 """remove rebaseset data from the repo"""
1446 1450 repo = repo.unfiltered()
1447 1451 if '_rebaseset' in vars(repo):
1448 1452 del repo._rebaseset
1449 1453
1450 1454 def _rebasedvisible(orig, repo):
1451 1455 """ensure rebased revs stay visible (see issue4504)"""
1452 1456 blockers = orig(repo)
1453 1457 blockers.update(getattr(repo, '_rebaseset', ()))
1454 1458 return blockers
1455 1459
1456 1460 def _filterobsoleterevs(repo, revs):
1457 1461 """returns a set of the obsolete revisions in revs"""
1458 1462 return set(r for r in revs if repo[r].obsolete())
1459 1463
1460 1464 def _computeobsoletenotrebased(repo, rebaseobsrevs, dest):
1461 1465 """return a mapping obsolete => successor for all obsolete nodes to be
1462 1466 rebased that have a successors in the destination
1463 1467
1464 1468 obsolete => None entries in the mapping indicate nodes with no successor"""
1465 1469 obsoletenotrebased = {}
1466 1470
1467 1471 # Build a mapping successor => obsolete nodes for the obsolete
1468 1472 # nodes to be rebased
1469 1473 allsuccessors = {}
1470 1474 cl = repo.changelog
1471 1475 for r in rebaseobsrevs:
1472 1476 node = cl.node(r)
1473 1477 for s in obsolete.allsuccessors(repo.obsstore, [node]):
1474 1478 try:
1475 1479 allsuccessors[cl.rev(s)] = cl.rev(node)
1476 1480 except LookupError:
1477 1481 pass
1478 1482
1479 1483 if allsuccessors:
1480 1484 # Look for successors of obsolete nodes to be rebased among
1481 1485 # the ancestors of dest
1482 1486 ancs = cl.ancestors([repo[dest].rev()],
1483 1487 stoprev=min(allsuccessors),
1484 1488 inclusive=True)
1485 1489 for s in allsuccessors:
1486 1490 if s in ancs:
1487 1491 obsoletenotrebased[allsuccessors[s]] = s
1488 1492 elif (s == allsuccessors[s] and
1489 1493 allsuccessors.values().count(s) == 1):
1490 1494 # plain prune
1491 1495 obsoletenotrebased[s] = None
1492 1496
1493 1497 return obsoletenotrebased
1494 1498
1495 1499 def summaryhook(ui, repo):
1496 1500 if not repo.vfs.exists('rebasestate'):
1497 1501 return
1498 1502 try:
1499 1503 rbsrt = rebaseruntime(repo, ui, {})
1500 1504 rbsrt.restorestatus()
1501 1505 state = rbsrt.state
1502 1506 except error.RepoLookupError:
1503 1507 # i18n: column positioning for "hg summary"
1504 1508 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1505 1509 ui.write(msg)
1506 1510 return
1507 1511 numrebased = len([i for i in state.itervalues() if i >= 0])
1508 1512 # i18n: column positioning for "hg summary"
1509 1513 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1510 1514 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1511 1515 ui.label(_('%d remaining'), 'rebase.remaining') %
1512 1516 (len(state) - numrebased)))
1513 1517
1514 1518 def uisetup(ui):
1515 1519 #Replace pull with a decorator to provide --rebase option
1516 1520 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1517 1521 entry[1].append(('', 'rebase', None,
1518 1522 _("rebase working directory to branch head")))
1519 1523 entry[1].append(('t', 'tool', '',
1520 1524 _("specify merge tool for rebase")))
1521 1525 cmdutil.summaryhooks.add('rebase', summaryhook)
1522 1526 cmdutil.unfinishedstates.append(
1523 1527 ['rebasestate', False, False, _('rebase in progress'),
1524 1528 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1525 1529 cmdutil.afterresolvedstates.append(
1526 1530 ['rebasestate', _('hg rebase --continue')])
1527 1531 # ensure rebased rev are not hidden
1528 1532 extensions.wrapfunction(repoview, 'pinnedrevs', _rebasedvisible)
@@ -1,607 +1,613
1 1 # transaction.py - simple journaling scheme for mercurial
2 2 #
3 3 # This transaction scheme is intended to gracefully handle program
4 4 # errors and interruptions. More serious failures like system crashes
5 5 # can be recovered with an fsck-like tool. As the whole repository is
6 6 # effectively log-structured, this should amount to simply truncating
7 7 # anything that isn't referenced in the changelog.
8 8 #
9 9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
10 10 #
11 11 # This software may be used and distributed according to the terms of the
12 12 # GNU General Public License version 2 or any later version.
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import errno
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 error,
21 21 util,
22 22 )
23 23
24 24 version = 2
25 25
26 26 # These are the file generators that should only be executed after the
27 27 # finalizers are done, since they rely on the output of the finalizers (like
28 28 # the changelog having been written).
29 29 postfinalizegenerators = {
30 30 'bookmarks',
31 31 'dirstate'
32 32 }
33 33
34 34 gengroupall='all'
35 35 gengroupprefinalize='prefinalize'
36 36 gengrouppostfinalize='postfinalize'
37 37
38 38 def active(func):
39 39 def _active(self, *args, **kwds):
40 40 if self.count == 0:
41 41 raise error.Abort(_(
42 42 'cannot use transaction when it is already committed/aborted'))
43 43 return func(self, *args, **kwds)
44 44 return _active
45 45
46 46 def _playback(journal, report, opener, vfsmap, entries, backupentries,
47 47 unlink=True):
48 48 for f, o, _ignore in entries:
49 49 if o or not unlink:
50 50 try:
51 51 fp = opener(f, 'a', checkambig=True)
52 52 fp.truncate(o)
53 53 fp.close()
54 54 except IOError:
55 55 report(_("failed to truncate %s\n") % f)
56 56 raise
57 57 else:
58 58 try:
59 59 opener.unlink(f)
60 60 except (IOError, OSError) as inst:
61 61 if inst.errno != errno.ENOENT:
62 62 raise
63 63
64 64 backupfiles = []
65 65 for l, f, b, c in backupentries:
66 66 if l not in vfsmap and c:
67 67 report("couldn't handle %s: unknown cache location %s\n"
68 68 % (b, l))
69 69 vfs = vfsmap[l]
70 70 try:
71 71 if f and b:
72 72 filepath = vfs.join(f)
73 73 backuppath = vfs.join(b)
74 74 try:
75 75 util.copyfile(backuppath, filepath, checkambig=True)
76 76 backupfiles.append(b)
77 77 except IOError:
78 78 report(_("failed to recover %s\n") % f)
79 79 else:
80 80 target = f or b
81 81 try:
82 82 vfs.unlink(target)
83 83 except (IOError, OSError) as inst:
84 84 if inst.errno != errno.ENOENT:
85 85 raise
86 86 except (IOError, OSError, error.Abort) as inst:
87 87 if not c:
88 88 raise
89 89
90 90 backuppath = "%s.backupfiles" % journal
91 91 if opener.exists(backuppath):
92 92 opener.unlink(backuppath)
93 93 opener.unlink(journal)
94 94 try:
95 95 for f in backupfiles:
96 96 if opener.exists(f):
97 97 opener.unlink(f)
98 98 except (IOError, OSError, error.Abort) as inst:
99 99 # only pure backup file remains, it is sage to ignore any error
100 100 pass
101 101
102 102 class transaction(object):
103 103 def __init__(self, report, opener, vfsmap, journalname, undoname=None,
104 104 after=None, createmode=None, validator=None, releasefn=None):
105 105 """Begin a new transaction
106 106
107 107 Begins a new transaction that allows rolling back writes in the event of
108 108 an exception.
109 109
110 110 * `after`: called after the transaction has been committed
111 111 * `createmode`: the mode of the journal file that will be created
112 112 * `releasefn`: called after releasing (with transaction and result)
113 113 """
114 114 self.count = 1
115 115 self.usages = 1
116 116 self.report = report
117 117 # a vfs to the store content
118 118 self.opener = opener
119 119 # a map to access file in various {location -> vfs}
120 120 vfsmap = vfsmap.copy()
121 121 vfsmap[''] = opener # set default value
122 122 self._vfsmap = vfsmap
123 123 self.after = after
124 124 self.entries = []
125 125 self.map = {}
126 126 self.journal = journalname
127 127 self.undoname = undoname
128 128 self._queue = []
129 129 # A callback to validate transaction content before closing it.
130 130 # should raise exception is anything is wrong.
131 131 # target user is repository hooks.
132 132 if validator is None:
133 133 validator = lambda tr: None
134 134 self.validator = validator
135 135 # A callback to do something just after releasing transaction.
136 136 if releasefn is None:
137 137 releasefn = lambda tr, success: None
138 138 self.releasefn = releasefn
139 139
140 140 # A dict dedicated to precisely tracking the changes introduced in the
141 141 # transaction.
142 142 self.changes = {}
143 143
144 144 # a dict of arguments to be passed to hooks
145 145 self.hookargs = {}
146 146 self.file = opener.open(self.journal, "w")
147 147
148 148 # a list of ('location', 'path', 'backuppath', cache) entries.
149 149 # - if 'backuppath' is empty, no file existed at backup time
150 150 # - if 'path' is empty, this is a temporary transaction file
151 151 # - if 'location' is not empty, the path is outside main opener reach.
152 152 # use 'location' value as a key in a vfsmap to find the right 'vfs'
153 153 # (cache is currently unused)
154 154 self._backupentries = []
155 155 self._backupmap = {}
156 156 self._backupjournal = "%s.backupfiles" % self.journal
157 157 self._backupsfile = opener.open(self._backupjournal, 'w')
158 158 self._backupsfile.write('%d\n' % version)
159 159
160 160 if createmode is not None:
161 161 opener.chmod(self.journal, createmode & 0o666)
162 162 opener.chmod(self._backupjournal, createmode & 0o666)
163 163
164 164 # hold file generations to be performed on commit
165 165 self._filegenerators = {}
166 166 # hold callback to write pending data for hooks
167 167 self._pendingcallback = {}
168 168 # True is any pending data have been written ever
169 169 self._anypending = False
170 170 # holds callback to call when writing the transaction
171 171 self._finalizecallback = {}
172 172 # hold callback for post transaction close
173 173 self._postclosecallback = {}
174 174 # holds callbacks to call during abort
175 175 self._abortcallback = {}
176 176
177 177 def __del__(self):
178 178 if self.journal:
179 179 self._abort()
180 180
181 181 @active
182 182 def startgroup(self):
183 183 """delay registration of file entry
184 184
185 185 This is used by strip to delay vision of strip offset. The transaction
186 186 sees either none or all of the strip actions to be done."""
187 187 self._queue.append([])
188 188
189 189 @active
190 190 def endgroup(self):
191 191 """apply delayed registration of file entry.
192 192
193 193 This is used by strip to delay vision of strip offset. The transaction
194 194 sees either none or all of the strip actions to be done."""
195 195 q = self._queue.pop()
196 196 for f, o, data in q:
197 197 self._addentry(f, o, data)
198 198
199 199 @active
200 200 def add(self, file, offset, data=None):
201 201 """record the state of an append-only file before update"""
202 202 if file in self.map or file in self._backupmap:
203 203 return
204 204 if self._queue:
205 205 self._queue[-1].append((file, offset, data))
206 206 return
207 207
208 208 self._addentry(file, offset, data)
209 209
210 210 def _addentry(self, file, offset, data):
211 211 """add a append-only entry to memory and on-disk state"""
212 212 if file in self.map or file in self._backupmap:
213 213 return
214 214 self.entries.append((file, offset, data))
215 215 self.map[file] = len(self.entries) - 1
216 216 # add enough data to the journal to do the truncate
217 217 self.file.write("%s\0%d\n" % (file, offset))
218 218 self.file.flush()
219 219
220 220 @active
221 221 def addbackup(self, file, hardlink=True, location=''):
222 222 """Adds a backup of the file to the transaction
223 223
224 224 Calling addbackup() creates a hardlink backup of the specified file
225 225 that is used to recover the file in the event of the transaction
226 226 aborting.
227 227
228 228 * `file`: the file path, relative to .hg/store
229 229 * `hardlink`: use a hardlink to quickly create the backup
230 230 """
231 231 if self._queue:
232 232 msg = 'cannot use transaction.addbackup inside "group"'
233 233 raise error.ProgrammingError(msg)
234 234
235 235 if file in self.map or file in self._backupmap:
236 236 return
237 237 vfs = self._vfsmap[location]
238 238 dirname, filename = vfs.split(file)
239 239 backupfilename = "%s.backup.%s" % (self.journal, filename)
240 240 backupfile = vfs.reljoin(dirname, backupfilename)
241 241 if vfs.exists(file):
242 242 filepath = vfs.join(file)
243 243 backuppath = vfs.join(backupfile)
244 244 util.copyfile(filepath, backuppath, hardlink=hardlink)
245 245 else:
246 246 backupfile = ''
247 247
248 248 self._addbackupentry((location, file, backupfile, False))
249 249
250 250 def _addbackupentry(self, entry):
251 251 """register a new backup entry and write it to disk"""
252 252 self._backupentries.append(entry)
253 253 self._backupmap[entry[1]] = len(self._backupentries) - 1
254 254 self._backupsfile.write("%s\0%s\0%s\0%d\n" % entry)
255 255 self._backupsfile.flush()
256 256
257 257 @active
258 258 def registertmp(self, tmpfile, location=''):
259 259 """register a temporary transaction file
260 260
261 261 Such files will be deleted when the transaction exits (on both
262 262 failure and success).
263 263 """
264 264 self._addbackupentry((location, '', tmpfile, False))
265 265
266 266 @active
267 267 def addfilegenerator(self, genid, filenames, genfunc, order=0,
268 268 location=''):
269 269 """add a function to generates some files at transaction commit
270 270
271 271 The `genfunc` argument is a function capable of generating proper
272 272 content of each entry in the `filename` tuple.
273 273
274 274 At transaction close time, `genfunc` will be called with one file
275 275 object argument per entries in `filenames`.
276 276
277 277 The transaction itself is responsible for the backup, creation and
278 278 final write of such file.
279 279
280 280 The `genid` argument is used to ensure the same set of file is only
281 281 generated once. Call to `addfilegenerator` for a `genid` already
282 282 present will overwrite the old entry.
283 283
284 284 The `order` argument may be used to control the order in which multiple
285 285 generator will be executed.
286 286
287 287 The `location` arguments may be used to indicate the files are located
288 288 outside of the the standard directory for transaction. It should match
289 289 one of the key of the `transaction.vfsmap` dictionary.
290 290 """
291 291 # For now, we are unable to do proper backup and restore of custom vfs
292 292 # but for bookmarks that are handled outside this mechanism.
293 293 self._filegenerators[genid] = (order, filenames, genfunc, location)
294 294
295 @active
296 def removefilegenerator(self, genid):
297 """reverse of addfilegenerator, remove a file generator function"""
298 if genid in self._filegenerators:
299 del self._filegenerators[genid]
300
295 301 def _generatefiles(self, suffix='', group=gengroupall):
296 302 # write files registered for generation
297 303 any = False
298 304 for id, entry in sorted(self._filegenerators.iteritems()):
299 305 any = True
300 306 order, filenames, genfunc, location = entry
301 307
302 308 # for generation at closing, check if it's before or after finalize
303 309 postfinalize = group == gengrouppostfinalize
304 310 if (group != gengroupall and
305 311 (id in postfinalizegenerators) != (postfinalize)):
306 312 continue
307 313
308 314 vfs = self._vfsmap[location]
309 315 files = []
310 316 try:
311 317 for name in filenames:
312 318 name += suffix
313 319 if suffix:
314 320 self.registertmp(name, location=location)
315 321 else:
316 322 self.addbackup(name, location=location)
317 323 files.append(vfs(name, 'w', atomictemp=True,
318 324 checkambig=not suffix))
319 325 genfunc(*files)
320 326 finally:
321 327 for f in files:
322 328 f.close()
323 329 return any
324 330
325 331 @active
326 332 def find(self, file):
327 333 if file in self.map:
328 334 return self.entries[self.map[file]]
329 335 if file in self._backupmap:
330 336 return self._backupentries[self._backupmap[file]]
331 337 return None
332 338
333 339 @active
334 340 def replace(self, file, offset, data=None):
335 341 '''
336 342 replace can only replace already committed entries
337 343 that are not pending in the queue
338 344 '''
339 345
340 346 if file not in self.map:
341 347 raise KeyError(file)
342 348 index = self.map[file]
343 349 self.entries[index] = (file, offset, data)
344 350 self.file.write("%s\0%d\n" % (file, offset))
345 351 self.file.flush()
346 352
347 353 @active
348 354 def nest(self):
349 355 self.count += 1
350 356 self.usages += 1
351 357 return self
352 358
353 359 def release(self):
354 360 if self.count > 0:
355 361 self.usages -= 1
356 362 # if the transaction scopes are left without being closed, fail
357 363 if self.count > 0 and self.usages == 0:
358 364 self._abort()
359 365
360 366 def __enter__(self):
361 367 return self
362 368
363 369 def __exit__(self, exc_type, exc_val, exc_tb):
364 370 try:
365 371 if exc_type is None:
366 372 self.close()
367 373 finally:
368 374 self.release()
369 375
370 376 def running(self):
371 377 return self.count > 0
372 378
373 379 def addpending(self, category, callback):
374 380 """add a callback to be called when the transaction is pending
375 381
376 382 The transaction will be given as callback's first argument.
377 383
378 384 Category is a unique identifier to allow overwriting an old callback
379 385 with a newer callback.
380 386 """
381 387 self._pendingcallback[category] = callback
382 388
383 389 @active
384 390 def writepending(self):
385 391 '''write pending file to temporary version
386 392
387 393 This is used to allow hooks to view a transaction before commit'''
388 394 categories = sorted(self._pendingcallback)
389 395 for cat in categories:
390 396 # remove callback since the data will have been flushed
391 397 any = self._pendingcallback.pop(cat)(self)
392 398 self._anypending = self._anypending or any
393 399 self._anypending |= self._generatefiles(suffix='.pending')
394 400 return self._anypending
395 401
396 402 @active
397 403 def addfinalize(self, category, callback):
398 404 """add a callback to be called when the transaction is closed
399 405
400 406 The transaction will be given as callback's first argument.
401 407
402 408 Category is a unique identifier to allow overwriting old callbacks with
403 409 newer callbacks.
404 410 """
405 411 self._finalizecallback[category] = callback
406 412
407 413 @active
408 414 def addpostclose(self, category, callback):
409 415 """add a callback to be called after the transaction is closed
410 416
411 417 The transaction will be given as callback's first argument.
412 418
413 419 Category is a unique identifier to allow overwriting an old callback
414 420 with a newer callback.
415 421 """
416 422 self._postclosecallback[category] = callback
417 423
418 424 @active
419 425 def addabort(self, category, callback):
420 426 """add a callback to be called when the transaction is aborted.
421 427
422 428 The transaction will be given as the first argument to the callback.
423 429
424 430 Category is a unique identifier to allow overwriting an old callback
425 431 with a newer callback.
426 432 """
427 433 self._abortcallback[category] = callback
428 434
429 435 @active
430 436 def close(self):
431 437 '''commit the transaction'''
432 438 if self.count == 1:
433 439 self.validator(self) # will raise exception if needed
434 440 self.validator = None # Help prevent cycles.
435 441 self._generatefiles(group=gengroupprefinalize)
436 442 categories = sorted(self._finalizecallback)
437 443 for cat in categories:
438 444 self._finalizecallback[cat](self)
439 445 # Prevent double usage and help clear cycles.
440 446 self._finalizecallback = None
441 447 self._generatefiles(group=gengrouppostfinalize)
442 448
443 449 self.count -= 1
444 450 if self.count != 0:
445 451 return
446 452 self.file.close()
447 453 self._backupsfile.close()
448 454 # cleanup temporary files
449 455 for l, f, b, c in self._backupentries:
450 456 if l not in self._vfsmap and c:
451 457 self.report("couldn't remove %s: unknown cache location %s\n"
452 458 % (b, l))
453 459 continue
454 460 vfs = self._vfsmap[l]
455 461 if not f and b and vfs.exists(b):
456 462 try:
457 463 vfs.unlink(b)
458 464 except (IOError, OSError, error.Abort) as inst:
459 465 if not c:
460 466 raise
461 467 # Abort may be raise by read only opener
462 468 self.report("couldn't remove %s: %s\n"
463 469 % (vfs.join(b), inst))
464 470 self.entries = []
465 471 self._writeundo()
466 472 if self.after:
467 473 self.after()
468 474 self.after = None # Help prevent cycles.
469 475 if self.opener.isfile(self._backupjournal):
470 476 self.opener.unlink(self._backupjournal)
471 477 if self.opener.isfile(self.journal):
472 478 self.opener.unlink(self.journal)
473 479 for l, _f, b, c in self._backupentries:
474 480 if l not in self._vfsmap and c:
475 481 self.report("couldn't remove %s: unknown cache location"
476 482 "%s\n" % (b, l))
477 483 continue
478 484 vfs = self._vfsmap[l]
479 485 if b and vfs.exists(b):
480 486 try:
481 487 vfs.unlink(b)
482 488 except (IOError, OSError, error.Abort) as inst:
483 489 if not c:
484 490 raise
485 491 # Abort may be raise by read only opener
486 492 self.report("couldn't remove %s: %s\n"
487 493 % (vfs.join(b), inst))
488 494 self._backupentries = []
489 495 self.journal = None
490 496
491 497 self.releasefn(self, True) # notify success of closing transaction
492 498 self.releasefn = None # Help prevent cycles.
493 499
494 500 # run post close action
495 501 categories = sorted(self._postclosecallback)
496 502 for cat in categories:
497 503 self._postclosecallback[cat](self)
498 504 # Prevent double usage and help clear cycles.
499 505 self._postclosecallback = None
500 506
501 507 @active
502 508 def abort(self):
503 509 '''abort the transaction (generally called on error, or when the
504 510 transaction is not explicitly committed before going out of
505 511 scope)'''
506 512 self._abort()
507 513
508 514 def _writeundo(self):
509 515 """write transaction data for possible future undo call"""
510 516 if self.undoname is None:
511 517 return
512 518 undobackupfile = self.opener.open("%s.backupfiles" % self.undoname, 'w')
513 519 undobackupfile.write('%d\n' % version)
514 520 for l, f, b, c in self._backupentries:
515 521 if not f: # temporary file
516 522 continue
517 523 if not b:
518 524 u = ''
519 525 else:
520 526 if l not in self._vfsmap and c:
521 527 self.report("couldn't remove %s: unknown cache location"
522 528 "%s\n" % (b, l))
523 529 continue
524 530 vfs = self._vfsmap[l]
525 531 base, name = vfs.split(b)
526 532 assert name.startswith(self.journal), name
527 533 uname = name.replace(self.journal, self.undoname, 1)
528 534 u = vfs.reljoin(base, uname)
529 535 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
530 536 undobackupfile.write("%s\0%s\0%s\0%d\n" % (l, f, u, c))
531 537 undobackupfile.close()
532 538
533 539
534 540 def _abort(self):
535 541 self.count = 0
536 542 self.usages = 0
537 543 self.file.close()
538 544 self._backupsfile.close()
539 545
540 546 try:
541 547 if not self.entries and not self._backupentries:
542 548 if self._backupjournal:
543 549 self.opener.unlink(self._backupjournal)
544 550 if self.journal:
545 551 self.opener.unlink(self.journal)
546 552 return
547 553
548 554 self.report(_("transaction abort!\n"))
549 555
550 556 try:
551 557 for cat in sorted(self._abortcallback):
552 558 self._abortcallback[cat](self)
553 559 # Prevent double usage and help clear cycles.
554 560 self._abortcallback = None
555 561 _playback(self.journal, self.report, self.opener, self._vfsmap,
556 562 self.entries, self._backupentries, False)
557 563 self.report(_("rollback completed\n"))
558 564 except BaseException:
559 565 self.report(_("rollback failed - please run hg recover\n"))
560 566 finally:
561 567 self.journal = None
562 568 self.releasefn(self, False) # notify failure of transaction
563 569 self.releasefn = None # Help prevent cycles.
564 570
565 571 def rollback(opener, vfsmap, file, report):
566 572 """Rolls back the transaction contained in the given file
567 573
568 574 Reads the entries in the specified file, and the corresponding
569 575 '*.backupfiles' file, to recover from an incomplete transaction.
570 576
571 577 * `file`: a file containing a list of entries, specifying where
572 578 to truncate each file. The file should contain a list of
573 579 file\0offset pairs, delimited by newlines. The corresponding
574 580 '*.backupfiles' file should contain a list of file\0backupfile
575 581 pairs, delimited by \0.
576 582 """
577 583 entries = []
578 584 backupentries = []
579 585
580 586 fp = opener.open(file)
581 587 lines = fp.readlines()
582 588 fp.close()
583 589 for l in lines:
584 590 try:
585 591 f, o = l.split('\0')
586 592 entries.append((f, int(o), None))
587 593 except ValueError:
588 594 report(_("couldn't read journal entry %r!\n") % l)
589 595
590 596 backupjournal = "%s.backupfiles" % file
591 597 if opener.exists(backupjournal):
592 598 fp = opener.open(backupjournal)
593 599 lines = fp.readlines()
594 600 if lines:
595 601 ver = lines[0][:-1]
596 602 if ver == str(version):
597 603 for line in lines[1:]:
598 604 if line:
599 605 # Shave off the trailing newline
600 606 line = line[:-1]
601 607 l, f, b, c = line.split('\0')
602 608 backupentries.append((l, f, b, bool(c)))
603 609 else:
604 610 report(_("journal was created by a different version of "
605 611 "Mercurial\n"))
606 612
607 613 _playback(file, report, opener, vfsmap, entries, backupentries)
@@ -1,957 +1,957
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > rebase=
4 4 > drawdag=$TESTDIR/drawdag.py
5 5 >
6 6 > [phases]
7 7 > publish=False
8 8 >
9 9 > [alias]
10 10 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
11 11 > EOF
12 12
13 13
14 14 $ hg init a
15 15 $ cd a
16 16 $ hg unbundle "$TESTDIR/bundles/rebase.hg"
17 17 adding changesets
18 18 adding manifests
19 19 adding file changes
20 20 added 8 changesets with 7 changes to 7 files (+2 heads)
21 21 (run 'hg heads' to see heads, 'hg merge' to merge)
22 22 $ hg up tip
23 23 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 24 $ cd ..
25 25
26 26
27 27 Rebasing
28 28 D onto H - simple rebase:
29 29 (this also tests that editor is invoked if '--edit' is specified, and that we
30 30 can abort or warn for colliding untracked files)
31 31
32 32 $ hg clone -q -u . a a1
33 33 $ cd a1
34 34
35 35 $ hg tglog
36 36 @ 7: 'H'
37 37 |
38 38 | o 6: 'G'
39 39 |/|
40 40 o | 5: 'F'
41 41 | |
42 42 | o 4: 'E'
43 43 |/
44 44 | o 3: 'D'
45 45 | |
46 46 | o 2: 'C'
47 47 | |
48 48 | o 1: 'B'
49 49 |/
50 50 o 0: 'A'
51 51
52 52
53 53 $ hg status --rev "3^1" --rev 3
54 54 A D
55 55 $ echo collide > D
56 56 $ HGEDITOR=cat hg rebase -s 3 -d 7 --edit --config merge.checkunknown=warn
57 57 rebasing 3:32af7686d403 "D"
58 58 D: replacing untracked file
59 59 D
60 60
61 61
62 62 HG: Enter commit message. Lines beginning with 'HG:' are removed.
63 63 HG: Leave message empty to abort commit.
64 64 HG: --
65 65 HG: user: Nicolas Dumazet <nicdumz.commits@gmail.com>
66 66 HG: branch 'default'
67 67 HG: added D
68 68 saved backup bundle to $TESTTMP/a1/.hg/strip-backup/32af7686d403-6f7dface-backup.hg (glob)
69 69 $ cat D.orig
70 70 collide
71 71 $ rm D.orig
72 72
73 73 $ hg tglog
74 74 o 7: 'D'
75 75 |
76 76 @ 6: 'H'
77 77 |
78 78 | o 5: 'G'
79 79 |/|
80 80 o | 4: 'F'
81 81 | |
82 82 | o 3: 'E'
83 83 |/
84 84 | o 2: 'C'
85 85 | |
86 86 | o 1: 'B'
87 87 |/
88 88 o 0: 'A'
89 89
90 90 $ cd ..
91 91
92 92
93 93 D onto F - intermediate point:
94 94 (this also tests that editor is not invoked if '--edit' is not specified, and
95 95 that we can ignore for colliding untracked files)
96 96
97 97 $ hg clone -q -u . a a2
98 98 $ cd a2
99 99 $ echo collide > D
100 100
101 101 $ HGEDITOR=cat hg rebase -s 3 -d 5 --config merge.checkunknown=ignore
102 102 rebasing 3:32af7686d403 "D"
103 103 saved backup bundle to $TESTTMP/a2/.hg/strip-backup/32af7686d403-6f7dface-backup.hg (glob)
104 104 $ cat D.orig
105 105 collide
106 106 $ rm D.orig
107 107
108 108 $ hg tglog
109 109 o 7: 'D'
110 110 |
111 111 | @ 6: 'H'
112 112 |/
113 113 | o 5: 'G'
114 114 |/|
115 115 o | 4: 'F'
116 116 | |
117 117 | o 3: 'E'
118 118 |/
119 119 | o 2: 'C'
120 120 | |
121 121 | o 1: 'B'
122 122 |/
123 123 o 0: 'A'
124 124
125 125 $ cd ..
126 126
127 127
128 128 E onto H - skip of G:
129 129 (this also tests that we can overwrite untracked files and don't create backups
130 130 if they have the same contents)
131 131
132 132 $ hg clone -q -u . a a3
133 133 $ cd a3
134 134 $ hg cat -r 4 E | tee E
135 135 E
136 136
137 137 $ hg rebase -s 4 -d 7
138 138 rebasing 4:9520eea781bc "E"
139 139 rebasing 6:eea13746799a "G"
140 140 note: rebase of 6:eea13746799a created no changes to commit
141 141 saved backup bundle to $TESTTMP/a3/.hg/strip-backup/9520eea781bc-fcd8edd4-backup.hg (glob)
142 142 $ f E.orig
143 143 E.orig: file not found
144 144
145 145 $ hg tglog
146 146 o 6: 'E'
147 147 |
148 148 @ 5: 'H'
149 149 |
150 150 o 4: 'F'
151 151 |
152 152 | o 3: 'D'
153 153 | |
154 154 | o 2: 'C'
155 155 | |
156 156 | o 1: 'B'
157 157 |/
158 158 o 0: 'A'
159 159
160 160 $ cd ..
161 161
162 162
163 163 F onto E - rebase of a branching point (skip G):
164 164
165 165 $ hg clone -q -u . a a4
166 166 $ cd a4
167 167
168 168 $ hg rebase -s 5 -d 4
169 169 rebasing 5:24b6387c8c8c "F"
170 170 rebasing 6:eea13746799a "G"
171 171 note: rebase of 6:eea13746799a created no changes to commit
172 172 rebasing 7:02de42196ebe "H" (tip)
173 173 saved backup bundle to $TESTTMP/a4/.hg/strip-backup/24b6387c8c8c-c3fe765d-backup.hg (glob)
174 174
175 175 $ hg tglog
176 176 @ 6: 'H'
177 177 |
178 178 o 5: 'F'
179 179 |
180 180 o 4: 'E'
181 181 |
182 182 | o 3: 'D'
183 183 | |
184 184 | o 2: 'C'
185 185 | |
186 186 | o 1: 'B'
187 187 |/
188 188 o 0: 'A'
189 189
190 190 $ cd ..
191 191
192 192
193 193 G onto H - merged revision having a parent in ancestors of target:
194 194
195 195 $ hg clone -q -u . a a5
196 196 $ cd a5
197 197
198 198 $ hg rebase -s 6 -d 7
199 199 rebasing 6:eea13746799a "G"
200 200 saved backup bundle to $TESTTMP/a5/.hg/strip-backup/eea13746799a-883828ed-backup.hg (glob)
201 201
202 202 $ hg tglog
203 203 o 7: 'G'
204 204 |\
205 205 | @ 6: 'H'
206 206 | |
207 207 | o 5: 'F'
208 208 | |
209 209 o | 4: 'E'
210 210 |/
211 211 | o 3: 'D'
212 212 | |
213 213 | o 2: 'C'
214 214 | |
215 215 | o 1: 'B'
216 216 |/
217 217 o 0: 'A'
218 218
219 219 $ cd ..
220 220
221 221
222 222 F onto B - G maintains E as parent:
223 223
224 224 $ hg clone -q -u . a a6
225 225 $ cd a6
226 226
227 227 $ hg rebase -s 5 -d 1
228 228 rebasing 5:24b6387c8c8c "F"
229 229 rebasing 6:eea13746799a "G"
230 230 rebasing 7:02de42196ebe "H" (tip)
231 231 saved backup bundle to $TESTTMP/a6/.hg/strip-backup/24b6387c8c8c-c3fe765d-backup.hg (glob)
232 232
233 233 $ hg tglog
234 234 @ 7: 'H'
235 235 |
236 236 | o 6: 'G'
237 237 |/|
238 238 o | 5: 'F'
239 239 | |
240 240 | o 4: 'E'
241 241 | |
242 242 | | o 3: 'D'
243 243 | | |
244 244 +---o 2: 'C'
245 245 | |
246 246 o | 1: 'B'
247 247 |/
248 248 o 0: 'A'
249 249
250 250 $ cd ..
251 251
252 252
253 253 These will fail (using --source):
254 254
255 255 G onto F - rebase onto an ancestor:
256 256
257 257 $ hg clone -q -u . a a7
258 258 $ cd a7
259 259
260 260 $ hg rebase -s 6 -d 5
261 261 nothing to rebase
262 262 [1]
263 263
264 264 F onto G - rebase onto a descendant:
265 265
266 266 $ hg rebase -s 5 -d 6
267 267 abort: source is ancestor of destination
268 268 [255]
269 269
270 270 G onto B - merge revision with both parents not in ancestors of target:
271 271
272 272 $ hg rebase -s 6 -d 1
273 273 rebasing 6:eea13746799a "G"
274 274 transaction abort!
275 275 rollback completed
276 276 abort: cannot use revision 6 as base, result would have 3 parents
277 277 [255]
278 278 $ hg rebase --abort
279 279 rebase aborted
280 280
281 281 These will abort gracefully (using --base):
282 282
283 283 G onto G - rebase onto same changeset:
284 284
285 285 $ hg rebase -b 6 -d 6
286 286 nothing to rebase - eea13746799a is both "base" and destination
287 287 [1]
288 288
289 289 G onto F - rebase onto an ancestor:
290 290
291 291 $ hg rebase -b 6 -d 5
292 292 nothing to rebase
293 293 [1]
294 294
295 295 F onto G - rebase onto a descendant:
296 296
297 297 $ hg rebase -b 5 -d 6
298 298 nothing to rebase - "base" 24b6387c8c8c is already an ancestor of destination eea13746799a
299 299 [1]
300 300
301 301 C onto A - rebase onto an ancestor:
302 302
303 303 $ hg rebase -d 0 -s 2
304 304 rebasing 2:5fddd98957c8 "C"
305 305 rebasing 3:32af7686d403 "D"
306 306 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/5fddd98957c8-f9244fa1-backup.hg (glob)
307 307 $ hg tglog
308 308 o 7: 'D'
309 309 |
310 310 o 6: 'C'
311 311 |
312 312 | @ 5: 'H'
313 313 | |
314 314 | | o 4: 'G'
315 315 | |/|
316 316 | o | 3: 'F'
317 317 |/ /
318 318 | o 2: 'E'
319 319 |/
320 320 | o 1: 'B'
321 321 |/
322 322 o 0: 'A'
323 323
324 324
325 325 Check rebasing public changeset
326 326
327 327 $ hg pull --config phases.publish=True -q -r 6 . # update phase of 6
328 328 $ hg rebase -d 0 -b 6
329 329 nothing to rebase
330 330 [1]
331 331 $ hg rebase -d 5 -b 6
332 332 abort: can't rebase public changeset e1c4361dd923
333 333 (see 'hg help phases' for details)
334 334 [255]
335 335 $ hg rebase -d 5 -r '1 + (6::)'
336 336 abort: can't rebase public changeset e1c4361dd923
337 337 (see 'hg help phases' for details)
338 338 [255]
339 339
340 340 $ hg rebase -d 5 -b 6 --keep
341 341 rebasing 6:e1c4361dd923 "C"
342 342 rebasing 7:c9659aac0000 "D" (tip)
343 343
344 344 Check rebasing mutable changeset
345 345 Source phase greater or equal to destination phase: new changeset get the phase of source:
346 346 $ hg id -n
347 347 5
348 348 $ hg rebase -s9 -d0
349 349 rebasing 9:2b23e52411f4 "D" (tip)
350 350 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/2b23e52411f4-f942decf-backup.hg (glob)
351 351 $ hg id -n # check we updated back to parent
352 352 5
353 353 $ hg log --template "{phase}\n" -r 9
354 354 draft
355 355 $ hg rebase -s9 -d1
356 356 rebasing 9:2cb10d0cfc6c "D" (tip)
357 357 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/2cb10d0cfc6c-ddb0f256-backup.hg (glob)
358 358 $ hg log --template "{phase}\n" -r 9
359 359 draft
360 360 $ hg phase --force --secret 9
361 361 $ hg rebase -s9 -d0
362 362 rebasing 9:c5b12b67163a "D" (tip)
363 363 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/c5b12b67163a-4e372053-backup.hg (glob)
364 364 $ hg log --template "{phase}\n" -r 9
365 365 secret
366 366 $ hg rebase -s9 -d1
367 367 rebasing 9:2a0524f868ac "D" (tip)
368 368 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/2a0524f868ac-cefd8574-backup.hg (glob)
369 369 $ hg log --template "{phase}\n" -r 9
370 370 secret
371 371 Source phase lower than destination phase: new changeset get the phase of destination:
372 372 $ hg rebase -s8 -d9
373 373 rebasing 8:6d4f22462821 "C"
374 374 saved backup bundle to $TESTTMP/a7/.hg/strip-backup/6d4f22462821-3441f70b-backup.hg (glob)
375 375 $ hg log --template "{phase}\n" -r 'rev(9)'
376 376 secret
377 377
378 378 $ cd ..
379 379
380 380 Test for revset
381 381
382 382 We need a bit different graph
383 383 All destination are B
384 384
385 385 $ hg init ah
386 386 $ cd ah
387 387 $ hg unbundle "$TESTDIR/bundles/rebase-revset.hg"
388 388 adding changesets
389 389 adding manifests
390 390 adding file changes
391 391 added 9 changesets with 9 changes to 9 files (+2 heads)
392 392 (run 'hg heads' to see heads, 'hg merge' to merge)
393 393 $ hg tglog
394 394 o 8: 'I'
395 395 |
396 396 o 7: 'H'
397 397 |
398 398 o 6: 'G'
399 399 |
400 400 | o 5: 'F'
401 401 | |
402 402 | o 4: 'E'
403 403 |/
404 404 o 3: 'D'
405 405 |
406 406 o 2: 'C'
407 407 |
408 408 | o 1: 'B'
409 409 |/
410 410 o 0: 'A'
411 411
412 412 $ cd ..
413 413
414 414
415 415 Simple case with keep:
416 416
417 417 Source on have two descendant heads but ask for one
418 418
419 419 $ hg clone -q -u . ah ah1
420 420 $ cd ah1
421 421 $ hg rebase -r '2::8' -d 1
422 422 abort: can't remove original changesets with unrebased descendants
423 423 (use --keep to keep original changesets)
424 424 [255]
425 425 $ hg rebase -r '2::8' -d 1 -k
426 426 rebasing 2:c9e50f6cdc55 "C"
427 427 rebasing 3:ffd453c31098 "D"
428 428 rebasing 6:3d8a618087a7 "G"
429 429 rebasing 7:72434a4e60b0 "H"
430 430 rebasing 8:479ddb54a924 "I" (tip)
431 431 $ hg tglog
432 432 o 13: 'I'
433 433 |
434 434 o 12: 'H'
435 435 |
436 436 o 11: 'G'
437 437 |
438 438 o 10: 'D'
439 439 |
440 440 o 9: 'C'
441 441 |
442 442 | o 8: 'I'
443 443 | |
444 444 | o 7: 'H'
445 445 | |
446 446 | o 6: 'G'
447 447 | |
448 448 | | o 5: 'F'
449 449 | | |
450 450 | | o 4: 'E'
451 451 | |/
452 452 | o 3: 'D'
453 453 | |
454 454 | o 2: 'C'
455 455 | |
456 456 o | 1: 'B'
457 457 |/
458 458 o 0: 'A'
459 459
460 460
461 461 $ cd ..
462 462
463 463 Base on have one descendant heads we ask for but common ancestor have two
464 464
465 465 $ hg clone -q -u . ah ah2
466 466 $ cd ah2
467 467 $ hg rebase -r '3::8' -d 1
468 468 abort: can't remove original changesets with unrebased descendants
469 469 (use --keep to keep original changesets)
470 470 [255]
471 471 $ hg rebase -r '3::8' -d 1 --keep
472 472 rebasing 3:ffd453c31098 "D"
473 473 rebasing 6:3d8a618087a7 "G"
474 474 rebasing 7:72434a4e60b0 "H"
475 475 rebasing 8:479ddb54a924 "I" (tip)
476 476 $ hg tglog
477 477 o 12: 'I'
478 478 |
479 479 o 11: 'H'
480 480 |
481 481 o 10: 'G'
482 482 |
483 483 o 9: 'D'
484 484 |
485 485 | o 8: 'I'
486 486 | |
487 487 | o 7: 'H'
488 488 | |
489 489 | o 6: 'G'
490 490 | |
491 491 | | o 5: 'F'
492 492 | | |
493 493 | | o 4: 'E'
494 494 | |/
495 495 | o 3: 'D'
496 496 | |
497 497 | o 2: 'C'
498 498 | |
499 499 o | 1: 'B'
500 500 |/
501 501 o 0: 'A'
502 502
503 503
504 504 $ cd ..
505 505
506 506 rebase subset
507 507
508 508 $ hg clone -q -u . ah ah3
509 509 $ cd ah3
510 510 $ hg rebase -r '3::7' -d 1
511 511 abort: can't remove original changesets with unrebased descendants
512 512 (use --keep to keep original changesets)
513 513 [255]
514 514 $ hg rebase -r '3::7' -d 1 --keep
515 515 rebasing 3:ffd453c31098 "D"
516 516 rebasing 6:3d8a618087a7 "G"
517 517 rebasing 7:72434a4e60b0 "H"
518 518 $ hg tglog
519 519 o 11: 'H'
520 520 |
521 521 o 10: 'G'
522 522 |
523 523 o 9: 'D'
524 524 |
525 525 | o 8: 'I'
526 526 | |
527 527 | o 7: 'H'
528 528 | |
529 529 | o 6: 'G'
530 530 | |
531 531 | | o 5: 'F'
532 532 | | |
533 533 | | o 4: 'E'
534 534 | |/
535 535 | o 3: 'D'
536 536 | |
537 537 | o 2: 'C'
538 538 | |
539 539 o | 1: 'B'
540 540 |/
541 541 o 0: 'A'
542 542
543 543
544 544 $ cd ..
545 545
546 546 rebase subset with multiple head
547 547
548 548 $ hg clone -q -u . ah ah4
549 549 $ cd ah4
550 550 $ hg rebase -r '3::(7+5)' -d 1
551 551 abort: can't remove original changesets with unrebased descendants
552 552 (use --keep to keep original changesets)
553 553 [255]
554 554 $ hg rebase -r '3::(7+5)' -d 1 --keep
555 555 rebasing 3:ffd453c31098 "D"
556 556 rebasing 4:c01897464e7f "E"
557 557 rebasing 5:41bfcc75ed73 "F"
558 558 rebasing 6:3d8a618087a7 "G"
559 559 rebasing 7:72434a4e60b0 "H"
560 560 $ hg tglog
561 561 o 13: 'H'
562 562 |
563 563 o 12: 'G'
564 564 |
565 565 | o 11: 'F'
566 566 | |
567 567 | o 10: 'E'
568 568 |/
569 569 o 9: 'D'
570 570 |
571 571 | o 8: 'I'
572 572 | |
573 573 | o 7: 'H'
574 574 | |
575 575 | o 6: 'G'
576 576 | |
577 577 | | o 5: 'F'
578 578 | | |
579 579 | | o 4: 'E'
580 580 | |/
581 581 | o 3: 'D'
582 582 | |
583 583 | o 2: 'C'
584 584 | |
585 585 o | 1: 'B'
586 586 |/
587 587 o 0: 'A'
588 588
589 589
590 590 $ cd ..
591 591
592 592 More advanced tests
593 593
594 594 rebase on ancestor with revset
595 595
596 596 $ hg clone -q -u . ah ah5
597 597 $ cd ah5
598 598 $ hg rebase -r '6::' -d 2
599 599 rebasing 6:3d8a618087a7 "G"
600 600 rebasing 7:72434a4e60b0 "H"
601 601 rebasing 8:479ddb54a924 "I" (tip)
602 602 saved backup bundle to $TESTTMP/ah5/.hg/strip-backup/3d8a618087a7-b4f73f31-backup.hg (glob)
603 603 $ hg tglog
604 604 o 8: 'I'
605 605 |
606 606 o 7: 'H'
607 607 |
608 608 o 6: 'G'
609 609 |
610 610 | o 5: 'F'
611 611 | |
612 612 | o 4: 'E'
613 613 | |
614 614 | o 3: 'D'
615 615 |/
616 616 o 2: 'C'
617 617 |
618 618 | o 1: 'B'
619 619 |/
620 620 o 0: 'A'
621 621
622 622 $ cd ..
623 623
624 624
625 625 rebase with multiple root.
626 626 We rebase E and G on B
627 627 We would expect heads are I, F if it was supported
628 628
629 629 $ hg clone -q -u . ah ah6
630 630 $ cd ah6
631 631 $ hg rebase -r '(4+6)::' -d 1
632 632 rebasing 4:c01897464e7f "E"
633 633 rebasing 5:41bfcc75ed73 "F"
634 634 rebasing 6:3d8a618087a7 "G"
635 635 rebasing 7:72434a4e60b0 "H"
636 636 rebasing 8:479ddb54a924 "I" (tip)
637 637 saved backup bundle to $TESTTMP/ah6/.hg/strip-backup/3d8a618087a7-aae93a24-backup.hg (glob)
638 638 $ hg tglog
639 639 o 8: 'I'
640 640 |
641 641 o 7: 'H'
642 642 |
643 643 o 6: 'G'
644 644 |
645 645 | o 5: 'F'
646 646 | |
647 647 | o 4: 'E'
648 648 |/
649 649 | o 3: 'D'
650 650 | |
651 651 | o 2: 'C'
652 652 | |
653 653 o | 1: 'B'
654 654 |/
655 655 o 0: 'A'
656 656
657 657 $ cd ..
658 658
659 659 More complex rebase with multiple roots
660 660 each root have a different common ancestor with the destination and this is a detach
661 661
662 662 (setup)
663 663
664 664 $ hg clone -q -u . a a8
665 665 $ cd a8
666 666 $ echo I > I
667 667 $ hg add I
668 668 $ hg commit -m I
669 669 $ hg up 4
670 670 1 files updated, 0 files merged, 3 files removed, 0 files unresolved
671 671 $ echo I > J
672 672 $ hg add J
673 673 $ hg commit -m J
674 674 created new head
675 675 $ echo I > K
676 676 $ hg add K
677 677 $ hg commit -m K
678 678 $ hg tglog
679 679 @ 10: 'K'
680 680 |
681 681 o 9: 'J'
682 682 |
683 683 | o 8: 'I'
684 684 | |
685 685 | o 7: 'H'
686 686 | |
687 687 +---o 6: 'G'
688 688 | |/
689 689 | o 5: 'F'
690 690 | |
691 691 o | 4: 'E'
692 692 |/
693 693 | o 3: 'D'
694 694 | |
695 695 | o 2: 'C'
696 696 | |
697 697 | o 1: 'B'
698 698 |/
699 699 o 0: 'A'
700 700
701 701 (actual test)
702 702
703 703 $ hg rebase --dest 'desc(G)' --rev 'desc(K) + desc(I)'
704 704 rebasing 8:e7ec4e813ba6 "I"
705 705 rebasing 10:23a4ace37988 "K" (tip)
706 706 saved backup bundle to $TESTTMP/a8/.hg/strip-backup/23a4ace37988-b06984b3-backup.hg (glob)
707 707 $ hg log --rev 'children(desc(G))'
708 708 changeset: 9:adb617877056
709 709 parent: 6:eea13746799a
710 710 user: test
711 711 date: Thu Jan 01 00:00:00 1970 +0000
712 712 summary: I
713 713
714 714 changeset: 10:882431a34a0e
715 715 tag: tip
716 716 parent: 6:eea13746799a
717 717 user: test
718 718 date: Thu Jan 01 00:00:00 1970 +0000
719 719 summary: K
720 720
721 721 $ hg tglog
722 722 @ 10: 'K'
723 723 |
724 724 | o 9: 'I'
725 725 |/
726 726 | o 8: 'J'
727 727 | |
728 728 | | o 7: 'H'
729 729 | | |
730 730 o---+ 6: 'G'
731 731 |/ /
732 732 | o 5: 'F'
733 733 | |
734 734 o | 4: 'E'
735 735 |/
736 736 | o 3: 'D'
737 737 | |
738 738 | o 2: 'C'
739 739 | |
740 740 | o 1: 'B'
741 741 |/
742 742 o 0: 'A'
743 743
744 744
745 745 Test that rebase is not confused by $CWD disappearing during rebase (issue4121)
746 746
747 747 $ cd ..
748 748 $ hg init cwd-vanish
749 749 $ cd cwd-vanish
750 750 $ touch initial-file
751 751 $ hg add initial-file
752 752 $ hg commit -m 'initial commit'
753 753 $ touch dest-file
754 754 $ hg add dest-file
755 755 $ hg commit -m 'dest commit'
756 756 $ hg up 0
757 757 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
758 758 $ touch other-file
759 759 $ hg add other-file
760 760 $ hg commit -m 'first source commit'
761 761 created new head
762 762 $ mkdir subdir
763 763 $ cd subdir
764 764 $ touch subfile
765 765 $ hg add subfile
766 766 $ hg commit -m 'second source with subdir'
767 767 #if rmcwd
768 768 $ hg rebase -b . -d 1 --traceback
769 769 rebasing 2:779a07b1b7a0 "first source commit"
770 770 current directory was removed
771 771 (consider changing to repo root: $TESTTMP/cwd-vanish)
772 772 rebasing 3:a7d6f3a00bf3 "second source with subdir" (tip)
773 773 saved backup bundle to $TESTTMP/cwd-vanish/.hg/strip-backup/779a07b1b7a0-853e0073-backup.hg (glob)
774 774 #else
775 775 $ hg rebase -b . -d 1 --traceback
776 776 rebasing 2:779a07b1b7a0 "first source commit"
777 777 rebasing 3:a7d6f3a00bf3 "second source with subdir" (tip)
778 778 saved backup bundle to $TESTTMP/cwd-vanish/.hg/strip-backup/779a07b1b7a0-853e0073-backup.hg (glob)
779 779 #endif
780 780
781 781 Get back to the root of cwd-vanish. Note that even though `cd ..`
782 782 works on most systems, it does not work on FreeBSD 10, so we use an
783 783 absolute path to get back to the repository.
784 784 $ cd $TESTTMP
785 785
786 786 Test that rebase is done in topo order (issue5370)
787 787
788 788 $ hg init order
789 789 $ cd order
790 790 $ touch a && hg add a && hg ci -m A
791 791 $ touch b && hg add b && hg ci -m B
792 792 $ touch c && hg add c && hg ci -m C
793 793 $ hg up 1
794 794 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
795 795 $ touch d && hg add d && hg ci -m D
796 796 created new head
797 797 $ hg up 2
798 798 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
799 799 $ touch e && hg add e && hg ci -m E
800 800 $ hg up 3
801 801 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
802 802 $ touch f && hg add f && hg ci -m F
803 803 $ hg up 0
804 804 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
805 805 $ touch g && hg add g && hg ci -m G
806 806 created new head
807 807
808 808 $ hg tglog
809 809 @ 6: 'G'
810 810 |
811 811 | o 5: 'F'
812 812 | |
813 813 | | o 4: 'E'
814 814 | | |
815 815 | o | 3: 'D'
816 816 | | |
817 817 | | o 2: 'C'
818 818 | |/
819 819 | o 1: 'B'
820 820 |/
821 821 o 0: 'A'
822 822
823 823
824 824 $ hg rebase -s 1 -d 6
825 825 rebasing 1:76035bbd54bd "B"
826 826 rebasing 2:d84f5cfaaf14 "C"
827 827 rebasing 4:82ae8dc7a9b7 "E"
828 828 rebasing 3:ab709c9f7171 "D"
829 829 rebasing 5:412b391de760 "F"
830 830 saved backup bundle to $TESTTMP/order/.hg/strip-backup/76035bbd54bd-e341bc99-backup.hg (glob)
831 831
832 832 $ hg tglog
833 833 o 6: 'F'
834 834 |
835 835 o 5: 'D'
836 836 |
837 837 | o 4: 'E'
838 838 | |
839 839 | o 3: 'C'
840 840 |/
841 841 o 2: 'B'
842 842 |
843 843 @ 1: 'G'
844 844 |
845 845 o 0: 'A'
846 846
847 847
848 848 Test experimental revset
849 849 ========================
850 850
851 851 $ cd ../cwd-vanish
852 852
853 853 Make the repo a bit more interesting
854 854
855 855 $ hg up 1
856 856 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
857 857 $ echo aaa > aaa
858 858 $ hg add aaa
859 859 $ hg commit -m aaa
860 860 created new head
861 861 $ hg log -G
862 862 @ changeset: 4:5f7bc9025ed2
863 863 | tag: tip
864 864 | parent: 1:58d79cc1cf43
865 865 | user: test
866 866 | date: Thu Jan 01 00:00:00 1970 +0000
867 867 | summary: aaa
868 868 |
869 869 | o changeset: 3:1910d5ff34ea
870 870 | | user: test
871 871 | | date: Thu Jan 01 00:00:00 1970 +0000
872 872 | | summary: second source with subdir
873 873 | |
874 874 | o changeset: 2:82901330b6ef
875 875 |/ user: test
876 876 | date: Thu Jan 01 00:00:00 1970 +0000
877 877 | summary: first source commit
878 878 |
879 879 o changeset: 1:58d79cc1cf43
880 880 | user: test
881 881 | date: Thu Jan 01 00:00:00 1970 +0000
882 882 | summary: dest commit
883 883 |
884 884 o changeset: 0:e94b687f7da3
885 885 user: test
886 886 date: Thu Jan 01 00:00:00 1970 +0000
887 887 summary: initial commit
888 888
889 889
890 890 Testing from lower head
891 891
892 892 $ hg up 3
893 893 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
894 894 $ hg log -r '_destrebase()'
895 895 changeset: 4:5f7bc9025ed2
896 896 tag: tip
897 897 parent: 1:58d79cc1cf43
898 898 user: test
899 899 date: Thu Jan 01 00:00:00 1970 +0000
900 900 summary: aaa
901 901
902 902
903 903 Testing from upper head
904 904
905 905 $ hg log -r '_destrebase(4)'
906 906 changeset: 3:1910d5ff34ea
907 907 user: test
908 908 date: Thu Jan 01 00:00:00 1970 +0000
909 909 summary: second source with subdir
910 910
911 911 $ hg up 4
912 912 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
913 913 $ hg log -r '_destrebase()'
914 914 changeset: 3:1910d5ff34ea
915 915 user: test
916 916 date: Thu Jan 01 00:00:00 1970 +0000
917 917 summary: second source with subdir
918 918
919 919 Testing rebase being called inside another transaction
920 920
921 921 $ cd $TESTTMP
922 922 $ hg init tr-state
923 923 $ cd tr-state
924 924 $ cat > $TESTTMP/wraprebase.py <<EOF
925 925 > from __future__ import absolute_import
926 926 > from mercurial import extensions
927 927 > def _rebase(orig, ui, repo, *args, **kwargs):
928 928 > with repo.wlock():
929 929 > with repo.lock():
930 930 > with repo.transaction('wrappedrebase'):
931 931 > return orig(ui, repo, *args, **kwargs)
932 932 > def wraprebase(loaded):
933 933 > assert loaded
934 934 > rebasemod = extensions.find('rebase')
935 935 > extensions.wrapcommand(rebasemod.cmdtable, 'rebase', _rebase)
936 936 > def extsetup(ui):
937 937 > extensions.afterloaded('rebase', wraprebase)
938 938 > EOF
939 939
940 940 $ cat >> .hg/hgrc <<EOF
941 941 > [extensions]
942 942 > wraprebase=$TESTTMP/wraprebase.py
943 943 > [experimental]
944 944 > evolution=all
945 945 > EOF
946 946
947 947 $ hg debugdrawdag <<'EOS'
948 948 > B C
949 949 > |/
950 950 > A
951 951 > EOS
952 952
953 953 $ hg rebase -s C -d B
954 954 rebasing 2:dc0947a82db8 "C" (C tip)
955 955
956 956 $ [ -f .hg/rebasestate ] && echo 'WRONG: rebasestate should not exist'
957 WRONG: rebasestate should not exist
957 [1]
General Comments 0
You need to be logged in to leave comments. Login now