#!/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" require "svn/fs" 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 WORDS << $1 end end end # Constructor for SvnShell # # path: The path to a Subversion repository def initialize(path) @repos_path = path @path = "/" self.rev = youngest_rev @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 # Open a new connection to the repo @fs = Svn::Repos.open(@repos_path).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 # Private functions private # Get the current prompt string def prompt # Gather data for the prompt string if rev_mode? mode = "rev" info = @rev else 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) rescue ArgumentError # puts $!.message # puts $@ puts("Invalid argument for #{cmd}: #{args.join(' ')}") end else puts("Unknown command: #{cmd}") puts("Try one of these commands: ", WORDS.sort.join(" ")) end end # Output the contents of 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 puts "Path '#{normalized_path}' is not a valid filesystem directory." end end # List the contents of the current directory or provided paths 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 date = @fs.prop(Svn::Core::PROP_REVISION_DATE, created_rev) args = [ created_rev, author[0,8], node_id, size, date.strftime("%b %d %H:%M(%Z)"), name ] puts "%6s %8s <%10s> %8s %17s %s" % args end end end # List all currently open transactions available for browsing def do_lstxns # Get a sorted list of open transactions txns = @fs.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 end end puts end # Output the properties of a particular path def do_pcat(path=nil) # Default to the current directory catpath = path ? normalize_path(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 to view 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 to view def do_settxn(name) # Make sure the specified transaction exists begin txn = @fs.open_txn(name) txn.root.close rescue Svn::Error puts "Error setting the transaction to '#{name}': #{$!.message}" return end # Set the transaction self.txn = name end # List the youngest revision available for browsing 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::Repos.open(@repos_path).fs.youngest_rev 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 setup_root end end # Setup a new root def setup_root if rev_mode? @root = @fs.root(@rev) else @root = @fs.open_txn(name).root 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 when "." # ignore when ".." normalized_parts.pop else 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 # Try to land on the specified path as a directory. # If the specified path does not exist, look for # an ancestor path that does exist. def find_available_path(path=@path) if @root.check_path(path) == Svn::Core::NODE_DIR path else find_available_path(parent_dir(path)) end end 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 SvnShell.new(ARGV.shift).run