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