#!/usr/bin/python
-### forward-diffs.py --- forward emacs-elpa-diffs mails to maintainers
+### forward-diffs.py --- forward emacs-diffs mails to maintainers
-## Copyright (C) 2012 Free Software Foundation, Inc.
+## Copyright (C) 2012-2014 Free Software Foundation, Inc.
## Author: Glenn Morris <rgm@gnu.org>
+## Maintainer: emacs-devel@gnu.org
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
### Commentary:
-## Forward emails from the emacs-elpa-diffs mailing list to the
+## Forward emails from an emacs-diffs style mailing list to the
## maintainer(s) of the modified files.
-## Example usage from procmail (all arguments are compulsory):
+## Two modes of operation:
+
+## 1) Create the maintfile (really this is just an optimization):
+## forward-diffs.py --create -p packagesdir -m maintfile
+
+## You can start with an empty maintfile and normal operation in 2)
+## will append information as needed.
+
+## 2) Call from eg procmail to forward diffs. Example usage:
## :0c
## * ^TO_emacs-elpa-diffs@gnu\.org
-## | forward-diffs.py -m maintfile -l logfile -s sender
+## | forward-diffs.py -p packagedir -m maintfile -l logfile \
+## -o overmaint -s sender
## where
+## packagedir = /path/to/packages
## sender = your email address
## logfile = file to write log to (you might want to rotate/compress/examine it)
-## maintfile = file listing packages and their maintainers, with format:
+## maintfile = file listing files and their maintainers, with format:
+##
+## package1/file1 email1
+## package2/file2 email2,email3
+## package3 email4
+##
+## Use "nomail" for the email field to not send a mail.
+## An entry that is a directory applies to all files in that directory
+## that do not have specific maintainers.
##
-## package1 email1
-## package2 email2,email3
+## overmaint = like maintfile, but takes precedence over it.
### Code:
import datetime
import os
-usage="""usage: %prog <-m maintfile> <-l logfile> <-s sender> [--sendmail]
-Take a GNU ELPA diff on stdin, and forward it to the maintainer(s)."""
+
+## Scan FILE for Author or Maintainer (preferred) headers.
+## Return a list of all email addresses found in MAINTS.
+def scan_file(file, maints):
+
+ try:
+ fd = open( file, 'r')
+ except Exception as err:
+ lfile.write('Error opening file %s: %s\n' % (file, str(err)))
+ return 1
+
+ ## Max number of lines to scan looking for a maintainer.
+ ## (20 seems to be the highest at present).
+ max_lines = 50
+ nline = 0
+ cont = 0
+ type = ""
+
+ for line in fd:
+
+ nline += 1
+
+ if ( nline > max_lines ): break
+
+ ## Try and de-obfuscate. Worth it?
+ line = re.sub( '(?i) AT ', '@', line )
+ line = re.sub( '(?i) DOT ', '.', line )
+
+ if cont: # continued header?
+ reg = re.match( ('%s[ \t]+[^:]*?<?([\w.-]+@[\w.-]+)>?' % prefix), line, re.I )
+ if not reg: # not a continued header
+ cont = 0
+ prefix = ""
+ if ( type == "maint" ): break
+ type = ""
+
+ ## Check for one header immediately after another.
+ if not cont:
+ reg = re.match( '([^ ]+)? *(Author|Maintainer)s?: .*?<?([\w.-]+@[\w.-]+)>?', line, re.I )
+
+
+ if not reg: continue
+
+ if cont:
+ email = reg.group(1)
+ maints.append(email)
+ else:
+ cont = 1
+ prefix = reg.group(1) or ""
+ type = reg.group(2)
+ email = reg.group(3)
+ type = "maint" if re.search( 'Maintainer', type, re.I ) else "auth"
+ ## maints = [] does the wrong thing.
+ if type == "maint": del maints[:]
+ maints.append(email)
+
+ fd.close()
+
+
+## Scan all the files under dir for maintainer information.
+## Write to stdout, or optional argument outfile (which is overwritten).
+def scan_dir(dir, outfile=None):
+
+ dir = re.sub( '/+$', '', dir) + '/' # ensure trailing /
+
+ if not os.path.isdir(dir):
+ sys.stderr.write('No such directory: %s\n' % dir)
+ sys.exit(1)
+
+ fd = 0
+ if outfile:
+ try:
+ fd = open( outfile, 'w' )
+ except Exception as err:
+ sys.stderr.write("Error opening `%s': %s\n" % (outfile, str(err)))
+ sys.exit(1)
+
+
+ for dirpath, dirnames, filenames in os.walk(dir):
+ for file in filenames:
+ path = os.path.join(dirpath, file)
+ maints = []
+ scan_file(path, maints)
+ ## This would skip printing empty maints.
+ ## That would mean we would scan the file each time for no reason.
+ ## But empty maintainers are an error at present.
+ if not maints: continue
+ path = re.sub( '^%s' % dir, '', path )
+ string = "%-50s %s\n" % (path, ",".join(maints))
+ if fd:
+ fd.write(string)
+ else:
+ print string,
+
+ if fd: fd.close()
+
+
+usage="""usage: %prog <-p /path/to/packages> <-m maintfile>
+ <-l logfile -s sender|--create> [-o overmaintfile] [--prefix prefix]
+ [--sendmail] [--debug]
+Take an emacs-diffs mail on stdin, and forward it to the maintainer(s)."""
parser = optparse.OptionParser()
parser.set_usage ( usage )
help="file listing packages and maintainers")
parser.add_option( "-l", dest="logfile", default=None,
help="file to append output to")
+parser.add_option( "-o", dest="overmaintfile", default=None,
+ help="override file listing packages and maintainers")
+parser.add_option( "-p", dest="packagedir", default=None,
+ help="path to packages directory")
parser.add_option( "-s", dest="sender", default=None,
help="sender address for forwards")
+parser.add_option( "--create", dest="create", default=False,
+ action="store_true", help="create maintfile")
+parser.add_option( "--no-scan", dest="noscan", default=True,
+ action="store_true",
+ help="don't scan for maintainers; implies --no-update")
+parser.add_option( "--no-update", dest="noupdate", default=False,
+ action="store_true",
+ help="do not update the maintfile")
+parser.add_option( "--prefix", dest="prefix", default="packages/",
+ help="prefix to remove from modified file name [default: %default]")
parser.add_option( "--sendmail", dest="sendmail", default=False,
action="store_true", help="use sendmail rather than smtp")
+parser.add_option( "--debug", dest="debug", default=False,
+ action="store_true", help="debug only, do not send mail")
+
( opts, args ) = parser.parse_args()
-if not opts.logfile:
- parser.error('No logfile specified')
if not opts.maintfile:
parser.error('No maintfile specified')
-if not opts.sender:
- parser.error('No sender specified')
+if not opts.packagedir:
+ parser.error('No packagedir specified')
+
+if not os.path.isdir(opts.packagedir):
+ sys.stderr.write('No such directory: %s\n' % opts.packagedir)
+ sys.exit(1)
+
+
+if not opts.create:
+ if not opts.logfile:
+ parser.error('No logfile specified')
+
+ if not opts.sender:
+ parser.error('No sender specified')
try:
lfile.write('Error opening maintfile: %s\n' % str(err))
sys.exit(1)
-## Each element is package: maint1, maint2, ...
+## Create the maintfile.
+if opts.create:
+ scan_dir( opts.packagedir, opts.maintfile )
+ sys.exit()
+
+
+## Each element is package/file: maint1, maint2, ...
maints = {}
for line in mfile:
- if re.match( '^#|^ *$', line ): continue
- (pack, maint) = line.split()
- maints[pack] = maint.split(',')
+ if re.match( '#| *$', line ): continue
+ ## FIXME error here if empty maintainer.
+ (pfile, maint) = line.split()
+ maints[pfile] = maint.split(',')
mfile.close()
+if opts.overmaintfile:
+ try:
+ ofile = open( opts.overmaintfile, 'r' )
+ except Exception as err:
+ lfile.write('Error opening overmaintfile: %s\n' % str(err))
+ sys.exit(1)
+
+ for line in ofile:
+ if re.match( '#| *$', line ): continue
+ (pfile, maint) = line.split()
+ maints[pfile] = maint.split(',')
+
+ ofile.close()
+
+
stdin = sys.stdin
text = stdin.read()
-resent_via = 'GNU ELPA diff forwarder'
+resent_via = 'GNU Emacs diff forwarder'
message = email.message_from_string( text )
lfile.write('From: %s\n' % msg_from)
if resent_via == message['x-resent-via']:
- lfile.write('Mail loop; aborting')
+ lfile.write('Mail loop; aborting\n')
sys.exit(1)
start = False
-packs_seen = []
+pfiles_seen = []
maints_seen = []
for line in text.splitlines():
- if re.match( '^modified:$', line ):
+ # Look for and process things that look like (Git):
+ #
+ # Summary of changes:
+ # packages/vlf/vlf.el | 2 +-
+ # 1 files changed, 1 insertions(+), 1 deletions(-)
+ #
+ # or things that look like (Git):
+ #
+ # ---
+ # packages/vlf/vlf.el | 2 +-
+ # 1 files changed, 1 insertions(+), 1 deletions(-)
+
+ #BZR: if re.match( 'modified:$', line ):
+ if re.match( '---|Summary of changes:$', line ):
start = True
continue
if not start: continue
- if re.match( '^ *$', line ): break
-
-
- reg = re.match( '^packages/([^/]+)', line.strip() )
- if not reg: break
-
+ ## An empty line or a line with non-empty first character.
+ if re.match( '( *$|[^ ])', line ): break
+ # Any line that doesn't match the diffstat format (Git).
+ if not re.match( ' [^ ]+ +\| ', line ):
+ lfile.write('Stop scanning at: %s\n' % line)
+ break
+
+ if opts.prefix:
+ #BZR: reg = re.match( '%s([^ ]+)' % opts.prefix, line.strip() )
+ reg = re.match( ' %s([^ ]+)' % opts.prefix, line )
+ if not reg:
+ lfile.write('Skip: %s\n' % line)
+ continue
+ pfile = reg.group(1)
+ else:
+ pfile = line.strip()
- pack = reg.group(1)
- lfile.write('Package: %s\n' % pack)
+ lfile.write('File: %s\n' % pfile)
- if pack in packs_seen:
- lfile.write('Already seen this package')
+ ## Should not be possible for files (rather than packages)...
+ if pfile in pfiles_seen:
+ lfile.write('Already seen this file\n')
continue
- packs_seen.append(pack)
+ pfiles_seen.append(pfile)
- if not pack in maints:
+ if not pfile in maints:
+
lfile.write('Unknown maintainer\n')
+
+ if not opts.noscan:
+
+ lfile.write('Scanning file...\n')
+ thismaint = []
+ thisfile = os.path.join( opts.packagedir, pfile )
+ # scan_file( thisfile, thismaint )
+
+ if thismaint:
+ maints[pfile] = thismaint
+
+ ## Append maintainer to file.
+ if not opts.noupdate:
+ try:
+ mfile = open( opts.maintfile, 'a' )
+ string = "%-50s %s\n" % (pfile, ",".join(thismaint))
+ mfile.write(string)
+ mfile.close()
+ lfile.write('Appended to maintfile\n')
+ except Exception as err:
+ lfile.write('Error appending to maintfile: %s\n' %
+ str(err))
+
+ ## Didn't scan, or scanning did not work.
+ ## Look for a directory maintainer.
+ if not pfile in maints:
+ lfile.write('No file maintainer, trying directories...\n')
+ while True:
+ (pfile, tail) = os.path.split(pfile)
+ if not pfile: break
+ if pfile in maints: break
+
+
+ if not pfile in maints:
+ lfile.write('No maintainer, skipping\n')
continue
- for maint in maints[pack]:
+ for maint in maints[pfile]:
lfile.write('Maint: %s\n' % maint)
if maint in maints_seen:
- lfile.write('Already seen this maintainer')
+ lfile.write('Already seen this maintainer\n')
continue
maints_seen.append(maint)
+ if maint == "nomail":
+ lfile.write('Not resending, no mail is requested\n')
+ continue
+
+
if maint == msg_from:
- lfile.write('Not resending, since maintainer = committer')
+ lfile.write('Not resending, since maintainer = committer\n')
continue
if opts.sendmail else 'smtp') )
+ if opts.debug: continue
+
+
if opts.sendmail:
s = os.popen("/usr/sbin/sendmail -i -f %s %s" %
(opts.sender, maint), "w")