#! /usr/bin/env python # # Copyright 2011 - 2019 by Ben Key # # 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 AUTHOR ``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 THE AUTHOR 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. # import getopt import locale import os import stat import sys import unicodedata class Command(object): """Run a command and capture it's output string, error string and exit status""" def __init__(self, command): self.command = command def run(self, Shell=True): import subprocess as sp process = sp.Popen(self.command, shell=Shell, stdout=sp.PIPE, stderr=sp.PIPE) self.pid = process.pid self.output, self.error = process.communicate() self.returncode = process.returncode return self class Singleton(object): """A Python implementation of the Singleton Design Patern obtained from the "python singleton and singleton borg" article found at http://snippets.dzone.com/posts/show/651.""" def __new__(cls, *p, **k): cls._do_initialization = False if not '_the_instance' in cls.__dict__: cls._do_initialization = True cls._the_instance = object.__new__(cls) return cls._the_instance class GetCurrentCodepage(Singleton): """Obtains the active code page. This is implemented as a singleton callable object because this information is needed in several places and the work to obtain the information only needs to be done once.""" def __init__(self): """Constructor of the GetCurrentCodepage class. Uses the GetConsoleOutputCP function to obtain the current codepage on Microsoft Windows. An implementation that will work on other operating systems is pending.""" Singleton(self) if self._do_initialization: self.__codepage = "" if os.name.lower() == "nt": try: import ctypes import ctypes.wintypes _GetConsoleOutputCP = ctypes.windll.kernel32.GetConsoleOutputCP _GetConsoleOutputCP.restype = ctypes.wintypes.UINT self.__codepage = str(_GetConsoleOutputCP()) except ImportError: pass def __call__(self): """The __call__ object method that makes the GetCurrentCodepage class a callable object.""" return self.__codepage class CommandLineArgumentsParser: """A class that parses the command line arguments that are passed to this script.""" def __init__(self, argv = sys.argv): """Constructor of the CommandLineArgumentsParser class.""" self.__display_usage = False self.__display_version = False self.__list_only = False self.__show_have_files_list = False self.__client = None self.__argv = argv self.__args = [] def parse_command_line(self): """Parses the command line options.""" parse_opts = True if len(self.__argv) > 1: arg1 = self.__argv[1] arg1 = arg1.lower() if ( ("/?" == arg1) or ("/h" == arg1) or ("/help" == arg1) ): self.__display_usage = True return 0 try: opts, args = getopt.getopt( self.__argv[1:], "hH?vVlLc:C:", ["help", "HELP", "version", "VERSION", "list", "LIST", "client=", "CLIENT=", "have", "HAVE"]) self.__args = args except getopt.GetoptError as err: print (err.msg) parse_opts = False self.__display_usage = True if parse_opts: for o, a in opts: if o in ("-h", "-H", "-?", "--help", "--HELP"): self.__display_usage = True elif o in ("-v", "-V", "--version", "--VERSION"): self.__display_version = True elif o in ("-l", "-L", "--list", "--LIST"): self.__list_only = True elif o in ("--have", "--HAVE"): self.__show_have_files_list = True elif o in ("-c", "-C", "--client", "--CLIENT"): self.__client = a def should_display_usage(self): """Return the current value of the display_usage member variable.""" return self.__display_usage def should_display_version(self): """Return the current value of the display_version member variable.""" return self.__display_version def should_list_only(self): """Return the current value of the list_only member variable.""" return self.__list_only def should_show_have_files_list(self): """Return the current value of the show_have_files_list member variable. Useful for debugging changes to the GetPerforceHaveFilesList function.""" return self.__show_have_files_list def get_client(self): """Return the current value of the client member variable.""" return self.__client def get_workspace(self): """Return the current value of the client member variable.""" return self.__client def get_args(self): """Return the current value of the args member variable.""" return self.__args def InitLocale(): loc = locale.setlocale(locale.LC_ALL, '') if len(loc) != 0 and os.name.lower() == "nt": pos = loc.rfind('.') if pos != -1: cp = int(loc[pos + 1:]) try: import ctypes import ctypes.wintypes _SetConsoleOutputCP = ctypes.windll.kernel32.SetConsoleOutputCP _SetConsoleOutputCP.argtypes = [ctypes.wintypes.UINT ] _SetConsoleOutputCP.restype = ctypes.wintypes.BOOL _SetConsoleOutputCP(cp) except ImportError: pass return loc def _find(name, matchFunc=os.path.isfile): path = os.environ['PATH'].split(os.pathsep) path.extend(sys.path) for dirname in path: candidate = os.path.join(dirname, name) if matchFunc(candidate): return candidate return None def find(name): return _find(name) def findDir(name): return _find(name, matchFunc=os.path.isdir) def findP4(): p4name = "p4" if "nt" == os.name.lower(): p4name = "p4.exe" if (None != find(p4name)): return True return False def normalize_caseless(text): return unicodedata.normalize("NFKD", text.casefold()) def caseless_equal(left, right): return normalize_caseless(left) == normalize_caseless(right) def GetPerforceWorkspaceRoot(workspaceName): """Obtains the root directory of the Perforce client that is currently in use.""" ret = None commandString = "p4 client -o" if (workspaceName != None): commandString = "p4 client -o %s" % (workspaceName, ) command = Command(commandString).run() if 0 == command.returncode: codePage = GetCurrentCodepage()() rootStr = "Root:" foundCount = 0 lines = command.output.splitlines() for line in lines: lineString = line.decode(codePage) pos = lineString.find(rootStr) if -1 != pos: foundCount += 1 if 2 == foundCount: ret = lineString[pos + len(rootStr):].strip() break return ret def GetPerforceHaveFilesList(workspaceName, workspaceRoot): """Returns a list of all files in your Perforce workspace using the p4 have command. Also returns a set of all directories in your Perforce workspace. All items inserted into the returned list and set are converted to lower case.""" haveFilesList = [] perforceDirSet = set() commandString = "p4 have" if (workspaceName != None): commandString = "p4 -c %s have" % (workspaceName, ) command = Command(commandString).run() if 0 == command.returncode: lines = command.output.splitlines() codePage = GetCurrentCodepage()() for line in lines: lineString = line.decode(codePage) pos = lineString.rfind(workspaceRoot) fileName = lineString[pos:] haveFilesList.append(fileName.lower()) dir = os.path.dirname(fileName) perforceDirSet.add(dir.lower()) if (workspaceName != None): auxWorkspaceName = "%s-AUX" % (workspaceName,) auxWorkspaceRoot = GetPerforceWorkspaceRoot(auxWorkspaceName) if not auxWorkspaceRoot is None: if caseless_equal(auxWorkspaceRoot, workspaceRoot): auxCommandString = "p4 -c %s have" % (auxWorkspaceName, ) auxCommand = Command(auxCommandString).run() if 0 == auxCommand.returncode: lines = auxCommand.output.splitlines() codePage = GetCurrentCodepage()() for line in lines: lineString = line.decode(codePage) pos = lineString.rfind(auxWorkspaceRoot) fileName = lineString[pos:] haveFilesList.append(fileName.lower()) dir = os.path.dirname(fileName) perforceDirSet.add(dir.lower()) return haveFilesList, perforceDirSet def FindP4CleanIgnoreFile(dir): """Locate the .p4cleanIgnore file in the specified directory or in an ancestor directory.""" if dir == None or len(dir) == 0: return None ret = os.path.join(dir, ".p4cleanIgnore") if os.path.exists(ret): return ret parentDir = os.path.abspath(os.path.join(dir, os.pardir)) if parentDir == dir: return None ret = FindP4CleanIgnoreFile(parentDir) return ret def LoadIgnoreList(dirOrFile): """Loads the ignore list from self.__p4cleanIgnore.""" ret = set() ret.add(".p4cleanIgnore") ret.add("devenvw.ini") ret.add("p4-set.bat") ret.add("P4CONFIG") p4config = os.getenv("P4CONFIG") if None != p4config: ret.add(p4config.lower()) if dirOrFile == None or len(dirOrFile) == 0: return ret if not os.path.exists(dirOrFile): return ret p4cleanIgnore = None if os.path.isdir(dirOrFile): p4cleanIgnore = FindP4CleanIgnoreFile(dirOrFile) else: p4cleanIgnore = dirOrFile if p4cleanIgnore == None: return ret if not os.path.exists(p4cleanIgnore): return ret list = [line.strip() for line in open(p4cleanIgnore, "r")] ret.update(list) return ret def IsFileInIgnorelist(fileName, ignoreList): """Determines whether or not the specified file is included in the ignore list.""" if ignoreList == None: return False ret = False fileNameLower = fileName.lower() if fileName in ignoreList: ret = True elif fileNameLower in ignoreList: ret = True return ret