Implement the 'mail' command
authorCatalin Marinas <catalin.marinas@gmail.com>
Tue, 19 Jul 2005 16:30:45 +0000 (17:30 +0100)
committerCatalin Marinas <catalin.marinas@gmail.com>
Tue, 19 Jul 2005 16:30:45 +0000 (17:30 +0100)
This command is used to send a series of patches via SMTP. The
.git/patchmail.tmpl file is used as the default template. A first message
template can be used with the --first option. Example templates are in
the examples/ directory.

Signed-off-by: Catalin Marinas <catalin.marinas@gmail.com>
examples/firstmail.tmpl [new file with mode: 0644]
examples/patchmail.tmpl [new file with mode: 0644]
stgit/commands/diff.py
stgit/commands/mail.py [new file with mode: 0644]
stgit/git.py
stgit/main.py
stgitrc

diff --git a/examples/firstmail.tmpl b/examples/firstmail.tmpl
new file mode 100644 (file)
index 0000000..fa5ff9a
--- /dev/null
@@ -0,0 +1,11 @@
+From: Your Name <your.name@yourcompany.com>
+To: Some Address <some.address@somelist.com>
+Cc: other.address@otherlist.com, other.person@othercompany.com
+Bcc: Your Name <your.name@yourcompany.com>
+Subject: [PATCH 0/%(totalnr)s] Series short description
+Date: %(date)s
+%(endofheaders)s
+The following series implements...
+
+--
+Your signature
diff --git a/examples/patchmail.tmpl b/examples/patchmail.tmpl
new file mode 100644 (file)
index 0000000..83cdf5c
--- /dev/null
@@ -0,0 +1,13 @@
+From: Your Name <your.name@yourcompany.com>
+To: Some Address <some.address@somelist.com>
+Cc: other.address@otherlist.com, other.person@othercompany.com
+Bcc: Your Name <your.name@yourcompany.com>
+Subject: [PATCH %(patchnr)s/%(totalnr)s] %(shortdescr)s
+Date: %(date)s
+%(endofheaders)s
+%(longdescr)s
+---
+
+%(diffstat)s
+
+%(diff)s
index 6036a18..b6ed82e 100644 (file)
@@ -64,4 +64,4 @@ def func(parser, options, args):
     if options.stat:
         print git.diffstat(args, git_id(rev1), git_id(rev2))
     else:
