Update release.py for use with git-versioned BE.
[be.git] / release.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Bugs Everywhere.
6 #
7 # Bugs Everywhere is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the
9 # Free Software Foundation, either version 2 of the License, or (at your
10 # option) any later version.
11 #
12 # Bugs Everywhere is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
19
20 import os
21 import os.path
22 import shutil
23 import string
24 import sys
25
26 from libbe.util.subproc import Pipe, invoke
27 from libbe.util.encoding import set_file_contents
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     p = Pipe([['git', 'diff', 'HEAD']])
64     assert p.status == 0, p.statuses
65     if len(p.stdout) == 0:
66         return False
67     return True
68
69 def set_release_version(tag):
70     print "set libbe.version._VERSION = '%s'" % tag
71     p = Pipe([['sed', '-i', "s/^# *_VERSION *=.*/_VERSION = '%s'/" % tag,
72                os.path.join('libbe', 'version.py')]])
73     assert p.status == 0, p.statuses
74
75 def remove_makefile_libbe_version_dependencies():
76     print "set Makefile LIBBE_VERSION :="
77     p = Pipe([['sed', '-i', "s/^LIBBE_VERSION *:=.*/LIBBE_VERSION :=/",
78                'Makefile']])
79     assert p.status == 0, p.statuses
80
81 def commit(commit_message):
82     print 'commit current status:', commit_message
83     p = Pipe([['git', 'commit', '-m', commit_message]])
84     assert p.status == 0, p.statuses
85
86 def tag(tag):
87     print 'tag current revision', tag
88     p = Pipe([['git', 'tag', tag]])
89     assert p.status == 0, p.statuses
90
91 def export(target_dir):
92     print 'export current revision to', target_dir
93     p = Pipe([['git', 'archive', '--prefix', target_dir, 'HEAD'],
94               ['tar', '-xv']])
95     assert p.status == 0, p.statuses
96
97 def make_version():
98     print 'generate libbe/_version.py'
99     p = Pipe([['make', os.path.join('libbe', '_version.py')]])
100     assert p.status == 0, p.statuses
101
102 def make_changelog(filename, tag):
103     """Generate a ChangeLog from the git history.
104
105     Based on
106       http://stackoverflow.com/questions/2976665/git-changelog-day-by-day
107     """
108     print 'generate ChangeLog file', filename, 'up to tag', tag
109     p = invoke(['git', 'log', '--no-merges', '--format="%cd"', '--date=short',
110                 '%s..%s' % (INITIAL_COMMIT, tag)])
111     days = sorted(set(p.stdout.split('\n')), reverse=True)
112     log = []
113     next = None
114     for day in days:
115         args = ['git', 'log', '--no-merges', '--format=" * s"', '--since', day]
116         if next != None:
117             args.extend(['--until', next])
118         p = invoke(args)
119         log.extend(['', day, p.stdout])
120         next = day
121     set_file_contents(filename, '\n'.join(log), encoding='utf-8')
122
123 def set_vcs_name(filename, vcs_name='None'):
124     """Exported directory is not a git repository, so set vcs_name to
125     something that will work.
126       vcs_name: new_vcs_name
127     """
128     print 'set vcs_name in', filename, 'to', vcs_name
129     p = Pipe([['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name,
130                filename]])
131     assert p.status == 0, p.statuses
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     print 'copy libbe/_version.py to %s/libbe/_version.py' % export_dir
139     shutil.copy(os.path.join('libbe', '_version.py'),
140                 os.path.join(export_dir, 'libbe', '_version.py'))
141     make_changelog(os.path.join(export_dir, 'ChangeLog'), tag)
142     set_vcs_name(os.path.join(export_dir, '.be', 'settings'))
143     tarball_file = '%s.tar.gz' % release_name
144     print 'create tarball', tarball_file
145     p = Pipe([['tar', '-czf', tarball_file, export_dir]])
146     assert p.status == 0, p.statuses
147     print 'remove', export_dir
148     shutil.rmtree(export_dir)
149
150 def test():
151     import doctest
152     doctest.testmod() 
153
154 if __name__ == '__main__':
155     import optparse
156     usage = """%prog [options] TAG
157
158 Create a git tag and a release tarball from the current revision.
159 For example
160   %prog 1.0.0
161
162 You may wish to test this out in a dummy branch first to make sure it
163 works as expected to avoid the tedium of unwinding the version-bump
164 commit if it fails.
165 """
166     p = optparse.OptionParser(usage)
167     p.add_option('--test', dest='test', default=False,
168                  action='store_true', help='Run internal tests and exit')
169     options,args = p.parse_args()
170
171     if options.test == True:
172         test()
173         sys.exit(0)
174
175     assert len(args) == 1, '%d (!= 1) arguments: %s' % (len(args), args)
176     tag = args[0]
177     validate_tag(tag)
178
179     if pending_changes() == True:
180         print "Handle pending changes before releasing."
181         sys.exit(1)
182     set_release_version(tag)
183     update_authors()
184     update_files()
185     commit("Bumped to version %s" % tag)
186     tag(tag)
187     create_tarball(tag)