|  | #!/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 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 |