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