Source

egg-author / egg-author.py

Full commit
# egg-authors.py
#
# Just drop this file in your ~/.hg directory and add
# the following lines to your .hgrc:
#
# [extensions]
# egg-author=~/.hg/egg-author.py
#
# If you don't want automatic updates of your .meta-files, you can turn
# it off by putting this in ~/.hg or in the repository-specific .hg/hgrc file:
#
# [egg-author]
# update-meta=False
#
# This silly file may be used and distributed according to the terms of
# the GNU General Public License, version 2 or later "(at your option)".
#
# See http://mercurial.selenic.com/wiki/License for more info, including
# a link to the license text.

'''Tools to help make egg authors' lives easier'''

import fnmatch, re
from mercurial.i18n import _
from mercurial import cmdutil, commands, util
import mercurial.match as matchmod

# This is probably really really dumb code. I don't know python
def _find_egg_info_file(repo, type):
    stat = repo.status(clean=True)
    
    for x in stat[:5]:
        for fn in x:
            if fnmatch.fnmatch(fn, '*.%s' % type):
                raise util.Abort(_('%s is modified (please commit or revert '
                                   'and retry)') % fn)
    
    egg_info_file = None
    
    for x in stat[6:]:
        for fn in x:
            if fnmatch.fnmatch(fn, "*.%s" % type):
                if egg_info_file:
                    raise util.Abort(_('Found more than one %s file!') % type)
                else:
                    egg_info_file = fn

    if not egg_info_file:
        if type == 'release-info':
            help_uri = 'http://wiki.call-cc.org/releasing-your-egg'
        elif type == 'meta':
            help_uri = 'http://wiki.call-cc.org/Metafile%20reference'
        else:
            raise util.Abort(_('No help URI for egg file type %s') % type)
        raise util.Abort(_('Could not find %s file. You need to '
                           'create one first. See %s for more info.') % (type, help_uri))
    
    return egg_info_file

def _to_scheme_string(s):
    # This is a pathetic attempt at being safe. You shouldn't be using
    # these names anywway, and the user should be already be trusted when
    # they're allowed to commit.
    return '"' + re.sub(r'\\', r'\\\\', re.sub(r'"', r'\\"', s)) + '"'

def eggtag(ui, repo, name1, *names, **opts):
    '''Tag a Chicken egg for release.

    The syntax is identical to "hg tag", which it executes
    automatically.  This command just adds a (release ..) entry to
    your .release-info file for each tag.
    '''
    allnames = [t.strip() for t in (name1,) + names]

    release_info_file = _find_egg_info_file(repo, 'release-info')

    # Duplicate check in tag() to prevent meta-file from getting updated
    # while tagging might fail afterwards
    for n in allnames:
        if n in repo.tags():
            raise util.Abort('Release %s already exists!' % n)
    
    if ui.configbool('egg-author', 'update-meta', default=True):
        meta_message = 'Updated meta-file for release %s' % (', '.join(allnames))
        meta_file = _find_egg_info_file(repo, 'meta')
        update_meta(ui, repo)
        m = matchmod.exact(repo.root, '', [meta_file])
        repo.commit(text=meta_message, user=opts.get('user'), date=opts.get('date'), match=m)

    fp = repo.wfile(release_info_file, 'r+')
    commands.tag(ui, repo, name1, *names, **opts)
    ui.status(_('Tagged %s\n') % (', '.join(allnames)))
    
    # if hg's original tag command succeeded, we can do our stuff
    relinfo_message = 'Updated release-info file for release tag %s' % (', '.join(allnames))
    fp.seek(0, 2) # to the end
    for n in allnames:
        fp.write("(release %s)\n" % _to_scheme_string(n))
    fp.close()

    m = matchmod.exact(repo.root, '', [release_info_file])
    repo.commit(text=relinfo_message, user=opts.get('user'), date=opts.get('date'), match=m)
    ui.status(_('Updated and committed release-info %s\n') % (', '.join(allnames)))

def read_byte(f,res):
    byte = f.read(1)
    if (len(byte) == 0):
        return None
    else:
        res.extend(byte[0])
        return byte[0]

class FoundFiles(Exception):
    def __init__(self, val):
        self.res = val
    
# A *really* hacky s-expression reader
def _read_over_files(f, res, end = None):
    byte = read_byte(f,res)

    first_identifier = True
    
    while byte != None and byte != end:
        if byte == '"':
            byte = read_byte(f,res)
            while byte != None and byte != '"':
                if byte == '\\': # Escaped, so just read it without interpretation
                    read_byte(f,res)
                byte = read_byte(f,res)
            byte = read_byte(f,res)
        elif byte.isspace():
            byte = read_byte(f,res)
        elif byte == ';':
            byte = read_byte(f,res)
            while byte != None and byte != '\n':
                byte = read_byte(f,res)
        elif byte == '(':
            _read_over_files(f, res, ')')
            byte = read_byte(f,res)
        else: # Assume identifier
            identifier = []
            while byte != None and not byte.isspace() and byte != '(' and byte != ')' and byte != ';' and byte != '"':
                identifier.extend(byte)
                byte = read_byte(f,res)
            if first_identifier and ''.join(identifier) == 'files':
                # Skip until end of list
                _read_over_files(f, [], ')')
                raise FoundFiles(res)

        first_identifier = False
    return res

def update_meta(ui, repo):
    '''Update the FILES entry in an egg's meta-file.
    
    Only version-controlled files are added.'''

    meta_file = _find_egg_info_file(repo, 'meta')
    files = repo.status(clean=True)[6:][0]
    if '.hgtags' in files:
        files.remove('.hgtags')
    if '.hgignore' in files:
        files.remove('.hgignore')

    # A list without the parens around it
    files_list = ' '.join(map(_to_scheme_string, files))
    
    mf = repo.wfile(meta_file, 'rb')
    try:
        s = ''.join(_read_over_files(mf, []))
        s = s.rstrip() # Assuming no trailing comments...
        s = s[:len(s)-1] + '\n (files ' + files_list + '))\n'
    except FoundFiles, value:
        s = ''.join(value.res) + files_list + ')'
        s += mf.read() # the rest of the file
    mf.close
    
    # reopen and write out the new string
    mf = repo.wfile(meta_file, 'w')
    mf.write(s)
    mf.close()

    # Let the user know the file has been updated (or not, if unchanged)
    if len(repo.status(match=matchmod.exact('.', '.', [meta_file]))[0]) == 0:
        ui.status(_('Meta-file %s was already up-to-date\n') % meta_file)
    else:
        ui.status(_('Meta-file %s is updated\n') % meta_file)

cmdtable = {
    "eggtag": (eggtag,
               [('r', 'rev', '',
                 _('revision to tag'), _('REV')),
                ('', 'remove', None, _('remove a tag')),
                ('e', 'edit', None, _('edit commit message')),
                ('m', 'message', '',
                 _('use <text> as commit message'), _('TEXT')),
                ],
               "hg eggtag [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME..."),
    "update-meta": (update_meta,
                    [],
                    "hg update-meta")
}