class Usedby::Cli

Define the command line interface.

Constants

GEMFILE_LOCK_SEARCH_TERM
GEMSPEC_SEARCH_TERM
USAGE

Public Instance Methods

run() click to toggle source
# File lib/usedby/cli.rb, line 24
def run
  parse_options
  if ARGV.size != 1
    STDERR.puts USAGE
    return 1
  end
  github_organization = ARGV[0]

  access_token = ENV['GITHUB_ACCESS_TOKEN'] || \
                 STDIN.getpass('GitHub Personal Access Token: ')
  github = Octokit::Client.new(access_token: access_token)

  gems = {}

  remote_search(github, github_organization, GEMFILE_LOCK_SEARCH_TERM) do |gemfile_lock|
    content = remote_file(github, gemfile_lock)
    next unless content
    content = Bundler::LockfileParser.new(content)
    merge!(gems, process_gemfile(content, "#{gemfile_lock.repository.name}/#{gemfile_lock.path}"))
  end

  remote_search(github, github_organization, GEMSPEC_SEARCH_TERM) do |gemspec|
    content = remote_file(github, gemspec)
    next unless content
    merge!(gems, process_gemspec(content, "#{gemspec.repository.name}/#{gemspec.path}"))
  end

  output gems

  0
end

Private Instance Methods

archived_repositories(github, organization) click to toggle source
# File lib/usedby/cli.rb, line 58
def archived_repositories(github, organization)
  github.organization_repositories(organization)
  last_response = github.last_response

  repositories = []
  last_response.data.each do |repository|
    repositories << repository.name if repository.archived
  end
  until last_response.rels[:next].nil?
    sleep_time = 0
    begin
      last_response = last_response.rels[:next].get
    rescue StandardError
      sleep_time += 1
      STDERR.puts "Sleeping #{sleep_time} seconds"
      sleep(sleep_time)
      retry
    end
    last_response.data.each do |repository|
      repositories << repository.name if repository.archived
    end
  end
  repositories
end
build_ignore_paths(ignored_paths, file) click to toggle source
# File lib/usedby/cli.rb, line 83
def build_ignore_paths(ignored_paths, file)
  File.open(file).each do |line|
    cleaned = line.strip
    ignored_paths << cleaned if cleaned != ''
  end
rescue Errno::ENOENT, Errno::EISDIR
  STDERR.puts "No such file #{file}"
  exit 1
end
filtered?(gemfile_path) click to toggle source
# File lib/usedby/cli.rb, line 93
def filtered?(gemfile_path)
  @options[:ignore_paths].each do |ignore_path|
    return true if gemfile_path.start_with?(ignore_path)
  end
  false
end
merge!(base, additions) click to toggle source
# File lib/usedby/cli.rb, line 148
def merge!(base, additions)
  additions.each do |gem, versions|
    if base.include? gem
      base_versions = base[gem]
      versions.each do |version, projects|
        if base_versions.include? version
          base_versions[version].concat(projects)
        else
          base_versions[version] = projects
        end
      end
    else
      base[gem] = versions
    end
  end
end
output(gems) click to toggle source
# File lib/usedby/cli.rb, line 165
def output(gems)
  sorted_gems = {}
  gems.sort.each do |gem, versions|
    sorted_gems[gem] = {}
    versions.sort.each do |version, projects|
      sorted_gems[gem][version_ranges_to_s(version)] = projects.sort
    end
  end
  puts JSON.pretty_generate(sorted_gems)
end
parse_options() click to toggle source
# File lib/usedby/cli.rb, line 176
def parse_options
  @options = { direct: false, ignore_paths: [] }
  OptionParser.new do |config|
    config.banner = USAGE
    config.on('-d', '--direct',
              'Consider only direct dependencies.') do |direct|
      @options[:direct] = direct
    end
    config.on('-i', '--ignore-file [FILEPATH]',
              'Ignore projects included in file.') do |ignore_file|

      build_ignore_paths(@options[:ignore_paths], ignore_file)
    end
    config.on('-g', '--gems [GEM1,GEM2,GEM3]',
              'Consider only given gems.') do |gems|

      @options[:gems] = gems.split(',')
    end
    config.version = Usedby::VERSION
  end.parse!
