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