sending patch sets (was: Re: [PATCH 01/10] refs: add "for_each_bisect_ref" function)

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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()

[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux