1 --- cvsspam-0.2.12/svn_cvsspam.rb 2005-07-11 18:53:29.000000000 +0300
2 +++ cvsspam-svn/svn_cvsspam.rb 2008-08-07 17:27:52.632725455 +0300
4 # to your cvssppam.conf
11 $maxSubjectLength = 200
14 # gets the Repository object for the first component of the given path
15 def Repository.get(name)
18 + # Leading './' is ignored (for peeps who have done 'cvs checkout .')
19 + # Trailing '/' ensures no match for files in root (we just want dirs)
20 + name =~ /^(?:\.\/)?([^\/]+)\//
22 + name = "/" if name.nil? # file at top-level? fake up a name for repo
23 rep = @@repositories[name]
25 rep = Repository.new(name)
30 + @fromVer = @toVer = nil
31 @lineAdditions = @lineRemovals = 0
32 @repository = Repository.get(path)
33 @repository.merge_common_prefix(basedir())
36 # the full path and filename within the repository
38 - # the type of change committed 'M'=modified, 'A'=added, 'R'=removed
39 + # the type of change committed 'M'=modified, 'A'=added, 'R'=removed, 'P'=properties, 'C'=copied
41 # records number of 'addition' lines in diff output, once counted
42 attr_accessor :lineAdditions
44 # works out the filename part of #path
51 # set the branch on which this change was committed, and add it to the list
53 # works out the directory part of #path
60 # gives the Repository object this file was automatically associated with
67 # was this file added during the commit?
72 + # was this file copied during the commit?
77 # was this file simply modified during the commit?
82 + # was this file simply modified during the commit?
88 # passing true, this object remembers that a diff will appear in the email,
89 # passing false, this object remembers that no diff will appear in the email.
91 # TODO: consolidate these into a nicer framework,
92 mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" }
93 urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" }
94 +gforgeTaskSub = proc { |match|
96 + "<a href=\"#{$gforgeTaskURL.sub(/%s/, $1)}\">#{match}</a>"
98 +gforgeBugSub = proc { |match|
100 + "<a href=\"#{$gforgeBugURL.sub(/%s/, $1)}\">#{match}</a>"
102 bugzillaSub = proc { |match|
104 "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>"
107 "<a href=\"#{$ticketURL.sub(/%s/, $1)}\">#{match}</a>"
109 +wikiSub = proc { |match|
110 + match =~ /\[\[(.*)\]\]/
112 + "<a href=\"#{$wikiURL.sub(/%s/, urlEncode(raw))}\">[[#{raw}]]</a>"
114 commentSubstitutions = {
115 '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub,
116 - '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub}
117 + '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub
120 # outputs commit log comment text supplied by LogReader as preformatted HTML
121 class CommentHandler < LineConsumer
127 + # may be overridden by subclasses that are able to make a hyperlink to a
128 + # history log for a file
134 # Superclass for objects that can link to CVS frontends on the web (ViewCVS,
136 "<a href=\"#{diff_url(file)}\">#{super(file)}</a>"
140 + link = log_url(file)
142 + return "<span id=\"info\">(<a href=\"#{link}\">log</a>)</span>"
163 add_repo("#{@base_url}#{urlEncode(file.path)}.diff?r1=text&tr1=#{file.fromVer}&r2=text&tr2=#{file.toVer}&f=h")
170 + log_anchor = "#rev#{file.toVer}"
174 + add_repo("#{@base_url}#{urlEncode(file.path)}#{log_anchor}")
183 +# Note when LogReader finds record of a file that was copied in this commit
184 +class CopiedFileHandler < FileHandler
185 + def handleFile(file)
187 + file.fromVer=$fromVer
192 # Note when LogReader finds record of a file that was modified in this commit
193 class ModifiedFileHandler < FileHandler
199 +# Note when LogReader finds record of a file whose properties were modified in this commit
200 +class ModifiedPropsFileHandler < FileHandler
201 + def handleFile(file)
203 + file.fromVer=$fromVer
209 # Used by UnifiedDiffHandler to record the number of added and removed lines
210 # appearing in a unidiff.
211 @@ -967,11 +1043,21 @@
212 print($frontend.path($file.basedir, $file.tag))
213 println("</span><br />")
214 println("<div class=\"fileheader\" id=\"removed\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">removed after #{$frontend.version($file.path,$file.fromVer)}</small></div>")
216 + print("<span class=\"pathname\" id=\"copied\">")
217 + print($frontend.path($file.basedir, $file.tag))
218 + println("</span><br />")
219 + println("<div class=\"fileheader\" id=\"copied\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">copied from #{$frontend.version($file.path,$file.fromVer)}</small></div>")
221 print("<span class=\"pathname\">")
222 print($frontend.path($file.basedir, $file.tag))
223 println("</span><br />")
224 println("<div class=\"fileheader\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">#{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}</small></div>")
226 + print("<span class=\"pathname\">")
227 + print($frontend.path($file.basedir, $file.tag))
228 + println("</span><br />")
229 + println("<div class=\"fileheader\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">#{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}</small></div>")
231 print("<pre class=\"diff\"><small id=\"info\">")
233 @@ -1045,7 +1131,7 @@
236 if $file.wants_diff_in_mail?
237 - if @stats.diffLines < $maxLinesPerDiff
238 + if $maxLinesPerDiff.nil? || @stats.diffLines < $maxLinesPerDiff
239 @colour.consume(line)
240 elsif @stats.diffLines == $maxLinesPerDiff
241 @colour.consume(line)
242 @@ -1062,7 +1148,7 @@
243 $file.isBinary = true
245 if $file.wants_diff_in_mail?
246 - if @stats.diffLines > $maxLinesPerDiff
247 + if $maxLinesPerDiff && @stats.diffLines > $maxLinesPerDiff
249 println("<strong class=\"error\">[truncated at #{$maxLinesPerDiff} lines; #{@stats.diffLines-$maxLinesPerDiff} more skipped]</strong>")
251 @@ -1230,13 +1316,18 @@
252 $users_file = "#{cvsroot_dir}/users"
256 $recipients = Array.new
257 $sendmail_prog = "/usr/sbin/sendmail"
258 +$hostname = ENV['HOSTNAME'] || 'localhost'
259 $no_removed_file_diff = false
260 $no_added_file_diff = false
262 -$task_keywords = ['TODO', 'FIXME']
263 +$task_keywords = ['TODO', 'FIXME', 'FIXIT', 'todo']
266 +$gforgeTaskURL = nil
271 @@ -1257,6 +1348,7 @@
272 [ "--to", "-t", GetoptLong::REQUIRED_ARGUMENT ],
273 [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
274 [ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
275 + [ "--svn", "-s", GetoptLong::NO_ARGUMENT ],
276 [ "--from", "-u", GetoptLong::REQUIRED_ARGUMENT ],
277 [ "--charset", GetoptLong::REQUIRED_ARGUMENT ]
279 @@ -1265,6 +1357,7 @@
280 $recipients << EmailAddress.new(arg) if opt=="--to"
281 $config = arg if opt=="--config"
282 $debug = true if opt=="--debug"
283 + $svn = true if opt=="--svn"
284 $from_address = EmailAddress.new(arg) if opt=="--from"
285 # must use different variable as the config is readed later.
286 $arg_charset = arg if opt == "--charset"
287 @@ -1277,12 +1370,13 @@
289 $stderr.puts "missing required file argument"
291 - puts "Usage: cvsspam.rb [ --to <email> ] [ --config <file> ] <collect_diffs file>"
292 + puts "Usage: cvsspam.rb [ --svn ] [ --to <email> ] [ --config <file> ] <collect_diffs file>"
299 $additionalHeaders = Array.new
300 $problemHeaders = Array.new
302 @@ -1343,12 +1437,21 @@
303 if $bugzillaURL != nil
304 commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugzillaSub
306 +if $gforgeBugURL != nil
307 + commentSubstitutions['\B\[#[0-9]+\]'] = gforgeBugSub
309 +if $gforgeTaskURL != nil
310 + commentSubstitutions['\B\[[Tt][0-9]+\]'] = gforgeTaskSub
313 commentSubstitutions['\b[a-zA-Z]+-[0-9]+\b'] = jiraSub
316 commentSubstitutions['\b[Tt][Ii][Cc][Kk][Ee][Tt]\s*#?[0-9]+\b'] = ticketSub
319 + commentSubstitutions['\[\[.+\]\]'] = wikiSub
321 $commentEncoder = MultiSub.new(commentSubstitutions)
324 @@ -1359,12 +1462,16 @@
326 "A" => AddedFileHandler.new,
327 "R" => RemovedFileHandler.new,
328 + "C" => CopiedFileHandler.new,
329 "M" => ModifiedFileHandler.new,
330 + "P" => ModifiedPropsFileHandler.new,
331 "V" => VersionHandler.new]
333 $handlers["A"].setTagHandler(tagHandler)
334 $handlers["R"].setTagHandler(tagHandler)
335 +$handlers["C"].setTagHandler(tagHandler)
336 $handlers["M"].setTagHandler(tagHandler)
337 +$handlers["P"].setTagHandler(tagHandler)
339 $fileEntries = Array.new
340 $task_list = Array.new
341 @@ -1374,7 +1481,8 @@
343 $diff_output_limiter = OutputSizeLimiter.new(mail, $mail_size_limit)
345 - reader = LogReader.new($stdin)
346 + File.open($logfile) do |log|
347 + reader = LogReader.new(log)
350 handler = $handlers[reader.currentLineCode]
351 @@ -1383,11 +1491,16 @@
353 handler.handleLines(reader.getLines, $diff_output_limiter)
359 if $subjectPrefix == nil
360 - $subjectPrefix = "[SVN #{Repository.array.join(',')}]"
362 + $subjectPrefix = "[SVN #{Repository.array.join(',')}]"
364 + $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
369 @@ -1434,13 +1547,15 @@
370 #removed {background-color:#ffdddd;}
371 #removedchars {background-color:#ff9999;font-weight:bolder;}
372 tr.alt #removed {background-color:#f7cccc;}
373 + #copied {background-color:#ccccff;}
374 + tr.alt #copied {background-color:#bbbbf7;}
375 #info {color:#888888;}
376 #context {background-color:#eeeeee;}
377 td {padding-left:.3em;padding-right:.3em;}
378 tr.head {border-bottom-width:1px;border-bottom-style:solid;}
379 tr.head td {padding:0;padding-top:.2em;}
380 .task {background-color:#ffff00;}
381 - .comment {padding:4px;border:1px dashed #000000;background-color:#ffffdd}
382 + .comment {white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;white-space:pre-wrap;word-wrap:break-word;padding:4px;border:1px dashed #000000;background-color:#ffffdd}
384 hr {border-width:0px;height:2px;background:black;}
386 @@ -1466,7 +1581,9 @@
392 + filesModifiedProps = 0
394 totalLinesRemoved = 0
396 @@ -1475,24 +1592,26 @@
397 $fileEntries.each do |file|
398 unless file.repository == last_repository
399 last_repository = file.repository
400 - mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 5 : 4}\">")
401 + mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 6 : 5}\">")
402 if last_repository.has_multiple_tags
403 mail.print("Mixed-tag commit")
407 mail.print(" in <b><tt>#{htmlEncode(last_repository.common_prefix)}</tt></b>")
408 - if last_repository.trunk_only?
409 - mail.print("<span id=\"info\"> on MAIN</span>")
413 - last_repository.each_tag do |tag|
416 - mail.print tagCount<last_repository.tag_count ? ", " : " & "
418 + if last_repository.trunk_only?
419 + mail.print("<span id=\"info\"> on MAIN</span>")
423 + last_repository.each_tag do |tag|
426 + mail.print tagCount<last_repository.tag_count ? ", " : " & "
428 + mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>"
430 - mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>"
433 mail.puts("</td></tr>")
434 @@ -1507,8 +1626,12 @@
440 elsif file.modification?
442 + elsif file.modifiedprops?
443 + filesModifiedProps += 1
445 name = htmlEncode(file.name_after_common_prefix)
446 slashPos = name.rindex("/")
447 @@ -1528,17 +1651,29 @@
448 name = "<span id=\"added\">#{name}</span>"
450 name = "<span id=\"removed\">#{name}</span>"
452 + name = "<span id=\"copied\">#{name}</span>"
456 - mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt></td>")
457 + mail.print("<tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a></tt>")
459 - mail.print("<td><tt>#{prefix}#{name}</tt></td>")
460 + mail.print("<tt>#{prefix}#{name}</tt>")
463 - mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
464 + mail.print(" #{$frontend.log(file)}")
465 + mail.print("</td>")
467 + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[copied]</small></td>")
469 + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[empty]</small></td>")
471 - mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[binary]</small></td>")
472 + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[binary]</small></td>")
474 + if file.modifiedprops?
475 + mail.print("<td align=\"right\"><small id=\"info\">[props]</small></td>")
477 + mail.print("<td></td>")
479 if file.lineAdditions>0
480 totalLinesAdded += file.lineAdditions
481 mail.print("<td align=\"right\" id=\"added\">+#{file.lineAdditions}</td>")
482 @@ -1565,15 +1700,19 @@
483 mail.print("<td nowrap=\"nowrap\" align=\"right\">added #{$frontend.version(file.path,file.toVer)}</td>")
485 mail.print("<td nowrap=\"nowrap\">#{$frontend.version(file.path,file.fromVer)} removed</td>")
487 + mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
488 elsif file.modification?
489 mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
490 + elsif file.modifiedprops?
491 + mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
496 if $fileEntries.size>1 && (totalLinesAdded+totalLinesRemoved)>0
497 # give total number of lines added/removed accross all files
498 - mail.print("<tr><td></td>")
499 + mail.print("<tr><td></td><td></td>")
501 mail.print("<td align=\"right\" id=\"added\">+#{totalLinesAdded}</td>")
503 @@ -1590,7 +1729,7 @@
505 mail.puts("</table>")
507 - totalFilesChanged = filesAdded+filesRemoved+filesModified
508 + totalFilesChanged = filesAdded+filesRemoved+filesCopied+filesModified+filesModifiedProps
509 if totalFilesChanged > 1
510 mail.print("<small id=\"info\">")
512 @@ -1603,11 +1742,21 @@
513 mail.print("#{filesRemoved} removed")
517 + mail.print(" + ") if changeKind>0
518 + mail.print("#{filesCopied} copied")
522 mail.print(" + ") if changeKind>0
523 mail.print("#{filesModified} modified")
526 + if filesModifiedProps>0
527 + mail.print(" + ") if changeKind>0
528 + mail.print("#{filesModifiedProps} modified properties")
531 mail.print(", total #{totalFilesChanged}") if changeKind > 1
532 mail.puts(" files</small><br />")
534 @@ -1742,7 +1891,7 @@
535 from = EmailAddress.new(ENV['USER'] || ENV['USERNAME'] || 'cvsspam')
537 unless from.address =~ /@/
538 - from.address = "#{from.address}@#{ENV['HOSTNAME']||'localhost'}"
539 + from.address = "#{from.address}@#{$hostname}"
541 smtp = Net::SMTP.new(@smtp_host)
542 blah("connecting to '#{@smtp_host}'")
543 @@ -1758,6 +1907,40 @@
548 +def make_msg_id(localpart, hostpart)
549 + "<cvsspam-#{localpart}@#{hostpart}>"
553 +# replaces control characters, and a selection of other characters that
554 +# may not appear unquoted in an RFC822 'word', with underscores. (It
555 +# doesn't actually zap '.' though.)
556 +def zap_header_special_chars(text)
557 + text.gsub(/<>()\[\]@,;:\\[\000-\037\177]/, "_")
561 +# Mail clients will try to 'thread' together a conversation over
562 +# several email messages by inspecting the In-Reply-To and References headers,
563 +# which should refer to previous emails in the conversation by mentioning
564 +# the value of the previous message's Message-Id header. This function invents
565 +# values for these headers so that, in the special case where a *single* file
566 +# is committed to repeatedly, the emails giving notification of these commits
567 +# can be threaded together automatically by the mail client.
568 +def inject_threading_headers(mail)
569 + return unless $fileEntries.length == 1
570 + file = $fileEntries[0]
571 + name = zap_header_special_chars(file.path)
573 + mail.header("References", make_msg_id("#{name}.#{file.fromVer}", $hostname))
576 + mail.header("Message-ID", make_msg_id("#{name}.#{file.toVer}", $hostname))
583 mailer = SMTPMailer.new($smtp_host)
584 @@ -1769,6 +1952,7 @@
586 mailer.send($from_address, $recipients) do |mail|
587 mail.header("Subject", mailSubject)
588 + inject_threading_headers(mail)
589 mail.header("MIME-Version", "1.0")
590 mail.header("Content-Type", "text/html" + ($charset.nil? ? "" : "; charset=\"#{$charset}\""))
591 if ENV['REMOTE_HOST']