|
|
#!/usr/bin/python
|
|
|
#
|
|
|
# Interactive script for sending a collection of Mercurial changesets
|
|
|
# as a series of patch emails.
|
|
|
#
|
|
|
# The series is started off with a "[PATCH 0 of N]" introduction,
|
|
|
# which describes the series as a whole.
|
|
|
#
|
|
|
# Each patch email has a Subject line of "[PATCH M of N] ...", using
|
|
|
# the first line of the changeset description as the subject text.
|
|
|
# The message contains two or three body parts:
|
|
|
#
|
|
|
# The remainder of the changeset description.
|
|
|
#
|
|
|
# [Optional] If the diffstat program is installed, the result of
|
|
|
# running diffstat on the patch.
|
|
|
#
|
|
|
# The patch itself, as generated by "hg export".
|
|
|
#
|
|
|
# Each message refers to all of its predecessors using the In-Reply-To
|
|
|
# and References headers, so they will show up as a sequence in
|
|
|
# threaded mail and news readers, and in mail archives.
|
|
|
#
|
|
|
# For each changeset, you will be prompted with a diffstat summary and
|
|
|
# the changeset summary, so you can be sure you are sending the right
|
|
|
# changes.
|
|
|
#
|
|
|
# It is best to run this script with the "-n" (test only) flag before
|
|
|
# firing it up "for real", in which case it will use your pager to
|
|
|
# display each of the messages that it would send.
|
|
|
#
|
|
|
# To configure a default mail host, add a section like this to your
|
|
|
# hgrc file:
|
|
|
#
|
|
|
# [smtp]
|
|
|
# host = my_mail_host
|
|
|
# port = 1025
|
|
|
#
|
|
|
# To configure other defaults, add a section like this to your hgrc
|
|
|
# file:
|
|
|
#
|
|
|
# [patchbomb]
|
|
|
# from = My Name <my@email>
|
|
|
# to = recipient1, recipient2, ...
|
|
|
# cc = cc1, cc2, ...
|
|
|
|
|
|
from email.MIMEMultipart import MIMEMultipart
|
|
|
from email.MIMEText import MIMEText
|
|
|
from mercurial import commands
|
|
|
from mercurial import fancyopts
|
|
|
from mercurial import hg
|
|
|
from mercurial import ui
|
|
|
import os
|
|
|
import popen2
|
|
|
import smtplib
|
|
|
import socket
|
|
|
import sys
|
|
|
import tempfile
|
|
|
import time
|
|
|
|
|
|
try:
|
|
|
# readline gives raw_input editing capabilities, but is not
|
|
|
# present on windows
|
|
|
import readline
|
|
|
except ImportError: pass
|
|
|
|
|
|
def diffstat(patch):
|
|
|
fd, name = tempfile.mkstemp()
|
|
|
try:
|
|
|
p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
|
|
|
try:
|
|
|
for line in patch: print >> p.tochild, line
|
|
|
p.tochild.close()
|
|
|
if p.wait(): return
|
|
|
fp = os.fdopen(fd, 'r')
|
|
|
stat = []
|
|
|
for line in fp: stat.append(line.lstrip())
|
|
|
last = stat.pop()
|
|
|
stat.insert(0, last)
|
|
|
stat = ''.join(stat)
|
|
|
if stat.startswith('0 files'): raise ValueError
|
|
|
return stat
|
|
|
except: raise
|
|
|
finally:
|
|
|
try: os.unlink(name)
|
|
|
except: pass
|
|
|
|
|
|
def patchbomb(ui, repo, *revs, **opts):
|
|
|
def prompt(prompt, default = None, rest = ': ', empty_ok = False):
|
|
|
if default: prompt += ' [%s]' % default
|
|
|
prompt += rest
|
|
|
while True:
|
|
|
r = raw_input(prompt)
|
|
|
if r: return r
|
|
|
if default is not None: return default
|
|
|
if empty_ok: return r
|
|
|
ui.warn('Please enter a valid value.\n')
|
|
|
|
|
|
def confirm(s):
|
|
|
if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
|
|
|
raise ValueError
|
|
|
|
|
|
def cdiffstat(summary, patch):
|
|
|
s = diffstat(patch)
|
|
|
if s:
|
|
|
if summary:
|
|
|
ui.write(summary, '\n')
|
|
|
ui.write(s, '\n')
|
|
|
confirm('Does the diffstat above look okay')
|
|
|
return s
|
|
|
|
|
|
def makepatch(patch, idx, total):
|
|
|
desc = []
|
|
|
node = None
|
|
|
body = ''
|
|
|
for line in patch:
|
|
|
if line.startswith('#'):
|
|
|
if line.startswith('# Node ID'): node = line.split()[-1]
|
|
|
continue
|
|
|
if line.startswith('diff -r'): break
|
|
|
desc.append(line)
|
|
|
if not node: raise ValueError
|
|
|
|
|
|
#body = ('\n'.join(desc[1:]).strip() or
|
|
|
# 'Patch subject is complete summary.')
|
|
|
#body += '\n\n\n'
|
|
|
|
|
|
if opts['diffstat']:
|
|
|
body += cdiffstat('\n'.join(desc), patch) + '\n\n'
|
|
|
body += '\n'.join(patch)
|
|
|
msg = MIMEText(body)
|
|
|
subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
|
|
|
if subj.endswith('.'): subj = subj[:-1]
|
|
|
msg['Subject'] = subj
|
|
|
msg['X-Mercurial-Node'] = node
|
|
|
return msg
|
|
|
|
|
|
start_time = int(time.time())
|
|
|
|
|
|
def genmsgid(id):
|
|
|
return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
|
|
|
|
|
|
patches = []
|
|
|
|
|
|
class exportee:
|
|
|
def __init__(self, container):
|
|
|
self.lines = []
|
|
|
self.container = container
|
|
|
self.name = 'email'
|
|
|
|
|
|
def write(self, data):
|
|
|
self.lines.append(data)
|
|
|
|
|
|
def close(self):
|
|
|
self.container.append(''.join(self.lines).split('\n'))
|
|
|
self.lines = []
|
|
|
|
|
|
commands.export(ui, repo, *args, **{'output': exportee(patches),
|
|
|
'text': None})
|
|
|
|
|
|
jumbo = []
|
|
|
msgs = []
|
|
|
|
|
|
ui.write('This patch series consists of %d patches.\n\n' % len(patches))
|
|
|
|
|
|
for p, i in zip(patches, range(len(patches))):
|
|
|
jumbo.extend(p)
|
|
|
msgs.append(makepatch(p, i + 1, len(patches)))
|
|
|
|
|
|
ui.write('\nWrite the introductory message for the patch series.\n\n')
|
|
|
|
|
|
sender = (opts['from'] or ui.config('patchbomb', 'from') or
|
|
|
prompt('From', ui.username()))
|
|
|
|
|
|
msg = MIMEMultipart()
|
|
|
msg['Subject'] = '[PATCH 0 of %d] %s' % (
|
|
|
len(patches),
|
|
|
opts['subject'] or
|
|
|
prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
|
|
|
|
|
|
def getaddrs(opt, prpt, default = None):
|
|
|
addrs = opts[opt] or (ui.config('patchbomb', opt) or
|
|
|
prompt(prpt, default = default)).split(',')
|
|
|
return [a.strip() for a in addrs if a.strip()]
|
|
|
to = getaddrs('to', 'To')
|
|
|
cc = getaddrs('cc', 'Cc', '')
|
|
|
|
|
|
ui.write('Finish with ^D or a dot on a line by itself.\n\n')
|
|
|
|
|
|
body = []
|
|
|
|
|
|
while True:
|
|
|
try: l = raw_input()
|
|
|
except EOFError: break
|
|
|
if l == '.': break
|
|
|
body.append(l)
|
|
|
|
|
|
msg.attach(MIMEText('\n'.join(body) + '\n'))
|
|
|
|
|
|
ui.write('\n')
|
|
|
|
|
|
if opts['diffstat']:
|
|
|
d = cdiffstat('Final summary:\n', jumbo)
|
|
|
if d: msg.attach(MIMEText(d))
|
|
|
|
|
|
msgs.insert(0, msg)
|
|
|
|
|
|
if not opts['test']:
|
|
|
s = smtplib.SMTP()
|
|
|
s.connect(host = ui.config('smtp', 'host', 'mail'),
|
|
|
port = int(ui.config('smtp', 'port', 25)))
|
|
|
|
|
|
parent = None
|
|
|
tz = time.strftime('%z')
|
|
|
for m in msgs:
|
|
|
try:
|
|
|
m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
|
|
|
except TypeError:
|
|
|
m['Message-Id'] = genmsgid('patchbomb')
|
|
|
if parent:
|
|
|
m['In-Reply-To'] = parent
|
|
|
else:
|
|
|
parent = m['Message-Id']
|
|
|
m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
|
|
|
start_time += 1
|
|
|
m['From'] = sender
|
|
|
m['To'] = ', '.join(to)
|
|
|
if cc: m['Cc'] = ', '.join(cc)
|
|
|
ui.status('Sending ', m['Subject'], ' ...\n')
|
|
|
if opts['test']:
|
|
|
fp = os.popen(os.getenv('PAGER', 'more'), 'w')
|
|
|
fp.write(m.as_string(0))
|
|
|
fp.write('\n')
|
|
|
fp.close()
|
|
|
else:
|
|
|
s.sendmail(sender, to + cc, m.as_string(0))
|
|
|
if not opts['test']:
|
|
|
s.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
|
|
|
('d', 'diffstat', None, 'add diffstat output to messages'),
|
|
|
('f', 'from', '', 'email address of sender'),
|
|
|
('n', 'test', None, 'print messages that would be sent'),
|
|
|
('s', 'subject', '', 'subject of introductory message'),
|
|
|
('t', 'to', [], 'email addresses of recipients')]
|
|
|
options = {}
|
|
|
try:
|
|
|
args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
|
|
|
options)
|
|
|
except fancyopts.getopt.GetoptError, inst:
|
|
|
u = ui.ui()
|
|
|
u.warn('error: %s' % inst)
|
|
|
sys.exit(1)
|
|
|
|
|
|
u = ui.ui(options["verbose"], options["debug"], options["quiet"],
|
|
|
not options["noninteractive"])
|
|
|
repo = hg.repository(ui = u)
|
|
|
|
|
|
patchbomb(u, repo, *args, **options)
|
|
|
|