contrib/patchbomb
changeset 875 d3f836bf6cc1
child 876 14cfaaec2e8e
equal deleted inserted replaced
871:c2e77581bc84 875:d3f836bf6cc1
       
     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 #   If the diffstat program is installed, the result of running
       
    16 #   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 display each of the
       
    30 # 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 from email.MIMEMultipart import MIMEMultipart
       
    40 from email.MIMEText import MIMEText
       
    41 from mercurial import commands
       
    42 from mercurial import fancyopts
       
    43 from mercurial import hg
       
    44 from mercurial import ui
       
    45 import os
       
    46 import popen2
       
    47 import readline
       
    48 import smtplib
       
    49 import socket
       
    50 import sys
       
    51 import tempfile
       
    52 import time
       
    53 
       
    54 def diffstat(patch):
       
    55     fd, name = tempfile.mkstemp()
       
    56     try:
       
    57         p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
       
    58         try:
       
    59             for line in patch: print >> p.tochild, line
       
    60             p.tochild.close()
       
    61             if p.wait(): return
       
    62             fp = os.fdopen(fd, 'r')
       
    63             stat = []
       
    64             for line in fp: stat.append(line.lstrip())
       
    65             last = stat.pop()
       
    66             stat.insert(0, last)
       
    67             stat = ''.join(stat)
       
    68             if stat.startswith('0 files'): raise ValueError
       
    69             return stat
       
    70         except: raise
       
    71     finally:
       
    72         try: os.unlink(name)
       
    73         except: pass
       
    74 
       
    75 def patchbomb(ui, repo, *revs, **opts):
       
    76     def prompt(prompt, default = None, rest = ': ', empty_ok = False):
       
    77         try:
       
    78             if default: prompt += ' [%s]' % default
       
    79             prompt += rest
       
    80             r = raw_input(prompt)
       
    81             if not r and not empty_ok: raise EOFError
       
    82             return r
       
    83         except EOFError:
       
    84             if default is None: raise
       
    85             return default
       
    86 
       
    87     def confirm(s):
       
    88         if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
       
    89             raise ValueError
       
    90 
       
    91     def cdiffstat(summary, patch):
       
    92         s = diffstat(patch)
       
    93         if s:
       
    94             if summary:
       
    95                 ui.write(summary, '\n')
       
    96                 ui.write(s, '\n')
       
    97             confirm('Does the diffstat above look okay')
       
    98         return s
       
    99 
       
   100     def make_patch(patch, idx, total):
       
   101         desc = []
       
   102         node = None
       
   103         for line in patch:
       
   104             if line.startswith('#'):
       
   105                 if line.startswith('# Node ID'): node = line.split()[-1]
       
   106                 continue
       
   107             if line.startswith('diff -r'): break
       
   108             desc.append(line)
       
   109         if not node: raise ValueError
       
   110         msg = MIMEMultipart()
       
   111         msg['X-Mercurial-Node'] = node
       
   112         subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
       
   113         if subj.endswith('.'): subj = subj[:-1]
       
   114         msg['Subject'] = subj
       
   115         body = '\n'.join(desc[1:]).strip() + '\n'
       
   116         summary = subj
       
   117         if body != '\n':
       
   118             msg.attach(MIMEText(body))
       
   119             summary += '\n\n' + body
       
   120         else:
       
   121             summary += '\n'
       
   122         d = cdiffstat(summary, patch)
       
   123         if d: msg.attach(MIMEText(d))
       
   124         p = MIMEText('\n'.join(patch), 'x-patch')
       
   125         p['Content-Disposition'] = commands.make_filename(repo, None,
       
   126                                                           'inline; filename=%b-%n.patch',
       
   127                                                           seqno = idx)
       
   128         msg.attach(p)
       
   129         return msg
       
   130 
       
   131     start_time = int(time.time())
       
   132 
       
   133     def make_msgid(id):
       
   134         return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
       
   135 
       
   136     patches = []
       
   137 
       
   138     class exportee:
       
   139         def __init__(self, container):
       
   140             self.lines = []
       
   141             self.container = container
       
   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(make_patch(p, i + 1, len(patches)))
       
   160 
       
   161     ui.write('\nWrite the introductory message for the patch series.\n\n')
       
   162 
       
   163     sender = opts['sender'] or prompt('From', ui.username())
       
   164 
       
   165     msg = MIMEMultipart()
       
   166     msg['Subject'] = '[PATCH 0 of %d] %s' % (
       
   167         len(patches),
       
   168         prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
       
   169     to = opts['to'] or [s.strip() for s in prompt('To').split(',')]
       
   170     cc = opts['cc'] or [s.strip() for s in prompt('Cc', default = '').split(',')]
       
   171 
       
   172     ui.write('Finish with ^D or a dot on a line by itself.\n\n')
       
   173 
       
   174     body = []
       
   175 
       
   176     while True:
       
   177         try: l = raw_input()
       
   178         except EOFError: break
       
   179         if l == '.': break
       
   180         body.append(l)
       
   181 
       
   182     msg.attach(MIMEText('\n'.join(body) + '\n'))
       
   183 
       
   184     ui.write('\n')
       
   185 
       
   186     d = cdiffstat('Final summary:\n', jumbo)
       
   187     if d: msg.attach(MIMEText(d))
       
   188 
       
   189     msgs.insert(0, msg)
       
   190 
       
   191     s = smtplib.SMTP()
       
   192     s.connect(host = ui.config('smtp', 'host', 'mail'),
       
   193               port = int(ui.config('smtp', 'port', 25)))
       
   194 
       
   195     refs = []
       
   196     parent = None
       
   197     tz = time.strftime('%z')
       
   198     for m in msgs:
       
   199         try:
       
   200             m['Message-Id'] = make_msgid(m['X-Mercurial-Node'])
       
   201         except TypeError:
       
   202             m['Message-Id'] = make_msgid('patchbomb')
       
   203         if parent:
       
   204             m['In-Reply-To'] = parent
       
   205         parent = m['Message-Id']
       
   206         if len(refs) > 1:
       
   207             m['References'] = ' '.join(refs[:-1])
       
   208         refs.append(parent)
       
   209         m['Date'] = time.strftime('%a, %m %b %Y %T ', time.localtime(start_time)) + tz
       
   210         start_time += 1
       
   211         m['From'] = sender
       
   212         m['To'] = ', '.join(to)
       
   213         if cc: m['Cc'] = ', '.join(cc)
       
   214         ui.status('Sending ', m['Subject'], ' ...\n')
       
   215         if opts['test']:
       
   216             fp = os.popen(os.getenv('PAGER', 'more'), 'w')
       
   217             fp.write(m.as_string(0))
       
   218             fp.write('\n')
       
   219             fp.close()
       
   220         else:
       
   221             s.sendmail(sender, to + cc, m.as_string(0))
       
   222     s.close()
       
   223 
       
   224 if __name__ == '__main__':
       
   225     optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
       
   226                ('n', 'test', None, 'print messages that would be sent'),
       
   227                ('s', 'sender', '', 'email address of sender'),
       
   228                ('t', 'to', [], 'email addresses of recipients')]
       
   229     options = {}
       
   230     try:
       
   231         args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
       
   232                                    options)
       
   233     except fancyopts.getopt.GetoptError, inst:
       
   234         u = ui.ui()
       
   235         u.warn('error: %s' % inst)
       
   236         sys.exit(1)
       
   237 
       
   238     u = ui.ui(options["verbose"], options["debug"], options["quiet"],
       
   239               not options["noninteractive"])
       
   240     repo = hg.repository(ui = u)
       
   241 
       
   242     patchbomb(u, repo, *args, **options)