#-- #------------------------------------------------------------------------------- #++ # #= Introduction # #== Name: P4Triggers.rb # #== Author: Tony Smith <tony@perforce.com> # #== 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. # # == Version # # $Id: //guest/tony_smith/perforce/P4Rubylib/triggers/P4Triggers.rb#4 $ # #-- #------------------------------------------------------------------------------- #++ # # 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 # # == 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 @change = nil @p4 = P4.new @p4.parse_forms begin @p4.connect rescue P4Exception message( 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 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" ) 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.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| $stdeoutrr.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
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#8 | 6437 | Tony Smith |
Update P4Ruby Library scripts to support Perforce P4Ruby 2007.3 rather than my old public depot P4Ruby. |
||
#7 | 5807 | Tony Smith |
Add optional parameter to P4Trigger constructor allowing scripts to specify their API level. A following change will set this level for specific scripts. |
||
#6 | 4685 | Tony Smith |
Move the connect() down into the parse_change() method from the constructor. This gives users time to change the configuration of the P4 object before they connect. Useful for setting usernames etc. |
||
#5 | 4684 | Tony Smith | Minor bugfixes found while working on an example for a customer. | ||
#4 | 4656 | Tony Smith |
Rework the defaultclient.rb trigger to be more robust and more efficient. This involved a certain amount of rework to the P4Triggers.rb module to include functionality that will be useful to other spec triggers - there's a new class P4Trigger::FormFile that helps scripts manage their interaction with the temporary formfile supplied by the server. |
||
#3 | 4555 | Tony Smith |
Improve error reporting in trigger framework. Now Perforce warnings will also generate trigger errors, and the error messages will be sent to stdout and stderr. Stderr will also get a stack trace. |
||
#2 | 3637 | Tony Smith | Add RDoc documentation to the sample triggers. | ||
#1 | 3634 | Tony Smith |
Kick off a library of P4Ruby resources with some sample trigger implementations based on Jeff Bowles and Wes Peters' scripts. These are not strict ports of their scripts to P4Ruby, but are roughly in the same area. |