end
process_gemfile(gemfile, project) click to toggle source
# File lib/usedby/cli.rb, line 198
def process_gemfile(gemfile, project)
  dependencies = gemfile.dependencies.map { |dependency, _, _| dependency }
  gems = {}

  gemfile.specs.each do |spec|
    next if @options[:direct] && !dependencies.include?(spec.name)
    next if @options[:gems] && !@options[:gems].include?(spec.name)
    spec_version_ranges = Bundler::VersionRanges.for(Gem::Requirement.new(spec.version))
    gems[spec.name] = {}
    gems[spec.name][spec_version_ranges] = [project]
  end
  gems
end
process_gemspec(content, project) click to toggle source

Process dependencies in gemspec according to: guides.rubygems.org/specification-reference/ Sample supported formats:

   s.add_dependency(%q<rspec>.freeze, ["~> 3.2"])
spec.add_runtime_dependency "multi_json", "~>1.12", ">=1.12.0"

Sample unsupported formats:

s.add_development_dependency "rake", "~> 10.5" if on_less_than_1_9_3?
s.add_dependency 'sunspot', Sunspot::VERSION
# File lib/usedby/cli.rb, line 220
def process_gemspec(content, project)
  gems = {}
  dummy_spec = Gem::Specification.new
  content.each_line do |line|
    if line =~ /^\s*(\w+)\.add_(development_dependency|runtime_dependency|dependency)\b/
      spec_name = $1
      begin
        eval line.sub(spec_name, 'dummy_spec')
      rescue => e
        $stderr.puts e
        next
      end
      dep = dummy_spec.dependencies.last
      gem_name = dep.name
      next if @options[:gems] && !@options[:gems].include?(gem_name)
      gem_version_ranges = Bundler::VersionRanges.for(dep.requirement)
      gems[gem_name] = {}
      gems[gem_name][gem_version_ranges] = [project]
    end
  end
  gems
end
remote_file(github, file) click to toggle source
# File lib/usedby/cli.rb, line 129
def remote_file(github, file)
  github_path = "#{file.repository.name}/#{file.path}"
  if filtered?(github_path)
    STDERR.puts "Skipping #{github_path}"
    return
  end
  STDERR.puts "Processing #{github_path}"
  sleep_time = 0
  begin
    content = Base64.decode64(github.get(file.url).content)
  rescue StandardError
    sleep_time += 1
    STDERR.puts "Sleeping #{sleep_time} seconds"
    sleep(sleep_time)
    retry
  end
  content
end
version_ranges_to_s(gem_version_ranges) click to toggle source

Uses mathematical notation for ranges The unbounded range is [0, ∞)

# File lib/usedby/cli.rb, line 245
def version_ranges_to_s(gem_version_ranges)
  if !gem_version_ranges.kind_of?(Array) || gem_version_ranges.size != 2
    $stderr.puts "Unknown format for version ranges: #{gem_version_ranges}"
    return ""
  end

  # base case: a specific version
  if gem_version_ranges[0].size == 1
    range = gem_version_ranges[0][0]
    if range.left.version == range.right.version
      return range.left.version.to_s
    end
  end

  gem_version_ranges[0] = Bundler::VersionRanges::ReqR.reduce(gem_version_ranges[0])

  arr = []
  gem_version_ranges[0].each do |reqr|
    range_begin = reqr.left.inclusive ? "[" : "("
    range_end = reqr.right.inclusive ? "]" : ")"
    arr << "#{range_begin}#{reqr.left.version.to_s}, #{reqr.right.version.to_s}#{range_end}"
  end
  gem_version_ranges[1].each do |neq|
    # special case: exclude specific version
    arr << "!= #{neq.version.to_s}"
  end
  arr.join(", ")
end