summaryrefslogtreecommitdiff
path: root/tools/release-wrangler.py
blob: 548af7fe235a0814a76f92fa068bbad4f371cf25 (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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
#!/usr/bin/python
#
# release-wrangler.py - very basic release system, primarily for
# Metacity, might be useful for others. In very early stages of
# development.
#
# Copyright (C) 2008 Thomas Thurman
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.

import os
import posixpath
import re
import sys
import commands
import time
import commands

def report_error(message):
  print message
  sys.exit(255)

def get_up_to_date():
  "First step is always to get up to date."
  os.system("svn up")

# yes, I know this is MY username. I will come back and fix it
# later, but for now there is a lot else to do. FIXME
your_username = 'Thomas Thurman  <tthurman@gnome.org>'

def changelog_and_checkin(filename, message):
  changelog = open('ChangeLog.tmp', 'w')
  changelog.write('%s  %s\n\n        * %s: %s\n\n' % (
    time.strftime('%Y-%m-%d',time.gmtime()),
    your_username,
    filename,
    message))

  for line in open('ChangeLog').readlines():
    changelog.write(line)

  changelog.close()
  os.rename('ChangeLog.tmp', 'ChangeLog')

  if os.system('svn commit -m "%s"' % (message.replace('"','\\"')))!=0:
    report_error("Could not commit; bailing.")

def check_we_are_up_to_date():
  changed = []
  for line in commands.getoutput('/usr/bin/svn status').split('\n'):
    if line!='' and (line[0]=='C' or line[0]=='M'):
      if line.find('release-wrangler.py')==-1 and line.find('ChangeLog')==-1:
        # we should be insensitive to changes in this script itself
        # to avoid chicken-and-egg problems
        changed.append(line[1:].lstrip())

  if changed:
    report_error('These files are out of date; I can\'t continue until you fix them: ' + \
      ', '.join(changed))

def version_numbers():
  # FIXME: This is all very metacity-specific. Compare fusa, etc
  """Okay, read through configure.in and find who and where we are.

  We also try to figure out where the next micro version number
  will be; some programs (e.g. Metacity) use a custom numbering
  scheme, and if they have a list of numbers on the line before the
  micro version then that will be used. Otherwise we will just
  increment."""

  version = {}
  previous_line = ''
  for line in file("configure.in").readlines():
    product_name = re.search("^AC_INIT\(\[([^\]]*)\]", line)
    if product_name:
      version['name'] = product_name.group(1)

    version_number = re.search("^m4_define\(\[.*_(.*)_version\], \[(\d+)\]", line)

    if version_number:
      version_type = version_number.group(1)
      version_value = int(version_number.group(2))

      version[version_type] = version_value

      if version_type == 'micro':
        group_of_digits = re.search("^\#\s*([0-9, ]+)\n$", previous_line)
        if group_of_digits:
          versions = [int(x) for x in group_of_digits.group(1).split(',')]

          if version_value in versions:
            try:
              version_index = versions.index(version_value)+1

              if versions[version_index] == version['micro']:
                # work around metacity giving "1" twice
                version_index += 1

              version['micro_next'] = versions[version_index]
            except:
              report_error("You gave a list of micro version numbers, but we've used them up!")
          else:
            report_error("You gave a list of micro version numbers, but the current one wasn't in it! Current is %s and your list is %s" % (
              `version_value`, `versions`))

    previous_line = line

  if not 'micro_next' in version:
    version['micro_next'] = version['micro']+1

  version['string'] = '%(major)s.%(minor)s.%(micro)s' % (version)
  version['filename'] = '%(name)s-%(string)s.tar.gz' % (version)

  return version

def check_file_does_not_exist(version):
  if os.access(version['filename'], os.F_OK):
    report_error("Sorry, you already have a file called %s! Please delete it or move it first." % (version['filename']))

def is_date(str):
  return len(str)>4 and str[4]=='-'

def scan_changelog(version):
  changelog = file("ChangeLog").readlines()

  # Find the most recent release.

  release_date = None

  for line in changelog:
    if is_date(line):
      release_date = line[:10]

    if "Post-release bump to" in line:
      changelog = changelog[:changelog.index(line)+1]
      break

  contributors = {}
  thanks = ''
  entries = []

  def assumed_surname(name):
    if name=='': return ''
    # might get more complicated later, but for now...
    return name.split()[-1]

  def assumed_forename(name):
    if name=='': return ''
    return name.split()[0]

  bug_re = re.compile('bug \#?(\d+)', re.IGNORECASE)
  hash_re = re.compile('\#(\d+)')

  for line in changelog:
    if is_date(line):
      line = line[10:].lstrip()
      line = line[:line.find('<')].rstrip()
      contributors[assumed_surname(line)] = line
      entries.append('(%s)' % (assumed_forename(line)))
    else:
      match = bug_re.search(line)
      if not match: match = hash_re.search(line)
      if match:
        entries[-1] += ' (#%s)' % (match.group(1))

  # FIXME: getting complex enough we should be returning a dictionary
  return (contributors, changelog, entries, release_date)

def wordwrap(str, prefix=''):
  "Really simple wordwrap"

  # Ugly hack:
  # We know that all open brackets are preceded by spaces.
  # We don't want to split on these spaces. Therefore:
  str = str.replace(' (','(')

  result = ['']
  for word in str.split():

    if result[-1]=='':
      candidate = prefix + word
    else:
      candidate = '%s %s' % (result[-1], word)

    if len(candidate)>80:
      result.append(prefix+word)
    else:
      result[-1] = candidate

  return '\n'.join(result).replace('(',' (')

def favourite_editor():
  e = os.environ
  if e.has_key('VISUAL'): return e['VISUAL']
  if e.has_key('EDITOR'): return e['EDITOR']
  if os.access('/usr/bin/nano', os.F_OK):
    return '/usr/bin/nano'
  report_error("I can't find an editor for you!")

def edit_news_entry(version):

  # FIXME: still needs a lot of tidying up. Translator stuff especially needs to be
  # factored out into a separate function.

  (contributors, changelog, entries, release_date) = scan_changelog(version)

  contributors_list = contributors.keys()
  contributors_list.sort()
  thanksline = ', '.join([contributors[x] for x in contributors_list])
  thanksline = thanksline.replace(contributors[contributors_list[-1]], 'and '+contributors[contributors_list[-1]])

  thanks = '%s\n%s\n\n' % (version['string'], '='*len(version['string']))
  thanks += wordwrap('Thanks to %s for improvements in this version.' % (thanksline))
  thanks += '\n\n'
  for line in entries:
    thanks += '  - xxx %s\n' % (line)

  # and now pick up the translations.

  translations = {}
  language_re = re.compile('\*\s*(.+)\.po')

  for line in file("po/ChangeLog").readlines():
    match = language_re.search(line)
    if match:
      translations[match.group(1)] = 1
    if is_date(line) and line[:10]<release_date:
      break

  translator_list = translations.keys()
  translator_list.sort()

  last_translator_re = re.compile('Last-Translator:([^<"]*)', re.IGNORECASE)

  def translator_name(language):
    name = 'unknown'

    if ',' in language:
      language = language[:language.find(',')].replace('.po','')

    filename = 'po/%s.po' % (language)

    if not os.access(filename, os.F_OK):
      # Never mind the translator being unknown, we don't even
      # know about the language!
      return 'Mystery translator (%s)'  % (language)

    for line in file(filename).readlines():
      match = last_translator_re.search(line)
      if match:
        name = match.group(1).rstrip().lstrip()
        break

    return "%s (%s)" % (name, language)

  thanks += '\nTranslations\n'
  thanks += wordwrap(', '.join([translator_name(x) for x in translator_list]), '  ')
  thanks += '\n\n'

  changes = '## '+ ' '.join(changelog).replace('\n', '\n## ')

  filename = posixpath.expanduser("~/.release-wrangler-%(name)s-%(string)s.txt" % version)
  tmp = open(filename, 'w')
  tmp.write('## You are releasing %(name)s, version %(major)s.%(minor)s.%(micro)s.\n' % version)
  tmp.write('## The text at the foot of the page is the part of the ChangeLog which\n')
  tmp.write('## has changed since the last release. Please summarise it.\n')
  tmp.write('## Anything preceded by a # is ignored.\n')
  tmp.write(thanks)
  tmp.write(changes)
  tmp.close()

  os.system(favourite_editor()+' +6 %s ' % (filename))
  # FIXME: if they abort, would be useful to abort here too

  # Write it out to NEWS

  version['announcement'] = ''

  news_tmp = open('NEWS.tmp', 'a')
  for line in open(filename, 'r').readlines():
    if line=='' or line[0]!='#':
      news_tmp.write(line)
      version['announcement'] += line

  for line in open('NEWS').readlines():
    news_tmp.write(line)

  news_tmp.close()

  os.rename('NEWS.tmp', 'NEWS')
  changelog_and_checkin('NEWS', '%(major)s.%(minor)s.%(micro)s release.' % (version))

def build_it_all(version):
  "Now build the thing."
  autogen_prefix= '/prefix' # FIXME: this is specific to tthurman's laptop!

  # FIXME: These should use os.system

  if os.spawnl(os.P_WAIT, './autogen.sh', './autogen.sh', '--prefix', autogen_prefix) != 0:
    print 'autogen failed'
    sys.exit(255)
    
  if os.spawnl(os.P_WAIT, '/usr/bin/make', '/usr/bin/make') != 0:
    print 'make failed'
    sys.exit(255)

  if os.spawnl(os.P_WAIT, '/usr/bin/make', '/usr/bin/make', 'install') != 0:
    print 'install failed'
    sys.exit(255)

  if os.spawnl(os.P_WAIT, '/usr/bin/make', '/usr/bin/make', 'distcheck') != 0:
    print 'distcheck failed'
    sys.exit(255)

  if not os.access(version['filename'], os.F_OK):
    print "Sorry, we don't appear to have a file called %s!" % (archive_filename)
    sys.exit(255)

def upload(version):
  # No, we won't have a configuration option to set your name on master.g.o; that's
  # what ~/.ssh/config is for.

  print "Uploading..."
  upload_result = commands.getstatusoutput('scp %s master.gnome.org:' % (version['filename']))

  if upload_result[0]!=0:
    report_error("There appears to have been an uploading problem: %d\n%s\n" % (upload_result[0], upload_result[1]))

def increment_version(version):
  configure_in = file('configure.in.tmp', 'w')
  for line in file('configure.in'):
    if re.search("^m4_define\(\[.*_micro_version\], \[(\d+)\]", line):
      line = line.replace('[%(micro)s]' % version, '[%(micro_next)s]' % version)
    configure_in.write(line)
  
  configure_in.close()
  os.rename('configure.in.tmp', 'configure.in')

  changelog_and_checkin('configure.in', 'Post-release bump to %(major)s.%(minor)s.%(micro_next)s.' % version)

def tag_the_release(version):
  version['ucname'] = version['name'].upper()
  if os.system("svn cp -m release . svn+ssh://svn.gnome.org/svn/%(name)s/tags/%(ucname)s_%(major)s_%(minor)s_%(micro)s" % (version))!=0:
    report_error("Could not tag; bailing.")

def md5s(version):
  return commands.getstatusoutput('ssh master.gnome.org "cd /ftp/pub/GNOME/sources/%(name)s/%(major)s.%(minor)s/;md5sum $(name)s-%(major)s.%(minor)s.%(micro)s.tar*"' % (version))

def main():
  get_up_to_date()
  check_we_are_up_to_date()
  version = version_numbers()
  check_file_does_not_exist(version)
  edit_news_entry(version)
  build_it_all(version)
  tag_the_release(version)
  increment_version(version)
  upload(version)
  print version['announcement']
  print "-- Done --"

if __name__=='__main__':
  main()