From: Julian Phillips Date: Fri, 27 Mar 2009 01:32:10 GMT Subject: sending patch sets (was: Re: [PATCH 01/10] refs: add "for_each_bisect_ref" function) Message-ID: In-Reply-To: <200903270141.57426.chriscool@tuxfamily.org> 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(.*) <(.*)>) (\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@nightshade.la.mastaler.com> 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()