hgext/qsync.py
author Pierre-Yves David <pierre-yves.david@logilab.fr>
Wed, 25 Apr 2012 18:11:52 +0200
changeset 215 0eceb5a48485
parent 201 94360b577857
child 216 9400e234b3d7
permissions -rw-r--r--
obsolete: fix changelog reload This changelog was not properly reloaded. This leaded to bug with rollback and strip.


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() + 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()
    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:
                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]
    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]