# This Python script checks directory and file names for case variation # when submitting files for add or branch. # See Perforce Technical Note #3 for more details of the problem # we are trying to prevent with this pre-submit trigger. # http://www.perforce.com/perforce/technotes/note003.html # This only works with Perforce server version 2002.2 or greater. # ( p4 describe doesn't return a list of file names in pending # changelists from earlier servers. ) # # Check case before submit for add or branch in a changelist. # Checks file names and directory path names. # # This is a recursive script. Perhaps a time saver for very large depots, # since only directories or files that match are queried. # An optimization has been added to group files by the first name under the # depot name, and finding out if the lowest common path matches a depot path # before walking the tree. This can be very helpful, for example, when # branching a large number of files. # # Could be slower for a small depot since more p4 commands issued compared # to simply running a 'p4 files //...' command and wading through the output. # # Administrators can create depot names that differ in case but this script # will prevent users from adding files to the offending depot(s) until the # problem is corrected. (That's assuming we're running checkcase from the # beginning of the Perforce server's existence.) # # As long as your Perforce server can locate the script you won't need the # p4port setting. This is left in for testing so you can run the script # remotely by supplying a changelist as an argument, e.g. # python checkcase.py 10327 # # Gerry Thompson training Perforce 2005.2 version 1.1 import os, re, string, sys p4port = 'YourServer:1666' # This is a Perforce server. p4user = 'YourUserName' # Perforce user name to use. p4client = 'none' # This script does not require a client spec. p4password = 'apassword' # If user has a password, include in p4 below. p4path = 'p4' # Path to p4 executable. debug = 1 # Set this to 0 (zero) to suppress print statements. p4calls = 1 # Set this to 1 to see how many p4 commands are issued. morep4calls = 0 # Set this to 0 (zero) to use the 'next' optimization # methods.(see below) # Example, using a single test changelist (93873): # Number of files in test server 347478 # Number of files opened for branch in 93873 1020 # Number of files opened for edit in 93873 909 # Number of files opened for add in 93873 4 # Number of files opened for delete in 93873 2 # # All opened files had no case conflict so all files were evaluated. # # Time to run with morep4calls = 1 432 seconds requiring 2069 p4 calls. # Time to run CheckC.pl#4 (Perforce Public Depot) 12 seconds requiring 2 p4 calls. # Time to run with debug=0, morep4calls=0, p4calls=0 4 seconds requiring 19 p4 calls. p4 = p4path + ' -u'+ p4user + ' -p' + p4port # + ' -P ' + p4password + ' -c ' + p4client numberofp4calls = 0 def evalfile(lowerfilename, filename, depotpath): testpath = '"//' + depotpath + '/*"' getfile = re.compile('^info:.*'+depotpath+'/(.*)\#/?') reglist = [] reglistlower = [] # Get a list of depot files for comparison. for cline in os.popen(p4 + ' -s files ' + testpath, 'r').readlines(): if re.match('^info:.*', cline): regfile = getfile.search(cline).group(1) reglist.append(regfile) reglistlower.append(string.lower(regfile)) if p4calls: global numberofp4calls numberofp4calls = numberofp4calls + 1 if lowerfilename in reglistlower: if debug: # DEBUG -start print print "Filename: " + lowerfilename + " has been detected in the server." # DEBUG -end print if filename not in reglist: print "CASE mismatch detected in file name: " + filename sys.exit(1) # DEBUG -else not needed. else: if debug: print "\n" print "New file found: " + filename # DEBUG -end else, print def nextlevel(lower, depot): list = [] # The depot path gets passed in unaltered. for singleline in os.popen(p4 + ' dirs "//' + depot + '/*"','r').readlines(): if lower: list.append(string.lower(string.rstrip(singleline))) else: list.append(string.rstrip(singleline)) # string.rstrip removes '\n' if p4calls: global numberofp4calls numberofp4calls = numberofp4calls + 1 return(list) def processpaths(lowerpath, upperpath, depot, advancednext): smallerpath = [] # We use these lists to reset our path search when # processing the next path. makeitlower = 0 ablist = nextlevel(makeitlower, depot) makeitlower = 1 ablistlower = nextlevel(makeitlower, depot) for j in range(len(lowerpath)): morepaths = 1 if debug: # DEBUG -start print print '\n' print "Checking this path: ", lowerpath[j] # DEBUG -end print next = advancednext nextdepotpath = depot blist = ablist blistlower = ablistlower while morepaths: next = next + 1 afile = lowerpath[j] anotherfile = upperpath[j] dirs = string.split(afile, '/') upperdirs = string.split(anotherfile, '/') if debug: # DEBUG -start print print "Number of directories under //"+nextdepotpath+":", len(blistlower) # DEBUG -end print print "If",len(dirs)-1,"=",next,"we have a file." if ((len(dirs)-1) == next): lowerfilename = dirs[next] upperfilename = upperdirs[next] evalfile(lowerfilename, upperfilename, nextdepotpath) # We have reached the end of searching since we # have found a file. morepaths = 0 elif (len(blistlower) == 0): if debug: print "New directory found." elif (len(blistlower) > 0): # If blistlower is empty then we # have a new directory. We use elif # so we don't execute this part if # we've just evaluated a file name. candidate = dirs[next] nextdepotpath = nextdepotpath + '/' + upperdirs[next] checkblist = re.compile('/'+candidate+'$') checklowerpath = re.compile('/'+candidate+'/') tempdirs = filter(checkblist.search, blistlower) if debug: print "Candidate path is: ", candidate if len(tempdirs) > 0: newsmallpath = filter(checklowerpath.search, lowerpath) checkupperdir = re.compile('^.*/'+upperdirs[next]+'/?') casecheck = filter(checkupperdir.search, blist) if debug: print "This path found in server: " , casecheck[0] if len(casecheck) == 0: # This means a lower case match was # found but not an upper case match. print 'CASE mismatch detected in directory name.' sys.exit(1) else: # Evaluate next set of directories. upperlist = filter(checkupperdir.search, upperpath) makeitlower = 0 blist = nextlevel(makeitlower, nextdepotpath) makeitlower = 1 blistlower = nextlevel(makeitlower, nextdepotpath) else: morepaths = 0 if debug: print "New directory being added: " + upperdirs[next] def quickcheck(upper, depot, next): # We check to see if we can advance next to a higher level, # If not then we set next = 2 and walk the tree starting at the depot name. if (len(upper) > 1): # Test if we have more than one element. onepath = upper[0] pathlist = string.split(onepath, '/') for i in range(len(pathlist)): if (i == 2): newstring = depot if (i > 2): if (i < next+1): newstring = newstring+"/"+pathlist[i] for singleline in os.popen(p4 + ' files "//'+ newstring + '/*"','r').readlines(): index = string.find(singleline, "no such file(s).") if (index > 0): # This means our path doesn't match a depot path. next = 2 else: depot = newstring break # No need to process any further. if p4calls: global numberofp4calls numberofp4calls = numberofp4calls + 1 pathlist = [depot, next] else: pathlist =[depot, next] return pathlist def launchevaluation(lowerdict, upperdict, depot, next): for key in upperdict.keys(): next = 2 upper = upperdict[key] lower = lowerdict[string.lower(key)] if (len(upper) > 1): # More than one element in our dictionary. first = upper[0] firstlist = string.split(first, "/") test = 4000 # Arbitrarily large number. I hope my depot doesn't lowest = 3000 # have 4000 directory levels! for i in range(len(upper)-1): compare = upper[i+1] comparelist = string.split(compare, "/") for j in range(len(firstlist)-2): if (firstlist[j+2] == comparelist[j+2]): test = j else: test = j-1 continue if (test < lowest): # We are looking for the least number of paths # that match in each dictionary element. lowest = test next = lowest shortlist = quickcheck(upper, depot, next) newdepot = shortlist[0] newnext = shortlist[1] processpaths(lower, upper, newdepot, newnext) def makedictionary(paths, next): # We are going one name past the depot name, could be a file name. nextpath = next+1 dict = {} list = [] tempname = "" for i in range(len(paths)): #get the first name: singlepath = paths[i] dirs = string.split(singlepath, '/') name = dirs[nextpath] if (tempname == name): list.append(singlepath) else: tempname = name list = [] list.append(singlepath) dict[name] = list return dict def checkdepots(depotnames, lowerdepots, path, lowerpath): # Need to sort the list so the rest will go smoothly: path.sort() lowerpath.sort() next = 2 name = re.compile('^//(.+)/?') # Evaluate the depot path names to see if there is an error for i in range(len(lowerdepots)): depot = name.search(lowerdepots[i]).group(1) test = re.compile('//'+depot+'/') lower = filter(test.search, lowerpath) if len(lower) > 0: # Compare unaltered names, if one doesn't match, fail. depot = name.search(depotnames[i]).group(1) test = re.compile('//'+depot+'/') upper = filter(test.search, path) if len(lower) != len(upper): print 'CASE error detected in depot name.' sys.exit(1) else: if morep4calls: processpaths(lower, upper, depot, next) else: lowerdict = makedictionary(lower, next) upperdict = makedictionary(upper, next) launchevaluation(lowerdict, upperdict, depot, next) def getlists(change): # Get a list of depots and files open for add or branch. depots = [] depotslower = [] # This list is the lowercased depot path. path = [] pathlower = [] # This list the the lowercased file path. # Arguments are passed to a regular expression as a string. checkoutput = re.compile('(\sadd$|\sbranch$)') cleanup = re.compile('^....(//.*?)\#/?') # Error may be encountered if a remote depot is # defined that is off-line. for aline in os.popen(p4 + ' dirs "//*"','r').readlines(): # Quoting helps so the * doesn't get expanded locally. depots.append(string.rstrip(aline)) depotslower.append(string.lower(string.rstrip(aline))) for bline in os.popen(p4 + ' describe ' + change,'r').readlines(): if checkoutput.search(bline): path.append(cleanup.search(bline).group(1)) pathlower.append(string.lower((cleanup.search(bline).group(1)))) if p4calls: global numberofp4calls numberofp4calls = numberofp4calls + 2 if len(path) > 0: checkdepots(depots, depotslower, path, pathlower) elif debug: print "No files open for add or branch." if __name__ == '__main__': changelist = sys.argv[1] getlists(changelist) if p4calls: print "\n" print "Total number of p4 commands run:",numberofp4calls