Index: tools/examples/svnshell.rb =================================================================== --- tools/examples/svnshell.rb (revision 15032) +++ tools/examples/svnshell.rb (working copy) @@ -1,4 +1,27 @@ #!/usr/bin/env ruby +# +# svnshell.rb : a Ruby-based shell interface for cruising 'round in +# the filesystem. +# +# Usage: ruby svnshell.rb REPOS_PATH, where REPOS_PATH is a path to +# a repository on your local filesystem. +# +# NOTE: This program requires the Ruby readline extension. +# See http://wiki.rubyonrails.com/rails/show/ReadlineLibrary +# for details on how to install readline for Ruby. +# +###################################################################### +# +# Copyright (c) 2000-2005 CollabNet. All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://subversion.tigris.org/license-1.html. +# If newer versions of this license are posted there, you may use a +# newer version instead, at your option. +# +###################################################################### +# require "readline" require "shellwords" @@ -10,8 +33,12 @@ class SvnShell + # A list of potential commands. This list is populated by + # the 'method_added' function (see below). WORDS = [] + # Check for methods that start with "do_" + # and list them as potential commands class << self def method_added(name) if /^do_(.*)$/ =~ name.to_s @@ -19,122 +46,226 @@ end end end - + + # Constructor for SvnShell. + # + # pool: A Subversion memory pool for this object to use + # path: The path to a Subversion repository def initialize(pool, path) @pool = pool @repos_path = path - @in_rev_mode = true - Svn::Core::Pool.new(@pool) do |tmp_pool| - @rev = Svn::Repos.open(@repos_path, tmp_pool).fs.youngest_rev - end + + + # Get the youngest revision in the repository + @rev = get_youngest_rev(); + + # At first, there's no transaction currently active... @txn = nil + + # We're at the root of the repository @path = "/" + + # The shell is currently running @exited = false + end + # Run the shell def run + # While the user hasn't typed 'exit' and there is + # still input to be read... while !@exited and buf = Readline.readline(prompt, true) + + # Parse the command line into a single command and + # arguments cmd, *args = Shellwords.shellwords(buf) - next if /\A\s*\z/ =~ cmd + + # Skip empty lines + next if cmd.nil? or /\A\s*\z/ =~ cmd + + # Open up a new connection to the repository + # and dispatch the command Svn::Core::Pool.new(@pool) do |pool| + + # Open up a new connection to the repo @fs = Svn::Repos.open(@repos_path, pool).fs - if @in_rev_mode + + if @txn.nil? + # If there's no transaction, default to the root + # of the repository at the latest revision @root = @fs.root(@rev) else + # If there is a transaction, continue with it @root = @fs.open_txn(name).root end + + # Dispatch the command dispatch(cmd, *args) + + # Close the connection @root.close end end end - private + # Private functions + private + + # Get the current prompt string def prompt - if @in_rev_mode + + if @txn.nil? + # If there's no transaction in progress, + # work with the latest revision from the repo mode = "rev" info = @rev else + # Continue with the current transaction mode = "txn" info = @txn end + + # Indicate the status of the current transaction + # in the prompt string "<#{mode}: #{info} #{@path}>$ " end + # Get the youngest revision in the repository + def get_youngest_rev() + Svn::Core::Pool.new(@pool) do |tmp_pool| + return Svn::Repos.open(@repos_path, tmp_pool).fs.youngest_rev + end + end + + # Dispatch a command the the appropriate do_* subroutine def dispatch(cmd, *args) + + # If the appropriate do_* function exists... if respond_to?("do_#{cmd}", true) begin + # ... then run it __send__("do_#{cmd}", *args) rescue ArgumentError # puts $!.message # puts $@ - puts("invalid argument for #{cmd}: #{args.join(' ')}") + puts("Invalid argument for #{cmd}: #{args.join(' ')}") end else - puts("unknown command: #{cmd}") + puts("Unknown command '#{cmd}'.") + puts("Try one of these commands: ", WORDS.sort.join(" ")) end end + # Output a file from the repository def do_cat(path) - new_path = parse_path(path) - case @root.check_path(new_path) - when Svn::Core::NODE_NONE - puts "Path '#{new_path}' does not exist." - when Svn::Core::NODE_DIR - puts "Path '#{new_path}' is not a file." - else - @root.file_contents(new_path) do |stream| - puts stream.read(@root.file_length(new_path)) + + # Normalize the path + normalized_path = normalize_path(path) + + # Check what type of node exists at the specified path + case @root.check_path(normalized_path) + when Svn::Core::NODE_NONE + puts "Path '#{normalized_path}' does not exist." + when Svn::Core::NODE_DIR + puts "Path '#{normalized_path}' is not a file." + else + # Output the file to standard out + @root.file_contents(normalized_path) do |stream| + puts stream.read(@root.file_length(normalized_path)) + end end - end end def do_cd(path="/") - new_path = parse_path(path) - if @root.check_path(new_path) == Svn::Core::NODE_DIR - @path = new_path + + # Normalize the path + normalized_path = normalize_path(path) + + # If it's a directory, set the path appropriately + if @root.check_path(normalized_path) == Svn::Core::NODE_DIR + @path = normalized_path else - puts "Path '#{new_path}' is not a valid filesystem directory." + puts "Path '#{normalized_path}' is not a valid filesystem directory." end + end def do_ls(*paths) + # If no path is specified, look at the current directory paths << @path if paths.empty? + + # Foreach path paths.each do |path| - new_path = parse_path(path) - case @root.check_path(new_path) - when Svn::Core::NODE_DIR - parent = new_path - entries = @root.dir_entries(parent) - when Svn::Core::NODE_FILE - parts = path_to_parts(new_path) - name = parts.pop - parent = parts_to_path(parts) - puts "#{parent}:#{name}" - tmp = @root.dir_entries(parent) - if tmp[name].nil? - return + + # Get the normalized path + normalized_path = normalize_path(path) + + # Check what type of node we found + case @root.check_path(normalized_path) + + # If it's a directory, output its contents + when Svn::Core::NODE_DIR + parent = normalized_path + entries = @root.dir_entries(parent) + + # If it's a file, + when Svn::Core::NODE_FILE + + # Split the normalized path into directory and filename components + parts = path_to_parts(normalized_path) + name = parts.pop + parent = parts_to_path(parts) + + # Output the filename + puts "#{parent}:#{name}" + + # Double check that the file exists inside + # its parent's entry + parent_entries = @root.dir_entries(parent) + + if parent_entries[name].nil? + # Hmm. We couldn't find the file inside its parent. + # I wonder why that would happen? + puts "No directory entry found for '#{normalized_path}'" + + # Might as well keep on working + next + + else + # Output the entries found + entries = {name => tmp[name]} + end + else - entries = {name => tmp[name]} + # Couldn't find the file + puts "Path '#{normalized_path}' not found." + + # Might as well keep on printing the found files + next end - else - puts "Path '#{new_path}' not found." - return - end + # Output a directory header puts " REV AUTHOR NODE-REV-ID SIZE DATE NAME" puts "-" * 76 + # Foreach entry... entries.keys.sort.each do |entry| + + # The full filename of the entry fullpath = parent + '/' + entry - size = '' + + # If its a directory, leave the size field empty + # and add a slash to the end if @root.dir?(fullpath) + size = '' name = entry + '/' else + # Get the the size of the file and the entry name size = @root.file_length(fullpath).to_i.to_s name = entry end + # Get the properties of this entry node_id = entries[entry].id.to_s created_rev = @root.node_created_rev(fullpath) author = @fs.prop(Svn::Core::PROP_REVISION_AUTHOR, created_rev).to_s @@ -143,125 +274,186 @@ created_rev, author[0,8], node_id, size, format_date(date), name ] + + # Output the current entry puts "%6s %8s <%10s> %8s %17s %s" % args end end end + # List all currently active transactions def do_lstxns + + # List all active transactions txns = @fs.list_transactions + + # Sort the list of transactions txns.sort + + # Output each transaction counter = 0 txns.each do |txn| counter = counter + 1 puts "%8s " % txn + + # Output an extra newline after every six transactions if counter == 6 puts counter = 0 end end + + # Output an extra newline puts + end - + + # Output the properties of a particular path def do_pcat(path=nil) + + # Default to the current directory catpath = path || @path + + # If the path doesn't exist, don't print anything if @root.check_path(catpath) == Svn::Core::NODE_NONE puts "Path '#{catpath}' does not exist." return end + # Get the properties of the specified path plist = @root.node_proplist(catpath) + + # If there are no properties, don't bother printing anything return if plist.nil? + # Output all the properties of the specified path as key-value pairs plist.each do |key, value| puts "K #{key.size}" puts key puts "P #{value.size}" puts value end + + # That's all, folks. puts 'PROPS-END' end - + + # Set the current revision def do_setrev(rev) + + # Make sure that the specified revision exists begin @fs.root(Integer(rev)).close rescue Svn::Error puts "Error setting the revision to '#{rev}': #{$!.message}" return end + + # Close the current transaction, if any + @txn = nil + + # Set the current revision @rev = Integer(rev) - @in_rev_mode = true - path_landing + + # The current directory might not exist in the context of a different + # revision, so we'll have to just see where we land. + @path = find_landing_path(@path) end + # Open a named transaction def do_settxn(name) - new_root = nil begin + # Open the specified transaction txn = @fs.open_txn(name) txn.root.close rescue Svn::Error puts "Error setting the transaction to '#{name}': #{$!.message}" return end + + # Specify the current transaction @txn = name - @in_rev_mode = false - path_landing + + # The current directory might not exist in the context of the transaction, + # so we'll have to just see where we land. + @path = find_landing_path(@path) end + # Get the youngest revision of the current repository def do_youngest rev = @fs.youngest_rev puts rev end + # Exit the shell def do_exit @exited = true end + # Convert a path into its consituent parts def path_to_parts(path) - path.split(/\/+/) + return path.split(/\/+/) end + # Join the parts of a path together into a long string def parts_to_path(parts) normalized_parts = parts.reject{|part| part.empty?} - "/#{normalized_parts.join('/')}" + return "/#{normalized_parts.join('/')}" end - def parse_path(path) + # Normalize a path by converting it into an absolute path + # and removing any portions that are relative + # (e.g. ".", "..") + def normalize_path(path) + + # Convert into an absolute path if path[0,1] != "/" and @path != "/" path = "#{@path}/#{path}" end + + # Create an array of the normalized parts of the path + normalized_parts = [] + + # Convert the path into its constituent parts parts = path_to_parts(path) - normalized_parts = [] + # Foreach part of the path parts.each do |part| case part - when "." - # ignore - when ".." - normalized_parts.pop - else - normalized_parts << part - end + when "." + # ignore + when ".." + # Get rid of the parent + normalized_parts.pop + else + # Add to the list of normalized parts + normalized_parts << part + end end - parts_to_path(normalized_parts) + + # Return the path as a joined string + return parts_to_path(normalized_parts) end - def path_landing - found = false - new_path = @path - until found - case @root.check_path(new_path) - when Svn::Core::NODE_DIR - found = true - else - parts = path_to_parts(new_path) - parts.pop - new_path = parts_to_path(parts) - end + # Get the directory-name portion of a path + def parent_dir(path) + return normalized_path("${path}/..") + end + + + # Find the directory containing a particular file. + # If the file is already a directory, simply return that directory. + def find_landing_path(path) + # If it's already a directory, return the path as is + if @root.check_path(path) == Svn::Core::NODE_DIR + return path + else + # Try the parent directory + return find_landing_path(parent_dir(path)) end - @path = new_path end + # Format a Subversion date string nicely def format_date(date_str) date = Svn::Util.string_to_time(date_str, @taskpool) date.strftime("%b %d %H:%M(%Z)") @@ -270,14 +462,22 @@ end +# Set up the list of valid commands +# so that tab-completion will work Readline.completion_proc = Proc.new do |word| SvnShell::WORDS.grep(/^#{Regexp.quote(word)}/) end +# If we don't have the right number of arguments, complain +# and output an explanation of how to use this program. if ARGV.size != 1 - puts "#{$0} REPOS_PATH" + puts "Usage: #{$0} REPOS_PATH" exit(1) end + + +# Launch the shell with a new memory pool Svn::Core::Pool.new do |pool| + # Create a new shell and launch it SvnShell.new(pool, ARGV.shift).run end