--- /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")))