class YARD::CLI::Diff
@since 0.6.0
of a project (library, gem, working copy).
CLI command to return the objects that were added/removed from 2 versions
def added_objects(registry1, registry2)
def added_objects(registry1, registry2) registry2.reject {|o| registry1.find {|o2| o2.path == o.path } } end
def all_objects
def all_objects return Registry.all if @verifier.expressions.empty? @verifier.run(Registry.all) end
def cleanup(gemfile)
def cleanup(gemfile) dir = File.join(Dir.tmpdir, gemfile) log.info "Cleaning up #{dir}..." FileUtils.rm_rf(dir) end
def description
def description 'Returns the object diff of two gems or .yardoc files' end
def expand_and_parse(gemfile, io)
def expand_and_parse(gemfile, io) dir = expand_gem(gemfile, io) generate_yardoc(dir) cleanup(gemfile) end
def expand_gem(gemfile, io)
def expand_gem(gemfile, io) tmpdir = File.join(Dir.tmpdir, gemfile) FileUtils.mkdir_p(tmpdir) log.info "Expanding #{gemfile} to #{tmpdir}..." if Gem::VERSION >= '2.0.0' require 'rubygems/package/tar_reader' reader = Gem::Package::TarReader.new(io) reader.each do |pkg| next unless pkg.full_name == 'data.tar.gz' Zlib::GzipReader.wrap(pkg) do |gzio| tar = Gem::Package::TarReader.new(gzio) tar.each do |entry| file = File.join(tmpdir, entry.full_name) FileUtils.mkdir_p(File.dirname(file)) File.open(file, 'wb') do |out| out.write(entry.read) begin out.fsync rescue NotImplementedError nil # noop end end end end break end else Gem::Package.open(io) do |pkg| pkg.each do |entry| pkg.extract_entry(tmpdir, entry) end end end tmpdir end
def generate_yardoc(dir)
def generate_yardoc(dir) Dir.chdir(dir) do log.enter_level(Logger::ERROR) { Yardoc.run('-n', '--no-save') } end end
def initialize
def initialize super @list_all = false @use_git = false @compact = false @modified = true @verifier = Verifier.new @old_git_commit = nil @old_path = Dir.pwd log.show_backtraces = true end
def load_gem_data(gemfile)
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}" next unless File.directory?(yardoc) Registry.load_yardoc(yardoc) Registry.load_all return true end # Next check installed RubyGems gemfile_without_ext = gemfile.sub(/\.gem$/, '') log.info "Searching for installed gem #{gemfile_without_ext}" YARD::GemIndex.each.find do |spec| next unless spec.full_name == gemfile_without_ext yardoc = Registry.yardoc_file_for_gem(spec.name, "= #{spec.version}") if yardoc 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 # 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 # Note: In Ruby 2.4.x, URI.open is a private method. After # 2.5, URI.open behaves much like Kernel#open once you've # required 'open-uri' OpenURI.open_uri(url) {|io| expand_and_parse(gemfile, io) } return true rescue OpenURI::HTTPError nil # noop end false end
def load_git_commit(commit)
def load_git_commit(commit) Registry.clear 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) ensure Dir.chdir(@old_path) cleanup(commit_path) end
def modified_objects(registry1, registry2)
def modified_objects(registry1, registry2) registry1.select do |obj| case obj when CodeObjects::MethodObject registry2.find {|o| obj == o && o.source != obj.source } when CodeObjects::ConstantObject registry2.find {|o| obj == o && o.value != obj.value } end end.compact end
def optparse(*args)
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('--compact', 'Show compact results') { @compact = true } opts.on('--git', 'Compare versions from two git commit/branches') do @use_git = true end opts.on('--query QUERY', 'Only diff filtered objects') do |query| @verifier.add_expressions(query) end opts.on('--no-modified', 'Ignore modified objects') do @modified = false end common_options(opts) parse_options(opts, args) unless args.size == 2 log.puts opts.banner exit(0) end args end
def removed_objects(registry1, registry2)
def removed_objects(registry1, registry2) registry1.reject {|o| registry2.find {|o2| o2.path == o.path } } end
def require_rubygems
def require_rubygems require 'rubygems' require 'rubygems/package' rescue LoadError => e log.error "Missing RubyGems, cannot run this command." raise(e) end
def run(*args)
def run(*args) registry = optparse(*args).map do |gemfile| if @use_git load_git_commit(gemfile) all_objects elsif load_gem_data(gemfile) log.info "Found #{gemfile}" all_objects else log.error "Cannot find gem #{gemfile}" nil end end.compact return if registry.size != 2 first_object = nil [["Added objects", "A", added_objects(*registry)], ["Modified objects", "M", modified_objects(*registry)], ["Removed objects", "D", removed_objects(*registry)]].each do |name, short, objects| next if short == "M" && @modified == false next if objects.empty? last_object = nil all_objects_notice = false log.puts name + ":" unless @compact objects.sort_by(&:path).each do |object| if !@list_all && last_object && object.parent == last_object log.print " (...)" unless all_objects_notice all_objects_notice = true next elsif @compact log.puts if first_object else log.puts end all_objects_notice = false log.print "" + (@compact ? "#{short} " : " ") + object.path + " (#{object.file}:#{object.line})" last_object = object first_object = true end unless @compact log.puts; log.puts end end log.puts if @compact end