Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [BUGFIX: example](https://github.com/fastruby/next_rails/pull/<number>)
- [FEATURE: Validate the DeprecationTracker mode at initialization, treating a blank mode as the default `save`](https://github.com/fastruby/next_rails/pull/186)
- [BUGFIX: `bundle_report outdated` no longer confuses a locally-sourced (`path:`) gem with a same-named public gem on rubygems; local gems are excluded from the out-of-date check and counted separately](https://github.com/fastruby/next_rails/pull/189)

* Your changes/patches go here.

Expand Down
21 changes: 14 additions & 7 deletions lib/next_rails/bundle_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,27 @@ def compatible_ruby_version(rails_version)

def outdated(format = nil)
gems = NextRails::GemInfo.all
out_of_date_gems = gems.reject(&:up_to_date?).sort_by(&:created_at)
sourced_locally = gems.select(&:sourced_locally?)
sourced_from_git = gems.select(&:sourced_from_git?)

# Locally-sourced gems (e.g. `path:` engines) are excluded from the
# out-of-date check: looking them up by name on rubygems can match an
# unrelated public gem with the same name and report a bogus upgrade.
out_of_date_gems = (gems - sourced_locally).reject(&:up_to_date?).sort_by(&:created_at)

if format == 'json'
output_to_json(out_of_date_gems, gems.count, sourced_from_git.count)
output_to_json(out_of_date_gems, gems.count, sourced_from_git.count, sourced_locally.count)
else
output_to_stdout(out_of_date_gems, gems.count, sourced_from_git.count)
output_to_stdout(out_of_date_gems, gems.count, sourced_from_git.count, sourced_locally.count)
end
end

def output_to_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
obj = build_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
def output_to_json(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
obj = build_json(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
puts JSON.pretty_generate(obj)
end

def build_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
def build_json(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
output = Hash.new { [] }
out_of_date_gems.each do |gem|
output[:outdated_gems] += [
Expand All @@ -95,12 +100,13 @@ def build_json(out_of_date_gems, total_gem_count, sourced_from_git_count)
output.merge(
{
sourced_from_git_count: sourced_from_git_count,
sourced_locally_count: sourced_locally_count,
total_gem_count: total_gem_count
}
)
end

def output_to_stdout(out_of_date_gems, total_gem_count, sourced_from_git_count)
def output_to_stdout(out_of_date_gems, total_gem_count, sourced_from_git_count, sourced_locally_count)
out_of_date_gems.each do |gem|
header = "#{gem.name} #{gem.version}"

Expand All @@ -112,6 +118,7 @@ def output_to_stdout(out_of_date_gems, total_gem_count, sourced_from_git_count)
percentage_out_of_date = ((out_of_date_gems.count / total_gem_count.to_f) * 100).round
footer = <<-MESSAGE
#{NextRails::Tint(sourced_from_git_count.to_s).yellow} gems are sourced from git
#{NextRails::Tint(sourced_locally_count.to_s).yellow} gems are sourced from a local path
#{NextRails::Tint(out_of_date_gems.count.to_s).red} of the #{total_gem_count} gems are out-of-date (#{percentage_out_of_date}%)
MESSAGE

Expand Down
11 changes: 11 additions & 0 deletions lib/next_rails/gem_info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ def sourced_from_git?
!!gem_specification.git_version
end

def sourced_locally?
# Bundler defines Source::Path and patches Gem::Specification#source to return the
# gem's real source. Without Bundler loaded, #source is RubyGems' own and returns a
# Gem::Source::Installed, so no path source can exist; treat the gem as not local.
return false unless defined?(Bundler::Source::Path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can this happen that this class does not exist?

is this some Bundler version thing that it didn't exist in some older versions? it would be good to either add a comment explaining that (it would mean this works only for some minimum bundler version) or find an alternative to this for when Bundler::Source::Path is not defined

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @arielj

This is not a Bundler version thing. Bundler::Source::Path has existed since Bundler 1.0, so any supported version has it. The case the guard covers is Bundler not being loaded at all, since GemInfo is a plain class that can be instantiated without bundler at specs or any caller that does not boot Bundler.

Added a comment for this, thanks for checking

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the script file to reproduce this, run with ruby:

ruby path/to/script.rb

script.rb.zip


source = gem_specification.source
# Git sources subclass Path, so exclude them; they are reported via #sourced_from_git?.
source.is_a?(Bundler::Source::Path) && !source.is_a?(Bundler::Source::Git)
end

def created_at
@created_at ||= gem_specification.date
end
Expand Down
62 changes: 39 additions & 23 deletions spec/next_rails/bundle_report_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
RSpec.describe NextRails::BundleReport do
describe '.outdated' do
let(:mock_version) { Struct.new(:version, :age) }
let(:mock_gem) { Struct.new(:name, :version, :age, :latest_version, :up_to_date?, :created_at, :sourced_from_git?) }
let(:mock_gem) { Struct.new(:name, :version, :age, :latest_version, :up_to_date?, :created_at, :sourced_from_git?, :sourced_locally?) }
let(:format_str) { '%b %e, %Y' }
let(:alpha_date) { Date.parse('2022-01-01') }
let(:alpha_age) { alpha_date.strftime(format_str) }
Expand All @@ -18,50 +18,66 @@
before do
allow(NextRails::GemInfo).to receive(:all).and_return(
[
mock_gem.new('alpha', '0.0.1', alpha_age, mock_version.new('0.0.2', bravo_age), false, alpha_date, false),
mock_gem.new('bravo', '0.2.0', bravo_age, mock_version.new('0.2.2', charlie_age), false, bravo_date, true)
mock_gem.new('alpha', '0.0.1', alpha_age, mock_version.new('0.0.2', bravo_age), false, alpha_date, false, false),
mock_gem.new('bravo', '0.2.0', bravo_age, mock_version.new('0.2.2', charlie_age), false, bravo_date, true, false)
]
)
end

context 'when writing human-readable output' do
#subject { described_class.outdated }

it 'invokes $stdout.puts properly', :aggregate_failures do
allow($stdout)
.to receive(:puts)
.with("#{NextRails::Tint('alpha 0.0.1').bold.white}: released #{alpha_age} (latest version, 0.0.2, released #{bravo_age})\n")
allow($stdout)
.to receive(:puts)
.with("#{NextRails::Tint('bravo 0.2.0').bold.white}: released #{bravo_age} (latest version, 0.2.2, released #{charlie_age})\n")
allow($stdout).to receive(:puts).with('')
allow($stdout).to receive(:puts).with(<<-EO_MULTLINE_STRING)
#{NextRails::Tint('1').yellow} gems are sourced from git
#{NextRails::Tint('2').red} of the 2 gems are out-of-date (100%)
EO_MULTLINE_STRING
it 'prints each out-of-date gem and a footer with the source counts', :aggregate_failures do
output = with_captured_stdout { described_class.outdated }

expect(output).to include("#{NextRails::Tint('alpha 0.0.1').bold.white}: released #{alpha_age} (latest version, 0.0.2, released #{bravo_age})")
expect(output).to include("#{NextRails::Tint('bravo 0.2.0').bold.white}: released #{bravo_age} (latest version, 0.2.2, released #{charlie_age})")
expect(output).to include("#{NextRails::Tint('1').yellow} gems are sourced from git")
expect(output).to include("#{NextRails::Tint('0').yellow} gems are sourced from a local path")
expect(output).to include("#{NextRails::Tint('2').red} of the 2 gems are out-of-date (100%)")
end
end

context 'when writing JSON output' do
it 'JSON is correctly formatted' do
gems = NextRails::GemInfo.all
out_of_date_gems = gems.reject(&:up_to_date?).sort_by(&:created_at)
sourced_from_git = gems.select(&:sourced_from_git?)
output = with_captured_stdout { described_class.outdated('json') }

expect(NextRails::BundleReport.build_json(out_of_date_gems, gems.count, sourced_from_git.count)).to eq(
expect(JSON.parse(output, symbolize_names: true)).to eq(
{
outdated_gems: [
{ name: 'alpha', installed_version: '0.0.1', installed_age: alpha_age, latest_version: '0.0.2',
latest_age: bravo_age },
{ name: 'bravo', installed_version: '0.2.0', installed_age: bravo_age, latest_version: '0.2.2',
latest_age: charlie_age }
],
sourced_from_git_count: sourced_from_git.count,
total_gem_count: gems.count
sourced_from_git_count: 1,
sourced_locally_count: 0,
total_gem_count: 2
}
)
end
end

context 'when a gem is sourced from a local path' do
let(:delta_date) { Date.parse('2022-04-04') }
let(:delta_age) { delta_date.strftime(format_str) }

before do
allow(NextRails::GemInfo).to receive(:all).and_return(
[
mock_gem.new('alpha', '0.0.1', alpha_age, mock_version.new('0.0.2', bravo_age), false, alpha_date, false, false),
# same name as a public gem, but sourced locally and out-of-date
mock_gem.new('delta', '0.1.0', delta_age, mock_version.new('0.1.2', charlie_age), false, delta_date, false, true)
]
)
end

it 'excludes the local gem from the out-of-date list and counts it separately', :aggregate_failures do
output = with_captured_stdout { described_class.outdated('json') }
result = JSON.parse(output, symbolize_names: true)

expect(result[:outdated_gems].map { |gem| gem[:name] }).to eq(['alpha'])
expect(result[:sourced_locally_count]).to eq(1)
end
end
end

describe ".rails_compatibility" do
Expand Down
35 changes: 35 additions & 0 deletions spec/next_rails/gem_info_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,41 @@
end
end

describe "#sourced_locally?" do
let(:source) { nil }
let(:spec) do
Gem::Specification.new do |s|
s.date = release_date
s.version = "1.0.0"
s.source = source
end
end

context "when the gem is sourced from a local path" do
let(:source) { Bundler::Source::Path.new("path" => "engines/foo") }

it "is true" do
expect(subject.sourced_locally?).to be(true)
end
end

context "when the gem is sourced from git" do
let(:source) { Bundler::Source::Git.new("uri" => "https://example.com/foo.git") }

it "is false (git is reported separately)" do
expect(subject.sourced_locally?).to be(false)
end
end

context "when the gem is sourced from rubygems" do
let(:source) { Bundler::Source::Rubygems.new }

it "is false" do
expect(subject.sourced_locally?).to be(false)
end
end
end

describe "#find_latest_compatible" do
let(:mock_gem) { Struct.new(:name, :version) }

Expand Down
Loading