# # File:: %RS_TOOLSLIB%/util/p4_integrate.rb # Description:: Perforce integration script for integrating changes between # two Perforce servers. # # Author:: David Muir # Date:: 5 November 2009 # #---------------------------------------------------------------------------- # Uses #---------------------------------------------------------------------------- require 'pipeline/config/projects' require 'pipeline/gui/exception_dialog' require 'pipeline/os/getopt' require 'pipeline/scm/perforce' require 'pipeline/scm/perforce_helper' include Pipeline #---------------------------------------------------------------------------- # Constants #---------------------------------------------------------------------------- OPTIONS = [ [ '--source_port', '-p', OS::Getopt::REQUIRED, 'source Perforce server (and port).' ], [ '--source_client', '-c', OS::Getopt::REQUIRED, 'source client/workspace name.' ], [ '--source_user', '-u', OS::Getopt::REQUIRED, 'source username.' ], [ '--source_path', '-s', OS::Getopt::REQUIRED, 'source path mapping.' ], [ '--ignore', '-i', OS::Getopt::REQUIRED, 'source paths to ignore (depotFile P4 wildcards).' ], [ '--filter', '-f', OS::Getopt::REQUIRED, 'source paths to filter out (clientFile wildcards).' ], [ '--target_port', '-p', OS::Getopt::REQUIRED, 'target Perforce server (and port).' ], [ '--target_client', '-c', OS::Getopt::REQUIRED, 'target client/workspace name.' ], [ '--target_user', '-v', OS::Getopt::REQUIRED, 'target username.' ], [ '--target_path', '-t', OS::Getopt::REQUIRED, 'target path mapping.' ], [ '--autosubmit', '-a', OS::Getopt::BOOLEAN, 'automatically submit changelist on target.' ], [ '--autorevertunchanged', '-r', OS::Getopt::BOOLEAN, 'automatically revert unchanged files on target.' ], [ '--each_changelist', '-r', OS::Getopt::BOOLEAN, 'when integrating a folder or file sync and integrate each changelist one at a time (by using this it retains changelist descriptions)' ], [ '--same_port', '-sp', OS::Getopt::BOOLEAN, 'ignores validation which errors on integrating on same port' ], [ '--help', '-h', OS::Getopt::BOOLEAN, 'display usage information.' ], ] TRAILING = { 'changelist/file' => 'Perforce changelist to integrate.' } #---------------------------------------------------------------------------- # Functions #---------------------------------------------------------------------------- # Convert source path String to destination path String. def path_src_to_dst( source, target, filename ) src = source[:path].sub( '...', '' ) dst = target[:path].sub( '...', '' ) filename.sub( src, dst ) end # Determine whether we should ignore this source file? # source is a Hash of source Perforce options. # filename is the fstat depotFile String. # Return true if we want to ignore the file; otherwise false. def path_src_to_ignore?( source, filename ) norm_filename = OS::Path::normalise( filename ) return false if source[:ignore] == nil source[:ignore].each do |path| src = path.sub( '...', '' ) return true if ( norm_filename.starts_with( src ) ) end false end # Determine whether we should filter this source file? # source is a Hash of source Perforce options. # filename is the fstat clientFile String. # Return true if we want to ignore the file; otherwise false. def path_src_to_filter?( source, filename ) return false if source[:filter] == nil norm_filename = OS::Path::normalise( filename ) source[:filter].each do |path| return true if ( File::fnmatch?( path, filename ) ) end false end # Create a Perforce changelist. New changelist # returned. def create_changelist( source, target, cl = nil, desc_suffix = nil ) if ( cl.is_a?( Hash ) ) then # Create a changelist for our automatic integration. description = cl['desc'] description += "\n\nAutomatic Integration of Changelist #{cl['change']} by #{cl['user']}\n" description += "(source: #{source[:port]},#{source[:path]})\n\n" description += desc_suffix unless ( desc_suffix.nil? ) elsif ( cl.is_a?( String ) ) then description = "Automatic Integration of File #{cl}\n" description += "(source: #{source[:port]},#{target[:path]})\n\n" description += desc_suffix unless ( desc_suffix.nil? ) end target[:p4].create_changelist( description ) end # Submit changelist to target Perforce server. def submit_changelist( target, target_cl, log ) # Submit will raise an exception if there are no files to submit # so we detect here if we need to submit. files = target[:p4].run_opened( '-c', target_cl.to_s, '-C', target[:p4].client ) revert_ran = false if ( target[:autorevertunchanged] ) then if ( files.size > 0 ) then log.info( "Automatically reverting unchanged files in CL ##{target_cl.to_s}." ) result = target[:p4].run_revert( '-a', '-c', target_cl.to_s ) log.info( "Revert result: #{result}." ) revert_ran = true end end files = target[:p4].run_opened( '-c', target_cl.to_s, '-C', target[:p4].client ) if revert_ran if ( target[:autosubmit] and files.size > 0 ) then log.info( "Submitting changelist #{target_cl}." ) result = target[:p4].run_submit( '-c', target_cl.to_s ) log.info( "Submission result: #{result}." ) elsif ( 0 == files.size ) then log.warn( "Deleting empty changelist: #{target_cl}." ) target[:p4].delete_changelist( target_cl ) end end # Integrate an individual file using target_cl as a changelist number. # Its assumed the target_cl already exists, as multiple files may be being # integrated in separate calls to this function. def integrate_file( source, target, log, src_file, src_file_spec, target_cl ) begin source[:p4].connect() unless ( source[:p4].connected?() ) target[:p4].connect() unless ( target[:p4].connected?() ) fstat = source[:p4].run_fstat( src_file_spec ).shift # Determine file action. action = fstat['headAction'] if ( fstat.has_key?( 'headAction' ) ) action = fstat['action'] if ( fstat.has_key?( 'action' ) ) case action when 'delete', 'deleted', 'move/delete' action = :delete else action = :copy end src_file_local = fstat['clientFile'] dst_file = path_src_to_dst( source, target, src_file ) dst_file_local = target[:p4].depot2local( dst_file ) if ( dst_file.nil? or dst_file_local.nil? ) then log.error( "Unable to determine destination file from source: #{src_file}. File skipped!" ) return end # Determine if we need to ignore or filter the file; # if we do we abort the integration. if ( path_src_to_ignore?( source, src_file ) ) then log.warn( "Ignore file #{src_file} (matched ignore list)." ) return end if ( path_src_to_filter?( source, src_file_local ) ) then log.warn( "Ignoring file #{src_file_local} (matched filter list)." ) return end log.info( "Integrating file: " ) log.info( "\t#{src_file} [#{action}] to" ) log.info( "\t#{dst_file}" ) if ( ( :delete == action ) and target[:p4].exists?( dst_file ) ) then target[:p4].run_delete( '-c', target_cl, dst_file ) elsif ( :delete == action ) then log.warn( "Source file deleted #{src_file} but it does not exist on target to delete." ) elsif ( File::exists?( dst_file_local ) and target[:p4].exists?( dst_file ) ) then target[:p4].run_edit( '-c', target_cl, dst_file ) FileUtils::chmod( 0666, dst_file_local ) FileUtils::cp( src_file_local, dst_file_local ) elsif ( File::exists?( dst_file_local ) ) then FileUtils::chmod( 0666, dst_file_local ) FileUtils::cp( src_file_local, dst_file_local ) target[:p4].run_add( '-c', target_cl, dst_file ) else FileUtils::mkdir_p( OS::Path::get_directory( dst_file_local ) ) FileUtils::cp( src_file_local, dst_file_local ) target[:p4].run_add( '-c', target_cl, dst_file ) end rescue Exception => ex log.exception( ex, "Unhandled exception integrating file #{src_file}" ) end end # integrate a changelist def integrate_changelist(source,target,changelist_id,log = nil) log.info( "Integrate Changelist: #{changelist_id}" ) changelist = source[:p4].run_describe( '-s', changelist_id.to_s ).shift if ( changelist.nil? ) then # Describe failed; likely invalid changelist number of source. log.error( "invalid changelist (#{changelist_id}); p4 describe did not return any data." ) return end # Create target changelist. target_cl = create_changelist( source, target, changelist ) # Loop through our changelist files, sync to changelist revision # for the copy, then copy to new location. changelist['depotFile'].each_with_index do |depotFile, index| filespec = "#{depotFile}##{changelist['rev'][index]}" log.info( "Integrate, sync: #{filespec}" ) source[:p4].run_sync( filespec ) integrate_file( source, target, log, depotFile, filespec, target_cl ) end submit_changelist( target, target_cl, log ) end # integrate each valid changelist in the filespec, to the head or the version in the filespec. def integrate_each_changelist(source,target,filename,log = nil) changelist_id = 0 begin # Filename or filespec. log.info( "Integrate (each changelist) Filespec: #{filename}" ) # get the latest change we have on our client change_have = source[:p4].run_changes( "-m 1", "#{filename}@#{source[:p4].client}" ).shift curr_cl = change_have ? change_have['change'].to_i() + 1 : nil if (curr_cl) then log.info( "Integrate starts at changelist #{curr_cl}" ) # get a list of changes up till now changes = source[:p4].run_changes( "#{filename}@#{curr_cl},@now" ) log.info( "Integrate #{changes.length} changelists" ) # sort em changes.sort! {|a,b| a['change'].to_i() <=> b['change'].to_i() } # integrate each one changes.each do |change| changelist_id = change['change'] log.info( " Integrate change #{changelist_id}" ) integrate_changelist(source,target,changelist_id,log) end else log.warn( "Integrate didn't run because we can't tell what changelist is synced - consider syncing to head first." ) end rescue Exception => ex log.exception( ex, "Unhandled exception in integrate_changelist #{changelist_id} no more changelists will be integrated from here - Fix problem and retry." ) end end # integrate a filespec def integrate_filespec(source,target,filename,log = nil) # Filename or filespec. log.info( "Integrate Filespec: #{filename}" ) # Run fstat so we can support filespecs properly (e.g. //depot/..., //depot/*) # Then use its output as filename to actually integrate. fstat = source[:p4].run_fstat( filename ) target_cl = create_changelist( source, target, filename ) fstat.each do |filespec| filename = filespec['depotFile'] integrate_file( source, target, log, filename, "#{filename}\#head", target_cl ) end submit_changelist( target, target_cl, log ) end def integrate_source_to_target( source, target, files, log = nil ) log = LogSystem::instance.rootlog if log == nil # Create Perforce connection objects. source[:p4] = SCM::Perforce.new( ) source[:p4].port = source[:port] source[:p4].client = source[:client] source[:p4].user = source[:user] source[:p4].connect( ) target[:p4] = SCM::Perforce.new( ) target[:p4].port = target[:port] target[:p4].client = target[:client] target[:p4].user = target[:user] target[:p4].connect( ) # Loop through files and changelists specified as trailing arguments. files.each do |filename| if ( filename.to_i.to_s == filename ) then # Changelist identifier. changelist_id = filename.to_i integrate_changelist(source,target,changelist_id,log) elsif (source[:each_changelist]) then integrate_each_changelist(source,target,filename,log) else integrate_filespec(source,target,filename,log) end end end # Initialization of all options to the source Perforce server. def initialize_source_options(g_Source, g_Options) g_Source[:ignore] = g_Options['ignore'].split( ',' ) unless ( g_Options['ignore'].nil? ) g_Source[:ignore] = [] if ( g_Options['ignore'].nil? ) g_Source[:filter] = g_Options['filter'].split( ',' ) unless ( g_Options['filter'].nil? ) g_Source[:filter] = [] if ( g_Options['filter'].nil? ) g_Source[:each_changelist] = g_Options['each_changelist'] unless ( g_Options['each_changelist'].nil? ) g_Source[:each_changelist] = false if ( g_Options['each_changelist'].nil? ) end # Initialization of all options to the target Perforce server. def initialize_target_options(g_Target, g_Options) g_Target[:autosubmit] = g_Options['autosubmit'] unless ( g_Options['autosubmit'].nil? ) g_Target[:autosubmit] = false if ( g_Options['autosubmit'].nil? ) g_Target[:autorevertunchanged] = g_Options['autorevertunchanged'] unless ( g_Options['autorevertunchanged'].nil? ) g_Target[:autorevertunchanged] = false if ( g_Options['autorevertunchanged'].nil? ) end #---------------------------------------------------------------------------- # Entry #---------------------------------------------------------------------------- if ( __FILE__ == $0 ) then begin g_AppName = OS::Path::get_basename( __FILE__ ) g_Config = Pipeline::Config.instance( ) g_HasErrors = false; g_Source = {} # Source Perforce server settings g_Target = {} # Target Perforce server settings #-------------------------------------------------------------------- # Parse Command Line #-------------------------------------------------------------------- g_Options, g_Trailing = OS::Getopt.getopts( OPTIONS ) g_Debug = g_Options['debug'].nil? ? false : true # Force log output g_Config::log_trace = g_Debug g_Config::logtostdout = true g_Config::log_level = 2 g_Config::log_level = 1 if ( g_Debug ) g_Log = Log::new( g_AppName ) if ( g_Options['help'] ) then puts OS::Getopt.usage( OPTIONS ) exit( 1 ) end throw RuntimeError.new( 'Tools Framework integrations are not permitted by non programmers.' ) \ unless ( g_Config.user.is_tools() or g_Config.user.is_programmer() or g_Config.user.is_builder_client() or g_Config.user.is_builder_server() ) #-------------------------------------------------------------------- # Parse Command Line : Source Arguments and Validate #-------------------------------------------------------------------- g_Source[:port] = g_Options['source_port'] g_Source[:client] = g_Options['source_client'] g_Source[:user] = g_Options['source_user'] g_Source[:path] = g_Options['source_path'] initialize_source_options(g_Source, g_Options) # Validate g_Source.each_pair do |k, v| next unless ( v.nil? ) g_Log.error( "source settings incomplete (#{k})." ) puts OS::Getopt::usage( OPTIONS, TRAILING ) exit( 2 ) end #-------------------------------------------------------------------- # Parse Command Line : Target Arguments and Validate #-------------------------------------------------------------------- g_Target[:port] = g_Options['target_port'] g_Target[:client] = g_Options['target_client'] g_Target[:user] = g_Options['target_user'] g_Target[:path] = g_Options['target_path'] initialize_target_options(g_Target, g_Options) # Validate g_Target.each_pair do |k, v| next unless ( v.nil? ) g_Log.error( "target settings incomplete (#{k})." ) puts OS::Getopt::usage( OPTIONS, TRAILING ) exit( 3 ) end #-------------------------------------------------------------------- # Additional Validation #-------------------------------------------------------------------- if ( g_Options['same_port'].nil? ) if ( 0 == g_Source[:port].casecmp( g_Target[:port] ) ) then g_Log.error( "source and target ports are the same. Crazy fool use 'p4 integ'." ) puts OS::Getopt::usage( OPTIONS, TRAILING ) exit( 4 ) end end #-------------------------------------------------------------------- # Integrate #-------------------------------------------------------------------- if ( 0 == g_Trailing.size ) then g_Log.error( "no files or changelists specified to integrate." ) puts OS::Getopt::usage( OPTIONS, TRAILING ) exit( 5 ) end integrate_source_to_target(g_Source, g_Target, g_Trailing, g_Log) if ( LogSystem::instance().was_error ) then exit( 1 ) else exit( 0 ) end rescue SystemExit => ex exit( ex.status ) rescue Exception => ex g_Log.exception( ex, 'Unhandled exception' ) puts "Unhandled exception: #{ex.message}" puts ex.backtrace.join("\n") exit -1 end end # %RS_TOOLSLIB%/util/p4_integrate.rb