contrib/patchbomb
changeset 896 01215ad04283
parent 878 781266a78fe1
child 997 458b84a96e1c
equal deleted inserted replaced
867:0cd2ee61b10a 896:01215ad04283
       
     1 #!/usr/bin/python
       
     2 #
       
     3 # Interactive script for sending a collection of Mercurial changesets
       
     4 # as a series of patch emails.
       
     5 #
       
     6 # The series is started off with a "[PATCH 0 of N]" introduction,
       
     7 # which describes the series as a whole.
       
     8 #
       
     9 # Each patch email has a Subject line of "[PATCH M of N] ...", using
       
    10 # the first line of the changeset description as the subject text.
       
    11 # The message contains two or three body parts:
       
    12 #
       
    13 #   The remainder of the changeset description.
       
    14 #
       
    15 #   [Optional] If the diffstat program is installed, the result of
       
    16 #   running diffstat on the patch.
       
    17 #
       
    18 #   The patch itself, as generated by "hg export".
       
    19 #
       
    20 # Each message refers to all of its predecessors using the In-Reply-To
       
    21 # and References headers, so they will show up as a sequence in
       
    22 # threaded mail and news readers, and in mail archives.
       
    23 #
       
    24 # For each changeset, you will be prompted with a diffstat summary and
       
    25 # the changeset summary, so you can be sure you are sending the right
       
    26 # changes.
       
    27 #
       
    28 # It is best to run this script with the "-n" (test only) flag before
       
    29 # firing it up "for real", in which case it will use your pager to
       
    30 # display each of the messages that it would send.
       
    31 #
       
    32 # To configure a default mail host, add a section like this to your
       
    33 # hgrc file:
       
    34 #
       
    35 # [smtp]
       
    36 # host = my_mail_host
       
    37 # port = 1025
       
    38 #
       
    39 # To configure other defaults, add a section like this to your hgrc
       
    40 # file:
       
    41 #
       
    42 # [patchbomb]
       
    43 # from = My Name <my@email>
       
    44 # to = recipient1, recipient2, ...
       
    45 # cc = cc1, cc2, ...
       
    46 
       
    47 from email.MIMEMultipart import MIMEMultipart
       
    48 from email.MIMEText import MIMEText
       
    49 from mercurial import commands
       
    50 from mercurial import fancyopts
       
    51 from mercurial import hg
       
    52 from mercurial import ui
       
    53 import os
       
    54 import popen2
       
    55 import readline
       
    56 import smtplib
       
    57 import socket
       
    58 import sys
       
    59 import tempfile
       
    60 import time
       
    61 
       
    62 def diffstat(patch):
       
    63     fd, name = tempfile.mkstemp()
       
    64     try:
       
    65         p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
       
    66         try:
       
    67             for line in patch: print >> p.tochild, line
       
    68             p.tochild.close()
       
    69             if p.wait(): return
       
    70             fp = os.fdopen(fd, 'r')
       
    71             stat = []
       
    72             for line in fp: stat.append(line.lstrip())
       
    73             last = stat.pop()
       
    74             stat.insert(0, last)
       
    75             stat = ''.join(stat)
       
    76             if stat.startswith('0 files'): raise ValueError
       
    77             return stat
       
    78         except: raise
       
    79     finally:
       
    80         try: os.unlink(name)
       
    81         except: pass
       
    82 
       
    83 def patchbomb(ui, repo, *revs, **opts):
       
    84     def prompt(prompt, default = None, rest = ': ', empty_ok = False):
       
    85         if default: prompt += ' [%s]' % default
       
    86         prompt += rest
       
    87         while True:
       
    88             r = raw_input(prompt)
       
    89             if r: return r
       
    90             if default is not None: return default
       
    91             if empty_ok: return r
       
    92             ui.warn('Please enter a valid value.\n')
       
    93 
       
    94     def confirm(s):
       
    95         if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
       
    96             raise ValueError
       
    97 
       
    98     def cdiffstat(summary, patch):
       
    99         s = diffstat(patch)
       
   100         if s:
       
   101             if summary:
       
   102                 ui.write(summary, '\n')
       
   103                 ui.write(s, '\n')
       
   104             confirm('Does the diffstat above look okay')
       
   105         return s
       
   106 
       
   107     def makepatch(patch, idx, total):
       
   108         desc = []
       
   109         node = None
       
   110         for line in patch:
       
   111             if line.startswith('#'):
       
   112                 if line.startswith('# Node ID'): node = line.split()[-1]
       
   113                 continue
       
   114             if line.startswith('diff -r'): break
       
   115             desc.append(line)
       
   116         if not node: raise ValueError
       
   117         body = ('\n'.join(desc[1:]).strip() or
       
   118                 'Patch subject is complete summary.')
       
   119         body += '\n\n\n'
       
   120         if opts['diffstat']:
       
   121             body += cdiffstat('\n'.join(desc), patch) + '\n\n'
       
   122         body += '\n'.join(patch)
       
   123         msg = MIMEText(body)
       
   124         subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
       
   125         if subj.endswith('.'): subj = subj[:-1]
       
   126         msg['Subject'] = subj
       
   127         msg['X-Mercurial-Node'] = node
       
   128         return msg
       
   129 
       
   130     start_time = int(time.time())
       
   131 
       
   132     def genmsgid(id):
       
   133         return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
       
   134 
       
   135     patches = []
       
   136 
       
   137     class exportee:
       
   138         def __init__(self, container):
       
   139             self.lines = []
       
   140             self.container = container
       
   141             self.name = 'email'
       
   142 
       
   143         def write(self, data):
       
   144             self.lines.append(data)
       
   145 
       
   146         def close(self):
       
   147             self.container.append(''.join(self.lines).split('\n'))
       
   148             self.lines = []
       
   149 
       
   150     commands.export(ui, repo, *args, **{'output': exportee(patches)})
       
   151 
       
   152     jumbo = []
       
   153     msgs = []
       
   154 
       
   155     ui.write('This patch series consists of %d patches.\n\n' % len(patches))
       
   156 
       
   157     for p, i in zip(patches, range(len(patches))):
       
   158         jumbo.extend(p)
       
   159         msgs.append(makepatch(p, i + 1, len(patches)))
       
   160 
       
   161     ui.write('\nWrite the introductory message for the patch series.\n\n')
       
   162 
       
   163     sender = (opts['from'] or ui.config('patchbomb', 'from') or
       
   164               prompt('From', ui.username()))
       
   165 
       
   166     msg = MIMEMultipart()
       
   167     msg['Subject'] = '[PATCH 0 of %d] %s' % (
       
   168         len(patches),
       
   169         opts['subject'] or
       
   170         prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
       
   171     to = (opts['to'] or ui.config('patchbomb', 'to') or
       
   172           [s.strip() for s in prompt('To').split(',')])
       
   173     cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
       
   174           [s.strip() for s in prompt('Cc', default = '').split(',')])
       
   175 
       
   176     ui.write('Finish with ^D or a dot on a line by itself.\n\n')
       
   177 
       
   178     body = []
       
   179 
       
   180     while True:
       
   181         try: l = raw_input()
       
   182         except EOFError: break
       
   183         if l == '.': break
       
   184         body.append(l)
       
   185 
       
   186     msg.attach(MIMEText('\n'.join(body) + '\n'))
       
   187 
       
   188     ui.write('\n')
       
   189 
       
   190     d = cdiffstat('Final summary:\n', jumbo)
       
   191     if d: msg.attach(MIMEText(d))
       
   192 
       
   193     msgs.insert(0, msg)
       
   194 
       
   195     if not opts['test']:
       
   196         s = smtplib.SMTP()
       
   197         s.connect(host = ui.config('smtp', 'host', 'mail'),
       
   198                   port = int(ui.config('smtp', 'port', 25)))
       
   199 
       
   200     parent = None
       
   201     tz = time.strftime('%z')
       
   202     for m in msgs:
       
   203         try:
       
   204             m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
       
   205         except TypeError:
       
   206             m['Message-Id'] = genmsgid('patchbomb')
       
   207         if parent:
       
   208             m['In-Reply-To'] = parent
       
   209         else:
       
   210             parent = m['Message-Id']
       
   211         m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
       
   212         start_time += 1
       
   213         m['From'] = sender
       
   214         m['To'] = ', '.join(to)
       
   215         if cc: m['Cc'] = ', '.join(cc)
       
   216         ui.status('Sending ', m['Subject'], ' ...\n')
       
   217         if opts['test']:
       
   218             fp = os.popen(os.getenv('PAGER', 'more'), 'w')
       
   219             fp.write(m.as_string(0))
       
   220             fp.write('\n')
       
   221             fp.close()
       
   222         else:
       
   223             s.sendmail(sender, to + cc, m.as_string(0))
       
   224     if not opts['test']:
       
   225         s.close()
       
   226 
       
   227 if __name__ == '__main__':
       
   228     optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
       
   229                ('d', 'diffstat', None, 'add diffstat output to messages'),
       
   230                ('f', 'from', '', 'email address of sender'),
       
   231                ('n', 'test', None, 'print messages that would be sent'),
       
   232                ('s', 'subject', '', 'subject of introductory message'),
       
   233                ('t', 'to', [], 'email addresses of recipients')]
       
   234     options = {}
       
   235     try:
       
   236         args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
       
   237                                    options)
       
   238     except fancyopts.getopt.GetoptError, inst:
       
   239         u = ui.ui()
       
   240         u.warn('error: %s' % inst)
       
   241         sys.exit(1)
       
   242 
       
   243     u = ui.ui(options["verbose"], options["debug"], options["quiet"],
       
   244               not options["noninteractive"])
       
   245     repo = hg.repository(ui = u)
       
   246 
       
   247     patchbomb(u, repo, *args, **options)