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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.4 - (show annotations)
Sat Jan 18 23:54:58 2003 UTC (21 years, 5 months ago) by jonen
Branch: MAIN
Changes since 1.3: +1 -1 lines
+ updated

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

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