blob: a49000ecfac56529ca7cd6e6ab320ab68da3a28e [file] [log] [blame]
#!/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