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