<html> <STYLE type="text/css"> H1 {border-width: 1; border: solid} </STYLE> <title>Scripting Column as of $Date: 2004/08/20 $</title> <h1><center>Monthly Scripting Column for May</center></h1> <table border=0 width="100%"> <tr><td width="70%"> <p>Greetings! This continues a series of bulletins, in which we introduce one of the scripts that will be in a future release. <td> <table border=1 width="100%"> <tr><td><i><font color="blue">Tip: retrieve the data, then look at it.</font></i> </table> </tr> </table> <hr> <h2>Today's script</h2> <p>Back in 1997, there was a thread in the perforce-user mailing list in which customers asked for a script that tells you what files you need to run "p4 add" on. <p>Greg Spencer posted a script called <em>p4unknown</em> that did this. (We like the name.) We've written a variant, below, as an example of P4Ruby; the full source code is <a href="examples/p4unknown.rb">here</a> and at the end of this page. <blockquote> Note for reading any code on this page: <font color="green">Green text</font> is what you'll cut/paste when you make your own P4Ruby script. </blockquote> <p>Comments should indicate the flow: <ol><li> Get a list of files from 'p4 fstat //myclient/...' <li> Get a list from a recursive directory list (we use the one provided in a library function, instead of writing our own or calling the 'find' command - which might not exist on another platform) <li> Compare the two lists. In <a href="http://www.ruby-lang.org">Ruby</a>, the set intersection operations are built-in, so it's easy! </ol> <hr> <h3>First step: Setting up P4 object</h3> <p> If you're using P4Ruby, which is the Perforce hook for Ruby, then you'll need to initialize your Perforce connection: <pre> <font color="green">require "P4" p4 = P4.new p4.port = defaultPort if defaultPort != nil p4.user = defaultUser if defaultUser != nil p4.client = defaultClient if defaultClient != nil p4.tagged p4.parse_forms p4.connect begin ... end p4.disconnect </font> </pre> <p>(There will need to be an <font color="green">end</font> somewhere at the end of your Perforce script, as you see in the example.) <p> Note that we copy this block into most of our P4Ruby programs, setting a default user/port/client in the argument processing. The other calls are handy because they foist the parsing off to someone else: <ol><li> The 'parse_forms' call make it easy to process client specs in the next step,<li>and the 'tagged' call makes it easy to process the fstat output a bit later. </ol> <h3>Second step: retrieving information from a client spec</h3> <p> Note how easy certain things are. "Retrieve a client spec" boils down to this: <pre> <font color="green">cl_spec = p4.fetch_client</font> cl_name = cl_spec['Client'] cl_root = cl_spec['Root'] </pre> <p> If we'd wanted to update it and stash it back into the database, we'd use a call to 'save_client'. <pre> <font color="green">cl_spec = p4.fetch_client</font> cl_name = cl_spec['Client'] cl_spec['Root'] = "/tmp/herman" puts "Setting root of #{cl_name} to #{cl_spec['Root']}" <font color="green">ret = p4.save_client(cl_spec)</font> </pre> <p> <h3>Third step: getting information about what's mapped in</h3> <p> We chose to run: <pre> <font color="green">ret = p4.run_fstat("//#{cl_name}/...")</font> </pre> <p> This, in turn, runs "p4 fstat //myclient/...". <p> You might think, <em>why not just run "p4 have" on every file we find in the workspace?</em> For performance reasons, we choose not to: we'll poll the database once to retrieve data, and then look at data separately. That will save the expense of building/running many similar, small queries. (Those small queries would in turn poll the database individually.) <p> It turns out that "p4 fstat" returns the local pathname as one of the columns/fields in its -Ztag output, which Python and P4Ruby/P4Perl users see in a hash/dict/associative array. Although "p4 fstat" is not a trivial command, it will still be less expensive to call a single time, than other commands ("have" and "opened" for every possible file). <em>Large sites should always examine this closely; it's often worth the ounce of investigation, or a note to <a href="mailto:support@perforce.com">Tech Support</a>, to verify such assumptions.</em> <p> There's a way to specify pathnames, that describes all the files in the client workspace. It's "//myclient/...", and it includes those mapped onto my workspace (but not sync'ed) and those opened for add (but not yet submitted). This is a tidy way to get specific information about the files mapped to your workspace, without showing clutter that wouldn't be mapped to the local area anyhow. Hence, "p4 fstat //myclient/..." provides a local pathname for every file that was mapped into the local area. (Aside: The Tech Support folk that I consulted were happy to help, and pointed out that the //myclient/... version of the syntax helped optimize some database accesses.) <p> The results included all files, including those that had been officially deleted, so I added a bit of follow-up to remove that specific case: <pre> <font color="green">ret = p4.run_fstat("//#{cl_name}/...").delete_if { |r| r['headAction'] == 'delete' }</font> </pre> <h3>Fourth step: Figuring out what's on the disk</h3> <p> This really has nothing to do with P4Ruby, just with scripting. We needed a recursive directory list, and used the library functions to get it: <pre> allFilesPresent = [] Find.find(cl_root) do |f| Find.prune if f == "." || f == ".." allFilesPresent << f if File.stat(f).file? end </pre> <p> The rule applies: <em>always use library functions</em>. Writing the code to do this will usually be nastier and more problematic. <h3>Last step: Home free!</h3> <p> From there to the end, it's just a Ruby program. I invite you to look through the rest: it's just grabbing information from two sources, and the set intersection ("puts This - That") make it easy. <hr> Reminder: <font color="green">Green text</font> for P4Ruby hooks. <hr> <pre> # <font color="green"> # num of calls to 'p4': 2 </font> # status: tested on Darwin Mac OS X using P4Ruby API # <font color="green">require "P4"</font> require 'getoptlong' require "find" verboseOption = false defaultPort = nil defaultUser = nil defaultClient = nil options = GetoptLong.new( [ '--verbose', '-v', GetoptLong::OPTIONAL_ARGUMENT], [ '--user', '-u', GetoptLong::REQUIRED_ARGUMENT], [ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT], [ '--client', '-c', GetoptLong::REQUIRED_ARGUMENT], [ '--help', '-h', GetoptLong::REQUIRED_ARGUMENT], [ '--quiet', '-q', GetoptLong::REQUIRED_ARGUMENT] ) options.each do |opt, arg| case opt when "--verbose" verboseOption = true when "--user" defaultUser = arg when "--client" defaultClient = arg when "--port" defaultPort = arg when "--quiet" puts "'--quiet' not implemented yet." when "--help" puts options.Usage end end <font color="green"> p4 = P4.new p4.port = defaultPort if defaultPort != nil p4.user = defaultUser if defaultUser != nil p4.client = defaultClient if defaultClient != nil p4.tagged p4.parse_forms p4.connect begin #----------------------------------------------------------- # first call to P4: 'p4 client -o' #----------------------------------------------------------- cl_spec = p4.fetch_client cl_name = cl_spec['Client'] cl_root = cl_spec['Root'] #----------------------------------------------------------- # second call to P4: 'p4 fstat //myclient/...' #----------------------------------------------------------- ret = p4.run_fstat("//#{cl_name}/...").delete_if { |r| r['headAction'] == 'delete' } </font> # # at this point, we create two arrays to hold # the filenames: # allFilesPerforce - from "p4 fstat //myclient/..." # allFilesPresent - from "Find.find(cl_root)" # we can use set operations for the tricky stuff, and # it's a great advert for Ruby. # # (note that we map the path-separator to be '/', regardless # of platform. Ruby's polite about using '/' everywhere; the # output of "p4 fstat" uses '\' for Windows.) # allFilesPerforce = ret.collect { |r| r['clientFile'].tr('\\', '/') } allFilesPresent = [] Find.find(cl_root) do |f| Find.prune if f == "." || f == ".." allFilesPresent << f if File.stat(f).file? end puts "List of files present in workspace, but unknown to Perforce:" puts (allFilesPresent - allFilesPerforce) puts "List of files known to Perforce, but not (yet) sync'ed to workspace:" puts (allFilesPerforce - allFilesPresent) <font color="green"> rescue P4Exception p4.errors.each { |e| $stderr.puts( e ) } raise end p4.disconnect </font> </pre> <hr> <small><p>Note: all the programs shown in these columns have been written four times: in Perl, in P4Perl, in Python, and in P4Ruby. Look into the Perforce example database for the other versions.</small> <i><small>$Id: //guest/jeff_bowles/scripts/0530ruby.html#1 $</small> <br>© 2004 Perforce Corporation, Inc.</small></i> </html>
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 4420 | Jeff Bowles |
adding the beginnings of example "columns" for scripting examples. |