/******************************************************************************* Copyright (c) 1997-2004, Perforce Software, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTR IBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ /******************************************************************************* * Name : clientuserruby.cc * * Author : Tony Smith <tony@perforce.com> or <tony@smee.org> * * Description : Ruby bindings for the Perforce API. User interface class * for getting Perforce results into Ruby. * ******************************************************************************/ #include <ctype.h> #include <ruby.h> #include "undefdups.h" #include <clientapi.h> #include <spec.h> #include <diff.h> #include "extconf.h" #include "gc_hack.h" #include "p4result.h" #include "p4rubydebug.h" #include "p4mergedata.h" #include "clientuserruby.h" #include "specmgr.h" /******************************************************************************* * ClientUserRuby - the user interface part. Gets responses from the Perforce * server, and converts the data to Ruby form for returning to the caller. ******************************************************************************/ ClientUserRuby::ClientUserRuby() { debug = 0; input = Qnil; mergeData = Qnil; mergeResult = Qnil; } void ClientUserRuby::Reset() { results.Reset(); lastSpecDef.Clear(); // Leave input alone. } void ClientUserRuby::Finished() { // Reset input coz we should be done with it now. Keeping hold of // it just prevents GC from sweeping it if possible if ( P4RDB_CALLS && input != Qnil ) fprintf( stderr, "[P4] Cleaning up saved input\n" ); input = Qnil; } void ClientUserRuby::HandleError( Error *e ) { if( P4RDB_CALLS ) fprintf( stderr, "[P4] HandleError()\n" ); if( P4RDB_DATA ) { StrBuf t; e->Fmt( t, EF_PLAIN ); fprintf( stderr, "... [%s] %s\n", e->FmtSeverity(), t.Text() ); } results.AddError( e ); } void ClientUserRuby::OutputText( const_char *data, int length ) { if( P4RDB_CALLS ) fprintf( stderr, "[P4] OutputText()\n" ); if( P4RDB_DATA ) fprintf( stderr, "... [%d]%*s\n", length, length, data ); results.AddOutput( data ); } void ClientUserRuby::OutputInfo( char level, const_char *data ) { if( P4RDB_CALLS ) fprintf( stderr, "[P4] OutputInfo()\n" ); if( P4RDB_DATA ) fprintf( stderr, "... %s\n", data ); results.AddOutput( data ); } void ClientUserRuby::OutputBinary( const_char *data, int length ) { if( P4RDB_CALLS ) fprintf( stderr, "[P4] OutputBinary()\n" ); if( P4RDB_DATA ) { for( int l = 0; l < length; l++ ) { if( l % 16 == 0 ) fprintf( stderr, "%s... ", l ? "\n" : "" ); fprintf( stderr, "%#hhx ", data[ l ] ); } } // // Binary is just stored in a string. Since the char * version of // P4Result::AddOutput() assumes it can strlen() to find the length, // we'll make the String object here. // results.AddOutput( rb_str_new( data, length) ); } void ClientUserRuby::OutputStat( StrDict *values ) { SpecMgr m; StrPtr * spec = values->GetVar( "specdef" ); StrPtr * data = values->GetVar( "data" ); StrPtr * sf = values->GetVar( "specFormatted" ); StrDict * dict = values; SpecDataTable specData; Error e; // // Determine whether or not the data we've got contains a spec in one form // or another. 2000.1 -> 2005.1 servers supplied the form in a data variable // and we use the spec variable to parse the form. 2005.2 and later servers // supply the spec ready-parsed but set the 'specFormatted' variable to tell // the client what's going on. Either way, we need the specdef variable set // to enable spec parsing. // int isspec = spec && ( sf || data ); // Propagate our debug level m.SetDebug( debug ); // // Save the spec definition for later retrieval by P4ClientApi // if( spec ) lastSpecDef = spec->Text(); // // Parse any form supplied in the 'data' variable and convert it into a // dictionary. // if( spec && data ) { // 2000.1 -> 2005.1 server's handle tagged form output by supplying the form // as text in the 'data' variable. We need to convert it to a dictionary // using the supplied spec. if( P4RDB_CALLS ) fprintf( stderr, "[P4] OutputStat() - parsing form\n" ); // Parse the form. Use the ParseNoValid() interface to prevent // errors caused by the use of invalid defaults for select items in // jobspecs. #if P4APIVER_ID >= 513538 Spec s( spec->Text(), "", &e ); #else Spec s( spec->Text(), "" ); #endif if( !e.Test() ) s.ParseNoValid( data->Text(), &specData, &e ); if( e.Test() ) { HandleError( &e ); return; } dict = specData.Dict(); } // // If what we've got is a parsed form, then we'll convert it to a P4::Spec // object. Otherwise it's a plain hash. // if( isspec ) { if( P4RDB_CALLS ) fprintf(stderr ,"[P4] OutputStat() - Converting to P4::Spec object\n"); results.AddOutput( m.DictToSpec( dict, spec ) ); } else { if( P4RDB_CALLS ) fprintf(stderr ,"[P4] OutputStat() - Converting to hash\n"); results.AddOutput( m.DictToHash( dict ) ); } } /* * Diff support for Ruby API. Since the Diff class only writes its output * to files, we run the requested diff putting the output into a temporary * file. Then we read the file in and add its contents line by line to the * results. */ void ClientUserRuby::Diff( FileSys *f1, FileSys *f2, int doPage, char *diffFlags, Error *e ) { if ( P4RDB_CALLS ) fprintf( stderr, "[P4] Diff() - comparing files\n" ); // // Duck binary files. Much the same as ClientUser::Diff, we just // put the output into Ruby space rather than stdout. // if( !f1->IsTextual() || !f2->IsTextual() ) { if ( f1->Compare( f2, e ) ) results.AddOutput( "(... files differ ...)" ); return; } // Time to diff the two text files. Need to ensure that the // files are in binary mode, so we have to create new FileSys // objects to do this. FileSys *f1_bin = FileSys::Create( FST_BINARY ); FileSys *f2_bin = FileSys::Create( FST_BINARY ); FileSys *t = FileSys::CreateGlobalTemp( f1->GetType() ); f1_bin->Set( f1->Name() ); f2_bin->Set( f2->Name() ); { // // In its own block to make sure that the diff object is deleted // before we delete the FileSys objects. // #ifndef OS_NEXT :: #endif Diff d; d.SetInput( f1_bin, f2_bin, diffFlags, e ); if ( ! e->Test() ) d.SetOutput( t->Name(), e ); if ( ! e->Test() ) d.DiffWithFlags( diffFlags ); d.CloseOutput( e ); // OK, now we have the diff output, read it in and add it to // the output. if ( ! e->Test() ) t->Open( FOM_READ, e ); if ( ! e->Test() ) { StrBuf b; while( t->ReadLine( &b, e ) ) results.AddOutput( b.Text() ); } } delete t; delete f1_bin; delete f2_bin; if ( e->Test() ) HandleError( e ); } /* * convert input from the user into a form digestible to Perforce. This * involves either (a) converting any supplied hash to a Perforce form, or * (b) running to_s on whatever we were given. */ void ClientUserRuby::InputData( StrBuf *strbuf, Error *e ) { if ( P4RDB_CALLS ) fprintf( stderr, "[P4] InputData(). Using supplied input\n" ); VALUE inval = input; if( Qtrue == rb_obj_is_kind_of( input, rb_cArray ) ) { inval = rb_ary_shift( input ); } if( Qnil == inval ) { rb_warn( "[P4] Expected user input, found none. " "Missing call to P4#input()?" ); return; } if ( Qtrue == rb_obj_is_kind_of( inval, rb_cHash ) ) { SpecMgr m; StrPtr * specDef = varList->GetVar( "specdef" ); m.SetDebug( debug ); m.HashToText( inval, strbuf, specDef, e ); return; } // Convert whatever's left into a string ID to_s = rb_intern( "to_s" ); VALUE str = rb_funcall( inval, to_s, 0 ); strbuf->Set( STR2CSTR( str ) ); } /* * In a script we don't really want the user to see a prompt, so we * (ab)use the SetInput() function to allow the caller to supply the * answer before the question is asked. */ void ClientUserRuby::Prompt( const StrPtr &msg, StrBuf &rsp, int noEcho, Error *e ) { if ( P4RDB_CALLS ) fprintf( stderr, "[P4] Prompt(): %s\n", msg.Text() ); InputData( &rsp, e ); } /* * Do a resolve. We implement a resolve by calling a block. */ int ClientUserRuby::Resolve( ClientMerge *m, Error *e ) { // // If no block has been passed, default to using the merger's resolve // if( ! rb_block_given_p() ) return m->Resolve( e ); // // First detect what the merger thinks the result ought to be // StrBuf t; MergeStatus autoMerge = m->AutoResolve( CMF_FORCE ); // Now convert that to a string; switch( autoMerge ) { case CMS_QUIT: t = "q"; break; case CMS_SKIP: t = "s"; break; case CMS_MERGED: t = "am"; break; case CMS_EDIT: t = "e"; break; case CMS_YOURS: t = "ay"; break; case CMS_THEIRS: t = "at"; break; } mergeData = MkMergeInfo( m, t ); VALUE r; StrBuf reply; for( int loop=0 ; loop < 10 ; loop++ ) { int excepted; // // Call the block using rb_protect to make sure that if the // block raises any exceptions we trap them here. We don't want // some random longjmp() trashing the Perforce connection. If an // exception is raised, we'll abort the merge. // r = rb_protect( P4RUBY_FUNC_CAST(rb_yield), mergeData, &excepted ); if( excepted ) return CMS_QUIT; reply = STR2CSTR( r ); if( reply == "ay" ) return CMS_YOURS; else if( reply == "at" ) return CMS_THEIRS; else if( reply == "am" ) return CMS_MERGED; else if( reply == "ae" ) return CMS_EDIT; else if( reply == "s" ) return CMS_SKIP; else if( reply == "q" ) return CMS_QUIT; else rb_warn( "Invalid 'p4 resolve' response." ); } rb_warn( "Aborting resolve after 10 attempts" ); return CMS_QUIT; } /* * Accept input from Ruby and convert to a StrBuf for Perforce * purposes. We just save what we're given here because we may not * have the specdef available to parse it with at this time. */ VALUE ClientUserRuby::SetInput( VALUE i ) { if ( P4RDB_CALLS ) fprintf( stderr, "[P4] SetInput()\n" ); input = i; return Qtrue; } VALUE ClientUserRuby::MkMergeInfo( ClientMerge *m, StrPtr &hint ) { ID idP4 = rb_intern( "P4" ); ID idP4M = rb_intern( "MergeData" ); VALUE cP4 = rb_const_get_at( rb_cObject, idP4 ); VALUE cP4M = rb_const_get_at( cP4, idP4M ); P4MergeData *d = new P4MergeData( this, m, hint ); return d->Wrap( cP4M ); } // // GC support // void ClientUserRuby::GCMark() { if( P4RDB_GC ) fprintf( stderr, "[P4] Marking results and errors for garbage collection\n" ); if( input != Qnil ) rb_gc_mark( input ); if( mergeData != Qnil ) rb_gc_mark( mergeData ); if( mergeResult != Qnil ) rb_gc_mark( mergeResult ); results.GCMark(); }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#23 | 5953 | Tony Smith |
Bug fix: P4Ruby could crash attempting to parse a form when tagged mode is enabled, but form parsing was not. This only occurred when used against a 2005.2 or later server. |
||
#22 | 5800 | Tony Smith |
Bug fix: P4Ruby commands could segfault on garbage collection due to uninitialized values. This change initializes them. |
||
#21 | 5793 | Tony Smith |
Back-port previous change to Ruby 1.6. Unfortunately the prototype for rb_protect() changed between 1.6 and 1.8, so the calling code needs some special handling. A subsequent change will update the distribution and version as I'm submitting this from Windows and Cygwin's tar isn't up to the task of building the source tarball properly. |
||
#20 | 5792 | Tony Smith | Update P4Ruby to support 2006.2 beta API. | ||
#19 | 5791 | Tony Smith |
Add experimental support for passing a block to P4#run_resolve. The block is passed a P4::MergeData object encapsulating the context of each merge performed. The block should evaluate to a string indicating the desired result of the merge: 'ay', 'at', 'am', 's', etc. The P4::MergeData object contains information about the files involved in the merge and can invoke an external merge tool. This is still experimental at this stage so the interface may change as it evolves. |
||
#18 | 5693 | Tony Smith |
Update p4conf.rb to define const_char on all platforms if building against a 2006.1 or later API. Also squelched some compiler noise. No functional change |
||
#17 | 5279 | Tony Smith |
Bug fix: Change 5258 (changes for 2005.2 API), broke the creation of P4::Spec objects for parsed forms. This change reworks the logic for form parsing in a way that should be compatible with all server versions (from about 2000.1 onwards) |
||
#16 | 5258 | Tony Smith |
Adapt P4Ruby for 2005.2 API changes. The 2005.2 API supplies forms ready-parsed in tagged mode and in general P4Ruby worked with it. The only issue was that P4Ruby wasn't caching the specdefs properly so conversions in the reverse direction were broken. This change ensures that we cache the specdef if we have it regardless of the API level. Squelched on a compiler warning too while I was there. |
||
#15 | 5222 | Tony Smith |
Improve debug output in P4Ruby: p4.debug = 1 * Show commands being executed p4.debug = 2 * Show function calls p4.debug = 3 * Show data p4.debug = 4 * Show ruby garbage collection calls. Debug levels are cumulative as you'd expect. |
||
#14 | 4753 | Tony Smith |
Add support for executing commands which prompt the user for input more than once during their execution. A perfect example is 'p4 password' which prompts the user three times. This works by allowing P4#input() to take an array argument. Each time Perforce prompts the user (by calling ClientUserRuby::Prompt()), the array is shifted by one and the first value in the array is passed to Perforce. Thus, to change your password a three-element array is needed comprising of your old password, and the new password twice. To make this a little easier on the eye, this change also includes a thin wrapper called P4#run_password() which takes simply the old password and the new password and constructs a suitable input array. This change also includes docs for the above, and docs for P4#run_filelog() which were found to be missing. |
||
#13 | 4680 | Tony Smith |
Make P4Ruby return new P4::Spec objects instead of plain old hashes when parse_forms mode is in use. A P4::Spec object is derived from Hash so should be backwards compatible with previous code. P4::Spec provides limited fieldname validation on forms and accessor methods for quick and easy access to the fields in the form. The accessor methods are all prefixed with '_' to avoid colliding with methods from the Hash parent class. This is a little ugly, but deriving from hash is a big win, so it's worth it. This change also fixes a minor bug found along the way. Spec parsing and formatting wouldn't work with labels, branches, depots and groups unless you'd previously run a P4::fetch_label( <label> ), P4::fetch_branch( <branch> ) etc. etc. This is because the spec parsing code internally runs one of these commands in order to grab the specdef from the server but it wasn't providing a spec name. i.e. it was using 'p4 client -o' and assuming that this would work for other types of spec too. It does, but not for all spec types. So, now the spec parsing code will use a bogus name for the spec types that require it. |
||
#12 | 4651 | Tony Smith |
Add format_spec() method and format_* shortcuts to make it easy to convert a spec in a hash back to its string form without sending it to the server. |
||
#11 | 4636 | Tony Smith |
Bug fix: P4Ruby was segfaulting on 'p4 diff2' due to the use of 'depotFile' and 'depotFile2' variable names. Normally a variable suffixed by a number occurs when there are multiple results for a command. i.e. 'p4 -Ztag filelog' on a file with multiple revisions and/or integrations. In these cases you'll see: rev0, rev1, rev2, rev3,... action0, action1, ... and in these cases P4Ruby puts all the 'rev*' elements into an array. But in the case of diff2, there's no 'depotFile0' so P4Ruby was trying to insert depotFile2 into an array that wasn't an array. With this change, if P4Ruby finds that a name has already been defined, and the value it holds is not an array then it will simply use the name it's been given - including the numerical suffix. |
||
#10 | 4593 | Tony Smith |
Add support for 'p4 login' to P4Ruby per request from Robert Cowham. New installer to follow. |
||
#9 | 4261 | Tony Smith |
Add support for parsing arbitrary specs from strings in Ruby space. Useful with spec depots. You might obtain the spec by running a "p4 print -q" against a file in a spec depot, but want to parse it into a Ruby hash. i.e. p4 = P4.new p4.parse_forms # Required! p4.connect buf = p4.run_print( "-q", "//specs/client/myclient" ) spec = p4.parse_client( buf ) # Or equivalently spec = p4.parse_spec( "client", buf ) |
||
#8 | 4157 | Tony Smith |
Copyright notice update. No functional change |
||
#7 | 2593 | Tony Smith |
Bug fix. Form parsing failed when attempting to parse forms containing invalid select field values. Specifically a jobspec that initialised select fields to invalid values meant that you could not create a new job easily using P4Ruby. This fix ensures that the client makes no attempt to validate the form data sent by the server. It's the server's job to do the validation. |
||
#6 | 2409 | Tony Smith |
Port bug fix from P4Perl to P4Ruby. This fixes the problems with tagged mode parsing of fields whose names contain embedded numbers like P4DTI-* fields in jobs. |
||
#5 | 2407 | Tony Smith |
Bug fix (kind of). The reuse of the variable names otherLock and otherOpen in the output of "p4 fstat" was causing P4Ruby to trash the first value of the variable in favour of the second. So for otherOpen you'd get the number of other people who have the file open, but not their names or clients. This change introduces a slightly unpleasant fix which renames the second, unindexed occurrence of the variable (i.e. otherOpen not otherOpen1) by simply appending an "s". So otherOpen becomes otherOpens and otherLock becomes otherLocks. In the case of otherOpen(s) this makes sense because the hash member otherOpen points to an array of user/client combo's for people who have the file open whilst otherOpens is the number of other people who have the file open (i.e h[ "otherOpen" ].length ). In the case of otherLock it makes slightly less sense since otherLock is normally a flag but now it's a (one-element) array containing the name & client of the locking user. otherLocks is now the flag and contains only an empty string as its value. In future it may make more sense to simply drop otherLocks and otherOpens as the array elements give you all the info you need. |
||
#4 | 2210 | Tony Smith |
Bug fix to change 2085 which broke form parsing as the specdef variable is only available within the context of the command being executed and the SetInput() method was trying to use it outside of any command context. That and having a bad condition in the test for its existence was causing a segvio. |
||
#3 | 2086 | Tony Smith |
Add support for capturing the output of "p4 diff" to the Ruby API interface. |
||
#2 | 2085 | Tony Smith |
Get rid of specdef hack in the Ruby API. No need to store it in the hash, just grab it from the RPC buffer when needed. |
||
#1 | 1750 | Tony Smith | Build environment tweaks. | ||
//guest/tony_smith/perforce/API/Ruby/main/clientuserruby.cc | |||||
#9 | 1725 | Tony Smith | Update P4/Ruby with support for 2002.1 API | ||
#8 | 1464 | Tony Smith |
Add support for binary output - mainly for "p4 print binary_file". Binary output is returned in a Ruby string. Note that due to the fragmentation of large messages by the Perforce server, P4#run( "print", "-q", "binfile" ) can return an array of more than one element - each element comprising a chunk of the same file. If you're only printing one file, then you can simply use: p4.run_print( "-q", binfile ).join( "" ) If you're printing more than one file, you'll have to iterate over the result array to find the file markers ( using "-q" is a bad plan in this case ). Each file marker will be in a separate element of the array though. |
||
#7 | 1426 | Tony Smith |
Cleaned up the debug output a little. Introduced some debug levels so you can decide (roughly) what output you want to see. Level 1 shows command execution, connect and disconnect. Level 2 includes Level 1 and also shows the RPC callbacks as they happen. Level 3 includes 1 and 2 and also shows when Ruby garbage collection takes place. Converted all the simple methods of the form P4#meth( arg ) to aliases for P4#meth=. Added P4#debug= to complete the scheme. The P4#meth( arg ) forms are now deprecated. i.e. you should use: p4.user = "tony" and not: p4.user( "tony" ) It's just more Ruby-like. |
||
#6 | 1391 | Tony Smith |
Bug fix. Garbage collection can apparently run at any time (i.e. when you're in C space and not just when you're in Ruby space) and it was occasionally running in between adjacent "delete" and "new" statements when the result set was being reset. This change removes this race condition by making the result member of ClientUserRuby a permanently instantiated variable and extending the P4Result class so that it can reset itself in a way that GC respects. Now the only dynamically allocated C++ object is the top level P4ClientApi object. No functional change. |
||
#5 | 1164 | Tony Smith |
Reworked exception handling (hopefully for the last time) in P4/Ruby. Now exceptions are raised on completion of Perforce commands if any errors or warnings were received as part of executing the command. This change also adds documentation, and indexes the Ruby interface off my main page. Bad form to combine so many changes in one changelist, but it's getting late and I want to get them submitted! |
||
#4 | 1083 | Tony Smith |
Sweeping change to exception handling and garbage collection. Exceptions are no longer raised for errors encoutered during execution of Perforce commands as that was causing processing to abort at the first error when several success messages may have been close behind. Now exceptions are raised for events which are fatal to the execution of commands - such as failure to connect to the Perforce server for example. For other errors, the user must call "p4.errors? " to determine whether or not errors occured and "p4.errors" to get an array of error messages. You can of course then raise exceptions yourself if you want to: begin client = p4.fetch_client if p4.errors? raise P4Exception, "p4 client -o failed" end rescue P4Exception => m puts( m ) p4.errors.each { |e| puts( e ) } end version.h got renamed because it conflicts with ruby's own version.h file. We may need to look in there at some point for ruby's version so I'm getting it out of the way now. Added gc_hack.h to make sure that GC works properly on all platforms now so Ruby shouldn't nuke any objects we're holding now. |
||
#3 | 1081 | Tony Smith |
Debugging and identification support. Adds two new methods: P4#identify() P4#debug( int ) |
||
#2 | 1051 | Tony Smith | Port nested data handling code from Perl API to Ruby API. | ||
#1 | 1015 | Tony Smith |
First cut of Perforce bindings for the Ruby scripting language. Similar functionality to the Perl API stuff, but "rubyfied". Supports error reporting via exceptions, and presents tagged output and parsed forms as hash structures, with nested arrays where required. Still early days so the docs are thin on the ground. See the example.pl for a brief guide. Built with Ruby 1.6.4 on Linux. May still be some memory management issues as the Ruby Garbage Collection API has changed a little since the docs I've got and I've just dodged garbage collection for now. Not indexing this just yet. |