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 ######################################################################
23 # Chomp off trailing slashes
28 # SvnLook: a Ruby-based replacement for svnlook
31 # Initialize the SvnLook application
32 def initialize(path, rev, txn)
34 @fs = Svn::Repos.open(basename(path)).fs
36 # If a transaction was specified, open it
38 @txn = @fs.open_txn(txn)
40 # Use the latest revision from the repo,
41 # if they haven't specified a revision
43 rev ||= @fs.youngest_rev
49 # Dispatch all commands to appropriate subroutines
56 # Dispatch all commands to appropriate subroutines
57 def dispatch(cmd, *args)
58 if respond_to?("cmd_#{cmd}", true)
60 __send__("cmd_#{cmd}", *args)
64 puts("invalid argument for #{cmd}: #{args.join(' ')}")
67 puts("unknown command: #{cmd}")
71 # Default command: Run the 'info' and 'tree' commands
77 # Print the 'author' of the specified revision or transaction
79 puts(property(Svn::Core::PROP_REVISION_AUTHOR) || "")
86 # Find out what has changed in the specified revision or transaction
88 print_tree(ChangedEditor, nil, true)
91 # Output the date that the current revision was committed.
94 # It's not committed yet, so output nothing
97 # Get the time the revision was committed
98 date = property(Svn::Core::PROP_REVISION_DATE)
101 # Print out the date in a nice format
102 puts date.strftime('%Y-%m-%d %H:%M(%Z)')
104 # The specified revision doesn't have an associated date.
105 # Output just a blank line.
111 # Output what changed in the specified revision / transaction
113 print_tree(DiffEditor, nil, true)
116 # Output what directories changed in the specified revision / transaction
118 print_tree(DirsChangedEditor)
121 # Output the tree, with node ids
123 print_tree(Editor, 0, true)
126 # Output the author, date, and the log associated with the specified
127 # revision / transaction
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
141 # Output the tree associated with the provided tree
143 print_tree(Editor, 0)
146 # Output the repository's UUID.
151 # Output the repository's youngest revision.
153 puts @fs.youngest_rev
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
167 # Print a tree of differences between two revisions
168 def print_tree(editor_class, base_rev=nil, pass_root=false)
171 # Output changes since the base revision of the transaction
172 base_rev = @txn.base_revision
174 # Output changes since the previous revision
179 # Get the root of the specified transaction or revision
183 root = @fs.root(@rev)
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?
192 # Create a new editor with the provided root and base_root
193 editor = editor_class.new(root, base_root)
195 # Create a new editor with nil root and base_roots
196 editor = editor_class.new
199 # Do a directory delta between the two roots with
200 # the specified editor
201 base_root.dir_delta('', '', root, '', editor)
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)
215 # Recurse through the root (and increase the indent level)
216 def open_root(base_revision)
221 # If a directory is added, output this and increase
223 def add_directory(path, *args)
224 puts "#{@indent}#{basename(path)}/#{id(path)}"
228 alias open_directory add_directory
230 # If a directory is closed, reduce the ident level
231 def close_directory(baton)
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)}"
240 alias open_file add_file
245 # Get the node id of a particular path
248 fs_id = @root.node_id(path)
249 " <#{fs_id.unparse}>"
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
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)
270 # The directory hasn't been printed yet,
274 # Make sure we don't print this directory out twice
280 # Output files that have been changed between two roots
281 class ChangedEditor < Svn::Delta::BaseEditor
284 def initialize(root, base_root)
286 @base_root = base_root
289 # Look at the root node
290 def open_root(base_revision)
291 # Nothing has been printed out yet, so return 'true'.
295 # Output deleted files
296 def delete_entry(path, revision, parent_baton)
297 # Output deleted paths with a D in front of them
300 # If we're deleting a directory,
301 # indicate this with a trailing slash
302 if @base_root.dir?('/' + path)
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.
316 # The directory has been printed -- don't print it again.
320 # Recurse inside directories
321 def open_directory(path, parent_baton, base_revision)
322 # Nothing has been printed out yet, so return true.
326 def change_dir_prop(dir_baton, name, value)
327 # Has the directory been printed yet?
329 # Print the directory
330 puts "_U #{dir_baton[1]}/"
332 # Don't let this directory get printed again.
337 def add_file(path, parent_baton,
338 copyfrom_path, copyfrom_revision)
339 # Output that a directory has been added
342 # We've already printed out this entry, so return '_'
343 # to prevent it from being printed again
348 def open_file(path, parent_baton, base_revision)
349 # Changes have been made -- return '_' to indicate as such
353 def apply_textdelta(file_baton, base_checksum)
354 # The file has been changed -- we'll print that out later.
359 def change_file_prop(file_baton, name, value)
360 # The file has been changed -- we'll print that out later.
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.
368 status = text_mod + prop_mod
369 # Was there some kind of change?
371 puts "#{status} #{path}"
377 # Output diffs of files that have been changed
378 class DiffEditor < Svn::Delta::BaseEditor
381 def initialize(root, base_root)
383 @base_root = base_root
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)
396 def add_file(path, parent_baton,
397 copyfrom_path, copyfrom_revision)
398 # If a file has been added, print out the diff.
405 def open_file(path, parent_baton, base_revision)
409 # If a file is changed, print out the diff
410 def apply_textdelta(file_baton, base_checksum)
411 if file_baton[2].nil?
414 do_diff(file_baton[2], file_baton[2])
420 # Print out a diff between two paths
421 def do_diff(base_path, path)
423 # If there's no base path, then the file
424 # must have been added
425 puts("Added: #{path}")
428 # If there's no new path, then the file
429 # must have been deleted
430 puts("Removed: #{base_path}")
433 # Otherwise, the file must have been modified
434 puts "Modified: #{path}"
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
444 differ = Svn::Fs::FileDiff.new(@base_root, base_path, @root, path)
445 puts differ.unified(base_label, label)
451 # Output usage message and exit
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",
458 "REV is a revision number > 0.",
459 "TXN is a transaction name.",
461 "If no command is given, the default output (which is the same as",
462 "running the subcommands `info' then `tree') will be printed.",
464 "COMMAND can be one of: ",
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).",
478 puts(messages.join("\n"))
482 # Output usage if necessary
495 rev = Integer(ARGV.shift)
502 # If no command is specified, use the 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)