Mercurial > hg
changeset 1669:91d40fc959f0
turn patchbomb script into an extension module.
command name is now 'hg email'.
author | Vadim Gelfer <vadim.gelfer@gmail.com> |
---|---|
date | Tue, 31 Jan 2006 08:06:35 -0800 |
parents | daff3ef0de8d |
children | fe19c54ee403 |
files | contrib/patchbomb hgext/patchbomb.py |
diffstat | 2 files changed, 264 insertions(+), 276 deletions(-) [+] |
line wrap: on
line diff
--- a/contrib/patchbomb Mon Jan 30 19:34:35 2006 +1300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,276 +0,0 @@ -#!/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 -# tls = yes # or omit if not needed -# username = user # if SMTP authentication required -# password = password # if SMTP authentication required - PLAINTEXT -# -# 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['plain']: - while patch and patch[0].startswith('# '): patch.pop(0) - if patch: patch.pop(0) - while patch and not patch[0].strip(): patch.pop(0) - 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), - 'switch_parent': False, - '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))) - if ui.configbool('smtp', 'tls'): - s.ehlo() - s.starttls() - s.ehlo() - username = ui.config('smtp', 'username') - password = ui.config('smtp', 'password') - if username and password: - s.login(username, password) - 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'), - ('', 'plain', None, 'omit hg patch header'), - ('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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/patchbomb.py Tue Jan 31 08:06:35 2006 -0800 @@ -0,0 +1,264 @@ +# Command 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 +# tls = yes # or omit if not needed +# username = user # if SMTP authentication required +# password = password # if SMTP authentication required - PLAINTEXT +# +# 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 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): + '''send changesets as a series of patch emails''' + 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['plain']: + while patch and patch[0].startswith('# '): patch.pop(0) + if patch: patch.pop(0) + while patch and not patch[0].strip(): patch.pop(0) + 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, *revs, **{'output': exportee(patches), + 'switch_parent': False, + '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))) + if ui.configbool('smtp', 'tls'): + s.ehlo() + s.starttls() + s.ehlo() + username = ui.config('smtp', 'username') + password = ui.config('smtp', 'password') + if username and password: + s.login(username, password) + 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() + +cmdtable = { + 'email': + (patchbomb, + [('c', 'cc', [], 'email addresses of copy recipients'), + ('d', 'diffstat', None, 'add diffstat output to messages'), + ('f', 'from', '', 'email address of sender'), + ('', 'plain', None, 'omit hg patch header'), + ('n', 'test', None, 'print messages that would be sent'), + ('s', 'subject', '', 'subject of introductory message'), + ('t', 'to', [], 'email addresses of recipients')], + "hg email [OPTION]... [REV]...") + }