CheckCaseTrigger.py #11

  • //
  • guest/
  • sven_erik_knop/
  • P4Pythonlib/
  • triggers/
  • CheckCaseTrigger.py
  • View
  • Commits
  • Open Download .zip Download (9 KB)
#!/usr/bin/python
#
# CaseCheckTrigger.py
#
#
# Copyright (c) 2008, Perforce Software, Inc.  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.
# 
# $Id: //guest/sven_erik_knop/P4Pythonlib/triggers/CheckCaseTrigger.py#11 $
# 
# 

import P4
import P4Triggers
import sys

class CheckCaseTrigger( P4Triggers.P4Trigger ):
	"""CaseCheckTrigger is a subclass of P4Trigger. Use this trigger to ensure
	   that your depot does not contain two filenames or directories that only
	   differ in case.
	   Having files with different case spelling will cause problems in mixed 
	   environments where both case-insensitive clients like Windows and case-
	   sensitive clients like UNIX access the same server.
	"""
	
	def __init__( self, maxErrors, **kargs):
		kargs['charset'] = 'none'
		kargs['api_level'] = 71
		
		fileFilter = None
		if 'filefilter' in kargs:
			fileFilter = kargs['filefilter']
			del kargs['filefilter']
		
		P4Triggers.P4Trigger.__init__(self, **kargs)
	  
		# need to reset the args in case a p4config file overwrote them
		for (k,v) in kargs.iteritems():
			if k != "log":
				setattr( self.p4, k, v)
		
		self.map = None
		if fileFilter:
			try:
				with open(fileFilter) as f:
					self.map = P4.Map()
					for line in f:
						self.map.insert(line.strip())
			except IOError:
				print >> self.log, "Could not open filter file %s" % fileFilter
		
		self.maxErrors = maxErrors
		self.depotCache = {}
		self.dirCache = {}
		self.fileCache = {}
	  
	def setUp( self ):
		info = self.p4.run_info()[0]
		if "unicode" in info and info["unicode"] == "enabled":
			self.p4.charset = "utf8"
		self.p4.exception_level = 1 # ignore WARNINGS like "no such file"
		self.p4.prog = "CheckCaseTrigger"
	  

		self.USER_MESSAGE="""
	
	Your submission has been rejected because the following files
	are inconsistent in their use of case with respect to existing
	directories
	
		"""
	
		self.BADFILE_FORMAT="""
	Your file:         '%s'
	existing file/dir: '%s'
		"""
	
	def validate( self ):
		"""Here the fun begins. This method overrides P4Trigger.validate()"""
		badlist = {}
		files = self.change.files
	  
		self.filterRenames(files)
	  
		for file in files:
			action = file.revisions[0].action
			if self.map and self.map.includes(file.depotFile):
				continue
			if not (action == "add" or action == "branch"): 
				continue
		
			mismatch = self.findMismatch( file.depotFile )
			if mismatch:
				badlist[ file.depotFile ] = mismatch
		  
	 	 # end for
	  
		if len( badlist ) > 0:
			self.report( badlist ) 
	
		return len(badlist) == 0
	
	# Method canonical 
	# IN:  string, utf8 compatible
	# OUT: unicode string, all lower case
	def canonical( self, aString ):
		if self.p4.charset == "utf8":
			return unicode( aString, "utf8").lower()
		else:
			return aString.lower()

	# Method filterRenames:
	# Removes pairs of files that only differ in case
	# where one action is branch, and the other delete
	def filterRenames( self, files ):
		branches = [ x for x in files if x.revisions[0].action == 'branch' ]
		deletes = [ x.depotFile.lower() for x in files if x.revisions[0].action == 'delete' ]
		
		for f in branches:
			if f.depotFile.lower() in deletes:
				files.remove(f)

	# This method returns either a depot or directory that differs from 
	# the supplied path only in case, or None if no mismatch is found
	def findMismatch( self, path ):
		path = path[2:] # cut the // at the beginning of the depot off
		dirs = path.split('/')
		depot = dirs.pop(0)
		file = dirs.pop()
		
		depotMismatch = self.findDepotMismatch( depot )
		if ( depotMismatch ):
			return depotMismatch
		
		oldDirs = dirs[:]
		dirMismatch = self.findDirMismatch( '//' + depot, dirs )
		if ( dirMismatch ):
			return dirMismatch
		
		fileMismatch = self.findFileMismatch( depot, oldDirs, file )
		if ( fileMismatch ): 
		  return fileMismatch
		
		return None

	# This method looks for a depot mismatch
	# It caches the depots as it found them
	# IN: depot name (as provided by the change list
	# OUT: None if (no mismatch found) else (stored depot name)

	def findDepotMismatch( self, depot ):
		canonicalDepot = self.canonical( depot )
	
		# if the depot is in the cache, the cached version is authorative
	
		if canonicalDepot in self.depotCache:
			if self.depotCache[ canonicalDepot ] == depot:
				return None
			else:
				return '//' + self.depotCache[ canonicalDepot ]
	
		# depot not found in cache. Populate the cache
	
		mismatch = None
		for d in self.p4.run_depots():
			dname = d[ "name" ]
			cd = self.canonical( dname )
			self.depotCache[ cd ] = dname
			if canonicalDepot == cd and depot != dname:
				mismatch = '//' + dname
	
		return mismatch
	# end findDepotMismatch

	# Look for matching or mismatching directories
	# Recursive algorithm, descending down the directory paths
	# Caching directories as we find them
	# 
	
	def findDirMismatch( self, top, dirs ):
		if len(dirs) == 0:
			return None
		
		aDir = dirs.pop(0)
		path = top + '/' + aDir
		cpath = self.canonical( path )
		
		if cpath in self.dirCache :
			# negative lookups are cached with the value set to False
			if ( self.dirCache[ cpath ] == path ) or ( self.dirCache[ cpath ] == False ) :
			   return self.findDirMismatch( path, dirs )
			else:
				# it is a mismatch, either with a previously stored name, or, worse 
				# with a previous file in the same change list
			
				return self.dirCache [ cpath ]
			# end if
		# end if
		
		# so, it is not in the cache. Let's populate the cache and check 
		# while we are at it
		
		mismatch = None
		for d in self.p4.run_dirs( top + "/*"):
			d = d[ "dir" ]	# result is in tagged mode, single entry "dir"=>directory name
			cd = self.canonical ( d )
			self.dirCache[ cd ] = d
			
			if cd == cpath:
				# we found the dir entry. Is it the right case?
				
				if d == path:
					# so far so good, lets step one directory level down
					
					mismatch = self.findDirMismatch( path, dirs )
				else:
					# oh-ho, we found a mismatch
					mismatch = d
				# end if d == path
			# end if cd == cpath
		# end for
		
		if not mismatch:
			# enter as a negative match
			self.dirCache [ cpath ] = False
			
		return mismatch
		
	def findFileMismatch( self, depot, dirs, file ):
		base = '//' + depot + '/'
		if ( len(dirs) > 0 ):
			base += '/'.join(dirs) 
			base += '/'
		
		name = base + file
		cname = self.canonical( name )
		if cname in self.fileCache:
			if ( self.fileCache[ cname ] == name ):
				return None # all is well, but cannot happen, add would fail
			else:
				return self.fileCache[ cname ]
		
		# so the file is not in the cache. Let's load the cache
		for f in self.p4.run_files(base + "*"):
			if "delete" not in f[ "action"]:
				f = f[ "depotFile" ]
				cf = self.canonical( f )
				self.fileCache[ cf ] = f
				if( cf == cname and f != name ):
					return f
		
		# so it is not in the file cache
		self.fileCache[ cname ] = name
		return None
		
	def report( self, badfiles ):
		msg = self.USER_MESSAGE
		for ( n, (file, mismatch) ) in enumerate( badfiles.iteritems() ):
		  if n >= self.maxErrors:
			break
		  msg += self.BADFILE_FORMAT % ( file, mismatch )
		  
		self.message( msg )

