diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index c27ce896c88b..2938f30e97e6 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -420,6 +420,7 @@ def add(*gems) method_option "filter-patch", :type => :boolean, :banner => "Only list patch newer versions" method_option "parseable", :aliases => "--porcelain", :type => :boolean, :banner => "Use minimal formatting for more parseable output" + method_option "json", :type => :boolean, :banner => "Produce parseable json output" method_option "only-explicit", :type => :boolean, :banner => "Only list gems specified in your Gemfile, not their dependencies" def outdated(*gems) diff --git a/bundler/lib/bundler/cli/outdated.rb b/bundler/lib/bundler/cli/outdated.rb index 68c701aefbbb..783e5fc2c025 100644 --- a/bundler/lib/bundler/cli/outdated.rb +++ b/bundler/lib/bundler/cli/outdated.rb @@ -53,13 +53,13 @@ def run options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! end - if options[:parseable] + if options[:parseable] || options[:json] Bundler.ui.silence(&definition_resolution) else definition_resolution.call end - Bundler.ui.info "" + Bundler.ui.info "" unless options[:json] # Loop through the current specs gemfile_specs, dependency_specs = current_specs.partition do |spec| @@ -98,27 +98,24 @@ def run end if outdated_gems.empty? - unless options[:parseable] + if options[:json] + print_gems_json([]) + elsif !options[:parseable] Bundler.ui.info(nothing_outdated_message) end else - if options_include_groups - relevant_outdated_gems = outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems| - contains_group = groups.split(", ").include?(options[:group]) - next unless options[:groups] || contains_group - - gems - end.compact - - if options[:parseable] - print_gems(relevant_outdated_gems) - else - print_gems_table(relevant_outdated_gems) - end + relevant_outdated_gems = if options_include_groups + by_group(outdated_gems, :filter => options[:group]) + else + outdated_gems + end + + if options[:json] + print_gems_json(relevant_outdated_gems) elsif options[:parseable] - print_gems(outdated_gems) + print_gems(relevant_outdated_gems) else - print_gems_table(outdated_gems) + print_gems_table(relevant_outdated_gems) end exit 1 @@ -162,6 +159,13 @@ def retrieve_active_spec(definition, current_spec) active_specs.last end + def by_group(gems, filter: nil) + gems.group_by {|g| g[:groups] }.sort.flat_map do |groups_string, grouped_gems| + next if filter && !groups_string.split(", ").include?(filter) + grouped_gems + end.compact + end + def print_gems(gems_list) gems_list.each do |gem| print_gem( @@ -173,6 +177,21 @@ def print_gems(gems_list) end end + def print_gems_json(gems_list) + require "json" + data = gems_list.map do |gem| + gem_data_for( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups] + ) + end + + data = { :outdated_count => gems_list.count, :outdated_gems => data } + Bundler.ui.info data.to_json + end + def print_gems_table(gems_list) data = gems_list.map do |gem| gem_column_for( @@ -212,6 +231,26 @@ def print_gem(current_spec, active_spec, dependency, groups) Bundler.ui.info output_message.rstrip end + def gem_data_for(current_spec, active_spec, dependency, groups) + { + :current_spec => spec_data_for(current_spec), + :active_spec => spec_data_for(active_spec), + :dependency => dependency&.to_s, + :groups => (groups || "").split(", "), + } + end + + def spec_data_for(spec) + { + :name => spec.name, + :version => spec.version.to_s, + :platform => spec.platform, + :source => spec.source.to_s, + :required_ruby_version => spec.required_ruby_version.to_s, + :required_rubygems_version => spec.required_rubygems_version.to_s, + } + end + def gem_column_for(current_spec, active_spec, dependency, groups) current_version = "#{current_spec.version}#{current_spec.git_version}" spec_version = "#{active_spec.version}#{active_spec.git_version}" diff --git a/bundler/spec/bundler/cli_spec.rb b/bundler/spec/bundler/cli_spec.rb index b752cd7e703a..7acdc496e764 100644 --- a/bundler/spec/bundler/cli_spec.rb +++ b/bundler/spec/bundler/cli_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "bundler/cli" +require "json" RSpec.describe "bundle executable" do it "returns non-zero exit status when passed unrecognized options" do @@ -154,6 +155,26 @@ def out_with_macos_man_workaround end end + context "with --json" do + let(:flags) { "--json" } + + it "prints json output data when there are outdated gems" do + run_command + out_data = JSON.parse(out) + expect(out_data.keys).to contain_exactly("outdated_count", "outdated_gems") + expect(out_data["outdated_count"]).to eq(1) + expect(out_data["outdated_gems"].length).to eq(1) + + gem_data = out_data["outdated_gems"].first + expect(gem_data).to include({ + "current_spec" => hash_including("name" => "rack", "version" => "0.9.1"), + "active_spec" => hash_including("name" => "rack", "version" => "1.0.0"), + "dependency" => "rack (= 0.9.1)", + "groups" => ["default"], + }) + end + end + context "with --parseable" do let(:flags) { "--parseable" }