contrib/patchbomb
changeset 1680 c21b54f7f7b8
parent 1679 675ca845c2f8
parent 1676 0690d0f202e1
child 1681 98eef041f9c7
equal deleted inserted replaced
1679:675ca845c2f8 1680:c21b54f7f7b8
     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['diffstat']:
       
   132             body += cdiffstat('\n'.join(desc), patch) + '\n\n'
       
   133         body += '\n'.join(patch)
       
   134         msg = MIMEText(body)
       
   135         subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
       
   136         if subj.endswith('.'): subj = subj[:-1]
       
   137         msg['Subject'] = subj
       
   138         msg['X-Mercurial-Node'] = node
       
   139         return msg
       
   140 
       
   141     start_time = int(time.time())
       
   142 
       
   143     def genmsgid(id):
       
   144         return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
       
   145 
       
   146     patches = []
       
   147 
       
   148     class exportee:
       
   149         def __init__(self, container):
       
   150             self.lines = []
       
   151             self.container = container
       
   152             self.name = 'email'
       
   153 
       
   154         def write(self, data):
       
   155             self.lines.append(data)
       
   156 
       
   157         def close(self):
       
   158             self.container.append(''.join(self.lines).split('\n'))
       
   159             self.lines = []
       
   160 
       
   161     commands.export(ui, repo, *args, **{'output': exportee(patches),
       
   162                                         'text': None})
       
   163 
       
   164     jumbo = []
       
   165     msgs = []
       
   166 
       
   167     ui.write('This patch series consists of %d patches.\n\n' % len(patches))
       
   168 
       
   169     for p, i in zip(patches, range(len(patches))):
       
   170         jumbo.extend(p)
       
   171         msgs.append(makepatch(p, i + 1, len(patches)))
       
   172 
       
   173     ui.write('\nWrite the introductory message for the patch series.\n\n')
       
   174 
       
   175     sender = (opts['from'] or ui.config('patchbomb', 'from') or
       
   176               prompt('From', ui.username()))
       
   177 
       
   178     msg = MIMEMultipart()
       
   179     msg['Subject'] = '[PATCH 0 of %d] %s' % (
       
   180         len(patches),
       
   181         opts['subject'] or
       
   182         prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
       
   183 
       
   184     def getaddrs(opt, prpt, default = None):
       
   185         addrs = opts[opt] or (ui.config('patchbomb', opt) or
       
   186                               prompt(prpt, default = default)).split(',')
       
   187         return [a.strip() for a in addrs if a.strip()]
       
   188     to = getaddrs('to', 'To')
       
   189     cc = getaddrs('cc', 'Cc', '')
       
   190 
       
   191     ui.write('Finish with ^D or a dot on a line by itself.\n\n')
       
   192 
       
   193     body = []
       
   194 
       
   195     while True:
       
   196         try: l = raw_input()
       
   197         except EOFError: break
       
   198         if l == '.': break
       
   199         body.append(l)
       
   200 
       
   201     msg.attach(MIMEText('\n'.join(body) + '\n'))
       
   202 
       
   203     ui.write('\n')
       
   204 
       
   205     if opts['diffstat']:
       
   206         d = cdiffstat('Final summary:\n', jumbo)
       
   207         if d: msg.attach(MIMEText(d))
       
   208 
       
   209     msgs.insert(0, msg)
       
   210 
       
   211     if not opts['test']:
       
   212         s = smtplib.SMTP()
       
   213         s.connect(host = ui.config('smtp', 'host', 'mail'),
       
   214                   port = int(ui.config('smtp', 'port', 25)))
       
   215         if ui.configbool('smtp', 'tls'):
       
   216             s.ehlo()
       
   217             s.starttls()
       
   218             s.ehlo()
       
   219         username = ui.config('smtp', 'username')
       
   220         password = ui.config('smtp', 'password')
       
   221         if username and password:
       
   222             s.login(username, password)
       
   223     parent = None
       
   224     tz = time.strftime('%z')
       
   225     for m in msgs:
       
   226         try:
       
   227             m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
       
   228         except TypeError:
       
   229             m['Message-Id'] = genmsgid('patchbomb')
       
   230         if parent:
       
   231             m['In-Reply-To'] = parent
       
   232         else:
       
   233             parent = m['Message-Id']
       
   234         m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
       
   235         start_time += 1
       
   236         m['From'] = sender
       
   237         m['To'] = ', '.join(to)
       
   238         if cc: m['Cc'] = ', '.join(cc)
       
   239         ui.status('Sending ', m['Subject'], ' ...\n')
       
   240         if opts['test']:
       
   241             fp = os.popen(os.getenv('PAGER', 'more'), 'w')
       
   242             fp.write(m.as_string(0))
       
   243             fp.write('\n')
       
   244             fp.close()
       
   245         else:
       
   246             s.sendmail(sender, to + cc, m.as_string(0))
       
   247     if not opts['test']:
       
   248         s.close()
       
   249 
       
   250 if __name__ == '__main__':
       
   251     optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
       
   252                ('d', 'diffstat', None, 'add diffstat output to messages'),
       
   253                ('f', 'from', '', 'email address of sender'),
       
   254                ('n', 'test', None, 'print messages that would be sent'),
       
   255                ('s', 'subject', '', 'subject of introductory message'),
       
   256                ('t', 'to', [], 'email addresses of recipients')]
       
   257     options = {}
       
   258     try:
       
   259         args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
       
   260                                    options)
       
   261     except fancyopts.getopt.GetoptError, inst:
       
   262         u = ui.ui()
       
   263         u.warn('error: %s' % inst)
       
   264         sys.exit(1)
       
   265 
       
   266     u = ui.ui(options["verbose"], options["debug"], options["quiet"],
       
   267               not options["noninteractive"])
       
   268     repo = hg.repository(ui = u)
       
   269 
       
   270     patchbomb(u, repo, *args, **options)