?? mailer.py
字號:
#!/usr/bin/env python2
#
# mailer.py: send email describing a commit
#
# $HeadURL: http://svn.collab.net/repos/svn/branches/1.1.x/tools/hook-scripts/mailer/mailer.py $
# $LastChangedDate: 2005-03-26 12:14:01 -0800 (Sat, 26 Mar 2005) $
# $LastChangedBy: maxb $
# $LastChangedRevision: 13698 $
#
# USAGE: mailer.py commit REPOS-DIR REVISION [CONFIG-FILE]
# mailer.py propchange REPOS-DIR REVISION AUTHOR PROPNAME [CONFIG-FILE]
#
# Using CONFIG-FILE, deliver an email describing the changes between
# REV and REV-1 for the repository REPOS.
#
import os
import sys
import string
import ConfigParser
import time
import popen2
import cStringIO
import smtplib
import re
import types
import svn.fs
import svn.delta
import svn.repos
import svn.core
SEPARATOR = '=' * 78
def main(pool, cmd, config_fname, repos_dir, rev, author, propname):
repos = Repository(repos_dir, rev, pool)
if cmd == 'commit':
cfg = Config(config_fname, repos, { 'author' : author or repos.author })
messenger = Commit(pool, cfg, repos)
elif cmd == 'propchange':
# Override the repos revision author with the author of the propchange
repos.author = author
cfg = Config(config_fname, repos, { 'author' : author })
messenger = PropChange(pool, cfg, repos, author, propname)
else:
raise UnknownSubcommand(cmd)
messenger.generate()
# ============================================================================
if sys.platform == "win32":
_escape_shell_arg_re = re.compile(r'(\\+)(\"|$)')
def escape_shell_arg(arg):
# The (very strange) parsing rules used by the C runtime library are
# described at:
# http://msdn.microsoft.com/library/en-us/vclang/html/_pluslang_Parsing_C.2b2b_.Command.2d.Line_Arguments.asp
# double up slashes, but only if they are followed by a quote character
arg = re.sub(_escape_shell_arg_re, r'\1\1\2', arg)
# surround by quotes and escape quotes inside
arg = '"' + string.replace(arg, '"', '"^""') + '"'
return arg
def argv_to_command_string(argv):
"""Flatten a list of command line arguments into a command string.
The resulting command string is expected to be passed to the system
shell which os functions like popen() and system() invoke internally.
"""
# According cmd's usage notes (cmd /?), it parses the command line by
# "seeing if the first character is a quote character and if so, stripping
# the leading character and removing the last quote character."
# So to prevent the argument string from being changed we add an extra set
# of quotes around it here.
return '"' + string.join(map(escape_shell_arg, argv), " ") + '"'
else:
def escape_shell_arg(str):
return "'" + string.replace(str, "'", "'\\''") + "'"
def argv_to_command_string(argv):
"""Flatten a list of command line arguments into a command string.
The resulting command string is expected to be passed to the system
shell which os functions like popen() and system() invoke internally.
"""
return string.join(map(escape_shell_arg, argv), " ")
# ============================================================================
# Minimal, incomplete, versions of popen2.Popen[34] for those platforms
# for which popen2 does not provide them.
try:
Popen3 = popen2.Popen3
Popen4 = popen2.Popen4
except AttributeError:
class Popen3:
def __init__(self, cmd, capturestderr = False):
if type(cmd) != types.StringType:
cmd = argv_to_command_string(cmd)
if capturestderr:
self.fromchild, self.tochild, self.childerr \
= popen2.popen3(cmd, mode='b')
else:
self.fromchild, self.tochild = popen2.popen2(cmd, mode='b')
self.childerr = None
def wait(self):
rv = self.fromchild.close()
rv = self.tochild.close() or rv
if self.childerr is not None:
rv = self.childerr.close() or rv
return rv
class Popen4:
def __init__(self, cmd):
if type(cmd) != types.StringType:
cmd = argv_to_command_string(cmd)
self.fromchild, self.tochild = popen2.popen4(cmd, mode='b')
def wait(self):
rv = self.fromchild.close()
rv = self.tochild.close() or rv
return rv
class MailedOutput:
def __init__(self, cfg, repos, prefix_param):
self.cfg = cfg
self.repos = repos
self.prefix_param = prefix_param
self._CHUNKSIZE = 128 * 1024
def start(self, group, params):
# whitespace-separated list of addresses; split into a clean list:
self.to_addrs = \
filter(None, string.split(self.cfg.get('to_addr', group, params)))
self.from_addr = self.cfg.get('from_addr', group, params) \
or self.repos.author or 'no_author'
self.reply_to = self.cfg.get('reply_to', group, params)
def mail_headers(self, group, params):
prefix = self.cfg.get(self.prefix_param, group, params)
if prefix:
subject = prefix + ' ' + self.subject
else:
subject = self.subject
hdrs = 'From: %s\n' \
'To: %s\n' \
'Subject: %s\n' \
'MIME-Version: 1.0\n' \
'Content-Type: text/plain; charset=UTF-8\n' \
% (self.from_addr, string.join(self.to_addrs, ', '), subject)
if self.reply_to:
hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to)
return hdrs + '\n'
def run(self, cmd):
# By default we choose to incorporate child stderr into the output
pipe_ob = Popen4(cmd)
buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
while buf:
self.write(buf)
buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
# wait on the child so we don't end up with a billion zombies
pipe_ob.wait()
class SMTPOutput(MailedOutput):
"Deliver a mail message to an MTA using SMTP."
def start(self, group, params, **args):
MailedOutput.start(self, group, params, **args)
self.buffer = cStringIO.StringIO()
self.write = self.buffer.write
self.write(self.mail_headers(group, params))
def finish(self):
server = smtplib.SMTP(self.cfg.general.smtp_hostname)
if self.cfg.is_set('general.smtp_username'):
server.login(self.cfg.general.smtp_username,
self.cfg.general.smtp_password)
server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue())
server.quit()
class StandardOutput:
"Print the commit message to stdout."
def __init__(self, cfg, repos, prefix_param):
self.cfg = cfg
self.repos = repos
self._CHUNKSIZE = 128 * 1024
self.write = sys.stdout.write
def start(self, group, params, **args):
pass
def finish(self):
pass
def run(self, cmd):
# By default we choose to incorporate child stderr into the output
pipe_ob = Popen4(cmd)
buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
while buf:
self.write(buf)
buf = pipe_ob.fromchild.read(self._CHUNKSIZE)
# wait on the child so we don't end up with a billion zombies
pipe_ob.wait()
class PipeOutput(MailedOutput):
"Deliver a mail message to an MDA via a pipe."
def __init__(self, cfg, repos, prefix_param):
MailedOutput.__init__(self, cfg, repos, prefix_param)
# figure out the command for delivery
self.cmd = string.split(cfg.general.mail_command)
def start(self, group, params, **args):
MailedOutput.start(self, group, params, **args)
### gotta fix this. this is pretty specific to sendmail and qmail's
### mailwrapper program. should be able to use option param substitution
cmd = self.cmd + [ '-f', self.from_addr ] + self.to_addrs
# construct the pipe for talking to the mailer
self.pipe = Popen3(cmd)
self.write = self.pipe.tochild.write
# we don't need the read-from-mailer descriptor, so close it
self.pipe.fromchild.close()
# start writing out the mail message
self.write(self.mail_headers(group, params))
def finish(self):
# signal that we're done sending content
self.pipe.tochild.close()
# wait to avoid zombies
self.pipe.wait()
class Messenger:
def __init__(self, pool, cfg, repos, prefix_param):
self.pool = pool
self.cfg = cfg
self.repos = repos
self.determine_output(cfg, repos, prefix_param)
def determine_output(self, cfg, repos, prefix_param):
if cfg.is_set('general.mail_command'):
cls = PipeOutput
elif cfg.is_set('general.smtp_hostname'):
cls = SMTPOutput
else:
cls = StandardOutput
self.output = cls(cfg, repos, prefix_param)
class Commit(Messenger):
def __init__(self, pool, cfg, repos):
Messenger.__init__(self, pool, cfg, repos, 'commit_subject_prefix')
# get all the changes and sort by path
editor = svn.repos.RevisionChangeCollector(repos.fs_ptr, repos.rev,
self.pool)
e_ptr, e_baton = svn.delta.make_editor(editor, self.pool)
svn.repos.svn_repos_replay(repos.root_this, e_ptr, e_baton, self.pool)
self.changelist = editor.changes.items()
self.changelist.sort()
### hunh. this code isn't actually needed for StandardOutput. refactor?
# collect the set of groups and the unique sets of params for the options
self.groups = { }
for path, change in self.changelist:
for (group, params) in self.cfg.which_groups(path):
# turn the params into a hashable object and stash it away
param_list = params.items()
param_list.sort()
self.groups[group, tuple(param_list)] = params
# figure out the changed directories
dirs = { }
for path, change in self.changelist:
if change.item_kind == svn.core.svn_node_dir:
dirs[path] = None
else:
idx = string.rfind(path, '/')
if idx == -1:
dirs[''] = None
else:
dirs[path[:idx]] = None
dirlist = dirs.keys()
# figure out the common portion of all the dirs. note that there is
# no "common" if only a single dir was changed, or the root was changed.
if len(dirs) == 1 or dirs.has_key(''):
commondir = ''
else:
common = string.split(dirlist.pop(), '/')
for d in dirlist:
parts = string.split(d, '/')
for i in range(len(common)):
if i == len(parts) or common[i] != parts[i]:
del common[i:]
break
commondir = string.join(common, '/')
if commondir:
# strip the common portion from each directory
l = len(commondir) + 1
dirlist = [ ]
for d in dirs.keys():
if d == commondir:
dirlist.append('.')
else:
dirlist.append(d[l:])
else:
# nothing in common, so reset the list of directories
dirlist = dirs.keys()
# compose the basic subject line. later, we can prefix it.
dirlist.sort()
dirlist = string.join(dirlist)
if commondir:
self.output.subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist)
else:
self.output.subject = 'r%d - %s' % (repos.rev, dirlist)
def generate(self):
"Generate email for the various groups and option-params."
### the groups need to be further compressed. if the headers and
### body are the same across groups, then we can have multiple To:
### addresses. SMTPOutput holds the entire message body in memory,
### so if the body doesn't change, then it can be sent N times
### rather than rebuilding it each time.
subpool = svn.core.svn_pool_create(self.pool)
for (group, param_tuple), params in self.groups.items():
self.output.start(group, params)
# generate the content for this group and set of params
generate_content(self.output, self.cfg, self.repos, self.changelist,
group, params, subpool)
self.output.finish()
svn.core.svn_pool_clear(subpool)
svn.core.svn_pool_destroy(subpool)
class PropChange(Messenger):
def __init__(self, pool, cfg, repos, author, propname):
Messenger.__init__(self, pool, cfg, repos, 'propchange_subject_prefix')
self.author = author
self.propname = propname
### hunh. this code isn't actually needed for StandardOutput. refactor?
# collect the set of groups and the unique sets of params for the options
self.groups = { }
for (group, params) in self.cfg.which_groups(''):
# turn the params into a hashable object and stash it away
param_list = params.items()
param_list.sort()
self.groups[group, tuple(param_list)] = params
self.output.subject = 'r%d - %s' % (repos.rev, propname)
def generate(self):
for (group, param_tuple), params in self.groups.items():
self.output.start(group, params)
self.output.write('Author: %s\nRevision: %s\nProperty Name: %s\n\n'
% (self.author, self.repos.rev, self.propname))
propvalue = self.repos.get_rev_prop(self.propname)
self.output.write('New Property Value:\n')
self.output.write(propvalue)
self.output.finish()
def generate_content(output, cfg, repos, changelist, group, params, pool):
svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE)
### pick a different date format?
date = time.ctime(svn.core.secs_from_timestr(svndate, pool))
output.write('Author: %s\nDate: %s\nNew Revision: %s\n\n'
% (repos.author, date, repos.rev))
# print summary sections
generate_list(output, 'Added', changelist, _select_adds)
generate_list(output, 'Removed', changelist, _select_deletes)
generate_list(output, 'Modified', changelist, _select_modifies)
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -