util:wsgi: catch NoIDMatches in BEExceptionApp
[be.git] / release.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
4 #                         W. Trevor King <wking@tremily.us>
5 #
6 # This file is part of Bugs Everywhere.
7 #
8 # Bugs Everywhere is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the Free
10 # Software Foundation, either version 2 of the License, or (at your option) any
11 # later version.
12 #
13 # Bugs Everywhere is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16 # more details.
17 #
18 # You should have received a copy of the GNU General Public License along with
19 # Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
20
21 import codecs
22 import optparse
23 import os
24 import os.path
25 import shutil
26 import string
27 import sys
28
29 from libbe.util.subproc import invoke
30
31
32 INITIAL_COMMIT = '1bf1ec598b436f41ff27094eddf0b28c797e359d'
33
34
35 def validate_tag(tag):
36     """
37     >>> validate_tag('1.0.0')
38     >>> validate_tag('A.B.C-r7')
39     >>> validate_tag('A.B.C r7')
40     Traceback (most recent call last):
41       ...
42     Exception: Invalid character ' ' in tag 'A.B.C r7'
43     >>> validate_tag('"')
44     Traceback (most recent call last):
45       ...
46     Exception: Invalid character '"' in tag '"'
47     >>> validate_tag("'")
48     Traceback (most recent call last):
49       ...
50     Exception: Invalid character ''' in tag '''
51     """
52     for char in tag:
53         if char in string.digits:
54             continue
55         elif char in string.letters:
56             continue
57         elif char in ['.','-']:
58             continue
59         raise Exception("Invalid character '%s' in tag '%s'" % (char, tag))
60
61 def pending_changes():
62     """Use `git diff`s output to detect change.
63     """
64     status,stdout,stderr = invoke(['git', 'diff', 'HEAD'])
65     if len(stdout) == 0:
66         return False
67     return True
68
69 def set_release_version(tag):
70     print "set libbe.version._VERSION = '%s'" % tag
71     invoke(['sed', '-i', "s/^[# ]*_VERSION *=.*/_VERSION = '%s'/" % tag,
72             os.path.join('libbe', 'version.py')])
73
74 def remove_makefile_libbe_version_dependencies(filename):
75     print "set %s LIBBE_VERSION :=" % filename
76     invoke(['sed', '-i', "s/^LIBBE_VERSION *:=.*/LIBBE_VERSION :=/",
77             filename])
78
79 def commit(commit_message):
80     print 'commit current status:', commit_message
81     invoke(['git', 'commit', '-a', '-m', commit_message])
82
83 def tag(tag):
84     print 'tag current revision', tag
85     invoke(['git', 'tag', '-s', '-m', 'version {}'.format(tag), tag])
86
87 def export(target_dir):
88     if not target_dir.endswith(os.path.sep):
89         target_dir += os.path.sep
90     print 'export current revision to', target_dir
91     status,stdout,stderr = invoke(
92         ['git', 'archive', '--prefix', target_dir, 'HEAD'],
93         unicode_output=False)
94     status,stdout,stderr = invoke(['tar', '-xv'], stdin=stdout)
95
96 def make_version():
97     print 'generate libbe/_version.py'
98     invoke(['make', os.path.join('libbe', '_version.py')])
99
100 def make_changelog(filename, tag):
101     """Generate a ChangeLog from the git history.
102
103     Not the most ChangeLog-esque format, but iterating through commits
104     by hand is just too slow.
105     """
106     print 'generate ChangeLog file', filename, 'up to tag', tag
107     status,stdout,stderr = invoke(
108         ['git', 'log', '--no-merges', '{}..{}'.format(INITIAL_COMMIT, tag)])
109     with codecs.open(filename, 'w', 'utf-8') as f:
110         for line in stdout.splitlines():
111             f.write(line.rstrip())
112             f.write(u'\n')
113
114 def set_vcs_name(be_dir, vcs_name='None'):
115     """Exported directory is not a git repository, so set vcs_name to
116     something that will work.
117       vcs_name: new_vcs_name
118     """
119     for directory in os.listdir(be_dir):
120         if not os.path.isdir(os.path.join(be_dir, directory)):
121             continue
122         filename = os.path.join(be_dir, directory, 'settings')
123         if os.path.exists(filename):
124             print 'set vcs_name in', filename, 'to', vcs_name
125             invoke(['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name,
126                     filename])
127
128 def make_id_cache():
129     """Generate .be/id-cache so users won't need to.
130     """
131     invoke([sys.executable, './be', 'list'])
132
133 def create_tarball(tag):
134     release_name='be-%s' % tag
135     export_dir = release_name
136     export(export_dir)
137     make_version()
138     remove_makefile_libbe_version_dependencies(
139         os.path.join(export_dir, 'Makefile'))
140     print 'copy libbe/_version.py to %s/libbe/_version.py' % export_dir
141     shutil.copy(os.path.join('libbe', '_version.py'),
142                 os.path.join(export_dir, 'libbe', '_version.py'))
143     make_changelog(os.path.join(export_dir, 'ChangeLog'), tag)
144     make_id_cache()
145     print 'copy .be/id-cache to %s/.be/id-cache' % export_dir
146     shutil.copy(os.path.join('.be', 'id-cache'),
147                 os.path.join(export_dir, '.be', 'id-cache'))
148     set_vcs_name(os.path.join(export_dir, '.be'))
149     tarball_file = '%s.tar.gz' % release_name
150     print 'create tarball', tarball_file
151     invoke(['tar', '-czf', tarball_file, export_dir])
152     print 'remove', export_dir
153     shutil.rmtree(export_dir)
154
155 def test():
156     import doctest
157     doctest.testmod() 
158
159 def main(*args, **kwargs):
160     usage = """%prog [options] TAG
161
162 Create a git tag and a release tarball from the current revision.
163 For example
164   %prog 1.0.0
165
166 If you don't like what got committed, you can undo the release with
167   $ git tag -d 1.0.0
168   $ git reset --hard HEAD^
169 """
170     p = optparse.OptionParser(usage)
171     p.add_option('--test', dest='test', default=False,
172                  action='store_true', help='Run internal tests and exit')
173     options,args = p.parse_args(*args, **kwargs)
174
175     if options.test == True:
176         test()
177         sys.exit(0)
178
179     assert len(args) == 1, '%d (!= 1) arguments: %s' % (len(args), args)
180     _tag = args[0]
181     validate_tag(_tag)
182
183     if pending_changes() == True:
184         print "Handle pending changes before releasing."
185         sys.exit(1)
186     set_release_version(_tag)
187     print "Update copyright information..."
188     env = dict(os.environ)
189     pythonpath = os.path.abspath('update-copyright')
190     if 'PYTHONPATH' in env:
191         env['PYTHONPATH'] = '{}:{}'.format(pythonpath, env['PYTHONPATH'])
192     else:
193         env['PYTHONPATH'] = pythonpath
194     status,stdout,stderr = invoke([
195             os.path.join('update-copyright', 'bin', 'update-copyright.py')],
196             env=env)
197     commit("Bumped to version %s" % _tag)
198     tag(_tag)
199     create_tarball(_tag)
200
201
202 if __name__ == '__main__':
203     main()