Mercurial > evolve
diff hgext/evolve.py @ 133:aa182b912d62
rename evolution to evolve
too much confusion with the email client
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Fri, 17 Feb 2012 10:29:01 +0100 |
parents | hgext/evolution.py@3124889cad55 |
children | bbc653876876 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/evolve.py Fri Feb 17 10:29:01 2012 +0100 @@ -0,0 +1,386 @@ +# states.py - introduce the state concept for mercurial changeset +# +# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com> +# Logilab SA <contact@logilab.fr> +# Pierre-Yves David <pierre-yves.david@ens-lyon.org> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''A set of command to make changeset evolve.''' + +from mercurial import cmdutil +from mercurial import scmutil +from mercurial import node +from mercurial import error +from mercurial import extensions +from mercurial import commands +from mercurial import bookmarks +from mercurial import phases +from mercurial import context +from mercurial import commands +from mercurial import util +from mercurial.i18n import _ +from mercurial.commands import walkopts, commitopts, commitopts2, logopt +from mercurial import hg + +### util function +############################# +def noderange(repo, revsets): + """The same as revrange but return node""" + return map(repo.changelog.node, + scmutil.revrange(repo, revsets)) + +### extension check +############################# + +def extsetup(ui): + try: + obsolete = extensions.find('obsolete') + except KeyError: + raise error.Abort(_('evolution extension require obsolete extension.')) + try: + rebase = extensions.find('rebase') + except KeyError: + raise error.Abort(_('evolution extension require rebase extension.')) + +### changeset rewriting logic +############################# + +def rewrite(repo, old, updates, head, newbases, commitopts): + if len(old.parents()) > 1: #XXX remove this unecessary limitation. + raise error.Abort(_('cannot amend merge changesets')) + base = old.p1() + bm = bookmarks.readcurrent(repo) + + wlock = repo.wlock() + try: + + # commit a new version of the old changeset, including the update + # collect all files which might be affected + files = set(old.files()) + for u in updates: + files.update(u.files()) + # prune files which were reverted by the updates + def samefile(f): + if f in head.manifest(): + a = head.filectx(f) + if f in base.manifest(): + b = base.filectx(f) + return (a.data() == b.data() + and a.flags() == b.flags() + and a.renamed() == b.renamed()) + else: + return False + else: + return f not in base.manifest() + files = [f for f in files if not samefile(f)] + # commit version of these files as defined by head + headmf = head.manifest() + def filectxfn(repo, ctx, path): + if path in headmf: + return head.filectx(path) + raise IOError() + if commitopts.get('message') and commitopts.get('logfile'): + raise util.Abort(_('options --message and --logfile are mutually' + ' exclusive')) + if commitopts.get('logfile'): + message= open(commitopts['logfile']).read() + elif commitopts.get('message'): + message = commitopts['message'] + else: + message = old.description() + + + + new = context.memctx(repo, + parents=newbases, + text=message, + files=files, + filectxfn=filectxfn, + user=commitopts.get('user') or None, + date=commitopts.get('date') or None, + extra=commitopts.get('extra') or None) + + if commitopts.get('edit'): + new._text = cmdutil.commitforceeditor(repo, new, []) + newid = repo.commitctx(new) + new = repo[newid] + + # update the bookmark + if bm: + repo._bookmarks[bm] = newid + bookmarks.write(repo) + + # hide obsolete csets + repo.changelog.hiddeninit = False + + # add evolution metadata + repo.addobsolete(new.node(), old.node()) + for u in updates: + repo.addobsolete(u.node(), old.node()) + repo.addobsolete(new.node(), u.node()) + + finally: + wlock.release() + + return newid + +def relocate(repo, rev, dest): + """rewrite <rev> on dest""" + try: + rebase = extensions.find('rebase') + # dummy state to trick rebase node + assert repo[rev].p2().rev() == node.nullrev, 'no support yet' + cmdutil.duplicatecopies(repo, rev, repo[dest].node(), + repo[rev].p2().node()) + rebase.rebasenode(repo, rev, dest, {node.nullrev: node.nullrev}) + nodenew = rebase.concludenode(repo, rev, dest, node.nullid) + nodesrc = repo.changelog.node(rev) + repo.addobsolete(nodenew, nodesrc) + phases.retractboundary(repo, repo[nodesrc].phase(), [nodenew]) + oldbookmarks = repo.nodebookmarks(nodesrc) + for book in oldbookmarks: + repo._bookmarks[book] = nodenew + if oldbookmarks: + bookmarks.write(repo) + except util.Abort: + # Invalidate the previous setparents + repo.dirstate.invalidate() + raise + + + +### new command +############################# +cmdtable = {} +command = cmdutil.command(cmdtable) + +@command('^evolve', + [], + '') +def evolve(ui, repo): + """suggest the next evolution step""" + obsolete = extensions.find('obsolete') + next = min(obsolete.unstables(repo)) + obs = repo[next].parents()[0] + if not obs.obsolete(): + obs = next.parents()[1] + assert obs.obsolete() + newer = obsolete.newerversion(repo, obs.node()) + target = newer[-1] + repo.ui.status('hg relocate --rev %s %s\n' % (repo[next], repo[target])) + +shorttemplate = '[{rev}] {desc|firstline}\n' + +@command('^gdown', + [], + 'update to working directory parent an display summary lines') +def cmdgdown(ui, repo): + wkctx = repo[None] + wparents = wkctx.parents() + if len(wparents) != 1: + raise util.Abort('merge in progress') + + parents = wparents[0].parents() + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if len(parents) == 1: + p = parents[0] + hg.update(repo, p.rev()) + displayer.show(p) + return 0 + else: + for p in parents: + displayer.show(p) + ui.warn(_('multiple parents, explicitly update to one\n')) + return 1 + +@command('^gup', + [], + 'update to working directory children an display summary lines') +def cmdup(ui, repo): + wkctx = repo[None] + wparents = wkctx.parents() + if len(wparents) != 1: + raise util.Abort('merge in progress') + + children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()] + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if not children: + ui.warn(_('No non-obsolete children\n')) + return 1 + if len(children) == 1: + c = children[0] + hg.update(repo, c.rev()) + displayer.show(c) + return 0 + else: + for c in children: + displayer.show(c) + ui.warn(_('Multiple non-obsolete children, explicitly update to one\n')) + return 1 + + +@command('^kill', + [ + ('n', 'new', [], _("New changeset that justify this one to be killed")) + ], + '<revs>') +def kill(ui, repo, *revs, **opts): + """mark a changeset as obsolete + + This update the parent directory to a not-killed parent if the current + working directory parent are killed. + + XXX bookmark support + XXX handle merge + XXX check immutable first + """ + wlock = repo.wlock() + try: + new = opts['new'] + targetnodes = set(noderange(repo, revs)) + if not new: + new = [node.nullid] + for n in targetnodes: + if not repo[n].mutable(): + ui.warn(_("Can't kill immutable changeset %s") % repo[n]) + else: + for ne in new: + repo.addobsolete(ne, n) + # update to an unkilled parent + wdp = repo['.'] + newnode = wdp + while newnode.obsolete(): + newnode = newnode.parents()[0] + if newnode.node() != wdp.node(): + commands.update(ui, repo, newnode.rev()) + ui.status(_('working directory now at %s\n') % newnode) + + finally: + wlock.release() + +@command('^amend', + [('A', 'addremove', None, + _('mark new/missing files as added/removed before committing')), + ('n', 'note', '', + _('use text as commit message for this update')), + ('c', 'change', '', + _('specifies the changeset to amend'), _('REV')), + ('b', 'branch', '', + _('specifies a branch for the new.'), _('REV')), + ('e', 'edit', False, + _('edit commit message.'), _('')), + ] + walkopts + commitopts + commitopts2, + _('[OPTION]... [FILE]...')) + +def amend(ui, repo, *pats, **opts): + """combine a changeset with updates and replace it with a new one + + Commits a new changeset incorporating both the changes to the given files + and all the changes from the current parent changeset into the repository. + + See :hg:`commit` for details about committing changes. + + If you don't specify -m, the parent's message will be reused. + + If you specify --change, amend additionally considers all changesets between + the indicated changeset and the working copy parent as updates to be subsumed. + This allows you to commit updates manually first. As a special shorthand you + can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest + commit as an update of its parent. + + Behind the scenes, Mercurial first commits the update as a regular child + of the current parent. Then it creates a new commit on the parent's parents + with the updated contents. Then it changes the working copy parent to this + new combined changeset. Finally, the old changeset and its update are hidden + from :hg:`log` (unless you use --hidden with log). + + Returns 0 on success, 1 if nothing changed. + """ + + # determine updates to subsume + change = opts.get('change') + if change == '.': + change = 'p1(p1())' + old = scmutil.revsingle(repo, change) + branch = opts.get('branch') + if branch: + opts.setdefault('extra', {})['branch'] = branch + else: + if old.branch() != 'default': + opts.setdefault('extra', {})['branch'] = old.branch() + + lock = repo.lock() + try: + wlock = repo.wlock() + try: + if not old.phase(): + raise util.Abort(_("can not rewrite immutable changeset %s") % old) + + # commit current changes as update + # code copied from commands.commit to avoid noisy messages + ciopts = dict(opts) + ciopts.pop('message', None) + ciopts.pop('logfile', None) + ciopts['message'] = opts.get('note') or ('amends %s' % old.hex()) + e = cmdutil.commiteditor + def commitfunc(ui, repo, message, match, opts): + return repo.commit(message, opts.get('user'), opts.get('date'), match, + editor=e) + cmdutil.commit(ui, repo, commitfunc, pats, ciopts) + + # find all changesets to be considered updates + cl = repo.changelog + head = repo['.'] + updatenodes = set(cl.nodesbetween(roots=[old.node()], + heads=[head.node()])[0]) + updatenodes.remove(old.node()) + if not updatenodes and not (opts.get('message') or opts.get('logfile') or opts.get('edit')): + raise error.Abort(_('no updates found')) + updates = [repo[n] for n in updatenodes] + + # perform amend + if opts.get('edit'): + opts['force_editor'] = True + newid = rewrite(repo, old, updates, head, + [old.p1().node(), old.p2().node()], opts) + + # reroute the working copy parent to the new changeset + phases.retractboundary(repo, old.phase(), [newid]) + repo.dirstate.setparents(newid, node.nullid) + finally: + wlock.release() + finally: + lock.release() + +def commitwrapper(orig, ui, repo, *arg, **kwargs): + obsoleted = kwargs.get('obsolete', []) + if obsoleted: + obsoleted = repo.set('%lr', obsoleted) + result = orig(ui, repo, *arg, **kwargs) + if not result: # commit successed + new = repo['-1'] + for old in obsoleted: + repo.addobsolete(new.node(), old.node()) + return result + +def graftwrapper(orig, ui, repo, *revs, **kwargs): + lock = repo.lock() + try: + if kwargs.get('old_obsolete'): + obsoleted = kwargs.setdefault('obsolete', []) + if kwargs['continue']: + obsoleted.extend(repo.opener.read('graftstate').splitlines()) + else: + obsoleted.extend(revs) + return commitwrapper(orig, ui, repo,*revs, **kwargs) + finally: + lock.release() + +def extsetup(ui): + entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) + entry[1].append(('o', 'obsolete', [], _("this commit obsolet this revision"))) + entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) + entry[1].append(('o', 'obsolete', [], _("this graft obsolet this revision"))) + entry[1].append(('O', 'old-obsolete', False, _("graft result obsolete graft source")))