libyear-bundler 0.3.0 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 734b3af6eedfa8bd832666a33e04ccf4d36dbcec
4
- data.tar.gz: 70d4f09011a3102403bec195b4638d7dbbbc73d8
3
+ metadata.gz: 19cf55f9516aaf1b3ea4e870df4786d871844aec
4
+ data.tar.gz: 1f9ed94b7ce2d79bdebbb07fa8d59e2d7bfc6d5c
5
5
  SHA512:
6
- metadata.gz: cb0e53a70abcd38aba307d8ea2dac3ffbace0192cd26a8f2b2de960a42c36e6aa4fbae86d951dbfa9eab27796dddbd73d10e8fa7fbfb952dc3e3795bcc595e61
7
- data.tar.gz: 65b896dcdbbbc1feea606390b5a20b54680c95e8feb763989a651d1e646f11c2799d83c0e7c54c9c40ca1d464d67fcec3932eb15c2e058b08ad30384d28d8a31
6
+ metadata.gz: '09111fa6dbb524adb6f3a6af71f2fcd4803451f22e3d6d289cb61d8ef926553c781ddd3b801f33d7bbb7d957ea8fac52388982d752bb96625feedb3fd304aca2'
7
+ data.tar.gz: 7c9f3884523e18478563d45d5f35ef4f34f8b5d60d5fca78c369420a32dd3de6d3070e298347cc15d486c80405e357e0926b140abaf07e3d8d7a515dd664a91e
@@ -0,0 +1,63 @@
1
+ require "English"
2
+ require "open3"
3
+ require 'libyear_bundler/calculators/libyear'
4
+ require 'libyear_bundler/calculators/version_number_delta'
5
+ require 'libyear_bundler/calculators/version_sequence_delta'
6
+ require 'libyear_bundler/models/gem'
7
+
8
+ module LibyearBundler
9
+ # Responsible for getting all the data that goes into the `Report`.
10
+ class BundleOutdated
11
+ # Format of `bundle outdated --parseable` (BOP)
12
+ BOP_FMT = /\A(?<name>[^ ]+) \(newest (?<newest>[^,]+), installed (?<installed>[^,)]+)/
13
+
14
+ def initialize(gemfile_path)
15
+ @gemfile_path = gemfile_path
16
+ end
17
+
18
+ def execute
19
+ bundle_outdated.lines.each_with_object([]) do |line, gems|
20
+ match = BOP_FMT.match(line)
21
+ next if match.nil?
22
+ if malformed_version_strings?(match)
23
+ warn "Skipping #{match['name']} because of a malformed version string"
24
+ next
25
+ end
26
+
27
+ gem = ::LibyearBundler::Models::Gem.new(
28
+ match['name'],
29
+ match['installed'],
30
+ match['newest']
31
+ )
32
+ gems.push(gem)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def bundle_outdated
39
+ stdout, stderr, status = Open3.capture3(
40
+ %(BUNDLE_GEMFILE="#{@gemfile_path}" bundle outdated --parseable)
41
+ )
42
+ # Known statuses:
43
+ # 0 - Nothing is outdated
44
+ # 256 - Something is outdated
45
+ # 1792 - Unable to determine if something is outdated
46
+ unless [0, 256].include?(status.to_i)
47
+ $stderr.puts "`bundle outdated` failed with status: #{status.to_i}"
48
+ $stderr.puts "stderr: #{stderr}"
49
+ $stderr.puts "stdout: #{stdout}"
50
+ $stderr.puts "Try running `bundle install`."
51
+ Kernel.exit(CLI::E_BUNDLE_OUTDATED_FAILED)
52
+ end
53
+ stdout
54
+ end
55
+
56
+ # We rely on Gem::Version to handle version strings. If the string is malformed (usually because
57
+ # of a gem installed from git), then we won't be able to determine the dependency's freshness
58
+ def malformed_version_strings?(dependency)
59
+ !Gem::Version.correct?(dependency['installed']) ||
60
+ !Gem::Version.correct?(dependency['newest'])
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,23 @@
1
+ module LibyearBundler
2
+ module Calculators
3
+ # A libyear is the difference in time between releases of the newest and
4
+ # installed versions of the gem in years
5
+ class Libyear
6
+ class << self
7
+ def calculate(installed_version_release_date, newest_version_release_date)
8
+ di = installed_version_release_date
9
+ dn = newest_version_release_date
10
+ if di.nil? || dn.nil? || dn <= di
11
+ # Known issue: Backports and maintenance releases of older minor versions.
12
+ # Example: json 1.8.6 (2017-01-13) was released *after* 2.0.3 (2017-01-12)
13
+ years = 0.0
14
+ else
15
+ days = (dn - di).to_f
16
+ years = days / 365.0
17
+ end
18
+ years
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ module LibyearBundler
2
+ module Calculators
3
+ # The version number delta is the absolute difference between the highest-
4
+ # order version number of the installed and newest releases
5
+ class VersionNumberDelta
6
+ class << self
7
+ def calculate(installed_version, newest_version)
8
+ installed_version_tuple = version_tuple(installed_version.to_s.split('.'))
9
+ newest_version_tuple = version_tuple(newest_version.to_s.split('.'))
10
+ major_version_delta = version_delta(
11
+ newest_version_tuple.major, installed_version_tuple.major
12
+ )
13
+ minor_version_delta = version_delta(
14
+ newest_version_tuple.minor, installed_version_tuple.minor
15
+ )
16
+ patch_version_delta = version_delta(
17
+ newest_version_tuple.patch, installed_version_tuple.patch
18
+ )
19
+ highest_order([major_version_delta, minor_version_delta, patch_version_delta])
20
+ end
21
+
22
+ private
23
+
24
+ def highest_order(arr)
25
+ arr[1] = arr[2] = 0 if arr[0] > 0
26
+ arr[2] = 0 if arr[1] > 0
27
+ arr
28
+ end
29
+
30
+ def version_delta(newest_version, installed_version)
31
+ delta = newest_version - installed_version
32
+ delta < 0 ? 0 : delta
33
+ end
34
+
35
+ def version_tuple(version_array)
36
+ version_struct = Struct.new(:major, :minor, :patch)
37
+ version_struct.new(
38
+ version_array[0].to_i,
39
+ version_array[1].to_i,
40
+ version_array[2].to_i
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module LibyearBundler
2
+ module Calculators
3
+ # The version sequence delta is the number of releases between the newest and
4
+ # installed versions of the gem
5
+ class VersionSequenceDelta
6
+ class << self
7
+ def calculate(installed_seq_index, newest_seq_index)
8
+ installed_seq_index - newest_seq_index
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,28 +1,31 @@
1
1
  require "bundler/cli"
2
2
  require "bundler/cli/outdated"
3
+ require "libyear_bundler/bundle_outdated"
4
+ require "libyear_bundler/options"
3
5
  require "libyear_bundler/report"
4
- require "libyear_bundler/query"
6
+ require 'libyear_bundler/models/ruby'
5
7
 
6
8
  module LibyearBundler
9
+ # The `libyear-bundler` command line program
7
10
  class CLI
8
- OPTIONS = %w(
9
- --grand-total
10
- ).freeze
11
-
12
11
  E_BUNDLE_OUTDATED_FAILED = 1
13
12
  E_NO_GEMFILE = 2
13
+ E_INVALID_CLI_ARG = 3
14
14
 
15
15
  def initialize(argv)
16
+ @options = ::LibyearBundler::Options.new(argv).parse
17
+ # Command line flags are removed form `argv` in `Options` by
18
+ # `OptionParser`, leaving non-flag command line arguments,
19
+ # such as a Gemfile path
16
20
  @argv = argv
17
21
  @gemfile_path = load_gemfile_path
18
- validate_arguments
19
22
  end
20
23
 
21
24
  def run
22
- if @argv.include?("--grand-total")
25
+ if @options.grand_total?
23
26
  grand_total
24
27
  else
25
- print Report.new(query).to_s
28
+ print report.to_s
26
29
  end
27
30
  end
28
31
 
@@ -46,34 +49,57 @@ module LibyearBundler
46
49
  '' # `bundle outdated` will default to local Gemfile
47
50
  else
48
51
  $stderr.puts "Gemfile not found"
49
- exit
52
+ exit E_NO_GEMFILE
50
53
  end
51
54
  end
52
55
 
53
- def query
54
- Query.new(@gemfile_path).execute
56
+ def bundle_outdated
57
+ BundleOutdated.new(@gemfile_path).execute
55
58
  end
56
59
 
57
- def unexpected_options
58
- @_unexpected_options ||= begin
59
- options = @argv.select { |arg| arg.start_with?("--") }
60
- options.each_with_object([]) do |arg, memo|
61
- memo << arg unless OPTIONS.include?(arg)
62
- end
63
- end
60
+ def report
61
+ @_report ||= Report.new(bundle_outdated, ruby, @options)
64
62
  end
65
63
 
66
- def validate_arguments
67
- unless unexpected_options.empty?
68
- puts "Unexpected args: #{unexpected_options.join(", ")}"
69
- puts "Allowed args: #{OPTIONS.join(", ")}"
70
- exit E_NO_GEMFILE
71
- end
64
+ def ruby
65
+ lockfile = @gemfile_path + '.lock'
66
+ ::LibyearBundler::Models::Ruby.new(lockfile)
72
67
  end
73
68
 
74
69
  def grand_total
75
- sum_years = query.map { |gem| gem[:libyears] }.inject(0.0, :+)
76
- puts format("%.1f", sum_years)
70
+ puts calculate_grand_total
71
+ end
72
+
73
+ def calculate_grand_total
74
+ if [:libyears?, :releases?, :versions?].all? { |opt| @options[opt] }
75
+ [
76
+ libyears_grand_total,
77
+ releases_grand_total,
78
+ versions_grand_total
79
+ ].join("\n")
80
+ elsif @options.releases?
81
+ releases_grand_total
82
+ elsif @options.versions?
83
+ versions_grand_total
84
+ else
85
+ libyears_grand_total
86
+ end
87
+ end
88
+
89
+ def libyears_grand_total
90
+ report.to_h[:sum_libyears].truncate(1)
91
+ end
92
+
93
+ def releases_grand_total
94
+ report.to_h[:sum_seq_delta]
95
+ end
96
+
97
+ def versions_grand_total
98
+ [
99
+ report.to_h[:sum_major_version],
100
+ report.to_h[:sum_minor_version],
101
+ report.to_h[:sum_patch_version]
102
+ ].to_s
77
103
  end
78
104
  end
79
105
  end
@@ -0,0 +1,104 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module LibyearBundler
6
+ module Models
7
+ # Logic and information pertaining to the installed and newest versions of
8
+ # a gem
9
+ class Gem
10
+ def initialize(name, installed_version, newest_version)
11
+ @name = name
12
+ @installed_version = installed_version
13
+ @newest_version = newest_version
14
+ end
15
+
16
+ def installed_version
17
+ ::Gem::Version.new(@installed_version)
18
+ end
19
+
20
+ def installed_version_release_date
21
+ release_date(name, installed_version)
22
+ end
23
+
24
+ def installed_version_sequence_index
25
+ versions_sequence.index(installed_version.to_s)
26
+ end
27
+
28
+ def libyears
29
+ ::LibyearBundler::Calculators::Libyear.calculate(
30
+ installed_version_release_date,
31
+ newest_version_release_date
32
+ )
33
+ end
34
+
35
+ def name
36
+ @name
37
+ end
38
+
39
+ def newest_version
40
+ ::Gem::Version.new(@newest_version)
41
+ end
42
+
43
+ def newest_version_sequence_index
44
+ versions_sequence.index(newest_version.to_s)
45
+ end
46
+
47
+ def newest_version_release_date
48
+ release_date(name, newest_version)
49
+ end
50
+
51
+ def version_number_delta
52
+ ::LibyearBundler::Calculators::VersionNumberDelta.calculate(
53
+ installed_version,
54
+ newest_version
55
+ )
56
+ end
57
+
58
+ def version_sequence_delta
59
+ ::LibyearBundler::Calculators::VersionSequenceDelta.calculate(
60
+ installed_version_sequence_index,
61
+ newest_version_sequence_index
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ # docs: http://guides.rubygems.org/rubygems-org-api/#gem-version-methods
68
+ # Versions are returned ordered by version number, descending
69
+ def versions_sequence
70
+ @_versions_sequence ||= begin
71
+ uri = URI.parse("https://rubygems.org/api/v1/versions/#{name}.json")
72
+ response = Net::HTTP.get_response(uri)
73
+ parsed_response = JSON.parse(response.body)
74
+ parsed_response.map { |version| version['number'] }
75
+ end
76
+ end
77
+
78
+ # Known issue: Probably performs a network request every time, unless
79
+ # there's some kind of caching.
80
+ def release_date(gem_name, gem_version)
81
+ dep = nil
82
+ begin
83
+ dep = ::Bundler::Dependency.new(gem_name, gem_version)
84
+ rescue ::Gem::Requirement::BadRequirementError => e
85
+ $stderr.puts "Could not find release date for: #{gem_name}"
86
+ $stderr.puts(e)
87
+ $stderr.puts(
88
+ "Maybe you used git in your Gemfile, which libyear doesn't support " \
89
+ "yet. Contributions welcome."
90
+ )
91
+ return nil
92
+ end
93
+ tuples, _errors = ::Gem::SpecFetcher.fetcher.search_for_dependency(dep)
94
+ if tuples.empty?
95
+ $stderr.puts "Could not find release date for: #{gem_name}"
96
+ return nil
97
+ end
98
+ tup, source = tuples.first # Gem::NameTuple
99
+ spec = source.fetch_spec(tup) # raises Gem::RemoteFetcher::FetchError
100
+ spec.date.to_date
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,155 @@
1
+ require 'bundler/lockfile_parser'
2
+ require 'bundler/ruby_version'
3
+ require 'date'
4
+ require 'net/http'
5
+ require 'yaml'
6
+
7
+ require 'libyear_bundler/calculators/libyear'
8
+ require 'libyear_bundler/calculators/version_number_delta'
9
+ require 'libyear_bundler/calculators/version_sequence_delta'
10
+
11
+ module LibyearBundler
12
+ module Models
13
+ # Logic and information pertaining to the installed and newest Ruby versions
14
+ class Ruby
15
+ RUBY_VERSION_DATA_URL = "https://raw.githubusercontent.com/ruby/" \
16
+ "www.ruby-lang.org/master/_data/releases.yml".freeze
17
+
18
+ def initialize(lockfile)
19
+ @lockfile = lockfile
20
+ end
21
+
22
+ def installed_version
23
+ @_installed_version ||= begin
24
+ version_from_bundler ||
25
+ version_from_ruby_version_file ||
26
+ version_from_ruby
27
+ end
28
+ end
29
+
30
+ def installed_version_release_date
31
+ release_date(installed_version.to_s)
32
+ end
33
+
34
+ def libyears
35
+ ::LibyearBundler::Calculators::Libyear.calculate(
36
+ release_date(installed_version.to_s),
37
+ release_date(newest_version.to_s)
38
+ )
39
+ end
40
+
41
+ def name
42
+ 'ruby'
43
+ end
44
+
45
+ def newest_version
46
+ ::Gem::Version.new(all_stable_versions.first)
47
+ end
48
+
49
+ def newest_version_release_date
50
+ release_date(newest_version.to_s)
51
+ end
52
+
53
+ def outdated?
54
+ installed_version < newest_version
55
+ end
56
+
57
+ def version_number_delta
58
+ ::LibyearBundler::Calculators::VersionNumberDelta.calculate(
59
+ installed_version,
60
+ newest_version
61
+ )
62
+ end
63
+
64
+ def version_sequence_delta
65
+ ::LibyearBundler::Calculators::VersionSequenceDelta.calculate(
66
+ installed_version_sequence_index,
67
+ newest_version_sequence_index
68
+ )
69
+ end
70
+
71
+ private
72
+
73
+ # The following URL is the only official, easily-parseable document with
74
+ # Ruby version information that I'm aware of, but is not supported as such
75
+ # (https://github.com/ruby/www.ruby-lang.org/pull/1637#issuecomment-344934173).
76
+ # It's been recommend that ruby-lang.org provide a supported document:
77
+ # https://github.com/ruby/www.ruby-lang.org/pull/1637#issuecomment-344934173
78
+ # TODO: Use supported document with version information if it becomes
79
+ # available.
80
+ def all_versions
81
+ @_all_versions ||= begin
82
+ uri = ::URI.parse(RUBY_VERSION_DATA_URL)
83
+ response = ::Net::HTTP.get_response(uri)
84
+ # The Date object is passed through here due to a bug in
85
+ # YAML#safe_load
86
+ # https://github.com/ruby/psych/issues/262
87
+ ::YAML.safe_load(response.body, [Date])
88
+ .map { |release| release['version'] }
89
+ end
90
+ end
91
+
92
+ # We'll only consider non-prerelease versions when analyzing ruby version,
93
+ # which we also implcitly do for gem versions because that's bundler's
94
+ # default behavior
95
+ def all_stable_versions
96
+ all_versions.reject do |version|
97
+ ::Gem::Version.new(version).prerelease?
98
+ end
99
+ end
100
+
101
+ def installed_version_sequence_index
102
+ all_stable_versions.index(installed_version.to_s)
103
+ end
104
+
105
+ def newest_version_sequence_index
106
+ all_stable_versions.find_index(newest_version.to_s)
107
+ end
108
+
109
+ def release_date(version)
110
+ v = all_stable_versions.detect { |ver| ver == version }
111
+
112
+ if v.nil?
113
+ raise format('Cannot determine release date for ruby %s', version)
114
+ end
115
+
116
+ # YAML#safe_load provides an already-parsed Date object, so the following
117
+ # is a Date object
118
+ v['date']
119
+ end
120
+
121
+ def shell_out_to_ruby
122
+ # ruby appends a 'p' followed by the patch level number
123
+ # to the version number for stable releases, which returns
124
+ # a false positive using `::Gem::Version#prerelease?`.
125
+ # Understandably, because ruby is not a gem, but we'd like
126
+ # to use `prerelease?`.
127
+ # Pre-releases are appended with 'dev', and so adhere to
128
+ # `::Gem::Version`'s definition of a pre-release.
129
+ # Sources:
130
+ # - https://github.com/ruby/ruby/blob/trunk/version.h#L37
131
+ # - https://ruby-doc.org/stdlib-1.9.3/libdoc/rubygems/rdoc/Version.html
132
+ `ruby --version`.split[1].gsub(/p\d*/, '')
133
+ end
134
+
135
+ def version_from_bundler
136
+ return unless File.exist?(@lockfile)
137
+ ruby_version_string = ::Bundler::LockfileParser
138
+ .new(@lockfile)
139
+ .ruby_version
140
+ return if ruby_version_string.nil?
141
+
142
+ ::Bundler::RubyVersion.from_string(ruby_version_string).gem_version
143
+ end
144
+
145
+ def version_from_ruby_version_file
146
+ return unless ::File.exist?('.ruby-version')
147
+ ::Gem::Version.new(::File.read('.ruby-version').strip)
148
+ end
149
+
150
+ def version_from_ruby
151
+ ::Gem::Version.new(shell_out_to_ruby)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,63 @@
1
+ require 'optparse'
2
+ require 'libyear_bundler/version'
3
+ require "libyear_bundler/cli"
4
+ require 'ostruct'
5
+
6
+ module LibyearBundler
7
+ # Uses OptionParser from Ruby's stdlib to hand command-line arguments
8
+ class Options
9
+ BANNER = <<-BANNER.freeze
10
+ Usage: libyear-bundler [Gemfile ...] [options]
11
+ https://github.com/jaredbeck/libyear-bundler/
12
+ BANNER
13
+
14
+ def initialize(argv)
15
+ @argv = argv
16
+ @options = ::OpenStruct.new
17
+ @optparser = OptionParser.new do |opts|
18
+ opts.banner = BANNER
19
+ opts.program_name = 'libyear-bundler'
20
+ opts.version = ::LibyearBundler::VERSION
21
+ @options.send('libyears?=', true)
22
+
23
+ opts.on_head('-h', '--help', 'Prints this help') do
24
+ puts opts
25
+ exit
26
+ end
27
+
28
+ opts.on('--all', 'Calculate all metrics') do
29
+ @options.send('libyears?=', true)
30
+ @options.send('releases?=', true)
31
+ @options.send('versions?=', true)
32
+ end
33
+
34
+ opts.on('--libyears', '[default] Calculate libyears out-of-date') do
35
+ @options.send('libyears?=', true)
36
+ end
37
+
38
+ opts.on('--releases', 'Calculate number of releases out-of-date') do
39
+ @options.send('libyears?=', false)
40
+ @options.send('releases?=', true)
41
+ end
42
+
43
+ opts.on('--versions', 'Calculate major, minor, and patch versions out-of-date') do
44
+ @options.send('libyears?=', false)
45
+ @options.send('versions?=', true)
46
+ end
47
+
48
+ opts.on('--grand-total', 'Return value for given metric(s)') do
49
+ @options.send('grand_total?=', true)
50
+ end
51
+ end
52
+ end
53
+
54
+ def parse
55
+ @optparser.parse!(@argv)
56
+ @options
57
+ rescue OptionParser::InvalidOption => e
58
+ warn e
59
+ warn @optparser.help
60
+ exit ::LibyearBundler::CLI::E_INVALID_CLI_ARG
61
+ end
62
+ end
63
+ end
@@ -1,30 +1,149 @@
1
1
  module LibyearBundler
2
- # Responsible presenting data from the `Query`. Should only be concerned
3
- # with presentation, nothing else.
2
+ # Responsible presenting data from the `::LibyearBundler::Models`. Should only
3
+ # be concerned with presentation, nothing else.
4
4
  class Report
5
- # `gems` - Array of hashes.
6
- def initialize(gems)
5
+ FMT_LIBYEARS_COLUMN = "%10.1f".freeze
6
+ FMT_RELEASES_COLUMN = "%10d".freeze
7
+ FMT_VERSIONS_COLUMN = "%15s".freeze
8
+ FMT_SUMMARY_COLUMNS = "%30s%15s%15s%15s%15s".freeze
9
+
10
+ # `gems` - Array of `::LibyearBundler::Models::Gem` instances
11
+ # `options` - Instance of `::LibyearBundler::Options`
12
+ def initialize(gems, ruby, options)
7
13
  @gems = gems
14
+ @ruby = ruby
15
+ @options = options
8
16
  end
9
17
 
10
18
  def to_s
11
- sum_years = 0.0
12
- @gems.each do |gem|
13
- years = gem[:libyears]
14
- sum_years += years
15
- puts(
16
- format(
17
- "%30s%15s%15s%15s%15s%10.1f",
18
- gem[:name],
19
- gem[:installed][:version],
20
- gem[:installed][:date],
21
- gem[:newest][:version],
22
- gem[:newest][:date],
23
- years
24
- )
19
+ to_h[:gems].each { |gem| put_line_summary(gem) }
20
+
21
+ begin
22
+ put_line_summary(@ruby) if @ruby.outdated?
23
+ rescue StandardError => e
24
+ warn "Unable to calculate libyears for ruby itself: #{e} (line summary)"
25
+ end
26
+
27
+ put_summary(to_h)
28
+ end
29
+
30
+ def to_h
31
+ @_to_h ||=
32
+ begin
33
+ summary = {
34
+ gems: @gems,
35
+ sum_libyears: 0.0
36
+ }
37
+ @gems.each { |gem| increment_metrics_summary(gem, summary) }
38
+
39
+ begin
40
+ increment_metrics_summary(@ruby, summary) if @ruby.outdated?
41
+ rescue StandardError => e
42
+ warn "Unable to calculate libyears for ruby itself: #{e}"
43
+ end
44
+
45
+ summary
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def increment_metrics_summary(model, summary)
52
+ increment_libyears(model, summary) if @options.libyears?
53
+ increment_version_deltas(model, summary) if @options.versions?
54
+ increment_seq_deltas(model, summary) if @options.releases?
55
+ end
56
+
57
+ def put_line_summary(gem_or_ruby)
58
+ meta = meta_line_summary(gem_or_ruby)
59
+
60
+ if @options.releases?
61
+ releases = format(FMT_RELEASES_COLUMN, gem_or_ruby.version_sequence_delta)
62
+ meta << releases
63
+ end
64
+
65
+ if @options.versions?
66
+ versions = format(FMT_VERSIONS_COLUMN, gem_or_ruby.version_number_delta)
67
+ meta << versions
68
+ end
69
+
70
+ if @options.libyears?
71
+ libyears = format(FMT_LIBYEARS_COLUMN, gem_or_ruby.libyears)
72
+ meta << libyears
73
+ end
74
+
75
+ puts meta
76
+ end
77
+
78
+ def meta_line_summary(gem_or_ruby)
79
+ format(
80
+ FMT_SUMMARY_COLUMNS,
81
+ gem_or_ruby.name,
82
+ gem_or_ruby.installed_version.to_s,
83
+ gem_or_ruby.installed_version_release_date,
84
+ gem_or_ruby.newest_version.to_s,
85
+ gem_or_ruby.newest_version_release_date
86
+ )
87
+ end
88
+
89
+ def put_libyear_summary(sum_libyears)
90
+ puts format("System is %.1f libyears behind", sum_libyears)
91
+ end
92
+
93
+ def put_version_delta_summary(sum_major_version, sum_minor_version, sum_patch_version)
94
+ puts format(
95
+ "Major, minor, patch versions behind: %<major>d, %<minor>d, %<patch>d",
96
+ major: sum_major_version || 0,
97
+ minor: sum_minor_version || 0,
98
+ patch: sum_patch_version || 0
99
+ )
100
+ end
101
+
102
+ def put_sum_seq_delta_summary(sum_seq_delta)
103
+ puts format(
104
+ "Total releases behind: %<seq_delta>d",
105
+ seq_delta: sum_seq_delta || 0
106
+ )
107
+ end
108
+
109
+ def put_summary(summary)
110
+ if [:libyears?, :releases?, :versions?].all? { |opt| @options.send(opt) }
111
+ put_libyear_summary(summary[:sum_libyears])
112
+ put_sum_seq_delta_summary(summary[:sum_seq_delta])
113
+ put_version_delta_summary(
114
+ summary[:sum_major_version],
115
+ summary[:sum_minor_version],
116
+ summary[:sum_patch_version]
117
+ )
118
+ elsif @options.versions?
119
+ put_version_delta_summary(
120
+ summary[:sum_major_version],
121
+ summary[:sum_minor_version],
122
+ summary[:sum_patch_version]
25
123
  )
124
+ elsif @options.releases?
125
+ put_sum_seq_delta_summary(summary[:sum_seq_delta])
126
+ elsif @options.libyears?
127
+ put_libyear_summary(summary[:sum_libyears])
26
128
  end
27
- puts format("System is %.1f libyears behind", sum_years)
129
+ end
130
+
131
+ def increment_libyears(model, memo)
132
+ memo[:sum_libyears] += model.libyears
133
+ end
134
+
135
+ def increment_seq_deltas(model, memo)
136
+ memo[:sum_seq_delta] ||= 0
137
+ memo[:sum_seq_delta] += model.version_sequence_delta
138
+ end
139
+
140
+ def increment_version_deltas(model, memo)
141
+ memo[:sum_major_version] ||= 0
142
+ memo[:sum_major_version] += model.version_number_delta[0]
143
+ memo[:sum_minor_version] ||= 0
144
+ memo[:sum_minor_version] += model.version_number_delta[1]
145
+ memo[:sum_patch_version] ||= 0
146
+ memo[:sum_patch_version] += model.version_number_delta[2]
28
147
  end
29
148
  end
30
149
  end
@@ -1,3 +1,3 @@
1
1
  module LibyearBundler
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.3".freeze
3
3
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'libyear_bundler/version'
@@ -11,12 +12,26 @@ Gem::Specification.new do |spec|
11
12
  spec.summary = "A simple measure of dependency freshness"
12
13
  spec.homepage = "https://libyear.com"
13
14
  spec.licenses = ["GPL-3.0"]
14
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
15
- f.match(%r{^(test|spec|features)/})
15
+ spec.files = `git ls-files -z`.split("\x0").select do |f|
16
+ f.start_with?('lib/') ||
17
+ [
18
+ 'bin/libyear-bundler',
19
+ 'libyear-bundler.gemspec',
20
+ 'LICENSE.txt'
21
+ ].include?(f)
16
22
  end
17
23
  spec.bindir = "bin"
18
24
  spec.executables = ["libyear-bundler"]
19
25
  spec.require_paths = ["lib"]
20
26
  spec.required_ruby_version = ">= 2.1"
21
- spec.add_dependency "bundler", "~> 1.14"
27
+
28
+ # We will support bundler 1 as long as we can. It's important that people
29
+ # with badly out-of-date systems can still measure how bad they are.
30
+ spec.add_dependency "bundler", ">= 1.14", "< 3"
31
+
32
+ spec.add_development_dependency "rspec", "~> 3.9"
33
+
34
+ # rubocop 0.57 drops support for ruby 2.1, so 0.56 is the highest we can
35
+ # use. We're going to support ruby 2.1 as long as possible. See above.
36
+ spec.add_development_dependency "rubocop", "~> 0.56.0"
22
37
  end
metadata CHANGED
@@ -1,29 +1,63 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libyear-bundler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jared Beck
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-24 00:00:00.000000000 Z
11
+ date: 2020-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.14'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '1.14'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.9'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.9'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rubocop
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.56.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.56.0
27
61
  description:
28
62
  email:
29
63
  - jared@jaredbeck.com
@@ -32,15 +66,17 @@ executables:
32
66
  extensions: []
33
67
  extra_rdoc_files: []
34
68
  files:
35
- - ".gitignore"
36
- - CHANGELOG.md
37
- - Gemfile
38
69
  - LICENSE.txt
39
- - README.md
40
70
  - bin/libyear-bundler
41
71
  - lib/libyear_bundler.rb
72
+ - lib/libyear_bundler/bundle_outdated.rb
73
+ - lib/libyear_bundler/calculators/libyear.rb
74
+ - lib/libyear_bundler/calculators/version_number_delta.rb
75
+ - lib/libyear_bundler/calculators/version_sequence_delta.rb
42
76
  - lib/libyear_bundler/cli.rb
43
- - lib/libyear_bundler/query.rb
77
+ - lib/libyear_bundler/models/gem.rb
78
+ - lib/libyear_bundler/models/ruby.rb
79
+ - lib/libyear_bundler/options.rb
44
80
  - lib/libyear_bundler/report.rb
45
81
  - lib/libyear_bundler/version.rb
46
82
  - libyear-bundler.gemspec
@@ -64,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
100
  version: '0'
65
101
  requirements: []
66
102
  rubyforge_project:
67
- rubygems_version: 2.6.11
103
+ rubygems_version: 2.5.2.3
68
104
  signing_key:
69
105
  specification_version: 4
70
106
  summary: A simple measure of dependency freshness
data/.gitignore DELETED
@@ -1,10 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- .ruby-version
@@ -1,100 +0,0 @@
1
- # libyear
2
-
3
- This project follows [semver 2.0.0][1] and the recommendations
4
- of [keepachangelog.com][2].
5
-
6
- ## Unreleased
7
-
8
- Breaking changes:
9
-
10
- - None
11
-
12
- Added:
13
-
14
- - None
15
-
16
- Fixed:
17
-
18
- - None
19
-
20
- ## 0.3.0 (2017-03-24)
21
-
22
- Breaking changes:
23
-
24
- - None
25
-
26
- Added:
27
-
28
- - [#1](https://github.com/jaredbeck/libyear-bundler/pull/1)
29
- Add --grand-total option
30
-
31
- Fixed:
32
-
33
- - None
34
-
35
- ## 0.2.0 (2017-03-10)
36
-
37
- Breaking changes:
38
-
39
- - Rename project
40
- - Rename project from libyear-rb to libyear-bundler
41
- - Rename binary from libyear to libyear-bundler
42
- - Discussion: https://github.com/jaredbeck/libyear-rb/issues/1
43
-
44
- Added:
45
-
46
- - None
47
-
48
- Fixed:
49
-
50
- - None
51
-
52
- ## 0.1.3 (2017-03-07)
53
-
54
- Breaking changes:
55
-
56
- - None
57
-
58
- Added:
59
-
60
- - None
61
-
62
- Fixed:
63
-
64
- - Don't crash when Gemfile uses git
65
-
66
- ## 0.1.2 (2017-02-16)
67
-
68
- Breaking changes:
69
-
70
- - None
71
-
72
- Added:
73
-
74
- - None
75
-
76
- Fixed:
77
-
78
- - Better handling of weird sources like rails-assets
79
- - Wider report columns
80
-
81
- ## 0.1.1 (2017-02-14)
82
-
83
- Breaking changes:
84
-
85
- - None
86
-
87
- Added:
88
-
89
- - None
90
-
91
- Fixed:
92
-
93
- - Better handling of error when bundle outdated fails
94
-
95
- ## 0.1.0 (2017-02-13)
96
-
97
- Initial version. Proof of concept.
98
-
99
- [1]: http://semver.org/spec/v2.0.0.html
100
- [2]: http://keepachangelog.com/
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in libyear.gemspec
4
- gemspec
data/README.md DELETED
@@ -1,30 +0,0 @@
1
- # Libyear
2
-
3
- A simple measure of dependency freshness for ruby apps.
4
-
5
- A libyear (library year) is a measure of how old a software dependency is.
6
-
7
- If your system has two dependencies, the first one year old, the second three,
8
- then your system is four libyears out-of-date.
9
-
10
- A dependency is one year old when the version you are using is one year older
11
- than its latest version.
12
-
13
- ## Usage
14
-
15
- Early access. Output and usage subject to change.
16
-
17
- ```
18
- gem install libyear-bundler
19
- libyear-bundler Gemfile
20
- activesupport 4.2.7.1 2016-08-10 5.0.1 2016-12-21 0.4
21
- json 1.8.6 2017-01-13 2.0.3 2017-01-12 0.0
22
- minitest_to_rspec 0.6.0 2015-06-09 0.8.0 2017-01-02 1.6
23
- System is 1.9 libyears behind
24
- ```
25
-
26
- ## Development
27
-
28
- ```
29
- ruby -I lib bin/libyear-bundler spec/fixtures/01/Gemfile
30
- ```
@@ -1,88 +0,0 @@
1
- require "English"
2
- require "open3"
3
-
4
- module LibyearBundler
5
- # Responsible for getting all the data that goes into the `Report`.
6
- class Query
7
- # Format of `bundle outdated --parseable` (BOP)
8
- BOP_FMT = /\A(?<name>[^ ]+) \(newest (?<newest>[^,]+), installed (?<installed>[^,)]+)/
9
-
10
- def initialize(gemfile_path)
11
- @gemfile_path = gemfile_path
12
- end
13
-
14
- def execute
15
- gems = []
16
- bundle_outdated.lines.each do |line|
17
- match = BOP_FMT.match(line)
18
- next if match.nil?
19
- gems.push(
20
- installed: { version: match["installed"] },
21
- name: match["name"],
22
- newest: { version: match["newest"] }
23
- )
24
- end
25
- gems.each do |gem|
26
- di = release_date(gem[:name], gem[:installed][:version])
27
- dn = release_date(gem[:name], gem[:newest][:version])
28
- gem[:installed][:date] = di
29
- gem[:newest][:date] = dn
30
- if di.nil? || dn.nil? || dn <= di
31
- # Known issue: Backports and maintenance releases of older minor versions.
32
- # Example: json 1.8.6 (2017-01-13) was released *after* 2.0.3 (2017-01-12)
33
- years = 0.0
34
- else
35
- days = (dn - di).to_f
36
- years = days / 365.0
37
- end
38
- gem[:libyears] = years
39
- end
40
- gems
41
- end
42
-
43
- private
44
-
45
- def bundle_outdated
46
- stdout, stderr, status = Open3.capture3(
47
- %Q(BUNDLE_GEMFILE="#{@gemfile_path}" bundle outdated --parseable)
48
- )
49
- # Known statuses:
50
- # 0 - Nothing is outdated
51
- # 256 - Something is outdated
52
- # 1792 - Unable to determine if something is outdated
53
- unless [0, 256].include?(status.to_i)
54
- $stderr.puts "`bundle outdated` failed with status: #{status.to_i}"
55
- $stderr.puts "stderr: #{stderr}"
56
- $stderr.puts "stdout: #{stdout}"
57
- $stderr.puts "Try running `bundle install`."
58
- Kernel.exit(CLI::E_BUNDLE_OUTDATED_FAILED)
59
- end
60
- stdout
61
- end
62
-
63
- # Known issue: Probably performs a network request every time, unless
64
- # there's some kind of caching.
65
- def release_date(gem_name, gem_version)
66
- dep = nil
67
- begin
68
- dep = ::Bundler::Dependency.new(gem_name, gem_version)
69
- rescue ::Gem::Requirement::BadRequirementError => e
70
- $stderr.puts "Could not find release date for: #{gem_name}"
71
- $stderr.puts(e)
72
- $stderr.puts(
73
- "Maybe you used git in your Gemfile, which libyear doesn't support " \
74
- "yet. Contributions welcome."
75
- )
76
- return nil
77
- end
78
- tuples, _errors = ::Gem::SpecFetcher.fetcher.search_for_dependency(dep)
79
- if tuples.empty?
80
- $stderr.puts "Could not find release date for: #{gem_name}"
81
- return nil
82
- end
83
- tup, source = tuples.first # Gem::NameTuple
84
- spec = source.fetch_spec(tup) # raises Gem::RemoteFetcher::FetchError
85
- spec.date.to_date
86
- end
87
- end
88
- end