1 --- cvsspam-0.2.12/CREDITS 2005-07-11 18:53:29.000000000 +0300
2 +++ cvsspam/CREDITS 2006-12-21 11:44:26.837358000 +0200
14 --- cvsspam-0.2.12/collect_diffs.rb 2005-07-11 18:53:29.000000000 +0300
15 +++ cvsspam/collect_diffs.rb 2006-12-21 11:44:26.827358000 +0200
17 $dirtemplate = "#cvsspam.#{Process.getpgrp}.#{Process.uid}"
21 + safe_from = make_fromaddr_safe_for_filename($from_address)
22 + Dir["#{$tmpdir}/#{$dirtemplate}.#{safe_from}-*"].each do |dir|
23 + stat = File.stat(dir)
24 + return dir if stat.owned?
27 Dir["#{$tmpdir}/#{$dirtemplate}-*"].each do |dir|
29 return dir if stat.owned?
34 +# transform any special / unexpected characters appearing in the argument to
35 +# --from so that they will not cause problems if the value is inserted into
36 +# a file or directory name
37 +def make_fromaddr_safe_for_filename(addr)
38 + addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
44 $stderr.puts "collect_diffs.rb: #{msg}"
48 while i < cvs_info.length
49 - changes << ChangeInfo.new(cvs_info[i], cvs_info[i+=1], cvs_info[i+=1])
50 + change_file = cvs_info[i]
51 + # It's been reported,
52 + # http://lists.badgers-in-foil.co.uk/pipermail/cvsspam-devel/2005-September/000380.html
53 + # that sometimes the second revision number that CVS gives us contains a
54 + # trailing newline character, so we strip ws from these values before use,
55 + change_from = cvs_info[i+=1].strip
56 + change_to = cvs_info[i+=1].strip
57 + changes << ChangeInfo.new(change_file, change_from, change_to)
63 diff_cmd = Array.new << $cvs_prog << "-nq" << "diff" << "-Nu"
64 diff_cmd << "-kk" if $diff_ignore_keywords
65 + diff_cmd << "-b" if $diff_ignore_whitespace
76 $diff_ignore_keywords = false
77 +$diff_ignore_whitespace = false
80 unless ENV.has_key?('CVSROOT')
83 $config = arg if opt=="--config"
84 $debug = true if opt == "--debug"
85 + $from_address = arg if opt == "--from"
88 blah("CVSROOT is #{ENV['CVSROOT']}")
89 --- cvsspam-0.2.12/cvsspam-doc.xml 2005-07-11 18:53:29.000000000 +0300
90 +++ cvsspam/cvsspam-doc.xml 2006-12-21 11:44:26.837358000 +0200
92 </screen></informalexample>
99 + <para>For Gforge, when a CVS log comment contains text like <userinput>Fix
100 + for Bug [#123]</userinput>, or <userinput>Task [T456] ...</userinput>, the
101 + text "[#123]" or "[T456]" will become a hyper-link to that Gforge page in
102 + the generated email. The format [#<replaceable>nnn</replaceable>] and
103 + [T<replaceable>nnn</replaceable>] is taken from the existing plugin for
104 + Gforge called cvstracker.</para>
106 + <para>To enable, give your Gforge's URL in CVSspam's configuration file:
107 +<informalexample><screen>$gforgeBugURL = "http://gforge.org/tracker/index.php?func=detail&aid=%s"
108 +$gforgeTaskURL = "http://gforge.org/pm/task.php?func=detailtask&project_task_id=%s"</screen></informalexample>
109 + The marker %s tells CVSspam where in the URL to put the bugId from the
110 + log message.</para>
114 <section><title>CVS Web Frontends</title>
115 --- cvsspam-0.2.12/cvsspam.conf 2005-07-11 18:53:30.000000000 +0300
116 +++ cvsspam/cvsspam.conf 2006-12-21 11:44:26.827358000 +0200
119 # When $jiraURL is given, text of the form 'project-1234' will be linked
120 # to this issue in JIRA.
122 +# When $xplannerStoryURL, $xplannerIterationURL and $xplannerProjectURL are
123 +# given, text of the form XS1234 will be linked to XPlanner stories; text of
124 +# the form XI1234 will be linked to XPlanner iterations; and text of the form
125 +# XP1234 will be linked to XPlanner projects.
127 #$bugzillaURL = "http://bugzilla.mozilla.org/show_bug.cgi?id=%s"
129 #$jiraURL = "http://jira.atlassian.com/secure/ViewIssue.jspa?key=%s"
131 +#$xplannerStoryURL = "http://www.example.com/xplanner/do/view/userstory?oid=%s"
132 +#$xplannerIterationURL = "http://www.example.com/xplanner/do/view/iteration?oid=%s"
133 +#$xplannerProjectURL = "http://www.example.com/xplanner/do/view/project?oid=%s"
135 # Link to Wiki systems
137 @@ -119,12 +127,21 @@
138 # cvsdiff keyword ignoring (Default: show changes in keywords)
140 # Changes in CVS keywords can be distracting. For instance, the
141 -# $Revision$ keyword will change on each commit. Set this value to true
142 +# $Revision$ keyword will change on each commit. Set this value to true
143 # to exclude changes in keyword fields (adds the -kk option to cvs diff).
145 #$diff_ignore_keywords = true
148 +# cvsdiff whitespace ignoring (Default: show whitespace-only changes)
150 +# Whitespace-only changes can distract from the rest of a diff. Set this
151 +# value to true to exclude changes in the amount of whitespace (adds the -b
152 +# option to cvs diff).
154 +$diff_ignore_whitespace = true
157 # $no_removed_file_diff and $no_added_file_diff
159 # Set both these options, and emails will only include diffs for files
161 # them happy, you can say $files_in_subject = true here.
163 #$files_in_subject = false
167 +# Email size limit (Default: around 2MB)
169 +# When large changes are committed, large CVSspam emails can result. Here
170 +# you can set the size of email that CVSspam is not allowed to append any
171 +# more diffs onto. Specify the number of bytes.
173 +#$mail_size_limit = 2097152
174 --- cvsspam-0.2.12/cvsspam.rb 2005-07-11 18:53:29.000000000 +0300
175 +++ cvsspam/cvsspam.rb 2006-12-21 17:36:44.342608880 +0200
182 $maxSubjectLength = 200
183 $maxLinesPerDiff = 1000
188 -# NB must ensure the time is UTC
189 -# (the Ruby Time object's strftime() doesn't supply a numeric timezone)
190 -DATE_HEADER_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
192 # Perform (possibly) multiple global substitutions on a string.
193 # the regexps given as keys must not use capturing subexpressions '(...)'
196 UNDERSCORE = chr("_")
202 # encode a header value according to the RFC-2047 quoted-printable spec,
203 # allowing non-ASCII characters to appear in header values, and wrapping
205 # return a string representing the given character-code in quoted-printable
207 def quoted_encode_char(b)
208 - if b>126 || b==UNDERSCORE || b==TAB
209 + if b>126 || b==UNDERSCORE || b==TAB || b==HOOK || b==EQUALS
217 + @fromVer = @toVer = nil
218 @lineAdditions = @lineRemovals = 0
219 @repository = Repository.get(path)
220 @repository.merge_common_prefix(basedir())
222 # TODO: consolidate these into a nicer framework,
223 mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" }
224 urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" }
225 +gforgeTaskSub = proc { |match|
226 + match =~ /([0-9]+)/
227 + "<a href=\"#{$gforgeTaskURL.sub(/%s/, $1)}\">#{match}</a>"
229 +gforgeBugSub = proc { |match|
230 + match =~ /([0-9]+)/
231 + "<a href=\"#{$gforgeBugURL.sub(/%s/, $1)}\">#{match}</a>"
233 bugzillaSub = proc { |match|
235 "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>"
236 @@ -544,11 +552,27 @@
238 "<a href=\"#{$ticketURL.sub(/%s/, $1)}\">#{match}</a>"
240 +issueSub = proc { |match|
241 + match =~ /([0-9]+)/
242 + "<a href=\"#{$issueURL.sub(/%s/, $1)}\">#{match}</a>"
244 wikiSub = proc { |match|
245 - match =~ /\[\[(.*)\]\]/
246 + match =~ /\[\[(.*?)\]\]/
248 "<a href=\"#{$wikiURL.sub(/%s/, urlEncode(raw))}\">[[#{raw}]]</a>"
250 +xplannerIterationSub = proc { |match|
251 + match =~ /([0-9]+)/
252 + "<a href=\"#{$xplannerIterationURL.sub(/%s/, $1)}\">#{match}</a>"
254 +xplannerProjectSub = proc { |match|
255 + match =~ /([0-9]+)/
256 + "<a href=\"#{$xplannerProjectURL.sub(/%s/, $1)}\">#{match}</a>"
258 +xplannerStorySub = proc { |match|
259 + match =~ /([0-9]+)/
260 + "<a href=\"#{$xplannerStoryURL.sub(/%s/, $1)}\">#{match}</a>"
262 commentSubstitutions = {
263 '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub,
264 '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub
270 + # may be overridden by subclasses that are able to make a hyperlink to a
271 + # history log for a file
277 # Superclass for objects that can link to CVS frontends on the web (ViewCVS,
279 "<a href=\"#{diff_url(file)}\">#{super(file)}</a>"
283 + link = log_url(file)
285 + return "<span id=\"info\">(<a href=\"#{link}\">log</a>)</span>"
306 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=#{file.fromVer}&r2=#{file.toVer}")
311 + log_anchor = "#rev#{file.toVer}"
315 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
319 # Link to Chora, from the Horde framework
321 class CVSwebFrontend < WebFrontend
322 def path_url(path, tag)
324 - add_repo(@base_url + urlEncode(path))
325 + add_repo(@base_url + urlEncode(path) + "/")
327 - add_repo("#{@base_url}#{urlEncode(path)}?only_with_tag=#{urlEncode(tag)}")
328 + add_repo("#{@base_url}#{urlEncode(path)}/?only_with_tag=#{urlEncode(tag)}")
334 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=text&tr1=#{file.fromVer}&r2=text&tr2=#{file.toVer}&f=h")
341 + log_anchor = "#rev#{file.toVer}"
345 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
353 if @truncatedLineCount>0
354 - println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partialy shown]</strong>")
355 + println("<strong class=\"error\" title=\"#{@truncatedLineCount} lines truncated at column #{$maxDiffLineLength}\">[Note: Some over-long lines of diff output only partially shown]</strong>")
359 @@ -1247,10 +1309,16 @@
361 $task_keywords = ['TODO', 'FIXME']
364 +$gforgeTaskURL = nil
370 +$xplannerIterationURL = nil
371 +$xplannerProjectURL = nil
372 +$xplannerStoryURL = nil
376 @@ -1261,6 +1329,7 @@
377 # 2MiB limit on attached diffs,
378 $mail_size_limit = 1024 * 1024 * 2
380 +$cvsroot_email_header = false
384 @@ -1353,7 +1422,13 @@
387 if $bugzillaURL != nil
388 - commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugzillaSub
389 + commentSubstitutions['\b[Bb]([Uu][Gg])?\s*[#:]?\s*\[?[0-9]+\]?'] = bugzillaSub
391 +if $gforgeBugURL != nil
392 + commentSubstitutions['\B\[#[0-9]+\]'] = gforgeBugSub
394 +if $gforgeTaskURL != nil
395 + commentSubstitutions['\B\[[Tt][0-9]+\]'] = gforgeTaskSub
398 commentSubstitutions['\b[a-zA-Z]+-[0-9]+\b'] = jiraSub
399 @@ -1361,9 +1436,21 @@
401 commentSubstitutions['\b[Tt][Ii][Cc][Kk][Ee][Tt]\s*#?[0-9]+\b'] = ticketSub
404 + commentSubstitutions['\b[Ii][Ss][Ss][Uu][Ee]\s*#?[0-9]+\b'] = issueSub
407 commentSubstitutions['\[\[.+\]\]'] = wikiSub
409 +if $xplannerIterationURL != nil
410 + commentSubstitutions['\bXI\[?[0-9]+\]?'] = xplannerIterationSub
412 +if $xplannerProjectURL != nil
413 + commentSubstitutions['\bXP\[?[0-9]+\]?'] = xplannerProjectSub
415 +if $xplannerStoryURL != nil
416 + commentSubstitutions['\bXS\[?[0-9]+\]?'] = xplannerStorySub
418 $commentEncoder = MultiSub.new(commentSubstitutions)
421 @@ -1546,11 +1633,14 @@
423 name = "<span id=\"removed\">#{name}</span>"
427 - mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt></td>")
428 + mail.print("<tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt>")
430 - mail.print("<td><tt>#{prefix}#{name}</tt></td>")
431 + mail.print("<tt>#{prefix}#{name}</tt>")
433 + mail.print(" #{$frontend.log(file)}")
434 + mail.print("</td>")
436 mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
438 @@ -1686,6 +1776,8 @@
439 # sensible header formatting, and for ensuring that the body is seperated
440 # from the message headers by a blank line (as it is required to be).
442 + ENCODE_HEADERS = ["Subject", "X-CVSspam-Module-Path"]
445 @done_headers = false
447 @@ -1695,8 +1787,8 @@
449 def header(name, value)
450 raise "headers already commited" if @done_headers
451 - if name == "Subject"
452 - $encoder.encode_header(@io, "Subject", value)
453 + if ENCODE_HEADERS.include?(name)
454 + $encoder.encode_header(@io, name, value)
456 @io.puts("#{name}: #{value}")
458 @@ -1769,7 +1861,7 @@
459 ctx.header("To", recipients.map{|addr| addr.encoded}.join(','))
460 blah("Mail From: <#{from}>")
461 ctx.header("From", from.encoded) if from
462 - ctx.header("Date", Time.now.utc.strftime(DATE_HEADER_FORMAT))
463 + ctx.header("Date", Time.now.rfc2822)
467 @@ -1800,10 +1892,10 @@
468 return unless $fileEntries.length == 1
469 file = $fileEntries[0]
470 name = zap_header_special_chars(file.path)
471 - unless file.fromVer == "NONE"
473 mail.header("References", make_msg_id("#{name}.#{file.fromVer}", $hostname))
475 - unless file.toVer == "NONE"
477 mail.header("Message-ID", make_msg_id("#{name}.#{file.toVer}", $hostname))
480 @@ -1834,6 +1926,14 @@
483 mail.header("X-Mailer", "CVSspam #{$version} <http://www.badgers-in-foil.co.uk/projects/cvsspam/>")
484 + if $cvsroot_email_header
486 + if Repository.count == 1
487 + rep = Repository.array.first
488 + mod << rep.common_prefix
490 + mail.header("X-CVSspam-Module-Path", mod)
494 make_html_email(body)
495 --- cvsspam-0.2.12/record_lastdir.rb 2005-07-11 18:53:29.000000000 +0300
496 +++ cvsspam/record_lastdir.rb 2006-12-21 11:44:26.827358000 +0200
498 # http://www.badgers-in-foil.co.uk/projects/cvsspam/
499 # Copyright (c) David Holroyd
501 -$repositorydir = ARGV.shift
503 $tmpdir = ENV["TMPDIR"] || "/tmp"
510 +# transform any special / unexpected characters appearing in the argument to
511 +# --from so that they will not cause problems if the value is inserted into
512 +# a file or directory name
513 +def make_fromaddr_safe_for_filename(addr)
514 + addr.gsub(/[^a-zA-Z0-1.,_-]/, "_")
517 +# Option processing doesn't use GetoptLong (for the moment) bacause arguments
518 +# given to this script by CVS include the names of committed files. It
519 +# seems quite possible that one of those file names could begin with a '-'
520 +# and therefore be treated by GetoptLong as a value which requires processing.
521 +# This would probably result in an error.
523 +# [That could be worked around by placing a '--' option (which tells GetoptLong
524 +# to stop processing option arguments) at the very end of the arguments to
525 +# record_lastdir.rb in commitinfo, but that's very easily forgotten, and isn't
526 +# really backwards compatable with the behaviour of older CVSspam releases.]
527 +if ARGV.first == "--from"
528 + # we could, of course, be tricked, if the first committed file in the list
529 + # happened to be named '--from' :S
531 + # drop the "--from"
533 + # and use the value which was given following the option,
534 + $dirtemplate << "." << make_fromaddr_safe_for_filename(ARGV.shift)
537 +$repositorydir = ARGV.shift
539 $datadir = find_data_dir()