Modified: subversion/branches/pristine-checksum-salt/tools/hook-scripts/mailer/mailer.py URL: http://svn.apache.org/viewvc/subversion/branches/pristine-checksum-salt/tools/hook-scripts/mailer/mailer.py?rev=1915519&r1=1915518&r2=1915519&view=diff ============================================================================== --- subversion/branches/pristine-checksum-salt/tools/hook-scripts/mailer/mailer.py (original) +++ subversion/branches/pristine-checksum-salt/tools/hook-scripts/mailer/mailer.py Thu Feb 1 20:04:07 2024 @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -130,7 +129,7 @@ def main(pool, cmd, config_fname, repos_ {'author': author, 'repos_basename': os.path.basename(repos.repos_dir) }) - messenger = PropChange(pool, cfg, repos, author, propname, action) + messenger = PropChange(cfg, repos, author, propname, action) elif cmd == 'lock' or cmd == 'unlock': author = cmd_args[0] repos = Repository(repos_dir, 0, pool) ### any old revision will do @@ -144,7 +143,19 @@ def main(pool, cmd, config_fname, repos_ else: raise UnknownSubcommand(cmd) - return messenger.generate() + output = create_output(cfg, repos) + return messenger.generate(output, pool) + + +def create_output(cfg, repos): + if cfg.is_set('general.mail_command'): + cls = PipeOutput + elif cfg.is_set('general.smtp_hostname'): + cls = SMTPOutput + else: + cls = StandardOutput + + return cls(cfg, repos) def remove_leading_slashes(path): @@ -153,90 +164,79 @@ def remove_leading_slashes(path): return path +class Writer: + "Simple class for writing strings/binary, with optional encoding." + + def __init__(self, encoding): + self.buffer = BytesIO() + + # Attach a couple functions to SELF, rather than methods. + self.write_binary = self.buffer.write + + if codecs.lookup(encoding) != codecs.lookup('utf-8'): + def _write(s): + "Write text string S using the given encoding." + return self.buffer.write(s.encode(encoding, 'backslashreplace')) + else: + def _write(s): + "Write text string S using the *default* encoding (utf-8)." + return self.buffer.write(to_bytes(s)) + self.write = _write + + class OutputBase: "Abstract base class to formalize the interface of output methods" - def __init__(self, cfg, repos, prefix_param): + def __init__(self, cfg, repos): self.cfg = cfg self.repos = repos - self.prefix_param = prefix_param self._CHUNKSIZE = 128 * 1024 - # This is a public member variable. This must be assigned a suitable - # piece of descriptive text before make_subject() is called. - self.subject = "" + def send(self, subject_line, group, params, long_func, short_func): + writer = Writer(self.get_encoding()) - def make_subject(self, group, params): - prefix = self.cfg.get(self.prefix_param, group, params) - if prefix: - subject = prefix + ' ' + self.subject - else: - subject = self.subject + try: + try: + long_func(writer) + except MessageTooLarge: + writer.buffer.truncate(0) + short_func(writer) + except MessageSendFailure: + return True # failed - try: - truncate_subject = int( - self.cfg.get('truncate_subject', group, params)) - except ValueError: - truncate_subject = 0 + self.deliver(subject_line, group, params, writer.buffer.getvalue()) - # truncate subject as UTF-8 string. - # Note: there still exists an issue on combining characters. - if truncate_subject: - bsubject = to_bytes(subject) - if len(bsubject) > truncate_subject: - idx = truncate_subject - 2 - while b'\x80' <= bsubject[idx-1:idx] <= b'\xbf': - idx -= 1 - subject = to_str(bsubject[:idx-1]) + "..." + return False # succeeded - return subject + def get_encoding(self): + """Get the encoding for text-to-bytes in the output. - def start(self, group, params): - """Override this method. - Begin writing an output representation. GROUP is the name of the - configuration file group which is causing this output to be produced. - PARAMS is a dictionary of any named subexpressions of regular expressions - defined in the configuration file, plus the key 'author' contains the - author of the action being reported.""" - raise NotImplementedError + This will default to UTF-8. If the output mechanism needs a different + encoding, then override this method to provide the custom encoding. + """ + return 'utf-8' - def finish(self): + def deliver(self, subject_line, group, params, body): """Override this method. - Flush any cached information and finish writing the output - representation.""" - raise NotImplementedError - def write_binary(self, output): - """Override this method. - Append the binary data OUTPUT to the output representation.""" - raise NotImplementedError + ### FIX THIS DOCSTRING - def write(self, output): - """Append the literal text string OUTPUT to the output representation.""" - return self.write_binary(to_bytes(output)) - - def run(self, cmd): - """Override this method, if the default implementation is not sufficient. - Execute CMD, writing the stdout produced to the output representation.""" - # By default we choose to incorporate child stderr into the output - pipe_ob = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - close_fds=sys.platform != "win32") - - buf = pipe_ob.stdout.read(self._CHUNKSIZE) - while buf: - self.write_binary(buf) - buf = pipe_ob.stdout.read(self._CHUNKSIZE) + Begin writing an output representation. SUBJECT_LINE is a subject line + describing the action (commit, properties, lock), which may be tweaked + given other conditions. GROUP is the name of the configuration file + group which is causing this output to be produced. PARAMS is a + dictionary of any named subexpressions of regular expressions defined + in the configuration file, plus the key 'author' contains the author + of the action being reported. - # wait on the child so we don't end up with a billion zombies - pipe_ob.wait() + Return bytes() for the prefix of the content to deliver. + """ + raise NotImplementedError class MailedOutput(OutputBase): - def __init__(self, cfg, repos, prefix_param): - OutputBase.__init__(self, cfg, repos, prefix_param) - def start(self, group, params): + def get_prefix(self, subject_line, group, params): # 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) @@ -263,6 +263,9 @@ class MailedOutput(OutputBase): and self.reply_to[2] == ']': self.reply_to = self.reply_to[3:] + # Return the prefix for the mail message. + return self.mail_headers(subject_line, group) + def _rfc2047_encode(self, hdr): # Return the result of splitting HDR into tokens (on space # characters), encoding (per RFC2047) each token as necessary, and @@ -278,10 +281,10 @@ class MailedOutput(OutputBase): return ' '.join(map(_maybe_encode_header, hdr.split())) - def mail_headers(self, group, params): + def mail_headers(self, subject_line, group): from email import utils - subject = self._rfc2047_encode(self.make_subject(group, params)) + subject = self._rfc2047_encode(subject_line) from_hdr = self._rfc2047_encode(self.from_addr) to_hdr = self._rfc2047_encode(', '.join(self.to_addrs)) @@ -303,21 +306,13 @@ class MailedOutput(OutputBase): os.path.basename(self.repos.repos_dir)) if self.reply_to: hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to) - return hdrs + '\n' + return (hdrs + '\n').encode() class SMTPOutput(MailedOutput): "Deliver a mail message to an MTA using SMTP." - def start(self, group, params): - MailedOutput.start(self, group, params) - - self.buffer = BytesIO() - self.write_binary = self.buffer.write - - self.write(self.mail_headers(group, params)) - - def finish(self): + def deliver(self, subject_line, group, params, body): """ Send email via SMTP or SMTP_SSL, logging in if username is specified. @@ -332,6 +327,8 @@ class SMTPOutput(MailedOutput): (to minimize the chances of said lockout). """ + prefix = self.get_prefix(subject_line, group, params) + if self.cfg.is_set('general.smtp_port'): smtp_port = self.cfg.general.smtp_port else: @@ -357,7 +354,7 @@ class SMTPOutput(MailedOutput): # Any error at login is fatal raise - server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue()) + server.sendmail(self.from_addr, self.to_addrs, prefix + body) ### TODO: 'raise .. from' is Python 3+. When we convert this ### script to Python 3, uncomment 'from detail' below @@ -390,35 +387,28 @@ class SMTPOutput(MailedOutput): class StandardOutput(OutputBase): "Print the commit message to stdout." - def __init__(self, cfg, repos, prefix_param): - OutputBase.__init__(self, cfg, repos, prefix_param) - self.write_binary = _stdout.write + def get_encoding(self): + return sys.stdout.encoding if PY3 else 'utf-8' - def start(self, group, params): - self.write("Group: " + (group or "defaults") + "\n") - self.write("Subject: " + self.make_subject(group, params) + "\n\n") - - def finish(self): - pass - - if (PY3 and (codecs.lookup(sys.stdout.encoding) != codecs.lookup('utf-8'))): - def write(self, output): - """Write text as *default* encoding string""" - return self.write_binary(output.encode(sys.stdout.encoding, - 'backslashreplace')) + def deliver(self, subject_line, group, params, body): + _stdout.write(( + ("Group: " + (group or "defaults") + "\n") + + ("Subject: %s\n\n" % (subject_line,)) + ).encode() ### whoops. use the encoding + + body) class PipeOutput(MailedOutput): "Deliver a mail message to an MTA via a pipe." - def __init__(self, cfg, repos, prefix_param): - MailedOutput.__init__(self, cfg, repos, prefix_param) + def __init__(self, cfg, repos): + MailedOutput.__init__(self, cfg, repos) # figure out the command for delivery self.cmd = cfg.general.mail_command.split() - def start(self, group, params): - MailedOutput.start(self, group, params) + def deliver(self, subject_line, group, params, body): + prefix = self.get_prefix(subject_line, group, params) ### gotta fix this. this is pretty specific to sendmail and qmail's ### mailwrapper program. should be able to use option param substitution @@ -427,12 +417,8 @@ class PipeOutput(MailedOutput): # construct the pipe for talking to the mailer self.pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, close_fds=sys.platform != "win32") - self.write_binary = self.pipe.stdin.write - - # start writing out the mail message - self.write(self.mail_headers(group, params)) + self.pipe.write(prefix + body) - def finish(self): # signal that we're done sending content self.pipe.stdin.close() @@ -441,39 +427,58 @@ class PipeOutput(MailedOutput): class Messenger: - def __init__(self, pool, cfg, repos, prefix_param): - self.pool = pool + def __init__(self, cfg, repos, prefix_param): self.cfg = cfg self.repos = repos + self.prefix_param = prefix_param - if cfg.is_set('general.mail_command'): - cls = PipeOutput - elif cfg.is_set('general.smtp_hostname'): - cls = SMTPOutput + # Subclasses should set this instance variable to describe the action + # being performed. See OutputBase.start() docstring. + self.basic_subject = '' + + def make_subject(self, basic_subject, group, params): + prefix = self.cfg.get(self.prefix_param, group, params) + if prefix: + subject = prefix + ' ' + basic_subject else: - cls = StandardOutput + subject = basic_subject - self.output = cls(cfg, repos, prefix_param) + try: + truncate_subject = int( + self.cfg.get('truncate_subject', group, params)) + except ValueError: + truncate_subject = 0 + + # truncate subject as UTF-8 string. + # Note: there still exists an issue on combining characters. + if truncate_subject: + bsubject = to_bytes(subject) + if len(bsubject) > truncate_subject: + idx = truncate_subject - 2 + while b'\x80' <= bsubject[idx-1:idx] <= b'\xbf': + idx -= 1 + subject = to_str(bsubject[:idx-1]) + "..." + + return subject class Commit(Messenger): def __init__(self, pool, cfg, repos): - Messenger.__init__(self, pool, cfg, repos, 'commit_subject_prefix') + Messenger.__init__(self, cfg, repos, 'commit_subject_prefix') # get all the changes and sort by path - editor = svn.repos.ChangeCollector(repos.fs_ptr, repos.root_this, \ - self.pool) - e_ptr, e_baton = svn.delta.make_editor(editor, self.pool) - svn.repos.replay2(repos.root_this, "", svn.core.SVN_INVALID_REVNUM, 1, e_ptr, e_baton, None, self.pool) + editor = svn.repos.ChangeCollector(repos.fs_ptr, repos.root_this, pool) + e_ptr, e_baton = svn.delta.make_editor(editor, pool) + svn.repos.replay2(repos.root_this, "", svn.core.SVN_INVALID_REVNUM, 1, e_ptr, e_baton, None, pool) self.changelist = sorted(editor.get_changes().items()) - log = to_str(repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or b'') + log = to_str(repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG, pool) or b'') # 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, log): + for (group, params) in self.cfg.which_groups(to_str(path), log): # turn the params into a hashable object and stash it away param_list = sorted(params.items()) # collect the set of paths belonging to this group @@ -502,14 +507,13 @@ class Commit(Messenger): commondir, dirlist = get_commondir(dirlist) # compose the basic subject line. later, we can prefix it. - dirlist.sort() - dirlist = ' '.join(dirlist) + dirlist_s = ' '.join(sorted(dirlist)) if commondir: - self.output.subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist) + self.basic_subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist_s) else: - self.output.subject = 'r%d - %s' % (repos.rev, dirlist) + self.basic_subject = 'r%d - %s' % (repos.rev, dirlist_s) - def generate(self): + def generate(self, output, scratch_pool): "Generate email for the various groups and option-params." ### the groups need to be further compressed. if the headers and @@ -518,32 +522,26 @@ class Commit(Messenger): ### 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) - ret = 0 - - # build a renderer, tied to our output stream - renderer = TextCommitRenderer(self.output) + iterpool = svn.core.svn_pool_create(scratch_pool) + failed = False for (group, param_tuple), (params, paths) in sorted(self.groups.items()): - try: - self.output.start(group, params) + subject_line = self.make_subject(self.basic_subject, group, params) + def long_commit(writer): # generate the content for this group and set of params - generate_content(renderer, self.cfg, self.repos, self.changelist, - group, params, paths, subpool) - - self.output.finish() - except MessageSendFailure: - ret = 1 - svn.core.svn_pool_clear(subpool) + generate_content(writer, self.cfg, self.repos, self.changelist, + group, params, paths, iterpool) + failed |= output.send(subject_line, group, params, long_commit, None) + svn.core.svn_pool_clear(iterpool) - svn.core.svn_pool_destroy(subpool) - return ret + svn.core.svn_pool_destroy(iterpool) + return failed class PropChange(Messenger): - def __init__(self, pool, cfg, repos, author, propname, action): - Messenger.__init__(self, pool, cfg, repos, 'propchange_subject_prefix') + def __init__(self, cfg, repos, author, propname, action): + Messenger.__init__(self, cfg, repos, 'propchange_subject_prefix') self.author = author self.propname = propname self.action = action @@ -555,15 +553,18 @@ class PropChange(Messenger): param_list = sorted(params.items()) self.groups[group, tuple(param_list)] = params - self.output.subject = 'r%d - %s' % (repos.rev, propname) + self.basic_subject = 'r%d - %s' % (repos.rev, propname) - def generate(self): + def generate(self, output, scratch_pool): actions = { 'A': 'added', 'M': 'modified', 'D': 'deleted' } - ret = 0 + failed = False + ### maybe create an iterpool? + for (group, param_tuple), params in self.groups.items(): - try: - self.output.start(group, params) - self.output.write('Author: %s\n' + subject_line = self.make_subject(self.basic_subject, group, params) + + def long_propchange(writer): + writer.write('Author: %s\n' 'Revision: %s\n' 'Property Name: %s\n' 'Action: %s\n' @@ -572,27 +573,27 @@ class PropChange(Messenger): actions.get(self.action, 'Unknown (\'%s\')' \ % self.action))) if self.action == 'A' or self.action not in actions: - self.output.write('Property value:\n') - propvalue = self.repos.get_rev_prop(self.propname) - self.output.write(propvalue) + writer.write('Property value:\n') + propvalue = self.repos.get_rev_prop(self.propname, scratch_pool) + writer.write(propvalue) elif self.action == 'M': - self.output.write('Property diff:\n') + writer.write('Property diff:\n') tempfile1 = tempfile.NamedTemporaryFile() tempfile1.write(_stdin.read()) tempfile1.flush() tempfile2 = tempfile.NamedTemporaryFile() - tempfile2.write(self.repos.get_rev_prop(self.propname)) + tempfile2.write(self.repos.get_rev_prop(self.propname, scratch_pool)) tempfile2.flush() - self.output.run(self.cfg.get_diff_cmd(group, { - 'label_from' : 'old property value', - 'label_to' : 'new property value', - 'from' : tempfile1.name, - 'to' : tempfile2.name, - })) - self.output.finish() - except MessageSendFailure: - ret = 1 - return ret + for diffs in generate_diff(self.cfg.get_diff_cmd(group, { + 'label_from' : 'old property value', + 'label_to' : 'new property value', + 'from' : tempfile1.name, + 'to' : tempfile2.name, + })): + writer.write(to_str(diffs.raw)) + failed |= output.send(subject_line, group, params, long_propchange, None) + + return failed def get_commondir(dirlist): @@ -635,7 +636,7 @@ class Lock(Messenger): self.author = author self.do_lock = do_lock - Messenger.__init__(self, pool, cfg, repos, + Messenger.__init__(self, cfg, repos, (do_lock and 'lock_subject_prefix' or 'unlock_subject_prefix')) @@ -659,40 +660,39 @@ class Lock(Messenger): commondir, dirlist = get_commondir(self.dirlist) # compose the basic subject line. later, we can prefix it. - dirlist.sort() - dirlist = ' '.join(dirlist) + dirlist_s = ' '.join(sorted(dirlist)) if commondir: - self.output.subject = '%s: %s' % (commondir, dirlist) + self.basic_subject = '%s: %s' % (commondir, dirlist_s) else: - self.output.subject = '%s' % (dirlist) + self.basic_subject = dirlist_s # The lock comment is the same for all paths, so we can just pull # the comment for the first path in the dirlist and cache it. self.lock = svn.fs.svn_fs_get_lock(self.repos.fs_ptr, to_bytes(self.dirlist[0]), - self.pool) + pool) + + def generate(self, output, scratch_pool): + failed = False - def generate(self): - ret = 0 for (group, param_tuple), (params, paths) in sorted(self.groups.items()): - try: - self.output.start(group, params) + subject_line = self.make_subject(self.basic_subject, group, params) - self.output.write('Author: %s\n' + def long_lock(writer): + writer.write('Author: %s\n' '%s paths:\n' % (self.author, self.do_lock and 'Locked' or 'Unlocked')) self.dirlist.sort() for dir in self.dirlist: - self.output.write(' %s\n\n' % dir) + writer.write(' %s\n\n' % dir) if self.do_lock: - self.output.write('Comment:\n%s\n' % (self.lock.comment or '')) + writer.write('Comment:\n%s\n' % (self.lock.comment or '')) - self.output.finish() - except MessageSendFailure: - ret = 1 - return ret + failed |= output.send(subject_line, group, params, long_lock, None) + + return failed class DiffSelections: @@ -763,16 +763,14 @@ class DiffURLSelections: def get_modify_url(self, repos_rev, change): return self._get_url('modify', repos_rev, change) -def generate_content(renderer, cfg, repos, changelist, group, params, paths, + +def generate_content(writer, cfg, repos, changelist, group, params, paths, pool): - svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE) + svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE, pool) ### pick a different date format? date = time.ctime(svn.core.secs_from_timestr(svndate, pool)) - diffsels = DiffSelections(cfg, group, params) - diffurls = DiffURLSelections(cfg, group, params) - show_nonmatching_paths = cfg.get('show_nonmatching_paths', group, params) \ or 'yes' @@ -781,55 +779,54 @@ def generate_content(renderer, cfg, repo commit_url = cfg.get('commit_url', group, params_with_rev) # figure out the lists of changes outside the selected path-space - other_added_data = other_replaced_data = other_deleted_data = \ - other_modified_data = [ ] if len(paths) != len(changelist) and show_nonmatching_paths != 'no': - other_added_data = generate_list('A', changelist, paths, False) - other_replaced_data = generate_list('R', changelist, paths, False) - other_deleted_data = generate_list('D', changelist, paths, False) - other_modified_data = generate_list('M', changelist, paths, False) + other_summary = generate_summary(changelist, paths, False) + else: + other_summary = None if len(paths) != len(changelist) and show_nonmatching_paths == 'yes': - other_diffs = DiffGenerator(changelist, paths, False, cfg, repos, date, - group, params, diffsels, diffurls, pool) + other_diffs = generate_changelist_diffs(changelist, paths, False, cfg, + repos, date, group, params, + pool) else: other_diffs = None + summary = generate_summary(changelist, paths, True) + data = _data( author=repos.author, date=date, rev=repos.rev, - log=to_str(repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG) or b''), + log=to_str(repos.get_rev_prop(svn.core.SVN_PROP_REVISION_LOG, pool) or b''), commit_url=commit_url, - added_data=generate_list('A', changelist, paths, True), - replaced_data=generate_list('R', changelist, paths, True), - deleted_data=generate_list('D', changelist, paths, True), - modified_data=generate_list('M', changelist, paths, True), + summary=summary, show_nonmatching_paths=show_nonmatching_paths, - other_added_data=other_added_data, - other_replaced_data=other_replaced_data, - other_deleted_data=other_deleted_data, - other_modified_data=other_modified_data, - diffs=DiffGenerator(changelist, paths, True, cfg, repos, date, group, - params, diffsels, diffurls, pool), + other_summary=other_summary, + diffs=generate_changelist_diffs(changelist, paths, True, cfg, repos, + date, group, params, pool), other_diffs=other_diffs, ) - renderer.render(data) + ### clean this up in future rev. Just use wb + w = writer.write + wb = writer.write_binary + render_commit(w, wb, data) + +def generate_summary(changelist, paths, in_paths): + def gather_info(action): + return _gather_paths(action, changelist, paths, in_paths) + return _data( + added=gather_info(svn.repos.CHANGE_ACTION_ADD), + replaced=gather_info(svn.repos.CHANGE_ACTION_REPLACE), + deleted=gather_info(svn.repos.CHANGE_ACTION_DELETE), + modified=gather_info(svn.repos.CHANGE_ACTION_MODIFY), + ) -def generate_list(changekind, changelist, paths, in_paths): - if changekind == 'A': - selection = lambda change: change.action == svn.repos.CHANGE_ACTION_ADD - elif changekind == 'R': - selection = lambda change: change.action == svn.repos.CHANGE_ACTION_REPLACE - elif changekind == 'D': - selection = lambda change: change.action == svn.repos.CHANGE_ACTION_DELETE - elif changekind == 'M': - selection = lambda change: change.action == svn.repos.CHANGE_ACTION_MODIFY +def _gather_paths(action, changelist, paths, in_paths): items = [ ] for path, change in changelist: - if selection(change) and (path in paths) == in_paths: + if change.action == action and (path in paths) == in_paths: item = _data( path=path, is_dir=change.item_kind == svn.core.svn_node_dir, @@ -846,38 +843,14 @@ def generate_list(changekind, changelist return items -class DiffGenerator: - "This is a generator-like object returning DiffContent objects." - - def __init__(self, changelist, paths, in_paths, cfg, repos, date, group, - params, diffsels, diffurls, pool): - self.changelist = changelist - self.paths = paths - self.in_paths = in_paths - self.cfg = cfg - self.repos = repos - self.date = date - self.group = group - self.params = params - self.diffsels = diffsels - self.diffurls = diffurls - self.pool = pool - - self.diff = self.diff_url = None +def generate_changelist_diffs(changelist, paths, in_paths, cfg, repos, + date, group, params, pool): + "This is a generator returning diffs for each change." - self.idx = 0 + diffsels = DiffSelections(cfg, group, params) + diffurls = DiffURLSelections(cfg, group, params) - def __nonzero__(self): - # we always have some items - return True - - def __getitem__(self, idx): - while True: - if self.idx == len(self.changelist): - raise IndexError - - path, change = self.changelist[self.idx] - self.idx = self.idx + 1 + for path, change in changelist: diff = diff_url = None kind = None @@ -894,14 +867,14 @@ class DiffGenerator: continue # is this change in (or out of) the set of matched paths? - if (path in self.paths) != self.in_paths: + if (path in paths) != in_paths: continue if change.base_rev != -1: - svndate = self.repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE, - change.base_rev) + svndate = repos.get_rev_prop(svn.core.SVN_PROP_REVISION_DATE, + pool, change.base_rev) ### pick a different date format? - base_date = time.ctime(svn.core.secs_from_timestr(svndate, self.pool)) + base_date = time.ctime(svn.core.secs_from_timestr(svndate, pool)) else: base_date = '' @@ -915,14 +888,14 @@ class DiffGenerator: kind = 'D' # get the diff url, if any is specified - diff_url = self.diffurls.get_delete_url(self.repos.rev, change) + diff_url = diffurls.get_delete_url(repos.rev, change) # show the diff? - if self.diffsels.delete: - diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), - base_path_bytes, None, None, self.pool) + if diffsels.delete: + diff = svn.fs.FileDiff(repos.get_root(change.base_rev), + base_path_bytes, None, None, pool) - label1 = '%s\t%s\t(r%s)' % (base_path, self.date, change.base_rev) + label1 = '%s\t%s\t(r%s)' % (base_path, date, change.base_rev) label2 = '/dev/null\t00:00:00 1970\t(deleted)' singular = True @@ -936,30 +909,30 @@ class DiffGenerator: kind = 'W' # get the diff url, if any is specified - diff_url = self.diffurls.get_copy_url(self.repos.rev, change) + diff_url = diffurls.get_copy_url(repos.rev, change) # show the diff? - if self.diffsels.modify: - diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), + if diffsels.modify: + diff = svn.fs.FileDiff(repos.get_root(change.base_rev), base_path_bytes, - self.repos.root_this, change.path, - self.pool) + repos.root_this, change.path, + pool) label1 = ('%s\t%s\t(r%s, copy source)' % (base_path, base_date, change.base_rev)) label2 = ('%s\t%s\t(r%s)' - % (to_str(change.path), self.date, self.repos.rev)) + % (to_str(change.path), date, repos.rev)) singular = False else: # this file was copied. kind = 'C' - if self.diffsels.copy: - diff = svn.fs.FileDiff(None, None, self.repos.root_this, - change.path, self.pool) + if diffsels.copy: + diff = svn.fs.FileDiff(None, None, repos.root_this, + change.path, pool) 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)' % (to_str(change.path), - self.date, self.repos.rev, change.base_rev, + date, repos.rev, change.base_rev, base_path)) singular = False else: @@ -967,16 +940,16 @@ class DiffGenerator: kind = 'A' # get the diff url, if any is specified - diff_url = self.diffurls.get_add_url(self.repos.rev, change) + diff_url = diffurls.get_add_url(repos.rev, change) # show the diff? - if self.diffsels.add: - diff = svn.fs.FileDiff(None, None, self.repos.root_this, - change.path, self.pool) + if diffsels.add: + diff = svn.fs.FileDiff(None, None, repos.root_this, + change.path, pool) label1 = '/dev/null\t00:00:00 1970\t' \ '(empty, because file is newly added)' label2 = '%s\t%s\t(r%s)' \ - % (to_str(change.path), self.date, self.repos.rev) + % (to_str(change.path), date, repos.rev) singular = True elif not change.text_changed: @@ -987,18 +960,18 @@ class DiffGenerator: kind = 'M' # get the diff url, if any is specified - diff_url = self.diffurls.get_modify_url(self.repos.rev, change) + diff_url = diffurls.get_modify_url(repos.rev, change) # show the diff? - if self.diffsels.modify: - diff = svn.fs.FileDiff(self.repos.get_root(change.base_rev), + if diffsels.modify: + diff = svn.fs.FileDiff(repos.get_root(change.base_rev), base_path, - self.repos.root_this, change.path, - self.pool) + repos.root_this, change.path, + pool) label1 = '%s\t%s\t(r%s)' \ % (base_path, base_date, change.base_rev) label2 = '%s\t%s\t(r%s)' \ - % (to_str(change.path), self.date, self.repos.rev) + % (to_str(change.path), date, repos.rev) singular = False if diff: @@ -1006,20 +979,16 @@ class DiffGenerator: if binary: content = src_fname = dst_fname = None else: - src_fname, dst_fname = diff.get_files() - try: - content = DiffContent(self.cfg.get_diff_cmd(self.group, { + src_fname, dst_fname = diff.get_files() + content = generate_diff(cfg.get_diff_cmd(group, { 'label_from' : label1, 'label_to' : label2, 'from' : src_fname, 'to' : dst_fname, })) - except OSError: - # diff command does not exist, try difflib.unified_diff() - content = DifflibDiffContent(label1, label2, src_fname, dst_fname) # return a data item for this diff - return _data( + yield _data( path=change.path, base_path=base_path_bytes, base_rev=change.base_rev, @@ -1035,6 +1004,7 @@ class DiffGenerator: content=content, ) + def _classify_diff_line(line, seen_change): # classify the type of line. first = line[:1] @@ -1063,78 +1033,32 @@ def _classify_diff_line(line, seen_chang return line, ltype, seen_change -class DiffContent: - "This is a generator-like object returning annotated lines of a diff." - - def __init__(self, cmd): - self.seen_change = False +def generate_diff(cmd): + seen_change = False # By default we choose to incorporate child stderr into the output - self.pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - close_fds=sys.platform != "win32") - - def __nonzero__(self): - # we always have some items - return True - - def __getitem__(self, idx): - if self.pipe is None: - raise IndexError - - line = self.pipe.stdout.readline() - if not line: - # wait on the child so we don't end up with a billion zombies - self.pipe.wait() - self.pipe = None - raise IndexError - - line, ltype, self.seen_change = _classify_diff_line(line, self.seen_change) - return _data( - raw=line, - text=line[1:-1], # remove indicator and newline - type=ltype, - ) - -class DifflibDiffContent(): - "This is a generator-like object returning annotated lines of a diff." - - def __init__(self, label_from, label_to, from_file, to_file): - import difflib - self.seen_change = False - fromlines = open(from_file, 'U').readlines() - tolines = open(to_file, 'U').readlines() - self.diff = difflib.unified_diff(fromlines, tolines, - label_from, label_to) - - def __nonzero__(self): - # we always have some items - return True + pipe = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + close_fds=sys.platform != "win32") - def __getitem__(self, idx): - - try: - line = self.diff.next() - except StopIteration: - raise IndexError - - line, ltype, self.seen_change = _classify_diff_line(line, self.seen_change) - return _data( - raw=line, - text=line[1:-1], # remove indicator and newline - type=ltype, - ) - -class TextCommitRenderer: - "This class will render the commit mail in plain text." - - def __init__(self, output): - self.output = output + while True: + line = pipe.stdout.readline() + if not line: + # wait on the child so we don't end up with a billion zombies + pipe.wait() + return # will raise StopIteration + + line, ltype, seen_change = _classify_diff_line(line, seen_change) + yield _data( + raw=line, + text=line[1:-1], # remove indicator and newline + type=ltype, + ) - def render(self, data): - "Render the commit defined by 'data'." - w = self.output.write +def render_commit(w, wb, data): + "Call W and/or WB to render the commit defined by DATA." w('Author: %s\nDate: %s\nNew Revision: %s\n' % (data.author, data.date, @@ -1148,33 +1072,33 @@ class TextCommitRenderer: w('Log:\n%s\n\n' % data.log.strip()) # print summary sections - self._render_list('Added', data.added_data) - self._render_list('Replaced', data.replaced_data) - self._render_list('Deleted', data.deleted_data) - self._render_list('Modified', data.modified_data) + _render_summary(w, data.summary) - if data.other_added_data or data.other_replaced_data \ - or data.other_deleted_data or data.other_modified_data: + if data.other_summary: if data.show_nonmatching_paths: w('\nChanges in other areas also in this revision:\n') - self._render_list('Added', data.other_added_data) - self._render_list('Replaced', data.other_replaced_data) - self._render_list('Deleted', data.other_deleted_data) - self._render_list('Modified', data.other_modified_data) + _render_summary(w, data.other_summary) else: w('and changes in other areas\n') - self._render_diffs(data.diffs, '') + _render_diffs(w, wb, data.diffs, '') if data.other_diffs: - self._render_diffs(data.other_diffs, + _render_diffs(w, wb, data.other_diffs, '\nDiffs of changes in other areas also' ' in this revision:\n') - def _render_list(self, header, data_list): + +def _render_summary(w, summary): + _render_list(w, 'Added', summary.added) + _render_list(w, 'Replaced', summary.replaced) + _render_list(w, 'Deleted', summary.deleted) + _render_list(w, 'Modified', summary.modified) + + +def _render_list(w, header, data_list): if not data_list: return - w = self.output.write w(header + ':\n') for d in data_list: if d.is_dir: @@ -1199,12 +1123,13 @@ class TextCommitRenderer: w(' - copied%s from r%d, %s%s\n' % (text, d.base_rev, to_str(d.base_path), is_dir)) - def _render_diffs(self, diffs, section_header): + +def _render_diffs(w, wb, diffs, section_header): """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 for diff in diffs: @@ -1244,7 +1169,6 @@ class TextCommitRenderer: w('Binary file (source and/or target). No diff available.\n') continue - wb = self.output.write_binary for line in diff.content: wb(line.raw) @@ -1255,7 +1179,9 @@ class Repository: def __init__(self, repos_dir, rev, pool): self.repos_dir = repos_dir self.rev = rev - self.pool = pool + + # Any data that we HOLD will be allocated in this pool. + self.hold_pool = pool self.repos_ptr = svn.repos.open(repos_dir, pool) self.fs_ptr = svn.repos.fs(self.repos_ptr) @@ -1264,21 +1190,21 @@ class Repository: self.root_this = self.get_root(rev) - self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR) + self.author = self.get_rev_prop(svn.core.SVN_PROP_REVISION_AUTHOR, pool) if self.author is not None: self.author = to_str(self.author) - def get_rev_prop(self, propname, rev = None): + def get_rev_prop(self, propname, scratch_pool, rev=None): if not rev: rev = self.rev - return svn.fs.revision_prop(self.fs_ptr, rev, propname, self.pool) + return svn.fs.revision_prop(self.fs_ptr, rev, propname, scratch_pool) def get_root(self, rev): try: return self.roots[rev] except KeyError: pass - root = self.roots[rev] = svn.fs.revision_root(self.fs_ptr, rev, self.pool) + root = self.roots[rev] = svn.fs.revision_root(self.fs_ptr, rev, self.hold_pool) return root @@ -1475,9 +1401,9 @@ class Config: "Return the path's associated groups." groups = [] for group, pattern, exclude_pattern, repos_params, search_logmsg_re in self._group_re: - match = pattern.match(to_str(path)) + match = pattern.match(path) if match: - if exclude_pattern and exclude_pattern.match(to_str(path)): + if exclude_pattern and exclude_pattern.match(path): continue params = repos_params.copy() params.update(match.groupdict()) @@ -1518,6 +1444,8 @@ class UnknownSubcommand(Exception): pass class MessageSendFailure(Exception): pass +class MessageTooLarge(Exception): + pass if __name__ == '__main__': @@ -1579,9 +1507,10 @@ if the property was added, modified or d if not os.path.exists(config_fname): raise MissingConfig(config_fname) - ret = svn.core.run_app(main, cmd, config_fname, repos_dir, - sys.argv[3:3+expected_args]) - sys.exit(1 if ret else 0) + failed = svn.core.run_app(main, cmd, config_fname, repos_dir, + sys.argv[3:3+expected_args]) + sys.exit(1 if failed else 0) + # ------------------------------------------------------------------------ # TODO @@ -1600,3 +1529,13 @@ if the property was added, modified or d # o look up authors (username -> email; for the From: header) in a # file(s) or DBM # * get rid of global functions that should properly be class methods + +# +# For Emacs, we want to move towards the standard 4-space indent. It +# inspects the current formatting of this file, and sets 2-space. +# Override that with a 4-space indent. +# +# Local Variables: +# python-indent-offset: 4 +# End: +#
Propchange: subversion/branches/pristine-checksum-salt/tools/hook-scripts/mailer/tests/ ------------------------------------------------------------------------------ --- svn:ignore (added) +++ svn:ignore Thu Feb 1 20:04:07 2024 @@ -0,0 +1,3 @@ +mailer-t1.current +mailer-init.* +t2-root