Allow hand-editing of patches before sending
[stgit] / stgit / commands / mail.py
index 456c4b1..2b28564 100644 (file)
@@ -20,26 +20,28 @@ from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
 from stgit.utils import *
 
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import stack, git, version, templates
 from stgit.config import config
 
 
 help = 'send a patch or series of patches by e-mail'
 from stgit.config import config
 
 
 help = 'send a patch or series of patches by e-mail'
-usage = """%prog [options] [<patch> [<patch2...]]
-
-Send a patch or a range of patches (defaulting to the applied patches)
-by e-mail using the 'smtpserver' configuration option. The From
-address and the e-mail format are generated from the template file
-passed as argument to '--template' (defaulting to .git/patchmail.tmpl
-or /usr/share/stgit/templates/patchmail.tmpl). The To/Cc/Bcc addresses
+usage = """%prog [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]
+
+Send a patch or a range of patches by e-mail using the 'smtpserver'
+configuration option. The From address and the e-mail format are
+generated from the template file passed as argument to '--template'
+(defaulting to '.git/patchmail.tmpl' or
+'~/.stgit/templates/patchmail.tmpl' or or
+'/usr/share/stgit/templates/patchmail.tmpl'). The To/Cc/Bcc addresses
 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
-/usr/share/stgit/templates/covermail.tmpl).
+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').
 
 All the subsequent e-mails appear as replies to the first e-mail sent
 (either the preamble or the first patch). E-mails can be seen as
 
 All the subsequent e-mails appear as replies to the first e-mail sent
 (either the preamble or the first patch). E-mails can be seen as
@@ -63,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'
@@ -79,9 +82,6 @@ variables are supported."""
 options = [make_option('-a', '--all',
                        help = 'e-mail all the applied patches',
                        action = 'store_true'),
 options = [make_option('-a', '--all',
                        help = 'e-mail all the applied patches',
                        action = 'store_true'),
-           make_option('-r', '--range',
-                       metavar = '[PATCH1][:[PATCH2]]',
-                       help = 'e-mail patches between PATCH1 and PATCH2'),
            make_option('--to',
                        help = 'add TO to the To: list',
                        action = 'append'),
            make_option('--to',
                        help = 'add TO to the To: list',
                        action = 'append'),
@@ -91,15 +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('-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',
@@ -183,24 +194,118 @@ 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.
+    """
+    headers  = 'Content-Type: text/plain; charset=utf-8; format=fixed\n'
+    headers += 'Content-Transfer-Encoding: 8bit\n'
+    headers += 'User-Agent: StGIT/%s\n' % version.version
+
+    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
@@ -209,17 +314,23 @@ 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 += "References: %s\n" % options.refid
     headers_end += 'Message-Id: %s\n' % msg_id
     if options.refid:
         headers_end += "In-Reply-To: %s\n" % options.refid
         headers_end += "References: %s\n" % options.refid
+    headers_end += __build_extra_headers()
 
     if options.version:
         version_str = ' %s' % options.version
     else:
         version_str = ''
 
 
     if options.version:
         version_str = ' %s' % options.version
     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:
@@ -231,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}
@@ -244,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')
 
@@ -288,17 +377,28 @@ 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 += "References: %s\n" % ref_id
     headers_end += 'Message-Id: %s\n' % msg_id
     if ref_id:
         headers_end += "In-Reply-To: %s\n" % ref_id
         headers_end += "References: %s\n" % ref_id
+    headers_end += __build_extra_headers()
 
     if options.version:
         version_str = ' %s' % options.version
     else:
         version_str = ''
 
 
     if options.version:
         version_str = ' %s' % options.version
     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:
@@ -311,12 +411,13 @@ def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id, options):
                  'shortdescr':   short_descr,
                  'longdescr':    long_descr,
                  'endofheaders': headers_end,
                  'shortdescr':   short_descr,
                  'longdescr':    long_descr,
                  'endofheaders': headers_end,
-                 'diff':         git.diff(rev1 = git_id('%s/bottom' % patch),
-                                          rev2 = git_id('%s/top' % patch)),
-                 'diffstat':     git.diffstat(rev1 = git_id('%s/bottom'%patch),
-                                              rev2 = git_id('%s/top' % patch)),
+                 'diff':         git.diff(rev1 = git_id('%s//bottom' % patch),
+                                          rev2 = git_id('%s//top' % patch)),
+                 'diffstat':     git.diffstat(rev1 = git_id('%s//bottom'%patch),
+                                              rev2 = git_id('%s//top' % patch)),
                  'date':         email.Utils.formatdate(localtime = True),
                  'version':      version_str,
                  '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,
@@ -338,14 +439,15 @@ 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):
     """Send the patches by e-mail using the patchmail.tmpl file as
     a template
     """
     return msg.strip('\n')
 
 def func(parser, options, args):
     """Send the patches by e-mail using the patchmail.tmpl file as
     a template
     """
-    if not config.has_option('stgit', 'smtpserver'):
-        raise CmdException, 'smtpserver not defined'
     smtpserver = config.get('stgit', 'smtpserver')
 
     smtpuser = None
     smtpserver = config.get('stgit', 'smtpserver')
 
     smtpuser = None
@@ -356,53 +458,11 @@ def func(parser, options, args):
         smtppassword = config.get('stgit', 'smtppassword')
 
     applied = crt_series.get_applied()
         smtppassword = config.get('stgit', 'smtppassword')
 
     applied = crt_series.get_applied()
