/[cvs]/jonen/ruby/CVSspam/cvsspam.rb
ViewVC logotype

Annotation of /jonen/ruby/CVSspam/cvsspam.rb

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.2 - (hide annotations)
Sat Jan 18 23:50:37 2003 UTC (21 years, 5 months ago) by jonen
Branch: MAIN
Changes since 1.1: +1 -1 lines
+ updated

1 jonen 1.1 #!/usr/bin/ruby
2    
3     # collect_diffs.rb expects to find this script in the same directory as it
4     #
5    
6     # TODO: exemplify syntax for 'cvs admin -m' when log message is missing
7     # TODO: make max-line limit on diff output configurable
8     # TODO: put max size limit on whole email
9     # TODO: support non-html mail too (text/plain, multipart/alternative)
10    
11     $maxSubjectLength = 200
12     $maxLinesPerDiff = 1000
13     $charset = nil # nil implies 'don't specify a charset'
14    
15     def blah(text)
16     puts("cvsspam.rb: #{text}") if $debug
17     end
18    
19     def min(a, b)
20     a<b ? a : b
21     end
22    
23    
24     # the regexps given as keys must not use capturing subexpressions '(...)'
25     class MultiSub
26     def initialize(hash)
27     @mash = Array.new
28     expr = nil
29     hash.each do |key,val|
30     if expr == nil ; expr="(" else expr<<"|(" end
31     expr << key << ")"
32     @mash << val
33     end
34     @re = Regexp.new(expr)
35     end
36    
37     def gsub!(text)
38     text.gsub!(@re) { |match|
39     idx = -1
40     $~.to_a.each { |subexp|
41     break unless idx==-1 || subexp==nil
42     idx += 1
43     }
44     idx==-1 ? match : @mash[idx].call(match)
45     }
46     end
47     end
48    
49     class LogReader
50     def initialize(logIO)
51     @io = logIO
52     advance
53     end
54    
55     def currentLineCode ; @line[1,1] end
56    
57    
58     class ConstrainedIO
59     def initialize(reader)
60     @reader = reader
61     @linecode = reader.currentLineCode
62     end
63    
64     def each
65     return if @reader == nil
66     while true
67     yield @reader.currentLine
68     break unless @reader.advance && currentValid?
69     end
70     @reader = nil
71     end
72    
73     def gets
74     return nil if @reader == nil
75     line = @reader.currentLine
76     return nil if line==nil || !currentValid?
77     @reader.advance
78     return line
79     end
80    
81     def currentValid?
82     @linecode == @reader.currentLineCode
83     end
84     end
85    
86     def getLines
87     ConstrainedIO.new(self)
88     end
89    
90     def eof ; @line==nil end
91    
92     def advance
93     @line = @io.gets
94     return false if @line == nil
95     unless @line[0,1] == "#"
96     raise "#{$logfile}:#{@io.lineno} line did not begin with '#': #{@line}"
97     end
98     return true
99     end
100    
101     def currentLine
102     @line==nil ? nil : @line[3, @line.length-4]
103     end
104     end
105    
106    
107     def htmlEncode(text)
108     text.gsub(/./) do
109     case $&
110     when "&" then "&amp;"
111     when "<" then "&lt;"
112     when ">" then "&gt;"
113     when "\"" then "&quot;"
114     else $&
115     end
116     end
117     end
118    
119     # actually, allows '/' to appear
120     def urlEncode(text)
121     text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
122     "%#{sprintf('%2X', $&[0])}"
123     end
124     end
125    
126     # a top-level directory under the $CVSROOT
127     class Repository
128     @@repositories = Hash.new
129    
130     def initialize(name)
131     @name = name
132     @common_prefix = nil
133     end
134    
135     # calculate the path prefix shared by all files commited to this
136     # reposotory
137     def merge_common_prefix(path)
138     if @common_prefix == nil
139     @common_prefix = path.dup
140     else
141     path = path.dup
142     until @common_prefix == path
143     if @common_prefix.size>path.size
144     if @common_prefix.sub!(/(.*)\/.*$/, '\1').nil?
145     raise "unable to merge '#{path}' in to '#{@common_prefix}': prefix totally different"
146     end
147     else
148     if path.sub!(/(.*)\/.*$/, '\1').nil?
149     raise "unable to merge '#{path}' in to '#{@common_prefix}': prefix totally different"
150     end
151     end
152     end
153     end
154     end
155    
156     attr_reader :name, :common_prefix
157    
158     def Repository.get(name)
159     name =~ /^[^\/]+/
160     name = $&
161     rep = @@repositories[name]
162     if rep.nil?
163     rep = Repository.new(name)
164     @@repositories[name] = rep
165     end
166     rep
167     end
168    
169     def Repository.count
170     @@repositories.size
171     end
172    
173     def Repository.each
174     @@repositories.each_value do |rep|
175     yield rep
176     end
177     end
178    
179     def Repository.array
180     @@repositories.values
181     end
182    
183     def to_s
184     @name
185     end
186     end
187    
188     class FileEntry
189     def initialize(path)
190     @path = path
191     @lineAdditions = @lineRemovals = 0
192     @repository = Repository.get(path)
193     @repository.merge_common_prefix(basedir())
194     end
195    
196     attr_accessor :path, :type, :lineAdditions, :lineRemovals, :isBinary, :isEmpty, :tag, :fromVer, :toVer
197    
198     def file
199     @path =~ /.*\/(.*)/
200     $1
201     end
202    
203     def basedir
204     @path =~ /(.*)\/.*/
205     $1
206     end
207    
208     def repository
209     @repository
210     end
211    
212     def name_after_common_prefix
213     @path.slice(@repository.common_prefix.size+1,@path.size-@repository.common_prefix.size-1)
214     end
215    
216     def removal?
217     @type == "R"
218     end
219    
220     def addition?
221     @type == "A"
222     end
223    
224     def modification?
225     @type == "M"
226     end
227     end
228    
229    
230     class LineConsumer
231     def handleLines(lines, emailIO)
232     @emailIO = emailIO
233     @lineCount = 0
234     setup
235     lines.each do |line|
236     @lineCount += 1
237     consume(line)
238     end
239     teardown
240     end
241    
242     def setup
243     end
244    
245     def teardown
246     end
247    
248     def lineno
249     @lineCount
250     end
251    
252     def println(text)
253     @emailIO.puts(text)
254     end
255    
256     def print(text)
257     @emailIO.print(text)
258     end
259     end
260    
261    
262     mailSub = proc { |match| "<a href=\"mailto:#{match}\">#{match}</a>" }
263     urlSub = proc { |match| "<a href=\"#{match}\">#{match}</a>" }
264     bugSub = proc { |match|
265     match =~ /([0-9]+)/
266     "<a href=\"#{$bugzillaURL.sub(/%s/, $1)}\">#{match}</a>"
267     }
268     commentSubstitutions = {
269     '(?:mailto:)?[\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+\b' => mailSub,
270     '\b(?:http|https|ftp):[^ \t\n<>"]+[\w/]' => urlSub}
271    
272    
273     class CommentHandler < LineConsumer
274     def setup
275     @haveBlank = false
276     @comment = ""
277     end
278    
279     def consume(line)
280     if line =~ /^\s*$/
281     @haveBlank = true
282     else
283     if @haveBlank
284     @comment += "\n"
285     @haveBlank = false
286     end
287     $mailSubject = line if $mailSubject == nil
288     @comment += line += "\n"
289     end
290     end
291    
292     def teardown
293     unless @comment == @lastComment
294     println("<pre class=\"comment\">")
295     encoded = htmlEncode(@comment)
296     $commentEncoder.gsub!(encoded)
297     println(encoded)
298     println("</pre>")
299     @lastComment = @comment
300     end
301     end
302     end
303    
304    
305     class TagHandler < LineConsumer
306     def consume(line)
307     # TODO: check there is only one line
308     @tag = line
309     $haveTag = true
310     end
311    
312     def getLastTag
313     tmp = @tag
314     @tag = nil
315     tmp
316     end
317     end
318    
319     class VersionHandler < LineConsumer
320     def consume(line)
321     # TODO: check there is only one line
322     $fromVer,$toVer = line.split(/,/)
323     end
324     end
325    
326    
327     class FileHandler < LineConsumer
328     def setTagHandler(handler)
329     @tagHandler = handler
330     end
331    
332     def setup
333     $fileHeaderHtml = ''
334     println("<hr noshade=\"nodhade\" size=\"1\" /><a name=\"file#{$fileEntries.size+1}\" /><div class=\"file\">")
335     end
336    
337     def consume(line)
338     $file = FileEntry.new(line)
339     $fileEntries << $file
340     $file.tag = getTag
341     handleFile($file)
342     end
343    
344     protected
345     def getTag
346     @tagHandler.getLastTag
347     end
348    
349     def println(text)
350     $fileHeaderHtml << text << "\n"
351     end
352    
353     def print(text)
354     $fileHeaderHtml << text
355     end
356     end
357    
358     # if using viewcvs, create a link to the named path's dir listing
359     def viewcvsPath(path)
360     if $viewcvsURL == nil
361     htmlEncode(path)
362     else
363     "<a href=\"#{$viewcvsURL}#{urlEncode(path)}\">#{htmlEncode(path)}</a>"
364     end
365     end
366    
367     def viewcvsFile(path, version)
368     if $viewcvsURL == nil
369     version
370     else
371     "<a href=\"#{$viewcvsURL}#{urlEncode(path)}?rev=#{version}&content-type=text/vnd.viewcvs-markup\">#{version}</a>"
372     end
373     end
374    
375    
376     # in need of refactoring...
377    
378     class AddedFileHandler < FileHandler
379     def handleFile(file)
380     file.type="A"
381     file.toVer=$toVer
382     print("<span class=\"pathname\" id=\"added\">")
383     print(viewcvsPath(file.basedir))
384     println("<br /></span>")
385     println("<div class=\"fileheader\" id=\"added\"><big><b>#{htmlEncode(file.file)}</b></big> <small id=\"info\">added at #{viewcvsFile(file.path,file.toVer)}</small></div>")
386     end
387     end
388    
389     class RemovedFileHandler < FileHandler
390     def handleFile(file)
391     file.type="R"
392     file.fromVer=$fromVer
393     print("<span class=\"pathname\" id=\"removed\">")
394     print(viewcvsPath(file.basedir))
395     println("<br /></span>")
396     println("<div class=\"fileheader\" id=\"removed\"><big><b>#{htmlEncode(file.file)}</b></big> <small id=\"info\">removed after #{viewcvsFile(file.path,file.fromVer)}</small></div>")
397     end
398     end
399    
400     def viewcvsDiff(file)
401     if $viewcvsURL == nil || file.isBinary
402     "-&gt;"
403     else
404     "<a href=\"#{$viewcvsURL}#{urlEncode(file.path)}.diff?r1=#{file.fromVer}&r2=#{file.toVer}\">-&gt;</a>"
405     end
406     end
407    
408     class ModifiedFileHandler < FileHandler
409     def handleFile(file)
410     file.type="M"
411     file.fromVer=$fromVer
412     file.toVer=$toVer
413     print("<span class=\"pathname\">")
414     print(viewcvsPath(file.basedir))
415     println("<br /></span>")
416     println("<div class=\"fileheader\"><big><b>#{htmlEncode(file.file)}</b></big> <small id=\"info\">#{viewcvsFile(file.path,file.fromVer)} #{viewcvsDiff(file)} #{viewcvsFile(file.path,file.toVer)}</small></div>")
417     end
418     end
419    
420    
421     class UnifiedDiffStats
422     def initialize
423     @diffLines=3 # the three initial lines in the unidiff
424     end
425    
426     def diffLines
427     @diffLines
428     end
429    
430     def consume(line)
431     @diffLines += 1
432     case line[0,1]
433     when "+" then $file.lineAdditions += 1
434     when "-" then $file.lineRemovals += 1
435     end
436     end
437     end
438    
439     # TODO: change-within-line colourisation should really be comparing the
440     # set of lines just removed with the set of lines just added, but
441     # it currently considers just a single line
442    
443     class UnifiedDiffColouriser < LineConsumer
444     def initialize
445     @currentState = "@"
446     @currentStyle = "info"
447     end
448    
449     def output=(io)
450     @emailIO = io
451     end
452    
453     def consume(line)
454     initial = line[0,1]
455     if initial != @currentState
456     prefixLen = 1
457     suffixLen = 0
458     if initial=="+" && @currentState=="-" && @lineJustDeleted!=nil
459     # may be an edit, try to highlight the changes part of the line
460     a = line[1,line.length-1]
461     b = @lineJustDeleted[1,@lineJustDeleted.length-1]
462     prefixLen = commonPrefixLength(a, b)+1
463     suffixLen = commonPrefixLength(a.reverse, b.reverse)
464     # prevent prefix/suffux having overlap,
465     suffixLen = min(suffixLen, min(line.length,@lineJustDeleted.length)-prefixLen)
466     deleteInfixSize = @lineJustDeleted.length - (prefixLen+suffixLen)
467     addInfixSize = line.length - (prefixLen+suffixLen)
468     oversize_change = deleteInfixSize*100/@lineJustDeleted.length>33 || addInfixSize*100/line.length>33
469    
470     if prefixLen==1 && suffixLen==0 || deleteInfixSize<=0 || oversize_change
471     println(htmlEncode(@lineJustDeleted))
472     else
473     print(htmlEncode(@lineJustDeleted[0,prefixLen]))
474     print("<span id=\"removedchars\">")
475     print(formatChange(@lineJustDeleted[prefixLen,deleteInfixSize]))
476     print("</span>")
477     println(htmlEncode(@lineJustDeleted[@lineJustDeleted.length-suffixLen,suffixLen]))
478     end
479     @lineJustDeleted = nil
480     end
481     if initial=="-"
482     @lineJustDeleted=line
483     shift(initial)
484     # we'll print it next time (fingers crossed)
485     return
486     elsif @lineJustDeleted!=nil
487     println(htmlEncode(@lineJustDeleted))
488     @lineJustDeleted = nil
489     end
490     shift(initial)
491     if prefixLen==1 && suffixLen==0 || addInfixSize<=0 || oversize_change
492     encoded = htmlEncode(line)
493     else
494     encoded = htmlEncode(line[0,prefixLen]) +
495     "<span id=\"addedchars\">" +
496     formatChange(line[prefixLen,addInfixSize]) +
497     "</span>" +
498     htmlEncode(line[line.length-suffixLen,suffixLen])
499     end
500     else
501     encoded = htmlEncode(line)
502     end
503     if initial=="-"
504     unless @lineJustDeleted==nil
505     println(htmlEncode(@lineJustDeleted))
506     @lineJustDeleted=nil
507     end
508     end
509     if initial=="+"
510     if line =~ /\b(TODO\b.*)/
511     $todoList << $1
512     encoded.sub!(/\bTODO\b/, "<span id=\"TODO\">TODO</span>")
513     encoded = "<a name=\"todo#{$todoList.size}\" />#{encoded}"
514     elsif line =~ /\b(FIXME\b.*)/
515     $todoList << $1
516     encoded.sub!(/\FIXME\b/, "<span id=\"TODO\">FIXME</span>")
517     encoded = "<a name=\"todo#{$todoList.size}\" />#{encoded}"
518     end
519     # won't bother with /XXX/ (false positives more likely?)
520     end
521     println(encoded)
522     end
523    
524     def teardown
525     unless @lineJustDeleted==nil
526     println(htmlEncode(@lineJustDeleted))
527     @lineJustDeleted = nil
528     end
529     shift(nil)
530     end
531    
532     private
533    
534     def formatChange(text)
535     return '<small id="info">^M</small>' if text=="\r"
536     htmlEncode(text).gsub(/ /, '&nbsp;')
537     end
538    
539     def shift(nextState)
540     unless @currentState == nil
541     if @currentStyle == "info"
542     print("</small></pre>")
543     else
544     print("</pre>")
545     end
546     @currentStyle = case nextState
547     when "\\" then "info" # as in '\ No newline at end of file'
548     when "@" then "info"
549     when " " then "context"
550     when "+" then "added"
551     when "-" then "removed"
552     end
553     unless nextState == nil
554     if @currentStyle=='info'
555     print("<pre class=\"diff\"><small id=\"info\">")
556     else
557     print("<pre class=\"diff\" id=\"#{@currentStyle}\">")
558     end
559     end
560     end
561     @currentState = nextState
562     end
563    
564     def commonPrefixLength(a, b)
565     length = 0
566     a.each_byte do |char|
567     break unless b[length]==char
568     length = length + 1
569     end
570     return length
571     end
572     end
573    
574    
575     class UnifiedDiffHandler < LineConsumer
576     def setup
577     @stats = UnifiedDiffStats.new
578     if $file.removal? && $no_removed_file_diff
579     def self.consume(line)
580     @stats.consume(line) if lineno() > 3
581     end
582     return
583     end
584     @colour = UnifiedDiffColouriser.new
585     @colour.output = @emailIO
586     @lookahead = nil
587     # evil here:
588     def self.consume(line)
589     case lineno()
590     when 1
591     @diffline = line
592     when 2
593     @lookahead = line
594     when 3
595     println($fileHeaderHtml)
596     # TODO: move to UnifiedDiffColouriser
597     print("<pre class=\"diff\"><small id=\"info\">")
598     println(htmlEncode(@diffline)) # 'diff ...'
599     println(htmlEncode(@lookahead)) # '--- ...'
600     println(htmlEncode(line)) # '+++ ...'
601    
602     # now, redefine this method for the subsequent lines of input
603     def self.consume(line)
604     @stats.consume(line)
605     if @stats.diffLines < $maxLinesPerDiff
606     @colour.consume(line)
607     elsif @stats.diffLines == $maxLinesPerDiff
608     @colour.consume(line)
609     @colour.teardown
610     end
611     end
612     end
613     end
614     end
615    
616     def teardown
617     if @lookahead == nil
618     $file.isEmpty = true
619     elsif @lookahead =~ /Binary files .* and .* differ/
620     $file.isBinary = true
621     else
622     unless $file.removal? && $no_removed_file_diff
623     if @stats.diffLines > $maxLinesPerDiff
624     println("</pre>")
625     println("<strong class=\"error\">[truncated at #{$maxLinesPerDiff} lines; #{@stats.diffLines-$maxLinesPerDiff} more skipped]</strong>")
626     else
627     @colour.teardown
628     end
629     println("</div>") # end of "file" div
630     end
631     end
632     end
633     end
634    
635    
636    
637    
638    
639    
640    
641    
642    
643     $config = "#{ENV['CVSROOT']}/CVSROOT/cvsspam.conf"
644    
645     $debug = false
646     $recipients = Array.new
647     $sendmail_prog = "/usr/sbin/sendmail"
648     $no_removed_file_diff = false
649    
650     require 'getoptlong'
651    
652     opts = GetoptLong.new(
653     [ "--to", "-t", GetoptLong::REQUIRED_ARGUMENT ],
654     [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
655     [ "--debug", "-d", GetoptLong::NO_ARGUMENT ]
656     )
657    
658     opts.each do |opt, arg|
659     $recipients << arg if opt=="--to"
660     $config = arg if opt=="--config"
661     $debug = true if opt=="--debug"
662     end
663    
664    
665     if ARGV.length != 1
666     if ARGV.length > 1
667     $stderr.puts "extra arguments not needed: #{ARGV[1, ARGV.length-1].join(', ')}"
668     else
669     $stderr.puts "missing required file argument"
670     end
671     puts "Usage: cvsspam.rb [ --to <email> ] [ --config <file> ] <collect_diffs file>"
672     exit(-1)
673     end
674    
675     $logfile = ARGV[0]
676    
677    
678     $additionalHeaders = Array.new
679     $problemHeaders = Array.new
680    
681     # helper functions called from the 'config file'
682     def addHeader(name, value)
683     if name =~ /^[!-9;-~]+$/
684     $additionalHeaders << [name, value]
685     else
686     $problemHeaders << [name, value]
687     end
688     end
689     def addRecipient(email)
690     $recipients << email
691     end
692    
693     if FileTest.exists?($config)
694     load $config
695     else
696     blah("Config file '#{$config}' not found, ignoring")
697     end
698    
699     if $recipients.empty?
700     fail "No email recipients defined"
701     end
702    
703     $viewcvsURL << "/" if $viewcvsURL!=nil && $viewcvsURL!~/\/$/
704    
705    
706     unless $bugzillaURL == nil
707     commentSubstitutions['\b[Bb][Uu][Gg]\s*#?[0-9]+'] = bugSub
708     end
709     $commentEncoder = MultiSub.new(commentSubstitutions)
710    
711    
712     tagHandler = TagHandler.new
713    
714     $handlers = Hash[">" => CommentHandler.new,
715     "U" => UnifiedDiffHandler.new,
716     "T" => tagHandler,
717     "A" => AddedFileHandler.new,
718     "R" => RemovedFileHandler.new,
719     "M" => ModifiedFileHandler.new,
720     "V" => VersionHandler.new]
721    
722     $handlers["A"].setTagHandler(tagHandler)
723     $handlers["R"].setTagHandler(tagHandler)
724     $handlers["M"].setTagHandler(tagHandler)
725    
726     $fileEntries = Array.new
727     $todoList = Array.new
728    
729     File.open("#{$logfile}.emailtmp", File::RDWR|File::CREAT|File::TRUNC) do |mail|
730    
731     File.open($logfile) do |log|
732     reader = LogReader.new(log)
733    
734     until reader.eof
735     handler = $handlers[reader.currentLineCode]
736     if handler == nil
737     raise "No handler file lines marked '##{reader.currentLineCode}'"
738     end
739     handler.handleLines(reader.getLines, mail)
740     end
741     end
742     end
743    
744     if $subjectPrefix == nil
745     $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
746     end
747     $fileEntries.each do |file|
748     unless file.repository == last_repository
749     last_repository = file.repository
750     file = #{htmlEncode(last_repository.common_prefix)}
751     all_files = all_files + " " + file
752     end
753     end
754 jonen 1.2 mailSubject = "#{$subjectPrefix} all_files #{$mailSubject}"
755 jonen 1.1 if mailSubject.length > $maxSubjectLength
756     mailSubject = mailSubject[0, $maxSubjectLength]
757     end
758    
759     blah("invoking #{$sendmail_prog} -t")
760     IO.popen("#{$sendmail_prog} -t", "w") do |mail|
761     mail.puts("To: #{$recipients.join(',')}")
762     mail.puts("Subject: #{mailSubject}")
763     mail.puts("Content-Type: text/html" + ($charset.nil? ? "" : "; charset=\"#{$charset}\""))
764     unless ($additionalHeaders.empty?)
765     $additionalHeaders.each do |header|
766     mail.puts("#{header[0]}: #{header[1]}")
767     end
768     end
769     mail.puts # end-of-headers
770    
771     mail.puts(<<HEAD)
772     <html>
773     <head>
774     <style><!--
775     body {background-color:#ffffff;}
776     .file {border:1px solid #eeeeee;margin-top:1em;margin-bottom:1em;}
777     .pathname {font-family:monospace; float:right;}
778     .fileheader {margin-bottom:.5em;}
779     .diff {margin:0;}
780     .todolist {padding:4px;border:1px dashed #000000;margin-top:1em;}
781     .todolist ul {margin-top:0;margin-bottom:0;}
782     tr.alt {background-color:#eeeeee}
783     #added {background-color:#ddffdd;}
784     #addedchars {background-color:#99ff99;font-weight:bolder;}
785     tr.alt #added {background-color:#ccf7cc;}
786     #removed {background-color:#ffdddd;}
787     #removedchars {background-color:#ff9999;font-weight:bolder;}
788     tr.alt #removed {background-color:#f7cccc;}
789     #info {color:#888888;}
790     #context {background-color:#eeeeee;}
791     td {padding-left:.3em;padding-right:.3em;}
792     tr.head {border-bottom-width:1px;border-bottom-style:solid;}
793     tr.head td {padding:0;padding-top:.2em;}
794     #todo {background-color:#ffff00;}
795     .comment {padding:4px;border:1px dashed #000000;background-color:#ffffdd}
796     .error {color:red;}
797     --></style>
798     </head>
799     <body>
800     HEAD
801    
802     unless ($problemHeaders.empty?)
803     mail.puts("<strong class=\"error\">Bad header format in '#{$config}':<ul>")
804     $stderr.puts("Bad header format in '#{$config}':")
805     $problemHeaders.each do |header|
806     mail.puts("<li><pre>#{htmlEncode(header[0])}</pre></li>")
807     $stderr.puts(" - #{header[0]}")
808     end
809     mail.puts("</ul></strong>")
810     end
811     mail.puts("<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" rules=\"cols\">")
812     filesAdded = 0
813     filesRemoved = 0
814     filesModified = 0
815     totalLinesAdded = 0
816     totalLinesRemoved = 0
817     file_count = 0
818     lastPath = ""
819     last_repository = nil
820     $fileEntries.each do |file|
821     unless file.repository == last_repository
822     last_repository = file.repository
823     mail.print("<tr class=\"head\"><td colspan=\"#{$haveTag ? 5 : 4}\">")
824     mail.print("Commit in <b><tt>#{htmlEncode(last_repository.common_prefix)}</tt></b>")
825     mail.puts("</td></tr>")
826     end
827     file_count += 1
828     if (file_count%2==0)
829     mail.print("<tr class=\"alt\">")
830     else
831     mail.print("<tr>")
832     end
833     if file.addition?
834     filesAdded += 1
835     elsif file.removal?
836     filesRemoved += 1
837     elsif file.modification?
838     filesModified += 1
839     end
840     name = htmlEncode(file.name_after_common_prefix)
841     slashPos = name.rindex("/")
842     if slashPos==nil
843     prefix = ""
844     else
845     thisPath = name[0,slashPos]
846     name = name[slashPos+1,name.length]
847     if thisPath == lastPath
848     prefix = "&nbsp;"*(slashPos) + "/"
849     else
850     prefix = thisPath + "/"
851     end
852     lastPath = thisPath
853     end
854     if file.addition?
855     name = "<span id=\"added\">#{name}</span>"
856     elsif file.removal?
857     name = "<span id=\"removed\">#{name}</span>"
858     end
859     if file.isEmpty || file.isBinary || (file.removal? && $no_removed_file_diff)
860     mail.print("<td><tt>#{prefix}#{name}&nbsp;</tt></td>")
861     else
862     mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a>&nbsp;</tt></td>")
863     end
864     if file.isEmpty
865     mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
866     elsif file.isBinary
867     mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[binary]</small></td>")
868     else
869     if file.lineAdditions>0
870     totalLinesAdded += file.lineAdditions
871     mail.print("<td align=\"right\" id=\"added\">+#{file.lineAdditions}</td>")
872     else
873     mail.print("<td>&nbsp;</td>")
874     end
875     if file.lineRemovals>0
876     totalLinesRemoved += file.lineRemovals
877     mail.print("<td align=\"right\" id=\"removed\">-#{file.lineRemovals}</td>")
878     else
879     mail.print("<td>&nbsp;</td>")
880     end
881     end
882     if file.tag
883     mail.print("<td>#{htmlEncode(file.tag)}</td>")
884     elsif $haveTag
885     mail.print("<td>&nbsp;</td>")
886     end
887     if file.addition?
888     mail.print("<td align=\"right\">added #{viewcvsFile(file.path,file.toVer)}</td>")
889     elsif file.removal?
890     mail.print("<td>#{viewcvsFile(file.path,file.fromVer)} removed</td>")
891     elsif file.modification?
892     mail.print("<td align=\"center\">#{viewcvsFile(file.path,file.fromVer)} #{viewcvsDiff(file)} #{viewcvsFile(file.path,file.toVer)}</td>")
893     end
894    
895     mail.puts("</tr>")
896     end
897     if $fileEntries.size>1
898     # give total number of lines added/removed accross all files
899     mail.print("<tr><td>&nbsp;</td>")
900     if totalLinesAdded>0
901     mail.print("<td align=\"right\" id=\"added\">+#{totalLinesAdded}</td>")
902     else
903     mail.print("<td>&nbsp;</td>")
904     end
905     if totalLinesRemoved>0
906     mail.print("<td align=\"right\" id=\"removed\">-#{totalLinesRemoved}</td>")
907     else
908     mail.print("<td>&nbsp;</td>")
909     end
910     mail.print("<td>&nbsp;</td>") if $haveTag
911     mail.puts("<td>&nbsp;</td></tr>")
912     end
913    
914     mail.puts("</table>")
915    
916     totalFilesChanged = filesAdded+filesRemoved+filesModified
917     if totalFilesChanged > 1
918     mail.print("<small id=\"info\">")
919     changeKind = 0
920     if filesAdded>0
921     mail.print("#{filesAdded} added")
922     changeKind += 1
923     end
924     if filesRemoved>0
925     mail.print(" + ") if changeKind>0
926     mail.print("#{filesRemoved} removed")
927     changeKind += 1
928     end
929     if filesModified>0
930     mail.print(" + ") if changeKind>0
931     mail.print("#{filesModified} modified")
932     changeKind += 1
933     end
934     mail.print(", total #{totalFilesChanged}") if changeKind > 1
935     mail.puts(" files</small><br />")
936     end
937    
938     todo_count = 0
939     $todoList.each do |item|
940     if todo_count == 0
941     mail.puts("<div class=\"todolist\"><ul>")
942     end
943     todo_count += 1
944     item = htmlEncode(item)
945     mail.puts("<li><a href=\"#todo#{todo_count}\">#{item}</a></li>")
946     end
947     mail.puts("</ul></div>") if todo_count>0
948    
949    
950     File.open("#{$logfile}.emailtmp") do |input|
951     input.each do |line|
952     mail.puts(line)
953     end
954     end
955     if $debug
956     blah("leaving file #{$logfile}.emailtmp")
957     else
958     File.unlink("#{$logfile}.emailtmp")
959     end
960    
961     mail.puts("</body></html>")
962    
963     end

MailToCvsAdmin">MailToCvsAdmin
ViewVC Help
Powered by ViewVC 1.1.26 RSS 2.0 feed