compare_linker_wraper (v1.0.2)のコードリーディングをしました。
使い方はこんな感じで使います。第一引数がGemfile.lockのファイルパス、baseが比較元のブランチ、formatterは出力のフォーマッタです。
compare-linker-wrapper 'Gemfile.lock' \
--base origin/master \
--formatter CompareLinker::Formatter::Markdown
exe/compare-linker-wrapperはCompareLinkerWrapper::CLI.startを呼び出します。
CompareLinkerWrapper::CLI.start(ARGV)
Thorで作られており、default_commandがcompareになっています。compareでは引数を調整しつつ、CompareLinkerWrapper::Linker#linkを呼びだして、戻り値をputsで標準出力に吐き出します。
module CompareLinkerWrapper
class CLI < Thor
# ...
desc 'compare', 'compare gemfile.lock'
option :debug, type: :boolean, default: false
option :verbose, type: :boolean, default: false
option :base, type: :string, default: 'origin/master'
option :head, type: :string, default: 'HEAD'
option :file, type: :array
option :formatter, type: :string, default: 'CompareLinker::Formatter::Text'
def compare(*args)
setup_logger(options)
params = {
head: options[:head],
base: options[:base],
formatter: options[:formatter]
}
if options[:file]
params[:file] = options[:file]
else
params[:file] = args
end
puts Linker.new('.').link(params)
rescue StandardError => e
suggest_messages(options)
raise e
end
default_command :compare
CompareLinkerWrapper::Linker#linkは比較元と比較先のGemfile.lockをパースして比較します。Gemfile.lockはgitのgemを使って取得しています。
module CompareLinkerWrapper
class Linker
# ...
def link(params)
formatter = add_formatter(params[:formatter])
comments = []
params[:file].each do |gemfile_lock|
old_lockfile = parse(git.show(params[:base], gemfile_lock))
new_lockfile = parse(git.show(params[:head], gemfile_lock))
comparator = ::CompareLinker::LockfileComparator.new
comparator.compare(old_lockfile, new_lockfile)
compare_links = comparator.updated_gems.map do |gem_name, gem_info|
# ...後述
end
comments << compare_links
end
comments
end
parseはBundler::LockfileParserのインスタンスを生成します。LockfileParserはその名の通りGemfile.lockをパースしてオブジェクト化したものです。
def parse(lock_file)
Bundler::LockfileParser.new(lock_file)
end
CompareLinker#compareではGemfile.lockのspecsのバージョンやリビジョンが異なっていれば、specの情報をupdated_gemsに格納します。
class CompareLinker
class LockfileComparator
attr_accessor :updated_gems
def initialize
@updated_gems = {}
end
def compare(old_lockfile, new_lockfile)
old_lockfile.specs.each do |old_spec|
new_lockfile.specs.each do |new_spec|
if old_spec.name == new_spec.name
old_rev = old_spec.source.options["revision"]
new_rev = new_spec.source.options["revision"]
if old_rev && new_rev && (old_rev != new_rev)
_, owner, gem_name = old_spec.source.uri.match(/github\.com\/([^\/]+)\/([^.]+)/).to_a
updated_gems[old_spec.name] = {
owner: owner,
gem_name: gem_name,
old_rev: old_rev,
new_rev: new_rev,
}
elsif old_spec.version != new_spec.version
updated_gems[old_spec.name] = {
owner: nil,
gem_name: old_spec.name,
old_ver: old_spec.version.to_s,
new_ver: new_spec.version.to_s,
}
end
end
end
end
updated_gems
end
end
end
updated_gemsのループ内の処理は以下のようになっています。
if gem_info[:owner].nil?
finder = ::CompareLinker::GithubLinkFinder.new(client)
finder.find(gem_name)
if finder.repo_owner.nil?
gem_info[:homepage_uri] = finder.homepage_uri
formatter.format(gem_info)
else
gem_info[:repo_owner] = finder.repo_owner
gem_info[:repo_name] = finder.repo_name
tag_finder = ::CompareLinker::GithubTagFinder.new(client)
old_tag = tag_finder.find(finder.repo_full_name, gem_info[:old_ver])
new_tag = tag_finder.find(finder.repo_full_name, gem_info[:new_ver])
if old_tag && new_tag
gem_info[:old_tag] = old_tag.name
gem_info[:new_tag] = new_tag.name
formatter.format(gem_info)
else
formatter.format(gem_info)
end
end
else
formatter.format(gem_info)
end
ownerがnilの場合=gitではなくrubygemsなどから取得した場合はGithubLinkerFinerでホームページURIやソースコードURIからgithub.comにマッチするものを取得し、そこからリポジトリ名とオーナー名を取得します。
class CompareLinker
class GithubLinkFinder
attr_reader :octokit, :repo_owner, :repo_name, :homepage_uri
def initialize(octokit)
@octokit = octokit
end
def find(gem_name)
gem_info = JSON.parse(
HTTPClient.get_content("https://rubygems.org/api/v1/gems/#{gem_name}.json")
)
github_url = [
gem_info["homepage_uri"],
gem_info["source_code_uri"]
].find { |uri| uri.to_s.match(/github\.com\//) }
if github_url = redirect_url(github_url)
_, @repo_owner, @repo_name = github_url.match(%r!github\.com/([^/]+)/([^/]+)!).to_a
else
@homepage_uri = gem_info["homepage_uri"]
end
rescue HTTPClient::BadResponseError
@homepage_uri = "https://rubygems.org/gems/#{gem_name}"
end
リポジトリ名が取得できたらさらにGithubTagFinderによって対象のバージョンのタグを取得します。
class CompareLinker
class GithubTagFinder
attr_reader :octokit
def initialize(octokit)
@octokit = octokit
end
def find(repo_full_name, gem_version)
tags = octokit.tags(repo_full_name)
if tags
tags.find { |tag|
tag.name == gem_version || tag.name == "v#{gem_version}"
}
end
end
end
end
このようにしてgem_infoのハッシュをハンドリングしてCompareLinker::Formatter::XXX#formatで文字列にフォーマットします。Markdownの場合は以下の通りです。
require "ostruct"
class CompareLinker
class Formatter
class Markdown < Base
def format(gem_info)
g = OpenStruct.new(gem_info)
text = "* [ ] "
text += case
when g.owner && g.old_rev && g.new_rev
"#{g.gem_name}: https://github.com/#{g.owner}/#{g.gem_name}/compare/#{g.old_rev}...#{g.new_rev}"
when g.homepage_uri
"[#{g.gem_name}](#{g.homepage_uri}): #{g.old_ver} => #{g.new_ver}"
when g.old_tag && g.new_tag
"#{g.gem_name}: https://github.com/#{g.repo_owner}/#{g.repo_name}/compare/#{g.old_tag}...#{g.new_tag}"
when g.repo_owner && g.repo_name
"[#{g.gem_name}](https://github.com/#{g.repo_owner}/#{g.repo_name}): #{g.old_ver} => #{g.new_ver}"
else
"#{g.gem_name}: (link not found) #{g.old_ver} => #{g.new_ver}"
end
if downgrade?(g.old_ver, g.new_ver, g.old_tag, g.new_tag)
text += " (downgrade)"
end
text
end
end
end
end