Allow hand-editing of patches before sending
[stgit] / stgit / commands / mail.py
index bd56f16..2b28564 100644 (file)
@@ -36,10 +36,10 @@ generated from the template file passed as argument to '--template'
 can either be added to the template file or passed via the
 corresponding command line options.
 
 can either be added to the template file or passed via the
 corresponding command line options.
 
-A preamble e-mail can be sent using the '--cover' and/or '--edit'
-options. The first allows the user to specify a file to be used as a
-template. The latter option will invoke the editor on the specified
-file (defaulting to '.git/covermail.tmpl' or
+A preamble e-mail can be sent using the '--cover' and/or
+'--edit-cover' options. The first allows the user to specify a file to
+be used as a template. The latter option will invoke the editor on the
+specified file (defaulting to '.git/covermail.tmpl' or
 '~/.stgit/templates/covermail.tmpl' or
 '/usr/share/stgit/templates/covermail.tmpl').
 
 '~/.stgit/templates/covermail.tmpl' or
 '/usr/share/stgit/templates/covermail.tmpl').
 
@@ -65,6 +65,7 @@ the following variables:
   %(diffstat)s     - diff statistics
   %(date)s         - current date/time
   %(version)s      - ' version' string passed on the command line (or empty)
   %(diffstat)s     - diff statistics
   %(date)s         - current date/time
   %(version)s      - ' version' string passed on the command line (or empty)
+  %(prefix)s       - 'prefix ' string passed on the command line
   %(patchnr)s      - patch number
   %(totalnr)s      - total number of patches to be sent
   %(number)s       - empty if only one patch is sent or ' patchnr/totalnr'
   %(patchnr)s      - patch number
   %(totalnr)s      - total number of patches to be sent
   %(number)s       - empty if only one patch is sent or ' patchnr/totalnr'
