CbdDev.py #1

  • //
  • cbd/
  • main/
  • scripts/
  • CbdDev.py
  • View
  • Commits
  • Open Download .zip Download (33 KB)
#!/p4/common/python/bin/python3
"""
Copyright (c) 2012-2016 Perforce Software, Inc.

To see expected sync bevhaviors for various forms of the 'p4 sync' command,
run: /p4/common/bin/cbd/test/test_cbd.sh -man

"""
# Support methods for CBD

# imports
import sys, types
import P4
import logging
import logging.config
import os
import ntpath, posixpath
import re
# DEBUG uncomment these two lines for remote debugging
# import pydevd;
# pydevd.settrace('192.168.1.15')

class CbdDev:

    def __init__(self, logstyle):
        # constants
        # If in an SDP environment, P4BIN will be define.  Otherwise, just set it to 'p4' and
        # expect an appropriate version to be on the path.
        self.P4Bin = os.getenv("P4BIN", "p4")
        self.Workspace = None
        self.WorkspaceRoot = None
        self.WorkspaceOptions = None
        self.WorkspaceOwner = None
        self.CSpec = None
        self.CBDSyncPathRules = {}
        self.p4 = P4.P4()

        # globals to read from config files
        self.P4USER = None

        # If in an SDP environment, LOGS will be set to the log dir.  Otherwise use $CBD_HOME/logs.
        # We assume CBD_HOME is set.
        self.LogDir = os.getenv ("LOGS", "%s/logs" % os.getenv ("CBD_HOME"))
        self.LogFile = "%s/%s" % (self.LogDir, os.getenv ("CBD_LOG", "cbd.log"))
        self.Log = None

        # set up logging and read config data
        self.initLog(logstyle)

    def printRewrite(self, msg):
        self.Log.debug("Rewrite: %s" % msg)
        print (msg)

    # set up logging
    def initLog(self, logstyle):
        format='%(levelname)s [%(asctime)s] [%(funcName)s : %(lineno)d] - %(message)s'
        logging.basicConfig(format=format, filename=self.LogFile, filemode='a', level=logging.DEBUG)
        self.Log = logging.getLogger(logstyle)

    # make logger available externally
    def getLogger(self):
        return self.Log

    def __del__(self):
        # disconnect
        if self.p4.connected():
            self.p4.disconnect()

    # Connect to Perforce
    def initP4(self, p4port):
        self.p4.prog = 'CBD'
        self.p4.port = p4port
        self.p4.user = os.getenv ('P4USER', 'perforce')
        self.p4.ticket_file = os.getenv ('P4TICKETS', 'UNDEFINED_P4TICKETS_VALUE')
        self.Log.debug("Using P4USER=%s" % self.p4.user)
        self.Log.debug("Using P4PORT=%s" % self.p4.port)
        self.Log.debug("Using P4TICKETS=%s" % self.p4.ticket_file)
        try:
            self.p4.connect()
            return True

        except P4.P4Exception:
            self.Log.error("Unable to connect to Perforce at P4PORT=%s" % p4port)
            for e in self.p4.errors:
                self.Log.error("Errors: " + e)
            for w in self.p4.warnings:
                self.Log.warn("Warnings: " + w)
            return False

    # Parse broker arguments and store them in a dictionary for
    # easy access.
    def parseBrokerArgs(self):

        vals = dict()
        data = sys.stdin.readlines()
        for arg in data:
            arg.rstrip('\n')
            # self.Log.debug("Read %s" % arg)
            m = re.match("^(.+?):\s+(.+)$", arg)
            k = m.group(1)
            v = m.group(2)
            vals[k] = v
            # self.Log.debug("Arg %s = %s" % (k, v))
        return vals

    # Load client spec fields into self.CSpec; do a 'p4 client -o' (aka fetch).
    def loadWorkspaceInfo(self):
        self.Log.debug("CALL loadWorkspaceInfo (%s)" % self.Workspace)

        try:
            self.CSpec = self.p4.fetch_client (self.Workspace)
            self.Log.debug("CSPEC = %s " % self.CSpec)
            return self.CSpec
        
        except:
            self.Log.error("Could not get client spec named [%s]." % self.Workspace)
            for e in self.p4.errors:  # Display errors
                self.Log.debug(e)
            return None
        
    # Return  'Field:' field of the workspace.
    def getWorkspaceField (self, field):
        self.Log.debug("CALL getWorkspaceField (%s)" % field)

        if (not self.CSpec):
            self.loadWorkspaceInfo()

        try:
            value = self.CSpec[field]
            self.Log.debug("Field %s value is %s." % (field, self.CSpec[field]))
            return value

        except:
            self.Log.debug("Field %s not defined in CSPEC." % field)
            return None

    def getPathInDepotSyntax (self, path):
        # Translate the user-supplied path to depot syntax.
        self.Log.debug("CALL getPathInDepotSyntax (%s)" % path)

        # Path Possibilities:
        # 1. Depot syntax, e.g. //fgs/main/src/Hello.c
        # 2. Workspace syntax, e.g. //my_ws/src/Hello.c
        # 3. Local OS syntax, absolute, e.g. C:/p4/my_ws/src/Hello.c
        # 4. Local OS syntax, simple relative, e.g. Hello.c, src/Hello.c, ./Hello.c, etc.
        # 5. Local OS syntax, complex relative, e.g. ../src/Hello.c, etc.
        # 6. Path is invalid.  Display a reasonable error.

        WorkspaceSyntaxTest = '//%s/' % self.Workspace

        if re.search (r'//', path):
            if not re.match (WorkspaceSyntaxTest, path):
                self.Log.debug("Path (%s) is already in depot syntax." % path)
                return path

        if (not self.CSpec['View']):
            self.Log.debug("Workspace has no View field! Skipping path translation.")
            return path

        viewWithoutExclusions = []

        for viewEntry in self.CSpec['View']:
            if not re.search (r'-//', viewEntry):
                viewWithoutExclusions.append(viewEntry)

        if re.search (r'//', path):
            self.Log.debug("Translating path in Workspace Syntax [%s]." % path)
            depotToWorkspaceMap = P4.Map()
            for viewEntry in viewWithoutExclusions:
                self.Log.debug("VE: %s" % viewEntry)
                depotToWorkspaceMap.insert (viewEntry)
            depotPath = depotToWorkspaceMap.translate (path, 0)
            self.Log.debug("Translated path [%s] to depot syntax as [%s]." % (path, depotPath))
            return depotPath

        self.Log.debug("Translating Local OS Syntax path [%s]." % path)

        if (re.match ('[a-zA-Z]{1}:', path)):
            self.Log.debug("Windows drive letter detected in path.  Normalizing to uppercase.")
            # Create a new string
            newPath = path[0].upper()
            i=1
            while (i < len(path)):
                newPath += path[i]
                i += 1
            path = newPath

        # Special Case: Is the path we are syncing from the workspace root?
        self.Log.debug ("Is workspace root? Checking ...")
        if (re.search ('\/\.\.\.$', path)):
            tempPath = re.sub ('\/\.\.\.$', '', path)
            endsIn3Dots=1
        else:
            tempPath = path
            endsIn3Dots=0

        self.Log.debug ("Comparing %s to %s." % (tempPath, self.WorkspaceRoot))

        if self.WorkspaceRoot == tempPath:
            if endsIn3Dots:
                depotPath = "%s/..." % self.stream
            else:
                depotPath = self.stream

            self.Log.debug ("Path is workspace root. Returning stream root as depotPath: [%s]." % depotPath)
            return depotPath
        else:
            self.Log.debug ("Path is not workspace root. Continuing.")

        depotToWorkspaceMap = P4.Map()
        workspaceToLocalMap = P4.Map()
        for viewEntry in viewWithoutExclusions:
            self.Log.debug("VE: %s" % viewEntry)
            depotToWorkspaceMap.insert (viewEntry)
            tmpMap = P4.Map()
            tmpMap.insert(viewEntry)
            dPath = tmpMap.lhs()[0]
            wPath = tmpMap.rhs()[0]
            lPath = re.sub ('//.*?/', self.WorkspaceRoot + '/', wPath)
            self.Log.debug("Map: Depot [%s] Workspace [%s] Local [%s]." % (dPath, wPath, lPath))
            workspaceToLocalMap.insert (wPath, lPath)
            tmpMap.clear()

        self.Log.debug("Depot to Workspace Map BEFORE:\n%s" % depotToWorkspaceMap)
        newDepotToWorkspaceMap = P4.Map()
        for entry in depotToWorkspaceMap.as_array():
            if re.search (r'-//', entry):
                continue
            self.Log.debug("NVE: [%s]." % entry)
            newDepotToWorkspaceMap.insert (entry)
            self.Log.debug("New Map is now:\n%s" % newDepotToWorkspaceMap)

        depotToWorkspaceMap.clear()
        depotToWorkspaceMap = newDepotToWorkspaceMap

        self.Log.debug("Depot to Workspace Map AFTER:\n%s" % depotToWorkspaceMap)

        depotToLocalMap = P4.Map.join (depotToWorkspaceMap, workspaceToLocalMap)

        self.Log.debug("Depot to Workspace Map:\n%s\n\nWorkspace To Local Map:\n%s\n\nJoined Depot to Local Map:%s\n\n" % (depotToWorkspaceMap, workspaceToLocalMap, depotToLocalMap))

        if (re.match (r'/', path) or re.match ('[a-zA-Z]{1}:', path)):
            self.Log.debug("Path %s is an absolute path." % path)
            absolutePath = path
        else:
            self.Log.debug("Path %s is a relative path." % path)
            cwd = self.CWD

            # For each '../' in the user-supplied path, remove a directory level
            # from the current directory of the user.
            if re.match (r'\.\./', path): 
                self.Log.debug("Relative path [%s] starts with '..'." % path)
                while re.match (r'\.\./', path): 
                    path = re.sub (r'^\.\./', '', path)
                    cwd = os.path.dirname(cwd)

            # If the user specified path starts with './', just trim it
            # since it will foil our mapping translatoins.
            absolutePath = re.sub (r'^\./', '', cwd) + '/' + path
            self.Log.debug("Absolute path [%s] derived from relative path [%s]." %  (absolutePath, path))

        depotPath = depotToLocalMap.translate (absolutePath, 0)

        if not depotPath:
            self.printRewrite("action: REJECT")
            self.printRewrite("message: ERROR: CBD was not able to translate path [%s] to depot syntax.  If you are using P4V, try using the Depot Tab instead of the Workspace tab." % path)
            sys.exit(1)

        self.Log.debug("Translated path [%s] to depot syntax as [%s]." % (path, depotPath))
        return depotPath

    def loadSyncPathsFromFile (self, reqWs, stream, vSpec):
        """ loadSyncPathsFromFile()
        """
        self.Log.debug("CALL loadSyncPathsFromFile(%s, %s)" % (reqWs, stream))

        # Find the right version of the Stream Spec Template (.cbdsst) file:
        sstFileFinder = "%s/....cbdsst%s" % (stream, vSpec)

        try:
            self.Log.debug ("Running: p4 files -m1 -e %s" % sstFileFinder)
            sstFileData = self.p4.run_files ('-m1', '-e', sstFileFinder)

        except P4.P4Exception:
            for e in self.p4.errors:
                self.Log.error("Errors: " + e)
            for w in self.p4.warnings:
                self.Log.warn("Warnings: " + w)

            self.Log.warning ("Could not access Stream Spec Template file.")
            # we are just going to throw a warning now and pass the command through
            # not having a CBD file in old versions should abide by old sync rules
            return

        # This will be something like: //FGS/dev/FGS.cbdsst#8
        sstFileRev = "%s#%s" % (sstFileData[0]['depotFile'], sstFileData[0]['rev'])

        try:
            self.Log.debug ("Running: p4 print -q %s" % sstFileRev)
            sstFileContentsData = self.p4.run_print ('-q', sstFileRev)

        except P4.P4Exception:
            self.printRewrite("action: REJECT")
            self.printRewrite("message: ERROR: CBD Could not print stream spec template file [%s].  Sync not performed." % sstFileRev)
            sys.exit(1)

        # The p4.run_print() call returns an array, with sstFileContentsData[0]
        # containing metadata and sstFileContentsData[1] containing file contents.
        sstFileContents = sstFileContentsData[1]

        pathEntriesStarted = False

        # Extract Path 'import' and 'import+' lines and their revision specifiers.
        for line in sstFileContents.split('\n'):
            if (re.match ('Paths\:', line)):
                pathEntriesStarted = True
                continue
            if (pathEntriesStarted == False):
                continue

            line = line.strip()

            if (re.match ('share ', line)):
                # shortPath is just what follows the 'share' token in the
                # 'Paths:' field entry of a stream spec.
                # sharePath is the fully-qualified form of the shortPath following
                # the 'share' token in values in the 'Paths:' field of the stream
                # spec.  It is obtained by prefixing shortPath with the stream
                # name and '/'.  So for 'share src/...' in the //Jam/MAIN
                # stream, sharePath would be "//Jam/MAIN/src/...".
                # PH - This shortPath isn't used?
                # shortPath = re.sub ('^share ', '', line)
                sharePath = "%s/%s" % (self.stream, re.sub ('^share ', '', line))
                self.CBDSyncPathRules [sharePath] = vSpec
                self.Log.debug ("Added CBDSyncPathRules[%s%s]." % (sharePath, vSpec))

            if (re.match ('import ', line) or re.match ('import\+ ', line)):
                # If a revsion specifier was found on the import line, store it.
                revSpec = '#head'
                if (re.search('#', line)):
                    revSpec = line
                    revSpec = re.sub('^.*#', '#', line)
                if (re.search('@', line)):
                    revSpec = line
                    revSpec = re.sub('^.*@', '@', line)
                depotPath = re.sub ('^.*//', '//', line)
                depotPath = re.sub ('(#|@).*$', '', depotPath)
                self.CBDSyncPathRules [depotPath] = revSpec
                self.Log.debug ("Added CBDSyncPathRules[%s%s]." % (depotPath, revSpec))

    def loadSyncPathsFromKeys (self, reqWs, stream):
        """ loadSyncPathsFromKeys()

         This gets the default sync paths, those stored in 'keys'
         on the Peforce server, for the given stream.

         The sync paths are stored in the self.CBDSyncPathRules class
         variable, a dictionary with keys being the path and
         values being the version spec for that path.
        """
        self.Log.debug("CALL loadSyncPathsFromKeys(%s, %s)" % (reqWs, stream))
        shortStreamName = re.sub (r'^//', '', stream)
        shortStreamName = re.sub (r'/', '_', shortStreamName)

        pathKeySearchString = "cbd_stream_%s_path*" % shortStreamName
        vspecKeySearchString = "cbd_stream_%s_vspec*" % shortStreamName

        self.Log.debug ("Searching for path keys: %s" % pathKeySearchString)
        self.Log.debug ("Searching for vspec keys: %s" % vspecKeySearchString)

        # These p4.run_keys calls return arrays of keys, one returning an
        # array of path elements, and another an array of version
        # specifiers.

        # We assume that the number of values returned, which corresponds
        # to the number of lines of output of our 'p4 keys' command, is
        # is the same for both the path and vspec variants of our key
        # search command.  It's the job of the StreamSpecUpdate trigger
        # to ensure this assumption is safe.
        pathList = self.p4.run_keys ("-e", pathKeySearchString)
        vspecList = self.p4.run_keys ("-e", vspecKeySearchString)

        i = 0
        for p in pathList:
            # Add the version specifer for this path, which is in the
            # vspecList list with the same index as the path from
            # pathList.  Add is at a two-part tuple consisting of the
            # path and the vspec.
            self.Log.debug ("i=%s" % i)
            self.Log.debug ("p=%s" % p['value'])
            self.Log.debug ("v=%s" % vspecList[i]['value'])
            self.CBDSyncPathRules[p['value']] = vspecList[i]['value']
            self.Log.debug ("Added CBDSyncPathRules[%s%s]." % (p['value'], vspecList[i]['value']))
            i = i + 1

    def syncWs(self, reqWs, reqCmd, reqCwd, reqArgs):
        self.Log.info("Syncing workspace %s with arguments %s" % (reqWs, ",".join(reqArgs)))
        self.Workspace = reqWs
        self.CWD = re.sub (r'\\', r'/', reqCwd)

        if (re.match ('[a-zA-Z]{1}:', reqCwd)):
            self.Log.debug("Windows drive letter detected in CWD.  Normalizing to uppercase.")
            # Create a new string
            newCWD = self.CWD[0].upper()
            i=1
            while (i < len(self.CWD)):
                newCWD += self.CWD[i]
                i += 1
            self.CWD = newCWD

        
        try:
            stream = self.getWorkspaceField('Stream')
            
            if stream == None:
                self.Log.debug("Ignoring classic workspace %s (for now)." % reqWs)
                print ("action: PASS\n")
                return True

            self.WorkspaceRoot = self.getWorkspaceField('Root')
            self.WorkspaceRoot = re.sub (r'\\', r'/', self.WorkspaceRoot)

            if (re.match('[a-zA-Z]{1}:', self.WorkspaceRoot)):
                self.Log.debug("Windows drive letter detected in workspace root.  Normalizing to uppercase.")
                # Create a new string
                newRoot = self.WorkspaceRoot[0].upper()
                i=1
                while (i < len(self.WorkspaceRoot)):
                    newRoot += self.WorkspaceRoot[i]
                    i += 1
                self.WorkspaceRoot = newRoot

            self.Log.debug("Workspace Root: %s." % self.WorkspaceRoot)

            self.WorkspaceOptions = self.getWorkspaceField('Options')
            self.Log.debug("Workspace Options: %s." % self.WorkspaceOptions)

            self.WorkspaceOwner = self.getWorkspaceField('Owner')
            self.Log.debug("Workspace Owner: %s." % self.WorkspaceOwner)

            self.Log.debug("Syncing with command: p4 %s" % reqCmd)

            self.stream = stream
            self.streamDepot = re.sub ('//', '', stream)
            self.streamDepot = re.sub ('/.*$', '', self.streamDepot)

            dData = self.p4.fetch_depot (self.streamDepot)
            if (dData['StreamDepth']):
                self.streamDepth = re.sub ('^.*/', '', dData['StreamDepth'])
                self.streamDepth = int(self.streamDepth)
            else:
                self.streamDepth = 1

            flagArgs = []
            pathArgs = []
            self.globalVSpec = ''
            # default to global path unless found otherwise
            treatAsGlobalPath = 1

            for anArg in reqArgs:
                if anArg.startswith("-"):
                    flagArgs.append(anArg)
                    continue
                
                # "Global" version specifiers are those specified with no path, e.g.
                # 'p4 sync @5036'.  In a stock Perforce, that applies to the enitre
                # scope of the workspace.  In CBD, it determines which stream spec
                # template to use, which in turn drives which revisions of imported
                # files to sync.  If multiple global specifiers are specifed, only
                # last one has meaning.
                if (anArg.startswith("@") or anArg.startswith("#")):
                    if not self.globalVSpec == '':
                        self.Log.warn ("Multiple global version specifers defined. Ignoring earlier ones.")
                    self.globalVSpec = anArg
                    self.Log.debug ("Global version specifier set to: [%s]." % self.globalVSpec)
                    continue
                
                # For paths containing explicit revision specifiers, just apply the
                # given specifier.
                
                # For other paths, determine if the user-supplied path is within the
                # scope of a CBD-defined path (e.g. //fgs/main/src/foo.c if //fgs/main/...
                # is governed by CBD).  Then select and apply the appropriate CBD rule.
                
                # If a path arg is just "//...@vSpec", treat the version specifier as
                # global, just as if there were no path component.

                # Strip '#head', since P4V blindly applies it.
                pathArg = re.sub (r'#head$', '', anArg)

                self.Log.debug ("Normalizing pathArg to forward slashes.")
                pathArg = re.sub (r'\\', r'/', pathArg)

                # pathPart is strictly the path part of pathArg, excluding any revision specifiers.
                pathPart = re.sub ('(@|#).*', '', pathArg)

                revSpecPart = ''
                if r'@' in anArg:
                    revSpecPart = re.sub ('^.*@', '@', anArg)

                if r'#' in anArg:
                    revSpecPart = re.sub ('^.*#', '#', anArg)

                depotSyntaxPath = self.getPathInDepotSyntax (pathPart)
                depotSyntaxPathWithRevSpec = depotSyntaxPath + revSpecPart

                self.Log.debug ("Looking for ...(@|#) or ...$ in [%s]." % depotSyntaxPathWithRevSpec)
                if (re.search ('\.\.\.(@|#)', depotSyntaxPathWithRevSpec) or re.search ('\.\.\.$', depotSyntaxPathWithRevSpec)):
                    self.Log.debug ("Path contains '...[@|#] or ends with ...'.")

                    slashes = re.findall ('/', depotSyntaxPath)
                    slashCount = len(slashes)
                    self.Log.debug ("Slash count for [%s] is %d." % (depotSyntaxPath, slashCount))

                    if (slashCount > (3+self.streamDepth)):
                        self.Log.debug ("Deferring handling of narrow path arg [%s]." % pathArg)
                        pathArgs.append(pathArg)
                        # defer to not a globalpath
                        self.Log.debug ("Treating path arg [%s] as non-global." % pathArg)
                        treatAsGlobalPath = 0

                    else:
                        self.Log.debug ("Checking [%s] for global version specifiers." % pathArg)

                        if (slashCount == (1 + self.streamDepth)):
                            vSpec = "#head"
                            if (re.search (r'...@', pathArg)):
                                vSpec = re.sub('^.*\.\.\.@', '@', pathArg)
                            if (re.search (r'...#', pathArg)):
                                vSpec = re.sub('^.*\.\.\.#', '#', pathArg)
                            if not self.globalVSpec == '':
                                self.Log.warn ("Multiple global version specifers defined. Ignoring earlier ones.")
                            self.globalVSpec = vSpec
                            self.Log.debug ("Global version specifier set to: %s." % self.globalVSpec)

                        if (slashCount == (2 + self.streamDepth)):
                            lvl1 = re.sub ('//', '', pathArg)
                            lvl1 = re.sub ('/.*$', '', lvl1)
                            if (re.search (':', lvl1)):
                                self.Log.debug ("Windows Local OS Syntax detected.  Does pathArg [%s] contain Workspace Root [%s]?" % (pathArg, self.WorkspaceRoot))
                                if (re.match (self.WorkspaceRoot, pathArg, re.IGNORECASE)):
                                    self.Log.debug ("Sync from workspace root detected.")
                                    treatAsGlobalPath = 1
                                    vSpec = "#head"

                                    if (re.search (r'...@', pathArg)):
                                        vSpec = re.sub('^.*\.\.\.@', '@', pathArg)
                                    if (re.search (r'...#', pathArg)):
                                        vSpec = re.sub('^.*\.\.\.#', '#', pathArg)

                                    if not self.globalVSpec == '':
                                        self.Log.warn ("Multiple global version specifers defined. Ignoring earlier ones.")

                                    self.globalVSpec = vSpec
                                    self.Log.debug ("Global version specifier set to: %s." % self.globalVSpec)

                            else:
                                self.Log.debug ("Comparing lvl1=[%s] with stream depot name [%s] and workspace name [%s]." % (lvl1, self.streamDepot, self.Workspace))

                                if (lvl1 == self.streamDepot or lvl1 == self.Workspace):
                                    vSpec = "#head"

                                    if (re.search (r'...@', pathArg)):
                                        vSpec = re.sub('^.*\.\.\.@', '@', pathArg)
                                    if (re.search (r'...#', pathArg)):
                                        vSpec = re.sub('^.*\.\.\.#', '#', pathArg)

                                    if not self.globalVSpec == '':
                                        self.Log.warn ("Multiple global version specifers defined. Ignoring earlier ones.")

                                    self.globalVSpec = vSpec
                                    self.Log.debug ("Global version specifier set to: %s." % self.globalVSpec)
                                else:
                                    self.Log.debug ("Treating path %s of form '//<imported_depot>/...' as non-global path." % pathArg)
                                    pathArgs.append(pathArg)
                                    treatAsGlobalPath = 0

                        if (slashCount == (3 + self.streamDepth)):
                            if (re.search (r'...@', pathArg) or re.search(r'...#', pathArg)):
                                self.Log.debug ("Has vSpec.")
                                pathPart = re.sub ('\.\.\.@.*$', '', pathArg)
                                pathPart = re.sub ('\.\.\.#.*$', '', pathArg)
                                pathPart = re.sub ('\/$', '', pathPart)
                            else:
                                self.Log.debug ("No vSpec.")
                                pathPart = re.sub ('\.\.\.$', '', pathArg)
                                pathPart = re.sub ('\/$', '', pathPart)

                            self.Log.debug ("Comparing lvl1/lvl2=[%s] with stream name [%s]." % (depotSyntaxPath, self.stream))
                            if (re.match (self.stream, depotSyntaxPath)):
                                vSpec = "#head"
                                if (re.search (r'...@', pathArg)):
                                    vSpec = re.sub('^.*\.\.\.@', '@', pathArg)
                                if (re.search (r'...#', pathArg)):
                                    vSpec = re.sub('^.*\.\.\.#', '#', pathArg)
                                if not self.globalVSpec == '':
                                    self.Log.warn ("Multiple global version specifers defined. Ignoring earlier ones.")
                                self.globalVSpec = vSpec
                                self.Log.debug ("Global version specifier set to: %s." % self.globalVSpec)
                            else:
                                self.Log.debug ("Treating path arg [%s] as non-global. Deferring handling." % pathArg)
                                treatAsGlobalPath = 0
                                pathArgs.append(pathArg)

                else:
                    self.Log.debug ("Deferring handling of non-global path arg [%s]." % pathArg)
                    treatAsGlobalPath = 0
                    pathArgs.append(pathArg)

            # Populate the 'CBDSyncPathRules' dictionary with path/vspec pairs, either from keys or a file.
            if self.globalVSpec == '' or self.globalVSpec == '#head':
                self.loadSyncPathsFromKeys (reqWs, stream)
            else:
                self.loadSyncPathsFromFile (reqWs, stream, self.globalVSpec)

            if not self.CBDSyncPathRules:
                self.Log.debug ("No CBD Keys found. Passing user command thru.")
                self.printRewrite("action: PASS\n")
                return

            if (treatAsGlobalPath == 1):
                self.Log.debug ("Handling rewrite for global sync.")

                self.printRewrite("action: REWRITE")
                self.printRewrite("command: %s" % reqCmd)

                # Write the flag args.
                for anArg in flagArgs:
                    self.printRewrite("arg: %s" % anArg)

                notes = []
                notes.append("CBD %s %s" % (reqCmd, flagArgs))
                # Write that path/vspec args with CBD-defined vspecsif we have nay
                for k in self.CBDSyncPathRules.keys():
                    self.Log.debug("K=%s" % k)
                    self.printRewrite("arg: %s%s" % (k, self.CBDSyncPathRules[k]))
                    notes.append("%s%s" % (k, self.CBDSyncPathRules[k]))

                if (len (notes) > 0):
                    print ("message: \"INFO: %s.\"" % notes)
                    self.Log.debug ("User Message: INFO: %s" % notes)

            if (treatAsGlobalPath == 0 and len(pathArgs) > 0):
                self.Log.debug("Processing user-supplied path args: %s" % pathArgs)

                self.printRewrite("action: REWRITE")
                self.printRewrite("command: %s" % reqCmd)

                for anArg in flagArgs:
                    self.printRewrite("arg: %s" % anArg)

                for pathArg in pathArgs:
                    """
                    For each path in the user-supplied list of paths, first check for
                    an explicit version specifier provided by the user.  If one is
                    found, use it, and provide a warning that CBD version specifiers
                    are ignored for that path.

                    If no explicit version specifier is provided by the user, determine
                    the corresponding CBD version specifier (if any).

                    Lastly, REWRITE each path as a separate 'p4 sync' command.

                    """

                    notes = []
                    notes.append("CBD %s %s" % (reqCmd, flagArgs))

                    # For path arguments for which the user supplied an explicit revision specifer,
                    # just use it.
                    self.Log.debug("Checking [%s] for an explicit revision specifier." % pathArg)
                    if (re.search ('@|#', pathArg)):
                        self.Log.debug ("Ignoring CBD rules in favor of explicit revision specifier for path [%s]." % pathArg)
                        self.printRewrite("arg: %s" % pathArg)
                        notes.append(pathArg)

                    else:
                        # In this block, we can safely assume pathArg does not contain a revision specifier.

                        self.Log.debug ("Seeking CBD rule for pathArg [%s]." % depotSyntaxPath)
                        # pathPart is strictly the path part of pathArg, excluding any revision specifiers.
                        depotSyntaxPath = self.getPathInDepotSyntax (pathArg)

                        # Strip the leading '//' for easier comparison.
                        depotPath = re.sub (r'^//', '', depotSyntaxPath)
                        pathFound = 0

                        self.Log.debug ("Seeking depot path [%s] in CBDSyncPathRules.keys dictionary." % depotPath)
                        for k in self.CBDSyncPathRules.keys():
                            try:
                                kcheck = k.rstrip(r'/...') + '/'
                                kcheck = re.sub (r'^//', '', kcheck)
                                self.Log.debug ("if (%s) starts with (%s) ..." % (depotPath, kcheck))
                                if (depotPath.startswith(kcheck)):
                                    self.printRewrite("arg: %s%s" % (pathArg, self.CBDSyncPathRules[k]))
                                    notes.append("%s%s" % (pathArg, self.CBDSyncPathRules[k]))
                                    pathFound = 1
                                    break
                            except:
                                self.Log.error ("Exception checking applicability of CBD rule [%s] to path [%s]." % (self.CBDSyncPathRules[k]), pathArg)

                        if (pathFound == 0):
                            self.Log.debug ("No explicit vspec or CBD rule found.  Passing thru.")
                            self.printRewrite("arg: %s" % pathArg)
                            notes.append("Path [%s] is out of CBD scope.  Assuming #head." % pathArg)

                    if (len (notes) > 0):
                        print ("message: \"INFO: %s.\"" % notes)
                        self.Log.debug ("User Message: INFO: %s" % notes)

            self.Log.debug ("Path processing complete.")

        except P4.P4Exception:
            errors = []
            for e in self.p4.errors:
                self.Log.error("Errors: " + e)
                errors.append(e)
            for w in self.p4.warnings:
                self.Log.warn("Warnings: " + w)
                errors.append(w)
            print ("action: REJECT\nmessage: \"CBD Error syncing workspace: %s\"" % ",".join(errors))
            return False

        except Exception as e:
            self.Log.debug("Passing thru due to exception: %s.\n" % e)
            print ("action: PASS\n")