# main routine.
# If called from the command line, go in here

if __name__ == "__main__":
	kargs = {}
	try:
	  for arg in sys.argv[2:]:
		(key,value) = arg.split("=")
		kargs[key] = value
	except Exception,e :
		print "Error, expecting arguments in form key=value. Bailing out ..."
		print e
		sys.exit(0)
		
	ct = CheckCaseTrigger( 10 , **kargs)
	sys.exit( ct.parseChange( sys.argv[1] ) )
# Change User Description Committed
#20 23173 Sven Erik Knop Fixed one problem - move/add now recognised
#19 23167 Sven Erik Knop Fix bug where a keyword argument contained '=' itself.

The trigger accepts keyword arguments in the form key=value, for example
port=server:1666

In the test case, I am passing in port as "rsh:p4d -r /path/to/root -vserver=3 -i", which
gets split into three and not two strings. Fortunately, Python's string.split() has an optional
argument limiting the number of splits.
#18 19873 Pascal Soccard Replaced iteritems() method by compatible Python 3 items() method
#17 19872 Pascal Soccard Fixed missing tabulations for previous fix
#16 19871 kwirth 'user' is a reserved argument name in P4Python.
Need to use 'mysuser'
       instead when testing if the current user is 'git-fusion-user'.
As 'mysuser' is not a valid P4Python argument need to remove it
after test and before other arguments are parsed.
#15 15864 Karl Wirth - Incorrect version shelved in update.
This version includes Typo fix.
#14 15860 Karl Wirth Adding an example of how the 'git-fusion-user' can be excluded from
the trigger.

