3 # svnshell.rb : a Ruby-based shell interface for cruising 'round in
6 # Usage: ruby svnshell.rb REPOS_PATH, where REPOS_PATH is a path to
7 # a repository on your local filesystem.
9 # NOTE: This program requires the Ruby readline extension.
10 # See http://wiki.rubyonrails.com/rails/show/ReadlineLibrary
11 # for details on how to install readline for Ruby.
13 ######################################################################
15 # Copyright (c) 2000-2005 CollabNet. All rights reserved.
17 # This software is licensed as described in the file COPYING, which
18 # you should have received as part of this distribution. The terms
19 # are also available at http://subversion.tigris.org/license-1.html.
20 # If newer versions of this license are posted there, you may use a
21 # newer version instead, at your option.
23 ######################################################################
33 # SvnShell: a Ruby-based shell interface for cruising 'round in
37 # A list of potential commands. This list is populated by
38 # the 'method_added' function (see below).
41 # Check for methods that start with "do_"
42 # and list them as potential commands
44 def method_added(name)
45 if /^do_(.*)$/ =~ name.to_s
51 # Constructor for SvnShell
53 # path: The path to a Subversion repository
57 self.rev = youngest_rev
64 # While the user hasn't typed 'exit' and there is still input to be read
65 while !@exited and buf = Readline.readline(prompt, true)
67 # Parse the command line into a single command and arguments
68 cmd, *args = Shellwords.shellwords(buf)
71 next if /\A\s*\z/ =~ cmd.to_s
73 # Open a new connection to the repo
74 @fs = Svn::Repos.open(@repos_path).fs
77 # Execute the specified command
80 # Find a path that exists in the current revision
81 @path = find_available_path
83 # Close the connection to the repo
92 # Get the current prompt string
95 # Gather data for the prompt string
104 # Return the prompt string
105 "<#{mode}: #{info} #{@path}>$ "
108 # Dispatch a command to the appropriate do_* subroutine
109 def dispatch(cmd, *args)
111 # Dispatch cmd to the appropriate do_* function
112 if respond_to?("do_#{cmd}", true)
114 __send__("do_#{cmd}", *args)
118 puts("Invalid argument for #{cmd}: #{args.join(' ')}")
121 puts("Unknown command: #{cmd}")
122 puts("Try one of these commands: ", WORDS.sort.join(" "))
126 # Output the contents of a file from the repository
129 # Normalize the path to an absolute path
130 normalized_path = normalize_path(path)
132 # Check what type of node exists at the specified path
133 case @root.check_path(normalized_path)
134 when Svn::Core::NODE_NONE
135 puts "Path '#{normalized_path}' does not exist."
136 when Svn::Core::NODE_DIR
137 puts "Path '#{normalized_path}' is not a file."
139 # Output the file to standard out
140 @root.file_contents(normalized_path) do |stream|
141 puts stream.read(@root.file_length(normalized_path))
146 # Set the current directory
149 # Normalize the path to an absolute path
150 normalized_path = normalize_path(path)
152 # If it's a valid directory, then set the directory
153 if @root.check_path(normalized_path) == Svn::Core::NODE_DIR
154 @path = normalized_path
156 puts "Path '#{normalized_path}' is not a valid filesystem directory."
160 # List the contents of the current directory or provided paths
163 # Default to listing the contents of the current directory
164 paths << @path if paths.empty?
169 # Normalize the path to an absolute path
170 normalized_path = normalize_path(path)
172 # Is it a directory or file?
173 case @root.check_path(normalized_path)
174 when Svn::Core::NODE_DIR
176 # Output the contents of the directory
177 parent = normalized_path
178 entries = @root.dir_entries(parent)
180 when Svn::Core::NODE_FILE
182 # Split the path into directory and filename components
183 parts = path_to_parts(normalized_path)
185 parent = parts_to_path(parts)
187 # Output the filename
188 puts "#{parent}:#{name}"
190 # Double check that the file exists
191 # inside the parent directory
192 parent_entries = @root.dir_entries(parent)
193 if parent_entries[name].nil?
194 # Hmm. We found the file, but it doesn't exist inside
195 # the parent directory. That's a bit unusual.
196 puts "No directory entry found for '#{normalized_path}'"
199 # Save the path so it can be output in detail
200 entries = {name => parent_entries[name]}
203 # Path is not a directory or a file,
204 # so it must not exist
205 puts "Path '#{normalized_path}' not found."
209 # Output a detailed listing of the files we found
210 puts " REV AUTHOR NODE-REV-ID SIZE DATE NAME"
213 # For each entry we found...
214 entries.keys.sort.each do |entry|
216 # Calculate the full path to the directory entry
217 fullpath = parent + '/' + entry
218 if @root.dir?(fullpath)
219 # If it's a directory, output an extra slash
223 # If it's a file, output the size of the file
224 size = @root.file_length(fullpath).to_i.to_s
229 node_id = entries[entry].id.to_s
230 created_rev = @root.node_created_rev(fullpath)
231 author = @fs.prop(Svn::Core::PROP_REVISION_AUTHOR, created_rev).to_s
232 date = @fs.prop(Svn::Core::PROP_REVISION_DATE, created_rev)
234 created_rev, author[0,8],
235 node_id, size, date.strftime("%b %d %H:%M(%Z)"), name
237 puts "%6s %8s <%10s> %8s %17s %s" % args
243 # List all currently open transactions available for browsing
246 # Get a sorted list of open transactions
247 txns = @fs.transactions
251 # Output the open transactions
253 counter = counter + 1
256 # Every six transactions, output an extra newline
265 # Output the properties of a particular path
266 def do_pcat(path=nil)
268 # Default to the current directory
269 catpath = path ? normalize_path(path) : @path
271 # Make sure that the specified path exists
272 if @root.check_path(catpath) == Svn::Core::NODE_NONE
273 puts "Path '#{catpath}' does not exist."
277 # Get the list of properties
278 plist = @root.node_proplist(catpath)
281 # Output each property
282 plist.each do |key, value|
285 puts "P #{value.size}"
294 # Set the current revision to view
297 # Make sure the specified revision exists
299 @fs.root(Integer(rev)).close
301 puts "Error setting the revision to '#{rev}': #{$!.message}"
306 self.rev = Integer(rev)
310 # Open an existing transaction to view
313 # Make sure the specified transaction exists
315 txn = @fs.open_txn(name)
318 puts "Error setting the transaction to '#{name}': #{$!.message}"
322 # Set the transaction
327 # List the youngest revision available for browsing
329 rev = @fs.youngest_rev
338 # Find the youngest revision
340 Svn::Repos.open(@repos_path).fs.youngest_rev
343 # Set the current revision
350 # Set the current transaction
356 # Check whether we are in 'revision-mode'
361 # Close the current root and setup a new one
372 @root = @fs.root(@rev)
374 @root = @fs.open_txn(name).root
378 # Convert a path into its component parts
379 def path_to_parts(path)
383 # Join the component parts of a path into a string
384 def parts_to_path(parts)
385 normalized_parts = parts.reject{|part| part.empty?}
386 "/#{normalized_parts.join('/')}"
389 # Convert a path to a normalized, absolute path
390 def normalize_path(path)
392 # Convert the path to an absolute path
393 if path[0,1] != "/" and @path != "/"
394 path = "#{@path}/#{path}"
397 # Split the path into its component parts
398 parts = path_to_parts(path)
400 # Build a list of the normalized parts of the path
401 normalized_parts = []
409 normalized_parts << part
413 # Join the normalized parts together into a string
414 parts_to_path(normalized_parts)
418 # Find the parent directory of a specified path
420 normalize_path("#{path}/..")
423 # Try to land on the specified path as a directory.
424 # If the specified path does not exist, look for
425 # an ancestor path that does exist.
426 def find_available_path(path=@path)
427 if @root.check_path(path) == Svn::Core::NODE_DIR
430 find_available_path(parent_dir(path))
437 # Autocomplete commands
438 Readline.completion_proc = Proc.new do |word|
439 SvnShell::WORDS.grep(/^#{Regexp.quote(word)}/)
442 # Output usage information if necessary
444 puts "Usage: #{$0} REPOS_PATH"
448 # Create a new SvnShell with the command-line arguments and run it
449 SvnShell.new(ARGV.shift).run