#****************************************************************************** #* Classes implementing a generic database model on top of plain text files #* stored in Perforce. #* #* Every record consists of fields/values and may have associated files. The #* data for each record is stored as both a job and a text file. The text #* file is always called <id>/meta and any associated files are also #* stored alongside the record file. i.e. <id>/file1 <id>/file2. #* #* Each table name is a folder name and all records for that table are stored #* in text files under that folder. i.e. <table>/<id>/record #* #* All column names must also be fields in the jobspec so you can search for #* records using "p4 jobs -e". NOTE: When creating your jobspec, make sure #* that all field names are in lowercase. Ruby likes method names to be #* lowercase and it fits better as well as reducing scope for errors. The #* one exception to this is the 'Job' field which Perforce doesn't like being #* renamed... #* #* The job version of any record contains only the latest version of that #* record for searching purposes. #* #* WARNING: This software is designed to have its OWN Perforce server to #* work with. DO NOT USE IT AGAINST AN EXISTING PERFORCE SERVER. #* #****************************************************************************** require "P4" #******************************************************************************* #* START OF CONFIGURATION SECTION #******************************************************************************* #------------------------------------------------------------------------------- #* Directory separator string for your platform #------------------------------------------------------------------------------- if ( RUBY_PLATFORM =~ /win32/i ) SEP = "\\" NL = "\r\n" else SEP = "/" NL = "\n" end #------------------------------------------------------------------------------- #* Specifies in depot syntax the depot path that is to be treated as the #* top of the database. This path MUST be mapped to the client root. #------------------------------------------------------------------------------- DEPOT_ROOT_PATH = "//depot" #******************************************************************************* #* END OF CONFIGURATION SECTION #******************************************************************************* #******************************************************************************* #* Class for creating and formatting record id's. Normally these are just #* of the form "<tablename>/<nnnnnn>" #******************************************************************************* class P4RecId #--------------------------------------------------------------------------- # Class methods. #--------------------------------------------------------------------------- #--------------------------------------------------------------------------- # Get the next ID for the specified table #--------------------------------------------------------------------------- def P4RecId.next( p4, table ) val = p4.run_counter( table.name ).shift.to_i val += 1 p4.run_counter( table.name, val ) P4RecId.new( table.name, val ) end def P4RecId.new_from_job( p4, job ) ( table, seq ) = job.split( "/" ) P4RecId.new( table, seq ) end #--------------------------------------------------------------------------- # Instance methods #--------------------------------------------------------------------------- def initialize( tablename, seq ) @tablename = tablename if ( seq.kind_of?( String ) ) if ( seq =~ /^0*([1-9]\d+)$/ ) @seq = $1.to_i else @seq = seq.to_i end else @seq = seq end end attr_accessor :tablename, :seq #--------------------------------------------------------------------------- # Format the sequence number for printing #--------------------------------------------------------------------------- def seq_str sprintf( "%06d", @seq ) end #--------------------------------------------------------------------------- # String representation of an id #--------------------------------------------------------------------------- def to_s @tablename + "/" + seq_str() end end #******************************************************************************* # Class for representing files that are versioned in the depot. #******************************************************************************* class P4RecFile def initialize( p4, name, rec ) @p4 = p4 @name = name @rec = rec @exists = false @depot_path = nil @ws_path = nil end attr_reader :p4, :name, :rec #--------------------------------------------------------------------------- # Convert the depot path into a client path. Obviously this precludes # complex client maps #--------------------------------------------------------------------------- def ws_path return @ws_path if ( @ws_path ) root = p4.fetch_client[ "Root" ] path = depot_path.sub( DEPOT_ROOT_PATH, root ) @ws_path = path.gsub!( "/", SEP ) end #--------------------------------------------------------------------------- # Explicitly set the depot path for a file. Needed to allow old records # to be found in their old locations even if the storage map has since # changed. #--------------------------------------------------------------------------- def depot_path=( path ) @depot_path = path end #--------------------------------------------------------------------------- # Compute the depot path based on the table storage map and the id #--------------------------------------------------------------------------- def depot_path return @depot_path if @depot_path table_path = @rec.table.storage_map( @rec ).join( "/" ) @depot_path = [ DEPOT_ROOT_PATH, table_path, @rec.id.seq_str, @name ].join( "/" ) end #--------------------------------------------------------------------------- # Does the file exist? #--------------------------------------------------------------------------- def exists? @exists end #--------------------------------------------------------------------------- # The file definitely does exist #--------------------------------------------------------------------------- def exists=( bool ) @exists = bool end #--------------------------------------------------------------------------- # Open this file for add #--------------------------------------------------------------------------- def add if ( exists? ) raise( RuntimeError, "Can't open existing file for add", caller ) end p4.run_add( ws_path() ) end #--------------------------------------------------------------------------- # Open this file for edit #--------------------------------------------------------------------------- def edit if ( ! exists? ) raise( RuntimeError,"Can't open non-existent file for edit", caller) end p4.run_sync( depot_path() ) p4.run_edit( depot_path() ) end #--------------------------------------------------------------------------- # Open this file for delete #--------------------------------------------------------------------------- def delete if ( ! exists? ) raise( RuntimeError, "Can't delete non-existent file", caller ) end p4.run_sync( depot_path() ) p4.run_delete( depot_path() ) end #--------------------------------------------------------------------------- # Read the contents of the file (returns a String) #--------------------------------------------------------------------------- def read p4.run_sync( depot_path() ) f = File.open( ws_path, "r" ) buf = f.read f.close return buf end #--------------------------------------------------------------------------- # Write the contents of the file #--------------------------------------------------------------------------- def write( string ) p4.run_sync( depot_path() ) mkdir() File.open( ws_path, "w" ) { |f| f.write( string ) } end #--------------------------------------------------------------------------- # Create the directory for the workspace file #--------------------------------------------------------------------------- def mkdir dirs = ws_path.split( SEP ) dirs.pop dir = "" dirs.each do |d| dir += d + SEP Dir.mkdir( dir ) unless File.directory?( dir ) end end #--------------------------------------------------------------------------- # Render the filename as a string (we use depot syntax) #--------------------------------------------------------------------------- def to_s depot_file end end #******************************************************************************* #* Main record manipulation class. Handles the loading and saving of record #* as both jobs and files. #******************************************************************************* class P4Record #*************************************************************************** # Class methods #*************************************************************************** #--------------------------------------------------------------------------- # Load a record from its associated job #--------------------------------------------------------------------------- def P4Record.load( p4, table, id ) if ( ! P4Record.exists?( p4, id ) ) raise( RuntimeError, "Record #{id.to_s} does not exist", caller ) end rec = P4Record.new( p4, table, id ) rec.spec = p4.fetch_job( id.to_s ) if rec.spec.has_key?( "files" ) rec.spec[ "files" ].each do |file| name = file.sub( /.*\//, "" ).chomp f = P4RecFile.new( p4, name, rec ) f.exists = true f.depot_path = file.chomp rec.files[ name ] = f end end rec.exists = true return rec end #--------------------------------------------------------------------------- # Load all records matching a query expression #--------------------------------------------------------------------------- def P4Record.query( p4, table, expr ) expr = "job=#{table} & ( " + expr + " )" p4.run_jobs( "-e", expr ).collect do |job| rec = P4Record.new(p4, table, P4RecId.new_from_job( p4,job["Job"])) rec.spec = job rec.spec[ "files" ].each do |file| name = file.sub( /.*\//, "" ).chomp f = P4RecFile.new( p4, name, rec.id ) f.exists = true f.depot_path = file rec.files[ name ] = f end rec.exists = true rec end end #--------------------------------------------------------------------------- # Create a new record in the given table #--------------------------------------------------------------------------- def P4Record.create( p4, table ) id = P4RecId.next( p4, table ) rec = P4Record.new( p4, table, id ) rec.spec = p4.fetch_job( id.to_s ) return rec end #--------------------------------------------------------------------------- # Test for record existence #--------------------------------------------------------------------------- def P4Record.exists?( p4, id ) jobs = p4.run_jobs( "-e", "job=" + id.to_s ) return jobs.length > 0 end #--------------------------------------------------------------------------- # Constructor: DON'T USE NEW DIRECTLY, CALL create/load. #--------------------------------------------------------------------------- def initialize( p4, table, id ) @p4 = p4 @id = id @seq = id.seq @table = table @spec = nil @exists = false @files = Hash.new @files[ "meta" ] = P4RecFile.new( @p4, "meta", self ) end #*************************************************************************** # Instance methods #*************************************************************************** attr_accessor :p4, :id, :table, :seq, :meta, :spec attr_reader :files #--------------------------------------------------------------------------- # Does the record exist? #--------------------------------------------------------------------------- def exists? return @exists end #--------------------------------------------------------------------------- # The record definitely does exist #--------------------------------------------------------------------------- def exists=( bool ) @exists = bool end #--------------------------------------------------------------------------- # Get a file handle by name (not the depot path) #--------------------------------------------------------------------------- def get_file( name ) return @files[ name ] if @files.has_key?( name ) raise( RuntimeError, "Record contains no file called #{name}", caller ) end #--------------------------------------------------------------------------- # Get the meta file #--------------------------------------------------------------------------- def metafile get_file( "meta" ) end #--------------------------------------------------------------------------- # Iterate over the files in the record #--------------------------------------------------------------------------- def each_file @files.each_value { |f| yield( f ) } end #--------------------------------------------------------------------------- # Add a new file attachment to this record #--------------------------------------------------------------------------- def add_file( name ) if ( name == "meta" ) raise( RuntimeError, "The meta file already exists.", caller ) end nfile = P4RecFile.new( p4, name, self ) nfile.add @files[ "name" ] = nfile end #--------------------------------------------------------------------------- # Remove a file attachment #--------------------------------------------------------------------------- def rm_file( file ) if ( file == "meta" ) raise( RuntimeError, "You can't delete the meta file", caller ) end f = get_file( file ) f.delete @files.delete( file ) end #--------------------------------------------------------------------------- # Abandon all edits to this record #--------------------------------------------------------------------------- def abandon each_file do |f| fs = p4.run_fstat( f.ws_path ) if ( fs && fs[ "action" ] == "add" ) File.unlink( f.ws_path ) end p4.run_revert( f.depot_path ) end end #--------------------------------------------------------------------------- # Get the list of files attached to this record in depot syntax #--------------------------------------------------------------------------- def file_list @files.values.collect do |f| f.depot_path end.compact.join( "\n" ) end #--------------------------------------------------------------------------- # Save changes #--------------------------------------------------------------------------- def save( desc ) # First rewrite the files list in case it's been modified @spec[ "files" ] = file_list # Next update the job with the values in the spec p4.save_job( @spec ) # Now update the metafile with the job -o output if ( exists? ) metafile.edit else metafile.add end metafile.write( p4.format_job( @spec ) ) # Now submit change = p4.fetch_change change[ "Description" ] = desc p4.run_submit( change ) @exists = true end #--------------------------------------------------------------------------- # Delete a record #--------------------------------------------------------------------------- def delete( desc ) if ( ! exists? ) raise( RuntimeError, "Can't delete. Record doesn't exist.", caller ) end @files.values.each { |f| f.delete } change = p4.fetch_change change[ "Description" ] = desc p4.run_submit( change ) p4.run_job( "-d", @spec[ "Job" ] ) @files = Hash.new @spec[ "files" ] = Array.new @exists = false end #--------------------------------------------------------------------------- # Obliterate a record. #--------------------------------------------------------------------------- def obliterate if ( ! exists? ) raise( RuntimeError, "Can't oblit. Record doesn't exist.", caller ) end args = @files.values.collect { |f| f.depot_path } p4.at_exception_level( P4::RAISE_ERRORS ) do p4.run_sync( args.collect { |a| a += "#none" } ) end p4.run_obliterate( "-y", args ) p4.run_job( "-d", @spec[ "Job" ] ) @spec[ "files" ] = Array.new @files = Hash.new @exists = false end #--------------------------------------------------------------------------- # Allow direct access to the fields in the jobspec by making them virtual # method names of the form _<field>() #--------------------------------------------------------------------------- def method_missing( meth, *args ) meth = meth.to_s raise if ( meth[0..0] != "_" ) meth = meth[ 1..-1 ] if ( meth =~ /^(.*)=$/ ) meth = $1 @spec[ meth ] = args.shift elsif ( args.length == 0 && @spec.has_key?( meth ) ) @spec[ meth ] else "" end end end #******************************************************************************* #* Base class for all table classes. #******************************************************************************* class P4Table def initialize( p4, name ) @p4 = p4 @name = name end attr_reader :p4, :name def new_record P4Record.create( p4, self ) end def load_record( seq ) P4Record.load( p4, self, P4RecId.new( @name, seq ) ) end def query( expr ) P4Record.query( p4, name, expr ) end #--------------------------------------------------------------------------- # Return an array of directory components representing the path where # the files for this object should be stored. This allows you to tweak # the directory structure your files are stored in so you don't end up # with 1000's of files in a single directory. Don't edit this method # though. Derive your own class and override it there. #--------------------------------------------------------------------------- def storage_map( rec ) return [ rec.table.name ] end end #******************************************************************************* # Built-in test harness - only executed if this file is the main ruby # script rather than a module being loaded. This script creates a temporary # Perforce repository and client workspace in two subdirectories of the # current working directory. You can safely remove these directories once # the test has been run. #******************************************************************************* if $0 == __FILE__ def init_jobspec( p4 ) js = <<EOS Fields: 101 Job word 32 required 102 status select 10 required 103 owner word 32 required 104 modified_date date 20 always 105 note text 0 optional 106 sender line 32 optional 107 fax_number line 32 optional 108 files text 80 optional 109 date_received date 20 once 110 date_closed date 20 optional Values: status open/closed Presets: status open owner $user modified_date $now date_received $now EOS p4.save_jobspec( js ) end def prepare( root ) deltree( root ) if File.exists?( root ) Dir.mkdir( root ) end def create_client( p4, root ) # root is relative. root = Dir.getwd + SEP + root spec = p4.fetch_client( p4.client ) spec[ "Root" ] = root p4.save_client( spec ) end def deltree( p ) if( File.directory?( p ) ) Dir.entries( p ).each do |ent| next if ( ent == "." ) next if ( ent == ".." ) path = p + SEP + ent deltree( path ) end Dir.delete( p ) else File.unlink( p ) end end puts( "Starting self-test..." ) P4ROOT = "p4table-test" CLIENT_ROOT = "test-client" prepare( P4ROOT ) prepare( CLIENT_ROOT ) p4 = P4.new p4.port = "rsh:p4d -r #{P4ROOT} -i" p4.user = "testuser" p4.exception_level = P4::RAISE_ERRORS p4.api_level = 57 begin p4.connect # Initialize the jobspec init_jobspec( p4 ) # Create the client workspace create_client( p4, CLIENT_ROOT ) # Initialize the tables tab = P4Table.new( p4, "mytab" ) puts( "Creating first test record" ) rec = tab.new_record() rec._sender = "Perforce Software UK" rec._fax_number = "01252 861415" seq = rec.seq a = rec.add_file( "attachment" ) a.write( "This file is attached to the fax from " + rec._sender ) rec.save( "Adding a test record" ) puts( "Creating second test record" ) rec = tab.new_record() rec._sender = "Perforce Software Inc" rec._fax_number = "+1-510-864-5340" rec.save( "Adding another test record" ) puts( "Now searching for matching records" ) tab.query( "sender=Perforce" ).each do |r| printf( "\tMatch: %s from %s\n", r._job, r._sender ) end puts( "Now loading a record by sequence no" ) rec = tab.load_record( seq ) printf( "\t%-10s: %s\n", "Sender", rec._sender ); printf( "\t%-10s: %s\n", "Fax No", rec._fax_number ); a = rec.get_file( "attachment" ) printf( "\tAttachment Contents:\n\t"); puts( a.read ) rescue P4Exception p4.errors.each { |e| $stderr.puts( "P4Error: " + e ) } raise end end # if
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#3 | 6437 | Tony Smith |
Update P4Ruby Library scripts to support Perforce P4Ruby 2007.3 rather than my old public depot P4Ruby. |
||
#2 | 4678 | Tony Smith |
Update p4table.rb to use the new spec parsing and formatting features of P4Ruby. This means we now only need one Perforce client instance to handle specs in both hash form and text form. |
||
#1 | 4677 | Tony Smith |
Move P4table.rb to the P4Ruby library and rename it to p4table.rb instead. There's an update coming so this is just the move. |
||
//guest/tony_smith/perforce/utils/P4table.rb | |||||
#2 | 2396 | Tony Smith |
Add RDoc documentation for P4Table.rb. Very pretty. |
||
#1 | 2393 | Tony Smith |
Publish P4Table Ruby module that allows you to treat a Perforce repository as a sort of relational database with inbuilt versioning of all its records. Very simple, but effective. |