#!/usr/bin/ruby #-- # ****************************************************************************** #++ # = Introduction # # A Ruby script to detect changes to different types of spec ( client/label/ # branch/etc. ) 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. # # = Getting Started # # The script creates its own clientspec so minimal setup is required. All # you need to do is to edit the variables in the configuration section and # ensure that the script has write access to the SPEC_PATH in the depot and # then you can run it. # # If you want to archive privileged metadata like your protections table, # then specsaver needs to be run under a super-user account. If you're just # after clientspecs etc. then no special privileges are required. # # = 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) # # = Known Uglies # # Currently we only use one counter to store a timestamp for all types of # object. This is occasionally a source of frustration and at some point # I'll adapt the script to use a counter for each type of object. Until then, # if you suspect that specsaver.rb isn't catching all the changed objects - # usually the result of some human interference - then delete the counter or # reset it to zero and let it figure it out for itself. It'll take a while, # but it will get it right. # # To avoid the problem altogether always use the same arguments when invoking # specsaver.rb. i.e. if you invoke it once with --all, then always use --all. # If you do want to change your mind, then just delete or reset the counter. # # = Version # # $Id: //guest/robert_cowham/perforce/utils/specsaver.rb#6 $ #-- # ****************************************************************************** #++ # ------------------------------------------------------------------------------ # CONFIGURATION SECTION # ------------------------------------------------------------------------------ # Depot path under which specs should be versioned. This path should not # already exist. SPEC_PATH = "//software/imports/cvs_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 = "robert" P4PORT = "1666" P4PASSWD = "Password" P4CLIENT = "specsaver" # Set the client root to a path that the script can use as its workspace CLIENTROOT = "c:\\temp\\specsaver_root" # ------------------------------------------------------------------------------ # 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 # # Optionally, you may also want to override the changed? method used to # determine whether or not a spec has changed since the last time it was # archived. The default method uses the "Update" timestamp in the spec, # but not all specs have this. If in doubt, changed? should just evaluate # to true anyway, and let the "p4 revert -a" sort it out. # class SpecMgr public # # Constructor: Supply the client root, and two P4 instances, one with # tagged mode enabled, and one without. # def initialize( root, p4tagged, p4untagged ) @root = root @p4t = p4tagged @p4u = p4untagged @modlist = Hash.new end # # Updates the archives for all specs of the specified type. This is the # main public interface method for this class # def update( type, since ) list_specs.each do |spec| save_spec( type, spec ) if ( changed?( spec, since ) ) end submit( description() ) end protected # # Method to determine whether or not a spec has changed since the # specified timestamp. The default implementation uses the "Update" # field of the spec, but this is not available in all types of spec so # this method should be overridden for those specs. Where there is no # easy way to decide whether or not a spec has changed, override # implementations of this method should just return true and let the # "p4 revert -a" sort it all out. # def changed?( spec, stamp ) spec[ "Update" ].to_i > stamp end # # Opens the specified file for add or edit as appropriate # 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 use the non-tagged P4 instance # here to run the "p4 xxxx -o" 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 # # Method to write form data into the workspace file # 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 end # # Generic spec manager class. Provides the base implementation for the # list_specs() and spec_file() methods and a shorthand for the update() method # class GenSpecMgr < SpecMgr public # # Constructor. Provide the type of spec ("client", "label" etc.), the # client root and tagged and noon-tagged P4 client instances. # def initialize( type, root, p4t, p4u ) @type = type super( root, p4t, p4u ) end # # Shorthand wrapper around SpecMgr#update() # def update( since ) super( @type, since ) end protected # # Generate a list of all specs of the current type. The default # implementation just appends an "s" to the type name and uses that # as a P4 command. e.g. "p4 clients" etc. etc. # def list_specs eval( %Q{@p4t.run_#{@type}s} ) end # # Map a spec type and name to a depot path where this spec will be # archived. # def spec_file( spec ) @root + "/#{@type}s/" + type2name( @type, spec ) end # # Provide a description for changelists and for stdout # def description "Archive updated #{@type}s" end end # # GenSpecMgr subclass for Clients. Nothing special needed for clientspecs, # they are very much the generic model. # class ClientMgr < GenSpecMgr # # Constructor. Provide the client root and tagged and non-tagged P4 # client instances. # def initialize( root, p4t, p4u ) super( "client", root, p4t, p4u ) end end # # GenSpecMgr subclass for Labels. Labels differ slightly from the generic # model in that we also archive the file list for the label. # class LabelMgr < GenSpecMgr # # Constructor. Provide the client root and tagged and non-tagged P4 # client instances. # def initialize( root, p4t, p4u ) super( "label", root, p4t, p4u ) end def clean_name( name ) return name.gsub(/[<>:]/, "_") end # # Map a group name into a depot path. Can't use the default implementation # as we don't have a hash available, just a group name # def spec_file( spec ) label = type2name( @type, spec ) cleaned = clean_name(label) if cleaned != label puts "Label #{label} stored as #{cleaned}" end @root + "/#{@type}s/" + cleaned end # # Override SpecMgr#update() so that we can archive the file list as well # as the spec # def update( since ) list_specs.each do |l| if ( changed?( l, since ) ) print "Saving label #{l['label']}\n" save_spec( "label", l ) save_files( l[ "label" ] ) end end submit( description() ) end protected # # Method to save the file list for a label. Appends ".files" to the label # name and saves the output of "p4 files //...@label" there. # def save_files( label ) filename = @root + "/labels/" + clean_name(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 # # GenSpecMgr subclass for Users. Nothing special required. # class UserMgr < GenSpecMgr # # Constructor. Provide the client root, and tagged and non-tagged P4 # client instances # def initialize( root, p4t, p4u ) super( "user", root, p4t, p4u ) end end # GenSpecMgr 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 public # # Constructor. Provide the client root and tagged and non-tagged P4 client # instances # def initialize( root, p4t, p4u ) super( "group", root, p4t, p4u ) end protected # # Groups are always considered to have changed - until proven otherwise # so we override the GenSpecMgr implementation of changed? and always # return true # def changed?( group, since ) true end # # Map a group name into a depot path. Can't use the default implementation # as we don't have a hash available, just a group name # def spec_file( group ) @root + "/#{@type}s/" + group end # # Convert a type and a group name into a group name. Pretty simple really. # def type2name( type, group ) group end end # # GenSpecMgr subclass for Depots. "p4 depots" doesn't support tagged mode so # we have to do some extra work. # class DepotMgr < GenSpecMgr public # # Constructor. Provide the client root and tagged and non-tagged P4 client # instances # def initialize( root, p4t, p4u ) super( "depot", root, p4t, p4u ) end protected # # Depots are always considered to have changed - until proven otherwise # so we just return true here. # def changed?( depot, since ) true end # # Map a depot name to a path in the depot for archiving the spec # def spec_file( depot ) @root + "/#{@type}s/" + depot end # # Convert a type and a depot name into a depot name. Pretty simple really # def type2name( type, depot ) depot end # # Generate a list of depots. Needs custom implementation because # "p4 depots" doesn't support tagged mode. # def list_specs @p4t.run_depots.collect do |rec| rec.split[ 1 ] end end end # # SpecMgr subclass for Branches. As the plural of branch is branchES, this # needs slightly different handling so it's derived from SpecMgr, rather # than GenSpecMgr # class BranchMgr < SpecMgr public # # Main interface method. Nothing special required, just shorthand for # the SpecMgr implementation. # def update( since ) super( "branch", since ) end protected # # Generate a list of branchspecs using "p4 branches" # def list_specs @p4t.run_branches end # # Map a branchspec to a depot path. # def spec_file( spec ) @root + "/branches/" + spec[ "branch" ] end # # Provide the description for changelist and stdout # def description "Archive updated branches" end end # # SpecMgr subclass for Jobs. Because you can have a huge number of # jobs, we use "p4 jobs -e" to grab only the jobs that have changed since # our saved timestamp. We use SpecMgr rather than GenSpecMgr for reasons # I don't really remember. # class JobMgr < SpecMgr public # # Main interface method. Saves each of the modified jobs. # def update( since ) list_specs( since ).each do |spec| save_spec( "job", spec ) end submit( description() ) end protected # # Get a list of jobs modified since the saved timestamp # def list_specs( stamp ) @p4t.run_jobs( "-e", "#{JOB_DATE_FIELD} > #{stamp}" ) end # # Locate the workspace file for a given job # def spec_file( spec ) @root + "/jobs/" + spec[ "Job" ] end # # Provide the description for changelist and stdout. # 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 public # # Main interface. Update the archives for the named object using # the specified label. Note that "label" in this context doesn't # mean a Perforce label, but rather a user friendly description of # what we're archiving. i.e. "protections table" rather than "protect" # 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" ] public # Constructor 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 # # Set the debug level. Useful levels are: # # 0 (default) - No debug output # 1 - Some debug output # 2 - More debug output # # What more could you want? # 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 # # Main interface method. Update all requested specs. # 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 @p4t.errors.each{ |e| puts( e ) } @p4u.errors.each{ |e| puts( e ) } raise end end # # Test whether or not any flags at all have been set. # def have_flags? return ! @flags.empty? end # # Resolve missing methods by looking in the flags hash. This is # used to implement get and set methods for specifying which types # of spec should be archived. If the method name ends in a "?" it's a # getter, if not, it's a setter. # 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 protected # # Initialise the client environment. Check whether or not the client # workspace exists, and if not, create it. Then create any missing # directories and sync to the head revision. # def init_client() begin @p4t.run_info.each do |line| create_client() if ( line['clientName'] =~ /unknown/ ) end init_workspace() @p4t.run_sync rescue @p4t.errors.each { |e| puts( e ) } raise end end # # Initialises the workspace by pre-creating a directory for each spec # type ready for the workspace files to be written into. # 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 # # Automatically create the client workspace if necessary. This # ensures that specsaver.rb is less prone to dodgy client views # than scripts that require the client to be pre-created. # 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 @p4t.errors.each { |e| puts( e ) } raise end end # # Get the value of the SPEC_COUNTER. This is used as a timestamp rather # than a traditional counter. # def counter? c = @p4t.run_counter( SPEC_COUNTER ).shift['value'].to_i return c end # # Update the SPEC_COUNTER with a new value. # def counter=( val ) @p4t.run_counter( SPEC_COUNTER, val ) true end end # # Dump the usage information to stdout and exit. # 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 begin 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 # Ignore flags if being run from scheduled tasks croak_syntax unless ( s.have_flags? ) # s.all if (!s.have_flags?) s.update rescue $stderr.puts( "\nError during script execution:\n\n" ) $stderr.puts( $!.to_s ) $stderr.print( "\nStack Trace:\n\n\t" ) $stderr.print( $!.backtrace.join( "\n\t" ) ) $stderr.puts( "" ) exit(1) end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#6 | 5452 | Robert Cowham | Dump label name more nicely | ||
#5 | 5451 | Robert Cowham | Cope with labels with funny names | ||
#4 | 5450 | Robert Cowham |
Made work with Ruby 1.8 (gsub and other changes). Improved error reporting. |
||
#3 | 3733 | Robert Cowham | Formatting! | ||
#2 | 3732 | Robert Cowham |
Save changes to allow to run as scheduled task under Windows (parameters get diverted somewhere) |
||
#1 | 3731 | Robert Cowham | My copy | ||
//guest/tony_smith/perforce/utils/specsaver.rb | |||||
#11 | 3094 | Tony Smith |
Tweaks to specsaver to (a) tidy up and (b) make it rdoc ready. Docs to follow |
||
#10 | 2740 | Tony Smith |
Followup to change #2678. Make the change description itself simple, but include the list of updated objects in the output of the script - mostly because that's the way I use it. |
||
#9 | 2678 | Tony Smith | Tweak specsaver.rb for clearer descriptions | ||
#8 | 1937 | Tony Smith |
Adapt specsaver.rb to batch up submissions by type. So all the changed userspecs are submitted together, same with groups, branches labels etc. |
||
#7 | 1936 | Tony Smith | Add --debug flag to specsaver so you can see what's going on. | ||
#6 | 1935 | Tony Smith |
Tweak label versioning. This is nicer. |
||
#5 | 1910 | Tony Smith |
Update specsaver.rb to take command line arguments specifying what to version. Syntax is now: 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) |
||
#4 | 1896 | Tony Smith | Some cosmetic tidying up following previous change. | ||
#3 | 1895 | Tony Smith |
Fix typo, and add support for proper versioning of labels. Now not only does it save the label spec, but it also runs a "p4 files //...@label" for each label and saves the file list alongside the spec. |
||
#2 | 1872 | Tony Smith |
Updated version of specsaver.rb. This version adds support for versioning of user and group specs (no, passwords are not included) and contains some general tidying up. Currently only works out of the box on Unix. |
||
#1 | 1871 | Tony Smith |
First release of specsaver.rb a script to version all "specs" in your depot. Currently this means, clients/labels/branches and protections, typemaps and jobspecs. Groups and users to follow. It does not currently handle deletion of these objects. |