-    unapplied = crt_series.get_unapplied()
-
-    if len(args) >= 1:
-        for patch in args:
-            if patch in unapplied:
-                raise CmdException, 'Patch "%s" not applied' % patch
-            if not patch in applied:
-                raise CmdException, 'Patch "%s" does not exist' % patch
-        patches = args
-    elif options.all:
-        patches = applied
-    elif options.range:
-        boundaries = options.range.split(':')
-        if len(boundaries) == 1:
-            start = boundaries[0]
-            stop = boundaries[0]
-        elif len(boundaries) == 2:
-            if boundaries[0] == '':
-                start = applied[0]
-            else:
-                start = boundaries[0]
-            if boundaries[1] == '':
-                stop = applied[-1]
-            else:
-                stop = boundaries[1]
-        else:
-            raise CmdException, 'incorrect parameters to "--range"'
-
-        if start in applied:
-            start_idx = applied.index(start)
-        else:
-            if start in unapplied:
-                raise CmdException, 'Patch "%s" not applied' % start
-            else:
-                raise CmdException, 'Patch "%s" does not exist' % start
-        if stop in applied:
-            stop_idx = applied.index(stop) + 1
-        else:
-            if stop in unapplied:
-                raise CmdException, 'Patch "%s" not applied' % stop
-            else:
-                raise CmdException, 'Patch "%s" does not exist' % stop
 
 
-        if start_idx >= stop_idx:
-            raise CmdException, 'Incorrect patch range order'
-
-        patches = applied[start_idx:stop_idx]
+    if options.all:
+        patches = applied
+    elif len(args) >= 1:
+        patches = parse_patches(args, applied)
     else:
         raise CmdException, 'Incorrect options. Unknown patches to send'
 
     else:
         raise CmdException, 'Incorrect options. Unknown patches to send'
 
@@ -421,7 +481,10 @@ def func(parser, options, args):
     if total_nr == 0:
         raise CmdException, 'No patches to send'
 
     if total_nr == 0:
         raise CmdException, 'No patches to send'
 
-    ref_id = options.refid
+    if options.noreply:
+        ref_id = None
+    else:
+        ref_id = options.refid
 
     if options.sleep != None:
         sleep = options.sleep
 
     if options.sleep != None:
         sleep = options.sleep
@@ -429,29 +492,22 @@ 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:
-            tfile_list = [options.cover]
+            tmpl = file(options.cover).read()
         else:
         else:
-            tfile_list = [os.path.join(git.get_base_dir(), 'covermail.tmpl'),
-                          os.path.join(sys.prefix,
-                                       'share/stgit/templates/covermail.tmpl')]
-
-        tmpl = None
-        for tfile in tfile_list:
-            if os.path.isfile(tfile):
-                tmpl = file(tfile).read()
-                break
-        if not tmpl:
-            raise CmdException, 'No cover message template file found'
+            tmpl = templates.get_template('covermail.tmpl')
+            if not tmpl:
+                raise CmdException, 'No cover message template file found'
 
         msg_id = email.Utils.make_msgid('stgit')
         msg = __build_cover(tmpl, total_nr, msg_id, options)
         from_addr, to_addr_list = __parse_addresses(msg)
 
         # subsequent e-mails are seen as replies to the first one
 
         msg_id = email.Utils.make_msgid('stgit')
         msg = __build_cover(tmpl, total_nr, msg_id, options)
         from_addr, to_addr_list = __parse_addresses(msg)
 
         # subsequent e-mails are seen as replies to the first one
-        ref_id = msg_id
+        if not options.noreply:
+            ref_id = msg_id
 
         if options.mbox:
             __write_mbox(from_addr, msg)
 
         if options.mbox:
             __write_mbox(from_addr, msg)
@@ -464,18 +520,11 @@ def func(parser, options, args):
 
     # send the patches
     if options.template:
 
     # send the patches
     if options.template:
-        tfile_list = [options.template]
+        tmpl = file(options.template).read()
     else:
     else:
-        tfile_list = [os.path.join(git.get_base_dir(), 'patchmail.tmpl'),
-                      os.path.join(sys.prefix,
-                                   'share/stgit/templates/patchmail.tmpl')]
-    tmpl = None
-    for tfile in tfile_list:
-        if os.path.isfile(tfile):
-            tmpl = file(tfile).read()
-            break
-    if not tmpl:
-        raise CmdException, 'No e-mail template file found'
+        tmpl = templates.get_template('patchmail.tmpl')
+        if not tmpl:
+            raise CmdException, 'No e-mail template file found'
 
     for (p, patch_nr) in zip(patches, range(1, len(patches) + 1)):
         msg_id = email.Utils.make_msgid('stgit')
 
     for (p, patch_nr) in zip(patches, range(1, len(patches) + 1)):
         msg_id = email.Utils.make_msgid('stgit')
@@ -484,7 +533,7 @@ def func(parser, options, args):
         from_addr, to_addr_list = __parse_addresses(msg)
 
         # subsequent e-mails are seen as replies to the first one
         from_addr, to_addr_list = __parse_addresses(msg)
 
         # subsequent e-mails are seen as replies to the first one
-        if not ref_id:
+        if not options.noreply and not ref_id:
             ref_id = msg_id
 
         if options.mbox:
             ref_id = msg_id
 
         if options.mbox: