specsaver.rb #6

  • //
  • guest/
  • robert_cowham/
  • perforce/
  • utils/
  • specsaver.rb
  • View
  • Commits
  • Open Download .zip Download (25 KB)
#!/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.