#! /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 from enum import Enum # Add script directory to sys.path. # This is complicated due to the fact that __file__ is not always defined. # # Note: This must be done before importing p4clean_utils. def GetScriptFile(): """Obtains the full path and file name of the Python script.""" if (hasattr(GetScriptFile, "file")): return GetScriptFile.file ret = "" try: # The easy way. Just use __file__. # Unfortunately, __file__ is not available when cx_freeze is used or in IDLE. ret = os.path.realpath(__file__) except NameError: # The hard way. if (len(sys.argv) > 0 and len(sys.argv[0]) > 0 and os.path.isabs(sys.argv[0])): ret = os.path.realpath(sys.argv[0]) else: ret = os.path.realpath(inspect.getfile(GetScriptFile)) if (not os.path.exists(ret)): # If cx_freeze is used the value of the ret variable at this point is in # the following format: {PathToExeFile}\{NameOfPythonSourceFile}. This # makes it necessary to strip off the file name to get the correct path. ret = os.path.dirname(ret) GetScriptFile.file = ret return GetScriptFile.file def GetScriptDirectory(): """Obtains the path to the directory containing the script.""" if (hasattr(GetScriptDirectory, "dir")): return GetScriptDirectory.dir module_path = GetScriptFile() GetScriptDirectory.dir = os.path.dirname(module_path) return GetScriptDirectory.dir sys.path.insert(0, GetScriptDirectory()) import p4clean_utils __version__ = 1.13 __doc__ = """ p4clean version %(version)s, Copyright 2011 - 2019 by Ben Key. p4clean is a simple utility designed to remove all files from a Perforce workspace directory that are not included in the Perforce depot. The directory that is to be cleaned is obtained using the p4 client command. The directory is obtained from the Root entry in the output provided by running "p4 client -o." Usage: p4clean.py [options] Options: --version, --VERSION, -v, -V Print version information and exit successfully. --help, --HELP, -h, -H, -? Print this help message and exit successfully. --list, --LIST, -l, -L List the files only, do not delete them. --have, --HAVE Show the have files list, useful for debugging changes to the GetPerforceHaveFilesList function. --client, --CLIENT, -c, -C {client_name} The name of the Perforce client that you wish to clean. This parameter is useful if you use multiple Perforce clients on one system. """ __version_info__ = """ p4clean version %(version)s, Copyright 2011 - 2019 by Ben Key. p4clean is a simple utility designed to remove all files from a Perforce client area that are not included in the Perforce depot. p4clean comes with ABSOLUTELY NO WARRANTY. This program is free software; your freedom to use, change and distribute this program is covered by the 2-Clause BSD License. """ class ExitCodes(Enum): success = 0 p4_error = 1 keyboard_interrupt = 2 unknown_error = 3 def usage(): """Prints the usage information for p4clean.""" print(__doc__ % {'version' : __version__}) def version(): """Prints the version information for p4clean.""" print(__version_info__ % {'version' : __version__}) class PerforceWorkspaceCleaner: """A class the removes all files from a Perforce client area that are not included in the Perforce depot.""" def __init__(self, workspaceName): self.__foundP4 = p4clean_utils.findP4() self.__workspaceName = workspaceName self.__workspaceRoot = None self.__haveFilesList = [] self.__perforceDirSet = set() def __IsDirectoryInSet(self, dir, dirSet): """Determines whether or not the specified directory is included in the set.""" dirInSet = True dirLower = dir.lower() if not dirLower in dirSet: dirInSet = False for item in dirSet: if 0 == item.find(dirLower): dirInSet = True break return dirInSet def __IsFileInHaveFileList(self, fileName, haveFileList): """Determines whether or not the specified file is included in the have file list.""" ret = True fileNameLower = fileName.lower() if not fileNameLower in haveFileList: ret = False return ret def __CleanDirectory(self, dir, ignoreList, listOnly): """Iterates through the specified directory recursively finding all files that are not included in the list of Perforce client files obtained using the GetHaveFilesList callable object that are not found in the ignoreList list. Returns a list of the files that were found. If listOnly is false, the files that are not included in the Perforce client files list that are not found in the ignoreList list will be deleted as well.""" filesNotInDepot = [] dirInPerforce = self.__IsDirectoryInSet(dir, self.__perforceDirSet) if False == dirInPerforce: print("Directory '%s' not in Perforce client." % (dir, )) for item in os.listdir(dir): if p4clean_utils.IsFileInIgnorelist(item, ignoreList): continue fileName = os.path.join(dir, item) if os.path.isfile(fileName): if os.access(fileName, os.W_OK): if False == dirInPerforce or False == self.__IsFileInHaveFileList(fileName, self.__haveFilesList): filesNotInDepot.append(fileName) if listOnly: print("Found non depot file: %s" % (fileName, )) else: print("Removing file: %s" % (fileName, )) try: os.chmod(fileName, stat.S_IWRITE) os.remove(fileName) except PermissionError: pass else: filesNotInDepot.extend(self.__CleanDirectory(fileName, ignoreList, listOnly)) if False == dirInPerforce and False == listOnly: print("Removing directory: %s" % (dir, )) try: os.rmdir(dir) except OSError: pass return filesNotInDepot def Clean(self, workspace, listOnly): """This function does the work of obtaining the Perforce client directory and calling CleanDirectory to recursively clean that directory.""" if (False == self.__foundP4): print("p4 not found.") return [] self.__workspaceRoot = p4clean_utils.GetPerforceWorkspaceRoot(self.__workspaceName) if None == self.__workspaceRoot: print("Failed to get the Perforce client root.") return [] ignoreList = p4clean_utils.LoadIgnoreList(self.__workspaceRoot) self.__haveFilesList, self.__perforceDirSet = p4clean_utils.GetPerforceHaveFilesList( self.__workspaceName, self.__workspaceRoot) if (0 == len(self.__haveFilesList) or 0 == len(self.__perforceDirSet)): print("Failed to get the Perforce Have Files List.") return [] filesNotInDepot = self.__CleanDirectory(self.__workspaceRoot, ignoreList, listOnly) return filesNotInDepot def ShowHaveFilesList(workspaceName): """Show the Have Files List in response to the --have command line argument.""" try: workspaceRoot = p4clean_utils.GetPerforceWorkspaceRoot(workspaceName) if workspaceRoot is None: print("Failed to get the Perforce client root.") return ExitCodes.p4_error haveFilesList, perforceDirSet = p4clean_utils.GetPerforceHaveFilesList( workspaceName, workspaceRoot) if (0 == len(haveFilesList) or 0 == len(perforceDirSet)): print("Failed to get the Perforce Have Files List.") return ExitCodes.p4_error for item in haveFilesList: print(item) return ExitCodes.success except KeyboardInterrupt: return ExitCodes.keyboard_interrupt def CleanWorkspace(workspaceName, listOnly): """Clean the specified workspace.""" workspaceCleaner = PerforceWorkspaceCleaner(workspaceName) try: filesNotInDepot = workspaceCleaner.Clean(workspaceName, listOnly) if (0 == len(filesNotInDepot)): return ExitCodes.p4_error return ExitCodes.success except KeyboardInterrupt: return ExitCodes.keyboard_interrupt def main(argv = sys.argv): """The main entry point for p4clean.""" p4clean_utils.InitLocale() parser = p4clean_utils.CommandLineArgumentsParser(argv) parser.parse_command_line() if parser.should_display_usage(): usage() sys.exit(ExitCodes.success) elif parser.should_display_version(): version() sys.exit(ExitCodes.success) workspaceName = parser.get_workspace() listOnly = parser.should_list_only() showHaveFilesList = parser.should_show_have_files_list() ret = ExitCodes.unknown_error if showHaveFilesList: ret = ShowHaveFilesList(workspaceName) else: ret = CleanWorkspace(workspaceName, listOnly) sys.exit(ret) if __name__ == "__main__": sys.exit(main(sys.argv))