--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/patchbomb Fri Aug 12 10:17:12 2005 -0800
@@ -0,0 +1,247 @@
+#!/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 readline
+import smtplib
+import socket
+import sys
+import tempfile
+import time
+
+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
+ 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)})
+
+ 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)))
+ to = (opts['to'] or ui.config('patchbomb', 'to') or
+ [s.strip() for s in prompt('To').split(',')])
+ cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
+ [s.strip() for s in prompt('Cc', default = '').split(',')])
+
+ 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')
+
+ 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)