1 --- svn_post_commit_hook.rb (.../trunk) (revision 0)
2 +++ svn_post_commit_hook.rb (.../branches/svn_support) (revision 269)
6 +$svnlook_exe = "svnlook" # default assumes the program is in $PATH
15 + $stderr.puts "svn_post_commit_hook.rb: #{msg}"
20 +$tmpdir = ENV["TMPDIR"] || "/tmp"
21 +$dirtemplate = "#svnspam.#{Process.getpgrp}.#{Process.uid}"
22 +# arguments to pass though to 'cvsspam.rb'
23 +$passthrough_args = []
26 + dir = "#{$tmpdir}/#{$dirtemplate}-#{rand(99999999)}"
27 + Dir.mkdir(dir, 0700)
32 + $datadir = make_data_dir
34 + # set PWD so that svnlook can create its .svnlook directory
40 + File.unlink("#{$datadir}/logfile")
46 + cmd = File.dirname($0) + "/cvsspam.rb"
47 + unless system(cmd,"--svn","#{$datadir}/logfile", *$passthrough_args)
48 + fail "problem running '#{cmd}'"
52 +# Like IO.popen, but accepts multiple arguments like Kernel.exec
53 +# (So no need to escape shell metacharacters)
54 +def safer_popen(*args)
55 + IO.popen("-") do |pipe|
65 +# Process the command-line arguments in the given list
67 + require 'getoptlong'
69 + opts = GetoptLong.new(
70 + [ "--to", "-t", GetoptLong::REQUIRED_ARGUMENT ],
71 + [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
72 + [ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
73 + [ "--from", "-u", GetoptLong::REQUIRED_ARGUMENT ],
74 + [ "--charset", GetoptLong::REQUIRED_ARGUMENT ]
77 + opts.each do |opt, arg|
78 + if ["--to", "--config", "--from", "--charset"].include?(opt)
79 + $passthrough_args << opt << arg
81 + if ["--debug"].include?(opt)
82 + $passthrough_args << opt
84 + $config = arg if opt=="--config"
85 + $debug = true if opt == "--debug"
88 + $repository = ARGV[0]
91 + unless $revision =~ /^\d+$/
92 + usage("revision must be an integer: #{revision.inspect}")
94 + $revision = $revision.to_i
96 + unless FileTest.directory?($repository)
97 + usage("no such directory: #{$repository.inspect}")
99 + $repository =~ /([^\/]+$)/
103 +# runs the given svnlook subcommand
104 +def svnlook(cmd, revision, *args)
105 + rev = revision.to_s
106 + safer_popen($svnlook_exe, cmd, $repository, "-r", rev, *args) do |io|
112 + def initialize(filechange, propchange, path)
113 + @filechange = filechange
114 + @propchange = propchange
118 + attr_accessor :filechange, :propchange, :path
120 + def property_change?
139 +# Line-oriented access to an underlying IO object. Remembers 'current' line
140 +# for lookahead during parsing.
151 + (@line = @io.gets) != nil
154 + def assert_current(re)
155 + raise "unexpected #{current.inspect}" unless @line =~ re
159 + def assert_next(re=nil)
160 + raise "unexpected end of text" unless next_line
162 + raise "unexpected #{current.inspect}" unless @line =~ re
169 +def read_modified_diff(out, lines, path)
170 + lines.assert_next(/^=+$/)
172 + if lines.current =~ /\(Binary files differ\)/
173 + process_modified_binary_diff(out, lines, path)
175 + process_modified_text_diff(out, lines, path)
180 +def process_modified_binary_diff(out, lines, path)
181 + prev_rev= $revision-1
182 + next_rev= $revision
183 + out.puts "#V #{prev_rev},#{next_rev}"
184 + out.puts "#M #{$shortrepo}/#{path}"
185 + out.puts "#U diff x x"
186 + out.puts "#U Binary files x and y differ"
190 +def process_modified_text_diff(out, lines, path)
191 + m = lines.assert_current(/^---.*\(rev (\d+)\)$/)
192 + prev_rev = m[1].to_i
193 + diff1 = lines.current
194 + m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
195 + next_rev = m[1].to_i
196 + diff2 = lines.current
197 + out.puts "#V #{prev_rev},#{next_rev}"
198 + out.puts "#M #{$shortrepo}/#{path}"
199 + out.puts "#U #{diff1}"
200 + out.puts "#U #{diff2}"
201 + while lines.next_line && lines.current =~ /^[-\+ @\\]/
202 + out.puts "#U #{lines.current}"
206 +def read_added_diff(out, lines, path)
207 + lines.assert_next(/^=+$/)
209 + if lines.current =~ /\(Binary files differ\)/
210 + process_added_binary_diff(out, lines, path)
212 + process_added_text_diff(out, lines, path)
216 +def process_added_binary_diff(out, lines, path)
217 + next_rev= $revision
218 + out.puts "#V NONE,#{next_rev}"
219 + out.puts "#A #{$shortrepo}/#{path}"
220 + out.puts "#U diff x x"
221 + out.puts "#U Binary file x added"
224 +def process_added_text_diff(out, lines, path)
225 + m = lines.assert_current(/^---.*\(rev (\d+)\)$/)
226 + prev_rev = m[1].to_i
227 + diff1 = lines.current
228 + m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
229 + next_rev = m[1].to_i
230 + diff2 = lines.current
231 + out.puts "#V NONE,#{next_rev}"
232 + out.puts "#A #{$shortrepo}/#{path}"
233 + out.puts "#U #{diff1}"
234 + out.puts "#U #{diff2}"
235 + while lines.next_line && lines.current =~ /^[-\+ @\\]/
236 + out.puts "#U #{lines.current}"
240 +def read_deleted_diff(out, lines, path)
241 + lines.assert_next(/^=+$/)
242 + m = lines.assert_next(/^---.*\(rev (\d+)\)$/)
243 + prev_rev = m[1].to_i
244 + diff1 = lines.current
245 + m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
246 + next_rev = m[1].to_i
247 + diff2 = lines.current
248 + out.puts "#V #{prev_rev},NONE"
249 + out.puts "#R #{$shortrepo}/#{path}"
250 + out.puts "#U #{diff1}"
251 + out.puts "#U #{diff2}"
252 + while lines.next_line && lines.current =~ /^[-\+ @\\]/
253 + out.puts "#U #{lines.current}"
257 +def read_property_lines(path, prop_name, revision)
259 + svnlook("propget", revision, prop_name, path) do |io|
260 + io.each_line do |line|
261 + lines << line.chomp
267 +def assert_prop_match(a, b)
268 + if !b.nil? && a != b
269 + raise "property mismatch: #{a.inspect}!=#{b.inspect}"
273 +# We need to read the property change from the output of svnlook, but have
274 +# a difficulty in that there's no unambiguous delimiter marking the end of
275 +# a potentially multi-line property value. Therefore, we do a seperate
276 +# svn propget on the given file to get the value of the property on its own,
277 +# and then use that value as a guide as to how much data to read from the
279 +def munch_prop_text(path, prop_name, revision, lines, line0)
280 + prop = read_property_lines(path, prop_name, revision)
282 + assert_prop_match(line0, "")
285 + assert_prop_match(line0, prop.shift)
286 + prop.each do |prop_line|
288 + assert_prop_match(lines.current.chomp, prop_line)
292 +def read_properties_changed(out, lines, path)
293 + prev_rev= $revision-1
294 + next_rev= $revision
295 + lines.assert_next(/^_+$/)
296 + return unless lines.next_line
297 + out.puts "#V #{prev_rev},#{next_rev}"
298 + out.puts "#P #{$shortrepo}/#{path}"
299 +# The first three get consumed and not highlighted
301 + out.puts "#U Property changes:"
305 + break unless lines.current =~ /^(?:Name|Added|Deleted): (.+)$/
308 + m = lines.assert_next(/^ ([-+]) (.*)/)
312 + munch_prop_text(path, prop_name, $revision-1, lines, line0)
313 + if lines.next_line && lines.current =~ /^ \+ (.*)/
314 + munch_prop_text(path, prop_name, $revision, lines, $1)
318 + munch_prop_text(path, prop_name, $revision, lines, line0)
321 + out.puts "#U #{m[1]} #{prop_name}:#{m[2]}"
326 +def handle_copy(out, lines, path, from_ref, from_file)
327 + prev_rev= $revision-1
328 + next_rev= $revision
329 + out.puts "#V #{$shortrepo}/#{from_file}:#{prev_rev},#{next_rev}"
330 + out.puts "#C #{$shortrepo}/#{path}"
331 + if lines.next_line && lines.current =~ /^=+$/
332 + m = lines.assert_next(/^---.*\(rev (\d+)\)$/)
333 + prev_rev = m[1].to_i
334 + diff1 = lines.current
335 + m = lines.assert_next(/^\+\+\+.*\(rev (\d+)\)$/)
336 + next_rev = m[1].to_i
337 + diff2 = lines.current
338 + out.puts "#U #{diff1}"
339 + out.puts "#U #{diff2}"
340 + while lines.next_line && lines.current =~ /^[-\+ @\\]/
341 + out.puts "#U #{lines.current}"
345 + out.puts "#U Copied from #{$shortrepo}/#{from_file}:#{from_ref}"
351 + svnlook("author", $revision) do |io|
352 + return io.readline.chomp
358 + return if $passthrough_args.include?("--from")
359 + author = svnlook_author
361 + blah("Author from svnlook: '#{author}'")
362 + $passthrough_args << "--from" << author
366 +def process_svnlook_log(file)
367 + svnlook("log", $revision) do |io|
368 + io.each_line do |line|
369 + file.puts("#> #{line}")
374 +def process_svnlook_diff(file)
375 + svnlook("diff", $revision) do |io|
376 + lines = LineReader.new(io)
377 + while lines.next_line
378 + if lines.current =~ /^Modified:\s+(.*)/
379 + read_modified_diff(file, lines, $1)
380 + elsif lines.current =~ /^Added:\s+(.*)/
381 + read_added_diff(file, lines, $1)
382 + elsif lines.current =~ /^Copied:\s+(.*) \(from rev (\d+), (.*)\)$/
383 + handle_copy(file, lines, $1, $2, $3)
384 + elsif lines.current =~ /^Deleted:\s+(.*)/
385 + read_deleted_diff(file, lines, $1)
386 + elsif lines.current =~ /^Property changes on:\s+(.*)/
387 + read_properties_changed(file, lines, $1)
388 + elsif lines.current == "\n"
391 + raise "unable to parse line '#{lines.current.inspect}'"
397 +def process_commit()
398 + File.open("#{$datadir}/logfile", File::WRONLY|File::CREAT) do |file|
399 + process_svnlook_log(file)
400 + process_svnlook_diff(file)
416 --- cvsspam.rb (.../trunk) (revision 269)
417 +++ cvsspam.rb (.../branches/svn_support) (revision 269)
420 # the full path and filename within the repository
422 - # the type of change committed 'M'=modified, 'A'=added, 'R'=removed
423 + # the type of change committed 'M'=modified, 'A'=added, 'R'=removed, 'P'=properties, 'C'=copied
425 # records number of 'addition' lines in diff output, once counted
426 attr_accessor :lineAdditions
427 @@ -453,17 +453,28 @@
433 # was this file added during the commit?
438 + # was this file copied during the commit?
443 # was this file simply modified during the commit?
448 + # was this file simply modified during the commit?
454 # passing true, this object remembers that a diff will appear in the email,
455 # passing false, this object remembers that no diff will appear in the email.
456 # Once the value is set, it will not be changed
461 +# Note when LogReader finds record of a file that was copied in this commit
462 +class CopiedFileHandler < FileHandler
463 + def handleFile(file)
465 + file.fromVer=$fromVer
470 # Note when LogReader finds record of a file that was modified in this commit
471 class ModifiedFileHandler < FileHandler
477 +# Note when LogReader finds record of a file whose properties were modified in this commit
478 +class ModifiedPropsFileHandler < FileHandler
479 + def handleFile(file)
481 + file.fromVer=$fromVer
487 # Used by UnifiedDiffHandler to record the number of added and removed lines
488 # appearing in a unidiff.
489 class UnifiedDiffStats
490 @@ -1064,11 +1093,21 @@
491 print($frontend.path($file.basedir, $file.tag))
492 println("</span><br />")
493 println("<div class=\"fileheader\" id=\"removed\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">removed after #{$frontend.version($file.path,$file.fromVer)}</small></div>")
495 + print("<span class=\"pathname\" id=\"copied\">")
496 + print($frontend.path($file.basedir, $file.tag))
497 + println("</span><br />")
498 + println("<div class=\"fileheader\" id=\"copied\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">copied from #{$frontend.version($file.path,$file.fromVer)}</small></div>")
500 print("<span class=\"pathname\">")
501 print($frontend.path($file.basedir, $file.tag))
502 println("</span><br />")
503 println("<div class=\"fileheader\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">#{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}</small></div>")
505 + print("<span class=\"pathname\">")
506 + print($frontend.path($file.basedir, $file.tag))
507 + println("</span><br />")
508 + println("<div class=\"fileheader\"><big><b>#{htmlEncode($file.file)}</b></big> <small id=\"info\">#{$frontend.version($file.path,$file.fromVer)} #{$frontend.diff($file)} #{$frontend.version($file.path,$file.toVer)}</small></div>")
510 print("<pre class=\"diff\"><small id=\"info\">")
512 @@ -1329,6 +1368,7 @@
513 $users_file_charset = nil
517 $recipients = Array.new
518 $sendmail_prog = "/usr/sbin/sendmail"
519 $hostname = ENV['HOSTNAME'] || 'localhost'
520 @@ -1366,6 +1406,7 @@
521 [ "--to", "-t", GetoptLong::REQUIRED_ARGUMENT ],
522 [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
523 [ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
524 + [ "--svn", "-s", GetoptLong::NO_ARGUMENT ],
525 [ "--from", "-u", GetoptLong::REQUIRED_ARGUMENT ],
526 [ "--charset", GetoptLong::REQUIRED_ARGUMENT ]
528 @@ -1374,6 +1415,7 @@
529 $recipients << EmailAddress.new(arg) if opt=="--to"
530 $config = arg if opt=="--config"
531 $debug = true if opt=="--debug"
532 + $svn = true if opt=="--svn"
533 $from_address = EmailAddress.new(arg) if opt=="--from"
534 # must use different variable as the config is readed later.
535 $arg_charset = arg if opt == "--charset"
536 @@ -1386,7 +1428,7 @@
538 $stderr.puts "missing required file argument"
540 - puts "Usage: cvsspam.rb [ --to <email> ] [ --config <file> ] <collect_diffs file>"
541 + puts "Usage: cvsspam.rb [ --svn ] [ --to <email> ] [ --config <file> ] <collect_diffs file>"
545 @@ -1495,12 +1537,16 @@
547 "A" => AddedFileHandler.new,
548 "R" => RemovedFileHandler.new,
549 + "C" => CopiedFileHandler.new,
550 "M" => ModifiedFileHandler.new,
551 + "P" => ModifiedPropsFileHandler.new,
552 "V" => VersionHandler.new]
554 $handlers["A"].setTagHandler(tagHandler)
555 $handlers["R"].setTagHandler(tagHandler)
556 +$handlers["C"].setTagHandler(tagHandler)
557 $handlers["M"].setTagHandler(tagHandler)
558 +$handlers["P"].setTagHandler(tagHandler)
560 $fileEntries = Array.new
561 $task_list = Array.new
562 @@ -1525,7 +1571,11 @@
565 if $subjectPrefix == nil
566 - $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
568 + $subjectPrefix = "[SVN #{Repository.array.join(',')}]"
570 + $subjectPrefix = "[CVS #{Repository.array.join(',')}]"
575 @@ -1572,6 +1622,8 @@
576 #removed {background-color:#ffdddd;}
577 #removedchars {background-color:#ff9999;font-weight:bolder;}
578 tr.alt #removed {background-color:#f7cccc;}
579 + #copied {background-color:#ccccff;}
580 + tr.alt #copied {background-color:#bbbbf7;}
581 #info {color:#888888;}
582 #context {background-color:#eeeeee;}
583 td {padding-left:.3em;padding-right:.3em;}
584 @@ -1604,7 +1656,9 @@
590 + filesModifiedProps = 0
592 totalLinesRemoved = 0
594 @@ -1613,24 +1667,26 @@
595 $fileEntries.each do |file|
596 unless file.repository == last_repository
597 last_repository = file.repository
598 - mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 5 : 4}\">")
599 + mail.print("<tr class=\"head\"><td colspan=\"#{last_repository.has_multiple_tags ? 6 : 5}\">")
600 if last_repository.has_multiple_tags
601 mail.print("Mixed-tag commit")
605 mail.print(" in <b><tt>#{htmlEncode(last_repository.common_prefix)}</tt></b>")
606 - if last_repository.trunk_only?
607 - mail.print("<span id=\"info\"> on MAIN</span>")
611 - last_repository.each_tag do |tag|
614 - mail.print tagCount<last_repository.tag_count ? ", " : " & "
616 + if last_repository.trunk_only?
617 + mail.print("<span id=\"info\"> on MAIN</span>")
621 + last_repository.each_tag do |tag|
624 + mail.print tagCount<last_repository.tag_count ? ", " : " & "
626 + mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>"
628 - mail.print tag ? htmlEncode(tag) : "<span id=\"info\">MAIN</span>"
631 mail.puts("</td></tr>")
632 @@ -1645,8 +1701,12 @@
638 elsif file.modification?
640 + elsif file.modifiedprops?
641 + filesModifiedProps += 1
643 name = htmlEncode(file.name_after_common_prefix)
644 slashPos = name.rindex("/")
645 @@ -1666,6 +1726,8 @@
646 name = "<span id=\"added\">#{name}</span>"
648 name = "<span id=\"removed\">#{name}</span>"
650 + name = "<span id=\"copied\">#{name}</span>"
654 @@ -1675,11 +1737,18 @@
656 mail.print(" #{$frontend.log(file)}")
659 - mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[empty]</small></td>")
661 + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[copied]</small></td>")
663 + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[empty]</small></td>")
665 - mail.print("<td colspan=\"2\" align=\"center\"><small id=\"info\">[binary]</small></td>")
666 + mail.print("<td colspan=\"3\" align=\"center\"><small id=\"info\">[binary]</small></td>")
668 + if file.modifiedprops?
669 + mail.print("<td align=\"right\"><small id=\"info\">[props]</small></td>")
671 + mail.print("<td></td>")
673 if file.lineAdditions>0
674 totalLinesAdded += file.lineAdditions
675 mail.print("<td align=\"right\" id=\"added\">+#{file.lineAdditions}</td>")
676 @@ -1706,15 +1775,19 @@
677 mail.print("<td nowrap=\"nowrap\" align=\"right\">added #{$frontend.version(file.path,file.toVer)}</td>")
679 mail.print("<td nowrap=\"nowrap\">#{$frontend.version(file.path,file.fromVer)} removed</td>")
681 + mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
682 elsif file.modification?
683 mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
684 + elsif file.modifiedprops?
685 + mail.print("<td nowrap=\"nowrap\" align=\"center\">#{$frontend.version(file.path,file.fromVer)} #{$frontend.diff(file)} #{$frontend.version(file.path,file.toVer)}</td>")
690 if $fileEntries.size>1 && (totalLinesAdded+totalLinesRemoved)>0
691 # give total number of lines added/removed accross all files
692 - mail.print("<tr><td></td>")
693 + mail.print("<tr><td></td><td></td>")
695 mail.print("<td align=\"right\" id=\"added\">+#{totalLinesAdded}</td>")
697 @@ -1731,7 +1804,7 @@
699 mail.puts("</table>")
701 - totalFilesChanged = filesAdded+filesRemoved+filesModified
702 + totalFilesChanged = filesAdded+filesRemoved+filesCopied+filesModified+filesModifiedProps
703 if totalFilesChanged > 1
704 mail.print("<small id=\"info\">")
706 @@ -1744,11 +1817,21 @@
707 mail.print("#{filesRemoved} removed")
711 + mail.print(" + ") if changeKind>0
712 + mail.print("#{filesCopied} copied")
716 mail.print(" + ") if changeKind>0
717 mail.print("#{filesModified} modified")
720 + if filesModifiedProps>0
721 + mail.print(" + ") if changeKind>0
722 + mail.print("#{filesModifiedProps} modified properties")
725 mail.print(", total #{totalFilesChanged}") if changeKind > 1
726 mail.puts(" files</small><br />")