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>