| #!/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. |
| # |
| ###################################################################### |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| ###################################################################### |
| # |
| |
| 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 subcommand: #{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 |