Files
2025-09-29 00:52:08 +02:00

438 lines
16 KiB
Ruby
Executable File

#
# File:: %RS_TOOLSLIB%/util/p4_integrate.rb
# Description:: Perforce integration script for integrating changes between
# two Perforce servers.
#
# Author:: David Muir <david.muir@rockstarnorth.com>
# 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