Index: commit-email.pl.in
===================================================================
--- commit-email.pl.in	(revision 21117)
+++ commit-email.pl.in	(working copy)
@@ -128,6 +128,9 @@
 # Use the reference to the first project to populate.
 my $current_project = $project_settings_list[0];
 
+# Size limit for the email (in bytes); default of 0 means no limit
+my $size_limit = 0;
+
 # This hash matches the command line option to the hash key in the
 # project.  If a key exists but has a false value (''), then the
 # command line option is allowed but requires special handling.
@@ -139,6 +142,7 @@
                        '-m'     => '',
                        '-r'     => 'reply_to',
                        '-s'     => 'subject_prefix',
+                       '--size-limit' => '',
                        '--diff' => '');
 
 while (@ARGV)
@@ -201,6 +205,16 @@
               {
                 $current_project->{show_diff} = parse_boolean($value);
               }
+            elsif ($arg eq '--size-limit')
+              {
+                $size_limit = $value;
+                # Validate the specified diff size limit.
+                if ($size_limit ne '' and $size_limit ne -1 and $size_limit !~ /^\d+$/)
+                  {
+                    die "$0: --size-limit takes only a positive integer or -1"
+                        . " argument; \"$size_limit\" is neither\n";
+                  }
+              }
             else
               {
                 die "$0: internal error:"
@@ -288,8 +302,8 @@
 # Harvest common data needed for both commit or revprop-change.
 
 # Figure out what directories have changed using svnlook.
-my @dirschanged = &read_from_process($svnlook, 'dirs-changed', $repos,
-                                     '-r', $rev);
+my ($ignore_size_1, @dirschanged) = &read_from_process(0, $svnlook, 'dirs-changed', $repos,
+                                                     '-r', $rev);
 
 # Lose the trailing slash in the directory names if one exists, except
 # in the case of '/'.
@@ -307,7 +321,8 @@
   }
 
 # Figure out what files have changed using svnlook.
-my @svnlooklines = &read_from_process($svnlook, 'changed', $repos, '-r', $rev);
+my ($ignore_size_2, @svnlooklines) = &read_from_process(0, $svnlook, 'changed',
+                                                      $repos, '-r', $rev);
 
 # Parse the changed nodes.
 my @adds;
@@ -353,7 +368,7 @@
     # Harvest data using svnlook.
 
     # Get the author, date, and log from svnlook.
-    my @infolines = &read_from_process($svnlook, 'info', $repos, '-r', $rev);
+    my ($ignore_size_3, @infolines) = &read_from_process(0, $svnlook, 'info', $repos, '-r', $rev);
     $author = shift @infolines;
     my $date = shift @infolines;
     shift @infolines;
@@ -463,8 +478,9 @@
       }
     else
       {
-        @svnlines = &read_from_process($svnlook, 'propget', '--revprop', '-r',
-                                       $rev, $repos, $propname);
+        my $ignore_size_4;
+        ($ignore_size_4, @svnlines) = &read_from_process(0, $svnlook, 'propget', '--revprop', '-r',
+                                                       $rev, $repos, $propname);
       }
 
     ######################################################################
@@ -487,7 +503,23 @@
 
 # Cached information - calculated when first needed.
 my @difflines;
+my $diff_howto = "Use \"svn diff -r " . ($rev - 1) . ":$rev\" to view diff.\n";
 
+# Work out how many bytes we have available for the diff.
+my $size_avail = 0;
+if ($size_limit > 0)
+  {
+    my $bodylen = 0;
+    for (@body) { $bodylen += length($_); }
+    $size_avail = $size_limit - $bodylen;
+
+    if ($size_avail <= 0)
+      {
+        @difflines = ( "Diff skipped; message reached limit of $size_limit"
+                       . " bytes with list of changed paths.\n$diff_howto" );
+      }
+  }
+
 # Go through each project and see if there are any matches for this
 # project.  If so, send the log out.
 foreach my $project (@project_settings_list)