# Change User Description Committed
#1 21633 C. Thomas Tyler Populate -o //guest/perforce_software/cbd/main/...
//cbd/main/....
//guest/perforce_software/cbd/main/scripts/CbdDev.py
#16 19429 C. Thomas Tyler Released CBD/MultiArch/2016.2/19425 (2016/05/17).
#15 19355 C. Thomas Tyler Released CBD/MultiArch/2016.2/19352 (2016/05/10).
Copy Up using 'p4 copy -r -b perforce_software-cbd-dev'.

No funtional changes to CBD behaviors.
#14 19298 C. Thomas Tyler Released CBD/MultiArch/2016.1/19295 (2016/05/09).
#13 19292 C. Thomas Tyler Copy Up from dev to main.

Released CBD/MultiArch/2016.1/19285 (2016/05/07).

* Added 'Version' file and Release Notes.
* Added test suite documentation.
* Tweaks to CBD logging, back to using append mode.
#12 16706 C. Thomas Tyler Tweaked to remove mode paramter.
#11 16705 C. Thomas Tyler Set to use SDP standard Python/P4Python.
#10 16702 C. Thomas Tyler Configured to ensure python3 is used.
#9 16353 C. Thomas Tyler Support for 'import+' was partially broken.
 Fixed!
#8 15631 C. Thomas Tyler Fix for users getting 'ERROR: CBD was not able to translate path ' errors.

