On Fri, 27 Mar 2009, Christian Couder wrote:
If someone knows some other tools that can easily send a threaded patch
series, I will try to see if I can use them...
I long ago gave up on send-email, as it seemed to cumbersome for what I
wanted, and my perl had got so rusty I really couldn't face trying to
improve it.
So I wrote a replacement in Python (attached), which I have subsequently
used for all patches I've sent. It calls format-patch, passing through
arguments (and you can use -- to let it pass options too).
(the only setting it reads from git config atm is mail-commit.to)
I find it much easier to use than send-email, but as usual YMMV ...
--
Julian
---
Have you seen the latest Japanese camera? Apparently it is so fast it can
photograph an American with his mouth shut!
#!/usr/bin/python
import optparse
import os
import random
import re
import smtplib
import socket
import sys
import tempfile
import time
from cStringIO import StringIO
from email.Generator import Generator
from email.Message import Message
from email.Parser import FeedParser, Parser
from email.Utils import parseaddr, parsedate, formatdate, \
getaddresses, formataddr
my_version = "0.1"
my_name = "git-mail-commits"
this_script = "%s v%s" % (my_name, my_version)
gci_re = re.compile("^(?P<email>(.*) <(.*)>) (\d+ [+-]\d{4})$")
smtp_server = "neutron"
# -----------------------------------------------------------------------------
def get_message_text(message):
text_msg = StringIO()
gen = Generator(text_msg, mangle_from_=False)
gen.flatten(message)
return text_msg.getvalue()
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
def send_message(msg):
sent = (None, 'No destination address given')
(a, fromaddr) = parseaddr(msg.get('From'))
toaddr_list = msg.get_all('To', [])
ccaddr_list = msg.get_all('CC', [])
bccaddr_list = msg.get_all('BCC', [])
all_recips = getaddresses(toaddr_list + ccaddr_list + bccaddr_list)
to_list = [ email for (name, email) in all_recips ]
if len(to_list) > 0:
server = smtplib.SMTP(smtp_server)
try:
errors = server.sendmail(fromaddr, to_list, get_message_text(msg))
sent = (errors, "Failed to send to one or more recipients")
except smtplib.SMTPRecipientsRefused, rr:
sent = (rr.recipients, "Failed to send to all recipients")
server.quit()
return sent
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
def get_msgid(idstring=None, idhost=socket.getfqdn(), email=None):
"""Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
<20020201195627.33539.96671@xxxxxxxxxxxxxxxxxxxxxxxxxx>
Optional idstring if given is a string used to strengthen the
uniqueness of the message id.
Based on email.Utils.make_msgid
"""
timeval = time.time()
utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
pid = os.getpid()
randint = random.randrange(100000)
if email is not None:
ids = ".%s" % (email)
else:
if idstring is None:
idstring = ''
else:
idstring = '.' + idstring
ids = "%s@%s" % (idstring, idhost)
msgid = '<%s.%s.%s%s>' % (utcdate, pid, randint, ids)
return msgid
# -----------------------------------------------------------------------------
def get_patches(args, numbered=True, signoff=True):
patches = []
cur_patch = None
# print args
opts = ['-M']
if numbered:
opts.append("-n")
if signoff:
opts.append("-s")
fp = os.popen("git format-patch --stdout %s %s" % (' '.join(opts),
' '.join(args)))
for line in fp.readlines():
if line[:5] == "From ":
if cur_patch is not None:
patches.append(cur_patch.close())
cur_patch = FeedParser()
cur_patch.feed(line)
fp.close()
if cur_patch is not None:
patches.append(cur_patch.close())
return patches
def format_patches(patches, to, initial_msg_id=None,
addr=None, cc_list=[]):
refs = []
if initial_msg_id is not None:
refs.append(initial_msg_id)
for patch in patches:
if addr is not None:
patch.replace_header("From", addr)
if len(cc_list) > 0:
cc = [x.strip() for x in patch.get("CC", "").split(",")]
if len(cc) == 1 and cc[0] == "":
cc = cc_list
else:
cc.extend(cc_list)
del patch['CC']
print cc
patch.add_header("CC", ",".join(cc))
subject = patch.get("Subject")
(name, email) = parseaddr(patch.get("From"))
sha1 = patch.get_unixfrom()[5:46]
msg_id = get_msgid(email=email)
patch.add_header("To", to)
del patch['Message-Id']
patch.add_header("Message-Id", msg_id)
patch.add_header("X-git-sha1", sha1)
del patch['X-Mailer']
patch.add_header("X-Mailer", this_script)
if len(refs) > 0:
del patch['In-Reply-To']
del patch['References']
patch.add_header("In-Reply-To", refs[-1])
patch.add_header("References", ' '.join(refs))
refs.append(msg_id)
# print "%s - %s" %(sha1[0:7], subject)
# print patch
def get_git_committer_email():
gv = os.popen("git var GIT_COMMITTER_IDENT")
gci = gv.readline()
gv.close()
# print gci
gci_m = gci_re.search(gci)
if gci_m:
return gci_m.group('email')
else:
print "Unable to get/parse GIT_COMMITTER_IDENT"
sys.exit(-1)
def get_intro_msg(to, frm_addr, count, filename=None):
if filename is None:
if 'EDITOR' not in os.environ:
print "$EDITOR not set, please set."
sys.exit(-1)
(fd, fname) = tempfile.mkstemp()
f = os.fdopen(fd)
ret = os.system("%s %s" % (os.environ['EDITOR'], fname))
if not (os.WIFEXITED(ret) and os.WEXITSTATUS(ret) == 0):
print "Failed to edit intro message."
sys.exit(-1)
else:
f = open(filename)
slist = []
blist = []
cur = slist
for line in f.readlines():
if line == "\n":
cur = blist
continue
cur.append(line)
f.close()
if filename is None:
os.remove(fname)
subject = ''.join(slist).replace('\n', ' ')
body = ''.join(blist)
if subject == "":
print "No subject for intro message, aborting."
sys.exit(-1)
msg = Message()
msg.add_header('From', frm_addr)
msg.add_header('To', to)
(name, email) = parseaddr(msg.get("From"))
msg.add_header('Message-Id', get_msgid(email=email))
msg.set_payload(body)
msg.add_header('Subject', "[PATCH 0/%d] %s" % (count, subject))
msg.add_header("X-Mailer", this_script)
return msg
def reply_to(fname):
p = Parser()
f = open(fname)
msg = p.parse(f)
f.close()
cc = [x.strip() for x in msg['CC'].split(',')]
return (msg['From'], msg['Message-ID'], cc)
def main():
description = "send the given commits as patch emails to the specified " \
"address, all non-option arguments are passed to " \
"git-format-patch (\"--\" can be used to indicate the end " \
"of the options for this script)."
parser = optparse.OptionParser(description=description)
parser.disable_interspersed_args()
parser.add_option("", "--to", action="store", default=None,
help="the address to send the mails to")
parser.add_option("", "--cc", action="append", default=[],
help="copy the mails to this address")
parser.add_option("", "--reply-to", action="store", default=None,
help="reply to the given mail (rather than use an intro)")
parser.add_option("-n", "--numbered", action="store_true", default=False,
help="send numbered patches (adds -n to the "
"git-format-patch options)")
parser.add_option("-f", "--from", dest="frm_addr", action="store",
default=None,
help="set FROM as the from address, otherwise use "
"GIT_COMMITTER_IDENT")
parser.add_option("-i", "--intro", dest="intro", action="store_true",
help="start with a 0/n intro message using $EDITOR to "
"write the message (implies -n).")
parser.add_option("-I", "--intro-file", dest="intro", action="store",
help="start with a 0/n intro message read from INTRO "
"(implies -n).")
parser.set_defaults(intro=None)
parser.add_option("-e", "--edit", action="store_true", default=False,
help="edit the patches before sending")
parser.add_option("-S", "--no-signoff", dest="signoff",
action="store_false", default=True,
help="Don't signoff the patches")
(options, args) = parser.parse_args()
if len(args) < 1:
print "You must specify at least one commit to send ..."
sys.exit(-1)
if options.to is None:
gc = os.popen("git config mail-commits.to")
to = gc.read()
gc.close()
if to == "":
print "you must specify the destination using --to"
sys.exit(-1)
else:
options.to = to.strip()
print "Sending to: %s\n" % options.to
if options.frm_addr is not None:
frm_addr = options.frm_addr
else:
frm_addr = get_git_committer_email()
if options.intro is not None:
options.numbered = True
patches = get_patches(args, options.numbered, signoff=options.signoff)
intro_msg = None
intro_msg_id = None
if options.intro is not None:
fname = None
if options.intro is not True:
fname = options.intro
intro_msg = get_intro_msg(options.to, frm_addr, len(patches),
filename=fname)
intro_msg_id = intro_msg.get('Message-Id')
if options.reply_to is not None:
(options.to, intro_msg_id, cc) = reply_to(options.reply_to)
options.cc.extend(cc)
# print intro_msg
format_patches(patches, to=options.to, cc_list=options.cc,
initial_msg_id=intro_msg_id,
addr=frm_addr)
if options.edit:
new_patches = []
for patch in patches:
(fd, fname) = tempfile.mkstemp()
f = os.fdopen(fd, "w+")
f.write(get_message_text(patch))
f.close()
if 'EDITOR' not in os.environ:
print "$EDITOR not set, please set."
sys.exit(-1)
ret = os.system("%s %s" % (os.environ['EDITOR'], fname))
if not (os.WIFEXITED(ret) and os.WEXITSTATUS(ret) == 0):
print "Failed to edit patch (%d)." % ret
sys.exit(-1)
p = FeedParser()
f = open(fname)
for line in f:
p.feed(line)
m = p.close()
if m:
new_patches.append(m)
f.close()
os.remove(fname)
patches = new_patches
if intro_msg:
print intro_msg['Subject']
for patch in patches:
print patch['Subject']
print
print "Press [Enter] to send patches, Ctrl-C to cancel."
try:
raw_input()
except KeyboardInterrupt:
print "Not sending patches."
sys.exit(-1)
print "Sending patches ...\n"
msgs=[intro_msg]
msgs.extend(patches)
for msg in msgs:
if msg is None:
continue
(errors, errmsg) = send_message(msg)
if len(errors) == 0:
print "sent %s" % msg['Subject']
else:
print "error sending %s" % msg['Subject']
for (name, (ecode, emsg)) in errors.items():
print " %s: %s %s" % (name, ecode, emsg)
if __name__ == "__main__":
main()