# 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 as commit message'), _('TEXT')), ], "hg eggtag [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME..."), "update-meta": (update_meta, [], "hg update-meta") }