@sven_erik_knop
#13 12221 kwirth Adding usage example text.

Example Usage:
 CheckCaseTrigger change-submit //... "python CheckCaseTrigger.py %changelist%"

Sample output:

Submit validation failed -- fix problems then use 'p4 submit -c 1234'.
 'CheckCaseTrigger' validation failed:

Your submission has been rejected because the following files
are inconsistent in their use of case with respect to existing
directories

Your file:         '//depot/dir/test'
existing file/dir: '//depot/DIR'
#12 8233 Sven Erik Knop CheckCaseTrigger is now safe to be used with Python 3, ...
that is,
when P4Python 2013.1 comes out. There is currently a bug with charsets that
prevents this trigger from working against non-Unicode servers.
#11 8179 Sven Erik Knop Added filefilter to the CheckCaseTrigger.

A filefilter is added as an additional key on the command line:

filefilter=path-to-my-filter-file

It should contain valid Perforce depot syntax lines, which are treated as
exceptions. Changed files that match any of the lines in the filefilter will
be allowed to submit even if they break the case consistency.

This is for cases where some files are Unix-specific and need to be able to
store two files or directories that only differ by case - as an exception.

Also fixed a tabbing problem in the P4Triggers.py file.
#10 8178 Sven Erik Knop Filter out move/delete as well as delete from the list of existing
files. This solves the problem of a re-add to the deleted file if a
differently spelled version is also present in the same directory, but
with action "move/delete".

Consider the following case:

p4 add foo
p4 submit -d foo
p4 edit foo
p4 move foo Foo
p4 submit -d renamed

p4 delete Foo
p4 submit -d deleted

p4 add Foo
=> this would fail because "foo" is present with an action of "move/delete".

This is now fixed.
#9 8165 Sven Erik Knop Prevent triggering of case rejection if the offending file
has been deleted at head revision.
#8 8144 Sven Erik Knop Filtering of renames in branches.
Now the trigger lets case renames in branches pass, if the corresponding delete
is in the same changelist.
#7 8142 Sven Erik Knop Fixed formatting and missing message output.
#6 7900 Sven Erik Knop Bumped up API level to 65 to allow move/add to be correctly recognized and ignored.
This opens up the trigger for abuse if someone renames a file to a new unrelated name that already exists in a different case spelling. This requires further investigation.
#5 7832 Sven Erik Knop Updated CheckCaseTrigger to show the proper exception if parameters are wrong.
#4 7379 Sven Erik Knop Added output to a log file.
The default is the send output to p4triggers.log in the P4ROOT directory, this can be overridden with the parameter log=<path>
Also, errors now cause the trigger to fail with sensible output first.
#3 7372 Sven Erik Knop Rollback Rename/move file(s).
To folder "perforce" is needed.
#2 7370 Sven Erik Knop Rename/move file(s) again - this time to the right location inside a perforce directory.
#1 7367 Sven Erik Knop New locations for the Python triggers.
//guest/sven_erik_knop/perforce/P4Pythonlib/triggers/CheckCaseTrigger.py
#1 7370 Sven Erik Knop Rename/move file(s) again - this time to the right location inside a perforce directory.
# Change User Description Committed
#1 7367 Sven Erik Knop New locations for the Python triggers.
//guest/sven_erik_knop/triggers/CheckCaseTrigger.py
#3 7219 Sven Erik Knop First attempt for renamer support, not finished yet, therefore disabled.
#2 7218 Sven Erik Knop Updated CheckCaseTrigger.py to fix problems with files within directories.

The trigger would not detect case problems for files that are located in
subdirectories. Unintentional side effect of modifying the dirs list recursively
when checking for mismatched directories.
The solution was simple: make a copy of the directory list for the file check.
#1 6413 Sven Erik Knop Added some P4Python-based Perforce triggers.

P4Triggers.py is the based class for change trigger in Python modelled on
Tony Smith's Ruby trigger with the same name.

CheckCaseTrigger.py is a trigger that ensures that no-one enters a file
or directory with a name only differing by case from an existing file. This
trigger is Unicode aware and uses Unicode-comparison of file names, so it
can be used on nocase-Unicode based Perforce servers, which cannot catch
the difference between, say, "�re" and "�re" at the moment.

clienttrigger.py is a simple trigger that modifies the option "normdir" to
"rmdir" for new client specs only. It is meant as a template to create more
complex default settings like standard views.