0530ruby.html #1

  • //
  • guest/
  • jeff_bowles/
  • scripts/
  • 0530ruby.html
  • View
  • Commits
  • Open Download .zip Download (9 KB)
<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. &quot;Retrieve a client spec&quot;
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 &quot;p4 fstat //myclient/...&quot;.
<p>
You might think, <em>why not just run &quot;p4 have&quot; 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 &quot;p4 fstat&quot; 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 &quot;p4 fstat&quot; is not a trivial command, it will
still be less expensive to call a single time, than other commands
(&quot;have&quot; and &quot;opened&quot; 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 &quot;//myclient/...&quot;,
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,
&quot;p4 fstat //myclient/...&quot;
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>&copy; 2004 Perforce Corporation, Inc.</small></i>
</html>
# Change User Description Committed
#1 4420 Jeff Bowles adding the beginnings of example "columns" for
    scripting examples.