#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ============================================================================== # Copyright and license info is available in the LICENSE file included with # the Server Deployment Package (SDP), and also available online: # https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE # ------------------------------------------------------------------------------ """ NAME: CheckFolderStructure.py DESCRIPTION: This trigger is a change-submit trigger for workflow enforcement using Workflow.yaml for streams. It will ensure that any attempt to add (or branch) a file does not create a new folder name at specific levels. As with other Workflow triggers, it looks for a project matching files in the change. The value of "new_folder_allowed_level" is an integer and controls at which level within a stream new folder names can be created. If not set, or set to 0, then new folders can be created in the root of the stream. If set to 1, then for stream //streams/main/new_folder/a.txt would not be allowed, but //streams/main/existing_folder/new_folder/a.txt would be allowed. Note that files can be added just not folders. The field "new_folder_exceptions" - an array of users/groups who can override the trigger. To install, add a line to your Perforce triggers table like the following: check-folder-structure change-submit //... "python3 /p4/common/bin/triggers/CheckFolderStructure.py -p %serverport% -u perforce %change%" or (if server is standard SDP and has appropriate environment defaults for P4PORT and P4USER): check-folder-structure change-submit //... "python3 /p4/common/bin/triggers/CheckFolderStructure.py %change%" You may need to provide the full path to python executable, or edit the path to the trigger. Also, don't forget to make the file executable. """ # Python 2.7/3.3 compatibility. from __future__ import print_function import sys import P4 import WorkflowTriggers from os.path import basename, splitext from collections import defaultdict trigger_name = basename(splitext(__file__)[0]) class NewDirFinder: "Finds new directories in paths - seperate class for ease of testing" def __init__(self, p4): self.p4 = p4 self.dirCache = {} # This method returns None if no new directory found, or the directory name def findNewDir(self, path, level): path = path[2:] # cut the // at the beginning of the depot off dirs = path.split('/') depot = dirs.pop(0) stream = dirs.pop(0) file = dirs.pop() # Remove file return self.findNewDirs('//%s/%s' % (depot, stream), dirs, level) # Look for new directories # Recursive algorithm, descending down the directory paths # Caching directories as we find them def findNewDirs(self, top, dirs, level): if len(dirs) == 0 or level == 0: return None aDir = dirs.pop(0) path = top + '/' + aDir newdir = None if top in self.dirCache: existingDirs = self.dirCache[top] else: # result is in tagged mode, single entry "dir"=>directory name existingDirs = [d["dir"] for d in self.p4.run_dirs(top + "/*")] self.dirCache[top] = existingDirs for d in existingDirs: if d == path: return self.findNewDirs(path, dirs, level - 1) return path # It is a new directory class CheckFolderStructure(WorkflowTriggers.WorkflowTrigger): """See module doc string for details""" def __init__(self, *args, **kwargs): WorkflowTriggers.WorkflowTrigger.__init__(self, **kwargs) self.parse_args(__doc__, args) def add_parse_args(self, parser): """Specific args for this trigger - also calls super class to add common trigger args""" parser.add_argument('-c', '--config-file', default=None, help="Configuration file for trigger. Default: Workflow.yaml") parser.add_argument('change', help="Change to validate - %%change%% argument from triggers entry.") super(CheckFolderStructure, self).add_parse_args(parser) def run(self): """Runs trigger""" try: self.logger.debug("%s firing" % trigger_name) config = self.load_config(self.options.config_file) errors = [] for k in "msg_new_folder_not_allowed".split(): if k not in config: errors.append("Config file %s missing definition for %s" % (self.options.config_file, k)) if errors: msg = "%s: Invalid config file for trigger %s\n" % (trigger_name, str(errors)) self.message(msg) return 1 self.setupP4() self.p4.connect() change = self.getChange(self.options.change) files = [x.depotFile for x in change.files] # Only process if adding or branching files actions = [x.revisions[0].action for x in change.files if x.revisions[0].action in ['add', 'branch', 'move/add']] if not actions: self.logger.debug("%s: Ignoring change as no add actions" % trigger_name) return 0 # If no project then don't check further prj = self.get_project_by_files(config, files) self.logger.debug("prj: %s" % str(prj)) if not prj: self.logger.debug("%s: Allowing folder as no project affected" % trigger_name) return 0 if not 'new_folder_allowed_level' in prj: self.logger.debug("%s: Allowing changes as 'new_folder_allowed_level' not specified" % trigger_name) return 0 level = 0 try: level = int(prj['new_folder_allowed_level']) except ValueError: level = 0 if level <= 0: self.logger.debug("%s: Allowing changes as level not specified" % trigger_name) return 0 # Now check for users and groups who are exceptions if 'new_folder_exceptions' in prj: gchk = GroupMemberChecker(self.p4) if gchk.IsMember(change.user, prj['new_folder_exceptions']): self.logger.debug("%s: User allowed to bypass trigger: %s" % (trigger_name, user)) return 0 # Finally we check the individual files errors = [] # Todo - calculate stream depth properly for streams depots referenced. new_files = [x.depotFile for x in change.files if x.revisions[0].action in ['add', 'branch', 'move/add']] df = NewDirFinder(self.p4) for f in new_files: newdir = df.findNewDir(f, level) if newdir: errors.append(newdir) if errors: err_msg = "\n".join(config['msg_new_folder_not_allowed']) msg = err_msg + "\nNew folders being created:\n%s\n\n" % ", ".join(errors) self.message(msg) return 1 except Exception: return self.reportException() return 0 if __name__ == '__main__': """ Main Program""" trigger = CheckFolderStructure(*sys.argv[1:]) sys.exit(trigger.run())
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#4 | 26652 | Robert Cowham |
This is Tom's change: Introduced new 'Unsupported' directory to clarify that some files in the SDP are not officially supported. These files are samples for illustration, to provide examples, or are deprecated but not yet ready for removal from the package. The Maintenance and many SDP triggers have been moved under here, along with other SDP scripts and triggers. Added comments to p4_vars indicating that it should not be edited directly. Added reference to an optional site_global_vars file that, if it exists, will be sourced to provide global user settings without needing to edit p4_vars. As an exception to the refactoring, the totalusers.py Maintenance script will be moved to indicate that it is supported. Removed settings to support long-sunset P4Web from supported structure. Structure under new .../Unsupported folder is: Samples/bin Sample scripts. Samples/triggers Sample trigger scripts. Samples/triggers/tests Sample trigger script tests. Samples/broker Sample broker filter scripts. Deprecated/triggers Deprecated triggers. To Do in a subsequent change: Make corresponding doc changes. |
||
#3 | 26433 | Robert Cowham | Refactored - group checker moved to WorkflowTriggers.py | ||
#2 | 25626 | Robert Cowham | Add group membership checking for exceptions list (to bypass trigger). | ||
#1 | 25625 | Robert Cowham |
First version of trigger to enforce stream folder naming structure - control at which level in a stream users are allowed to create new folders. |