/[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.1 - (show annotations)
Sat Jan 18 23:45:56 2003 UTC (21 years, 10 months ago) by jonen
Branch: MAIN
+ added file name at mail 'subject'

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 mailSubject = "#{$subjectPrefix} file #{$mailSubject}"
755 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