blob: a48dcca1544baea6f0b191548904c789763f9057 [file] [log] [blame]
#!/usr/bin/env ruby
#
# svnlook.rb : a Ruby-based replacement for svnlook
#
######################################################################
# 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 "svn/core"
require "svn/fs"
require "svn/delta"
require "svn/repos"
# Chomp off trailing slashes
def basename(path)
path.chomp("/")
end
# SvnLook: a Ruby-based replacement for svnlook
class SvnLook
# Initialize the SvnLook application
def initialize(path, rev, txn)
# Open a repository
@fs = Svn::Repos.open(basename(path)).fs
# If a transaction was specified, open it
if txn
@txn = @fs.open_txn(txn)
else
# Use the latest revision from the repo,
# if they haven't specified a revision
@txn = nil
rev ||= @fs.youngest_rev
end
@rev = rev
end
# Dispatch all commands to appropriate subroutines
def run(cmd, *args)
dispatch(cmd, *args)
end
private
# Dispatch all commands to appropriate subroutines
def dispatch(cmd, *args)
if respond_to?("cmd_#{cmd}", true)
begin
__send__("cmd_#{cmd}", *args)
rescue ArgumentError
puts $!.message
puts $@
puts("invalid argument for #{cmd}: #{args.join(' ')}")
end
else
puts("unknown command: #{cmd}")
end
end
# Default command: Run the 'info' and 'tree' commands
def cmd_default
cmd_info
cmd_tree
end
# Print the 'author' of the specified revision or transaction
def cmd_author
puts(property(Svn::Core::PROP_REVISION_AUTHOR) || "")
end
# Not implemented yet
def cmd_cat
end
# Find out what has changed in the specified revision or transaction
def cmd_changed
print_tree(ChangedEditor, nil, true)
end
# Output the date that the current revision was committed.
def cmd_date
if @txn
# It's not committed yet, so output nothing
puts
else
# Get the time the revision was committed
date = property(Svn::Core::PROP_REVISION_DATE)
if date
# Print out the date in a nice format
puts date.strftime('%Y-%m-%d %H:%M(%Z)')
else
# The specified revision doesn't have an associated date.
# Output just a blank line.
puts
end
end
end
# Output what changed in the specified revision / transaction
def cmd_diff
print_tree(DiffEditor, nil, true)
end
# Output what directories changed in the specified revision / transaction
def cmd_dirs_changed
print_tree(DirsChangedEditor)
end
# Output the tree, with node ids
def cmd_ids
print_tree(Editor, 0, true)
end
# Output the author, date, and the log associated with the specified
# revision / transaction
def cmd_info
cmd_author
cmd_date
cmd_log(true)
end
# Output the log message associated with the specified revision / transaction
def cmd_log(print_size=false)
log = property(Svn::Core::PROP_REVISION_LOG) || ''
puts log.length if print_size
puts log
end
# Output the tree associated with the provided tree
def cmd_tree
print_tree(Editor, 0)
end
# Output the repository's UUID.
def cmd_uuid
puts @fs.uuid
end
# Output the repository's youngest revision.
def cmd_youngest
puts @fs.youngest_rev
end
# Return a property of the specified revision or transaction.
# Name: the ID of the property you want to retrieve.
# E.g. Svn::Core::PROP_REVISION_LOG
def property(name)
if @txn
@txn.prop(name)
else
@fs.prop(name, @rev)
end
end
# Print a tree of differences between two revisions
def print_tree(editor_class, base_rev=nil, pass_root=false)
if base_rev.nil?
if @txn
# Output changes since the base revision of the transaction
base_rev = @txn.base_revision
else
# Output changes since the previous revision
base_rev = @rev - 1
end
end
# Get the root of the specified transaction or revision
if @txn
root = @txn.root
else
root = @fs.root(@rev)
end
# Get the root of the base revision
base_root = @fs.root(base_rev)
# Does the provided editor need to know
# the revision and base revision we're working with?
if pass_root
# Create a new editor with the provided root and base_root
editor = editor_class.new(root, base_root)
else
# Create a new editor with nil root and base_roots
editor = editor_class.new
end
# Do a directory delta between the two roots with
# the specified editor
base_root.dir_delta('', '', root, '', editor)
end
# Output the current tree for a specified revision
class Editor < Svn::Delta::BaseEditor
# Initialize the Editor object
def initialize(root=nil, base_root=nil)
@root = root
# base_root ignored
@indent = ""
end
# Recurse through the root (and increase the indent level)
def open_root(base_revision)
puts "/#{id('/')}"
@indent << ' '
end
# If a directory is added, output this and increase
# the indent level
def add_directory(path, *args)
puts "#{@indent}#{basename(path)}/#{id(path)}"
@indent << ' '
end
alias open_directory add_directory
# If a directory is closed, reduce the ident level
def close_directory(baton)
@indent.chop!
end
# If a file is added, output that it has been changed
def add_file(path, *args)
puts "#{@indent}#{basename(path)}#{id(path)}"
end
alias open_file add_file
# Private methods
private
# Get the node id of a particular path
def id(path)
if @root
fs_id = @root.node_id(path)
" <#{fs_id.unparse}>"
else
""
end
end
end
# Output directories that have been changed.
# In this class, methods such as open_root and add_file
# are inherited from Svn::Delta::ChangedDirsEditor.
class DirsChangedEditor < Svn::Delta::ChangedDirsEditor
# Private functions
private
# Print out the name of a directory if it has been changed.
# But only do so once.
# This behaves in a way like a callback function does.
def dir_changed(baton)
if baton[0]
# The directory hasn't been printed yet,
# so print it out.
puts baton[1] + '/'
# Make sure we don't print this directory out twice
baton[0] = nil
end
end
end
# Output files that have been changed between two roots
class ChangedEditor < Svn::Delta::BaseEditor
# Constructor
def initialize(root, base_root)
@root = root
@base_root = base_root
end
# Look at the root node
def open_root(base_revision)
# Nothing has been printed out yet, so return 'true'.
[true, '']
end
# Output deleted files
def delete_entry(path, revision, parent_baton)
# Output deleted paths with a D in front of them
print "D #{path}"
# If we're deleting a directory,
# indicate this with a trailing slash
if @base_root.dir?('/' + path)
puts "/"
else
puts
end
end
# Output that a directory has been added
def add_directory(path, parent_baton,
copyfrom_path, copyfrom_revision)
# Output 'A' to indicate that the directory was added.
# Also put a trailing slash since it's a directory.
puts "A #{path}/"
# The directory has been printed -- don't print it again.
[false, path]
end
# Recurse inside directories
def open_directory(path, parent_baton, base_revision)
# Nothing has been printed out yet, so return true.
[true, path]
end
def change_dir_prop(dir_baton, name, value)
# Has the directory been printed yet?
if dir_baton[0]
# Print the directory
puts "_U #{dir_baton[1]}/"
# Don't let this directory get printed again.
dir_baton[0] = false
end
end
def add_file(path, parent_baton,
copyfrom_path, copyfrom_revision)
# Output that a directory has been added
puts "A #{path}"
# We've already printed out this entry, so return '_'
# to prevent it from being printed again
['_', ' ', nil]
end
def open_file(path, parent_baton, base_revision)
# Changes have been made -- return '_' to indicate as such
['_', ' ', path]
end
def apply_textdelta(file_baton, base_checksum)
# The file has been changed -- we'll print that out later.
file_baton[0] = 'U'
nil
end
def change_file_prop(file_baton, name, value)
# The file has been changed -- we'll print that out later.
file_baton[1] = 'U'
end
def close_file(file_baton, text_checksum)
text_mod, prop_mod, path = file_baton
# Test the path. It will be nil if we added this file.
if path
status = text_mod + prop_mod
# Was there some kind of change?
if status != '_ '
puts "#{status} #{path}"
end
end
end
end
# Output diffs of files that have been changed
class DiffEditor < Svn::Delta::BaseEditor
# Constructor
def initialize(root, base_root)
@root = root
@base_root = base_root
end
# Handle deleted files and directories
def delete_entry(path, revision, parent_baton)
# Print out diffs of deleted files, but not
# deleted directories
unless @base_root.dir?('/' + path)
do_diff(path, nil)
end
end
# Handle added files
def add_file(path, parent_baton,
copyfrom_path, copyfrom_revision)
# If a file has been added, print out the diff.
do_diff(nil, path)
['_', ' ', nil]
end
# Handle files
def open_file(path, parent_baton, base_revision)
['_', ' ', path]
end
# If a file is changed, print out the diff
def apply_textdelta(file_baton, base_checksum)
if file_baton[2].nil?
nil
else
do_diff(file_baton[2], file_baton[2])
end
end
private
# Print out a diff between two paths
def do_diff(base_path, path)
if base_path.nil?
# If there's no base path, then the file
# must have been added
puts("Added: #{path}")
name = path
elsif path.nil?
# If there's no new path, then the file
# must have been deleted
puts("Removed: #{base_path}")
name = base_path
else
# Otherwise, the file must have been modified
puts "Modified: #{path}"
name = path
end
# Set up labels for the two files
base_label = "#{name} (original)"
label = "#{name} (new)"
# Output a unified diff between the two files
puts "=" * 78
differ = Svn::Fs::FileDiff.new(@base_root, base_path, @root, path)
puts differ.unified(base_label, label)
puts
end
end
end
# Output usage message and exit
def usage
messages = [
"usage: #{$0} REPOS_PATH rev REV [COMMAND] - inspect revision REV",
" #{$0} REPOS_PATH txn TXN [COMMAND] - inspect transaction TXN",
" #{$0} REPOS_PATH [COMMAND] - inspect the youngest revision",
"",
"REV is a revision number > 0.",
"TXN is a transaction name.",
"",
"If no command is given, the default output (which is the same as",
"running the subcommands `info' then `tree') will be printed.",
"",
"COMMAND can be one of: ",
"",
" author: print author.",
" changed: print full change summary: all dirs & files changed.",
" date: print the timestamp (revisions only).",
" diff: print GNU-style diffs of changed files and props.",
" dirs-changed: print changed directories.",
" ids: print the tree, with nodes ids.",
" info: print the author, data, log_size, and log message.",
" log: print log message.",
" tree: print the tree.",
" uuid: print the repository's UUID (REV and TXN ignored).",
" youngest: print the youngest revision number (REV and TXN ignored).",
]
puts(messages.join("\n"))
exit(1)
end
# Output usage if necessary
if ARGV.empty?
usage
end
# Process arguments
path = ARGV.shift
cmd = ARGV.shift
rev = nil
txn = nil
case cmd
when "rev"
rev = Integer(ARGV.shift)
cmd = ARGV.shift
when "txn"
txn = ARGV.shift
cmd = ARGV.shift
end
# If no command is specified, use the default
cmd ||= "default"
# Replace dashes in the command with underscores
cmd = cmd.gsub(/-/, '_')
# Start SvnLook with the specified command
SvnLook.new(path, rev, txn).run(cmd)