diff --git a/lib/degem.rb b/lib/degem.rb index 7b9275b..036e9dd 100644 --- a/lib/degem.rb +++ b/lib/degem.rb @@ -8,8 +8,9 @@ require_relative "degem/matcher" require_relative "degem/find_unused" require_relative "degem/multi_delegator" -require_relative "degem/decorated" -require_relative "degem/decorate" +require_relative "degem/rubygem" +require_relative "degem/decorate_rubygems" +require_relative "degem/commit" require_relative "degem/git_adapter" require_relative "degem/report" require_relative "degem/cli" diff --git a/lib/degem/cli.rb b/lib/degem/cli.rb index fec2d45..e02e294 100644 --- a/lib/degem/cli.rb +++ b/lib/degem/cli.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Degem class Cli GEMFILE = "Gemfile" @@ -16,18 +18,29 @@ def call return 1 end - rubygems = FindUnused - .new(gemfile_path: GEMFILE, gem_specification: Gem::Specification, grep: Grep.new(@stderr)) - .call - decorated = Decorate - .new(gem_specification: Gem::Specification) - .call(rubygems:, git_adapter: GitAdapter.new) + unused = find_unused.call + decorated = decorate_rubygems.call(unused) Report.new(@stderr).call(decorated) 0 end private + def find_unused + FindUnused.new( + gemfile_path: GEMFILE, + gem_specification: Gem::Specification, + grep: Grep.new(@stderr) + ) + end + + def decorate_rubygems + DecorateRubygems.new( + gem_specification: Gem::Specification, + git_adapter: GitAdapter.new + ) + end + def gemfile_exists? File.file?(GEMFILE) end diff --git a/lib/degem/commit.rb b/lib/degem/commit.rb new file mode 100644 index 0000000..2f4301b --- /dev/null +++ b/lib/degem/commit.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +Commit = Data.define(:hash, :date, :title, :url) diff --git a/lib/degem/decorate.rb b/lib/degem/decorate.rb deleted file mode 100644 index d3decac..0000000 --- a/lib/degem/decorate.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Degem - class Decorate - def initialize(gem_specification:) - @gem_specification = gem_specification - end - - def call(rubygems:, git_adapter:) - rubygems.map do |rubygem| - gemspec = @gem_specification.find_by_name(rubygem.name) - git = git_adapter.call(rubygem.name) - Decorated.new(rubygem, gemspec, git) - end - end - end -end diff --git a/lib/degem/decorate_rubygems.rb b/lib/degem/decorate_rubygems.rb new file mode 100644 index 0000000..44a03e9 --- /dev/null +++ b/lib/degem/decorate_rubygems.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Degem + class DecorateRubygems + def initialize(gem_specification:, git_adapter:) + @gem_specification = gem_specification + @git_adapter = git_adapter + end + + def call(rubygems) + rubygems.map do |rubygem| + gemspec = @gem_specification.find_by_name(rubygem.name) + git = @git_adapter.call(rubygem.name) + Rubygem.new(rubygem, gemspec, git) + end + end + end +end diff --git a/lib/degem/decorated.rb b/lib/degem/decorated.rb index d709f54..0ca9ff1 100644 --- a/lib/degem/decorated.rb +++ b/lib/degem/decorated.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Degem - class Decorated < MultiDelegator + class Rubygem < MultiDelegator attr_reader :commits def initialize(_, _, commits) diff --git a/lib/degem/find_unused.rb b/lib/degem/find_unused.rb index a51a074..e10f15f 100644 --- a/lib/degem/find_unused.rb +++ b/lib/degem/find_unused.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Degem class FindUnused def initialize(gemfile_path:, gem_specification:, grep: Grep.new, bundle_paths: GitLsFiles.new) @@ -22,7 +24,7 @@ def reject_railties(rubygems) .reject { _1.name == "rails" } .reject do |rubygem| gem_path = @gem_specification.find_by_name(rubygem.name).full_gem_path - @grep.inverse?(/(Rails::Railtie|Rails::Engine)/, gem_path) + @grep.match?(/(Rails::Railtie|Rails::Engine)/, gem_path) end end @@ -44,7 +46,7 @@ def matchers end def gemfile - @gemfile = ParseGemfile.new.call(gemfile_path) + @gemfile ||= ParseGemfile.new.call(gemfile_path) end def rails? diff --git a/lib/degem/gemfile.rb b/lib/degem/gemfile.rb index 407c70a..d0e3267 100644 --- a/lib/degem/gemfile.rb +++ b/lib/degem/gemfile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Degem class Gemfile def initialize(dsl) diff --git a/lib/degem/git_adapter.rb b/lib/degem/git_adapter.rb index 4aed90e..ff464b8 100644 --- a/lib/degem/git_adapter.rb +++ b/lib/degem/git_adapter.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + +require "ostruct" +require "open3" + module Degem class GitAdapter - require "ostruct" - require "open3" - def call(gem_name) out, _err, status = git_log(gem_name) return [] unless status.zero? - out.split("\n").map do |raw_commit| - hash, date, title = raw_commit.split("\t") - OpenStruct.new(hash:, date:, title:, url: to_commit_url(hash)) + out.split("\n").map do |commit| + hash, date, title = commit.split("\t") + Commit.new(hash:, date:, title:, url: to_commit_url(hash)) end end @@ -21,7 +23,17 @@ def git_remote_origin_url end def git_log(gem_name) - out, err, status = Open3.capture3("git log --pretty=format:'%H%x09%cs%x09%s' --pickaxe-regex -S '#{gem_name}' -- Gemfile | cat") + out, err, status = Open3.capture3([ + "git log", + "--pretty=format:'%H%x09%cs%x09%s'", + "--pickaxe-regex", + "-S '#{gem_name}'", + "--", + "Gemfile", + "|", + "cat" + ].join(" ")) + [out, err, status.exitstatus] end diff --git a/lib/degem/git_ls_files.rb b/lib/degem/git_ls_files.rb index f9ef629..4963888 100644 --- a/lib/degem/git_ls_files.rb +++ b/lib/degem/git_ls_files.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + +require "open3" + module Degem class GitLsFiles - require "open3" - def call(fallback) out, _err, status = git_ls return fallback unless status.zero? - out.split("\x0").select { _1.end_with?(".rb") }.map { File.expand_path(_1).to_s } + out.split("\x0").select { _1.end_with?(".rb") }.map { File.expand_path(_1) } end private diff --git a/lib/degem/grep.rb b/lib/degem/grep.rb index 466b156..c16859d 100644 --- a/lib/degem/grep.rb +++ b/lib/degem/grep.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + +require "find" + module Degem class Grep - require "find" - def initialize(stderr = StringIO.new) @stderr = stderr end - def inverse?(matcher, dir) + def match?(matcher, dir) Find.find(File.expand_path(dir)) do |path| next unless File.file?(path) next if File.extname(path) != ".rb" diff --git a/lib/degem/matcher.rb b/lib/degem/matcher.rb index 83c52cb..5b4823a 100644 --- a/lib/degem/matcher.rb +++ b/lib/degem/matcher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Degem class Matcher attr_reader :rubygem diff --git a/lib/degem/multi_delegator.rb b/lib/degem/multi_delegator.rb index d1d3ebb..db004f9 100644 --- a/lib/degem/multi_delegator.rb +++ b/lib/degem/multi_delegator.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + module Degem class MultiDelegator def initialize(*delegates) @delegates = delegates end - def method_missing(method, *args, &block) + def method_missing(method, *args) delegate = @delegates.find { _1.respond_to?(method) } - return delegate.public_send(method, *args, &block) if delegate + return delegate.public_send(method, *args) if delegate super end diff --git a/lib/degem/parse_gemfile.rb b/lib/degem/parse_gemfile.rb index 38aae4e..7052ce6 100644 --- a/lib/degem/parse_gemfile.rb +++ b/lib/degem/parse_gemfile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Degem class ParseGemfile def call(gemfile_path) diff --git a/lib/degem/report.rb b/lib/degem/report.rb index 2465c85..3b0f336 100644 --- a/lib/degem/report.rb +++ b/lib/degem/report.rb @@ -1,33 +1,44 @@ +# frozen_string_literal: true + module Degem class Report def initialize(stderr) @stderr = stderr end - def call(decorateds) + def call(rubygems) @stderr.puts @stderr.puts @stderr.puts "The following gems may be unused:" @stderr.puts - decorateds.each do |decorated| - heading = - if decorated.source_code_uri.nil? - decorated.name - else - "#{decorated.name}: #{decorated.source_code_uri}" - end - @stderr.puts(heading) - @stderr.puts("=" * heading.size) + rubygems.each do |rubygem| + gem_name(rubygem) + @stderr.puts + commits(rubygem) @stderr.puts + end + end - decorated.commits.each.with_index do |commit, i| - @stderr.puts("#{commit.hash[0..6]} (#{commit.date}) #{commit.title}") - @stderr.puts(commit.url) - @stderr.puts if i+1 == decorated.commits.size + private + + def gem_name(rubygem) + heading = + if rubygem.source_code_uri.nil? + rubygem.name + else + "#{rubygem.name}: #{rubygem.source_code_uri}" end - @stderr.puts + @stderr.puts(heading) + @stderr.puts("=" * heading.size) + end + + def commits(rubygem) + rubygem.commits.each.with_index do |commit, i| + @stderr.puts("#{commit.hash[0..6]} (#{commit.date}) #{commit.title}") + @stderr.puts(commit.url) + @stderr.puts if i + 1 == rubygem.commits.size end end end diff --git a/lib/degem/rubygem.rb b/lib/degem/rubygem.rb new file mode 100644 index 0000000..0ca9ff1 --- /dev/null +++ b/lib/degem/rubygem.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Degem + class Rubygem < MultiDelegator + attr_reader :commits + + def initialize(_, _, commits) + super + @commits = commits + end + + def source_code_uri + metadata["source_code_uri"] || homepage + end + end +end diff --git a/test/test_degem.rb b/test/test_degem.rb index 392f1ca..2b4aa79 100644 --- a/test/test_degem.rb +++ b/test/test_degem.rb @@ -460,7 +460,7 @@ def test_it_decorates_the_result_with_git_information gem_specification = TestableGemSpecification.new(foo_gemspec_path) - decorateds = Degem::Decorate.new(gem_specification:).call(rubygems:, git_adapter:) + decorateds = Degem::DecorateRubygems.new(gem_specification:, git_adapter:).call(rubygems) assert_equal ["foo"], decorateds.map(&:name) assert_equal [[true]], decorateds.map(&:autorequire) @@ -495,7 +495,7 @@ def test_with_minimal_gemspec_it_decorates_the_result_with_git_information git_adapter = TestableGitAdapter.new gem_specification = TestableGemSpecification.new(foo_gemspec_path) - actual = Degem::Decorate.new(gem_specification:).call(rubygems:, git_adapter:) + actual = Degem::DecorateRubygems.new(gem_specification:, git_adapter:).call(rubygems) assert_equal ["foo"], actual.map(&:name) assert_equal [nil], actual.map(&:autorequire) @@ -548,7 +548,7 @@ def test_it_reports_with_git_information gem_specification = TestableGemSpecification.new([foo_gemspec_path, bar_gemspec_path]) - decorated = Degem::Decorate.new(gem_specification:).call(rubygems:, git_adapter:) + decorated = Degem::DecorateRubygems.new(gem_specification:, git_adapter:).call(rubygems) stderr = StringIO.new Degem::Report.new(stderr).call(decorated)