This occured with sync commands from done specify the local OS
syntax path from the root of a workspace associated with a 'complex'
stream spec.

Copy Up to main from dev.
#7 15360 C. Thomas Tyler Copy Up using 'p4 copy -r -b perforce_software-cbd-dev'.

Added new test for job000330, which initially failed (a valid repro).
Then fixed it.
#6 15282 C. Thomas Tyler Copy Up using 'p4 copy -r -b perforce_software-cbd-dev'.
#5 15273 C. Thomas Tyler Copy Up using 'p4 copy -r -b perforce_software-cbd-ntx64'.
Stabilization changes.
Test suite enhancements.
#4 15179 C. Thomas Tyler Copy Up to main from dev for CBD to pick up bug fixes, using:
p4 copy -r -b perforce_software-cbd-dev
#3 15158 C. Thomas Tyler Copy Up from dev to main for CBD, using:
p4 copy -r -b perforce_software-cbd-dev
#2 15022 C. Thomas Tyler Copy-Up to main from ntx64:
* Fixed Cbd.py issue with an exception handling bug for Host field.
* Rebased CbdDev.py to Cbd.py
* Simplified test operations.
* Fixed a cosmetic typo in test doc/data.
#1 15009 C. Thomas Tyler Promoted CBD development work to main from dev.
//guest/perforce_software/cbd/dev/scripts/CbdDev.py
#2 14839 C. Thomas Tyler Updated to latest CBD from dev box.
#1 14198 C. Thomas Tyler Got Workshop up to date with latest version of CBD developed
elsewhere.

Added CbdDev.py to illustrate enabling testing a newer version
on a live server, as a supplement to the test suite.  This comes
with supporting scripts wssync.dev.(sh,py)

Added cmd_trig_by_auth.pl, a technology sample script.  Not used
presently.