438 lines
16 KiB
Ruby
Executable File
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
|