-        git.diff(args, git_id(rev1), git_id(rev2))
+        git.diff(args, git_id(rev1), git_id(rev2), sys.stdout)
diff --git a/stgit/commands/mail.py b/stgit/commands/mail.py
new file mode 100644 (file)
index 0000000..8ecc8a5
--- /dev/null
@@ -0,0 +1,251 @@
+__copyright__ = """
+Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import sys, os, re, time, smtplib, email.Utils
+from optparse import OptionParser, make_option
+from time import gmtime, strftime
+
+from stgit.commands.common import *
+from stgit.utils import *
+from stgit import stack, git
+from stgit.config import config
+
+
+help = 'send a patch or series of patches by e-mail'
+usage = """%prog [options]"""
+
+options = [make_option('-t', '--template', metavar = 'FILE',
+                       help = 'use FILE as the message template'),
+           make_option('-r', '--range',
+                       metavar = '[PATCH1][:[PATCH2]]',
+                       help = 'e-mail patches between PATCH1 and PATCH2'),
+           make_option('-f', '--first', metavar = 'FILE',
+                       help = 'send FILE as the first message'),
+           make_option('-s', '--sleep', type = 'int', metavar = 'SECONDS',
+                       help = 'sleep for SECONDS between e-mails sending'),
+           make_option('--refid',
+                       help = 'Use REFID as the reference id')]
+
+
+def __parse_addresses(string):
+    """Return a two elements tuple: (from, [to])
+    """
+    def __addr_list(string):
+        return re.split('.*?([\w\.]+@[\w\.]+)', string)[1:-1:2]
+
+    from_addr_list = []
+    to_addr_list = []
+    for line in string.split('\n'):
+        if re.match('from:\s+', line, re.I):
+            from_addr_list += __addr_list(line)
+        elif re.match('(to|cc|bcc):\s+', line, re.I):
+            to_addr_list += __addr_list(line)
+
+    if len(from_addr_list) != 1:
+        raise CmdException, 'No "From" address'
+    if len(to_addr_list) == 0:
+        raise CmdException, 'No "To/Cc/Bcc" addresses'
+
+    return (from_addr_list[0], to_addr_list)
+
+def __send_message(smtpserver, from_addr, to_addr_list, msg, sleep):
+    """Send the message using the given SMTP server
+    """
+    try:
+        s = smtplib.SMTP(smtpserver)
+    except Exception, err:
+        raise CmdException, str(err)
+
+    s.set_debuglevel(0)
+    try:
+        s.sendmail(from_addr, to_addr_list, msg)
+        # give recipients a chance of receiving patches in the correct order
+        time.sleep(sleep)
+    except Exception, err:
+        raise CmdException, str(err)
+
+    s.quit()
+
+def __build_first(tmpl, total_nr, msg_id):
+    """Build the first message (series description) to be sent via SMTP
+    """
+    headers_end = 'Message-Id: %s\n' % (msg_id)
+    total_nr_str = str(total_nr)
+
+    tmpl_dict = {'endofheaders': headers_end,
+                 'date':         email.Utils.formatdate(localtime = True),
+                 'totalnr':      total_nr_str}
+
+    try:
+        msg = tmpl % tmpl_dict
+    except KeyError, err:
+        raise CmdException, 'Unknown patch template variable: %s' \
+              % err
+    except TypeError:
+        raise CmdException, 'Only "%(name)s" variables are ' \
+              'supported in the patch template'
+
+    return msg
+
+
+def __build_message(tmpl, patch, patch_nr, total_nr, msg_id, ref_id = None):
+    """Build the message to be sent via SMTP
+    """
+    p = crt_series.get_patch(patch)
+
+    descr = p.get_description().strip()
+    descr_lines = descr.split('\n')
+
+    short_descr = descr_lines[0].rstrip()
+    long_descr = reduce(lambda x, y: x + '\n' + y,
+                        descr_lines[1:], '').lstrip()
+
+    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)
+
+    total_nr_str = str(total_nr)
+    patch_nr_str = str(patch_nr).zfill(len(total_nr_str))
+
+    tmpl_dict = {'patch':        patch,
+                 '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)),
+                 'date':         email.Utils.formatdate(localtime = True),
+                 'patchnr':      patch_nr_str,
+                 'totalnr':      total_nr_str,
+                 'authname':     p.get_authname(),
+                 'authemail':    p.get_authemail(),
+                 'authdate':     p.get_authdate(),
+                 'commname':     p.get_commname(),
+                 'commemail':    p.get_commemail()}
+    for key in tmpl_dict:
+        if not tmpl_dict[key]:
+            tmpl_dict[key] = ''
+
+    try:
+        msg = tmpl % tmpl_dict
+    except KeyError, err:
+        raise CmdException, 'Unknown patch template variable: %s' \
+              % err
+    except TypeError:
+        raise CmdException, 'Only "%(name)s" variables are ' \
+              'supported in the patch template'
+
+    return msg
+
+
+def func(parser, options, args):
+    """Send the patches by e-mail using the patchmail.tmpl file as
+    a template
+    """
+    if len(args) != 0:
+        parser.error('incorrect number of arguments')
+
+    if not config.has_option('stgit', 'smtpserver'):
+        raise CmdException, 'smtpserver not defined'
+    smtpserver = config.get('stgit', 'smtpserver')
+
+    applied = crt_series.get_applied()
+
+    if 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:
+            raise CmdException, 'Patch "%s" not applied' % start
+        if stop in applied:
+            stop_idx = applied.index(stop) + 1
+        else:
+            raise CmdException, 'Patch "%s" not applied' % stop
+
+        if start_idx >= stop_idx:
+            raise CmdException, 'Incorrect patch range order'
+    else:
+        start_idx = 0
+        stop_idx = len(applied)
+
+    patches = applied[start_idx:stop_idx]
+    total_nr = len(patches)
+
+    ref_id = options.refid
+
+    if options.sleep != None:
+        sleep = options.sleep
+    else:
+        sleep = 2
+
+    # send the first message (if any)
+    if options.first:
+        tmpl = file(options.first).read()
+        from_addr, to_addr_list = __parse_addresses(tmpl)
+
+        msg_id = email.Utils.make_msgid('stgit')
+        msg = __build_first(tmpl, total_nr, msg_id)
+
+        # subsequent e-mails are seen as replies to the first one
+        ref_id = msg_id
+
+        print 'Sending file "%s"...' % options.first,
+        sys.stdout.flush()
+
+        __send_message(smtpserver, from_addr, to_addr_list, msg, sleep)
+
+        print 'done'
+
+    # send the patches
+    if options.template:
+        tfile = options.template
+    else:
+        tfile = os.path.join(git.base_dir, 'patchmail.tmpl')
+    tmpl = file(tfile).read()
+
+    from_addr, to_addr_list = __parse_addresses(tmpl)
+
+    for (p, patch_nr) in zip(patches, range(1, len(patches) + 1)):
+        msg_id = email.Utils.make_msgid('stgit')
+        msg = __build_message(tmpl, p, patch_nr, total_nr, msg_id, ref_id)
+        # subsequent e-mails are seen as replies to the first one
+        if not ref_id:
+            ref_id = msg_id
+
+        print 'Sending patch "%s"...' % p,
+        sys.stdout.flush()
+
+        __send_message(smtpserver, from_addr, to_addr_list, msg, sleep)
+
+        print 'done'
index 5d754b6..1f5a129 100644 (file)
@@ -371,15 +371,20 @@ def status(files = [], modified = False, new = False, deleted = False,
         else:
             print '%s' % fs[1]
 
-def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = sys.stdout):
+def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
     """Show the diff between rev1 and rev2
     """
     os.system('git-update-cache --refresh > /dev/null')
 
     if rev2:
-        out_fd.write(_output(['git-diff-tree', '-p', rev1, rev2]+files))
+        diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
     else:
-        out_fd.write(_output(['git-diff-cache', '-p', rev1]+files))
+        diff_str = _output(['git-diff-cache', '-p', rev1] + files)
+
+    if out_fd:
+        out_fd.write(diff_str)
+    else:
+        return diff_str
 
 def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
     """Return the diffstat between rev1 and rev2
index 06611a9..72bb30e 100644 (file)
@@ -36,6 +36,7 @@ import stgit.commands.clean
 import stgit.commands.export
 import stgit.commands.files
 import stgit.commands.init
+import stgit.commands.mail
 import stgit.commands.new
 import stgit.commands.pop
 import stgit.commands.push
@@ -60,6 +61,7 @@ commands = {
     'export':   stgit.commands.export,
     'files':    stgit.commands.files,
     'init':     stgit.commands.init,
+    'mail':     stgit.commands.mail,
     'new':      stgit.commands.new,
     'pop':      stgit.commands.pop,
     'push':     stgit.commands.push,
diff --git a/stgitrc b/stgitrc
index 7698f31..429822c 100644 (file)
--- a/stgitrc
+++ b/stgitrc
@@ -9,6 +9,8 @@
 # 'refresh' will automatically mark the conflicts as resolved
 autoresolved: no
 
+smtpserver: localhost:25
+
 
 [gitmergeonefile]
 # Different three-way merge tools below. Uncomment the preferred one.