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