@@ -90,18 +91,26 @@ options = [make_option('-a', '--all',
            make_option('--bcc',
                        help = 'add BCC to the Bcc: list',
                        action = 'append'),
            make_option('--bcc',
                        help = 'add BCC to the Bcc: list',
                        action = 'append'),
+           make_option('--auto',
+                       help = 'automatically cc the patch signers',
+                       action = 'store_true'),
            make_option('--noreply',
                        help = 'do not send subsequent messages as replies',
                        action = 'store_true'),
            make_option('-v', '--version', metavar = 'VERSION',
                        help = 'add VERSION to the [PATCH ...] prefix'),
            make_option('--noreply',
                        help = 'do not send subsequent messages as replies',
                        action = 'store_true'),
            make_option('-v', '--version', metavar = 'VERSION',
                        help = 'add VERSION to the [PATCH ...] prefix'),
+           make_option('--prefix', metavar = 'PREFIX',
+                       help = 'add PREFIX to the [... PATCH ...] prefix'),
            make_option('-t', '--template', metavar = 'FILE',
                        help = 'use FILE as the message template'),
            make_option('-c', '--cover', metavar = 'FILE',
                        help = 'send FILE as the cover message'),
            make_option('-t', '--template', metavar = 'FILE',
                        help = 'use FILE as the message template'),
            make_option('-c', '--cover', metavar = 'FILE',
                        help = 'send FILE as the cover message'),
-           make_option('-e', '--edit',
+           make_option('-e', '--edit-cover',
                        help = 'edit the cover message before sending',
                        action = 'store_true'),
                        help = 'edit the cover message before sending',
                        action = 'store_true'),
+           make_option('-E', '--edit-patches',
+                       help = 'edit each patch before sending',
+                       action = 'store_true'),
            make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS',
                        help = 'sleep for SECONDS between e-mails sending'),
            make_option('--refid',
            make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS',
                        help = 'sleep for SECONDS between e-mails sending'),
            make_option('--refid',
@@ -185,24 +194,81 @@ def __write_mbox(from_addr, msg):
     print msg
     print
 
     print msg
     print
 
-def __build_address_headers(options):
-    headers_end = ''
+def __build_address_headers(tmpl, options, extra_cc = []):
+    """Build the address headers and check existing headers in the
+    template.
+    """
+    def csv(lst):
+        s = ''
+        for i in lst:
+            if not i:
+                continue
+            if s:
+                s += ', ' + i
+            else:
+                s = i
+        return s
+
+    def replace_header(header, addr, tmpl):
+        r = re.compile('^' + header + ':\s+.+$', re.I | re.M)
+        if r.search(tmpl):
+            tmpl = r.sub('\g<0>, ' + addr, tmpl, 1)
+            h = ''
+        else:
+            h = header + ': ' + addr
+
+        return tmpl, h
+
+    headers = ''
+    to_addr = ''
+    cc_addr = ''
+    bcc_addr = ''
+
+    if config.has_option('stgit', 'autobcc'):
+        autobcc = config.get('stgit', 'autobcc')
+    else:
+        autobcc = ''
+
     if options.to:
     if options.to:
-        headers_end += 'To: '
-        for to in options.to:
-            headers_end += '%s, ' % to
-        headers_end = headers_end[:-2] + '\n'
+        to_addr = csv(options.to)
     if options.cc:
     if options.cc:
-        headers_end += 'Cc: '
-        for cc in options.cc:
-            headers_end += '%s, ' % cc
-        headers_end = headers_end[:-2] + '\n'
+        cc_addr = csv(options.cc + extra_cc)
+    elif extra_cc:
+        cc_addr = csv(extra_cc)
     if options.bcc:
     if options.bcc:
-        headers_end += 'Bcc: '
-        for bcc in options.bcc:
-            headers_end += '%s, ' % bcc
-        headers_end = headers_end[:-2] + '\n'
-    return headers_end
+        bcc_addr = csv(options.bcc + [autobcc])
+    elif autobcc:
+        bcc_addr = autobcc
+
+    # replace existing headers
+    if to_addr:
+        tmpl, h = replace_header('To', to_addr, tmpl)
+        if h:
+            headers += h + '\n'
+    if cc_addr:
+        tmpl, h = replace_header('Cc', cc_addr, tmpl)
+        if h:
+            headers += h + '\n'
+    if bcc_addr:
+        tmpl, h = replace_header('Bcc', bcc_addr, tmpl)
+        if h:
+            headers += h + '\n'
+
+    return tmpl, headers
+
+def __get_signers_list(msg):
+    """Return the address list generated from signed-off-by and
+    acked-by lines in the message.
+    """
+    addr_list = []
+
+    r = re.compile('^(signed-off-by|acked-by):\s+(.+)$', re.I)
+    for line in msg.split('\n'):
+        m = r.match(line)
+        if m:
+            addr_list.append(m.expand('\g<2>'))
+
+    return addr_list
 
 def __build_extra_headers():
     """Build extra headers like content-type etc.
 
 def __build_extra_headers():
     """Build extra headers like content-type etc.
@@ -213,6 +279,34 @@ def __build_extra_headers():
 
     return headers
 
 
     return headers
 
+def edit_message(msg):
+    fname = '.stgitmail.txt'
+
+    # create the initial file
+    f = file(fname, 'w')
+    f.write(msg)
+    f.close()
+
+    # the editor
+    if config.has_option('stgit', 'editor'):
+        editor = config.get('stgit', 'editor')
+    elif 'EDITOR' in os.environ:
+        editor = os.environ['EDITOR']
+    else:
+        editor = 'vi'
+    editor += ' %s' % fname
+
+    print 'Invoking the editor: "%s"...' % editor,
+    sys.stdout.flush()
+    print 'done (exit code: %d)' % os.system(editor)
+
+    # read the message back
+    f = file(fname)
+    msg = f.read()
+    f.close()
+
+    return msg
+
 def __build_cover(tmpl, total_nr, msg_id, options):
     """Build the cover message (series description) to be sent via SMTP
     """
 def __build_cover(tmpl, total_nr, msg_id, options):
     """Build the cover message (series description) to be sent via SMTP
     """
@@ -220,7 +314,7 @@ def __build_cover(tmpl, total_nr, msg_id, options):
     if not maintainer:
         maintainer = ''
 
     if not maintainer:
         maintainer = ''
 
-    headers_end = __build_address_headers(options)
+    tmpl, headers_end = __build_address_headers(tmpl, options)
     headers_end += 'Message-Id: %s\n' % msg_id
     if options.refid:
         headers_end += "In-Reply-To: %s\n" % options.refid
     headers_end += 'Message-Id: %s\n' % msg_id
     if options.refid:
         headers_end += "In-Reply-To: %s\n" % options.refid
@@ -232,6 +326,11 @@ def __build_cover(tmpl, total_nr, msg_id, options):
     else:
         version_str = ''
 
     else:
         version_str = ''
 
+    if options.prefix:
+        prefix_str = options.prefix + ' '
+    else:
+        prefix_str = ''
+        
     total_nr_str = str(total_nr)
     patch_nr_str = '0'.zfill(len(total_nr_str))
     if total_nr > 1:
     total_nr_str = str(total_nr)
     patch_nr_str = '0'.zfill(len(total_nr_str))
     if total_nr > 1:
@@ -243,6 +342,7 @@ def __build_cover(tmpl, total_nr, msg_id, options):
                  'endofheaders': headers_end,
                  'date':         email.Utils.formatdate(localtime = True),
                  'version':      version_str,
                  'endofheaders': headers_end,
                  'date':         email.Utils.formatdate(localtime = True),
                  'version':      version_str,
+                 'prefix':      prefix_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str}
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str}
@@ -256,31 +356,8 @@ def __build_cover(tmpl, total_nr, msg_id, options):
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
-    if options.edit:
-        fname = '.stgitmail.txt'
-
-        # create the initial file
-        f = file(fname, 'w+')
-        f.write(msg)
-        f.close()
-
-        # the editor
-        if config.has_option('stgit', 'editor'):
-            editor = config.get('stgit', 'editor')
-        elif 'EDITOR' in os.environ:
-            editor = os.environ['EDITOR']
-        else:
-            editor = 'vi'
-        editor += ' %s' % fname
-
-        print 'Invoking the editor: "%s"...' % editor,
-        sys.stdout.flush()
-        print 'done (exit code: %d)' % os.system(editor)
-
-        # read the message back
-        f = file(fname)
-        msg = f.read()
-        f.close()
+    if options.edit_cover:
+        msg = edit_message(msg)
 
     return msg.strip('\n')
 
 
     return msg.strip('\n')
 
