1 --- ../cvsspam2/cvsspam-0.2.12/cvsspam.rb 2005-07-11 16:53:29.000000000 +0100
2 +++ cvsspam.rb 2007-09-21 11:08:17.000000000 +0100
7 +# outputs commit log comment text supplied by LogReader as preformatted Text
8 +class TextCommentHandler < LineConsumer
26 + $mailSubject = line unless $mailSubject.length > 0
27 + @comment += line += "\n"
32 + unless @comment == @lastComment
33 + #println("<pre class=\"comment\">")
35 + $commentEncoder.gsub!(encoded)
38 + @lastComment = @comment
44 # Handle lines from LogReader that represent the name of the branch tag for
45 # the next file in the log. When files are committed to the trunk, the log
50 +# Reads a line giving the path and name of the current file being considered
51 +# from our log of all files changed in this commit. Subclasses make different
52 +# records depending on whether this commit adds, removes, or just modifies this
54 +class TextFileHandler < LineConsumer
55 + def setTagHandler(handler)
56 + @tagHandler = handler
60 + $file = FileEntry.new(line)
61 + if $diff_output_limiter.choose_to_limit?
62 + $file.has_diff = false
64 + #$fileEntries << $file
71 + @tagHandler.getLastTag
75 # A do-nothing superclass for objects that know how to create hyperlinks to
76 # web CVS interfaces (e.g. CVSweb). Subclasses overide these methods to
77 # wrap HTML link tags arround the text that this classes methods generate.
83 + def textpath(path, tag)
87 # Just returns the value of the 'version' argument. Subclasses should change
88 # this into a link to the given version of the file.
89 def version(path, version)
101 # Superclass for objects that can link to CVS frontends on the web (ViewCVS,
106 +# Note when LogReader finds record of a file that was added in this commit
107 +class TextAddedFileHandler < TextFileHandler
108 + def handleFile(file)
114 +# Note when LogReader finds record of a file that was removed in this commit
115 +class TextRemovedFileHandler < TextFileHandler
116 + def handleFile(file)
118 + file.fromVer=$fromVer
122 +# Note when LogReader finds record of a file that was modified in this commit
123 +class TextModifiedFileHandler < TextFileHandler
124 + def handleFile(file)
126 + file.fromVer=$fromVer
132 # Used by UnifiedDiffHandler to record the number of added and removed lines
133 # appearing in a unidiff.
134 @@ -1030,6 +1126,160 @@
138 +# Used by TextUnifiedDiffHandler to produce an Text
139 +class TextUnifiedDiffColouriser < LineConsumer
141 + @currentState = "@"
142 + @currentStyle = "info"
143 + @lineJustDeleted = nil
144 + @lineJustDeletedSuperlong = false
145 + @truncatedLineCount = 0
153 + initial = line[0,1]
154 + superlong_line = false
155 + if $maxDiffLineLength && line.length > $maxDiffLineLength+1
156 + line = line[0, $maxDiffLineLength+1]
157 + superlong_line = true
158 + @truncatedLineCount += 1
160 + if initial != @currentState
163 + if initial=="+" && @currentState=="-" && @lineJustDeleted!=nil
164 + # may be an edit, try to highlight the changes part of the line
165 + a = line[1,line.length-1]
166 + b = @lineJustDeleted[1,@lineJustDeleted.length-1]
167 + prefixLen = commonPrefixLength(a, b)+1
168 + suffixLen = commonPrefixLength(a.reverse, b.reverse)
169 + # prevent prefix/suffux having overlap,
170 + suffixLen = min(suffixLen, min(line.length,@lineJustDeleted.length)-prefixLen)
171 + deleteInfixSize = @lineJustDeleted.length - (prefixLen+suffixLen)
172 + addInfixSize = line.length - (prefixLen+suffixLen)
173 + oversize_change = deleteInfixSize*100/@lineJustDeleted.length>33 || addInfixSize*100/line.length>33
175 + if prefixLen==1 && suffixLen==0 || deleteInfixSize<=0 || oversize_change
176 + print(@lineJustDeleted)
178 + print(@lineJustDeleted[0,prefixLen])
179 + print(@lineJustDeleted[prefixLen,deleteInfixSize])
180 + print(@lineJustDeleted[@lineJustDeleted.length-suffixLen,suffixLen])
187 + @lineJustDeleted = nil
190 + @lineJustDeleted=line
191 + @lineJustDeletedSuperlong = superlong_line
193 + # we'll print it next time (fingers crossed)
195 + elsif @lineJustDeleted!=nil
196 + print(@lineJustDeleted)
197 + if @lineJustDeletedSuperlong
202 + @lineJustDeleted = nil
205 + if prefixLen==1 && suffixLen==0 || addInfixSize<=0 || oversize_change
208 + encoded = line[0,prefixLen] +
209 + line[prefixLen,addInfixSize] +
210 + line[line.length-suffixLen,suffixLen]
216 + unless @lineJustDeleted==nil
217 + print(@lineJustDeleted)
218 + if @lineJustDeletedSuperlong
223 + @lineJustDeleted=nil
235 + unless @lineJustDeleted==nil
236 + print(@lineJustDeleted)
237 + if @lineJustDeletedSuperlong
242 + @lineJustDeleted = nil
245 + if @truncatedLineCount>0
246 + println("[Note: Some over-long lines of diff output only partialy shown]")
250 + # start the diff output, using the given lines as the 'preamble' bit
251 + def start_output(*lines)
252 + println("--------------------------------------------------------------------------------")
255 + print($frontend.textpath($file.basedir, $file.tag))
257 + println("#{$file.file} added at #{$frontend.version($file.path,$file.toVer)}")
259 + print($frontend.textpath($file.basedir, $file.tag))
261 + println("#{$file.file} removed after #{$frontend.version($file.path,$file.fromVer)}")
263 + print($frontend.textpath($file.basedir, $file.tag))
265 + println("#{$file.file} #{$frontend.version($file.path,$file.fromVer)} #{$frontend.textdiff($file)} #{$frontend.version($file.path,$file.toVer)}")
267 + lines.each do |line|
274 + def formatChange(text)
275 + return '^M' if text=="\r"
278 + def shift(nextState)
279 + @currentState = nextState
282 + def commonPrefixLength(a, b)
284 + a.each_byte do |char|
285 + break unless b[length]==char
286 + length = length + 1
293 # Handle lines from LogReader that are the output from 'cvs diff -u' for the
294 # particular file under consideration
295 @@ -1084,6 +1334,57 @@
299 +# Handle lines from LogReader that are the output from 'cvs diff -u' for the
300 +# particular file under consideration for Text
301 +class TextUnifiedDiffHandler < LineConsumer
303 + @stats = UnifiedDiffStats.new
304 + @colour = TextUnifiedDiffColouriser.new
305 + @colour.output = @emailIO
316 + if $file.wants_diff_in_mail?
317 + @colour.start_output(@diffline, @lookahead, line)
320 + @stats.consume(line)
321 + if $file.wants_diff_in_mail?
322 + if $maxLinesPerDiff.nil? || @stats.diffLines < $maxLinesPerDiff
323 + @colour.consume(line)
324 + elsif @stats.diffLines == $maxLinesPerDiff
325 + @colour.consume(line)
333 + if @lookahead == nil
334 + $file.isEmpty = true
335 + elsif @lookahead =~ /Binary files .* and .* differ/
336 + $file.isBinary = true
338 + if $file.wants_diff_in_mail?
339 + if $maxLinesPerDiff && @stats.diffLines > $maxLinesPerDiff
340 + println("[truncated at #{$maxLinesPerDiff} lines; #{@stats.diffLines-$maxLinesPerDiff} more skipped]")
344 + $file.has_diff = true
351 # a filter that counts the number of characters output to the underlying object
353 @@ -1381,6 +1682,18 @@
354 $handlers["R"].setTagHandler(tagHandler)
355 $handlers["M"].setTagHandler(tagHandler)
357 +$texthandlers = Hash[">" => TextCommentHandler.new,
358 + "U" => TextUnifiedDiffHandler.new,
360 + "A" => TextAddedFileHandler.new,
361 + "R" => TextRemovedFileHandler.new,
362 + "M" => TextModifiedFileHandler.new,
363 + "V" => VersionHandler.new]
365 +$texthandlers["A"].setTagHandler(tagHandler)
366 +$texthandlers["R"].setTagHandler(tagHandler)
367 +$texthandlers["M"].setTagHandler(tagHandler)
369 $fileEntries = Array.new
370 $task_list = Array.new
372 @@ -1403,6 +1716,24 @@
376 +File.open("#{$logfile}.emailtexttmp", File::RDWR|File::CREAT|File::TRUNC) do |mail|
378 + $diff_output_limiter = OutputSizeLimiter.new(mail, $mail_size_limit)
380 + File.open($logfile) do |log|
381 + reader = LogReader.new(log)
384 + handler = $texthandlers[reader.currentLineCode]
386 + raise "No handler file lines marked '##{reader.currentLineCode}'"
388 + handler.handleLines(reader.getLines, $diff_output_limiter)
394 if $subjectPrefix == nil
395 $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
397 @@ -1432,7 +1763,10 @@
399 # generate the email header (and footer) having already generated the diffs
400 # for the email body to a temp file (which is simply included in the middle)
401 -def make_html_email(mail)
402 +def make_html_email(mail, boundary)
403 + mail.puts("--#{boundary}")
404 + mail.puts("Content-Type: text/html;" + ($charset.nil? ? "" : "; charset=\"#{$charset}\""))
405 + mail.puts("Content-Disposition: inline\n\n");
409 @@ -1660,7 +1994,7 @@
410 mail.puts("<center><small><a href=\"http://www.badgers-in-foil.co.uk/projects/cvsspam/\" title=\"commit -> email\">CVSspam</a> #{$version}</small></center>")
412 mail.puts("</body></html>")
414 + mail.puts("\n\n--#{boundary}--\n\n")
417 # Tries to look up an 'alias' email address for the given string in the
418 @@ -1818,11 +2152,188 @@
420 $from_address = sender_alias($from_address) unless $from_address.nil?
422 +def rand_string(len)
423 + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
425 + 1.upto(len) { |i| newpass << chars[rand(chars.size-1)] }
429 +def make_text_email(mail, boundary)
432 +Content-Type: text/plain; charset=us-ascii
433 +Content-Disposition: inline
438 + Repository.each do |repository|
439 + haveTags |= repository.has_multiple_tags
445 + totalLinesAdded = 0
446 + totalLinesRemoved = 0
449 + last_repository = nil
450 + $fileEntries.each do |file|
451 + unless file.repository == last_repository
452 + last_repository = file.repository
453 + if last_repository.has_multiple_tags
454 + mail.print("Mixed-tag commit")
456 + mail.print("Commit")
458 + mail.print(" in #{last_repository.common_prefix}")
459 + if last_repository.trunk_only?
460 + mail.print(" on MAIN")
464 + last_repository.each_tag do |tag|
467 + mail.print tagCount<last_repository.tag_count ? ", " : " & "
469 + mail.print tag ? tag : "MAIN"
477 + elsif file.removal?
479 + elsif file.modification?
482 + name = file.name_after_common_prefix
483 + slashPos = name.rindex("/")
487 + thisPath = name[0,slashPos]
488 + name = name[slashPos+1,name.length]
489 + if thisPath == lastPath
490 + prefix = " "*(slashPos) + "/"
492 + prefix = thisPath + "/"
494 + lastPath = thisPath
498 + elsif file.removal?
502 + mail.print("#{prefix}#{name} ")
504 + mail.print("#{prefix}#{name} ")
507 + mail.print("[empty] ")
508 + elsif file.isBinary
509 + mail.print("[binary] ")
511 + if file.lineAdditions>0
512 + totalLinesAdded += file.lineAdditions
513 + mail.print("+#{file.lineAdditions} ")
515 + if file.lineRemovals>0
516 + totalLinesRemoved += file.lineRemovals
517 + mail.print("-#{file.lineRemovals} ")
520 + if last_repository.has_multiple_tags
522 + mail.print("#{file.tag} ")
524 + mail.print("MAIN ")
528 + mail.print("added #{$frontend.version(file.path,file.toVer)} ")
529 + elsif file.removal?
530 + mail.print("#{$frontend.version(file.path,file.fromVer)} removed ")
531 + elsif file.modification?
532 + mail.print("#{$frontend.version(file.path,file.fromVer)} #{$frontend.textdiff(file)} #{$frontend.version(file.path,file.toVer)} ")
537 + if $fileEntries.size>1 && (totalLinesAdded+totalLinesRemoved)>0
538 + if totalLinesAdded>0
539 + mail.print("+#{totalLinesAdded} ")
541 + if totalLinesRemoved>0
542 + mail.print("-#{totalLinesRemoved} ")
547 + totalFilesChanged = filesAdded+filesRemoved+filesModified
548 + if totalFilesChanged > 1
551 + mail.print("#{filesAdded} added")
555 + mail.print(" + ") if changeKind>0
556 + mail.print("#{filesRemoved} removed")
560 + mail.print(" + ") if changeKind>0
561 + mail.print("#{filesModified} modified")
564 + mail.print(", total #{totalFilesChanged}") if changeKind > 1
565 + mail.puts(" files\n")
568 + if $task_list.size > 0
570 + $task_list.each do |item|
572 + item = htmlEncode(item)
573 + mail.puts("* #{item}\n")
578 + File.open("#{$logfile}.emailtexttmp") do |input|
579 + input.each do |line|
580 + mail.puts(line.chomp)
583 + if $diff_output_limiter.choose_to_limit?
584 + mail.puts("[Reached #{$diff_output_limiter.written_count} bytes of diffs.")
585 + mail.puts("Since the limit is about #{$mail_size_limit} bytes,")
586 + mail.puts("a further #{$diff_output_limiter.total_count-$diff_output_limiter.written_count} were skipped.]")
589 + blah("leaving file #{$logfile}.emailtexttmp")
591 + File.unlink("#{$logfile}.emailtexttmp")
594 + mail.puts("CVSspam #{$version}")
597 mailer.send($from_address, $recipients) do |mail|
598 mail.header("Subject", mailSubject)
599 inject_threading_headers(mail)
600 mail.header("MIME-Version", "1.0")
601 - mail.header("Content-Type", "text/html" + ($charset.nil? ? "" : "; charset=\"#{$charset}\""))
602 + boundary = rand_string(32)
603 + mail.header("Content-Type", "multipart/alternative; boundary=\"#{boundary}\"")
604 + mail.header("Content-Disposition", "inline")
605 if ENV['REMOTE_HOST']
606 # TODO: I think this will always be an IP address. If a hostname is
607 # possible, it may need encoding of some kind,
608 @@ -1836,6 +2347,7 @@
609 mail.header("X-Mailer", "CVSspam #{$version} <http://www.badgers-in-foil.co.uk/projects/cvsspam/>")
612 - make_html_email(body)
613 + make_text_email(body, boundary)
614 + make_html_email(body, boundary)