aboutsummaryrefslogtreecommitdiff
path: root/egg-author.py
blob: 8155c509e25f7cc985bf76d90a40c9b1beb4fa04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# 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.') % 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")
}