--- /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]