@@ -572,15 +604,27 @@
 
     push(@head, "\n");
 
-    if ($diff_wanted and not @difflines)
+    if ($size_limit eq -1 and not @difflines)
       {
+        @difflines = ( $diff_howto );
+      }
+    elsif ($diff_wanted and not @difflines)
+      {
         # Get the diff from svnlook.
         my @no_diff_deleted = $no_diff_deleted ? ('--no-diff-deleted') : ();
         my @no_diff_added = $no_diff_added ? ('--no-diff-added') : ();
-        @difflines = &read_from_process($svnlook, 'diff', $repos,
-                                        '-r', $rev, @no_diff_deleted,
-                                        @no_diff_added);
+        my $numbytes;
+        ($numbytes, @difflines) = &read_from_process($size_avail, $svnlook,
+                                                     'diff', $repos, '-r', $rev,
+                                                     @no_diff_deleted,
+                                                     @no_diff_added);
         @difflines = map { /[\r\n]+$/ ? $_ : "$_\n" } @difflines;
+
+        # If the diff is larger than the remaining size limit, we must discard it.
+        if ($numbytes == -1) {
+          @difflines = ( "Including diff would make mail exceed size limit of"
+                         . " $size_limit bytes.\n$diff_howto" );
+          }
       }
 
     if (defined $sendmail and @email_addresses)
@@ -657,6 +701,9 @@
       "  -m regex              Regular expression to match committed path\n",
       "  -r email_address      Email address for 'Reply-To:'\n",
       "  -s subject_prefix     Subject line prefix\n",
+      "  --size-limit limit    Message size limit in bytes (positive\n",
+      "                        integer); if message exceeds limit, diff is\n",
+      "                        omitted; if set to -1, diff is never sent\n",
       "  --diff y|n            Include diff in message (default: y)\n",
       "                        (applies to commit mode only)\n",
       "\n",
@@ -710,6 +757,10 @@
 }
 
 # Start a child process safely without using /bin/sh.
+#
+# We take a parameter, $limit, which if greater than zero will limit the
+# amount we read -- this is a hack to avoid OOM errors.   If we return
+# $read_size == -1, we exceeded the limit.
 sub safe_read_from_pipe
 {
   unless (@_)
@@ -717,6 +768,7 @@
       croak "$0: safe_read_from_pipe passed no arguments.\n";
     }
 
+  my $limit = shift @_;
   my $openfork_available = $^O ne "MSWin32"; 
   if ($openfork_available) # We can fork on this system.
     {
@@ -750,9 +802,17 @@
         or die "$0: cannot pipe to command: $!\n";
     }
   my @output;
+  my $read_size = 0;
   while (<SAFE_READ>)
     {
       s/[\r\n]+$//;
+      $read_size += length;
+      if (($limit > 0) and ($read_size > $limit))
+      {
+        $read_size = -1;
+        @output = ("output size exceeds specified limit of " . $limit);
+        last;
+      }
       push(@output, $_);
     }
   close(SAFE_READ);
@@ -766,7 +826,7 @@
     }
   if (wantarray)
     {
-      return ($result, @output);
+      return ($result, $read_size, @output);
     }
   else
     {
@@ -776,20 +836,20 @@
 
 # Use safe_read_from_pipe to start a child process safely and return
 # the output if it succeeded or an error message followed by the output
-# if it failed.
+# if it failed.  Returns number of bytes read and the output.
 sub read_from_process
 {
   unless (@_)
     {
       croak "$0: read_from_process passed no arguments.\n";
     }
-  my ($status, @output) = &safe_read_from_pipe(@_);
-  if ($status)
+  my ($status, $read_size, @output) = &safe_read_from_pipe(@_);
+  if ($read_size >= 0 and $status)
     {
       return ("$0: `@_' failed with this output:", @output);
     }
   else
     {
-      return @output;
+      return ($read_size, @output);
     }
 }
