diff -r 5ff1fc726848 -r c6bca38c1cbf eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/rebase.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/rebase.py Sat Jan 08 11:20:57 2011 +0530 @@ -0,0 +1,577 @@ +# rebase.py - rebasing feature for mercurial +# +# Copyright 2008 Stefano Tortarolo +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to move sets of revisions to a different ancestor + +This extension lets you rebase changesets in an existing Mercurial +repository. + +For more information: +http://mercurial.selenic.com/wiki/RebaseExtension +''' + +from mercurial import hg, util, repair, merge, cmdutil, commands +from mercurial import extensions, ancestor, copies, patch +from mercurial.commands import templateopts +from mercurial.node import nullrev +from mercurial.lock import release +from mercurial.i18n import _ +import os, errno + +nullmerge = -2 + +def rebase(ui, repo, **opts): + """move changeset (and descendants) to a different branch + + Rebase uses repeated merging to graft changesets from one part of + history (the source) onto another (the destination). This can be + useful for linearizing *local* changes relative to a master + development tree. + + You should not rebase changesets that have already been shared + with others. Doing so will force everybody else to perform the + same rebase or they will end up with duplicated changesets after + pulling in your rebased changesets. + + If you don't specify a destination changeset (``-d/--dest``), + rebase uses the tipmost head of the current named branch as the + destination. (The destination changeset is not modified by + rebasing, but new changesets are added as its descendants.) + + You can specify which changesets to rebase in two ways: as a + "source" changeset or as a "base" changeset. Both are shorthand + for a topologically related set of changesets (the "source + branch"). If you specify source (``-s/--source``), rebase will + rebase that changeset and all of its descendants onto dest. If you + specify base (``-b/--base``), rebase will select ancestors of base + back to but not including the common ancestor with dest. Thus, + ``-b`` is less precise but more convenient than ``-s``: you can + specify any changeset in the source branch, and rebase will select + the whole branch. If you specify neither ``-s`` nor ``-b``, rebase + uses the parent of the working directory as the base. + + By default, rebase recreates the changesets in the source branch + as descendants of dest and then destroys the originals. Use + ``--keep`` to preserve the original source changesets. Some + changesets in the source branch (e.g. merges from the destination + branch) may be dropped if they no longer contribute any change. + + One result of the rules for selecting the destination changeset + and source branch is that, unlike ``merge``, rebase will do + nothing if you are at the latest (tipmost) head of a named branch + with two heads. You need to explicitly specify source and/or + destination (or ``update`` to the other head, if it's the head of + the intended source branch). + + If a rebase is interrupted to manually resolve a merge, it can be + continued with --continue/-c or aborted with --abort/-a. + + Returns 0 on success, 1 if nothing to rebase. + """ + originalwd = target = None + external = nullrev + state = {} + skipped = set() + targetancestors = set() + + lock = wlock = None + try: + lock = repo.lock() + wlock = repo.wlock() + + # Validate input and define rebasing points + destf = opts.get('dest', None) + srcf = opts.get('source', None) + basef = opts.get('base', None) + contf = opts.get('continue') + abortf = opts.get('abort') + collapsef = opts.get('collapse', False) + extrafn = opts.get('extrafn') + keepf = opts.get('keep', False) + keepbranchesf = opts.get('keepbranches', False) + detachf = opts.get('detach', False) + # keepopen is not meant for use on the command line, but by + # other extensions + keepopen = opts.get('keepopen', False) + + if contf or abortf: + if contf and abortf: + raise util.Abort(_('cannot use both abort and continue')) + if collapsef: + raise util.Abort( + _('cannot use collapse with continue or abort')) + if detachf: + raise util.Abort(_('cannot use detach with continue or abort')) + if srcf or basef or destf: + raise util.Abort( + _('abort and continue do not allow specifying revisions')) + + (originalwd, target, state, skipped, collapsef, keepf, + keepbranchesf, external) = restorestatus(repo) + if abortf: + return abort(repo, originalwd, target, state) + else: + if srcf and basef: + raise util.Abort(_('cannot specify both a ' + 'revision and a base')) + if detachf: + if not srcf: + raise util.Abort( + _('detach requires a revision to be specified')) + if basef: + raise util.Abort(_('cannot specify a base with detach')) + + cmdutil.bail_if_changed(repo) + result = buildstate(repo, destf, srcf, basef, detachf) + if not result: + # Empty state built, nothing to rebase + ui.status(_('nothing to rebase\n')) + return 1 + else: + originalwd, target, state = result + if collapsef: + targetancestors = set(repo.changelog.ancestors(target)) + external = checkexternal(repo, state, targetancestors) + + if keepbranchesf: + if extrafn: + raise util.Abort(_('cannot use both keepbranches and extrafn')) + def extrafn(ctx, extra): + extra['branch'] = ctx.branch() + + # Rebase + if not targetancestors: + targetancestors = set(repo.changelog.ancestors(target)) + targetancestors.add(target) + + sortedstate = sorted(state) + total = len(sortedstate) + pos = 0 + for rev in sortedstate: + pos += 1 + if state[rev] == -1: + ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])), + _('changesets'), total) + storestatus(repo, originalwd, target, state, collapsef, keepf, + keepbranchesf, external) + p1, p2 = defineparents(repo, rev, target, state, + targetancestors) + if len(repo.parents()) == 2: + repo.ui.debug('resuming interrupted rebase\n') + else: + stats = rebasenode(repo, rev, p1, p2, state) + if stats and stats[3] > 0: + raise util.Abort(_('unresolved conflicts (see hg ' + 'resolve, then hg rebase --continue)')) + updatedirstate(repo, rev, target, p2) + if not collapsef: + newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn) + else: + # Skip commit if we are collapsing + repo.dirstate.setparents(repo[p1].node()) + newrev = None + # Update the state + if newrev is not None: + state[rev] = repo[newrev].rev() + else: + if not collapsef: + ui.note(_('no changes, revision %d skipped\n') % rev) + ui.debug('next revision set to %s\n' % p1) + skipped.add(rev) + state[rev] = p1 + + ui.progress(_('rebasing'), None) + ui.note(_('rebase merging completed\n')) + + if collapsef and not keepopen: + p1, p2 = defineparents(repo, min(state), target, + state, targetancestors) + commitmsg = 'Collapsed revision' + for rebased in state: + if rebased not in skipped and state[rebased] != nullmerge: + commitmsg += '\n* %s' % repo[rebased].description() + commitmsg = ui.edit(commitmsg, repo.ui.username()) + newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg, + extrafn=extrafn) + + if 'qtip' in repo.tags(): + updatemq(repo, state, skipped, **opts) + + if not keepf: + # Remove no more useful revisions + rebased = [rev for rev in state if state[rev] != nullmerge] + if rebased: + if set(repo.changelog.descendants(min(rebased))) - set(state): + ui.warn(_("warning: new changesets detected " + "on source branch, not stripping\n")) + else: + # backup the old csets by default + repair.strip(ui, repo, repo[min(rebased)].node(), "all") + + clearstatus(repo) + ui.note(_("rebase completed\n")) + if os.path.exists(repo.sjoin('undo')): + util.unlink(repo.sjoin('undo')) + if skipped: + ui.note(_("%d revisions have been skipped\n") % len(skipped)) + finally: + release(lock, wlock) + +def rebasemerge(repo, rev, first=False): + 'return the correct ancestor' + oldancestor = ancestor.ancestor + + def newancestor(a, b, pfunc): + if b == rev: + return repo[rev].parents()[0].rev() + return oldancestor(a, b, pfunc) + + if not first: + ancestor.ancestor = newancestor + else: + repo.ui.debug("first revision, do not change ancestor\n") + try: + stats = merge.update(repo, rev, True, True, False) + return stats + finally: + ancestor.ancestor = oldancestor + +def checkexternal(repo, state, targetancestors): + """Check whether one or more external revisions need to be taken in + consideration. In the latter case, abort. + """ + external = nullrev + source = min(state) + for rev in state: + if rev == source: + continue + # Check externals and fail if there are more than one + for p in repo[rev].parents(): + if (p.rev() not in state + and p.rev() not in targetancestors): + if external != nullrev: + raise util.Abort(_('unable to collapse, there is more ' + 'than one external parent')) + external = p.rev() + return external + +def updatedirstate(repo, rev, p1, p2): + """Keep track of renamed files in the revision that is going to be rebased + """ + # Here we simulate the copies and renames in the source changeset + cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True) + m1 = repo[rev].manifest() + m2 = repo[p1].manifest() + for k, v in cop.iteritems(): + if k in m1: + if v in m1 or v in m2: + repo.dirstate.copy(v, k) + if v in m2 and v not in m1: + repo.dirstate.remove(v) + +def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None): + 'Commit the changes and store useful information in extra' + try: + repo.dirstate.setparents(repo[p1].node(), repo[p2].node()) + ctx = repo[rev] + if commitmsg is None: + commitmsg = ctx.description() + extra = {'rebase_source': ctx.hex()} + if extrafn: + extrafn(ctx, extra) + # Commit might fail if unresolved files exist + newrev = repo.commit(text=commitmsg, user=ctx.user(), + date=ctx.date(), extra=extra) + repo.dirstate.setbranch(repo[newrev].branch()) + return newrev + except util.Abort: + # Invalidate the previous setparents + repo.dirstate.invalidate() + raise + +def rebasenode(repo, rev, p1, p2, state): + 'Rebase a single revision' + # Merge phase + # Update to target and merge it with local + if repo['.'].rev() != repo[p1].rev(): + repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1])) + merge.update(repo, p1, False, True, False) + else: + repo.ui.debug(" already in target\n") + repo.dirstate.write() + repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev])) + first = repo[rev].rev() == repo[min(state)].rev() + stats = rebasemerge(repo, rev, first) + return stats + +def defineparents(repo, rev, target, state, targetancestors): + 'Return the new parent relationship of the revision that will be rebased' + parents = repo[rev].parents() + p1 = p2 = nullrev + + P1n = parents[0].rev() + if P1n in targetancestors: + p1 = target + elif P1n in state: + if state[P1n] == nullmerge: + p1 = target + else: + p1 = state[P1n] + else: # P1n external + p1 = target + p2 = P1n + + if len(parents) == 2 and parents[1].rev() not in targetancestors: + P2n = parents[1].rev() + # interesting second parent + if P2n in state: + if p1 == target: # P1n in targetancestors or external + p1 = state[P2n] + else: + p2 = state[P2n] + else: # P2n external + if p2 != nullrev: # P1n external too => rev is a merged revision + raise util.Abort(_('cannot use revision %d as base, result ' + 'would have 3 parents') % rev) + p2 = P2n + repo.ui.debug(" future parents are %d and %d\n" % + (repo[p1].rev(), repo[p2].rev())) + return p1, p2 + +def isagitpatch(repo, patchname): + 'Return true if the given patch is in git format' + mqpatch = os.path.join(repo.mq.path, patchname) + for line in patch.linereader(file(mqpatch, 'rb')): + if line.startswith('diff --git'): + return True + return False + +def updatemq(repo, state, skipped, **opts): + 'Update rebased mq patches - finalize and then import them' + mqrebase = {} + mq = repo.mq + for p in mq.applied: + rev = repo[p.node].rev() + if rev in state: + repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' % + (rev, p.name)) + mqrebase[rev] = (p.name, isagitpatch(repo, p.name)) + + if mqrebase: + mq.finish(repo, mqrebase.keys()) + + # We must start import from the newest revision + for rev in sorted(mqrebase, reverse=True): + if rev not in skipped: + name, isgit = mqrebase[rev] + repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name)) + mq.qimport(repo, (), patchname=name, git=isgit, + rev=[str(state[rev])]) + mq.save_dirty() + +def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches, + external): + 'Store the current status to allow recovery' + f = repo.opener("rebasestate", "w") + f.write(repo[originalwd].hex() + '\n') + f.write(repo[target].hex() + '\n') + f.write(repo[external].hex() + '\n') + f.write('%d\n' % int(collapse)) + f.write('%d\n' % int(keep)) + f.write('%d\n' % int(keepbranches)) + for d, v in state.iteritems(): + oldrev = repo[d].hex() + newrev = repo[v].hex() + f.write("%s:%s\n" % (oldrev, newrev)) + f.close() + repo.ui.debug('rebase status stored\n') + +def clearstatus(repo): + 'Remove the status files' + if os.path.exists(repo.join("rebasestate")): + util.unlink(repo.join("rebasestate")) + +def restorestatus(repo): + 'Restore a previously stored status' + try: + target = None + collapse = False + external = nullrev + state = {} + f = repo.opener("rebasestate") + for i, l in enumerate(f.read().splitlines()): + if i == 0: + originalwd = repo[l].rev() + elif i == 1: + target = repo[l].rev() + elif i == 2: + external = repo[l].rev() + elif i == 3: + collapse = bool(int(l)) + elif i == 4: + keep = bool(int(l)) + elif i == 5: + keepbranches = bool(int(l)) + else: + oldrev, newrev = l.split(':') + state[repo[oldrev].rev()] = repo[newrev].rev() + skipped = set() + # recompute the set of skipped revs + if not collapse: + seen = set([target]) + for old, new in sorted(state.items()): + if new != nullrev and new in seen: + skipped.add(old) + seen.add(new) + repo.ui.debug('computed skipped revs: %s\n' % skipped) + repo.ui.debug('rebase status resumed\n') + return (originalwd, target, state, skipped, + collapse, keep, keepbranches, external) + except IOError, err: + if err.errno != errno.ENOENT: + raise + raise util.Abort(_('no rebase in progress')) + +def abort(repo, originalwd, target, state): + 'Restore the repository to its original state' + if set(repo.changelog.descendants(target)) - set(state.values()): + repo.ui.warn(_("warning: new changesets detected on target branch, " + "can't abort\n")) + return -1 + else: + # Strip from the first rebased revision + merge.update(repo, repo[originalwd].rev(), False, True, False) + rebased = filter(lambda x: x > -1 and x != target, state.values()) + if rebased: + strippoint = min(rebased) + # no backup of rebased cset versions needed + repair.strip(repo.ui, repo, repo[strippoint].node()) + clearstatus(repo) + repo.ui.warn(_('rebase aborted\n')) + return 0 + +def buildstate(repo, dest, src, base, detach): + 'Define which revisions are going to be rebased and where' + targetancestors = set() + detachset = set() + + if not dest: + # Destination defaults to the latest revision in the current branch + branch = repo[None].branch() + dest = repo[branch].rev() + else: + dest = repo[dest].rev() + + # This check isn't strictly necessary, since mq detects commits over an + # applied patch. But it prevents messing up the working directory when + # a partially completed rebase is blocked by mq. + if 'qtip' in repo.tags() and (repo[dest].node() in + [s.node for s in repo.mq.applied]): + raise util.Abort(_('cannot rebase onto an applied mq patch')) + + if src: + commonbase = repo[src].ancestor(repo[dest]) + if commonbase == repo[src]: + raise util.Abort(_('source is ancestor of destination')) + if commonbase == repo[dest]: + raise util.Abort(_('source is descendant of destination')) + source = repo[src].rev() + if detach: + # We need to keep track of source's ancestors up to the common base + srcancestors = set(repo.changelog.ancestors(source)) + baseancestors = set(repo.changelog.ancestors(commonbase.rev())) + detachset = srcancestors - baseancestors + detachset.discard(commonbase.rev()) + else: + if base: + cwd = repo[base].rev() + else: + cwd = repo['.'].rev() + + if cwd == dest: + repo.ui.debug('source and destination are the same\n') + return None + + targetancestors = set(repo.changelog.ancestors(dest)) + if cwd in targetancestors: + repo.ui.debug('source is ancestor of destination\n') + return None + + cwdancestors = set(repo.changelog.ancestors(cwd)) + if dest in cwdancestors: + repo.ui.debug('source is descendant of destination\n') + return None + + cwdancestors.add(cwd) + rebasingbranch = cwdancestors - targetancestors + source = min(rebasingbranch) + + repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source)) + state = dict.fromkeys(repo.changelog.descendants(source), nullrev) + state.update(dict.fromkeys(detachset, nullmerge)) + state[source] = nullrev + return repo['.'].rev(), repo[dest].rev(), state + +def pullrebase(orig, ui, repo, *args, **opts): + 'Call rebase after pull if the latter has been invoked with --rebase' + if opts.get('rebase'): + if opts.get('update'): + del opts['update'] + ui.debug('--update and --rebase are not compatible, ignoring ' + 'the update flag\n') + + cmdutil.bail_if_changed(repo) + revsprepull = len(repo) + origpostincoming = commands.postincoming + def _dummy(*args, **kwargs): + pass + commands.postincoming = _dummy + try: + orig(ui, repo, *args, **opts) + finally: + commands.postincoming = origpostincoming + revspostpull = len(repo) + if revspostpull > revsprepull: + rebase(ui, repo, **opts) + branch = repo[None].branch() + dest = repo[branch].rev() + if dest != repo['.'].rev(): + # there was nothing to rebase we force an update + hg.update(repo, dest) + else: + orig(ui, repo, *args, **opts) + +def uisetup(ui): + 'Replace pull with a decorator to provide --rebase option' + entry = extensions.wrapcommand(commands.table, 'pull', pullrebase) + entry[1].append(('', 'rebase', None, + _("rebase working directory to branch head")) +) + +cmdtable = { +"rebase": + (rebase, + [ + ('s', 'source', '', + _('rebase from the specified changeset'), _('REV')), + ('b', 'base', '', + _('rebase from the base of the specified changeset ' + '(up to greatest common ancestor of base and dest)'), + _('REV')), + ('d', 'dest', '', + _('rebase onto the specified changeset'), _('REV')), + ('', 'collapse', False, _('collapse the rebased changesets')), + ('', 'keep', False, _('keep original changesets')), + ('', 'keepbranches', False, _('keep original branch names')), + ('', 'detach', False, _('force detaching of source from its original ' + 'branch')), + ('c', 'continue', False, _('continue an interrupted rebase')), + ('a', 'abort', False, _('abort an interrupted rebase'))] + + templateopts, + _('hg rebase [-s REV | -b REV] [-d REV] [options]\n' + 'hg rebase {-a|-c}')) +}