]> code.delx.au - gnu-emacs-elpa/blobdiff - admin/forward-diffs.py
* README: Update for new "Version: 0" convention.
[gnu-emacs-elpa] / admin / forward-diffs.py
index 9de81e23135477dcbfadf9f6a0b994d3347abecc..c0c330d230b554fd899cb87bf819a85dba495e73 100755 (executable)
@@ -1,9 +1,10 @@
 #!/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   email1
-## package2   email2,email3
+## 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.
 ##
-## overmaintfile = like maintfile, but takes precedence over it.
+## overmaint = like maintfile, but takes precedence over it.
 
 ### Code:
 
@@ -52,9 +66,107 @@ import smtplib
 import datetime
 import os
 
-usage="""usage: %prog <-m maintfile> <-l logfile> <-s sender>
-   <-p /path/to/packages> [-o overmaintfile] [--sendmail] [--debug]
-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 )
@@ -68,6 +180,16 @@ 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,
@@ -76,8 +198,6 @@ parser.add_option( "--debug", dest="debug", default=False,
 
 ( opts, args ) = parser.parse_args()
 
-if not opts.logfile:
-    parser.error('No logfile specified')
 
 if not opts.maintfile:
     parser.error('No maintfile specified')
@@ -85,15 +205,19 @@ if not opts.maintfile:
 if not opts.packagedir:
     parser.error('No packagedir specified')
 
-if not opts.sender:
-    parser.error('No sender specified')
-
-
 if not os.path.isdir(opts.packagedir):
-    sys.stderr.write('Error reading packagedir\n')
+    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 = open( opts.logfile, 'a' )
 except Exception as err:
@@ -107,13 +231,20 @@ except Exception as err:
     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()
 
@@ -126,9 +257,9 @@ if opts.overmaintfile:
         sys.exit(1)
 
     for line in ofile:
-        if re.match( '^#|^ *$', line ): continue
-        (pack, maint) = line.split()
-        maints[pack] = maint.split(',')
+        if re.match( '#| *$', line ): continue
+        (pfile, maint) = line.split()
+        maints[pfile] = maint.split(',')
 
     ofile.close()
 
@@ -138,7 +269,7 @@ stdin = sys.stdin
 text = stdin.read()
 
 
-resent_via = 'GNU ELPA diff forwarder'
+resent_via = 'GNU Emacs diff forwarder'
 
 message = email.message_from_string( text )
 
@@ -154,41 +285,100 @@ if resent_via == message['x-resent-via']:
 
 
 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\n')
+    ## 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)