Move the long option name enum from cl.h into main.c, because it is
[svn.git] / tools / examples / svnshell.rb
blob323430e79f5829f8a5f9d83e1a6f62cf7804ca2b
1 #!/usr/bin/env ruby
3 # svnshell.rb : a Ruby-based shell interface for cruising 'round in
4 #               the filesystem.
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 ######################################################################
26 require "readline"
27 require "shellwords"
29 require "svn/fs"
30 require "svn/core"
31 require "svn/repos"
33 # SvnShell: a Ruby-based shell interface for cruising 'round in
34 #           the filesystem.
35 class SvnShell
37   # A list of potential commands. This list is populated by
38   # the 'method_added' function (see below).
39   WORDS = []
41   # Check for methods that start with "do_"
42   # and list them as potential commands
43   class << self
44     def method_added(name)
45       if /^do_(.*)$/ =~ name.to_s
46         WORDS << $1
47       end
48     end
49   end
51   # Constructor for SvnShell
52   #
53   # path: The path to a Subversion repository
54   def initialize(path)
55     @repos_path = path
56     @path = "/"
57     self.rev = youngest_rev
58     @exited = false
59   end
61   # Run the shell
62   def run
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)
70       # Skip empty lines
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
75       setup_root
77       # Execute the specified command
78       dispatch(cmd, *args)
80       # Find a path that exists in the current revision
81       @path = find_available_path
83       # Close the connection to the repo
84       @root.close
86     end
87   end
89   # Private functions
90   private
92   # Get the current prompt string
93   def prompt
95     # Gather data for the prompt string
96     if rev_mode?
97       mode = "rev"
98       info = @rev
99     else
100       mode = "txn"
101       info = @txn
102     end
104     # Return the prompt string
105     "<#{mode}: #{info} #{@path}>$ "
106   end
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)
113       begin
114         __send__("do_#{cmd}", *args)
115       rescue ArgumentError
116         # puts $!.message
117         # puts $@
118         puts("Invalid argument for #{cmd}: #{args.join(' ')}")
119       end
120     else
121       puts("Unknown command: #{cmd}")
122       puts("Try one of these commands: ", WORDS.sort.join(" "))
123     end
124   end
126   # Output the contents of a file from the repository
127   def do_cat(path)
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."
138     else
139       # Output the file to standard out
140       @root.file_contents(normalized_path) do |stream|
141         puts stream.read(@root.file_length(normalized_path))
142       end
143     end
144   end
146   # Set the current directory
147   def do_cd(path="/")
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
155     else
156       puts "Path '#{normalized_path}' is not a valid filesystem directory."
157     end
158   end
160   # List the contents of the current directory or provided paths
161   def do_ls(*paths)
163     # Default to listing the contents of the current directory
164     paths << @path if paths.empty?
166     # Foreach path
167     paths.each do |path|
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)
184         name = parts.pop
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}'"
197           next
198         else
199           # Save the path so it can be output in detail
200           entries = {name => parent_entries[name]}
201         end
202       else
203         # Path is not a directory or a file,
204         # so it must not exist
205         puts "Path '#{normalized_path}' not found."
206         next
207       end
209       # Output a detailed listing of the files we found
210       puts "   REV   AUTHOR  NODE-REV-ID     SIZE              DATE NAME"
211       puts "-" * 76
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
220           size = ''
221           name = entry + '/'
222         else
223           # If it's a file, output the size of the file
224           size = @root.file_length(fullpath).to_i.to_s
225           name = entry
226         end
228         # Output the entry
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)
233         args = [
234           created_rev, author[0,8],
235           node_id, size, date.strftime("%b %d %H:%M(%Z)"), name
236         ]
237         puts "%6s %8s <%10s> %8s %17s %s" % args
239       end
240     end
241   end
243   # List all currently open transactions available for browsing
244   def do_lstxns
246     # Get a sorted list of open transactions
247     txns = @fs.transactions
248     txns.sort
249     counter = 0
251     # Output the open transactions
252     txns.each do |txn|
253       counter = counter + 1
254       puts "%8s  " % txn
256       # Every six transactions, output an extra newline
257       if counter == 6
258         puts
259         counter = 0
260       end
261     end
262     puts
263   end
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."
274       return
275     end
277     # Get the list of properties
278     plist = @root.node_proplist(catpath)
279     return if plist.nil?
281     # Output each property
282     plist.each do |key, value|
283       puts "K #{key.size}"
284       puts key
285       puts "P #{value.size}"
286       puts value
287     end
289     # That's all folks!
290     puts 'PROPS-END'
292   end
294   # Set the current revision to view
295   def do_setrev(rev)
297     # Make sure the specified revision exists
298     begin
299       @fs.root(Integer(rev)).close
300     rescue Svn::Error
301       puts "Error setting the revision to '#{rev}': #{$!.message}"
302       return
303     end
305     # Set the revision
306     self.rev = Integer(rev)
308   end
310   # Open an existing transaction to view
311   def do_settxn(name)
313     # Make sure the specified transaction exists
314     begin
315       txn = @fs.open_txn(name)
316       txn.root.close
317     rescue Svn::Error
318       puts "Error setting the transaction to '#{name}': #{$!.message}"
319       return
320     end
322     # Set the transaction
323     self.txn = name
325   end
327   # List the youngest revision available for browsing
328   def do_youngest
329     rev = @fs.youngest_rev
330     puts rev
331   end
333   # Exit this program
334   def do_exit
335     @exited = true
336   end
338   # Find the youngest revision
339   def youngest_rev
340     Svn::Repos.open(@repos_path).fs.youngest_rev
341   end
343   # Set the current revision
344   def rev=(new_value)
345     @rev = new_value
346     @txn = nil
347     reset_root
348   end
350   # Set the current transaction
351   def txn=(new_value)
352     @txn = new_value
353     reset_root
354   end
356   # Check whether we are in 'revision-mode'
357   def rev_mode?
358     @txn.nil?
359   end
361   # Close the current root and setup a new one
362   def reset_root
363     if @root
364       @root.close
365       setup_root
366     end
367   end
369   # Setup a new root
370   def setup_root
371     if rev_mode?
372       @root = @fs.root(@rev)
373     else
374       @root = @fs.open_txn(name).root
375     end
376   end
378   # Convert a path into its component parts
379   def path_to_parts(path)
380     path.split(/\/+/)
381   end
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('/')}"
387   end
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}"
395     end
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 = []
402     parts.each do |part|
403       case part
404       when "."
405         # ignore
406       when ".."
407         normalized_parts.pop
408       else
409         normalized_parts << part
410       end
411     end
413     # Join the normalized parts together into a string
414     parts_to_path(normalized_parts)
416   end
418   # Find the parent directory of a specified path
419   def parent_dir(path)
420     normalize_path("#{path}/..")
421   end
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
428       path
429     else
430       find_available_path(parent_dir(path))
431     end
432   end
437 # Autocomplete commands
438 Readline.completion_proc = Proc.new do |word|
439   SvnShell::WORDS.grep(/^#{Regexp.quote(word)}/)
442 # Output usage information if necessary
443 if ARGV.size != 1
444   puts "Usage: #{$0} REPOS_PATH"
445   exit(1)
448 # Create a new SvnShell with the command-line arguments and run it
449 SvnShell.new(ARGV.shift).run