@@ -300,7 +377,12 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     if not maintainer:
         maintainer = '%s <%s>' % (p.get_commname(), p.get_commemail())
 
     if not maintainer:
         maintainer = '%s <%s>' % (p.get_commname(), p.get_commemail())
 
-    headers_end = __build_address_headers(options)
+    if options.auto:
+        extra_cc = __get_signers_list(descr)
+    else:
+        extra_cc = []
+
+    tmpl, headers_end = __build_address_headers(tmpl, options, extra_cc)
     headers_end += 'Message-Id: %s\n' % msg_id
     if ref_id:
         headers_end += "In-Reply-To: %s\n" % ref_id
     headers_end += 'Message-Id: %s\n' % msg_id
     if ref_id:
         headers_end += "In-Reply-To: %s\n" % ref_id
@@ -312,6 +394,11 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
     else:
         version_str = ''
 
     else:
         version_str = ''
 
+    if options.prefix:
+        prefix_str = options.prefix + ' '
+    else:
+        prefix_str = ''
+        
     total_nr_str = str(total_nr)
     patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
     if total_nr > 1:
     total_nr_str = str(total_nr)
     patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
     if total_nr > 1:
@@ -330,6 +417,7 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
                                               rev2 = git_id('%s//top' % patch)),
                  'date':         email.Utils.formatdate(localtime = True),
                  'version':      version_str,
                                               rev2 = git_id('%s//top' % patch)),
                  'date':         email.Utils.formatdate(localtime = True),
                  'version':      version_str,
+                 'prefix':       prefix_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str,
                  'patchnr':      patch_nr_str,
                  'totalnr':      total_nr_str,
                  'number':       number_str,
@@ -351,6 +439,9 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
         raise CmdException, 'Only "%(name)s" variables are ' \
               'supported in the patch template'
 
+    if options.edit_patches:
+        msg = edit_message(msg)
+
     return msg.strip('\n')
 
 def func(parser, options, args):
     return msg.strip('\n')
 
 def func(parser, options, args):
@@ -401,10 +492,10 @@ def func(parser, options, args):
         sleep = config.getint('stgit', 'smtpdelay')
 
     # send the cover message (if any)
         sleep = config.getint('stgit', 'smtpdelay')
 
     # send the cover message (if any)
-    if options.cover or options.edit:
+    if options.cover or options.edit_cover:
         # find the template file
         if options.cover:
         # find the template file
         if options.cover:
-            tmpl = file(options.template).read()
+            tmpl = file(options.cover).read()
         else:
             tmpl = templates.get_template('covermail.tmpl')
             if not tmpl:
         else:
             tmpl = templates.get_template('covermail.tmpl')
             if not tmpl: