Index: svnshell.rb =================================================================== --- svnshell.rb (revision 15101) +++ 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" @@ -8,10 +31,16 @@ require "svn/core" require "svn/repos" +# SvnShell: a Ruby-based shell interface for cruising 'round in +# the filesystem. 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 @@ -20,6 +49,10 @@ 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 @@ -28,22 +61,44 @@ @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) + + # Skip empty lines next if /\A\s*\z/ =~ cmd.to_s + Svn::Core::Pool.new(@pool) do |pool| + + # Open a new connection to the repo @fs = Svn::Repos.open(@repos_path, pool).fs setup_root + + # Execute the specified command dispatch(cmd, *args) + + # Find a path that exists in the current revision @path = find_available_path + + # Close the connection to the repo @root.close + end end end + # Private functions private + + # Get the current prompt string def prompt + + # Gather data for the prompt string if rev_mode? mode = "rev" info = @rev @@ -51,10 +106,15 @@ mode = "txn" info = @txn end + + # Return the prompt string "<#{mode}: #{info} #{@path}>$ " end + # Dispatch a command to the appropriate do_* subroutine def dispatch(cmd, *args) + + # Dispatch cmd to the appropriate do_* function if respond_to?("do_#{cmd}", true) begin __send__("do_#{cmd}", *args) @@ -69,22 +129,32 @@ end end + # Output a file from the repository def do_cat(path) + + # Normalize the path to an absolute 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 + # Set the current directory def do_cd(path="/") + # Normalize the path to an absolute path normalized_path = normalize_path(path) + + # If it's a valid directory, then set the directory if @root.check_path(normalized_path) == Svn::Core::NODE_DIR @path = normalized_path else @@ -92,44 +162,75 @@ end end + # List the contents of a set of paths. Like `svn ls' def do_ls(*paths) + + # Default to listing the contents of the current directory paths << @path if paths.empty? + + # Foreach path paths.each do |path| + + # Normalize the path to an absolute path normalized_path = normalize_path(path) + + # Is it a directory or file? case @root.check_path(normalized_path) when Svn::Core::NODE_DIR + + # Output the contents of the directory parent = normalized_path entries = @root.dir_entries(parent) + when Svn::Core::NODE_FILE + + # Split the 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 the parent directory parent_entries = @root.dir_entries(parent) if parent_entries[name].nil? + # Hmm. We found the file, but it doesn't exist inside + # the parent directory. That's a bit unusual. puts "No directory entry found for '#{normalized_path}'" next else + # Save the path so it can be output in detail entries = {name => parent_entries[name]} end else + # Path is not a directory or a file, + # so it must not exist puts "Path '#{normalized_path}' not found." next end + # Output a detailed listing of the files we found puts " REV AUTHOR NODE-REV-ID SIZE DATE NAME" puts "-" * 76 + # For each entry we found... entries.keys.sort.each do |entry| + + # Calculate the full path to the directory entry fullpath = parent + '/' + entry if @root.dir?(fullpath) + # If it's a directory, output an extra slash size = '' name = entry + '/' else + # If it's a file, output the size of the file size = @root.file_length(fullpath).to_i.to_s name = entry end + # Output the 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 @@ -139,17 +240,25 @@ node_id, size, format_date(date), name ] puts "%6s %8s <%10s> %8s %17s %s" % args + end end end + # List all currently open transactions def do_lstxns + + # Get a sorted list of open transactions txns = @fs.list_transactions txns.sort counter = 0 + + # Output the open transactions txns.each do |txn| counter = counter + 1 puts "%8s " % txn + + # Every six transactions, output an extra newline if counter == 6 puts counter = 0 @@ -158,36 +267,55 @@ puts end + # Output the properties of a particular path def do_pcat(path=nil) + + # Default to the current directory catpath = path || @path + + # Make sure that the specified path exists if @root.check_path(catpath) == Svn::Core::NODE_NONE puts "Path '#{catpath}' does not exist." return end + # Get the list of properties plist = @root.node_proplist(catpath) return if plist.nil? + # Output each property 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 the specified revision exists begin @fs.root(Integer(rev)).close rescue Svn::Error puts "Error setting the revision to '#{rev}': #{$!.message}" return end + + # Set the revision self.rev = Integer(rev) + end + # Open an existing transaction def do_settxn(name) + + # Make sure the specified transaction exists begin txn = @fs.open_txn(name) txn.root.close @@ -195,39 +323,49 @@ puts "Error setting the transaction to '#{name}': #{$!.message}" return end + + # Set the transaction self.txn = name + end + # Set the revision to the latest/youngest revision def do_youngest rev = @fs.youngest_rev puts rev end + # Exit this program def do_exit @exited = true end + # Find the youngest revision def youngest_rev Svn::Core::Pool.new(@pool) do |tmp_pool| Svn::Repos.open(@repos_path, tmp_pool).fs.youngest_rev end end + # Set the current revision def rev=(new_value) @rev = new_value @txn = nil reset_root end + # Set the current transaction def txn=(new_value) @txn = new_value reset_root end + # Check whether we are in 'revision-mode' def rev_mode? @txn.nil? end + # Close the current root and setup a new one def reset_root if @root @root.close @@ -235,6 +373,7 @@ end end + # Setup a new root def setup_root if rev_mode? @root = @fs.root(@rev) @@ -243,21 +382,29 @@ end end + # Convert a path into its component parts def path_to_parts(path) path.split(/\/+/) end + # Join the component parts of a path into a string def parts_to_path(parts) normalized_parts = parts.reject{|part| part.empty?} "/#{normalized_parts.join('/')}" end + # Convert a path to a normalized, absolute path def normalize_path(path) + + # Convert the path to an absolute path if path[0,1] != "/" and @path != "/" path = "#{@path}/#{path}" end + + # Split the path into its component parts parts = path_to_parts(path) + # Build a list of the normalized parts of the path normalized_parts = [] parts.each do |part| case part @@ -269,13 +416,18 @@ normalized_parts << part end end + + # Join the normalized parts together into a string parts_to_path(normalized_parts) + end + # Find the parent directory of a specified path def parent_dir(path) normalize_path("#{path}/..") end + # Find an available path def find_available_path(path=@path) if @root.check_path(path) == Svn::Core::NODE_DIR path @@ -284,6 +436,7 @@ end end + # Format a date for output in a standard format def format_date(date_str) date = Svn::Util.string_to_time(date_str, @taskpool) date.strftime("%b %d %H:%M(%Z)") @@ -292,14 +445,18 @@ end +# Autocomplete commands Readline.completion_proc = Proc.new do |word| SvnShell::WORDS.grep(/^#{Regexp.quote(word)}/) end +# Output usage information if necessary if ARGV.size != 1 puts "Usage: #{$0} REPOS_PATH" exit(1) end + +# Create a new SvnShell with the command-line arguments and run it Svn::Core::Pool.new do |pool| SvnShell.new(pool, ARGV.shift).run end