Index: mailer.py =================================================================== --- mailer.py (revision 35290) +++ mailer.py (working copy) @@ -44,16 +44,17 @@ # Python <2.4 import popen2 platform_with_subprocess = False -if sys.version_info[0] >= 3: +try: # Python >=3.0 from io import StringIO -else: +except ImportError: # Python <3.0 from cStringIO import StringIO import smtplib import re import tempfile import types +import string # Minimal version of Subversion's bindings required _MIN_SVN_VERSION = [1, 5, 0] @@ -223,7 +224,7 @@ def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) - def start(self, group, params): + def start(self, group, params, header_list): # whitespace (or another character) separated list of addresses # which must be split into a clean list to_addr_in = self.cfg.get('to_addr', group, params) @@ -250,7 +251,13 @@ and self.reply_to[2] == ']': self.reply_to = self.reply_to[3:] - def mail_headers(self, group, params): + def mail_headers(self, group, params, header_list): + header_detail = header_list[0] + modules = header_list[1] + branches = header_list[2] + submodules = header_list[3] + files = header_list[4] + subject = self.make_subject(group, params) try: subject.encode('ascii') @@ -262,23 +269,38 @@ 'Subject: %s\n' \ 'MIME-Version: 1.0\n' \ 'Content-Type: text/plain; charset=UTF-8\n' \ - 'Content-Transfer-Encoding: 8bit\n' \ + 'Content-Transfer-Encoding: 8bit' \ % (self.from_addr, ', '.join(self.to_addrs), subject) if self.reply_to: - hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to) + hdrs = '%s\nReply-To: %s' % (hdrs, self.reply_to) + for header in header_detail: + if 'group' == header: + hdrs = '%s\nX-group: %s' % (hdrs, group) + if 'module' == header: + for m in modules: + hdrs = '%s\nX-module: %s' % (hdrs, m) + if 'branch' == header: + for b in branches: + hdrs = '%s\nX-branch: %s' % (hdrs, b) + if 'submodule' == header: + for s in submodules: + hdrs = '%s\nX-submod: %s' % (hdrs, s) + if 'file' == header: + for f in files: + hdrs = '%s\nX-file: %s' % (hdrs, f) return hdrs + '\n' class SMTPOutput(MailedOutput): "Deliver a mail message to an MTA using SMTP." - def start(self, group, params): + def start(self, group, params, header_list): MailedOutput.start(self, group, params) self.buffer = StringIO() self.write = self.buffer.write - self.write(self.mail_headers(group, params)) + self.write(self.mail_headers(group, params, header_list)) def finish(self): server = smtplib.SMTP(self.cfg.general.smtp_hostname) @@ -313,8 +335,8 @@ # figure out the command for delivery self.cmd = cfg.general.mail_command.split() - def start(self, group, params): - MailedOutput.start(self, group, params) + def start(self, group, params, header_list): + MailedOutput.start(self, group, params, header_list) ### gotta fix this. this is pretty specific to sendmail and qmail's ### mailwrapper program. should be able to use option param substitution @@ -333,7 +355,7 @@ self.pipe.fromchild.close() # start writing out the mail message - self.write(self.mail_headers(group, params)) + self.write(self.mail_headers(group, params, header_list)) def finish(self): # signal that we're done sending content @@ -429,7 +451,44 @@ renderer = TextCommitRenderer(self.output) for (group, param_tuple), (params, paths) in self.groups.items(): - self.output.start(group, params) + header_list = [] + module = [] + branch = [] + submodule = [] + file = [] + h_detail = self.cfg.get('header_detail', group, params) + if len(h_detail) >= 3 and h_detail[0] == '[' \ + and h_detail[2] == ']': + header_detail = \ + [_f for _f in h_detail[3:].split(h_detail[1]) if _f] + else: + header_detail = [_f for _f in h_detail.split() if _f] + # we should only generate the header details we truely want, but + # since we split the entire path, we might as well make all sets + for p in paths: + plist = string.split(p, '/') + try: + module = module[:] + [plist[0]] + except IndexError: + dummy = 0 + try: + branch = branch[:] + [plist[1]] + except IndexError: + dummy = 0 + try: + submodule = submodule[:] + [plist[2]] + except IndexError: + dummy = 0 + try: + file = file[:] + [plist[-1]] + except IndexError: + dummy = 0 + module = set(module) + branch = set(branch) + submodule = set(submodule) + file = set(file) + header_list = [header_detail, module, branch, submodule, file] + self.output.start(group, params, header_list) # generate the content for this group and set of params generate_content(renderer, self.cfg, self.repos, self.changelist, @@ -722,7 +781,7 @@ params, diffsels, diffurls, pool), other_diffs=other_diffs, ) - renderer.render(data) + renderer.render(data, cfg, group, params) def generate_list(changekind, changelist, paths, in_paths): @@ -796,11 +855,27 @@ binary = None singular = None content = None + ending_match = None # just skip directories. they have no diffs. if change.item_kind == svn.core.svn_node_dir: continue + file_endings = self.cfg.get('forced_binary_file_endings',\ + self.group, self.params) + if len(file_endings) >= 3 and file_endings[0] == '[' \ + and file_endings[2] == ']': + forced_binary = \ + [_f for _f in file_endings[3:].split(file_endings[1]) if _f] + else: + forced_binary = [_f for _f in file_endings.split() if _f] + for ending in forced_binary: + ending_match = re.search('\.' + ending + '$', change.path) + if ending_match != None: + label1='\nFORCED BINARY: ' + change.path + ' - %s file\n' % ending + singular=True + break + # is this change in (or out of) the set of matched paths? if (path in self.paths) != self.in_paths: continue @@ -825,12 +900,13 @@ # show the diff? if self.diffsels.delete: - diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), + if None == ending_match: + diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), base_path, None, None, self.pool) - label1 = '%s\t%s\t(r%s)' % (base_path, self.date, change.base_rev) - label2 = '/dev/null\t00:00:00 1970\t(deleted)' - singular = True + label1 = '%s\t%s\t(r%s)' % (base_path, self.date, change.base_rev) + label2 = '/dev/null\t00:00:00 1970\t(deleted)' + singular = True elif change.action == svn.repos.CHANGE_ACTION_ADD \ or change.action == svn.repos.CHANGE_ACTION_REPLACE: @@ -846,27 +922,29 @@ # show the diff? if self.diffsels.modify: - diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), + if None == ending_match: + diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), base_path, self.repos.root_this, change.path, self.pool) - label1 = '%s\t%s\t(r%s, copy source)' \ + label1 = '%s\t%s\t(r%s, copy source)' \ % (base_path, base_date, change.base_rev) - label2 = '%s\t%s\t(r%s)' \ + label2 = '%s\t%s\t(r%s)' \ % (change.path, self.date, self.repos.rev) - singular = False + singular = False else: # this file was copied. kind = 'C' if self.diffsels.copy: - diff = svn.fs.FileDiff(None, None, self.repos.root_this, + if None == ending_match: + diff = svn.fs.FileDiff(None, None, self.repos.root_this, change.path, self.pool) - label1 = '/dev/null\t00:00:00 1970\t' \ + label1 = '/dev/null\t00:00:00 1970\t' \ '(empty, because file is newly added)' - label2 = '%s\t%s\t(r%s, copy of r%s, %s)' \ + label2 = '%s\t%s\t(r%s, copy of r%s, %s)' \ % (change.path, self.date, self.repos.rev, \ change.base_rev, base_path) - singular = False + singular = False else: # the file was added. kind = 'A' @@ -876,13 +954,14 @@ # show the diff? if self.diffsels.add: - diff = svn.fs.FileDiff(None, None, self.repos.root_this, + if None == ending_match: + diff = svn.fs.FileDiff(None, None, self.repos.root_this, change.path, self.pool) - label1 = '/dev/null\t00:00:00 1970\t' \ + label1 = '/dev/null\t00:00:00 1970\t' \ '(empty, because file is newly added)' - label2 = '%s\t%s\t(r%s)' \ + label2 = '%s\t%s\t(r%s)' \ % (change.path, self.date, self.repos.rev) - singular = True + singular = True elif not change.text_changed: # the text didn't change, so nothing to show. @@ -896,28 +975,32 @@ # show the diff? if self.diffsels.modify: - diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), + if None == ending_match: + diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), base_path, self.repos.root_this, change.path, self.pool) - label1 = '%s\t%s\t(r%s)' \ + label1 = '%s\t%s\t(r%s)' \ % (base_path, base_date, change.base_rev) - label2 = '%s\t%s\t(r%s)' \ + label2 = '%s\t%s\t(r%s)' \ % (change.path, self.date, self.repos.rev) - singular = False + singular = False - if diff: - binary = diff.either_binary() - if binary: - content = src_fname = dst_fname = None - else: - src_fname, dst_fname = diff.get_files() - content = DiffContent(self.cfg.get_diff_cmd(self.group, { - 'label_from' : label1, - 'label_to' : label2, - 'from' : src_fname, - 'to' : dst_fname, - })) + if None == ending_match: + if diff: + binary = diff.either_binary() + if binary: + content = src_fname = dst_fname = None + else: + src_fname, dst_fname = diff.get_files() + content = DiffContent(self.cfg.get_diff_cmd(self.group, { + 'label_from' : label1, + 'label_to' : label2, + 'from' : src_fname, + 'to' : dst_fname, + })) + else: + kind='F' # return a data item for this diff return _data( @@ -1005,7 +1088,7 @@ def __init__(self, output): self.output = output - def render(self, data): + def render(self, data, cfg, group, params): "Render the commit defined by 'data'." w = self.output.write @@ -1038,11 +1121,11 @@ else: w('and changes in other areas\n') - self._render_diffs(data.diffs, '') + self._render_diffs(data.diffs, '', cfg, group, params) if data.other_diffs: self._render_diffs(data.other_diffs, '\nDiffs of changes in other areas also' - ' in this revision:\n') + ' in this revision:\n', cfg, group, params) def _render_list(self, header, data_list): if not data_list: @@ -1073,16 +1156,35 @@ w(' - copied%s from r%d, %s%s\n' % (text, d.base_rev, d.base_path, is_dir)) - def _render_diffs(self, diffs, section_header): + def _render_diffs(self, diffs, section_header, cfg, group, params): """Render diffs. Write the SECTION_HEADER if there are actually any diffs to render.""" if not diffs: return w = self.output.write section_header_printed = False + jmail = cfg.get('max_mail_size', group, params) + try: + max_mail_size = int(jmail) + mail_size = max_mail_size + except ValueError: + max_mail_size = -1 + mail_size = -1 + jnumber = cfg.get('max_number_of_diffs', group, params) + try: + max_number = int(jnumber) + except ValueError: + max_number = -1 + i=0 + for diff in diffs: - if not diff.diff and not diff.diff_url: + i=i+1 + if i > max_number and -1 != max_number: + w('\n\n================ MAX NUMBER OF FILES=%s' % max_number) + w(' IS REACHED, NO MORE DIFF ================\n\n') + break + if not diff.diff and not diff.diff_url and 'F' != diff.kind : continue if not section_header_printed: w(section_header) @@ -1091,6 +1193,9 @@ w('\nDeleted: %s\n' % diff.base_path) elif diff.kind == 'A': w('\nAdded: %s\n' % diff.path) + elif diff.kind == 'F': + # file is considered binary because of the file ending + w(diff.label_from) elif diff.kind == 'C': w('\nCopied: %s (from r%d, %s)\n' % (diff.path, diff.base_rev, diff.base_path)) @@ -1117,9 +1222,20 @@ continue for line in diff.content: + max_mail_size = max_mail_size - len(line.raw) + if max_mail_size <= 0 and -1 != mail_size: + w('\n============= MAX MAILSIZE=%s ' % mail_size) + w('IS REACHED NO MORE FILE DIFF INCLUDED =============\n\n') + # We ought to stop the generation of more diffs, but it + # might already be too late + break w(line.raw) + if max_mail_size <= 0 and -1 != mail_size: + # we dont need the rest of this email + break + class Repository: "Hold roots and other information about the repository." Index: mailer.conf.example =================================================================== --- mailer.conf.example (revision 35290) +++ mailer.conf.example (working copy) @@ -243,6 +243,33 @@ # Set to 0 to turn off. #truncate_subject = 200 +# Forces some file endings to be considered binary and thus no diff will be +# generated for these files. Example: file.ai will be considered binary +# if the forced_binary_file_endings contains ai +# forced_binary_file_endings = map po ai + +# specify which information you want included in extra headers +# for some reason author is always in the header as Author: +# group generates a X-group: header +# asuming paths in SVN looks like: ///.../ +# module generates a X-module: header for every module +# branch generates a X-branch: header for every branch +# submodule generates a X-submod: header for every submodule +# file generates a X-file: header for every file +# having an empty or specifying no header_detail variable means no extra +# headers. Specifying all is not an option +# header_detail = group module branch submodule file + +# Maximal number of diffed files. To prevent really big emails. +# 0 means no diffs included, below 0 means no limit +# max_number_of_diffs = 128 + +# The maximum size in bytes that an email can be. 0 means zero +# -1 means unlimited, and if the variable is not mentioned it means -1 +# The count is ONLY the actual diffs, so there will used bytes to inform +# which files was added, changed, deleted, modified, ... +# max_mail_size = 65536 + # -------------------------------------------------------------------------- [maps] @@ -328,4 +355,8 @@ # # commits to personal repositories should go to that person # for_repos = /home/(?P[^/]*)/repos # to_addr = %(who)s@example.com +# header_detail = branch module submodule +# max_mail_size = 65536 +# max_number_of_diffs = 128 +# forced_binary_file_endings = map po ai #