2 ===================================================================
3 --- cvsspam.conf (.../tags/RELEASE-0_2_12) (revision 269)
4 +++ cvsspam.conf (.../trunk) (revision 269)
7 # When $jiraURL is given, text of the form 'project-1234' will be linked
8 # to this issue in JIRA.
10 +# When $xplannerStoryURL, $xplannerIterationURL and $xplannerProjectURL are
11 +# given, text of the form XS1234 will be linked to XPlanner stories; text of
12 +# the form XI1234 will be linked to XPlanner iterations; and text of the form
13 +# XP1234 will be linked to XPlanner projects.
15 #$bugzillaURL = "http://bugzilla.mozilla.org/show_bug.cgi?id=%s"
17 #$jiraURL = "http://jira.atlassian.com/secure/ViewIssue.jspa?key=%s"
19 +#$xplannerStoryURL = "http://www.example.com/xplanner/do/view/userstory?oid=%s"
20 +#$xplannerIterationURL = "http://www.example.com/xplanner/do/view/iteration?oid=%s"
21 +#$xplannerProjectURL = "http://www.example.com/xplanner/do/view/project?oid=%s"
23 # Link to Wiki systems
27 #$cvswebURL = "http://localhost/cgi-bin/cvsweb.cgi"
29 +#$tracURL = "http://localhost/trac/project"
32 # Additional SMTP Headers (Optional)
34 #$diff_ignore_keywords = true
37 +# cvsdiff whitespace ignoring (Default: show whitespace-only changes)
39 +# Whitespace-only changes can distract from the rest of a diff. Set this
40 +# value to true to exclude changes in the amount of whitespace (adds the -b
41 +# option to cvs diff).
43 +$diff_ignore_whitespace = true
46 # $no_removed_file_diff and $no_added_file_diff
48 # Set both these options, and emails will only include diffs for files
50 # Allows the specification of a character set for all generated emails.
51 # The files CVS is dealing with should already be in the character set you
52 # specify -- no transcoding is done.
54 +# Note that you can override this with --charset argument per module, etc.
56 #$charset="ISO-8859-1"
60 +# Users file (Default: $CVSROOT/CVSROOT/users)
62 +# Specify users file to lookup From addresses for commites
64 +#$users_file = "/srv/svn/users"
66 +# Users file charset (Default: $charset)
68 +# If the users file is encoded differently than $charset, You can override
69 +# it here. Especially useful if you use --charset argument. See above.
71 +#$users_file_charset = "ISO-8859-1"
74 # File names in Subject (Default: no filenames in Subject)
76 # Some people like file names to appear in the email subject. To make
77 # them happy, you can say $files_in_subject = true here.
79 #$files_in_subject = false
82 +# Module Path email header (Default: no X-CVSspam-Module-Path header)
84 +# Sets 'X-CVSspam-Module-Path' header to contain common path of files commited.
85 +# Useful for server side mail filtering.
87 +#$cvsroot_email_header = true
89 +# Email size limit (Default: around 2MB)
91 +# When large changes are committed, large CVSspam emails can result. Here
92 +# you can set the size of email that CVSspam is not allowed to append any
93 +# more diffs onto. Specify the number of bytes.
95 +#$mail_size_limit = 2097152
97 Property changes on: cvsspam.conf
98 ___________________________________________________________________
99 Deleted: svn:executable
102 Index: collect_diffs.rb
103 ===================================================================
104 --- collect_diffs.rb (.../tags/RELEASE-0_2_12) (revision 269)
105 +++ collect_diffs.rb (.../trunk) (revision 269)
107 $dirtemplate = "#cvsspam.#{Process.getpgrp}.#{Process.uid}"
111 + safe_from = make_fromaddr_safe_for_filename($from_address)
112 + Dir["#{$tmpdir}/#{$dirtemplate}.#{safe_from}-*"].each do |dir|
113 + stat = File.stat(dir)
114 + return dir if stat.owned?
117 Dir["#{$tmpdir}/#{$dirtemplate}-*"].each do |dir|
118 stat = File.stat(dir)
119 return dir if stat.owned?
124 +# transform any special / unexpected characters appearing in the argument to
125 +# --from so that they will not cause problems if the value is inserted into
126 +# a file or directory name
127 +def make_fromaddr_safe_for_filename(addr)
128 + addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
134 $stderr.puts "collect_diffs.rb: #{msg}"
138 while i < cvs_info.length
139 - changes << ChangeInfo.new(cvs_info[i], cvs_info[i+=1], cvs_info[i+=1])
140 + change_file = cvs_info[i]
141 + # It's been reported,
142 + # http://lists.badgers-in-foil.co.uk/pipermail/cvsspam-devel/2005-September/000380.html
143 + # that sometimes the second revision number that CVS gives us contains a
144 + # trailing newline character, so we strip ws from these values before use,
145 + change_from = cvs_info[i+=1].strip
146 + change_to = cvs_info[i+=1].strip
147 + changes << ChangeInfo.new(change_file, change_from, change_to)
153 diff_cmd = Array.new << $cvs_prog << "-nq" << "diff" << "-Nu"
154 diff_cmd << "-kk" if $diff_ignore_keywords
155 + diff_cmd << "-b" if $diff_ignore_whitespace
166 $diff_ignore_keywords = false
167 +$diff_ignore_whitespace = false
170 unless ENV.has_key?('CVSROOT')
173 $config = arg if opt=="--config"
174 $debug = true if opt == "--debug"
175 + $from_address = arg if opt == "--from"
178 blah("CVSROOT is #{ENV['CVSROOT']}")
179 Index: record_lastdir.rb
180 ===================================================================
181 --- record_lastdir.rb (.../tags/RELEASE-0_2_12) (revision 269)
182 +++ record_lastdir.rb (.../trunk) (revision 269)
184 # http://www.badgers-in-foil.co.uk/projects/cvsspam/
185 # Copyright (c) David Holroyd
187 -$repositorydir = ARGV.shift
189 $tmpdir = ENV["TMPDIR"] || "/tmp"
196 +# transform any special / unexpected characters appearing in the argument to
197 +# --from so that they will not cause problems if the value is inserted into
198 +# a file or directory name
199 +def make_fromaddr_safe_for_filename(addr)
200 + addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
203 +# Option processing doesn't use GetoptLong (for the moment) bacause arguments
204 +# given to this script by CVS include the names of committed files. It
205 +# seems quite possible that one of those file names could begin with a '-'
206 +# and therefore be treated by GetoptLong as a value which requires processing.
207 +# This would probably result in an error.
209 +# [That could be worked around by placing a '--' option (which tells GetoptLong
210 +# to stop processing option arguments) at the very end of the arguments to
211 +# record_lastdir.rb in commitinfo, but that's very easily forgotten, and isn't
212 +# really backwards compatable with the behaviour of older CVSspam releases.]
213 +if ARGV.first == "--from"
214 + # we could, of course, be tricked, if the first committed file in the list
215 + # happened to be named '--from' :S
217 + # drop the "--from"
219 + # and use the value which was given following the option,
220 + $dirtemplate << "." << make_fromaddr_safe_for_filename(ARGV.shift)
223 +$repositorydir = ARGV.shift
225 $datadir = find_data_dir()
229 Property changes on: TODO
230 ___________________________________________________________________
231 Deleted: svn:executable
235 ===================================================================
237 Property changes on: COPYING
238 ___________________________________________________________________
239 Deleted: svn:executable
243 ===================================================================
244 --- CREDITS (.../tags/RELEASE-0_2_12) (revision 269)
245 +++ CREDITS (.../trunk) (revision 269)
257 Index: cvsspam-doc.xml
258 ===================================================================
259 --- cvsspam-doc.xml (.../tags/RELEASE-0_2_12) (revision 269)
260 +++ cvsspam-doc.xml (.../trunk) (revision 269)
262 </screen></informalexample>
269 + <para>For Gforge, when a CVS log comment contains text like <userinput>Fix
270 + for Bug [#123]</userinput>, or <userinput>Task [T456] ...</userinput>, the
271 + text "[#123]" or "[T456]" will become a hyper-link to that Gforge page in
272 + the generated email. The format [#<replaceable>nnn</replaceable>] and
273 + [T<replaceable>nnn</replaceable>] is taken from the existing plugin for
274 + Gforge called cvstracker.</para>
276 + <para>To enable, give your Gforge's URL in CVSspam's configuration file:
277 +<informalexample><screen>$gforgeBugURL = "http://gforge.org/tracker/index.php?func=detail&aid=%s"
278 +$gforgeTaskURL = "http://gforge.org/pm/task.php?func=detailtask&project_task_id=%s"</screen></informalexample>
279 + The marker %s tells CVSspam where in the URL to put the bugId from the
280 + log message.</para>
284 <section><title>CVS Web Frontends</title>
286 Property changes on: cvsspam-doc.xml
287 ___________________________________________________________________
288 Deleted: svn:executable
292 ===================================================================
293 --- cvsspam.rb (.../tags/RELEASE-0_2_12) (revision 269)
294 +++ cvsspam.rb (.../trunk) (revision 269)
301 $maxSubjectLength = 200
302 $maxLinesPerDiff = 1000
307 -# NB must ensure the time is UTC
308 -# (the Ruby Time object's strftime() doesn't supply a numeric timezone)
309 -DATE_HEADER_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
311 # Perform (possibly) multiple global substitutions on a string.
312 # the regexps given as keys must not use capturing subexpressions '(...)'
317 hash.each do |key,val|
318 - if expr == nil ; expr="(" else expr<<"|(" end
319 + if expr == nil ; expr="(" else expr << "|(" end
324 UNDERSCORE = chr("_")
330 # encode a header value according to the RFC-2047 quoted-printable spec,
331 # allowing non-ASCII characters to appear in header values, and wrapping
333 # return a string representing the given character-code in quoted-printable
335 def quoted_encode_char(b)
336 - if b>126 || b==UNDERSCORE || b==TAB
337 - sprintf("=%02x", b)
338 + if b>126 || b==UNDERSCORE || b==TAB || b==HOOK || b==EQUALS
339 + sprintf("=%02X", b)
345 # gives a string starting "=?", and including a charset specification, that
346 # marks the start of a quoted-printable character sequence
347 - def marker_start_quoted
348 - "=?#{@charset}?#{@encoding}?"
349 + def marker_start_quoted(charset=nil)
350 + charset = @charset if charset.nil?
351 + "=?#{charset}?#{@encoding}?"
354 # test to see of the given string contains non-ASCII characters
359 + @fromVer = @toVer = nil
360 @lineAdditions = @lineRemovals = 0
361 @repository = Repository.get(path)
362 @repository.merge_common_prefix(basedir())
364 # TODO: consolidate these into a nicer framework,
365 mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" }
366 urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" }
367 +gforgeTaskSub = proc { |match|
368 + match =~ /([0-9]+)/
369 + "<a href=\"#{$gforgeTaskURL.sub(/%s/, $1)}\">#{match}</a>"
371 +gforgeBugSub = proc { |match|
372 + match =~ /([0-9]+)/
373 + "<a href=\"#{$gforgeBugURL.sub(/%s/, $1)}\">#{match}</a>"
375 bugzillaSub = proc { |match|
377 "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>"
378 @@ -544,11 +553,27 @@
380 "<a href=\"#{$ticketURL.sub(/%s/, $1)}\">#{match}</a>"
382 +issueSub = proc { |match|
383 + match =~ /([0-9]+)/
384 + "<a href=\"#{$issueURL.sub(/%s/, $1)}\">#{match}</a>"
386 wikiSub = proc { |match|
387 - match =~ /\[\[(.*)\]\]/
388 + match =~ /\[\[(.*?)\]\]/
390 "<a href=\"#{$wikiURL.sub(/%s/, urlEncode(raw))}\">[[#{raw}]]</a>"
392 +xplannerIterationSub = proc { |match|
393 + match =~ /([0-9]+)/
394 + "<a href=\"#{$xplannerIterationURL.sub(/%s/, $1)}\">#{match}</a>"
396 +xplannerProjectSub = proc { |match|
397 + match =~ /([0-9]+)/
398 + "<a href=\"#{$xplannerProjectURL.sub(/%s/, $1)}\">#{match}</a>"
400 +xplannerStorySub = proc { |match|
401 + match =~ /([0-9]+)/
402 + "<a href=\"#{$xplannerStoryURL.sub(/%s/, $1)}\">#{match}</a>"
404 commentSubstitutions = {
405 '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub,
406 '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub
412 + # may be overridden by subclasses that are able to make a hyperlink to a
413 + # history log for a file
419 # Superclass for objects that can link to CVS frontends on the web (ViewCVS,
421 "<a href=\"#{diff_url(file)}\">#{super(file)}</a>"
425 + link = log_url(file)
427 + return "<span id=\"info\">(<a href=\"#{link}\">log</a>)</span>"
448 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=#{file.fromVer}&r2=#{file.toVer}")
453 + log_anchor = "#rev#{file.toVer}"
457 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
461 # Link to Chora, from the Horde framework
463 class CVSwebFrontend < WebFrontend
464 def path_url(path, tag)
466 - add_repo(@base_url + urlEncode(path))
467 + add_repo(@base_url + urlEncode(path) + "/")
469 - add_repo("#{@base_url}#{urlEncode(path)}?only_with_tag=#{urlEncode(tag)}")
470 + add_repo("#{@base_url}#{urlEncode(path)}/?only_with_tag=#{urlEncode(tag)}")
476 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=text&tr1=#{file.fromVer}&r2=text&tr2=#{file.toVer}&f=h")
483 + log_anchor = "#rev#{file.toVer}"
487 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
492 +class TracFrontend < WebFrontend
493 + def path_url(path, tag)
494 + add_repo("#{@base_url}browser/#{urlEncode(path)}")
497 + def version_url(path, version)
498 + add_repo("#{@base_url}browser/#{urlEncode(path)}?rev=#{version}")
502 + add_repo("#{@base_url}changeset/#{file.toVer}")
509 + log_anchor = "?rev=#{file.toVer}"
513 + add_repo("#{@base_url}log/#{urlEncode(file.path)}#{log_anchor}")
517 # in need of refactoring...
519 # Note when LogReader finds record of a file that was added in this commit
523 if @truncatedLineCount>0
524 - println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partialy shown]</strong>")
525 + println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partially shown]</strong>")
529 @@ -1181,7 +1269,7 @@
531 # an RFC 822 email address
533 - def initialize(text)
534 + def initialize(text, charset=nil)
535 if text =~ /^\s*([^<]+?)\s*<\s*([^>]+?)\s*>\s*$/
538 @@ -1189,9 +1277,10 @@
545 - attr_accessor :personal_name, :address
546 + attr_accessor :personal_name, :address, :charset
548 def has_personal_name?
549 return !@personal_name.nil?
550 @@ -1222,7 +1311,7 @@
551 # rfc2047 encode the word, if it contains non-ASCII characters
552 def encode_word(word)
553 if $encoder.requires_rfc2047?(word)
554 - encoded = $encoder.marker_start_quoted
555 + encoded = $encoder.marker_start_quoted(@charset)
556 $encoder.each_char_encoded(word) do |code|
559 @@ -1237,6 +1326,7 @@
560 cvsroot_dir = "#{ENV['CVSROOT']}/CVSROOT"
561 $config = "#{cvsroot_dir}/cvsspam.conf"
562 $users_file = "#{cvsroot_dir}/users"
563 +$users_file_charset = nil
566 $recipients = Array.new
567 @@ -1245,14 +1335,21 @@
568 $no_removed_file_diff = false
569 $no_added_file_diff = false
571 -$task_keywords = ['TODO', 'FIXME']
572 +$task_keywords = ['TODO', 'FIXME', 'FIXIT', 'todo']
575 +$gforgeTaskURL = nil
581 +$xplannerIterationURL = nil
582 +$xplannerProjectURL = nil
583 +$xplannerStoryURL = nil
589 $files_in_subject = false;
590 @@ -1261,6 +1358,7 @@
591 # 2MiB limit on attached diffs,
592 $mail_size_limit = 1024 * 1024 * 2
594 +$cvsroot_email_header = false
598 @@ -1321,6 +1419,8 @@
599 blah("Config file '#{$config}' not found, ignoring")
602 +blah("Users file: '#{$users_file}'")
604 unless $arg_charset.nil?
605 $charset = $arg_charset
607 @@ -1337,6 +1437,9 @@
608 elsif $cvswebURL !=nil
609 $cvswebURL << "/" unless $cvswebURL =~ /\/$/
610 $frontend = CVSwebFrontend.new($cvswebURL)
611 +elsif $tracURL !=nil
612 + $tracURL << "/" unless $tracURL =~ /\/$/
613 + $frontend = TracFrontend.new($tracURL)
615 $frontend = NoFrontend.new
617 @@ -1353,17 +1456,35 @@
620 if $bugzillaURL != nil
621 - commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugzillaSub
622 + commentSubstitutions['\b[Bb](?:[Uu][Gg])?\s*[#:]?\s*\[?[0-9]+\]?'] = bugzillaSub
624 +if $gforgeBugURL != nil
625 + commentSubstitutions['\B\[#[0-9]+\]'] = gforgeBugSub
627 +if $gforgeTaskURL != nil
628 + commentSubstitutions['\B\[[Tt][0-9]+\]'] = gforgeTaskSub
631 commentSubstitutions['\b[a-zA-Z]+-[0-9]+\b'] = jiraSub
634 commentSubstitutions['\b[Tt][Ii][Cc][Kk][Ee][Tt]\s*#?[0-9]+\b'] = ticketSub
637 + commentSubstitutions['\b[Ii][Ss][Ss][Uu][Ee]\s*#?[0-9]+\b'] = issueSub
640 commentSubstitutions['\[\[.+\]\]'] = wikiSub
642 +if $xplannerIterationURL != nil
643 + commentSubstitutions['\bXI\[?[0-9]+\]?'] = xplannerIterationSub
645 +if $xplannerProjectURL != nil
646 + commentSubstitutions['\bXP\[?[0-9]+\]?'] = xplannerProjectSub
648 +if $xplannerStoryURL != nil
649 + commentSubstitutions['\bXS\[?[0-9]+\]?'] = xplannerStorySub
651 $commentEncoder = MultiSub.new(commentSubstitutions)
654 @@ -1546,11 +1667,14 @@
656 name = "<span id=\"removed\">#{name}</span>"
660 - mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt></td>")
661 + mail.print("<tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt>")
663 - mail.print("<td><tt>#{prefix}#{name}</tt></td>")
664 + mail.print("<tt>#{prefix}#{name}</tt>")
666 + mail.print(" #{$frontend.log(file)}")
667 + mail.print("</td>")
669 mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
671 @@ -1667,12 +1791,13 @@
672 # CVSROOT/users file, if the file exists. The argument is returned unchanged
673 # if no alias is found.
674 def sender_alias(email)
675 + blah("Lookup '#{email}' from users file")
676 if File.exists?($users_file)
677 File.open($users_file) do |io|
678 io.each_line do |line|
679 if line =~ /^([^:]+)\s*:\s*(['"]?)([^\n\r]+)(\2)/
680 if email.address == $1
681 - return EmailAddress.new($3)
682 + return EmailAddress.new($3, $users_file_charset)
686 @@ -1686,6 +1811,8 @@
687 # sensible header formatting, and for ensuring that the body is seperated
688 # from the message headers by a blank line (as it is required to be).
690 + ENCODE_HEADERS = ["Subject", "X-CVSspam-Module-Path"]
693 @done_headers = false
695 @@ -1695,8 +1822,8 @@
697 def header(name, value)
698 raise "headers already commited" if @done_headers
699 - if name == "Subject"
700 - $encoder.encode_header(@io, "Subject", value)
701 + if ENCODE_HEADERS.include?(name)
702 + $encoder.encode_header(@io, name, value)
704 @io.puts("#{name}: #{value}")
706 @@ -1769,7 +1896,7 @@
707 ctx.header("To", recipients.map{|addr| addr.encoded}.join(','))
708 blah("Mail From: <#{from}>")
709 ctx.header("From", from.encoded) if from
710 - ctx.header("Date", Time.now.utc.strftime(DATE_HEADER_FORMAT))
711 + ctx.header("Date", Time.now.rfc2822)
715 @@ -1800,10 +1927,10 @@
716 return unless $fileEntries.length == 1
717 file = $fileEntries[0]
718 name = zap_header_special_chars(file.path)
719 - unless file.fromVer == "NONE"
721 mail.header("References", make_msg_id("#{name}.#{file.fromVer}", $hostname))
723 - unless file.toVer == "NONE"
725 mail.header("Message-ID", make_msg_id("#{name}.#{file.toVer}", $hostname))
728 @@ -1834,6 +1961,14 @@
731 mail.header("X-Mailer", "CVSspam #{$version} <http://www.badgers-in-foil.co.uk/projects/cvsspam/>")
732 + if $cvsroot_email_header
734 + if Repository.count == 1
735 + rep = Repository.array.first
736 + mod << rep.common_prefix
738 + mail.header("X-CVSspam-Module-Path", mod)
742 make_html_email(body)
744 Property changes on: testcases/data/remove.png
745 ___________________________________________________________________
746 Deleted: svn:executable
750 Property changes on: testcases/data/fiddlyedits.after
751 ___________________________________________________________________
752 Deleted: svn:executable
756 Property changes on: testcases/data/fiddlyedits.before
757 ___________________________________________________________________
758 Deleted: svn:executable
762 Property changes on: testcases/data/add.png
763 ___________________________________________________________________
764 Deleted: svn:executable
768 Property changes on: testcases/README
769 ___________________________________________________________________
770 Deleted: svn:executable