#!/usr/bin/ruby #******************************************************************************* #* A Ruby script to detect changes to different types of spec ( client/label/ #* branch ) and to record the changes by checking the specs in as text files #* into a dedicated area of the depot. Designed to be periodically executed #* via either cron or the Windows scheduler depending on platform. #* #* Note: This script REQUIRES a working build of P4Ruby which you can #* get from the Perforce public depot in #* #* //guest/tony_smith/perforce/API/Ruby/... #* #* You'll also need the Perforce API for your platform to build P4Ruby. #* #* specsaver.rb tries to be as efficient as possible in determining whether #* or not an object has been modified. It doesn't depend on a blanket #* "p4 revert -a" approach, but uses a counter and the built-in timestamps for #* objects that have them. #* #******************************************************************************* #******************************************************************************* #* CONFIGURATION SECTION #******************************************************************************* # Depot path under which specs should be versioned. This path should not # already exist. SPEC_PATH = "//depot/specs" # Name of the counter to use to keep track of update runs. One counter is # used for *all* types of spec. SPEC_COUNTER = "specsaver" # Name of the field in your jobspec which is used as the update date JOB_DATE_FIELD = "Date" # Perforce environment setup P4USER = "perforce" P4PORT = "localhost:1666" P4PASSWD = "" P4CLIENT = "specsaver" # Set the client root to a path that the script can use as its workspace CLIENTROOT = "/home/tony/p4-specmgr" #******************************************************************************* #* END OF CONFIGURATION SECTION #******************************************************************************* require "P4" require "getoptlong" # Virtual base class for handling all types of spec. For each spec you want # to manage, derive a class which must implement at least the following # methods: # # list_specs - list specs ("p4 clients"/"p4 labels" etc.) # spec_file - Locate spec file in workspace # class SpecMgr def initialize( root, p4tagged, p4untagged ) @root = root @p4t = p4tagged @p4u = p4untagged @modlist = Hash.new end def changed?( spec, stamp ) spec[ "Update" ].to_i > stamp end def add_edit_file( path, name ) fs = @p4t.run_fstat( path ).shift if ( fs ) @p4t.run_edit( path ) else @p4t.run_add( "-t", "text", path ) fs = @p4t.run_fstat( path ).shift end @modlist[ fs[ "depotFile" ] ] = name end # # Compute the name of a spec from its type. Necessary because the # field for jobs is "Job" whilst for clients it's "client" ... # def type2name( type, spec ) return spec[ type ] if spec.has_key?( type ) return spec[ type.capitalize ] if spec.has_key?( type.capitalize ) raise( RuntimeError, "Can't determine object name from type" ) end # # Save the named spec into its text file version. As we don't want # the spec in parsed form for this we instantiate another P4 instance # briefly here to run the "p4 xxxx -o" in non-tagged mode and the # output of that command is written to the workspace file. # def save_spec( type, spec ) path = spec_file( spec ) name = type2name( type, spec ) add_edit_file( path, name ) form = eval( %Q{@p4u.fetch_#{type}( "#{name}" )} ) write_ws_file( path, form ) end def write_ws_file( path, form ) File.open( path, "w+" ) do |file| file.write( form ) end end # # Revert all unchanged files and remove them from the modlist. # def revert_unchanged() @p4t.run_revert( "-a" ).each do |r| r = r.sub( /\#\d+.*/, "" ) @modlist.delete( r ) if @modlist.has_key?( r ) end end # # Submit the changelist and write out a message including the # list of objects being updated. # def submit( desc ) revert_unchanged() change = @p4t.fetch_change if ( change.has_key?( "Files" ) ) change[ "Description" ] = desc @p4t.submit_spec( change ) puts( desc + "\n\t" + @modlist.values.sort.join( "\n\t" ) ) end @modlist = Hash.new end # Update the specs of the specified type def update( type, since ) list_specs.each do |spec| save_spec( type, spec ) if ( changed?( spec, since ) ) end submit( description() ) end end class GenSpecMgr < SpecMgr def initialize( type, root, p4t, p4u ) @type = type super( root, p4t, p4u ) end def list_specs eval( %Q{@p4t.run_#{@type}s} ) end def spec_file( spec ) @root + "/#{@type}s/" + type2name( @type, spec ) end def update( since ) super( @type, since ) end def description "Archive updated #{@type}s" end end # Subclass for clients class ClientMgr < GenSpecMgr def initialize( root, p4t, p4u ) super( "client", root, p4t, p4u ) end end # Subclass for labels class LabelMgr < GenSpecMgr def initialize( root, p4t, p4u ) super( "label", root, p4t, p4u ) end # For labels we update the files list as well as the spec def update( since ) list_specs.each do |l| if ( changed?( l, since ) ) save_spec( "label", l ) save_files( l[ "label" ] ) end end submit( description() ) end def save_files( label ) filename = @root + "/labels/" + label + ".files" files = @p4t.run_files( "//...@#{label}" ).collect do |l| l[ "depotFile" ] + "#" + l[ "rev" ] end add_edit_file( filename, label ) write_ws_file( filename, files.join( "\n" ) ) end end # Subclass for users class UserMgr < GenSpecMgr def initialize( root, p4t, p4u ) super( "user", root, p4t, p4u ) end end # Subclass for groups. Groups don't have an update date so we have # to depend on revert -a doing the honours. "p4 groups" also doesn't # support tagged output at the moment (2002.1) so what are hashes in # the other classes are just group names in this one. class GroupMgr < GenSpecMgr def initialize( root, p4t, p4u ) super( "group", root, p4t, p4u ) end def changed?( group, since ) true end def spec_file( group ) @root + "/#{@type}s/" + group end def type2name( type, group ) group end end # Subclass for depots. "p4 depots" doesn't support tagged mode so we # do this the simple way. class DepotMgr < GenSpecMgr def initialize( root, p4t, p4u ) super( "depot", root, p4t, p4u ) end def changed?( depot, since ) true end def spec_file( depot ) @root + "/#{@type}s/" + depot end def type2name( type, depot ) depot end def list_specs @p4t.run_depots.collect do |rec| rec.split[ 1 ] end end end # Subclass for branches. As the plural of branch is branchES, this needs # a separate subclass of its own. class BranchMgr < SpecMgr def list_specs @p4t.run_branches end def spec_file( spec ) @root + "/branches/" + spec[ "branch" ] end def update( since ) super( "branch", since ) end def description "Archive updated branches" end end # Subclass for jobs class JobMgr < SpecMgr # Get a list of labels from the server. def list_specs( stamp ) @p4t.run_jobs( "-e", "#{JOB_DATE_FIELD} > #{stamp}" ) end # Locate the workspace file for a given spec def spec_file( spec ) @root + "/jobs/" + spec[ "Job" ] end def update( since ) list_specs( since ).each do |spec| save_spec( "job", spec ) end submit( description() ) end def description "Archive updated jobs" end end # Subclass for one off's like jobspec and protections. These are a little # different as "there can be only one!" We also can't use a timestamp to # see if they've changed so we rely on "p4 revert -a" class ConfigMgr < SpecMgr def update( name, label ) path = @root + "/" + name add_edit_file( path, name ) form = eval %Q{@p4u.fetch_#{name}()} write_ws_file( path, form ) submit( "Archive updated #{label}" ) end end # # Main top level class for co-ordinating the run. Here we can trap # Perforce exceptions and look after the overall environment. # class SpecSaver SPECS = [ "branches", "clients", "depots", "groups", "jobs", "labels", "users" ] def initialize @flags = Hash.new # Perforce client for handling tagged output @p4t = P4.new @p4t.parse_forms @p4t.exception_level = 1 @p4t.port = P4PORT @p4t.user = P4USER @p4t.password = P4PASSWD @p4t.client = P4CLIENT @p4t.connect # Perforce client for non-tagged output @p4u = P4.new @p4u.exception_level = 1 @p4u.port = P4PORT @p4u.user = P4USER @p4u.password = P4PASSWD @p4u.client = P4CLIENT @p4u.connect # Set the client root @root = CLIENTROOT init_client() end def debug( level ) $stderr.puts( "Setting debug level to #{level}" ) @p4u.disconnect @p4t.disconnect @p4u.debug = level @p4t.debug = level @p4u.connect @p4t.connect end # Create if necessary def init_client() begin @p4t.run_info.each do |line| create_client() if ( line =~ /^Client unknown./ ) end init_workspace() @p4t.run_sync rescue P4Exception @p4t.errors.each { |e| puts( e ) } raise end end def init_workspace() Dir.mkdir( @root ) unless ( File.exists?( @root ) ) SPECS.each do |type| dir = @root + "/" + type Dir.mkdir( dir ) unless ( File.exists?( dir ) ) end end # Create the client workspace. def create_client() begin spec = @p4t.fetch_client spec[ "Description" ] = "Client for spec versioning script" spec[ "Root" ] = @root # Don't use String#gsub! it seems to cause problems opt = spec[ "Options" ].gsub( "normdir", "rmdir" ) spec[ "Options" ] = opt spec[ "View" ] = Array.new spec[ "View" ].push( SPEC_PATH + "/... //" + @p4t.client? + "/..." ) @p4t.save_client( spec ) rescue P4Exception @p4t.errors.each { |e| puts( e ) } end end def counter? c = @p4t.run_counter( SPEC_COUNTER ).shift.to_i return c end def counter=( val ) @p4t.run_counter( SPEC_COUNTER, val ) true end # Resolve missing methods by looking in the flags hash. def method_missing( m, *args ) method = m.to_s if ( method !~ /\?$/ ) # Assignment return @flags[ method ] = true else method.gsub!( "\\?$", "" ) end if ( @flags.has_key?( method ) ) @flags[ method ] elsif ( @flags.has_key?( "all" ) ) @flags[ "all" ] else false end end def have_flags? return ! @flags.empty? end def update # Timestamp for this run now = Time.now.to_i # time of last update since = counter? begin branches = BranchMgr.new( @root, @p4t, @p4u ) clients = ClientMgr.new( @root, @p4t, @p4u ) depots = DepotMgr.new( @root, @p4t, @p4u ) groups = GroupMgr.new( @root, @p4t, @p4u ) jobs = JobMgr.new( @root, @p4t, @p4u ) labels = LabelMgr.new( @root, @p4t, @p4u ) users = UserMgr.new( @root, @p4t, @p4u ) config = ConfigMgr.new( @root, @p4t, @p4u ) branches.update( since ) if ( self.branches? ) clients.update( since ) if ( self.clients? ) depots.update( since ) if ( self.depots? ) groups.update( since ) if ( self.groups? ) jobs.update( since ) if ( self.jobs? ) labels.update( since ) if ( self.labels? ) users.update( since ) if ( self.users? ) if ( self.config? ) config.update( "protect", "protections" ) config.update( "jobspec", "jobspec" ) config.update( "typemap", "typemap" ) end # Save the counter. self.counter = now rescue P4Exception @p4t.errors.each{ |e| puts( e ) } @p4u.errors.each{ |e| puts( e ) } end end end def croak_syntax puts( %Q{ Usage: specsaver.rb [ options ] where options are: --all - archive all types of object --branches - archive branch specs --clients - archive client specs --config - archive typemap, protections and jobspec --depots - archive depot definitions --groups - archive group specs --jobs - archive jobs (but not fixes) --labels - archive label views and files --users - archive user specs (not passwords) } ) exit( 0 ) end #******************************************************************************* #* START OF MAIN SCRIPT #******************************************************************************* # Parse command line options opts = GetoptLong.new ( [ "--all", GetoptLong::NO_ARGUMENT ], [ "--branches", GetoptLong::NO_ARGUMENT ], [ "--clients", GetoptLong::NO_ARGUMENT ], [ "--config", GetoptLong::NO_ARGUMENT ], [ "--debug", GetoptLong::OPTIONAL_ARGUMENT ], [ "--depots", GetoptLong::NO_ARGUMENT], [ "--groups", GetoptLong::NO_ARGUMENT ], [ "--jobs", GetoptLong::NO_ARGUMENT ], [ "--labels", GetoptLong::NO_ARGUMENT ], [ "--users", GetoptLong::NO_ARGUMENT ] ) s = SpecSaver.new opts.each do |opt,arg| s.all if ( opt == "--all" ) s.branches if ( opt == "--branches" ) s.clients if ( opt == "--clients" ) s.config if ( opt == "--config" ) s.depots if ( opt == "--depots" ) s.groups if ( opt == "--groups" ) s.jobs if ( opt == "--jobs" ) s.labels if ( opt == "--labels" ) s.users if ( opt == "--users" ) s.debug( arg.empty? ? 1 : arg.to_i ) if ( opt == "--debug" ) end croak_syntax unless ( s.have_flags? ) s.update