evolve: clean up evolve implementation.
# Copyright 2011 Logilab SA <contact@logilab.fr>
"""synchronize patches queues and evolving changesets"""
import re
from cStringIO import StringIO
import json
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
from mercurial import phases
from mercurial import obsolete
### old compat code
#############################
BRANCHNAME="qsubmit2"
### 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 = 'edit'
if opts['review_all']:
review = 'all'
mqrepo = repo.mq.qrepo()
if mqrepo is None:
raise util.Abort('No patches repository')
try:
parent = mqrepo[BRANCHNAME]
except error.RepoLookupError:
parent = initqsubmit(mqrepo)
store, data, touched = fillstore(repo, parent)
try:
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 = []
applied_list = []
if review:
olddata = get_old_data(parent)
oldfiles = dict([(name, bin(ctxhex)) for ctxhex, name in olddata])
for patch_name in touched:
try:
store.getfile(patch_name)
review_list.append(patch_name)
except IOError:
oldnode = oldfiles[patch_name]
newnodes = obsolete.successorssets(repo, oldnode)
if newnodes:
newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing
if not newnodes:
# changeset has been killed (eg. reject)
pass
else:
assert len(newnodes) == 1 # conflict!!!
newnode = newnodes[0]
assert len(newnode) == 1 # split unsupported for now
newnode = list(newnode)[0]
# XXX unmanaged case where a cs is obsoleted by an unavailable one
#if newnode.node() not in repo.changelog.nodemap:
# raise util.Abort('%s is obsoleted by an unknown node %s'% (oldnode, newnode))
ctx = repo[newnode]
if ctx.phase() == phases.public:
# applied
applied_list.append(patch_name)
elif ctx.phase() == phases.secret:
# already exported changeset is now secret
repo.ui.warn("An already exported changeset is now secret!!!")
else:
# draft
assert False, "Should be exported"
if review:
if applied_list:
message += '\n'.join('* applied %s' % x for x in applied_list) + '\n'
if review_list:
message += '\n'.join('* %s ready for review' % x for x in review_list) + '\n'
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()
finally:
store.close()
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 no previous data exists"""
data = []
for ctx in repo.set('draft() - (obsolete() + merge())'):
name = makename(ctx)
data.append([ctx.hex(), makename(ctx)])
merges = repo.revs('draft() and merge()')
if merges:
repo.ui.warn('ignoring %i merge\n' % len(merges))
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()
evolve = extensions.find('evolve')
for oldhex, oldname in olddata:
if oldhex in usedold:
continue # no duplicate
usedold.add(oldhex)
oldname = str(oldname)
oldnode = bin(oldhex)
newnodes = obsolete.successorssets(repo, oldnode)
if newnodes:
newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing
if len(newnodes) > 1:
newnodes = [short(nodes[0]) for nodes in newnodes]
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]
stouched += [x for x in touched if x not in stouched]
return finaldata, stouched
def sort_key(ctx):
"""ctx sort key: (branch, rev)"""
return (ctx.branch(), ctx.rev())
def fillstore(repo, basemqctx):
"""fill 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
store.setfile('qsubmitdata', json.dumps(data, indent=True),
(False, False))
except:
store.close()
raise
return store, data, touched
def initqsubmit(mqrepo):
"""create initial qsubmit branch"""
store = patch.filestore()
try:
files = set()
store.setfile('DO-NOT-EDIT-THIS-WORKING-COPY-BY-HAND', 'WE WARNED YOU!', (False, False))
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]