Annotation of CVSROOT/collect_diffs.rb, revision 1.1

1.1     ! uid12904    1: #!/usr/bin/ruby -w
        !             2: 
        !             3: # Part of CVSspam
        !             4: #   http://www.badgers-in-foil.co.uk/projects/cvsspam/
        !             5: # Copyright (c) David Holroyd
        !             6: 
        !             7: #  CVSROOT is /var/lib/cvs
        !             8: #  ARGV is 'CVSROOT/foo/space dir space file,NONE,1.1 some,file,1.4,1.5'
        !             9: #  ----
        !            10: #  Update of /var/lib/cvs/CVSROOT/foo/space dir
        !            11: #  In directory foil:/tmp/cvs-serv13059
        !            12: #  
        !            13: #  Modified Files:
        !            14: #      some,file 
        !            15: #  Added Files:
        !            16: #      space file 
        !            17: #  Log Message:
        !            18: #  msg
        !            19: ####
        !            20: 
        !            21: 
        !            22: # Assumptions
        !            23: # - file names do not contain newlines or single quotes
        !            24: #raise "Created data dir #{$datadir}" 
        !            25: 
        !            26: $tmpdir = ENV["TMPDIR"] || "/tmp"
        !            27: $dirtemplate = "#cvsspam.#{Process.getpgrp}.#{Process.uid}"
        !            28: 
        !            29: def find_data_dir
        !            30:   Dir["#{$tmpdir}/#{$dirtemplate}-*"].each do |dir|
        !            31:     stat = File.stat(dir)
        !            32:     return dir if stat.owned?
        !            33:   end
        !            34:   nil
        !            35: end
        !            36: 
        !            37: 
        !            38: def blah(msg)
        !            39:   if $debug
        !            40:     $stderr.puts "collect_diffs.rb: #{msg}"
        !            41:   end
        !            42: end
        !            43: 
        !            44: 
        !            45: # Like IO.popen, but accepts multiple arguments like Kernel.exec
        !            46: # (So no need to escape shell metacharacters)
        !            47: def safer_popen(*args)
        !            48:   IO.popen("-") do |pipe|
        !            49:     if pipe==nil
        !            50:       exec(*args)
        !            51:     else
        !            52:       yield pipe
        !            53:     end
        !            54:   end
        !            55: end
        !            56: 
        !            57: class ChangeInfo
        !            58:   def initialize(file, fromVer, toVer)
        !            59:     @file, @fromVer, @toVer = file, fromVer, toVer
        !            60:     if fromVer == toVer
        !            61:       fail "'from' and 'to' versions should be different ('#{fromVer}')"
        !            62:     end
        !            63:   end
        !            64:   attr_reader :file, :fromVer, :toVer
        !            65:   def to_s
        !            66:     "<ChangeInfo \"#{@file}\" #{@toVer}<--#{@fromVer}>"
        !            67:   end
        !            68: 
        !            69:   def isAddition ; fromVer == 'NONE' end
        !            70: 
        !            71:   def isRemoval ; toVer == 'NONE' end
        !            72: 
        !            73:   #def isModification ; !(isAddition || isRemoval) end
        !            74: end
        !            75: 
        !            76: $commitinfo_tags = nil
        !            77: 
        !            78: def get_commitinfo_tag(filename)
        !            79:   if $commitinfo_tags.nil?
        !            80:     return nil unless FileTest.exists?("#{$datadir}/commitinfo-tags")
        !            81:     File.open("#{$datadir}/commitinfo-tags") do |file|
        !            82:       $commitinfo_tags = Hash.new
        !            83:       file.each_line do |line|
        !            84:        line =~ /([^\t]+)\t(.+)/
        !            85:        key = $2
        !            86:        val = $1
        !            87:        key.sub!(/^#{ENV['CVSROOT']}\//, '')
        !            88:        $commitinfo_tags[key] = val
        !            89:       end
        !            90:     end
        !            91:   end
        !            92:   return $commitinfo_tags[filename]
        !            93: end
        !            94: 
        !            95: 
        !            96: # cvs_info comes from the command line, ultimately as the expansion of the
        !            97: # %{sVv} in $CVSROOT/loginfo.  It isn't possible to parse this value
        !            98: # unambiguously, but we make an effort to get it right in as many cases as
        !            99: # possible.
        !           100: def collect_antique_style_args(cvs_info)
        !           101:   # remove leading slashes that may appear due to the user entering trailing
        !           102:   # slashes in their CVSROOT specification
        !           103:   cvs_info = cvs_info.sub(/^\/+/, "")
        !           104: 
        !           105:   unless cvs_info.slice(0, $repository_path.length+1) == "#{$repository_path} "
        !           106:     fail "calculated repository path ('#{$repository_path}') doesn't match start of command line arg ('#{cvs_info}')"
        !           107:   end
        !           108: 
        !           109:   version_info = cvs_info.slice($repository_path.length,
        !           110:                                 cvs_info.length-$repository_path.length)
        !           111: 
        !           112:   changes = Array.new
        !           113:   # make a list of changed files given on the command line
        !           114:   while version_info.length>0
        !           115:     if version_info.sub!(/^ (.+?),(NONE|[.0-9]+),(NONE|[.0-9]+)/, '') == nil
        !           116:       fail "'#{version_info}' doesn't match ' <name>,<ver>,<ver> ...'"
        !           117:     end
        !           118:     changes << ChangeInfo.new($1, $2, $3)
        !           119:   end
        !           120: 
        !           121:   return changes
        !           122: end
        !           123: 
        !           124: 
        !           125: def collect_modern_style_args(cvs_info)
        !           126: #  unless cvs_info[0] == $repository_path
        !           127: #    fail "calculated repository path ('#{$repository_path}') doesn't match first command line arg ('#{cvs_info[0]}')"
        !           128: #  end
        !           129:   changes = Array.new
        !           130:   i = 0
        !           131:   while i < cvs_info.length
        !           132:     changes << ChangeInfo.new(cvs_info[i], cvs_info[i+=1], cvs_info[i+=1])
        !           133:     i+=1
        !           134:   end
        !           135:   return changes
        !           136: end
        !           137: 
        !           138: # Replace multiple adjecent forward slashes with a single slash.
        !           139: def sanitise_path(path)
        !           140:   path.gsub(/\/+/, "/")
        !           141: end
        !           142: 
        !           143: def process_log(cvs_info)
        !           144:   cvsroot = sanitise_path(ENV['CVSROOT'])
        !           145: 
        !           146:   $datadir = find_data_dir()
        !           147: 
        !           148:   raise "missing data dir (#{$tmpdir}/#{$dirtemplate}-XXXXXX)" if $datadir==nil
        !           149: 
        !           150:   line = $stdin.gets
        !           151:   unless line =~ /^Update of (.+)/
        !           152:     fail "Log preamble looks suspect (doesn't start 'Update of ...')"
        !           153:   end
        !           154: 
        !           155:   $path = sanitise_path($1)
        !           156:   unless $path.slice(0,cvsroot.length) == cvsroot
        !           157:     fail "CVSROOT ('#{cvsroot}') doesn't match log preamble ('#{$path}')"
        !           158:   end
        !           159: 
        !           160:   $repository_path = $path.slice(cvsroot.length, $path.length-cvsroot.length)
        !           161:   $repository_path.sub!(/^\//, "")  # remove leading '/', if present
        !           162: 
        !           163:   if $use_modern_argument_list
        !           164:     changes = collect_modern_style_args(cvs_info)
        !           165:   else
        !           166:     changes = collect_antique_style_args(cvs_info)
        !           167:   end
        !           168: 
        !           169:   # look for the start of the user's comment
        !           170:   $stdin.each do |line|
        !           171:     break if line =~ /^Log Message/
        !           172:   end
        !           173: 
        !           174:   unless line =~ /^Log Message/
        !           175:     fail "Input did not contain a 'Log Message:' entry"
        !           176:   end
        !           177: 
        !           178:   File.open("#{$datadir}/logfile", File::WRONLY|File::CREAT|File::APPEND) do |file|
        !           179:     $stdin.each do |line|
        !           180:       # remove any trailing whitespace; we don't want the split() below to
        !           181:       # produce empty trailing items due to '\r' at the end of the line
        !           182:       # (if input is 'DOS' style),
        !           183:       line.sub!(/\s*$/, "")
        !           184:       
        !           185:       # 'Mac' clients sending logs to a unix server may denote end-of-line with
        !           186:       # a carriage-return, defeating the $stdin.each above (i.e. the whole log
        !           187:       # message will appear as a single line containing '\r's).  We handle this
        !           188:       # case explicitly here, so that cvsspam.rb's Subject header generation
        !           189:       # doesn't break,
        !           190:       line.split(/\r/).each do |part|
        !           191:         file.puts "#> #{part}"
        !           192:       end
        !           193:     end
        !           194: 
        !           195:     changes.each do |change|
        !           196: 
        !           197:       # record version information
        !           198:       file.puts "#V #{change.fromVer},#{change.toVer}"
        !           199: 
        !           200:       # remember that the 'binary' option was set for this file
        !           201:       binary_file = false
        !           202:       # note if the file is on a branch
        !           203:       tag = nil
        !           204:       if change.isRemoval
        !           205:        tag = get_commitinfo_tag("#{$repository_path}/#{change.file}")
        !           206:       else
        !           207:         status = nil
        !           208:         safer_popen($cvs_prog, "-nq", "status", change.file) do |io|
        !           209:           status = io.read
        !           210:         end
        !           211:         fail "couldn't get cvs status: #{$!} (exited with #{$?})" unless ($?>>8)==0
        !           212: 
        !           213:        if status =~ /^\s*Sticky Tag:\s*(.+) \(branch: +/m
        !           214:          tag = $1
        !           215:        end
        !           216: 
        !           217:        if status =~ /^\s*Sticky Options:\s*-kb/m
        !           218:          binary_file = true
        !           219:        end
        !           220:       end
        !           221:       file.puts "#T #{tag}" unless tag.nil?
        !           222: 
        !           223:       diff_cmd = Array.new << $cvs_prog << "-nq" << "diff" << "-Nu"
        !           224:       diff_cmd << "-kk" if $diff_ignore_keywords
        !           225: 
        !           226:       if change.isAddition
        !           227:         file.write "#A "
        !           228:         # cruft up a date in the distant past, when the file would not have
        !           229:         # existed, so that the diff will show all lines as added
        !           230:         diff_cmd << "-D1/26/1977" << "-r#{change.toVer}"
        !           231:       elsif change.isRemoval
        !           232:         file.write "#R "
        !           233:         # just specifying one version, cvs will diff between that version and
        !           234:         # the current version (will show all lines removed)
        !           235:         diff_cmd << "-r#{change.fromVer}"
        !           236:       else
        !           237:         file.write "#M "
        !           238:         diff_cmd << "-r#{change.fromVer}" << "-r#{change.toVer}"
        !           239:       end
        !           240:       file.puts "#{$repository_path}/#{change.file}"
        !           241:       diff_cmd << change.file
        !           242:       if binary_file
        !           243:        blah("not diffing #{change.file}; has -kb set")
        !           244:        # fake diff lines that will cause cvsspam.rb to consider this a binary
        !           245:        # file,
        !           246:        file.puts "#U diff x x"
        !           247:        file.puts "#U Binary files x and y differ"
        !           248:       else
        !           249:        # do a cvs diff and place the output into our temp file
        !           250:        blah("about to run #{diff_cmd.join(' ')}")
        !           251:        safer_popen(*diff_cmd) do |pipe|
        !           252:          # skip over cvs-diff's preamble
        !           253:          pipe.each do |line|
        !           254:            break if line =~ /^diff /
        !           255:          end
        !           256:          file.puts "#U #{line}"
        !           257:          pipe.each do |line|
        !           258:            file.puts "#U #{line}"
        !           259:          end
        !           260:        end
        !           261:       end
        !           262:       # TODO: don't how to do this reliably on different systems...
        !           263:       #fail "cvsdiff did not give exit status 1 for invocation: #{diff_cmd.join(' ')}" unless ($?>>8)==1
        !           264:     end
        !           265:   end
        !           266: 
        !           267: end
        !           268: 
        !           269: 
        !           270: # sometimes, CVS would exit with an error like,
        !           271: # 
        !           272: #   cvs [server aborted]: received broken pipe signal
        !           273: # 
        !           274: # consuming all the data on our standard input seems to stop this error
        !           275: # happening.  (This problem may have been fixed in CVS 1.12.6, looking at
        !           276: # a message in the NEWS file.)
        !           277: def consume_stdin()
        !           278:   $stdin.read()
        !           279: end
        !           280: 
        !           281: 
        !           282: def mailtest
        !           283:   lastdir = nil
        !           284:   File.open("#{$datadir}/lastdir") do |file|
        !           285:     lastdir = sanitise_path(file.gets)
        !           286:   end
        !           287:   if $path == lastdir
        !           288:     blah("sending spam.  (I am #{$0})")
        !           289:     # REVISIT: $0 will not contain the path to this script on all systems
        !           290:     cmd = File.dirname($0) + "/cvsspam.rb"
        !           291:     unless system(cmd, "#{$datadir}/logfile", *$passthroughArgs)
        !           292:       fail "problem running '#{cmd}'"
        !           293:     end
        !           294:     if $debug
        !           295:       blah("leaving file #{$datadir}/logfile")
        !           296:     else 
        !           297:       File.unlink("#{$datadir}/logfile")
        !           298:     end
        !           299:     File.unlink("#{$datadir}/lastdir")
        !           300:     File.unlink("#{$datadir}/commitinfo-tags") if FileTest.exists?("#{$datadir}/commitinfo-tags")
        !           301:     Dir.rmdir($datadir) unless $debug
        !           302:   else
        !           303:     blah("not spam time yet, #{$path}!=#{lastdir}")
        !           304:   end
        !           305: end
        !           306: 
        !           307: 
        !           308: class CVSConfig
        !           309:   def initialize(filename)
        !           310:     @data = Hash.new
        !           311:     File.open(filename) do |io|
        !           312:       read(io)
        !           313:     end
        !           314:   end
        !           315: 
        !           316:   def read(io)
        !           317:     io.each do |line|
        !           318:       parse_line(line)
        !           319:     end
        !           320:   end
        !           321: 
        !           322:   def parse_line(line)
        !           323:     # strip any comment (assumes values can't contain '#')
        !           324:     line.sub!(/#.*$/, "")
        !           325:     if line =~ /^\s*(.*?)\s*=\s*(.*?)\s*$/
        !           326:       @data[$1] = $2
        !           327:     end
        !           328:   end
        !           329: 
        !           330:   def [](key)
        !           331:     @data[key]
        !           332:   end
        !           333: end
        !           334: 
        !           335: $config = nil
        !           336: $cvs_prog = "cvs"
        !           337: $debug = false
        !           338: $diff_ignore_keywords = false
        !           339: $task_keywords = []
        !           340: 
        !           341: unless ENV.has_key?('CVSROOT')
        !           342:   fail "$CVSROOT not defined.  It should be when I am invoked from CVSROOT/loginfo"
        !           343: end
        !           344: 
        !           345: 
        !           346: def handle_operation?(args)
        !           347:   # The CVS 1.12.x series pass an argument with the value "- New directory"
        !           348:   # whereas previous versions passed "some/path - New directory".  The newer
        !           349:   # syntax looks like a command-line switch, and confuses GetOpt.  We check
        !           350:   # for that case before GetOpt processing
        !           351:   unless ARGV.detect{|el| el =~ /- New directory$/}.nil?
        !           352:     blah("No action taken on directory creation")
        !           353:     return false
        !           354:   end
        !           355:   unless ARGV.detect{|el| el =~ /- Imported sources$/}.nil?
        !           356:     blah("Imported not handled")
        !           357:     return false
        !           358:   end
        !           359:   return true
        !           360: end
        !           361: 
        !           362: 
        !           363: unless handle_operation?(ARGV)
        !           364:   consume_stdin()
        !           365:   exit
        !           366: end
        !           367: 
        !           368: 
        !           369: require 'getoptlong'
        !           370: 
        !           371: opts = GetoptLong.new(
        !           372:   [ "--to",     "-t", GetoptLong::REQUIRED_ARGUMENT ],
        !           373:   [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ],
        !           374:   [ "--debug",  "-d", GetoptLong::NO_ARGUMENT ],
        !           375:   [ "--from",   "-u", GetoptLong::REQUIRED_ARGUMENT ],
        !           376:   [ "--charset",      GetoptLong::REQUIRED_ARGUMENT ]
        !           377: )
        !           378: 
        !           379: # arguments to pass though to 'cvsspam.rb'
        !           380: $passthroughArgs = Array.new
        !           381: opts.each do |opt, arg|
        !           382:   if ["--to", "--config", "--from", "--charset"].include?(opt)
        !           383:     $passthroughArgs << opt << arg
        !           384:   end
        !           385:   if ["--debug"].include?(opt)
        !           386:     $passthroughArgs << opt
        !           387:   end
        !           388:   $config = arg if opt=="--config"
        !           389:   $debug = true if opt == "--debug"
        !           390: end
        !           391: 
        !           392: blah("CVSROOT is #{ENV['CVSROOT']}")
        !           393: blah("ARGV is <#{ARGV.join('>, <')}>")
        !           394: 
        !           395: cvsroot_dir = "#{ENV['CVSROOT']}/CVSROOT"
        !           396: 
        !           397: if $config == nil
        !           398:   if FileTest.exists?("#{cvsroot_dir}/cvsspam.conf")
        !           399:     $config = "#{cvsroot_dir}/cvsspam.conf"
        !           400:   elsif FileTest.exists?("/etc/cvsspam/cvsspam.conf")
        !           401:     $config = "/etc/cvsspam/cvsspam.conf"
        !           402:   end
        !           403: 
        !           404:   if $config != nil
        !           405:     $passthroughArgs << "--config" << $config
        !           406:   end
        !           407: end
        !           408: 
        !           409: 
        !           410: $use_modern_argument_list = false
        !           411: 
        !           412: cvs_config_filename = "#{cvsroot_dir}/config"
        !           413: 
        !           414: if FileTest.exists?(cvs_config_filename)
        !           415:   cvs_config = CVSConfig.new(cvs_config_filename)
        !           416: 
        !           417:   $use_modern_argument_list = cvs_config["UseNewInfoFmtStrings"] == "yes"
        !           418: end
        !           419: 
        !           420: if $config != nil
        !           421:   if FileTest.exists?($config)
        !           422:     def addHeader(name,val)
        !           423:     end
        !           424:     def addRecipient(who)
        !           425:     end
        !           426:     class GUESS
        !           427:     end
        !           428:     load $config
        !           429:   else
        !           430:     blah("Config file '#{$config}' not found, ignoring")
        !           431:   end
        !           432: end
        !           433: 
        !           434: if $use_modern_argument_list
        !           435:   if ARGV.length % 3 != 0
        !           436:     $stderr.puts "Expected 3 arguments for each file"
        !           437:   end
        !           438:   process_log(ARGV)
        !           439: else
        !           440:   if ARGV.length != 1
        !           441:     $stderr.puts "Expected arguments missing"
        !           442:     $stderr.puts "* You shouldn't run collect_diffs by hand, but from a CVSROOT/loginfo entry *"
        !           443:     $stderr.puts "Usage: collect_diffs.rb [ --to <email> ] [ --config <file> ] %{sVv}"
        !           444:     $stderr.puts "       (the sequence '%{sVv}' is expanded by CVS, when found in CVSROOT/loginfo)"
        !           445:     exit
        !           446:   end
        !           447:   process_log(ARGV[0])
        !           448: end
        !           449: mailtest

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>