From 06b0cbe426ae2fc7b3dfc1ecef8c9dd347da4125 Mon Sep 17 00:00:00 2001 From: "U-ILLFONIC\\bernst" Date: Wed, 13 Aug 2014 17:09:19 -0600 Subject: [PATCH] Adding huge improvements. There are still a few more to make to account for computers not setup correctly, but it's functional. Still has the occasional console hang bug. Now also prints out run time. There is one new minor bug, reverting back to the previously set client view. --- p4RemoveUnversioned.py | 164 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 18 deletions(-) diff --git a/p4RemoveUnversioned.py b/p4RemoveUnversioned.py index bb8805c..28bae51 100644 --- a/p4RemoveUnversioned.py +++ b/p4RemoveUnversioned.py @@ -11,7 +11,7 @@ # todo: buffer output, after exceeding a certain amount print to the output. # todo: allow logging output besides console output, or redirection altogether -import datetime, inspect, multiprocessing, optparse, os, re, stat, subprocess, sys, threading, traceback +import datetime, inspect, marshal, multiprocessing, optparse, os, re, stat, subprocess, sys, threading, time, traceback # trying ntpath, need to test on linux import ntpath @@ -80,6 +80,13 @@ def match_in_ignore_list( path, ignore_list ): def call_process( args ): return subprocess.call( args.split( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE ) +def try_call_process( args, path=None ): + try: + subprocess.check_output( args.split( ), shell=False, cwd=path ) + return 0 + except subprocess.CalledProcessError: + return 1 + use_bytearray_str_conversion = type( b"str" ) is not str def get_str_from_process_stdout( line ): if use_bytearray_str_conversion: @@ -90,6 +97,33 @@ def get_str_from_process_stdout( line ): def singular_pulural( val, singular, plural ): return singular if val == 1 else plural +def parse_info_from_command( args, value, path = None ): + """ + + :rtype : string + """ + proc = subprocess.Popen( args.split( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path ) + for line in proc.stdout: + line = get_str_from_process_stdout( line ) + + if not line.startswith( value ): + continue + return line[ len( value ) : ].strip( ) + return None + +def get_p4_py_results( args, path = None ): + results = [] + proc = subprocess.Popen( [ 'p4', '-G' ] + args.split( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path ) + try: + while True: + output = marshal.load( proc.stdout ) + results.append( output ) + except EOFError: + pass + finally: + proc.stdout.close() + return results + # Keep these in mind if you have issues: # https://stackoverflow.com/questions/16557908/getting-output-of-a-process-at-runtime # https://stackoverflow.com/questions/4417546/constantly-print-subprocess-output-while-process-is-running @@ -115,10 +149,33 @@ def get_client_set( path ): files.add( local_path ) - # TODO: check error to see if the path is not in the client view. Prompt anyway? + proc.wait( ) + + for line in proc.stderr: + raise Exception(line) return files +def get_client_root( ): + """ + + :rtype : string + """ + command = "p4 info" + + proc = subprocess.Popen( command.split( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE ) + for line in proc.stdout: + line = get_str_from_process_stdout( line ) + + clientFile_tag = "Client root: " + if not line.startswith( clientFile_tag ): + continue + + local_path = normpath( line[ len( clientFile_tag ) : ].strip( ) ) + + return local_path + return None + class PTable( list ): def __init__( self, *args ): list.__init__( self, args ) @@ -212,18 +269,18 @@ class Console( threading.Thread ): self.queue.task_done( ) -class Task( threading.Event ): - def __init__( data, cmd = None ): - threading.Event.__init__( self ) +# class Task( threading.Event ): + # def __init__( data, cmd = None ): + # threading.Event.__init__( self ) - self.cmd = cmd if cmd is None MSG.RUN_FUNCTION - self.data = data + # self.cmd = cmd if cmd is None MSG.RUN_FUNCTION + # self.data = data - def isDone( self ): - return self.isSet() + # def isDone( self ): + # return self.isSet() - def join( self ): - self.wait( ) + # def join( self ): + # self.wait( ) class Worker( threading.Thread ): def __init__( self, console, queue, files_to_ignore ): @@ -257,6 +314,8 @@ class Worker( threading.Thread ): self.queue.task_done( ) def main( args ): + start = time.clock() + # check requirements if call_process( 'p4 -V' ) != 0: print( 'Perforce Command-line Client(p4) is required for this script.' ) @@ -269,11 +328,60 @@ def main( args ): parser.add_option( "-t", "--threads", dest="thread_count", help="Number of threads to crawl your drive and poll p4.", default=100 ) parser.add_option( "-q", "--quiet", action="store_true", dest="quiet", help="This overrides verbose", default=False ) parser.add_option( "-v", "--verbose", action="store_true", dest="verbose", default=True ) + parser.add_option( "-i", "--interactive", action="store_true", dest="interactive", default=False ) - ( options, args ) = parser.parse_args( ) + ( options, args ) = parser.parse_args( args ) directory = normpath( options.directory if options.directory is not None else os.getcwd( ) ) + # get user + print("\nChecking p4 info...") + result = get_p4_py_results('info') + if len(result) == 0 or b'userName' not in result[0].keys(): + print("Can't find perforce info, is it even setup?") + sys.exit(1) + username = get_str_from_process_stdout(result[0][b'userName']) + client_host = get_str_from_process_stdout(result[0][b'clientHost']) + print("|Done.") + + client_root = get_client_root() + ldirectory = directory.lower() + workspace_name = None + if not ldirectory.startswith(client_root.lower()): + print("\nCurrent directory not in client view, checking other workspaces for user '" + username + "' ...") + + workspace_name = parse_info_from_command('p4 info', 'Client name: ') + + # get user workspaces + result = get_p4_py_results('workspaces -u ' + username) + workspaces = [] + for r in result: + whost = get_str_from_process_stdout(r[b'Host']) + if whost is not None and len(whost) != 0 and client_host != whost: + continue + workspace = {'root': get_str_from_process_stdout(r[b'Root']), 'name': get_str_from_process_stdout(r[b'client'])} + workspaces.append(workspace) + + del result + + # check current directory against current workspace, see if it matches existing workspaces. + for w in workspaces: + wname = w['name'] + wlower = w['root'].lower() + if ldirectory.startswith(wlower): + # set current directory, don't forget to revert it back to the existing one + print("|Setting client view to: " + wname) + + if try_call_process( 'p4 set P4CLIENT=' + wname ): + print("|There was a problem trying to set the p4 client view (workspace).") + sys.exit(1) + break + else: + print( "|Couldn't find a workspace root that matches the current directory for the current user." ) + sys.exit(1) + print("|Done.") + + # Files are added from .p4ignore # Key is the file root, the value is the table of file regexes for that directory. files_to_ignore = PDict() @@ -285,11 +393,13 @@ def main( args ): with Console( auto_flush_num=20, auto_flush_time=1000 ) as c: if not options.quiet: - c.writeflush( "Caching files in depot, this may take a little while..." ) + c.writeflush( "\nCaching files in depot, this may take a little while..." ) # TODO: push this off to a thread and walk the directory so we get a headstart. files_in_depot = get_client_set( directory ) + c.writeflush( "|Done." ) + # TODO: push a os.walk request off to a thread to build a list of files in the directory; create batch based on directory? # TODO: at this point join on both tasks to wait until they're done @@ -299,7 +409,7 @@ def main( args ): # will at least need to redo how the ignore lists are handled for efficiencies sake. if not options.quiet: - c.writeflush( "Checking " + directory) + c.writeflush( "\nChecking " + directory) for root, dirs, files in os.walk( directory ): ignore_list = get_ignore_list( root, files_to_ignore ) @@ -346,21 +456,35 @@ def main( args ): if match_in_ignore_list( path, ignore_list ): # add option of using send2trash if not options.quiet: - c.write( "| ignoring " + d ) + c.write( "| ignoring " + path ) dirs.remove( d ) try: os.rmdir(path) remove_dir_count += 1 if not options.quiet: - c.write( "| " + d + " was removed." ) + c.write( "| " + path + " was removed." ) except OSError: # Fails on non-empty directory pass if not options.quiet: c.write( "|Done." ) + # This needs to happen automatically even when an exception happens, when we leave scope. + if workspace_name is not None: + c.write("\nReverting back to original client view...") + # set workspace back to the original one + if try_call_process( 'p4 set P4CLIENT=' + workspace_name ): + error_count += 1 + if not options.quiet: + c.write("|There was a problem trying to restore the set p4 client view (workspace).") + else: + if not options.quiet: + c.write("|Reverted client view back to '" + workspace_name + "'.") + if not options.quiet: + c.write("|Done.") + if not options.quiet: - output = "\nRemoved " + str( remove_file_count ) + singular_pulural( remove_file_count, " file, ", " files, " ) + output = "\n[ Removed " + str( remove_file_count ) + singular_pulural( remove_file_count, " file, ", " files, " ) output += str( remove_dir_count ) + singular_pulural( remove_dir_count, " directory", " directories") if warning_count > 0: @@ -368,7 +492,11 @@ def main( args ): if error_count > 0: output += " w/ " + str( error_count ) + singular_pulural( error_count, " error", " errors" ) - c.write( output + "." ) + end = time.clock() + delta = end - start + output += ", and finished in " + str(delta) + "s" + + c.write( output + " ]" ) if __name__ == "__main__": try: