require 'tmpdir' require 'fileutils' require 'open-uri' module YARD module CLI # CLI command to return the objects that were added/removed from 2 versions # of a project (library, gem, working copy). # @since 0.6.0 class Diff < Command def initialize super @list_all = false @use_git = false @old_git_commit = nil @old_path = Dir.pwd log.show_backtraces = true end def description 'Returns the object diff of two gems or .yardoc files' end def run(*args) registry = optparse(*args).map do |gemfile| if @use_git load_git_commit(gemfile) Registry.all.map {|o| o.path } else if load_gem_data(gemfile) log.info "Found #{gemfile}" Registry.all.map {|o| o.path } else log.error "Cannot find gem #{gemfile}" nil end end end.compact return if registry.size != 2 [ ["Added objects", registry[1] - registry[0]], ["Removed objects", registry[0] - registry[1]]].each do |name, objects| next if objects.empty? last_object = nil all_objects_notice = false puts name + ":" objects.sort.each do |object| if !@list_all && last_object && object =~ /#{Regexp.quote last_object}(::|\.|#)/ print " (...)" unless all_objects_notice all_objects_notice = true next else puts end all_objects_notice = false print " " + object last_object = object end puts puts end end private def load_git_commit(commit) commit_path = 'git_commit' + commit.gsub(/\W/, '_') tmpdir = File.join(Dir.tmpdir, commit_path) log.info "Expanding #{commit} to #{tmpdir}..." Dir.chdir(@old_path) FileUtils.mkdir_p(tmpdir) FileUtils.cp_r('.', tmpdir) Dir.chdir(tmpdir) log.info("git says: " + `git reset --hard #{commit}`.chomp) generate_yardoc(tmpdir) Dir.chdir(@old_path) cleanup(commit_path) end def load_gem_data(gemfile) require_rubygems Registry.clear # First check for argument as .yardoc file [File.join(gemfile, '.yardoc'), gemfile].each do |yardoc| log.info "Searching for .yardoc db at #{yardoc}" if File.directory?(yardoc) Registry.load_yardoc(yardoc) Registry.load_all return true end end # Next check installed RubyGems gemfile_without_ext = gemfile.sub(/\.gem$/, '') log.info "Searching for installed gem #{gemfile_without_ext}" Gem.source_index.find_name('').find do |spec| if spec.full_name == gemfile_without_ext if yardoc = Registry.yardoc_file_for_gem(spec.name, "= #{spec.version}") Registry.load_yardoc(yardoc) Registry.load_all else log.enter_level(Logger::ERROR) do olddir = Dir.pwd Gems.run(spec.name, spec.version.to_s) Dir.chdir(olddir) end end return true end end # Look for local .gem file gemfile += '.gem' unless gemfile =~ /\.gem$/ log.info "Searching for local gem file #{gemfile}" if File.exist?(gemfile) File.open(gemfile, 'rb') do |io| expand_and_parse(gemfile, io) end return true end # Remote gemfile from rubygems.org url = "http://rubygems.org/downloads/#{gemfile}" log.info "Searching for remote gem file #{url}" begin open(url) {|io| expand_and_parse(gemfile, io) } return true rescue OpenURI::HTTPError end false end def expand_and_parse(gemfile, io) dir = expand_gem(gemfile, io) generate_yardoc(dir) cleanup(gemfile) end def generate_yardoc(dir) olddir = Dir.pwd Dir.chdir(dir) log.enter_level(Logger::ERROR) { Yardoc.run('-n', '--no-save') } Dir.chdir(olddir) end def expand_gem(gemfile, io) tmpdir = File.join(Dir.tmpdir, gemfile) log.info "Expanding #{gemfile} to #{tmpdir}..." FileUtils.mkdir_p(tmpdir) Gem::Package.open(io) do |pkg| pkg.each do |entry| pkg.extract_entry(tmpdir, entry) end end tmpdir end def require_rubygems require 'rubygems' require 'rubygems/package' rescue LoadError => e log.error "Missing RubyGems, cannot run this command." raise(e) end def cleanup(gemfile) dir = File.join(Dir.tmpdir, gemfile) log.info "Cleaning up #{dir}..." FileUtils.rm_rf(dir) end def optparse(*args) opts = OptionParser.new opts.banner = "Usage: yard diff [options] oldgem newgem" opts.separator "" opts.separator "Example: yard diff yard-0.5.6 yard-0.5.8" opts.separator "" opts.separator "If the files don't exist locally, they will be grabbed using the `gem fetch`" opts.separator "command. If the gem is a .yardoc directory, it will be used. Finally, if the" opts.separator "gem name matches an installed gem (full name-version syntax), that gem will be used." opts.on('-a', '--all', 'List all objects, even if they are inside added/removed module/class') do @list_all = true end opts.on('--git', 'Compare versions from two git commit/branches') do @use_git = true end common_options(opts) parse_options(opts, args) unless args.size == 2 puts opts.banner exit(0) end args end end end end