]> code.delx.au - gnu-emacs-elpa/blob - admin/forward-diffs.py
(scan_dir): Do not write empty maintainers to the maintfile.
[gnu-emacs-elpa] / admin / forward-diffs.py
1 #!/usr/bin/python
2 ### forward-diffs.py --- forward emacs-elpa-diffs mails to maintainers
3
4 ## Copyright (C) 2012 Free Software Foundation, Inc.
5
6 ## Author: Glenn Morris <rgm@gnu.org>
7
8 ## This program is free software; you can redistribute it and/or modify
9 ## it under the terms of the GNU General Public License as published by
10 ## the Free Software Foundation, either version 3 of the License, or
11 ## (at your option) any later version.
12
13 ## This program is distributed in the hope that it will be useful,
14 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ## GNU General Public License for more details.
17
18 ## You should have received a copy of the GNU General Public License
19 ## along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 ### Commentary:
22
23 ## Forward emails from the emacs-elpa-diffs mailing list to the
24 ## maintainer(s) of the modified files.
25
26 ## Two modes of operation:
27
28 ## 1) Create the maintfile (really this is just an optimization):
29 ## forward-diffs.py --create -p packagesdir -m maintfile
30
31 ## You can start with an empty maintfile and normal operation in 2)
32 ## will append information as needed.
33
34 ## 2) Call from eg procmail to forward diffs. Example usage:
35
36 ## :0c
37 ## * ^TO_emacs-elpa-diffs@gnu\.org
38 ## | forward-diffs.py -p packagedir -m maintfile -l logfile -s sender
39
40 ## where
41
42 ## packagedir = /path/to/packages
43 ## sender = your email address
44 ## logfile = file to write log to (you might want to rotate/compress/examine it)
45 ## maintfile = file listing files and their maintainers, with format:
46 ##
47 ## package1/file1 email1
48 ## package2/file2 email2,email3
49 ##
50 ## Use "nomail" for the email field to not send a mail.
51 ##
52 ## overmaintfile = like maintfile, but takes precedence over it.
53
54 ### Code:
55
56 import optparse
57 import sys
58 import re
59 import email
60 import smtplib
61 import datetime
62 import os
63
64
65 ## Scan FILE for Author or Maintainer (preferred) headers.
66 ## Return a list of all email addresses found in MAINTS.
67 def scan_file(file, maints):
68
69 try:
70 fd = open( file, 'r')
71 except Exception as err:
72 lfile.write('Error opening file %s: %s\n' % (file, str(err)))
73 return 1
74
75 ## Max number of lines to scan looking for a maintainer.
76 ## (20 seems to be the highest at present).
77 max_lines = 50
78 nline = 0
79 cont = 0
80 type = ""
81
82 for line in fd:
83
84 nline += 1
85
86 if ( nline > max_lines ): break
87
88 ## Try and de-obfuscate. Worth it?
89 line = re.sub( '(?i) AT ', '@', line )
90 line = re.sub( '(?i) DOT ', '.', line )
91
92 if cont: # continued header?
93 reg = re.match( ('%s[ \t]+[^:]*?<?([\w.-]+@[\w.-]+)>?' % prefix), line, re.I )
94 if not reg: # not a continued header
95 cont = 0
96 prefix = ""
97 if ( type == "maint" ): break
98 type = ""
99
100 ## Check for one header immediately after another.
101 if not cont:
102 reg = re.match( '([^ ]+)? *(Author|Maintainer)s?: .*?<?([\w.-]+@[\w.-]+)>?', line, re.I )
103
104
105 if not reg: continue
106
107 if cont:
108 email = reg.group(1)
109 maints.append(email)
110 else:
111 cont = 1
112 prefix = reg.group(1) or ""
113 type = reg.group(2)
114 email = reg.group(3)
115 type = "maint" if re.search( 'Maintainer', type, re.I ) else "auth"
116 ## maints = [] does the wrong thing.
117 if type == "maint": del maints[:]
118 maints.append(email)
119
120 fd.close()
121
122
123 ## Scan all the files under dir for maintainer information.
124 ## Write to stdout, or optional argument outfile (which is overwritten).
125 def scan_dir(dir, outfile=None):
126
127 dir = re.sub( '/+$', '', dir) + '/' # ensure trailing /
128
129 if not os.path.isdir(dir):
130 sys.stderr.write('No such directory: %s\n' % dir)
131 sys.exit(1)
132
133 fd = 0
134 if outfile:
135 try:
136 fd = open( outfile, 'w' )
137 except Exception as err:
138 sys.stderr.write("Error opening `%s': %s\n" % (outfile, str(err)))
139 sys.exit(1)
140
141
142 for dirpath, dirnames, filenames in os.walk(dir):
143 for file in filenames:
144 path = os.path.join(dirpath, file)
145 maints = []
146 scan_file(path, maints)
147 ## This would skip printing empty maints.
148 ## That would mean we would scan the file each time for no reason.
149 ## But empty maintainers are an error at present.
150 if not maints: continue
151 path = re.sub( '^%s' % dir, '', path )
152 string = "%-50s %s\n" % (path, ",".join(maints))
153 if fd:
154 fd.write(string)
155 else:
156 print string,
157
158 if fd: fd.close()
159
160
161 usage="""usage: %prog <-p /path/to/packages> <-m maintfile>
162 <-l logfile -s sender|--create> [-o overmaintfile] [--sendmail] [--debug]
163 Take a GNU ELPA diff on stdin, and forward it to the maintainer(s)."""
164
165 parser = optparse.OptionParser()
166 parser.set_usage ( usage )
167 parser.add_option( "-m", dest="maintfile", default=None,
168 help="file listing packages and maintainers")
169 parser.add_option( "-l", dest="logfile", default=None,
170 help="file to append output to")
171 parser.add_option( "-o", dest="overmaintfile", default=None,
172 help="override file listing packages and maintainers")
173 parser.add_option( "-p", dest="packagedir", default=None,
174 help="path to packages directory")
175 parser.add_option( "-s", dest="sender", default=None,
176 help="sender address for forwards")
177 parser.add_option( "--create", dest="create", default=False,
178 action="store_true", help="create maintfile")
179 parser.add_option( "--sendmail", dest="sendmail", default=False,
180 action="store_true", help="use sendmail rather than smtp")
181 parser.add_option( "--debug", dest="debug", default=False,
182 action="store_true", help="debug only, do not send mail")
183
184
185 ( opts, args ) = parser.parse_args()
186
187
188 if not opts.maintfile:
189 parser.error('No maintfile specified')
190
191 if not opts.packagedir:
192 parser.error('No packagedir specified')
193
194 if not os.path.isdir(opts.packagedir):
195 sys.stderr.write('No such directory: %s\n' % opts.packagedir)
196 sys.exit(1)
197
198
199 if not opts.create:
200 if not opts.logfile:
201 parser.error('No logfile specified')
202
203 if not opts.sender:
204 parser.error('No sender specified')
205
206
207 ## Create the maintfile.
208 if opts.create:
209 scan_dir( opts.packagedir, opts.maintfile )
210 sys.exit()
211
212
213 try:
214 lfile = open( opts.logfile, 'a' )
215 except Exception as err:
216 sys.stderr.write('Error opening logfile: %s\n' % str(err))
217 sys.exit(1)
218
219
220 try:
221 mfile = open( opts.maintfile, 'r' )
222 except Exception as err:
223 lfile.write('Error opening maintfile: %s\n' % str(err))
224 sys.exit(1)
225
226 ## Each element is package/file: maint1, maint2, ...
227 maints = {}
228
229 for line in mfile:
230 if re.match( '#| *$', line ): continue
231 ## FIXME error here if empty maintainer.
232 (pfile, maint) = line.split()
233 maints[pfile] = maint.split(',')
234
235 mfile.close()
236
237
238 if opts.overmaintfile:
239 try:
240 ofile = open( opts.overmaintfile, 'r' )
241 except Exception as err:
242 lfile.write('Error opening overmaintfile: %s\n' % str(err))
243 sys.exit(1)
244
245 for line in ofile:
246 if re.match( '#| *$', line ): continue
247 (pfile, maint) = line.split()
248 maints[pfile] = maint.split(',')
249
250 ofile.close()
251
252
253 stdin = sys.stdin
254
255 text = stdin.read()
256
257
258 resent_via = 'GNU ELPA diff forwarder'
259
260 message = email.message_from_string( text )
261
262 (msg_name, msg_from) = email.utils.parseaddr( message['from'] )
263
264 lfile.write('\nDate: %s\n' % str(datetime.datetime.now()))
265 lfile.write('Message-ID: %s\n' % message['message-id'])
266 lfile.write('From: %s\n' % msg_from)
267
268 if resent_via == message['x-resent-via']:
269 lfile.write('Mail loop; aborting\n')
270 sys.exit(1)
271
272
273 start = False
274 pfiles_seen = []
275 maints_seen = []
276
277 for line in text.splitlines():
278
279 if re.match( 'modified:$', line ):
280 start = True
281 continue
282
283 if not start: continue
284
285 if re.match( ' *$', line ): break
286
287
288 reg = re.match( 'packages/([^ ]+)', line.strip() )
289 if not reg: break
290
291
292 pfile = reg.group(1)
293
294 lfile.write('File: %s\n' % pfile)
295
296 ## Should not be possible for files (rather than packages)...
297 if pfile in pfiles_seen:
298 lfile.write('Already seen this file\n')
299 continue
300
301 pfiles_seen.append(pfile)
302
303
304 if not pfile in maints:
305
306 lfile.write('Unknown maintainer, scanning file...\n')
307
308 thismaint = []
309 thisfile = os.path.join( opts.packagedir, pfile )
310
311 scan_file( thisfile, thismaint )
312
313 if not thismaint: continue
314
315 maints[pfile] = thismaint
316
317 ## Append maintainer to file.
318 try:
319 mfile = open( opts.maintfile, 'a' )
320 string = "%-50s %s\n" % (pfile, ",".join(thismaint))
321 mfile.write(string)
322 mfile.close()
323 lfile.write('Appended to maintfile\n')
324 except Exception as err:
325 lfile.write('Error appending to maintfile: %s\n' % str(err))
326
327
328 for maint in maints[pfile]:
329
330 lfile.write('Maint: %s\n' % maint)
331
332
333 if maint in maints_seen:
334 lfile.write('Already seen this maintainer\n')
335 continue
336
337 maints_seen.append(maint)
338
339
340 if maint == "nomail":
341 lfile.write('Not resending, no mail is requested\n')
342 continue
343
344
345 if maint == msg_from:
346 lfile.write('Not resending, since maintainer = committer\n')
347 continue
348
349
350 forward = message
351 forward.add_header('X-Resent-Via', resent_via)
352 forward.add_header('Resent-To', maint)
353 forward.add_header('Resent-From', opts.sender)
354
355 lfile.write('Resending via %s...\n' % ('sendmail'
356 if opts.sendmail else 'smtp') )
357
358
359 if opts.debug: continue
360
361
362 if opts.sendmail:
363 s = os.popen("/usr/sbin/sendmail -i -f %s %s" %
364 (opts.sender, maint), "w")
365 s.write(forward.as_string())
366 status = s.close()
367 if status:
368 lfile.write('Sendmail exit status: %s\n' % status)
369
370 else:
371
372 try:
373 s = smtplib.SMTP('localhost')
374 except Exception as err:
375 lfile.write('Error opening smtp: %s\n' % str(err))
376 sys.exit(1)
377
378 try:
379 s.sendmail(opts.sender, maint, forward.as_string())
380 except Exception as err:
381 lfile.write('Error sending smtp: %s\n' % str(err))
382
383 s.quit()
384
385 ### forward-diffs.py ends here