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