#-- #------------------------------------------------------------------------------- #++ # #= 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 pre-submit/content/commit triggers 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! # # For other trigger types (spec triggers) there's no generic framework, # yet. But there is a class (P4Triggers::FormFile) to help you work with # the temporary form files the server creates. # # 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. # # == License # # Copyright (c) 1997-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. # # == Version # # $Id: //guest/tony_smith/perforce/P4Rubylib/triggers/P4Triggers.rb#8 $ # #-- #------------------------------------------------------------------------------- #++ require "P4" # # 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 = P4::DepotFile.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 # # == Description # # 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. # # Since this framework serves ALL types of Perforce trigger, not all # the methods are appropriate to any given trigger type. # # == Pre-Submit Triggers # # 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. # # The conventional use of this module for this trigger-type is: # # exit( trig.parse_change( change_no ) ) # # The parse_change() method will call validate() and will return a # suitable exit status to pass back to the Perforce Server based on the # results of validate() # # == Content Triggers # # As for Pre-Submit Triggers # # == Commit Triggers # # As for Pre-Submit Triggers # # == Spec Triggers # # # # class P4Trigger # Constructor. def initialize( api_level = nil ) @change = nil @p4 = P4.new @p4.api_level = api_level if api_level 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 @p4.connect 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 return report_error() end end # # Method to encapsulate error reporting so that all types of trigger # can report error messages in a consistent way. def report_error # Full error report to stderr, so they go into the Perforce server's # logfile $stderr.puts( "\nError during trigger execution:\n\n" ) $stderr.puts( $!.to_s ) if ( $!.kind_of?( P4Exception ) ) p4.warnings.each { |w| $stderr.puts( "WARNING: " + w ) } p4.errors.each { |e| $stderr.puts( "ERROR: " + e ) } end $stderr.print( "\nStack Trace:\n\n\t" ) $stderr.print( $!.backtrace.join( "\n\t" ) ) $stderr.puts( "" ) # # Simpler error report (sans-stack backtrace) to stdout. # $stdout.puts( error_message() ) p4.warnings.each { |w| $stdout.puts( "WARNING: " + w ) } p4.errors.each { |e| $stdout.puts( "ERROR: " + e ) } # Now we return false to the caller so they can return with # the correct exit status return 1 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 # # A class for encapsulating the interaction with the temporary formfile # provided by the Perforce Server for use by spec triggers. Allows you # to load/save the file contents in a manner than preserves the comment # block at the top. # class FormFile def initialize( filename ) @filename = filename @comments = "" end # # Return the contents of the formfile. Saves the comments in the # file for use in a future call to save(). # def load() f = File.open( @filename, "r" ) @comments = "" buf = "" f.each_line do |line| if( line[0...1] == '#' ) @comments += line else buf += line end end @comments += "\n" buf += "\n" end # # Update the formfile with a new definition of the spec. The input # should be a string, or respond to to_s(), and should not include # any of the leading comments. # def save( spec ) File.open( @filename, "w" ) do |f| f.write( @comments ) f.write( spec ) end end end end