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