#-- #------------------------------------------------------------------------------- #++ # #= Introduction # #== Name: P4Triggers.rb # #== Author: Tony Smith # #== Description # # A library for use when writing Perforce triggers. Methods and classes # common to all my example triggers are to be found in here. The framework # for a trigger is very simple: # # 1. You derive your trigger class from P4Trigger # 2. You implement the validate() method to decide whether or not to # allow the submit # 3. You send output to the user using the message() method # 4. You raise exceptions to report runtime errors P4Exceptions are # already handled for you. # 5. Your trigger script should call P4Trigger#parse_change() and should use # the value returned as its exit status. # # And that's all there is to it! # # The P4Change class merits some familiarisation because objects of this # class are what you get when you call P4Trigger#change() and it's a # very convenient way to examine the changelist being submitted. # # If you're just getting started with this framework, start by looking at # the P4Trigger class and then look at P4Change. #-- #------------------------------------------------------------------------------- #++ # A class for holding info about a change. This is intended to be populated # be the output of "p4 describe" rather than "p4 change -o". The difference # is mostly in the case of the hash keys, but that's significant enough. class P4Change # Constructor. Pass the hash returned by P4#run_describe( "-s" ) in # tagged mode. def initialize( hash ) @change = hash[ "change" ] @user = hash[ "user" ] @client = hash[ "client" ] @desc = hash[ "desc" ] @time = Time.at( hash[ "time" ].to_i ) @status = hash[ "status" ] @files = Array.new @jobs = Hash.new if ( hash.has_key?( "depotFile" ) ) hash[ "depotFile" ].each_index do |i| name = hash[ "depotFile" ][ i ] type = hash[ "type" ][ i ] rev = hash[ "rev" ][ i ] act = hash[ "action" ][ i ] df = P4DepotFile.new( name ) dr = df.new_revision dr.type = type dr.revno = rev.to_i dr.action = act @files.push( df ) end end if ( hash.has_key?( "job" ) ) hash[ "job" ].each_index do |i| job = hash[ "job" ][ i ] status = hash[ "jobstat" ][ i ] @jobs[ job ] = status end end end attr_reader :change, :user, :client, :desc, :time, :status, :files, :jobs # Shorthand iterator for looking at the files in the change def each_file( &block ) @files.each { |f| yield( f ) } end # Shorthand iterator for looking at the jobs fixed by the change def each_job( &block ) @jobs.each { |j| yield( j ) } end end # A class for encapsulating file types - type checking is quite common in # many triggers so it's nice to have it available to all. class FileType # Provide a regexp that the filetype must match and a message for the # types that match the regexp. i.e. # # FileType.new( /u?binary/, "binary/ubinary" ) # def initialize( regexp, message ) @re = regexp @msg = message end attr_reader :msg # Test to see if the given filetype matches this type using the # regexp. def match( type ) @re.match( type ) end end # Reopen the P4 class to restructuring the output of "p4 describe -s" into a # P4Change object, and to disable any attempt to submit - just in case # someone's foolish enough to try class P4 def run_describe( *args ) h = run( "describe", args ).shift return h unless h.kind_of?( Hash ) return P4Change.new( h ) end # Disable the submit_spec interface def submit_spec raise( "Attempt to submit during trigger execution. Don't do it." ) end # Disable the direct interface to submit def run_submit raise( "Attempt to submit during trigger execution. Don't do it." ) end end # A class providing a generic framework for all triggers. It provides # rudimentary error handling so all errors get logged to stderr, and we # also have a method for reporting a more friendly message to the user. # It is intended that all trigger scripts will derive their own subclass # of P4Trigger and override/expand it as necessary. # # You must override the validate() method if you want your trigger to # work. The default implementation accepts *everything* so make sure you # provide a method to reject bad changes or you will have accomplished # nothing. # class P4Trigger # Constructor. def initialize @change = nil @p4 = P4.new @p4.parse_forms begin @p4.connect rescue P4Exception error_message() raise end end # Direct access to the P4 object. You can use this for interrogating # the Perforce server. It's in parse_forms() mode and has an # exception_level of 1 so exceptions will only be raised on errors, not # warnings. attr_reader :p4 # Direct access to the changelist in question. Returns a P4Change object attr_reader :change # The main execution phase of the trigger. We assume since this is a # pre-submit trigger that there will be a changelist involved. The # steps for most triggers are common: # # 1. Find out about the changelist # 2. Enforce the rules # # We try to generalise this process so this class calls get_change() # for step 1, and validate() for step 2. Subclasses may override these # methods to tailor the trigger's behaviour # # parse_change() returns the correct exit status for the trigger so you # would normally use it like this: # # exit( trig.parse_change( change_no ) ) # def parse_change( change_no ) begin if ( ! change_no ) raise( "No changelist number supplied to trigger script.\n" + "Please check your trigger configuration." ) end get_change( change_no ) return ( validate() ? 0 : 1 ) rescue # Full error report to stderr, so they go into the Perforce server's # logfile $stderr.puts( "\nError during trigger execution:\n\n" ) if ( $!.kind_of?( P4Exception ) ) p4.warnings.each { |w| $stderr.puts( "WARNING: " + w ) } p4.errors.each { |e| $stderr.puts( "ERROR: " + e ) } else $stderr.puts( $!.to_s ) end $stderr.puts( "\nStack Trace:\n" ) $stderr.puts( $!.backtrace ) # # Simpler error report (sans-stack backtrace) to stdout. # $stdout.puts( error_message() ) p4.warnings.each { |w| $stdout.puts( "WARNING: " + w ) } p4.errors.each { |e| $stdeoutrr.puts( "ERROR: " + e ) } # Now we return false to the caller so they can return with # the correct exit status return 1 end end # The default implementation of get_change. Returns a hash describing # the change based on an execution of "p4 describe -s". The hash is # also saved in the @change member which subclasses may access in their # validate() method. For most triggers this will be sufficient. def get_change( change_no ) @change = p4.run_describe( "-s", change_no ) end # The default implementation of validate(). Very simple, it accepts # everything. You are expected to override this method with one of # your own. When you do so, you can use the @change member to get # at the details of the change you're validating. def validate() true end # # Method to send a message to the user. Just writes to stdout, but it's # nice to encapsulate that here. # def message( string ) $stdout.print( string ) end # Default message for when things go wrong def error_message() "\n" + "An error was encountered during trigger execution. Please\n" + "contact your Perforce administrator and ask them to\n" + "investigate the cause of this error\n\n" end end