#!/usr/bin/ruby
#--
#-------------------------------------------------------------------------------
#++
#
#= Introduction
#
#== Name: check_case_consistency.rb
#
#== Author: Tony Smith <tony@perforce.com>
# Robert Cowham <robert@vaccaperna.co.uk>
#
#== 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 ) )