/[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.6 - (show annotations)
Sun Jan 19 01:03:31 2003 UTC (21 years, 10 months ago) by jonen
Branch: MAIN
Changes since 1.5: +18 -9 lines
+ change seperator for file names at mail subject
+ add comment

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 all_files = ""
731
732 File.open("#{$logfile}.emailtmp", File::RDWR|File::CREAT|File::TRUNC) do |mail|
733
734 File.open($logfile) do |log|
735 reader = LogReader.new(log)
736
737 until reader.eof
738 handler = $handlers[reader.currentLineCode]
739 if handler == nil
740 raise "No handler file lines marked '##{reader.currentLineCode}'"
741 end
742 handler.handleLines(reader.getLines, mail)
743 end
744 end
745 end
746
747 if $subjectPrefix == nil
748 $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
749 end
750 # HACK: put all file names at mail subject
751 $fileEntries.each do |file|
752 name = htmlEncode(file.name_after_common_prefix)
753 if all_files == nil
754 all_files = name
755 else
756 all_files = all_files + ";" + name
757 end
758 end
759
760 mailSubject = "#{$subjectPrefix} #{all_files} #{$mailSubject}"
761 if mailSubject.length > $maxSubjectLength
762 mailSubject = mailSubject[0, $maxSubjectLength]
763 end
764
765 blah("invoking #{$sendmail_prog} -t")
766 IO.popen("#{$sendmail_prog} -t", "w") do |mail|
767 mail.puts("To: #{$recipients.join(',')}")
768 mail.puts("Subject: #{mailSubject}")
769 mail.puts("Content-Type: text/html" + ($charset.nil? ? "" : "; charset=\"#{$charset}\""))
770 unless ($additionalHeaders.empty?)
771 $additionalHeaders.each do |header|
772 mail.puts("#{header[0]}: #{header[1]}")
773 end
774 end
775 mail.puts # end-of-headers
776
777 mail.puts(<<HEAD)
778 <html>
779 <head>
780 <style><!--
781 body {background-color:#ffffff;}
782 .file {border:1px solid #eeeeee;margin-top:1em;margin-bottom:1em;}
783 .pathname {font-family:monospace; float:right;}
784 .fileheader {margin-bottom:.5em;}
785 .diff {margin:0;}
786 .todolist {padding:4px;border:1px dashed #000000;margin-top:1em;}
787 .todolist ul {margin-top:0;margin-bottom:0;}
788 tr.alt {background-color:#eeeeee}
789 #added {background-color:#ddffdd;}
790 #addedchars {background-color:#99ff99;font-weight:bolder;}
791 tr.alt #added {background-color:#ccf7cc;}
792 #removed {background-color:#ffdddd;}
793 #removedchars {background-color:#ff9999;font-weight:bolder;}
794 tr.alt #removed {background-color:#f7cccc;}
795 #info {color:#888888;}
796 #context {background-color:#eeeeee;}
797 td {padding-left:.3em;padding-right:.3em;}
798 tr.head {border-bottom-width:1px;border-bottom-style:solid;}
799 tr.head td {padding:0;padding-top:.2em;}
800 #todo {background-color:#ffff00;}
801 .comment {padding:4px;border:1px dashed #000000;background-color:#ffffdd}
802 .error {color:red;}
803 --></style>
804 </head>
805 <body>
806 HEAD
807
808 unless ($problemHeaders.empty?)
809 mail.puts("<strong class=\"error\">Bad header format in '#{$config}':<ul>")
810 $stderr.puts("Bad header format in '#{$config}':")
811 $problemHeaders.each do |header|
812 mail.puts("<li><pre>#{htmlEncode(header[0])}</pre></li>")
813 $stderr.puts(" - #{header[0]}")
814 end
815 mail.puts("</ul></strong>")
816 end
817 mail.puts("<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" rules=\"cols\">")
818 filesAdded = 0
819 filesRemoved = 0
820 filesModified = 0
821 totalLinesAdded = 0
822 totalLinesRemoved = 0
823 file_count = 0
824 lastPath = ""
825 last_repository = nil
826 $fileEntries.each do |file|
827 unless file.repository == last_repository
828 last_repository = file.repository
829 mail.print("<tr class=\"head\"><td colspan=\"#{$haveTag ? 5 : 4}\">")
830 mail.print("Commit in <b><tt>#{htmlEncode(last_repository.common_prefix)}</tt></b>")
831 mail.puts("</td></tr>")
832 end
833 file_count += 1
834 if (file_count%2==0)
835 mail.print("<tr class=\"alt\">")
836 else
837 mail.print("<tr>")
838 end
839 if file.addition?
840 filesAdded += 1
841 elsif file.removal?
842 filesRemoved += 1
843 elsif file.modification?
844 filesModified += 1
845 end
846 name = htmlEncode(file.name_after_common_prefix)
847 slashPos = name.rindex("/")
848 if slashPos==nil
849 prefix = ""
850 else
851 thisPath = name[0,slashPos]
852 name = name[slashPos+1,name.length]
853 if thisPath == lastPath
854 prefix = "&nbsp;"*(slashPos) + "/"
855 else
856 prefix = thisPath + "/"
857 end
858 lastPath = thisPath
859 end
860 if all_files == nil
861 all_files = name
862 else
863 all_files = all_files + " " + name
864 end
865 if file.addition?
866 name = "<span id=\"added\">#{name}</span>"
867 elsif file.removal?
868 name = "<span id=\"removed\">#{name}</span>"
869 end
870 if file.isEmpty || file.isBinary || (file.removal? && $no_removed_file_diff)
871 mail.print("<td><tt>#{prefix}#{name}&nbsp;</tt></td>")
872 else
873 mail.print("<td><tt>#{prefix}<a href=\"#file#{file_count}\">#{name}</a>&nbsp;</tt></td>")
874 end
875 if file.isEmpty
876 mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
877 elsif file.isBinary
878 mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[binary]</small></td>")
879 else
880 if file.lineAdditions>0
881 totalLinesAdded += file.lineAdditions
882 mail.print("<td align=\"right\" id=\"added\">+#{file.lineAdditions}</td>")
883 else
884 mail.print("<td>&nbsp;</td>")
885 end
886 if file.lineRemovals>0
887 totalLinesRemoved += file.lineRemovals
888 mail.print("<td align=\"right\" id=\"removed\">-#{file.lineRemovals}</td>")
889 else
890 mail.print("<td>&nbsp;</td>")
891 end
892 end
893 if file.tag
894 mail.print("<td>#{htmlEncode(file.tag)}</td>")
895 elsif $haveTag
896 mail.print("<td>&nbsp;</td>")
897 end
898 if file.addition?
899 mail.print("<td align=\"right\">added #{viewcvsFile(file.path,file.toVer)}</td>")
900 elsif file.removal?
901 mail.print("<td>#{viewcvsFile(file.path,file.fromVer)} removed</td>")
902 elsif file.modification?
903 mail.print("<td align=\"center\">#{viewcvsFile(file.path,file.fromVer)} #{viewcvsDiff(file)} #{viewcvsFile(file.path,file.toVer)}</td>")
904 end
905
906 mail.puts("</tr>")
907 end
908
909 if $fileEntries.size>1
910 # give total number of lines added/removed accross all files
911 mail.print("<tr><td>&nbsp;</td>")
912 if totalLinesAdded>0
913 mail.print("<td align=\"right\" id=\"added\">+#{totalLinesAdded}</td>")
914 else
915 mail.print("<td>&nbsp;</td>")
916 end
917 if totalLinesRemoved>0
918 mail.print("<td align=\"right\" id=\"removed\">-#{totalLinesRemoved}</td>")
919 else
920 mail.print("<td>&nbsp;</td>")
921 end
922 mail.print("<td>&nbsp;</td>") if $haveTag
923 mail.puts("<td>&nbsp;</td></tr>")
924 end
925
926 mail.puts("</table>")
927
928 totalFilesChanged = filesAdded+filesRemoved+filesModified
929 if totalFilesChanged > 1
930 mail.print("<small id=\"info\">")
931 changeKind = 0
932 if filesAdded>0
933 mail.print("#{filesAdded} added")
934 changeKind += 1
935 end
936 if filesRemoved>0
937 mail.print(" + ") if changeKind>0
938 mail.print("#{filesRemoved} removed")
939 changeKind += 1
940 end
941 if filesModified>0
942 mail.print(" + ") if changeKind>0
943 mail.print("#{filesModified} modified")
944 changeKind += 1
945 end
946 mail.print(", total #{totalFilesChanged}") if changeKind > 1
947 mail.puts(" files</small><br />")
948 end
949
950 todo_count = 0
951 $todoList.each do |item|
952 if todo_count == 0
953 mail.puts("<div class=\"todolist\"><ul>")
954 end
955 todo_count += 1
956 item = htmlEncode(item)
957 mail.puts("<li><a href=\"#todo#{todo_count}\">#{item}</a></li>")
958 end
959 mail.puts("</ul></div>") if todo_count>0
960
961
962 File.open("#{$logfile}.emailtmp") do |input|
963 input.each do |line|
964 mail.puts(line)
965 end
966 end
967 if $debug
968 blah("leaving file #{$logfile}.emailtmp")
969 else
970 File.unlink("#{$logfile}.emailtmp")
971 end
972
973 mail.puts("</body></html>")
974
975 end

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