Bump version.
[lilypond.git] / buildscripts / git-update-changelog.py
blob09f0d16b7afebad8214b9463cef4076fdc5f1301
1 #!/usr/bin/python
3 import sys
4 import time
5 import os
6 import re
7 import optparse
9 def read_pipe (x):
10 print 'pipe', x
11 return os.popen (x).read ()
13 def system (x):
14 print x
15 return os.system (x)
17 class PatchFailed(Exception):
18 pass
20 def sign (x):
21 if x < 0:
22 return -1
23 if x > 0:
24 return 1
26 return 0
29 class Commit:
30 def __init__ (self, dict):
31 for v in ('message',
32 'date',
33 'author',
34 'committish'):
35 self.__dict__[v] = dict[v]
37 self.date = ' '.join (self.date.split (' ')[:-1])
38 self.date = time.strptime (self.date, '%a %b %d %H:%M:%S %Y')
40 m = re.search ('(.*)<(.*)>', self.author)
41 self.email = m.group (2).strip ()
42 self.name = m.group (1).strip ()
43 self.diff = read_pipe ('git show %s' % self.committish)
44 def compare (self, other):
45 return sign (time.mktime (self.date) - time.mktime (other.date))
48 def check_diff_chunk (self, filename, chunk):
49 removals = []
50 def note_removal (m):
51 removals.append (m.group (1))
53 re.sub ('\n-([^\n]+)', note_removal, chunk)
55 if removals == []:
56 return True
57 if not os.path.exists (filename):
58 return False
60 contents = open (filename).read ()
61 for r in removals:
62 if r not in contents:
63 return False
65 return True
67 def check_diff (self):
68 chunks = re.split ('\ndiff --git ', self.diff)
70 ok = True
71 for c in chunks:
72 m = re.search ('^a/([^ ]+)', c)
73 if not m:
74 continue
76 file = m.group (1)
78 c = re.sub('\n--- [^\n]+', '', c)
79 ok = ok and self.check_diff_chunk (file, c)
80 if not ok:
81 break
83 return ok
85 def touched_files (self):
86 files = []
87 def note_file (x):
88 files.append (x.group (1))
89 return ''
91 re.sub ('\n--- a/([^\n]+)\n',
92 note_file, self.diff)
93 re.sub('\n--- /dev/null\n\\+\\+\\+ b/([^\n]+)',
94 note_file, self.diff)
96 return files
98 def has_patch (self):
99 return self.touched_files () <> []
101 def apply (self, add_del_files):
102 def note_add_file (x):
103 add_del_files.append (('add', x.group (1)))
104 return ''
106 def note_del_file (x):
107 add_del_files.append (('del', x.group (1)))
108 return ''
110 re.sub('\n--- /dev/null\n\\+\\+\\+ b/([^\n]+)',
111 note_add_file, self.diff)
113 re.sub('\n--- a/([^\n]+)\n\\+\\+\\+ /dev/null',
114 note_del_file, self.diff)
116 p = os.popen ('patch -f -p1 ', 'w')
117 p.write (self.diff)
119 if p.close ():
120 raise PatchFailed, self.committish
123 def parse_commit_log (log):
124 committish = re.search ('^([^\n]+)', log).group (1)
125 author = re.search ('\nAuthor:\s+([^\n]+)', log).group (1)
126 date_match = re.search ('\nDate:\s+([^\n]+)', log)
127 date = date_match.group (1)
128 log = log[date_match.end (1):]
130 message = re.sub ("\n *", '', log)
131 message = message.strip ()
133 c = Commit (locals ())
134 return c
136 def parse_add_changes (from_commit, max_count=0):
137 opt = ''
138 rest = '..'
139 if max_count:
141 # fixme.
142 assert max_count == 1
143 opt = '--max-count=%d' % max_count
144 rest = ''
146 log = read_pipe ('git log %(opt)s %(from_commit)s%(rest)s' % locals ())
148 log = log[len ('commit '):]
149 log = log.strip ()
151 if not log:
152 return []
154 commits = map (parse_commit_log, re.split ('\ncommit ', log))
155 commits.reverse ()
157 return commits
160 def header (commit):
161 return '%d-%02d-%02d %s <%s>\n' % (commit.date[:3] + (commit.name, commit.email))
163 def changelog_body (commit):
164 s = ''
165 s += ''.join ('\n* %s: ' % f for f in commit.touched_files())
166 s += '\n' + commit.message
168 s = s.replace ('\n', '\n\t')
169 s += '\n'
170 return s
172 def main ():
173 p = optparse.OptionParser (usage="usage git-update-changelog.py [options] [commits]",
174 description="""
175 Apply GIT patches and update change log.
177 Run this file from the CVS directory, with commits from the repository in --git-dir.
179 """)
180 p.add_option ("--start",
181 action='store',
182 default='',
183 metavar="FIRST",
184 dest="start",
185 help="all commits starting with FIRST (exclusive).")
187 p.add_option ("--git-dir",
188 action='store',
189 default='',
190 dest="gitdir",
191 help="the GIT directory to merge.")
193 (options, args) = p.parse_args ()
195 log = open ('ChangeLog').read ()
197 if options.gitdir:
198 os.environ['GIT_DIR'] = options.gitdir
201 if not args:
202 if not options.start:
203 print 'Must set start committish.'
204 sys.exit (1)
206 commits = parse_add_changes (options.start)
207 else:
208 commits = []
209 for a in args:
210 commits += parse_add_changes (a, max_count=1)
212 if not commits:
213 return
215 new_log = ''
216 last_commit = None
218 first = header (commits[0]) + '\n'
219 if first == log[:len (first)]:
220 log = log[len (first):]
222 try:
223 previously_done = dict((c, 1) for c in open ('.git-commits-done').read ().split ('\n'))
224 except IOError:
225 previously_done = {}
227 commits = [c for c in commits if not previously_done.has_key (c.committish)]
228 commits = sorted (commits, cmp=Commit.compare)
230 system ('cvs up')
232 file_adddel = []
233 collated_log = ''
234 collated_message = ''
235 commits_done = []
236 while commits:
237 c = commits[0]
239 if not c.has_patch ():
240 print 'patchless commit (merge?)'
241 continue
243 ok = c.check_diff ()
245 if not ok:
246 print "Patch doesn't seem to apply"
247 print 'skipping', c.committish
248 print 'message:', c.message
250 break
253 commits = commits[1:]
254 commits_done.append (c)
256 print 'patch ', c.committish
257 try:
258 c.apply (file_adddel)
259 except PatchFailed:
260 break
262 if c.touched_files () == ['ChangeLog']:
263 continue
265 if (last_commit
266 and c.author != last_commit.author
267 and c.date[:3] != last_commit.date[:3]):
269 new_log += header (last_commit)
271 collated_log = changelog_body (c) + collated_log
272 last_commit = c
274 collated_message += c.message + '\n'
278 for (op, f) in file_adddel:
279 if op == 'del':
280 system ('cvs remove %(f)s' % locals ())
281 if op == 'add':
282 system ('cvs add %(f)s' % locals ())
284 if last_commit:
285 collated_log = header (last_commit) + collated_log + '\n'
287 log = collated_log + log
289 try:
290 os.unlink ('ChangeLog~')
291 except OSError:
292 pass
294 os.rename ('ChangeLog', 'ChangeLog~')
295 open ('ChangeLog', 'w').write (log)
297 open ('.msg','w').write (collated_message)
298 print '\nCommit message\n**\n%s\n**\n' % collated_message
299 print '\nRun:\n\n\tcvs commit -F .msg\n\n'
300 print '\n\techo %s >> .git-commits-done\n\n' % ' '.join ([c.committish
301 for c in commits_done])
304 if commits:
305 print 'Commits left to do:'
306 print ' '.join ([c.committish for c in commits])
308 main ()