contrib/patchbomb
changeset 1669 91d40fc959f0
parent 1667 daff3ef0de8d
child 1670 fe19c54ee403
equal deleted inserted replaced
1667:daff3ef0de8d 1669:91d40fc959f0
     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 # tls = yes # or omit if not needed
       
    39 # username = user     # if SMTP authentication required
       
    40 # password = password # if SMTP authentication required - PLAINTEXT
       
    41 #
       
    42 # To configure other defaults, add a section like this to your hgrc
       
    43 # file:
       
    44 #
       
    45 # [patchbomb]
       
    46 # from = My Name <my@email>
       
    47 # to = recipient1, recipient2, ...
       
    48 # cc = cc1, cc2, ...
       
    49 
       
    50 from email.MIMEMultipart import MIMEMultipart
       
    51 from email.MIMEText import MIMEText
       
    52 from mercurial import commands
       
    53 from mercurial import fancyopts
       
    54 from mercurial import hg
       
    55 from mercurial import ui
       
    56 import os
       
    57 import popen2
       
    58 import smtplib
       
    59 import socket
       
    60 import sys
       
    61 import tempfile
       
    62 import time
       
    63 
       
    64 try:
       
    65     # readline gives raw_input editing capabilities, but is not
       
    66     # present on windows
       
    67     import readline
       
    68 except ImportError: pass
       
    69 
       
    70 def diffstat(patch):
       
    71     fd, name = tempfile.mkstemp()
       
    72     try:
       
    73         p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
       
    74         try:
       
    75             for line in patch: print >> p.tochild, line
       
    76             p.tochild.close()
       
    77             if p.wait(): return
       
    78             fp = os.fdopen(fd, 'r')
       
    79             stat = []
       
    80             for line in fp: stat.append(line.lstrip())
       
    81             last = stat.pop()
       
    82             stat.insert(0, last)
       
    83             stat = ''.join(stat)
       
    84             if stat.startswith('0 files'): raise ValueError
       
    85             return stat
       
    86         except: raise
       
    87     finally:
       
    88         try: os.unlink(name)
       
    89         except: pass
       
    90 
       
    91 def patchbomb(ui, repo, *revs, **opts):
       
    92     def prompt(prompt, default = None, rest = ': ', empty_ok = False):
       
    93         if default: prompt += ' [%s]' % default
       
    94         prompt += rest
       
    95         while True:
       
    96             r = raw_input(prompt)
       
    97             if r: return r
       
    98             if default is not None: return default
       
    99             if empty_ok: return r
       
   100             ui.warn('Please enter a valid value.\n')
       
   101 
       
   102     def confirm(s):
       
   103         if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
       
   104             raise ValueError
       
   105 
       
   106     def cdiffstat(summary, patch):
       
   107         s = diffstat(patch)
       
   108         if s:
       
   109             if summary:
       
   110                 ui.write(summary, '\n')
       
   111                 ui.write(s, '\n')
       
   112             confirm('Does the diffstat above look okay')
       
   113         return s
       
   114 
       
   115     def makepatch(patch, idx, total):
       
   116         desc = []
       
   117         node = None
       
   118         body = ''
       
   119         for line in patch:
       
   120             if line.startswith('#'):
       
   121                 if line.startswith('# Node ID'): node = line.split()[-1]
       
   122                 continue
       
   123             if line.startswith('diff -r'): break
       
   124             desc.append(line)
       
   125         if not node: raise ValueError
       
   126 
       
   127         #body = ('\n'.join(desc[1:]).strip() or
       
   128         #        'Patch subject is complete summary.')
       
   129         #body += '\n\n\n'
       
   130 
       
   131         if opts['plain']:
       
   132             while patch and patch[0].startswith('# '): patch.pop(0)
       
   133             if patch: patch.pop(0)
       
   134             while patch and not patch[0].strip(): patch.pop(0)
       
   135         if opts['diffstat']:
       
   136             body += cdiffstat('\n'.join(desc), patch) + '\n\n'
       
   137         body += '\n'.join(patch)
       
   138         msg = MIMEText(body)
       
   139         subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
       
   140         if subj.endswith('.'): subj = subj[:-1]
       
   141         msg['Subject'] = subj
       
   142         msg['X-Mercurial-Node'] = node
       
   143         return msg
       
   144 
       
   145     start_time = int(time.time())
       
   146 
       
   147     def genmsgid(id):
       
   148         return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
       
   149 
       
   150     patches = []
       
   151 
       
   152     class exportee:
       
   153         def __init__(self, container):
       
   154             self.lines = []
       
   155             self.container = container
       
   156             self.name = 'email'
       
   157 
       
   158         def write(self, data):
       
   159             self.lines.append(data)
       
   160 
       
   161         def close(self):
       
   162             self.container.append(''.join(self.lines).split('\n'))
       
   163             self.lines = []
       
   164 
       
   165     commands.export(ui, repo, *args, **{'output': exportee(patches),
       
   166                                         'switch_parent': False,
       
   167                                         'text': None})
       
   168 
       
   169     jumbo = []
       
   170     msgs = []
       
   171 
       
   172     ui.write('This patch series consists of %d patches.\n\n' % len(patches))
       
   173 
       
   174     for p, i in zip(patches, range(len(patches))):
       
   175         jumbo.extend(p)
       
   176         msgs.append(makepatch(p, i + 1, len(patches)))
       
   177 
       
   178     ui.write('\nWrite the introductory message for the patch series.\n\n')
       
   179 
       
   180     sender = (opts['from'] or ui.config('patchbomb', 'from') or
       
   181               prompt('From', ui.username()))
       
   182 
       
   183     msg = MIMEMultipart()
       
   184     msg['Subject'] = '[PATCH 0 of %d] %s' % (
       
   185         len(patches),
       
   186         opts['subject'] or
       
   187         prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
       
   188 
       
   189     def getaddrs(opt, prpt, default = None):
       
   190         addrs = opts[opt] or (ui.config('patchbomb', opt) or
       
   191                               prompt(prpt, default = default)).split(',')
       
   192         return [a.strip() for a in addrs if a.strip()]
       
   193     to = getaddrs('to', 'To')
       
   194     cc = getaddrs('cc', 'Cc', '')
       
   195 
       
   196     ui.write('Finish with ^D or a dot on a line by itself.\n\n')
       
   197 
       
   198     body = []
       
   199 
       
   200     while True:
       
   201         try: l = raw_input()
       
   202         except EOFError: break
       
   203         if l == '.': break
       
   204         body.append(l)
       
   205 
       
   206     msg.attach(MIMEText('\n'.join(body) + '\n'))
       
   207 
       
   208     ui.write('\n')
       
   209 
       
   210     if opts['diffstat']:
       
   211         d = cdiffstat('Final summary:\n', jumbo)
       
   212         if d: msg.attach(MIMEText(d))
       
   213 
       
   214     msgs.insert(0, msg)
       
   215 
       
   216     if not opts['test']:
       
   217         s = smtplib.SMTP()
       
   218         s.connect(host = ui.config('smtp', 'host', 'mail'),
       
   219                   port = int(ui.config('smtp', 'port', 25)))
       
   220         if ui.configbool('smtp', 'tls'):
       
   221             s.ehlo()
       
   222             s.starttls()
       
   223             s.ehlo()
       
   224         username = ui.config('smtp', 'username')
       
   225         password = ui.config('smtp', 'password')
       
   226         if username and password:
       
   227             s.login(username, password)
       
   228     parent = None
       
   229     tz = time.strftime('%z')
       
   230     for m in msgs:
       
   231         try:
       
   232             m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
       
   233         except TypeError:
       
   234             m['Message-Id'] = genmsgid('patchbomb')
       
   235         if parent:
       
   236             m['In-Reply-To'] = parent
       
   237         else:
       
   238             parent = m['Message-Id']
       
   239         m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
       
   240         start_time += 1
       
   241         m['From'] = sender
       
   242         m['To'] = ', '.join(to)
       
   243         if cc: m['Cc'] = ', '.join(cc)
       
   244         ui.status('Sending ', m['Subject'], ' ...\n')
       
   245         if opts['test']:
       
   246             fp = os.popen(os.getenv('PAGER', 'more'), 'w')
       
   247             fp.write(m.as_string(0))
       
   248             fp.write('\n')
       
   249             fp.close()
       
   250         else:
       
   251             s.sendmail(sender, to + cc, m.as_string(0))
       
   252     if not opts['test']:
       
   253         s.close()
       
   254 
       
   255 if __name__ == '__main__':
       
   256     optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
       
   257                ('d', 'diffstat', None, 'add diffstat output to messages'),
       
   258                ('f', 'from', '', 'email address of sender'),
       
   259                ('', 'plain', None, 'omit hg patch header'),
       
   260                ('n', 'test', None, 'print messages that would be sent'),
       
   261                ('s', 'subject', '', 'subject of introductory message'),
       
   262                ('t', 'to', [], 'email addresses of recipients')]
       
   263     options = {}
       
   264     try:
       
   265         args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
       
   266                                    options)
       
   267     except fancyopts.getopt.GetoptError, inst:
       
   268         u = ui.ui()
       
   269         u.warn('error: %s' % inst)
       
   270         sys.exit(1)
       
   271 
       
   272     u = ui.ui(options["verbose"], options["debug"], options["quiet"],
       
   273               not options["noninteractive"])
       
   274     repo = hg.repository(ui = u)
       
   275 
       
   276     patchbomb(u, repo, *args, **options)