Mercurial > evolve
changeset 153:c088f503cc97
add qsync extension to mutable history
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Tue, 20 Mar 2012 16:11:57 +0100 |
parents | 54c67d7f9eed |
children | d3c3211fcfc4 |
files | hgext/qsync.py |
diffstat | 1 files changed, 230 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/qsync.py Tue Mar 20 16:11:57 2012 +0100 @@ -0,0 +1,230 @@ + +import re + +from cStringIO import StringIO + +from mercurial.i18n import _ +from mercurial import commands +from mercurial import patch +from mercurial import util +from mercurial.node import nullid, hex, short, bin +from mercurial import cmdutil +from mercurial import hg +from mercurial import scmutil +from mercurial import error +from mercurial import extensions + + +import re + +import json + + +### old compat code +############################# + +BRANCHNAME="qsubmit2" +OLDBRANCHNAME="pyves-qsubmit" + +### new command +############################# +cmdtable = {} +command = cmdutil.command(cmdtable) + +@command('^qsync|sync', + [ + ('a', 'review-all', False, _('mark all touched patches ready for review (no editor)')), + ], + '') +def cmdsync(ui, repo, **opts): + '''Export draft changeset as mq patch in a mq patches repository commit. + + This command get all changesets in draft phase and create an mq changeset: + + * on a "qsubmit2" branch (based on the last changeset) + + * one patch per draft changeset + + * a series files listing all generated patch + + * qsubmitdata holding useful information + + It does use obsolete relation to update patches that already existing in the qsubmit2 branch. + + Already existing patch which became public, draft or got killed are remove from the mq repo. + + Patch name are generated using the summary line for changeset description. + + .. warning:: Series files is ordered topologically. So two series with + interleaved changeset will appear interleaved. + ''' + + review = None + review = 'edit' + if opts['review_all']: + review = 'all' + mqrepo = repo.mq.qrepo() + try: + parent = mqrepo[BRANCHNAME] + except error.RepoLookupError: + try: + parent = mqrepo[OLDBRANCHNAME] + except error.RepoLookupError: + parent = initqsubmit(mqrepo) + store, data, touched = fillstore(repo, parent) + if not touched: + raise util.Abort('Nothing changed') + files = ['qsubmitdata', 'series'] + touched + # mark some as ready for review + message = 'qsubmit commit\n\n' + review_list = [] + if review: + for patch_name in touched: + try: + store.getfile(patch_name) + review_list.append(patch_name) + except IOError: + pass + + if review: + message += '\n'.join('* %s ready for review' % x for x in review_list) + memctx = patch.makememctx(mqrepo, (parent.node(), nullid), + message, + None, + None, + parent.branch(), files, store, + editor=None) + if review == 'edit': + memctx._text = cmdutil.commitforceeditor(mqrepo, memctx, []) + mqrepo.savecommitmessage(memctx.description()) + n = memctx.commit() + return 0 + + +def makename(ctx): + """create a patch name form a changeset""" + descsummary = ctx.description().splitlines()[0] + descsummary = re.sub(r'\s+', '_', descsummary) + descsummary = re.sub(r'\W+', '', descsummary) + if len(descsummary) > 45: + descsummary = descsummary[:42] + '.' + return '%s-%s.diff' % (ctx.branch().upper(), descsummary) + + +def get_old_data(mqctx): + """read qsubmit data to fetch previous export data + + get old data from the content of an mq commit""" + try: + old_data = mqctx['qsubmitdata'] + return json.loads(old_data.data()) + except error.LookupError: + return [] + +def get_current_data(repo): + """Return what would be exported if not previous data exists""" + data = [] + for ctx in repo.set('draft() - obsolete()'): + name = makename(ctx) + data.append([ctx.hex(), makename(ctx)]) + return data + + +def patchmq(repo, store, olddata, newdata): + """export the mq patches and return all useful data to be exported""" + finaldata = [] + touched = set() + currentdrafts = set(d[0] for d in newdata) + usednew = set() + usedold = set() + obsolete = extensions.find('obsolete') + for oldhex, oldname in olddata: + if oldhex in usedold: + continue # no duplicate + usedold.add(oldhex) + oldname = str(oldname) + oldnode = bin(oldhex) + newnodes = obsolete.newerversion(repo, oldnode) + if newnodes: + newnodes = [n for n in newnodes if n] # remove killing + if len(newnodes) > 1: + raise util.Abort('%s have more than one newer version: %s'% (oldname, newnodes)) + if newnodes: + # else, changeset have been killed + newnode = list(newnodes)[0][0] + ctx = repo[newnode] + if ctx.hex() != oldhex and ctx.phase(): + fp = StringIO() + cmdutil.export(repo, [ctx.rev()], fp=fp) + data = fp.getvalue() + store.setfile(oldname, data, (None, None)) + finaldata.append([ctx.hex(), oldname]) + usednew.add(ctx.hex()) + touched.add(oldname) + continue + if oldhex in currentdrafts: + # else changeset is now public or secret + finaldata.append([oldhex, oldname]) + usednew.add(ctx.hex()) + continue + touched.add(oldname) + + for newhex, newname in newdata: + if newhex in usednew: + continue + newnode = bin(newhex) + ctx = repo[newnode] + fp = StringIO() + cmdutil.export(repo, [ctx.rev()], fp=fp) + data = fp.getvalue() + store.setfile(newname, data, (None, None)) + finaldata.append([ctx.hex(), newname]) + touched.add(newname) + # sort by branchrev number + finaldata.sort(key=lambda x: sort_key(repo[x[0]])) + # sort touched too (ease review list) + stouched = [f[1] for f in finaldata if f[1] in touched] + return finaldata, stouched + +def sort_key(ctx): + """ctx sort key: (branch, rev)""" + return (ctx.branch(), ctx.rev()) + + +def fillstore(repo, basemqctx): + """file store with patch data""" + olddata = get_old_data(basemqctx) + newdata = get_current_data(repo) + store = patch.filestore() + try: + data, touched = patchmq(repo, store, olddata, newdata) + # put all name in the series + series ='\n'.join(d[1] for d in data) + '\n' + store.setfile('series', series, (False, False)) + + # export data to ease futur work + series ='\n'.join(d[1] for d in data) + '\n' + store.setfile('qsubmitdata', json.dumps(data, indent=True), + (False, False)) + finally: + store.close() + return store, data, touched + + +def initqsubmit(mqrepo): + """create initial qsubmit branch""" + store = patch.filestore() + try: + files = set() + store.setfile('.hgignore', '^status$\n', (False, False)) + memctx = patch.makememctx(mqrepo, (nullid, nullid), + 'qsubmit init', + None, + None, + BRANCHNAME, ('.hgignore',), store, + editor=None) + mqrepo.savecommitmessage(memctx.description()) + n = memctx.commit() + finally: + store.close() + return mqrepo[n]