from __future__ import print_function import P4 import P4Triggers import sys import os class DirEntry: def __init__(self, parent, fullPath): self.parent = parent self.fullPath = fullPath def makePath(self, name): return os.path.join(self.fullPath, name) def __repr__(self): return self.fullPath class FileDirectoryProtector( P4Triggers.P4Trigger ): """FileDirectoryProtector is a subclass of P4Trigger. Use this trigger to ensure that your depot does not contain files and directories with exactly the same name. If you have a filename that matches a directory name with the same path, your depot is in trouble. Syncs will fail with funny error messages or subsequently toggle between file and directory - and only an obliterate can resolve this issue. This trigger is designed to prevent you to get into this illegal state in the first place. """ def __init__( self, **kargs): kargs['api_level'] = 82 P4Triggers.P4Trigger.__init__(self, **kargs) # need to reset the args in case a p4config file overwrote them for (k,v) in kargs.items(): if k != "log": setattr( self.p4, k, v) self.depots = {} self.directories = {} self.fileEntries = [] self.debug = False 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.caseSensitive = (info["caseHandling"] == "sensitive") self.USER_MESSAGE=""" Your submission has been rejected because the following files or directories already exist as a directory or file. """ self.BADFILE_FORMAT=""" Your file: {0} """ self.BADDIRECTORY_FORMAT=""" Your directory: {0} """ def validate( self ): """Here the fun begins. This method overrides P4Trigger.validate()""" files = self.change.files badfiles = [] baddirs = [] self.build_list(files) if len(self.fileEntries) > 0: badfiles = self.check_files() if len( badfiles ) > 0: self.report( badfiles, self.BADFILE_FORMAT ) if len(self.directories) > 0: baddirs = self.check_directories() if len( baddirs ) > 0: self.report( baddirs, self.BADDIRECTORY_FORMAT ) return ( len(badfiles) == 0 and len(baddirs) == 0 ) # Idea: # Break the files in the change down into directories + file # Build a tree hierarchy of directories # Then: # Check directories against existing files # Check files against existing directories def build_list(self, files): for file in files: df = file.depotFile action = file.revisions[0].action if not (action == "add" or action == "branch"): continue # a bit of hokus pokus. We want to split the path, include the depot but get rid of the first two slashes split_list = df[2:].split('/') depot = split_list[0] dirs = split_list[1:-1] filename = split_list[-1] if not self.caseSensitive: depot = depot.lower() dirs = [ x.lower() for x in dirs ] filename = filename.lower() if depot in self.depots: root = self.depots[depot] else: root = DirEntry(None, depot) self.depots[depot] = root for d in dirs: path = root.makePath(d) if path in self.directories: root = self.directories[path] else: root = DirEntry(root, path) self.directories[path] = root self.fileEntries.append(DirEntry(root, root.makePath(filename))) if self.debug: print("Depots:") for d in self.depots: print(d) print("\nDirectories:") for d in self.directories: print(d) print("\nFiles:") for f in self.fileEntries: print(f) def check_directories(self): filelist = [ '//' + x for x in self.directories ] # any existing file listed here is automatically bad badlist = self.p4.run_files(filelist) return [ x['depotFile'] for x in badlist ] def check_files(self): files = set() roots = set() for f in self.fileEntries: files.add('//' + f.fullPath) roots.add('//' + f.parent.fullPath) # print("Check these roots {}".format( roots )) # print("For these files {}".format(files)) to_check = [ x + '/*' for x in list(roots) ] dirs = self.p4.run_dirs(to_check, exception_level = 1) # ignore "no files" warning existing_dirs = set() for d in dirs: existing_dirs.add(d["dir"]) cross_section = existing_dirs & files # print("Cross section = {}".format(cross_section)) return list(cross_section) def report( self, badfiles, template ): msg = self.USER_MESSAGE for file in badfiles: msg += template.format( file ) 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 as e : print("Error, expecting arguments in form key=value. Bailing out ...") print(e) sys.exit(0) ct = FileDirectoryProtector(**kargs) sys.exit( ct.parseChange( sys.argv[1] ) )
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#4 | 21258 | Sven Erik Knop |
Fixed crashing on changes that have no add or branches. Improved reporting. |
||
#3 | 21202 | Sven Erik Knop |
Completed the trigger for now, subject to unit tests This trigger is supposed to prevent a submit of a new file (or a branch) that would create a conflict between a filename and a directory name. In Perforce, it is currently possible to submit a filename that exists already as a directory name, because the server does not recognise directory names as independent entities. When this happens, clients cannot sync files anymore from the path: files and directories cannot live in the same file system with the same name. This trigger will analyse a submit (as a change-submit trigger *only*): - extract all "add" and "branch" operations - extract all unique directories out of the above list - extract all file names out the above list - check that there is no file already submitted that matches the directories - check that there is no directory already submitted that matches the files This trigger will run 3 Perforce queries: extract the change, list matching files and list matching directories. This trigger should run as a user who has list permissions for all possible files under the path specified in the trigger table. The user needs to have a permanent ticket available on the server for the trigger to run. Output is a list of all violating files: Your submission has been rejected because the following files or directories already exist as a directory or file. Your file: //depot/file1 |
||
#2 | 21201 | Sven Erik Knop | Now with case-sensitive protection | ||
#1 | 21200 | Sven Erik Knop |
Beginning of FileDirectoryProtector. This trigger is supposed to protect from the situation where a user submits a file that has the same name as a directory. The Perforce server cannot detect this, causing all kinds of problems along the line. Step one: break a submit down into its directories and files. |