#!/usr/bin/ruby #-- #------------------------------------------------------------------------------- #++ # #= Introduction # #== Name: check_case_consistency.rb # #== Author: Tony Smith # Robert Cowham # #== Description # Example trigger to ensure that new files being added are # consistent in their use of case w.r.t. existing directories. # # This implementation is reasonably efficient as it only uses # "p4 depots" and "p4 dirs" commands and restricts itself to # looking only at the paths that it needs to. It also keeps # a cache of paths that it's already checked in order to keep # the number of commands issued to the minimum. # #== Requires # Ruby # P4Ruby # P4Triggers module # #== Example 'triggers' section: # # Triggers: # check_case_consistency change-submit //... "ruby whatever/check_case_consistency.rb %changelist%" # check_case_consistency change-submit //... "ruby c:/perforce/scripts/check_case_consistency.rb -p %serverport% -u perforce %changelist%" # #== Note # For triggers I recommend you use a P4CONFIG file rather than hard coding # username/password in the script itself. This script assumes you've taken # that advice. # # With the enhanced P4Triggers framework we have a generic way of passing these # via flags such as -p P4PORT -u P4USER etc. #-- #------------------------------------------------------------------------------- #++ $:.unshift( File.dirname( __FILE__ ) ) require "P4" require "P4Triggers" # # The trigger class itself. The main method in here is validate() which # is invoked from the super-class' parse_change() method. # class CaseTrigger < P4Trigger # Constructor. In this trigger we want to limit the number of errors # we might report to the user because on a large changelist that # could be a real pain. Simply pass the maximum number you want your # users to deal with at one time. def initialize( options, max_errors ) @max_errors = max_errors # Set API level to 58 (2005.2) @depot_cache = Hash.new @dir_cache = Hash.new super( options, 58 ) p4.debug = 1 end # # The error message we give to the user when we reject their change. # @@USER_MESSAGE = "\n\n" + "Your submission has been rejected because the following files\n" + "are inconsistent in their use of case with respect to existing\n" + "directories:\n\n" # The printf() style format string used for each of the bad file # entries @@BADFILE_FORMAT= " Your file: '%s' existing dir: '%s'\n" # Enforce case checking. the basic algorithm is that we split the # depot path of each file up into its components and use "p4 depots", # "p4 dirs" (depending on what level we're at to locate # existing depots/dirs with the same names in different case. If # we find any we reject submission. Note that we are only interested in # new files being added/branched since existing files are assumed # to be OK. def validate() badlist = Hash.new change.each_file do |file| # Ignore files not open for add or branch action = file.revisions[ 0 ].action next unless ( action == "add" || action == "branch" ) if ( mismatch_exists( file.depot_file ) ) # @mismatch set by mismatch_exists() et. al. badlist[ file.depot_file ] = @mismatch end end # Now report any problems to the user report( badlist ) if ( ! badlist.empty? ) return badlist.empty? end # This method does the work to check whether or not the components of # a path already exist in different case. We do it by breaking the depot # path up into components where the first part is the depot name, the # last part is the filename and everything in between is a directory # name. We then start at the depot and then iteratively check the # directories one by one. def mismatch_exists( path ) path = path[ 2..-1 ] # Strip off leading // dirs = path.split( "/" ) depot = dirs.shift file = dirs.pop # Now look for mis-matching depots return true if mismatch_depot( depot ) # Now look for mis-matching directories return true if mismatch_dirs( '//' + depot, dirs ) return false end # Method to see whether a depot with a name matching the argument # already exists. def mismatch_depot( depot ) # # Look in the cache first. If the cache has been populated # already, then it's authoritative # lcdepot = depot.downcase if( ! @depot_cache.empty? ) if( @depot_cache.has_key?( lcdepot ) ) if( @depot_cache[ lcdepot ] == depot ) return false else @mismatch = "//" + @depot_cache[ lcdepot ] end else @mismatch = "(no such depot)" # Shouldn't happen end return true end # # Run 'p4 depots' looking for matches. Populate the cache # as we go # match = false p4.run_depots.each do |d| dname = d[ "name" ] lcdname = dname.downcase @depot_cache[ lcdname ] = dname if ( lcdname == lcdepot && dname != depot ) match = true @mismatch = "//" + dname end end match end # Method to descend the tree looking for mismatched directory # names. If we find a mismatch at any level we break off and just # return true. If there are no mismatches, returns false. def mismatch_dirs( top, dirs ) return false if dirs.empty? dir = dirs.shift path = top + "/" + dir lcpath = path.downcase if( @dir_cache.has_key?( lcpath ) ) # # We cache negative lookups too - so the hashed value # may be false # if( @dir_cache[ lcpath ] == path || @dir_cache[ lcpath ] == false ) return mismatch_dirs( path, dirs ) end # It's a mis-match @mismatch = @dir_cache[ lcpath ] return true end # Not in cache, must run 'p4 dirs' mismatch = false p4.run_dirs( top + "/*" ).each do |d| d = d[ "dir" ] # Shift from tagged mode lcd = d.downcase @dir_cache[ lcd ] = d # Add to cache if( lcd == lcpath ) # It exists. If the paths match, we're fine so far and # we should check the lower levels. If not, we have a mismatch if( d == path ) mismatch = mismatch_dirs( path, dirs ) if( d == path ) else @mismatch = d mismatch = true end end end # # If the previous command returned no output, stash it as a # negative lookup # if( p4.output.empty? ) @dir_cache[ lcpath ] = false end mismatch end # Method to report the error to the user. Just formats the error # message and sends it. We only report the first @max_errors # bad files. On a large changelist they'll be grateful for that. def report( badfiles ) errors = 0 msg = @@USER_MESSAGE badfiles.each do |file,mismatch| msg += sprintf( @@BADFILE_FORMAT, file, mismatch ) errors += 1 break if ( errors >= @max_errors ) end message( msg ) end end #-- #------------------------------------------------------------------------------- # Start of main script execution #------------------------------------------------------------------------------- #++ # By this stage it's pretty simple. Even argument validation is handled by # the P4Trigger class so we don't even need to check that we were passed # a changelist number. options = P4TriggerOptions.new(ARGV) options.parse_options!() trig = CaseTrigger.new( options, 10 ) exit( trig.parse_change( ARGV.shift ) )