Fix compiler warning due to missing function prototype.
[svn.git] / tools / examples / svnlook.rb
blob574b8663d4b6dff345e5e38a1af1c0bb0feaed8a
1 #!/usr/bin/env ruby
3 # svnlook.rb : a Ruby-based replacement for svnlook
5 ######################################################################
7 # Copyright (c) 2000-2005 CollabNet.  All rights reserved.
9 # This software is licensed as described in the file COPYING, which
10 # you should have received as part of this distribution.  The terms
11 # are also available at http://subversion.tigris.org/license-1.html.
12 # If newer versions of this license are posted there, you may use a
13 # newer version instead, at your option.
15 ######################################################################
18 require "svn/core"
19 require "svn/fs"
20 require "svn/delta"
21 require "svn/repos"
23 # Chomp off trailing slashes
24 def basename(path)
25   path.chomp("/")
26 end
28 # SvnLook: a Ruby-based replacement for svnlook
29 class SvnLook
31   # Initialize the SvnLook application
32   def initialize(path, rev, txn)
33     # Open a repository
34     @fs = Svn::Repos.open(basename(path)).fs
36     # If a transaction was specified, open it
37     if txn
38       @txn = @fs.open_txn(txn)
39     else
40       # Use the latest revision from the repo,
41       # if they haven't specified a revision
42       @txn = nil
43       rev ||= @fs.youngest_rev
44     end
46     @rev = rev
47   end
49   # Dispatch all commands to appropriate subroutines
50   def run(cmd, *args)
51     dispatch(cmd, *args)
52   end
54   private
56   # Dispatch all commands to appropriate subroutines
57   def dispatch(cmd, *args)
58     if respond_to?("cmd_#{cmd}", true)
59       begin
60         __send__("cmd_#{cmd}", *args)
61       rescue ArgumentError
62         puts $!.message
63         puts $@
64         puts("invalid argument for #{cmd}: #{args.join(' ')}")
65       end
66     else
67       puts("unknown command: #{cmd}")
68     end
69   end
71   # Default command: Run the 'info' and 'tree' commands
72   def cmd_default
73     cmd_info
74     cmd_tree
75   end
77   # Print the 'author' of the specified revision or transaction
78   def cmd_author
79     puts(property(Svn::Core::PROP_REVISION_AUTHOR) || "")
80   end
82   # Not implemented yet
83   def cmd_cat
84   end
86   # Find out what has changed in the specified revision or transaction
87   def cmd_changed
88     print_tree(ChangedEditor, nil, true)
89   end
91   # Output the date that the current revision was committed.
92   def cmd_date
93     if @txn
94       # It's not committed yet, so output nothing
95       puts
96     else
97       # Get the time the revision was committed
98       date = property(Svn::Core::PROP_REVISION_DATE)
100       if date
101         # Print out the date in a nice format
102         puts date.strftime('%Y-%m-%d %H:%M(%Z)')
103       else
104         # The specified revision doesn't have an associated date.
105         # Output just a blank line.
106         puts
107       end
108     end
109   end
111   # Output what changed in the specified revision / transaction
112   def cmd_diff
113     print_tree(DiffEditor, nil, true)
114   end
116   # Output what directories changed in the specified revision / transaction
117   def cmd_dirs_changed
118     print_tree(DirsChangedEditor)
119   end
121   # Output the tree, with node ids
122   def cmd_ids
123     print_tree(Editor, 0, true)
124   end
126   # Output the author, date, and the log associated with the specified
127   # revision / transaction
128   def cmd_info
129     cmd_author
130     cmd_date
131     cmd_log(true)
132   end
134   # Output the log message associated with the specified revision / transaction
135   def cmd_log(print_size=false)
136     log = property(Svn::Core::PROP_REVISION_LOG) || ''
137     puts log.length if print_size
138     puts log
139   end
141   # Output the tree associated with the provided tree
142   def cmd_tree
143     print_tree(Editor, 0)
144   end
146   # Output the repository's UUID.
147   def cmd_uuid
148     puts @fs.uuid
149   end
151   # Output the repository's youngest revision.
152   def cmd_youngest
153     puts @fs.youngest_rev
154   end
156   # Return a property of the specified revision or transaction.
157   # Name: the ID of the property you want to retrieve.
158   #       E.g. Svn::Core::PROP_REVISION_LOG
159   def property(name)
160     if @txn
161       @txn.prop(name)
162     else
163       @fs.prop(name, @rev)
164     end
165   end
167   # Print a tree of differences between two revisions
168   def print_tree(editor_class, base_rev=nil, pass_root=false)
169     if base_rev.nil?
170       if @txn
171         # Output changes since the base revision of the transaction
172         base_rev = @txn.base_revision
173       else
174         # Output changes since the previous revision
175         base_rev = @rev - 1
176       end
177     end
179     # Get the root of the specified transaction or revision
180     if @txn
181       root = @txn.root
182     else
183       root = @fs.root(@rev)
184     end
186     # Get the root of the base revision
187     base_root = @fs.root(base_rev)
189     # Does the provided editor need to know
190     # the revision and base revision we're working with?
191     if pass_root
192       # Create a new editor with the provided root and base_root
193       editor = editor_class.new(root, base_root)
194     else
195       # Create a new editor with nil root and base_roots
196       editor = editor_class.new
197     end
199     # Do a directory delta between the two roots with
200     # the specified editor
201     base_root.dir_delta('', '', root, '', editor)
202   end
204   # Output the current tree for a specified revision
205   class Editor < Svn::Delta::BaseEditor
207     # Initialize the Editor object
208     def initialize(root=nil, base_root=nil)
209       @root = root
210       # base_root ignored
212       @indent = ""
213     end
215     # Recurse through the root (and increase the indent level)
216     def open_root(base_revision)
217       puts "/#{id('/')}"
218       @indent << ' '
219     end
221     # If a directory is added, output this and increase
222     # the indent level
223     def add_directory(path, *args)
224       puts "#{@indent}#{basename(path)}/#{id(path)}"
225       @indent << ' '
226     end
228     alias open_directory add_directory
230     # If a directory is closed, reduce the ident level
231     def close_directory(baton)
232       @indent.chop!
233     end
235     # If a file is added, output that it has been changed
236     def add_file(path, *args)
237       puts "#{@indent}#{basename(path)}#{id(path)}"
238     end
240     alias open_file add_file
242     # Private methods
243     private
245     # Get the node id of a particular path
246     def id(path)
247       if @root
248         fs_id = @root.node_id(path)
249         " <#{fs_id.unparse}>"
250       else
251         ""
252       end
253     end
254   end
257   # Output directories that have been changed.
258   # In this class, methods such as open_root and add_file
259   # are inherited from Svn::Delta::ChangedDirsEditor.
260   class DirsChangedEditor < Svn::Delta::ChangedDirsEditor
262     # Private functions
263     private
265     # Print out the name of a directory if it has been changed.
266     # But only do so once.
267     # This behaves in a way like a callback function does.
268     def dir_changed(baton)
269       if baton[0]
270         # The directory hasn't been printed yet,
271         # so print it out.
272         puts baton[1] + '/'
274         # Make sure we don't print this directory out twice
275         baton[0] = nil
276       end
277     end
278   end
280   # Output files that have been changed between two roots
281   class ChangedEditor < Svn::Delta::BaseEditor
283     # Constructor
284     def initialize(root, base_root)
285       @root = root
286       @base_root = base_root
287     end
289     # Look at the root node
290     def open_root(base_revision)
291       # Nothing has been printed out yet, so return 'true'.
292       [true, '']
293     end
295     # Output deleted files
296     def delete_entry(path, revision, parent_baton)
297       # Output deleted paths with a D in front of them
298       print "D   #{path}"
300       # If we're deleting a directory,
301       # indicate this with a trailing slash
302       if @base_root.dir?('/' + path)
303         puts "/"
304       else
305         puts
306       end
307     end
309     # Output that a directory has been added
310     def add_directory(path, parent_baton,
311                       copyfrom_path, copyfrom_revision)
312       # Output 'A' to indicate that the directory was added.
313       # Also put a trailing slash since it's a directory.
314       puts "A   #{path}/"
316       # The directory has been printed -- don't print it again.
317       [false, path]
318     end
320     # Recurse inside directories
321     def open_directory(path, parent_baton, base_revision)
322       # Nothing has been printed out yet, so return true.
323       [true, path]
324     end
326     def change_dir_prop(dir_baton, name, value)
327       # Has the directory been printed yet?
328       if dir_baton[0]
329         # Print the directory
330         puts "_U  #{dir_baton[1]}/"
332         # Don't let this directory get printed again.
333         dir_baton[0] = false
334       end
335     end
337     def add_file(path, parent_baton,
338                  copyfrom_path, copyfrom_revision)
339       # Output that a directory has been added
340       puts "A   #{path}"
342       # We've already printed out this entry, so return '_'
343       # to prevent it from being printed again
344       ['_', ' ', nil]
345     end
348     def open_file(path, parent_baton, base_revision)
349       # Changes have been made -- return '_' to indicate as such
350       ['_', ' ', path]
351     end
353     def apply_textdelta(file_baton, base_checksum)
354       # The file has been changed -- we'll print that out later.
355       file_baton[0] = 'U'
356       nil
357     end
359     def change_file_prop(file_baton, name, value)
360       # The file has been changed -- we'll print that out later.
361       file_baton[1] = 'U'
362     end
364     def close_file(file_baton, text_checksum)
365       text_mod, prop_mod, path = file_baton
366       # Test the path. It will be nil if we added this file.
367       if path
368         status = text_mod + prop_mod
369         # Was there some kind of change?
370         if status != '_ '
371           puts "#{status}  #{path}"
372         end
373       end
374     end
375   end
377   # Output diffs of files that have been changed
378   class DiffEditor < Svn::Delta::BaseEditor
380     # Constructor
381     def initialize(root, base_root)
382       @root = root
383       @base_root = base_root
384     end
386     # Handle deleted files and directories
387     def delete_entry(path, revision, parent_baton)
388       # Print out diffs of deleted files, but not
389       # deleted directories
390       unless @base_root.dir?('/' + path)
391         do_diff(path, nil)
392       end
393     end
395     # Handle added files
396     def add_file(path, parent_baton,
397                  copyfrom_path, copyfrom_revision)
398       # If a file has been added, print out the diff.
399       do_diff(nil, path)
401       ['_', ' ', nil]
402     end
404     # Handle files
405     def open_file(path, parent_baton, base_revision)
406       ['_', ' ', path]
407     end
409     # If a file is changed, print out the diff
410     def apply_textdelta(file_baton, base_checksum)
411       if file_baton[2].nil?
412         nil
413       else
414         do_diff(file_baton[2], file_baton[2])
415       end
416     end
418     private
420     # Print out a diff between two paths
421     def do_diff(base_path, path)
422       if base_path.nil?
423         # If there's no base path, then the file
424         # must have been added
425         puts("Added: #{path}")
426         name = path
427       elsif path.nil?
428         # If there's no new path, then the file
429         # must have been deleted
430         puts("Removed: #{base_path}")
431         name = base_path
432       else
433         # Otherwise, the file must have been modified
434         puts "Modified: #{path}"
435         name = path
436       end
438       # Set up labels for the two files
439       base_label = "#{name} (original)"
440       label = "#{name} (new)"
442       # Output a unified diff between the two files
443       puts "=" * 78
444       differ = Svn::Fs::FileDiff.new(@base_root, base_path, @root, path)
445       puts differ.unified(base_label, label)
446       puts
447     end
448   end
451 # Output usage message and exit
452 def usage
453   messages = [
454     "usage: #{$0} REPOS_PATH rev REV [COMMAND] - inspect revision REV",
455     "       #{$0} REPOS_PATH txn TXN [COMMAND] - inspect transaction TXN",
456     "       #{$0} REPOS_PATH [COMMAND] - inspect the youngest revision",
457     "",
458     "REV is a revision number > 0.",
459     "TXN is a transaction name.",
460     "",
461     "If no command is given, the default output (which is the same as",
462     "running the subcommands `info' then `tree') will be printed.",
463     "",
464     "COMMAND can be one of: ",
465     "",
466     "   author:        print author.",
467     "   changed:       print full change summary: all dirs & files changed.",
468     "   date:          print the timestamp (revisions only).",
469     "   diff:          print GNU-style diffs of changed files and props.",
470     "   dirs-changed:  print changed directories.",
471     "   ids:           print the tree, with nodes ids.",
472     "   info:          print the author, data, log_size, and log message.",
473     "   log:           print log message.",
474     "   tree:          print the tree.",
475     "   uuid:          print the repository's UUID (REV and TXN ignored).",
476     "   youngest:      print the youngest revision number (REV and TXN ignored).",
477   ]
478   puts(messages.join("\n"))
479   exit(1)
482 # Output usage if necessary
483 if ARGV.empty?
484   usage
487 # Process arguments
488 path = ARGV.shift
489 cmd = ARGV.shift
490 rev = nil
491 txn = nil
493 case cmd
494 when "rev"
495   rev = Integer(ARGV.shift)
496   cmd = ARGV.shift
497 when "txn"
498   txn = ARGV.shift
499   cmd = ARGV.shift
502 # If no command is specified, use the default
503 cmd ||= "default"
505 # Replace dashes in the command with underscores
506 cmd = cmd.gsub(/-/, '_')
508 # Start SvnLook with the specified command
509 SvnLook.new(path, rev, txn).run(cmd)