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



Reply via email to