Mercurial > hg > mercurial-crew-with-dirclash
diff contrib/patchbomb @ 878:781266a78fe1
Merge patchbomb script.
author | Bryan O'Sullivan <bos@serpentine.com> |
---|---|
date | Fri, 12 Aug 2005 10:17:12 -0800 |
parents | 25430c523677 |
children | 01215ad04283 |
line wrap: on
line diff
new file mode 100755 --- /dev/null +++ b/contrib/patchbomb @@ -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)