#!/usr/bin/env ruby # # Copyright (c) Matthew Attaway, Perforce Software Inc, 2013. All rights reserved # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE # SOFTWARE, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # # DAMAGE. # # User contributed content on the Perforce Public Depot is not supported by Perforce, although it may be supported by its author. # This applies to all contributions even those submitted by Perforce employees. # # = Synopsis # # code_swarm_generator: builds the XML file from a Perforce server to feed into the code_swarm development visualizer # # = Usage # # code-swarm_generator -p <port> -u <user> -s <startingChange> -e <endingChange> -d <depotpath> -C <config file> --verbose(-v) --condense(-c) # #Configuration File Syntax # #The config file uses the syntax <Keyword>=<Value> # #Keywords: # DepotPath - a path to look for changes on. There can be any number of DepotPath entries # IgnorePath - changes on this path will be excluded from the results. There can be any nnumber of IgnorePath entries. # IgnoreUser - changes and jobs from this user will be discarded # IgnoreExtension - files that match the extension will be excluded # User - the user to connect to the server with # Port - the server to connect to # Verbose - boolean. Shows generator progress. # Condense - collapses all activity of branched files into one file. This is good for cutting down on the amount of files code_swarm has to track. # StartingChange - the change to start generating from # EndingChange - the change stop generating at # CalculateWeight - boolean. Looks for the number of lines changed per file revision and weights the file appropriately. # IncludeJobs - boolean. Adds events for each modified job during the specified span of changes # JobModifiedDateField - the name of the field used to track the last modification date of a job # JobModifiedByField - the name of the field used to track who made the last change to a job # SpecDepotname - the name of the spec depot. Don't include any '/', just list the name require 'rubygems' require 'P4' require 'getoptlong' require 'rdoc' require 'time' require 'iconv' unless String.method_defined?(:encode) # from http://jeffgardner.org/2011/08/04/rails-string-to-boolean-method/, with modifications class String def to_bool return true if self == true || self =~ (/(true|t|yes|y|1)$/i) return false if self == false || self =~ (/(false|f|no|n|0)$/i) raise ArgumentError.new("invalid value for Boolean: \"#{self}\"") end end # translate invalid utf-8 sequences. # solution from: http://stackoverflow.com/a/8873922/502497 def StripInvalidUTF8( text ) if String.method_defined?(:encode) return text.encode('UTF-8', 'UTF-8', :invalid => :replace) else ic = Iconv.new('UTF-8', 'UTF-8//IGNORE') return ic.iconv(text) end end # # suck the data out of the config file # def ParseConfigFile( configFile ) data = Array.new File.open( configFile, 'r' ) do |f| data = f.readlines end data.each do | l | args = l.split( '=' ) case args[0] when 'DepotPath' $depotPaths.push( args[1].chomp ) when 'IgnorePath' $ignorePaths.push( args[1].chomp ) when 'IgnoreUser' $ignoreUsers[ args[1].chomp ] = 1 when 'IgnoreExtension' $ignoreExtensions[ args[1].chomp ] = 1 when 'User' $user = args[1].chomp when 'Port' $port = args[1].chomp when 'Verbose' $verbose = args[1].to_bool when 'Condense' $condense = args[1].to_bool when 'StartingChange' $startingChange = args[1].chomp when 'EndingChange' $endingChange = args[1].chomp when 'CalculateWeights' $calculateWeight = args[1].to_bool when 'IncludeJobs' $includeJobs = args[1].to_bool when 'JobModifiedDateField' $jobModifiedDateField = args[1].chomp when 'JobModifiedByField' $jobModifiedByField = args[1].chomp when 'JobNameField' $jobNameField = args[1].chomp when 'SpecDepotName' $specDepotName = args[1].chomp when 'Visualizer' $visualizer = args[1].chomp end end end # # get the total number of lines changed for each file # def GetLinesChanged( strs ) fileDict = {} file = "" strs.each do |s| str = StripInvalidUTF8( s ) total = 0 if( str =~ /^==== / ) file = str.chomp file.slice! 0,6 file.slice!( file.rindex('#'), file.length ) end if( str !~ /^add|deleted|changed\s\d+\schunks\s[\s\d\/]+\slines$/ || str =~ /^Change|job|\/\// ) next end args = str.split ' ' total += args[3].to_i total += args[8].to_i vals = Array.new vals.push args[13].to_i vals.push args[15].to_i total += vals.max fileDict[file] = total end return fileDict end def PrintHeader(output) case $visualizer when 'code_swarm' output.puts "<?xml version=\"1.0\"?>\n<file_events>\n" end end def PrintFooter(output) case $visualizer when 'code_swarm' output.puts "</file_events>\n" end end # # turn a job into code_swarm event # def PrintJob( output, job ) # no decent way to show jobs with gource; skip 'em if( $visualizer == 'gource' ) return end file = "" date = Time.new weight = 1 author = "" job = StripInvalidUTF8( job ) job.each_line do |j| if( j =~ /^#/ ) next end args = j.split( ":\t" ) case args[0] when $jobNameField file = args[1].chomp when $jobModifiedDateField date = Time.parse( args[1] ) #if args[1] != nil when $jobModifiedByField author = args[1].chomp end end file.gsub!("&", "&") file.gsub!("<", "<") file.gsub!(">", ">") file.gsub!("'", "'") file.gsub!("\"", """) if( $ignoreUsers.has_key?( author ) ) return end if( author == "\"\"" || file == "\"\"" || file == "new.job" ) return end output.puts "<event date=\"" + date.to_i.to_s + "000" + "\" filename=\"" + file + ".job" + "\" author=\"" + author + "\" weight=\"" + weight.to_s + "\"/>\n" rescue return end def PrintChange(output, cTime, path, cUser, weight, action) case $visualizer when 'code_swarm' output.puts "<event date=\"" + cTime + "000" + "\" filename=\"" + path.gsub( /&/, '&' ) + "\" author=\"" + cUser + "\" weight=\"" + weight.to_s + "\"/>\n" when 'gource' shortAction = 'M' if( action == 'add' ) shortAction = 'A' elsif( action == 'delete' ) shortAction = 'D' end output.puts "#{cTime}|#{cUser}|#{shortAction}|#{path}" end end # # main # begin progName = "viz_data_gen" $port = "" $user = "" $startingChange = "1" $endingChange = 0 $depotPaths = Array.new $ignorePaths = Array.new $ignoreUsers = {} $ignoreExtensions = {} $verbose = false $condense = false $calculateWeight = false $includeJobs = false $jobModifiedDateField = "Date" $jobModifiedByField = "" $jobNameField = "Job" $specDepotName = "" $visualizer = 'code_swarm' configFile = "" # get the command line options if any, overriding the defaults opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--user', '-u', GetoptLong::REQUIRED_ARGUMENT ], [ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT ], [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], [ '--condense', '-c', GetoptLong::NO_ARGUMENT ], [ '--startingChange', '-s', GetoptLong::REQUIRED_ARGUMENT ], [ '--endingChange', '-e', GetoptLong::REQUIRED_ARGUMENT ], [ '--depotPath', '-d', GetoptLong::REQUIRED_ARGUMENT ], [ '--configFile', '-C', GetoptLong::REQUIRED_ARGUMENT ], [ '--visualizer', '-V', GetoptLong::REQUIRED_ARGUMENT ] ) optDict = {} opts.each{ | opt, arg | optDict[ opt ] = arg } # parse the config file if any. args provided on the command line will override config file values if( optDict.has_key?( "--configFile" ) ) ParseConfigFile( optDict["--configFile"] ) end optDict.each do |opt, arg| case opt when '--help' exit when '--verbose' $verbose = true when '--condense' $condense = true when '--user' $user = arg when '--port' $port = arg when '--startingChange' $startingChange = arg when '--endingChange' $endingChange = arg when '--depotPath' $depotPaths.clear $depotPaths.push( arg ) when '--visualizer' $visualizer = arg end end # make sure we have the settings we need if the user asked to include jobs if( $includeJobs && ( $jobModifiedDateField == "" || $jobModifiedByField == "" ) ) STDERR.puts "JobModifiedDateField and JobModifiedByField must be set to include job changes." exit end if( $visualizer != 'code_swarm' && $visualizer != 'gource') STDERR.puts 'Visualizer must be set to either code_swarm or gource' exit end if( $visualizer == 'gource' && !$condense ) puts "Condensed history is automatically enabled for Gource." $condense = true end if( $visualizer == 'gource' && $includejobs ) puts "Job activity is not supported for Gource. Disabling." $includejobs = false end if( $visualizer == 'gource' && $calculateWeight ) puts 'Weight calculations are not supported in gource. Disabling.' $calculateWeight = false end # open target file filename = 'perforce.xml' if( $visualizer == 'gource' ) filename = 'perforce.gource' end output = File.new(filename, "w") # print header PrintHeader(output) # set us up the Perforce p4 = P4.new() p4.prog = progName if( $port != "" ) p4.port = $port end if( $user != "" ) p4.user = $user end p4.connect() # fetch job data if( $includeJobs && $visualizer == 'code_swarm') if( $specDepotName == "" ) $stderr.puts "Please specify a spec depot in the config file using the SpecDepotName variable" exit end # get bounding dates sd = p4.run_describe( $startingChange ) d = Time.at( sd[0]["time"].to_i ) startingDate = d.year.to_s + "/" + d.month.to_s + "/" + d.day.to_s endingDate = "" if( $endingChange != 0 ) ed = p4.run_describe( $endingChange ) d = Time.at( ed[0]["time"].to_i ) endingDate = d.year.to_s + "/" + d.month.to_s + "/" + d.day.to_s else endingDate = "now" end js = p4.run_files( "-a", "//" + $specDepotName + "/job/...@" + startingDate + "," + endingDate ) p4.tagged = false js.each do |j| if ( $verbose ) puts "Processing job " + j["depotFile"] + "#" + j["rev"] end p4.exception_level = P4::RAISE_NONE job = p4.run_print( "-q", j["depotFile"] + "#" + j["rev"] ) line = "" if ( job.length != 0 ) PrintJob( output, job.join( "\n" ) ) end end p4.tagged = true end # build the changes command, and get the changes $depotPaths.each_index do | i | $depotPaths[i] += "@>" + $startingChange if( $endingChange != 0 ) $depotPaths[i] += "," + $endingChange end end changeDict = {} $depotPaths.each do | dp | cs = p4.run_changes( dp ) cs.each do | c | if( !changeDict.has_key?( c["change"].to_i ) && !$ignoreUsers.has_key?(c["user"].chomp) ) changeDict[c["change"].to_i] = 1 end end end changes = changeDict.keys.sort # delete changes that are in the ignore paths $ignorePaths.each do | ip | cs = p4.run_changes( ip ) cs.each do | c | changes.delete( c["change"].to_i ) end end files = {} # run through each change to get the pertinent info changes.each do | change | weightDict = {} p4.exception_level = P4::RAISE_NONE result = p4.run_describe( change ) if( $calculateWeight ) p4.tagged = false; diffs = p4.run_describe( "-ds", change ) weightDict = GetLinesChanged( diffs ) p4.tagged = true; end if( result.length == 0 ) next end cUser = result[0]["user"] cTime = result[0]["time"] if( result[0]["depotFile"] == nil ) next end if ( $verbose ) print "Processing change " + change.to_s + "...\n" end for i in 0..result[0]["depotFile"].length-1 do path = result[0]["depotFile"][i] action = result[0]["action"][i] rev = result[0]["rev"][i] require 'iconv' unless String.method_defined?(:encode) if String.method_defined?(:encode) path.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') path.encode!('UTF-8', 'UTF-16') else ic = Iconv.new('UTF-8', 'UTF-8//IGNORE') path = ic.iconv(path) end path =~ /.*(\..*)/ if( $ignoreExtensions.has_key?( $1 ) ) next end if ( $condense ) if ( action == "branch" ) pathrev = path + '#' + rev log = p4.run_filelog( "-m1", pathrev ) if ( log.length < 1 || log[0].revisions.length < 1 ) next end for j in 0..log[0].revisions[0].integrations.length-1 do integ = log[0].revisions[0].integrations[j] if ( integ.how != "branch from" ) next end if ( files.has_key?(integ.file) ) files[path] = files[integ.file] break end files[path] = integ.file end next end if ( action == "integrate" ) next end if ( files.has_key?(path) ) path = files[path] end end # condense weight = 1 if( weightDict.has_key?( result[0]["depotFile"][i] ) ) weight = weightDict[result[0]["depotFile"][i]] end PrintChange(output, cTime, path, cUser, weight, action) end end # print footer PrintFooter(output) end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#3 | 8566 | drakino |
Converting path between UTF-8 to UTF-16 to scan for and replace any bad UTF byte sequences. Resolves possible issue where a bad character in a filename would result in an exception "invalid byte sequence in UTF-8 (ArgumentError)" being thrown. Suggested fix from http://stackoverflow.com/questions/2982677/ruby-1-9-invalid-byte-sequence-in-utf-8 #review @matt_attaway |
||
#2 | 8399 | Matt Attaway |
Add support for Gource friendly output With this change adding 'Visualizer=gource' to the config file will shockingly enough enable output for the quite excellent gource version control visualizer. To get a copy of gource visit: http://code.google.com/p/gource/ |
||
#1 | 8372 | Matt Attaway |
Add tool to generate data for the code_swarm version control visualizer. This tool generates the necessary data for code_swarm to display the evolution of your source code. It's unique in that it has options for compressing variants into one node, excluding paths and file extensions, and including jobs that are filed and fixed so that your QA people aren't left out of the fun. All details are in the ruby script. More info on code_swarm can be found at http://www.michaelogawa.com/code_swarm/ Thanks to @sam_stafford for the variant folding code. |