2 ===================================================================
3 --- cvsspam.conf (.../tags/RELEASE-0_2_12) (revision 256)
4 +++ cvsspam.conf (.../trunk) (revision 256)
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
26 #$diff_ignore_keywords = true
29 +# cvsdiff whitespace ignoring (Default: show whitespace-only changes)
31 +# Whitespace-only changes can distract from the rest of a diff. Set this
32 +# value to true to exclude changes in the amount of whitespace (adds the -b
33 +# option to cvs diff).
35 +$diff_ignore_whitespace = true
38 # $no_removed_file_diff and $no_added_file_diff
40 # Set both these options, and emails will only include diffs for files
42 # them happy, you can say $files_in_subject = true here.
44 #$files_in_subject = false
48 +# Email size limit (Default: around 2MB)
50 +# When large changes are committed, large CVSspam emails can result. Here
51 +# you can set the size of email that CVSspam is not allowed to append any
52 +# more diffs onto. Specify the number of bytes.
54 +#$mail_size_limit = 2097152
55 Index: collect_diffs.rb
56 ===================================================================
57 --- collect_diffs.rb (.../tags/RELEASE-0_2_12) (revision 256)
58 +++ collect_diffs.rb (.../trunk) (revision 256)
60 $dirtemplate = "#cvsspam.#{Process.getpgrp}.#{Process.uid}"
64 + safe_from = make_fromaddr_safe_for_filename($from_address)
65 + Dir["#{$tmpdir}/#{$dirtemplate}.#{safe_from}-*"].each do |dir|
66 + stat = File.stat(dir)
67 + return dir if stat.owned?
70 Dir["#{$tmpdir}/#{$dirtemplate}-*"].each do |dir|
72 return dir if stat.owned?
77 +# transform any special / unexpected characters appearing in the argument to
78 +# --from so that they will not cause problems if the value is inserted into
79 +# a file or directory name
80 +def make_fromaddr_safe_for_filename(addr)
81 + addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
87 $stderr.puts "collect_diffs.rb: #{msg}"
91 while i < cvs_info.length
92 - changes << ChangeInfo.new(cvs_info[i], cvs_info[i+=1], cvs_info[i+=1])
93 + change_file = cvs_info[i]
94 + # It's been reported,
95 + # http://lists.badgers-in-foil.co.uk/pipermail/cvsspam-devel/2005-September/000380.html
96 + # that sometimes the second revision number that CVS gives us contains a
97 + # trailing newline character, so we strip ws from these values before use,
98 + change_from = cvs_info[i+=1].strip
99 + change_to = cvs_info[i+=1].strip
100 + changes << ChangeInfo.new(change_file, change_from, change_to)
106 diff_cmd = Array.new << $cvs_prog << "-nq" << "diff" << "-Nu"
107 diff_cmd << "-kk" if $diff_ignore_keywords
108 + diff_cmd << "-b" if $diff_ignore_whitespace
119 $diff_ignore_keywords = false
120 +$diff_ignore_whitespace = false
123 unless ENV.has_key?('CVSROOT')
126 $config = arg if opt=="--config"
127 $debug = true if opt == "--debug"
128 + $from_address = arg if opt == "--from"
131 blah("CVSROOT is #{ENV['CVSROOT']}")
132 Index: record_lastdir.rb
133 ===================================================================
134 --- record_lastdir.rb (.../tags/RELEASE-0_2_12) (revision 256)
135 +++ record_lastdir.rb (.../trunk) (revision 256)
137 # http://www.badgers-in-foil.co.uk/projects/cvsspam/
138 # Copyright (c) David Holroyd
140 -$repositorydir = ARGV.shift
142 $tmpdir = ENV["TMPDIR"] || "/tmp"
149 +# transform any special / unexpected characters appearing in the argument to
150 +# --from so that they will not cause problems if the value is inserted into
151 +# a file or directory name
152 +def make_fromaddr_safe_for_filename(addr)
153 + addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
156 +# Option processing doesn't use GetoptLong (for the moment) bacause arguments
157 +# given to this script by CVS include the names of committed files. It
158 +# seems quite possible that one of those file names could begin with a '-'
159 +# and therefore be treated by GetoptLong as a value which requires processing.
160 +# This would probably result in an error.
162 +# [That could be worked around by placing a '--' option (which tells GetoptLong
163 +# to stop processing option arguments) at the very end of the arguments to
164 +# record_lastdir.rb in commitinfo, but that's very easily forgotten, and isn't
165 +# really backwards compatable with the behaviour of older CVSspam releases.]
166 +if ARGV.first == "--from"
167 + # we could, of course, be tricked, if the first committed file in the list
168 + # happened to be named '--from' :S
170 + # drop the "--from"
172 + # and use the value which was given following the option,
173 + $dirtemplate << "." << make_fromaddr_safe_for_filename(ARGV.shift)
176 +$repositorydir = ARGV.shift
178 $datadir = find_data_dir()
182 ===================================================================
184 ===================================================================
185 --- CREDITS (.../tags/RELEASE-0_2_12) (revision 256)
186 +++ CREDITS (.../trunk) (revision 256)
198 Index: cvsspam-doc.xml
199 ===================================================================
200 --- cvsspam-doc.xml (.../tags/RELEASE-0_2_12) (revision 256)
201 +++ cvsspam-doc.xml (.../trunk) (revision 256)
203 </screen></informalexample>
210 + <para>For Gforge, when a CVS log comment contains text like <userinput>Fix
211 + for Bug [#123]</userinput>, or <userinput>Task [T456] ...</userinput>, the
212 + text "[#123]" or "[T456]" will become a hyper-link to that Gforge page in
213 + the generated email. The format [#<replaceable>nnn</replaceable>] and
214 + [T<replaceable>nnn</replaceable>] is taken from the existing plugin for
215 + Gforge called cvstracker.</para>
217 + <para>To enable, give your Gforge's URL in CVSspam's configuration file:
218 +<informalexample><screen>$gforgeBugURL = "http://gforge.org/tracker/index.php?func=detail&aid=%s"
219 +$gforgeTaskURL = "http://gforge.org/pm/task.php?func=detailtask&project_task_id=%s"</screen></informalexample>
220 + The marker %s tells CVSspam where in the URL to put the bugId from the
221 + log message.</para>
225 <section><title>CVS Web Frontends</title>
227 ===================================================================
228 --- cvsspam.rb (.../tags/RELEASE-0_2_12) (revision 256)
229 +++ cvsspam.rb (.../trunk) (revision 256)
236 $maxSubjectLength = 200
237 $maxLinesPerDiff = 1000
242 -# NB must ensure the time is UTC
243 -# (the Ruby Time object's strftime() doesn't supply a numeric timezone)
244 -DATE_HEADER_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
246 # Perform (possibly) multiple global substitutions on a string.
247 # the regexps given as keys must not use capturing subexpressions '(...)'
252 hash.each do |key,val|
253 - if expr == nil ; expr="(" else expr<<"|(" end
254 + if expr == nil ; expr="(" else expr << "|(" end
259 UNDERSCORE = chr("_")
265 # encode a header value according to the RFC-2047 quoted-printable spec,
266 # allowing non-ASCII characters to appear in header values, and wrapping
268 # return a string representing the given character-code in quoted-printable
270 def quoted_encode_char(b)
271 - if b>126 || b==UNDERSCORE || b==TAB
272 + if b>126 || b==UNDERSCORE || b==TAB || b==HOOK || b==EQUALS
278 # gives a string starting "=?", and including a charset specification, that
279 # marks the start of a quoted-printable character sequence
280 - def marker_start_quoted
281 - "=?#{@charset}?#{@encoding}?"
282 + def marker_start_quoted(charset=nil)
283 + charset = @charset if charset.nil?
284 + "=?#{charset}?#{@encoding}?"
287 # test to see of the given string contains non-ASCII characters
292 + @fromVer = @toVer = nil
293 @lineAdditions = @lineRemovals = 0
294 @repository = Repository.get(path)
295 @repository.merge_common_prefix(basedir())
297 # TODO: consolidate these into a nicer framework,
298 mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" }
299 urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" }
300 +gforgeTaskSub = proc { |match|
301 + match =~ /([0-9]+)/
302 + "<a href=\"#{$gforgeTaskURL.sub(/%s/, $1)}\">#{match}</a>"
304 +gforgeBugSub = proc { |match|
305 + match =~ /([0-9]+)/
306 + "<a href=\"#{$gforgeBugURL.sub(/%s/, $1)}\">#{match}</a>"
308 bugzillaSub = proc { |match|
310 "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>"
311 @@ -544,11 +553,27 @@
313 "<a href=\"#{$ticketURL.sub(/%s/, $1)}\">#{match}</a>"
315 +issueSub = proc { |match|
316 + match =~ /([0-9]+)/
317 + "<a href=\"#{$issueURL.sub(/%s/, $1)}\">#{match}</a>"
319 wikiSub = proc { |match|
320 - match =~ /\[\[(.*)\]\]/
321 + match =~ /\[\[(.*?)\]\]/
323 "<a href=\"#{$wikiURL.sub(/%s/, urlEncode(raw))}\">[[#{raw}]]</a>"
325 +xplannerIterationSub = proc { |match|
326 + match =~ /([0-9]+)/
327 + "<a href=\"#{$xplannerIterationURL.sub(/%s/, $1)}\">#{match}</a>"
329 +xplannerProjectSub = proc { |match|
330 + match =~ /([0-9]+)/
331 + "<a href=\"#{$xplannerProjectURL.sub(/%s/, $1)}\">#{match}</a>"
333 +xplannerStorySub = proc { |match|
334 + match =~ /([0-9]+)/
335 + "<a href=\"#{$xplannerStoryURL.sub(/%s/, $1)}\">#{match}</a>"
337 commentSubstitutions = {
338 '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub,
339 '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub
345 + # may be overridden by subclasses that are able to make a hyperlink to a
346 + # history log for a file
352 # Superclass for objects that can link to CVS frontends on the web (ViewCVS,
354 "<a href=\"#{diff_url(file)}\">#{super(file)}</a>"
358 + link = log_url(file)
360 + return "<span id=\"info\">(<a href=\"#{link}\">log</a>)</span>"
381 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=#{file.fromVer}&r2=#{file.toVer}")
386 + log_anchor = "#rev#{file.toVer}"
390 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
394 # Link to Chora, from the Horde framework
396 class CVSwebFrontend < WebFrontend
397 def path_url(path, tag)
399 - add_repo(@base_url + urlEncode(path))
400 + add_repo(@base_url + urlEncode(path) + "/")
402 - add_repo("#{@base_url}#{urlEncode(path)}?only_with_tag=#{urlEncode(tag)}")
403 + add_repo("#{@base_url}#{urlEncode(path)}/?only_with_tag=#{urlEncode(tag)}")
409 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=text&tr1=#{file.fromVer}&r2=text&tr2=#{file.toVer}&f=h")
416 + log_anchor = "#rev#{file.toVer}"
420 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
428 if @truncatedLineCount>0
429 - println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partialy shown]</strong>")
430 + println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partially shown]</strong>")
434 @@ -1181,7 +1244,7 @@
436 # an RFC 822 email address
438 - def initialize(text)
439 + def initialize(text, charset=nil)
440 if text =~ /^\s*([^<]+?)\s*<\s*([^>]+?)\s*>\s*$/
443 @@ -1189,9 +1252,10 @@
450 - attr_accessor :personal_name, :address
451 + attr_accessor :personal_name, :address, :charset
453 def has_personal_name?
454 return !@personal_name.nil?
455 @@ -1222,7 +1286,7 @@
456 # rfc2047 encode the word, if it contains non-ASCII characters
457 def encode_word(word)
458 if $encoder.requires_rfc2047?(word)
459 - encoded = $encoder.marker_start_quoted
460 + encoded = $encoder.marker_start_quoted(@charset)
461 $encoder.each_char_encoded(word) do |code|
464 @@ -1237,6 +1301,7 @@
465 cvsroot_dir = "#{ENV['CVSROOT']}/CVSROOT"
466 $config = "#{cvsroot_dir}/cvsspam.conf"
467 $users_file = "#{cvsroot_dir}/users"
468 +$users_file_charset = nil
471 $recipients = Array.new
472 @@ -1247,10 +1312,16 @@
474 $task_keywords = ['TODO', 'FIXME']
477 +$gforgeTaskURL = nil
483 +$xplannerIterationURL = nil
484 +$xplannerProjectURL = nil
485 +$xplannerStoryURL = nil
489 @@ -1261,6 +1332,7 @@
490 # 2MiB limit on attached diffs,
491 $mail_size_limit = 1024 * 1024 * 2
493 +$cvsroot_email_header = false
497 @@ -1353,17 +1425,35 @@
500 if $bugzillaURL != nil
501 - commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugzillaSub
502 + commentSubstitutions['\b[Bb]([Uu][Gg])?\s*[#:]?\s*\[?[0-9]+\]?'] = bugzillaSub
504 +if $gforgeBugURL != nil
505 + commentSubstitutions['\B\[#[0-9]+\]'] = gforgeBugSub
507 +if $gforgeTaskURL != nil
508 + commentSubstitutions['\B\[[Tt][0-9]+\]'] = gforgeTaskSub
511 commentSubstitutions['\b[a-zA-Z]+-[0-9]+\b'] = jiraSub
514 commentSubstitutions['\b[Tt][Ii][Cc][Kk][Ee][Tt]\s*#?[0-9]+\b'] = ticketSub
517 + commentSubstitutions['\b[Ii][Ss][Ss][Uu][Ee]\s*#?[0-9]+\b'] = issueSub
520 commentSubstitutions['\[\[.+\]\]'] = wikiSub
522 +if $xplannerIterationURL != nil
523 + commentSubstitutions['\bXI\[?[0-9]+\]?'] = xplannerIterationSub
525 +if $xplannerProjectURL != nil
526 + commentSubstitutions['\bXP\[?[0-9]+\]?'] = xplannerProjectSub
528 +if $xplannerStoryURL != nil
529 + commentSubstitutions['\bXS\[?[0-9]+\]?'] = xplannerStorySub
531 $commentEncoder = MultiSub.new(commentSubstitutions)
534 @@ -1546,11 +1636,14 @@
536 name = "<span id=\"removed\">#{name}</span>"
540 - mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt></td>")
541 + mail.print("<tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt>")
543 - mail.print("<td><tt>#{prefix}#{name}</tt></td>")
544 + mail.print("<tt>#{prefix}#{name}</tt>")
546 + mail.print(" #{$frontend.log(file)}")
547 + mail.print("</td>")
549 mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
551 @@ -1672,7 +1765,7 @@
552 io.each_line do |line|
553 if line =~ /^([^:]+)\s*:\s*(['"]?)([^\n\r]+)(\2)/
554 if email.address == $1
555 - return EmailAddress.new($3)
556 + return EmailAddress.new($3, $users_file_charset)
560 @@ -1686,6 +1779,8 @@
561 # sensible header formatting, and for ensuring that the body is seperated
562 # from the message headers by a blank line (as it is required to be).
564 + ENCODE_HEADERS = ["Subject", "X-CVSspam-Module-Path"]
567 @done_headers = false
569 @@ -1695,8 +1790,8 @@
571 def header(name, value)
572 raise "headers already commited" if @done_headers
573 - if name == "Subject"
574 - $encoder.encode_header(@io, "Subject", value)
575 + if ENCODE_HEADERS.include?(name)
576 + $encoder.encode_header(@io, name, value)
578 @io.puts("#{name}: #{value}")
580 @@ -1769,7 +1864,7 @@
581 ctx.header("To", recipients.map{|addr| addr.encoded}.join(','))
582 blah("Mail From: <#{from}>")
583 ctx.header("From", from.encoded) if from
584 - ctx.header("Date", Time.now.utc.strftime(DATE_HEADER_FORMAT))
585 + ctx.header("Date", Time.now.rfc2822)
589 @@ -1800,10 +1895,10 @@
590 return unless $fileEntries.length == 1
591 file = $fileEntries[0]
592 name = zap_header_special_chars(file.path)
593 - unless file.fromVer == "NONE"
595 mail.header("References", make_msg_id("#{name}.#{file.fromVer}", $hostname))
597 - unless file.toVer == "NONE"
599 mail.header("Message-ID", make_msg_id("#{name}.#{file.toVer}", $hostname))
602 @@ -1834,6 +1929,14 @@
605 mail.header("X-Mailer", "CVSspam #{$version} <http://www.badgers-in-foil.co.uk/projects/cvsspam/>")
606 + if $cvsroot_email_header
608 + if Repository.count == 1
609 + rep = Repository.array.first
610 + mod << rep.common_prefix
612 + mail.header("X-CVSspam-Module-Path", mod)
616 make_html_email(body)