From b4bddc068e5440e8b747329df4db841a14483d85 Mon Sep 17 00:00:00 2001 From: Catalin Marinas Date: Tue, 19 Jul 2005 17:30:45 +0100 Subject: [PATCH] Implement the 'mail' command 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 --- examples/firstmail.tmpl | 11 +++ examples/patchmail.tmpl | 13 +++ stgit/commands/diff.py | 2 +- stgit/commands/mail.py | 251 ++++++++++++++++++++++++++++++++++++++++++++++++ stgit/git.py | 11 ++- stgit/main.py | 2 + stgitrc | 2 + 7 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 examples/firstmail.tmpl create mode 100644 examples/patchmail.tmpl create mode 100644 stgit/commands/mail.py diff --git a/examples/firstmail.tmpl b/examples/firstmail.tmpl new file mode 100644 index 0000000..fa5ff9a --- /dev/null +++ b/examples/firstmail.tmpl @@ -0,0 +1,11 @@ +From: Your Name +To: Some Address +Cc: other.address@otherlist.com, other.person@othercompany.com +Bcc: Your Name +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 index 0000000..83cdf5c --- /dev/null +++ b/examples/patchmail.tmpl @@ -0,0 +1,13 @@ +From: Your Name +To: Some Address +Cc: other.address@otherlist.com, other.person@othercompany.com +Bcc: Your Name +Subject: [PATCH %(patchnr)s/%(totalnr)s] %(shortdescr)s +Date: %(date)s +%(endofheaders)s +%(longdescr)s +--- + +%(diffstat)s + +%(diff)s diff --git a/stgit/commands/diff.py b/stgit/commands/diff.py index 6036a18..b6ed82e 100644 --- a/stgit/commands/diff.py +++ b/stgit/commands/diff.py @@ -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 index 0000000..8ecc8a5 --- /dev/null +++ b/stgit/commands/mail.py @@ -0,0 +1,251 @@ +__copyright__ = """ +Copyright (C) 2005, Catalin Marinas + +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' diff --git a/stgit/git.py b/stgit/git.py index 5d754b6..1f5a129 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -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 diff --git a/stgit/main.py b/stgit/main.py index 06611a9..72bb30e 100644 --- a/stgit/main.py +++ b/stgit/main.py @@ -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 --- 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. -- 2.11.0