#*******************************************************************************
# Copyright (c) 2001-2008, Perforce Software, Inc. All rights reserved.
#
# 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 PERFORCE SOFTWARE, INC. 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.
#*******************************************************************************
#*******************************************************************************
#* Ruby interface to the Perforce SCM System
#*******************************************************************************
#*******************************************************************************
#* P4 class
#*******************************************************************************
#
# Get the bulk of the definition of the P4 class from the API interface
#
require "P4.so"
#
# Add the extra's written purely in ruby.
#
class P4
#
# Named constants for the exception levels. Note they are cumulative,
# so RAISE_ALL includes RAISE_ERRORS (as you'd expect).
#
RAISE_NONE = 0
RAISE_ERRORS = 1
RAISE_ALL = 2
#
# Named values for merge actions. Values taken from clientmerge.h in
# the Perforce API
#
MERGE_SKIP = 1
MERGE_ACCEPT_MERGED = 2
MERGE_ACCEPT_EDIT = 3
MERGE_ACCEPT_THEIRS = 4
MERGE_ACCEPT_YOURS = 5
# Named values for generic error codes returned by
# P4::Message#generic
EV_NONE = 0 # misc
# The fault of the user
EV_USAGE = 0x01 # request not consistent with dox
EV_UNKNOWN = 0x02 # using unknown entity
EV_CONTEXT = 0x03 # using entity in wrong context
EV_ILLEGAL = 0x04 # trying to do something you can't
EV_NOTYET = 0x05 # something must be corrected first
EV_PROTECT = 0x06 # protections prevented operation
# No fault at all
EV_EMPTY = 0x11 # action returned empty results
# not the fault of the user
EV_FAULT = 0x21 # inexplicable program fault
EV_CLIENT = 0x22 # client side program errors
EV_ADMIN = 0x23 # server administrative action required
EV_CONFIG = 0x24 # client configuration inadequate
EV_UPGRADE = 0x25 # client or server too old to interact
EV_COMM = 0x26 # communications error
EV_TOOBIG = 0x27 # not even Perforce can handle this much
# Named values for error severities returned by
# P4::Message#severity
E_EMPTY = 0 # nothing yet
E_INFO = 1 # something good happened
E_WARN = 2 # something not good happened
E_FAILED = 3 # user did something wrong
E_FATAL = 4 # system broken -- nothing can continue
# OutputHandler return values constants
REPORT = 0
HANDLED = 1
CANCEL = 2
def method_missing( m, *a )
# Generic run_* methods
if ( m.to_s =~ /^run_(.*)/ )
return self.run( $1, a )
# Generic fetch_* methods
elsif ( m.to_s =~ /^fetch_(.*)/ )
return self.run( $1, "-o", a ).shift
# Generic save_* methods
elsif ( m.to_s =~ /^save_(.*)/ )
if ( a.length == 0 )
raise( P4Exception, "Method P4##{m.to_s} requires an argument", caller)
end
self.input = a.shift
return self.run( $1, "-i", a )
# Generic delete_* methods
elsif ( m.to_s =~ /^delete_(.*)/ )
if ( a.length == 0 )
raise( P4Exception, "Method P4##{m.to_s} requires an argument", caller)
end
return self.run( $1, "-d", a )
# Generic parse_* methods
elsif ( m.to_s == "parse_forms" )
raise( NoMethodError, "undefined method 'P4#parse_forms'", caller )
elsif ( m.to_s =~ /^parse_(.*)/ )
if ( a.length != 1 )
raise( P4Exception, "Method P4##{m.to_s} requires an argument", caller)
end
return self.parse_spec( $1, a.shift )
# Generic format_* methods
elsif ( m.to_s =~ /^format_(.*)/ )
if ( a.length != 1 )
raise( P4Exception, "Method P4##{m.to_s} requires an argument", caller)
end
return self.format_spec( $1, a.shift )
# That's all folks!
else
raise NameError, "No such method #{m.to_s} in class P4", caller
end
end
#
# Simple interface for submitting. If any argument is a Hash, (or subclass
# thereof - like P4::Spec), then it will be assumed to contain the change
# form. All other arguments are passed on to the server unchanged.
#
def run_submit( *args )
form = nil
nargs = args.flatten.collect do
|a|
if( a.kind_of?( Hash ) )
form = a
nil
else
a
end
end.compact
if( form )
self.input = form
nargs.push( "-i" )
end
return self.run( "submit", nargs )
end
#
# Simple interface for shelving. Same rules as for submit apply
def run_shelve( *args )
form = nil
nargs = args.flatten.collect do
|a|
if( a.kind_of?( Hash ) )
form = a
nil
else
a
end
end.compact
if( form )
self.input = form
nargs.push( "-i" )
end
return self.run( "shelve", nargs )
end
def delete_shelve( *args )
if( ! args.include?( "-c" ) )
args.unshift( "-c")
end
return self.run( "shelve", "-d", args)
end
#
# Simple interface for using "p4 login"
#
def run_login( *args )
self.input = self.password
return self.run( "login", args )
end
def run_resolve( *args )
if( block_given? )
self.run( "resolve", args ) do
|default|
yield( default )
end
else
self.run( "resolve", args )
end
end
#
# Interface for changing the user's password. Supply the old password
# and the new one.
#
def run_password( oldpass, newpass )
if( oldpass && oldpass.length > 0 )
self.input = [ oldpass, newpass, newpass ]
else
self.input = [ newpass, newpass ]
end
self.run( "password" )
end
#
# The following methods convert the standard output of some common
# Perforce commands into more structured form to make using the
# data easier.
#
# (Currently only run_filelog is defined. More to follow)
#
# run_filelog: convert "p4 filelog" responses into objects with useful
# methods
#
# Requires tagged output to be of any real use. If tagged output it not
# enabled then you just get the raw data back
#
def run_filelog( *args )
raw = self.run( 'filelog', args.flatten )
raw.collect do
|h|
if ( ! h.kind_of?( Hash ) )
h
else
df = P4::DepotFile.new( h[ "depotFile" ] )
h[ "rev" ].each_index do
|n|
# If rev is nil, there's nothing here for us
next unless h[ "rev" ][ n ]
# Create a new revision of this file ready for populating
r = df.new_revision
h.each do
|key,value|
next unless( value.kind_of?( Array ) )
next unless value[ n ]
next if( value[ n ].kind_of?( Array ) )
# If the field is the revision time, convert it to a Time object
value[ n ] = Time.at( value[ n ].to_i ) if key == "time"
r.set_attribute( key, value[ n ] )
end
# Now if there are any integration records for this revision,
# add them in too
next unless ( h[ "how" ] )
next unless ( h[ "how" ][ n ] )
h[ "how" ][ n ].each_index do
|m|
how = h[ "how" ][ n ][ m ]
file = h[ "file" ][ n ][ m ]
srev = h[ "srev" ][ n ][ m ]
erev = h[ "erev" ][ n ][ m ]
srev.gsub!( /^#/, "" )
erev.gsub!( /^#/, "" )
srev = ( srev == "none" ? 0 : srev.to_i )
erev = ( erev == "none" ? 0 : erev.to_i )
r.integration( how, file, srev, erev )
end
end
df
end
end
end
#
# Allow the user to run commands at a temporarily altered exception level.
# Pass the new exception level desired, and a block to be executed at that
# level.
#
def at_exception_level( level )
return self unless block_given?
old_level = self.exception_level
self.exception_level = level
begin
yield( self )
ensure
self.exception_level = old_level
end
self
end
#
# Allow users to run commands using a specified handler.
# Pass a handler and the block that will be executed using this handler
# The handler will be reset to its previous value at the end of this block
#
def with_handler( handler )
return self unless block_given?
old_handler = self.handler
self.handler = handler
begin
yield( self )
ensure
self.handler = old_handler
end
self
end
#
# Show some handy information when using irb
#
def inspect
sprintf( 'P4: [%s] %s@%s (%s)',
self.port, self.user, self.client,
self.connected? ? 'connected' : 'not connected' )
end
#*****************************************************************************
# The P4::Spec class holds the fields in a Perforce spec
#*****************************************************************************
class Spec < Hash
def initialize( fieldmap = nil )
@fields = fieldmap
end
#
# Override the default assignment method. This implementation
# ensures that any fields defined are valid ones for this type of
# spec.
#
def []=( key, value )
if( self.has_key?( key ) || @fields == nil )
super( key, value )
elsif( @fields.has_key?( key.downcase ) )
super( @fields[ key.downcase ], value )
else
raise( P4Exception, "Invalid field: #{key}" )
end
end
#
# Return the list of the fields that are permitted in this spec
#
def permitted_fields
@fields.values
end
#
# Implement accessor methods for the fields in the spec. The accessor
# methods are all prefixed with '_' to avoid conflicts with the Hash
# class' namespace. This is a little ugly, but we gain a lot by
# subclassing Hash so it's worth it.
#
def method_missing( m, *a )
k = m.to_s.downcase
# Check if we're being asked for 'to_ary'. If so, raise 'NoMethodError'.
return :to_ary ? raise( NoMethodError ) : super if ( k == "to_ary" )
if( k[ 0..0 ] != "_" )
raise( RuntimeError,
"undefined method `#{m.to_s}' for object of " +
"class #{self.class.to_s}" )
end
k = k[ 1..-1 ]
if( k =~ /(.*)=$/ )
if( a.length() == 0 )
raise( P4Exception, "Method P4##{m} requires an argument" );
end
k = $1
if( @fields == nil || @fields.has_key?( k ) )
return self[ @fields[ k ] ] = a.shift
end
elsif( self.has_key?( m.to_s ) )
return self[ m.to_s ]
elsif( @fields.has_key?( k ) )
return self[ @fields[ k ] ]
end
raise( P4Exception, "Invalid field: #{$1}" )
end
end
#*****************************************************************************
#* P4::MergeInfo class
#*****************************************************************************
class MergeInfo
def initialize( base, yours, theirs, merged, hint )
@base = base
@yours = yours
@theirs = theirs
@merged = merged
@hint = hint
end
attr_reader :base, :yours, :theirs, :merged, :hint
end
#*****************************************************************************
# P4::Integration class
# P4::Integration objects hold details about the integrations that have
# been performed on a particular revision. Used primarily with the
# P4::Revision class
#*****************************************************************************
class Integration
def initialize( how, file, srev, erev )
@how = how
@file = file
@srev = srev
@erev = erev
end
attr_reader :how, :file, :srev, :erev
end
#*****************************************************************************
# P4::Revision class
# Each P4::Revision object holds details about a particular revision
# of a file. It may also contain the history of any integrations
# to/from the file
#*****************************************************************************
class Revision
def initialize( depotFile )
@depot_file = depotFile
@integrations = Array.new
@attributes = Hash.new
end
attr_reader :depot_file
attr_accessor :integrations
def integration( how, file, srev, erev )
rec = P4::Integration.new( how, file, srev, erev )
@integrations.push( rec )
return rec
end
def each_integration
@integrations.each { |i| yield( i ) }
end
def set_attribute( name, value )
name = name.downcase
if( value =~ /^\d+$/ )
@attributes[ name ] = value.to_i
else
@attributes[ name ] = value
end
end
# Define #type and #type= explicitly as they clash with the
# deprecated Object#type. As it is deprecated, this clash should
# disappear in time.
def type
@attributes[ 'type' ]
end
def type=( t )
@attributes[ 'type' ] = t
end
#
# Generic getters and setters for revision attributes.
#
def method_missing( m, *a )
k = m.to_s.downcase
if( k =~ /(.*)=$/ )
if( a.length() == 0 )
raise( P4Exception, "Method P4##{m} requires an argument" );
end
k = $1
@attributes[ k ] = a.shift
else
@attributes[ k ]
end
end
end
#*****************************************************************************
# P4::DepotFile class.
# Each DepotFile entry contains details about one depot file.
#*****************************************************************************
class DepotFile
def initialize( name )
@depot_file = name
@revisions = Array.new
@headAction = @head_type = @head_time = @head_rev = @head_change = nil
end
attr_reader :depot_file, :revisions
attr_accessor :head_action, :head_type, :head_time, :head_rev, :head_change
def new_revision
r = P4::Revision.new( @depot_file )
@revisions.push( r )
return r
end
def each_revision
@revisions.each { |r| yield( r ) }
end
end
#*****************************************************************************
# P4::OutputHandler class.
# Base class for all Handler classes that can be passed to P4::handler.
#*****************************************************************************
class OutputHandler
def outputStat(stat)
REPORT
end
def outputInfo(info)
REPORT
end
def outputText(text)
REPORT
end
def outputBinary(binary)
REPORT
end
def outputMessage(message)
REPORT
end
end
class ReportHandler < OutputHandler
def outputStat(stat)
p "stat:", stat
HANDLED
end
def outputInfo(info)
p "info:", info
HANDLED
end
def outputText(text)
p "text:", text
HANDLED
end
def outputBinary(binary)
p "binary:", binary
HANDLED
end
def outputMessage(message)
p "message:", message
HANDLED
end
end
end # class P4