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