Mercurial > hg
changeset 875:d3f836bf6cc1
Add patchbomb script.
author | Bryan O'Sullivan <bos@serpentine.com> |
---|---|
date | Tue, 09 Aug 2005 20:18:58 -0800 |
parents | c2e77581bc84 |
children | 14cfaaec2e8e |
files | contrib/patchbomb |
diffstat | 1 files changed, 242 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/patchbomb Tue Aug 09 20:18:58 2005 -0800 @@ -0,0 +1,242 @@ +#!/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. +# +# 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 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 + +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): + try: + if default: prompt += ' [%s]' % default + prompt += rest + r = raw_input(prompt) + if not r and not empty_ok: raise EOFError + return r + except EOFError: + if default is None: raise + return default + + 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 make_patch(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 + msg = MIMEMultipart() + msg['X-Mercurial-Node'] = node + subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip()) + if subj.endswith('.'): subj = subj[:-1] + msg['Subject'] = subj + body = '\n'.join(desc[1:]).strip() + '\n' + summary = subj + if body != '\n': + msg.attach(MIMEText(body)) + summary += '\n\n' + body + else: + summary += '\n' + d = cdiffstat(summary, patch) + if d: msg.attach(MIMEText(d)) + p = MIMEText('\n'.join(patch), 'x-patch') + p['Content-Disposition'] = commands.make_filename(repo, None, + 'inline; filename=%b-%n.patch', + seqno = idx) + msg.attach(p) + return msg + + start_time = int(time.time()) + + def make_msgid(id): + return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn()) + + patches = [] + + class exportee: + def __init__(self, container): + self.lines = [] + self.container = container + + 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(make_patch(p, i + 1, len(patches))) + + ui.write('\nWrite the introductory message for the patch series.\n\n') + + sender = opts['sender'] or prompt('From', ui.username()) + + msg = MIMEMultipart() + msg['Subject'] = '[PATCH 0 of %d] %s' % ( + len(patches), + prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches))) + to = opts['to'] or [s.strip() for s in prompt('To').split(',')] + cc = opts['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) + + s = smtplib.SMTP() + s.connect(host = ui.config('smtp', 'host', 'mail'), + port = int(ui.config('smtp', 'port', 25))) + + refs = [] + parent = None + tz = time.strftime('%z') + for m in msgs: + try: + m['Message-Id'] = make_msgid(m['X-Mercurial-Node']) + except TypeError: + m['Message-Id'] = make_msgid('patchbomb') + if parent: + m['In-Reply-To'] = parent + parent = m['Message-Id'] + if len(refs) > 1: + m['References'] = ' '.join(refs[:-1]) + refs.append(parent) + m['Date'] = time.strftime('%a, %m %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)) + s.close() + +if __name__ == '__main__': + optspec = [('c', 'cc', [], 'email addresses of copy recipients'), + ('n', 'test', None, 'print messages that would be sent'), + ('s', 'sender', '', 'email address of sender'), + ('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)