]> git.donarmstrong.com Git - lilypond.git/blob - buildscripts/git-update-changelog.py
54ab112d0bcaf43fe4a24ec9090ec45b39ba884e
[lilypond.git] / buildscripts / git-update-changelog.py
1 #!/usr/bin/python
2
3 import sys
4 import time
5 import os
6 import re
7 import optparse
8
9 def read_pipe (x):
10     print 'pipe', x
11     return os.popen (x).read ()
12
13 def system (x):
14     print x
15     return os.system (x)
16     
17 class PatchFailed(Exception):
18     pass
19
20 def sign (x):
21     if x < 0:
22         return -1
23     if x > 0:
24         return 1
25
26     return 0
27     
28
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]
36         
37         self.date = ' '.join  (self.date.split (' ')[:-1])
38         self.date = time.strptime (self.date, '%a %b %d %H:%M:%S %Y')
39         
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))
46
47
48     def check_diff_chunk (self, filename, chunk):
49         removals = []
50         def note_removal (m):
51             removals.append (m.group (1))
52             
53         re.sub ('\n-([^\n]+)', note_removal, chunk)
54
55         if removals == []:
56             return True
57
58         if not os.path.exists (filename):
59             return False
60    
61         contents = open (filename).read ()
62         for r in removals:
63             if r not in contents:
64                 return False
65
66         return True
67
68     def check_diff (self):
69         chunks = re.split ('\ndiff --git ', self.diff)
70
71         ok = True
72         for c in chunks:
73             m = re.search ('^a/([^ ]+)', c)
74             if not m:
75                 continue
76             
77             file = m.group (1)
78             
79             c = re.sub('\n--- [^\n]+', '', c)
80             ok = ok and self.check_diff_chunk (file, c)
81             if not ok:
82                 break
83
84         return ok
85         
86     def touched_files (self):
87         files = []
88         def note_file (x):
89             files.append (x.group (1))
90             return ''
91
92         re.sub ('\n--- a/([^\n]+)\n',
93                 note_file, self.diff)
94         re.sub('\n--- /dev/null\n\\+\\+\\+ b/([^\n]+)',
95                note_file, self.diff)
96
97         return files
98
99     def has_patch (self):
100         return self.touched_files () <> []
101     
102     def apply (self, add_del_files):
103         def note_add_file (x):
104             add_del_files.append (('add', x.group (1)))
105             return ''
106         
107         def note_del_file (x):
108             add_del_files.append (('del', x.group (1)))
109             return ''
110         
111         re.sub('\n--- /dev/null\n\\+\\+\\+ b/([^\n]+)',
112                note_add_file, self.diff)
113         
114         re.sub('\n--- a/([^\n]+)\n\\+\\+\\+ /dev/null',
115                note_del_file, self.diff)
116
117         p = os.popen ('patch -f -p1 ', 'w')
118         p.write (self.diff)
119
120         if p.close ():
121             raise PatchFailed, self.committish
122         
123     
124 def parse_commit_log (log):
125     committish = re.search ('^([^\n]+)', log).group (1)
126     author = re.search ('\nAuthor:\s+([^\n]+)', log).group (1)
127     date_match = re.search ('\nDate:\s+([^\n]+)', log)
128     date = date_match.group (1)
129     log = log[date_match.end (1):]
130
131     message = re.sub ("\n *", '', log)
132     message = message.strip ()
133
134     c = Commit (locals ())
135     return c
136
137 def parse_add_changes (from_commit, max_count=0):
138     opt = ''
139     rest = '..'
140     if max_count:
141
142         # fixme.
143         assert max_count == 1
144         opt = '--max-count=%d' % max_count 
145         rest = ''
146         
147     log = read_pipe ('git log %(opt)s %(from_commit)s%(rest)s' % locals ())
148
149     log = log[len ('commit '):]
150     log = log.strip ()
151
152     if not log:
153         return []
154         
155     commits = map (parse_commit_log, re.split ('\ncommit ', log))
156     commits.reverse ()
157     
158     return commits
159
160
161 def header (commit):
162     return '%d-%02d-%02d  %s  <%s>\n' % (commit.date[:3] + (commit.name, commit.email))
163
164 def changelog_body (commit):
165     s = ''
166     s += ''.join ('\n* %s: ' % f for f in commit.touched_files())
167     s += '\n' + commit.message
168     
169     s = s.replace ('\n', '\n\t')
170     s += '\n'
171     return s
172
173 def main ():
174     p = optparse.OptionParser (usage="usage git-update-changelog.py [options] [commits]",
175                                description="""
176 Apply GIT patches and update change log.
177
178 Run this file from the CVS directory, with commits from the repository in --git-dir.
179
180
181
182
183 """)
184     p.add_option ("--start",
185                   action='store',
186                   default='',
187                   metavar="FIRST",
188                   dest="start",
189                   help="all commits starting with FIRST.")
190     
191     p.add_option ("--git-dir",
192                   action='store',
193                   default='',
194                   dest="gitdir",
195                   help="the GIT directory to merge.")
196
197     (options, args) = p.parse_args ()
198     
199     log = open ('ChangeLog').read ()
200
201     if options.gitdir:
202         os.environ['GIT_DIR'] = options.gitdir
203
204
205     if not args:
206         if not options.start:
207             print 'Must set start committish.'  
208             sys.exit (1)
209
210         commits = parse_add_changes (options.start)
211     else:
212         commits = [] 
213         for a in args:
214             commits += parse_add_changes (a, max_count=1)
215
216     if not commits:
217         return
218     
219     new_log = ''
220     last_commit = None
221
222     first = header (commits[0]) + '\n'
223     if first == log[:len (first)]:
224         log = log[len (first):]
225
226     try:
227         previously_done = dict((c, 1) for c in open ('.git-commits-done').read ().split ('\n'))
228     except OSError:
229         previously_done = {}
230
231     commits = [c for c in commits if not previously_done.has_key (c.committish)]
232     commits = [c for c in commits if not previously_done.has_key (c.committish)]
233     commits = sorted (commits, cmp=Commit.compare)
234
235     
236     file_adddel = []
237     collated_log = ''
238     collated_message = ''
239     commits_done = []
240     while commits:
241         c = commits[0]
242         
243         if not c.has_patch ():
244             print 'patchless commit (merge?)'
245             continue
246
247         ok = c.check_diff ()
248
249         if not ok:
250             print "Patch doesn't seem to apply"
251             print 'skipping', c.committish
252             print 'message:', c.message
253
254             break
255
256
257         commits = commits[1:]
258         commits_done.append (c) 
259             
260         print 'patch ', c.committish
261         try:
262             c.apply (file_adddel)
263         except PatchFailed:
264             break
265         
266         if c.touched_files () == ['ChangeLog']:
267             continue
268         
269         if (last_commit
270             and c.author != last_commit.author
271             and c.date[:3] != last_commit.date[:3]):
272
273             new_log += header (last_commit)
274
275         collated_log = changelog_body (c)  + collated_log
276         last_commit = c
277
278         collated_message += c.message + '\n'
279         
280
281
282     for (op, f) in file_adddel:
283         if op == 'del':
284             system ('cvs remove %(f)s' % locals ())
285         if op == 'add':
286             system ('cvs add %(f)s' % locals ())
287
288     if last_commit: 
289         collated_log = header (last_commit) + collated_log + '\n'
290
291     log = collated_log + log
292
293     try:
294         os.unlink ('ChangeLog~')
295     except OSError:
296         pass
297     
298     os.rename ('ChangeLog', 'ChangeLog~')
299     open ('ChangeLog', 'w').write (log)
300
301     open ('.msg','w').write (collated_message)
302     print '\nCommit message\n**\n%s\n**\n' % collated_message
303     print '\nRun:\n\n\tcvs commit -F .msg\n\n'
304     print '\n\techo %s >> .git-commits-done\n\n' % ' '.join ([c.committish
305                                                               for c in commits_done]) 
306
307
308     if commits:
309         print 'Commits left to do:'
310         print ' '.join ([c.committish for c in commits])
311     
312 main ()
313     
314     
315