patchbomb
252 lines
| 7.7 KiB
| text/plain
|
TextLexer
/ contrib / patchbomb
Bryan O'Sullivan
|
r875 | #!/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. | ||||
# | ||||
Bryan O'Sullivan
|
r877 | # [Optional] If the diffstat program is installed, the result of | ||
# running diffstat on the patch. | ||||
Bryan O'Sullivan
|
r875 | # | ||
# 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 | ||||
Bryan O'Sullivan
|
r877 | # firing it up "for real", in which case it will use your pager to | ||
# display each of the messages that it would send. | ||||
Bryan O'Sullivan
|
r875 | # | ||
# To configure a default mail host, add a section like this to your | ||||
# hgrc file: | ||||
# | ||||
# [smtp] | ||||
# host = my_mail_host | ||||
# port = 1025 | ||||
Bryan O'Sullivan
|
r877 | # | ||
# 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, ... | ||||
Bryan O'Sullivan
|
r875 | |||
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): | ||||
Bryan O'Sullivan
|
r876 | if default: prompt += ' [%s]' % default | ||
prompt += rest | ||||
while True: | ||||
Bryan O'Sullivan
|
r875 | r = raw_input(prompt) | ||
Bryan O'Sullivan
|
r876 | if r: return r | ||
if default is not None: return default | ||||
if empty_ok: return r | ||||
Bryan O'Sullivan
|
r877 | ui.warn('Please enter a valid value.\n') | ||
Bryan O'Sullivan
|
r875 | |||
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 | ||||
Bryan O'Sullivan
|
r876 | def makepatch(patch, idx, total): | ||
Bryan O'Sullivan
|
r875 | desc = [] | ||
node = None | ||||
Thomas Arendsen Hein
|
r1135 | body = '' | ||
Bryan O'Sullivan
|
r875 | 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 | ||||
mpm@selenic.com
|
r1118 | |||
#body = ('\n'.join(desc[1:]).strip() or | ||||
# 'Patch subject is complete summary.') | ||||
#body += '\n\n\n' | ||||
Bryan O'Sullivan
|
r877 | if opts['diffstat']: | ||
body += cdiffstat('\n'.join(desc), patch) + '\n\n' | ||||
Bryan O'Sullivan
|
r876 | body += '\n'.join(patch) | ||
msg = MIMEText(body) | ||||
Bryan O'Sullivan
|
r875 | subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip()) | ||
if subj.endswith('.'): subj = subj[:-1] | ||||
msg['Subject'] = subj | ||||
Bryan O'Sullivan
|
r876 | msg['X-Mercurial-Node'] = node | ||
Bryan O'Sullivan
|
r875 | return msg | ||
start_time = int(time.time()) | ||||
Bryan O'Sullivan
|
r876 | def genmsgid(id): | ||
Bryan O'Sullivan
|
r875 | return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn()) | ||
patches = [] | ||||
class exportee: | ||||
def __init__(self, container): | ||||
self.lines = [] | ||||
self.container = container | ||||
Bryan O'Sullivan
|
r876 | self.name = 'email' | ||
Bryan O'Sullivan
|
r875 | |||
def write(self, data): | ||||
self.lines.append(data) | ||||
def close(self): | ||||
self.container.append(''.join(self.lines).split('\n')) | ||||
self.lines = [] | ||||
Bryan O'Sullivan
|
r1032 | commands.export(ui, repo, *args, **{'output': exportee(patches), | ||
'text': None}) | ||||
Bryan O'Sullivan
|
r875 | |||
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) | ||||
Bryan O'Sullivan
|
r876 | msgs.append(makepatch(p, i + 1, len(patches))) | ||
Bryan O'Sullivan
|
r875 | |||
ui.write('\nWrite the introductory message for the patch series.\n\n') | ||||
Bryan O'Sullivan
|
r877 | sender = (opts['from'] or ui.config('patchbomb', 'from') or | ||
prompt('From', ui.username())) | ||||
Bryan O'Sullivan
|
r875 | |||
msg = MIMEMultipart() | ||||
msg['Subject'] = '[PATCH 0 of %d] %s' % ( | ||||
len(patches), | ||||
Bryan O'Sullivan
|
r877 | opts['subject'] or | ||
Bryan O'Sullivan
|
r875 | prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches))) | ||
Bryan O'Sullivan
|
r997 | to = opts['to'] or ui.config('patchbomb', 'to') or prompt('To') | ||
to = [t.strip() for t in to.split(',')] | ||||
Bryan O'Sullivan
|
r877 | cc = (opts['cc'] or ui.config('patchbomb', 'cc') or | ||
Bryan O'Sullivan
|
r997 | prompt('Cc', default = '')) | ||
Bryan O'Sullivan
|
r998 | cc = (cc and [c.strip() for c in cc.split(',')]) or [] | ||
Bryan O'Sullivan
|
r875 | |||
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) | ||||
Bryan O'Sullivan
|
r876 | if not opts['test']: | ||
s = smtplib.SMTP() | ||||
s.connect(host = ui.config('smtp', 'host', 'mail'), | ||||
port = int(ui.config('smtp', 'port', 25))) | ||||
Bryan O'Sullivan
|
r875 | |||
parent = None | ||||
tz = time.strftime('%z') | ||||
for m in msgs: | ||||
try: | ||||
Bryan O'Sullivan
|
r876 | m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) | ||
Bryan O'Sullivan
|
r875 | except TypeError: | ||
Bryan O'Sullivan
|
r876 | m['Message-Id'] = genmsgid('patchbomb') | ||
Bryan O'Sullivan
|
r875 | if parent: | ||
m['In-Reply-To'] = parent | ||||
Bryan O'Sullivan
|
r876 | else: | ||
parent = m['Message-Id'] | ||||
Bryan O'Sullivan
|
r877 | m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz | ||
Bryan O'Sullivan
|
r875 | 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)) | ||||
Bryan O'Sullivan
|
r876 | if not opts['test']: | ||
s.close() | ||||
Bryan O'Sullivan
|
r875 | |||
if __name__ == '__main__': | ||||
optspec = [('c', 'cc', [], 'email addresses of copy recipients'), | ||||
Bryan O'Sullivan
|
r877 | ('d', 'diffstat', None, 'add diffstat output to messages'), | ||
('f', 'from', '', 'email address of sender'), | ||||
Bryan O'Sullivan
|
r875 | ('n', 'test', None, 'print messages that would be sent'), | ||
Bryan O'Sullivan
|
r877 | ('s', 'subject', '', 'subject of introductory message'), | ||
Bryan O'Sullivan
|
r875 | ('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) | ||||