libyear-rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/workflows/ci.yml +24 -0
- data/.github/workflows/release.yml +24 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +11 -0
- data/exe/libyear-rb +6 -0
- data/lib/libyear_rb/cli.rb +95 -0
- data/lib/libyear_rb/dependency_analyzer.rb +48 -0
- data/lib/libyear_rb/gem_info_cacher.rb +47 -0
- data/lib/libyear_rb/gem_info_fetcher.rb +80 -0
- data/lib/libyear_rb/lockfile_parser.rb +221 -0
- data/lib/libyear_rb/models.rb +22 -0
- data/lib/libyear_rb/reporters/plaintext_reporter.rb +54 -0
- data/lib/libyear_rb/reporters/reporter.rb +13 -0
- data/lib/libyear_rb/runner.rb +47 -0
- data/lib/libyear_rb/version.rb +5 -0
- data/lib/libyear_rb.rb +16 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 780ffd231f0123d3d93b819886b7ec1bf90100c93a53b9a87eb3ff9028178392
|
|
4
|
+
data.tar.gz: 7b4405cfbde73ffa056506087378ff45b794fe93c102933ccb202209e087c7db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 282f83a4874582c3d835cc8d9c5fb89eb7ac6e27c122fc4f8e82e9e10da007970e9806de21ca8e64d7ae1766cca222356f4f68737923845a940c0def7a9fb6df
|
|
7
|
+
data.tar.gz: d3da23dc71125df5adeb55769624d82ead4642b97ac23c1931c355c264b4c803066758c625666c6a8b68f364d827a6029c2912508e45819ef3b6b575f0d2397a
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @Thomascountz @bquorning
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
testing:
|
|
8
|
+
name: Testing
|
|
9
|
+
runs-on: ubuntu-24.04-arm
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
|
+
- uses: ruby/setup-ruby@v1
|
|
13
|
+
with:
|
|
14
|
+
bundler-cache: true
|
|
15
|
+
- run: bundle exec rake test
|
|
16
|
+
linting:
|
|
17
|
+
name: Linting
|
|
18
|
+
runs-on: ubuntu-24.04-arm
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
- uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
bundler-cache: true
|
|
24
|
+
- run: bundle exec rake rubocop
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Release Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
name: Release gem to RubyGems.org
|
|
11
|
+
runs-on: ubuntu-24.04-arm
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
contents: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
- uses: ruby/setup-ruby@v1
|
|
20
|
+
with:
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
- run: bundle exec rake test
|
|
23
|
+
- run: bundle exec rake rubocop
|
|
24
|
+
- uses: rubygems/release-gem@v1
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- standard
|
|
3
|
+
|
|
4
|
+
plugins:
|
|
5
|
+
- rubocop-minitest
|
|
6
|
+
- rubocop-performance
|
|
7
|
+
- rubocop-rake
|
|
8
|
+
- standard-custom
|
|
9
|
+
- standard-performance
|
|
10
|
+
|
|
11
|
+
inherit_gem:
|
|
12
|
+
standard: config/base.yml
|
|
13
|
+
standard-custom: config/base.yml
|
|
14
|
+
standard-performance: config/base.yml
|
|
15
|
+
|
|
16
|
+
AllCops:
|
|
17
|
+
NewCops: enable
|
|
18
|
+
|
|
19
|
+
Layout/LineLength:
|
|
20
|
+
Enabled: true
|
|
21
|
+
Max: 140 # TODO: Reduce to 120
|
|
22
|
+
|
|
23
|
+
Lint/NoReturnInBeginEndBlocks:
|
|
24
|
+
Enabled: true
|
|
25
|
+
|
|
26
|
+
Style/FormatString:
|
|
27
|
+
Enabled: true
|
|
28
|
+
EnforcedStyle: percent
|
|
29
|
+
|
|
30
|
+
Style/RedundantInitialize:
|
|
31
|
+
Enabled: true
|
|
32
|
+
|
|
33
|
+
Style/Documentation:
|
|
34
|
+
Enabled: false # TODO: Enable
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2025-01-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial release of libyear-rb
|
|
10
|
+
- Analyze Gemfile.lock to measure dependency freshness in libyears
|
|
11
|
+
- Report versions behind (release count) and days/years behind for each dependency
|
|
12
|
+
- Support for any RubyGems.org-compatible gem server (private gem servers, mirrors, etc.)
|
|
13
|
+
- Historical analysis with `--as-of` flag to analyze dependencies as of a specific date
|
|
14
|
+
- Caching of gem metadata to minimize network requests (24-hour TTL)
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Countz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# libyear-rb
|
|
2
|
+
|
|
3
|
+
A simple measure of dependency freshness for Ruby apps.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
$ libyear-rb
|
|
7
|
+
Gem Current Current Date Latest Latest Date Versions Days Years
|
|
8
|
+
.................... ....... ............ ........ ........... ........ .... .....
|
|
9
|
+
addressable 2.8.7 2024-06-21 2.8.8 2025-11-25 1 522 1.43
|
|
10
|
+
bigdecimal 3.3.1 2025-10-09 4.0.1 2025-12-17 4 69 0.19
|
|
11
|
+
rails 7.0.0 2021-12-15 7.2.0 2024-08-10 15 968 2.65
|
|
12
|
+
System is 4.27 libyears behind
|
|
13
|
+
Total releases behind: 20
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`libyear-rb` tells you how out-of-date your Gemfile.lock is, in libyears (the time between your installed version and the newest version).
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- Analyzes Gemfile.lock to measure dependency freshness
|
|
21
|
+
- Reports libyears (time behind) and version distance (releases behind)
|
|
22
|
+
- Supports any RubyGems.org-compatible gem server (private gem servers, mirrors, etc.)
|
|
23
|
+
- Historical analysis with `--as-of` to see what your dependencies looked like at a specific date
|
|
24
|
+
- Caches API responses to minimize network requests
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
gem install libyear-rb
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or add to your Gemfile:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem "libyear-rb"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Run `libyear-rb` in a directory with a Gemfile.lock, or provide a path:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
libyear-rb # Uses ./Gemfile.lock
|
|
44
|
+
libyear-rb path/to/Gemfile.lock # Uses specified lockfile
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Options
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
--as-of DATE Analyze dependencies as of the given date (YYYY-MM-DD)
|
|
51
|
+
--verbose Run with verbose logs
|
|
52
|
+
--help Show help
|
|
53
|
+
--version Show version
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Historical Analysis
|
|
57
|
+
|
|
58
|
+
You can analyze what your dependencies looked like at a specific point in time:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
libyear-rb --as-of 2024-01-01
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Private Gem Servers
|
|
65
|
+
|
|
66
|
+
`libyear-rb` supports any gem server with a RubyGems.org-compatible API. It uses your configured gem sources from `~/.gemrc` or environment, so private gems hosted on servers like Gemfury, Artifactory, or self-hosted solutions work automatically.
|
|
67
|
+
|
|
68
|
+
## Caching
|
|
69
|
+
|
|
70
|
+
To reduce API requests and improve performance, `libyear-rb` caches gem version metadata for 24 hours.
|
|
71
|
+
|
|
72
|
+
**Cache location:**
|
|
73
|
+
- Uses `$XDG_CACHE_HOME/libyear-rb/` if `XDG_CACHE_HOME` is set
|
|
74
|
+
- Otherwise defaults to `~/.cache/libyear-rb/`
|
|
75
|
+
|
|
76
|
+
Cache files are organized by gem source host, so metadata from different gem servers is stored separately.
|
|
77
|
+
|
|
78
|
+
**To skip the cache:**
|
|
79
|
+
```bash
|
|
80
|
+
SKIP_CACHE=1 libyear-rb
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**To clear the cache:**
|
|
84
|
+
```bash
|
|
85
|
+
rm -rf ~/.cache/libyear-rb
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Alternatives
|
|
89
|
+
|
|
90
|
+
Other Ruby tools for measuring dependency freshness:
|
|
91
|
+
|
|
92
|
+
- [libyear-bundler](https://github.com/jaredbeck/libyear-bundler) - The original Ruby libyear implementation. Supports additional metrics like major/minor/patch version deltas and JSON output.
|
|
93
|
+
- [bundler-audit](https://github.com/rubysec/bundler-audit) - Focuses on security vulnerabilities rather than freshness, but useful for dependency health.
|
|
94
|
+
- [bundle outdated](https://bundler.io/man/bundle-outdated.1.html) - Built into Bundler. Shows outdated gems but doesn't calculate libyears.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt.
|
|
99
|
+
|
|
100
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
101
|
+
|
|
102
|
+
## Acknowledgements
|
|
103
|
+
|
|
104
|
+
The concept of libyear comes from the technical report "Measuring Dependency Freshness in Software Systems" by J. Cox, E. Bouwers, M. van Eekelen and J. Visser (ICSE 2015).
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/libyear-rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "date"
|
|
5
|
+
require "logger"
|
|
6
|
+
|
|
7
|
+
module LibyearRb
|
|
8
|
+
class CLI
|
|
9
|
+
def initialize(argv = ARGV)
|
|
10
|
+
@argv = argv.dup
|
|
11
|
+
@options = {}
|
|
12
|
+
parse_options!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
lockfile_contents = read_lockfile
|
|
17
|
+
logger = build_logger
|
|
18
|
+
|
|
19
|
+
lockfile_parser = LockfileParser.new
|
|
20
|
+
gem_info_fetcher = GemInfoFetcher.new
|
|
21
|
+
dependency_analyzer = DependencyAnalyzer.new(logger: logger)
|
|
22
|
+
reporter = PlaintextReporter.new
|
|
23
|
+
|
|
24
|
+
runner = Runner.new(
|
|
25
|
+
lockfile_parser: lockfile_parser,
|
|
26
|
+
gem_info_fetcher: gem_info_fetcher,
|
|
27
|
+
dependency_analyzer: dependency_analyzer,
|
|
28
|
+
reporter: reporter,
|
|
29
|
+
logger: logger
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
runner.run(lockfile_contents, as_of: @options[:as_of])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_logger
|
|
38
|
+
return nil unless @options[:verbose]
|
|
39
|
+
|
|
40
|
+
Logger.new($stderr)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_options!
|
|
44
|
+
OptionParser.new do |opts|
|
|
45
|
+
opts.banner = "Usage: libyear-rb [Gemfile.lock] [options]"
|
|
46
|
+
opts.program_name = "libyear-rb"
|
|
47
|
+
opts.version = LibyearRb::VERSION
|
|
48
|
+
|
|
49
|
+
opts.on("-h", "--help", "Prints this help") do
|
|
50
|
+
puts opts
|
|
51
|
+
exit
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
opts.on("-v", "--version", "Show version") do
|
|
55
|
+
puts "libyear-rb #{LibyearRb::VERSION}"
|
|
56
|
+
exit
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
opts.on("--as-of DATE", "Analyze dependencies as of the given date (YYYY-MM-DD)") do |date|
|
|
60
|
+
@options[:as_of] = Date.parse(date)
|
|
61
|
+
rescue ArgumentError
|
|
62
|
+
warn "Invalid date format. Please use YYYY-MM-DD."
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on("--verbose", "Run with verbose logs") do
|
|
67
|
+
@options[:verbose] = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
opts.separator ""
|
|
71
|
+
opts.separator "Environment variables:"
|
|
72
|
+
opts.separator " SKIP_CACHE=1 Disable reading to and writing from the libyear-rb cache"
|
|
73
|
+
end.parse!(@argv)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def read_lockfile
|
|
77
|
+
lockfile_path = @argv[0] || default_lockfile_path
|
|
78
|
+
|
|
79
|
+
File.read(lockfile_path)
|
|
80
|
+
rescue Errno::ENOENT
|
|
81
|
+
warn "Lockfile not found at path: #{lockfile_path}"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def default_lockfile_path
|
|
86
|
+
if ENV.key?("BUNDLE_LOCKFILE")
|
|
87
|
+
ENV["BUNDLE_LOCKFILE"]
|
|
88
|
+
elsif ENV.key?("BUNDLE_GEMFILE")
|
|
89
|
+
"#{ENV["BUNDLE_GEMFILE"]}.lock"
|
|
90
|
+
else
|
|
91
|
+
"Gemfile.lock"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibyearRb
|
|
4
|
+
class DependencyAnalyzer
|
|
5
|
+
def initialize(logger: nil)
|
|
6
|
+
@logger = logger
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def calculate_dependency_freshness(gem_name, gem_version, versions_metadata)
|
|
10
|
+
current_version_info = versions_metadata.find { |version| version.number == gem_version }
|
|
11
|
+
|
|
12
|
+
if current_version_info.nil?
|
|
13
|
+
@logger&.warn("Skipping #{gem_name}: installed version #{gem_version} not found in metadata")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
latest_version_info = if current_version_info.prerelease?
|
|
18
|
+
versions_metadata.first
|
|
19
|
+
else
|
|
20
|
+
versions_metadata.find { |version| !version.prerelease? }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
latest_version = latest_version_info.number
|
|
24
|
+
current_version = current_version_info.number
|
|
25
|
+
|
|
26
|
+
if latest_version == current_version
|
|
27
|
+
return nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
latest_release_date = latest_version_info.created_at
|
|
31
|
+
current_release_date = current_version_info.created_at
|
|
32
|
+
|
|
33
|
+
version_distance = versions_metadata.index { |version| version.number == current_version }
|
|
34
|
+
libyear_in_days = [(latest_release_date - current_release_date).to_i, 0].max
|
|
35
|
+
|
|
36
|
+
Result.new(
|
|
37
|
+
name: gem_name,
|
|
38
|
+
current_version: gem_version,
|
|
39
|
+
current_version_release_date: current_release_date,
|
|
40
|
+
latest_version: latest_version,
|
|
41
|
+
latest_version_release_date: latest_release_date,
|
|
42
|
+
version_distance: version_distance,
|
|
43
|
+
libyear_in_days: libyear_in_days,
|
|
44
|
+
is_direct: true
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module LibyearRb
|
|
7
|
+
module GemInfoCacher
|
|
8
|
+
CACHE_EXPIRATION = 86_400 # 24 hours in seconds
|
|
9
|
+
|
|
10
|
+
def with_cache(remote_host, gem_name, &block)
|
|
11
|
+
if ENV["SKIP_CACHE"] == "1"
|
|
12
|
+
return block.call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
cache_file = cache_path(remote_host, gem_name)
|
|
16
|
+
if cache_valid?(cache_file)
|
|
17
|
+
JSON.parse(File.read(cache_file))
|
|
18
|
+
else
|
|
19
|
+
block.call.tap do |data|
|
|
20
|
+
return [] unless data
|
|
21
|
+
|
|
22
|
+
FileUtils.mkdir_p(File.dirname(cache_file))
|
|
23
|
+
File.write(cache_file, JSON.dump(data))
|
|
24
|
+
File.utime(Time.now, Time.now, cache_file)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def cache_valid?(cache_file)
|
|
32
|
+
return false unless File.exist?(cache_file)
|
|
33
|
+
|
|
34
|
+
cache_age = Time.now - File.mtime(cache_file)
|
|
35
|
+
cache_age < CACHE_EXPIRATION
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cache_dir
|
|
39
|
+
ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cache_path(remote_host, gem_name)
|
|
43
|
+
host_key = remote_host.gsub(/[^a-zA-Z0-9]/, "_")
|
|
44
|
+
File.join(cache_dir, "libyear-rb", host_key, "#{gem_name}.json")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gems"
|
|
4
|
+
require "date"
|
|
5
|
+
require "rubygems"
|
|
6
|
+
|
|
7
|
+
module LibyearRb
|
|
8
|
+
class GemInfoFetcher
|
|
9
|
+
include GemInfoCacher
|
|
10
|
+
|
|
11
|
+
RATE_LIMIT = 10 # https://guides.rubygems.org/rubygems-org-rate-limits/
|
|
12
|
+
RATE_LIMIT_INTERVAL = 1.0 / RATE_LIMIT
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@gem_source_clients = {}
|
|
16
|
+
@last_request_time = Hash.new { |hash, key| hash[key] = Time.now - RATE_LIMIT_INTERVAL }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def gem_versions_for(gem_name, remote_host)
|
|
20
|
+
client = client_for(remote_host)
|
|
21
|
+
return [] unless client
|
|
22
|
+
|
|
23
|
+
raw_versions = fetch_raw_versions(client, remote_host, gem_name)
|
|
24
|
+
build_versions(gem_name, raw_versions)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def fetch_raw_versions(client, remote_host, gem_name)
|
|
30
|
+
with_cache(remote_host, gem_name) do
|
|
31
|
+
wait_for_rate_limit(remote_host)
|
|
32
|
+
|
|
33
|
+
client.versions(gem_name)
|
|
34
|
+
rescue Gems::GemError, Gems::NotFound
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_versions(gem_name, raw_versions)
|
|
40
|
+
Array(raw_versions)
|
|
41
|
+
.map do |attributes|
|
|
42
|
+
GemVersion.new(
|
|
43
|
+
name: gem_name,
|
|
44
|
+
number: Gem::Version.new(attributes["number"]),
|
|
45
|
+
created_at: Date.parse(attributes["created_at"]),
|
|
46
|
+
prerelease?: attributes["prerelease"]
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def client_for(remote_host)
|
|
52
|
+
return @gem_source_clients[remote_host] if @gem_source_clients.key?(remote_host)
|
|
53
|
+
|
|
54
|
+
client = nil
|
|
55
|
+
source = sources.find { |gem_source| gem_source.uri.host == remote_host }
|
|
56
|
+
|
|
57
|
+
if source
|
|
58
|
+
uri = source.uri
|
|
59
|
+
client = Gems::Client.new(
|
|
60
|
+
host: (uri.origin + uri.request_uri),
|
|
61
|
+
username: uri.user,
|
|
62
|
+
password: uri.password
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@gem_source_clients[remote_host] = client
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sources
|
|
70
|
+
@sources ||= Gem.sources.each_source.to_a
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def wait_for_rate_limit(remote_host)
|
|
74
|
+
now = Time.now
|
|
75
|
+
elapsed = now - @last_request_time[remote_host]
|
|
76
|
+
sleep(RATE_LIMIT_INTERVAL - elapsed) if elapsed < RATE_LIMIT_INTERVAL
|
|
77
|
+
@last_request_time[remote_host] = Time.now
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibyearRb
|
|
4
|
+
class LockfileParser
|
|
5
|
+
# Section markers
|
|
6
|
+
BUNDLED_WITH = /^BUNDLED WITH$/
|
|
7
|
+
CHECKSUMS = /^CHECKSUMS$/
|
|
8
|
+
DEPENDENCIES = /^DEPENDENCIES$/
|
|
9
|
+
GEM = /^GEM$/
|
|
10
|
+
GIT = /^GIT$/
|
|
11
|
+
PATH = /^PATH$/
|
|
12
|
+
PLATFORMS = /^PLATFORMS$/
|
|
13
|
+
PLUGIN = /^PLUGIN SOURCE$/
|
|
14
|
+
RUBY = /^RUBY VERSION$/
|
|
15
|
+
|
|
16
|
+
# Entry patterns
|
|
17
|
+
REMOTE = /^ remote: (.+)$/
|
|
18
|
+
REVISION = /^ revision: (.+)$/
|
|
19
|
+
SPECS = /^ specs:$/
|
|
20
|
+
OPTION = /^ ([a-z]+): (.+)$/i
|
|
21
|
+
SPEC_ENTRY = /^ ([^ (]+)(?: \(([^)]+)\))?$/
|
|
22
|
+
DEPENDENCY_ENTRY = /^ ([^ (]+)(?: \(([^)]+)\))?$/
|
|
23
|
+
TOP_LEVEL_DEPENDENCY = /^ ([^ (]+)(?: \(([^)]+)\))?(!)?$/
|
|
24
|
+
PLATFORM_ENTRY = /^ (.+)$/
|
|
25
|
+
VERSION_LINE = /^ ?([^ ].+)$/
|
|
26
|
+
BUNDLED_VERSION = /^ ?([^ ].+)$/
|
|
27
|
+
|
|
28
|
+
def parse(lockfile_content)
|
|
29
|
+
lines = lockfile_content.lines(chomp: true)
|
|
30
|
+
|
|
31
|
+
sources = []
|
|
32
|
+
platforms = []
|
|
33
|
+
dependencies = []
|
|
34
|
+
ruby_version = nil
|
|
35
|
+
bundled_with = nil
|
|
36
|
+
|
|
37
|
+
i = 0
|
|
38
|
+
while i < lines.length
|
|
39
|
+
line = lines[i]
|
|
40
|
+
|
|
41
|
+
case line
|
|
42
|
+
when GIT, GEM, PATH, PLUGIN
|
|
43
|
+
source, next_i = parse_source(lines, i)
|
|
44
|
+
sources << source
|
|
45
|
+
i = next_i
|
|
46
|
+
when PLATFORMS
|
|
47
|
+
platforms, i = parse_platforms(lines, i + 1)
|
|
48
|
+
when DEPENDENCIES
|
|
49
|
+
dependencies, i = parse_dependencies(lines, i + 1)
|
|
50
|
+
when RUBY
|
|
51
|
+
ruby_version, i = parse_ruby_version(lines, i + 1)
|
|
52
|
+
when BUNDLED_WITH
|
|
53
|
+
bundled_with, i = parse_bundled_with(lines, i + 1)
|
|
54
|
+
when CHECKSUMS
|
|
55
|
+
i = skip_section(lines, i + 1)
|
|
56
|
+
else
|
|
57
|
+
i += 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Lockfile.new(
|
|
62
|
+
sources: sources,
|
|
63
|
+
platforms: platforms,
|
|
64
|
+
dependencies: dependencies,
|
|
65
|
+
ruby_version: ruby_version,
|
|
66
|
+
bundled_with: bundled_with
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def parse_source(lines, start_idx)
|
|
73
|
+
type = case lines[start_idx]
|
|
74
|
+
when GIT then :git
|
|
75
|
+
when GEM then :gem
|
|
76
|
+
when PATH then :path
|
|
77
|
+
when PLUGIN then :plugin
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
remote = nil
|
|
81
|
+
revision = nil
|
|
82
|
+
specs = []
|
|
83
|
+
options = {}
|
|
84
|
+
|
|
85
|
+
i = start_idx + 1
|
|
86
|
+
while i < lines.length && !section_header?(lines[i])
|
|
87
|
+
line = lines[i]
|
|
88
|
+
|
|
89
|
+
case line
|
|
90
|
+
when REMOTE
|
|
91
|
+
remote = line.match(REMOTE)[1]
|
|
92
|
+
when REVISION
|
|
93
|
+
revision = line.match(REVISION)[1]
|
|
94
|
+
when SPECS
|
|
95
|
+
specs, i = parse_specs(lines, i + 1)
|
|
96
|
+
next
|
|
97
|
+
when OPTION
|
|
98
|
+
match = line.match(OPTION)
|
|
99
|
+
options[match[1]] = match[2]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
i += 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
source = Source.new(
|
|
106
|
+
type: type,
|
|
107
|
+
remote: remote,
|
|
108
|
+
revision: revision,
|
|
109
|
+
specs: specs,
|
|
110
|
+
options: options
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
[source, i]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_specs(lines, start_idx)
|
|
117
|
+
specs = []
|
|
118
|
+
i = start_idx
|
|
119
|
+
|
|
120
|
+
while i < lines.length && lines[i].match?(SPEC_ENTRY)
|
|
121
|
+
line = lines[i]
|
|
122
|
+
match = line.match(SPEC_ENTRY)
|
|
123
|
+
name = match[1]
|
|
124
|
+
version = match[2]
|
|
125
|
+
|
|
126
|
+
dependencies = []
|
|
127
|
+
i += 1
|
|
128
|
+
|
|
129
|
+
while i < lines.length && lines[i].match?(DEPENDENCY_ENTRY)
|
|
130
|
+
dep_match = lines[i].match(DEPENDENCY_ENTRY)
|
|
131
|
+
dependencies << Dependency.new(
|
|
132
|
+
name: dep_match[1],
|
|
133
|
+
version_requirements: dep_match[2]
|
|
134
|
+
)
|
|
135
|
+
i += 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
specs << Spec.new(
|
|
139
|
+
name: name,
|
|
140
|
+
version: version,
|
|
141
|
+
dependencies: dependencies
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
[specs, i]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def parse_platforms(lines, start_idx)
|
|
149
|
+
platforms = []
|
|
150
|
+
i = start_idx
|
|
151
|
+
|
|
152
|
+
while i < lines.length && lines[i].match?(PLATFORM_ENTRY) && !section_header?(lines[i])
|
|
153
|
+
match = lines[i].match(PLATFORM_ENTRY)
|
|
154
|
+
platforms << Platform.new(name: match[1])
|
|
155
|
+
i += 1
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
[platforms, i]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parse_dependencies(lines, start_idx)
|
|
162
|
+
dependencies = []
|
|
163
|
+
i = start_idx
|
|
164
|
+
|
|
165
|
+
while i < lines.length && lines[i].match?(TOP_LEVEL_DEPENDENCY)
|
|
166
|
+
match = lines[i].match(TOP_LEVEL_DEPENDENCY)
|
|
167
|
+
dependencies << Dependency.new(
|
|
168
|
+
name: match[1],
|
|
169
|
+
version_requirements: match[2]
|
|
170
|
+
)
|
|
171
|
+
i += 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
[dependencies, i]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_ruby_version(lines, start_idx)
|
|
178
|
+
return [nil, start_idx] if start_idx >= lines.length
|
|
179
|
+
|
|
180
|
+
line = lines[start_idx]
|
|
181
|
+
return [nil, start_idx] unless line.match?(VERSION_LINE)
|
|
182
|
+
|
|
183
|
+
version_string = line.match(VERSION_LINE)[1]
|
|
184
|
+
parts = version_string.split
|
|
185
|
+
|
|
186
|
+
version, patchlevel = parts[1].split("p")
|
|
187
|
+
engine = parts[2] if parts.length > 2
|
|
188
|
+
|
|
189
|
+
ruby_version = RubyVersion.new(
|
|
190
|
+
version: version,
|
|
191
|
+
engine: engine,
|
|
192
|
+
patchlevel: patchlevel
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
[ruby_version, start_idx + 1]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_bundled_with(lines, start_idx)
|
|
199
|
+
return [nil, start_idx] if start_idx >= lines.length
|
|
200
|
+
|
|
201
|
+
line = lines[start_idx]
|
|
202
|
+
return [nil, start_idx] unless line.match?(BUNDLED_VERSION)
|
|
203
|
+
|
|
204
|
+
version = line.match(BUNDLED_VERSION)[1]
|
|
205
|
+
[version, start_idx + 1]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def skip_section(lines, start_idx)
|
|
209
|
+
i = start_idx
|
|
210
|
+
i += 1 while i < lines.length && !section_header?(lines[i])
|
|
211
|
+
i
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def section_header?(line)
|
|
215
|
+
line.match?(GEM) || line.match?(GIT) || line.match?(PATH) ||
|
|
216
|
+
line.match?(PLUGIN) || line.match?(PLATFORMS) ||
|
|
217
|
+
line.match?(DEPENDENCIES) || line.match?(RUBY) ||
|
|
218
|
+
line.match?(BUNDLED_WITH) || line.match?(CHECKSUMS)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibyearRb
|
|
4
|
+
Lockfile = Data.define(:sources, :platforms, :dependencies, :ruby_version, :bundled_with)
|
|
5
|
+
Source = Data.define(:type, :remote, :revision, :specs, :options)
|
|
6
|
+
Spec = Data.define(:name, :version, :dependencies)
|
|
7
|
+
Dependency = Data.define(:name, :version_requirements)
|
|
8
|
+
Platform = Data.define(:name)
|
|
9
|
+
RubyVersion = Data.define(:version, :engine, :patchlevel)
|
|
10
|
+
GemVersion = Data.define(:name, :number, :created_at, :prerelease?)
|
|
11
|
+
|
|
12
|
+
Result = Data.define(
|
|
13
|
+
:name,
|
|
14
|
+
:current_version,
|
|
15
|
+
:current_version_release_date,
|
|
16
|
+
:latest_version,
|
|
17
|
+
:latest_version_release_date,
|
|
18
|
+
:version_distance,
|
|
19
|
+
:is_direct,
|
|
20
|
+
:libyear_in_days
|
|
21
|
+
)
|
|
22
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibyearRb
|
|
4
|
+
class PlaintextReporter < Reporter
|
|
5
|
+
UNKNOWN = "Unknown"
|
|
6
|
+
COLUMN_BUFFER = 3
|
|
7
|
+
HEADERS = ["Gem", "Current", "Current Date", "Latest", "Latest Date", "Versions", "Days", "Years"].freeze
|
|
8
|
+
|
|
9
|
+
def generate(results)
|
|
10
|
+
rows = results.sort_by(&:name).filter_map do |result|
|
|
11
|
+
row_for(result) unless result.version_distance.zero?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
return if rows.empty?
|
|
15
|
+
|
|
16
|
+
widths = calculate_widths(rows)
|
|
17
|
+
@io.puts format_row(HEADERS, widths)
|
|
18
|
+
@io.puts divider(widths)
|
|
19
|
+
rows.each { |row| @io.puts format_row(row, widths) }
|
|
20
|
+
|
|
21
|
+
total_days = results.sum { |r| r.libyear_in_days || 0 }
|
|
22
|
+
total_versions = results.sum { |r| r.version_distance || 0 }
|
|
23
|
+
@io.puts "System is %.2f libyears behind" % (total_days / 365.0)
|
|
24
|
+
@io.puts "Total releases behind: #{total_versions}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def row_for(result)
|
|
30
|
+
[
|
|
31
|
+
result.name.to_s,
|
|
32
|
+
result.current_version&.to_s || UNKNOWN,
|
|
33
|
+
result.current_version_release_date&.strftime("%Y-%m-%d") || UNKNOWN,
|
|
34
|
+
result.latest_version&.to_s || UNKNOWN,
|
|
35
|
+
result.latest_version_release_date&.strftime("%Y-%m-%d") || UNKNOWN,
|
|
36
|
+
result.version_distance&.to_s || UNKNOWN,
|
|
37
|
+
result.libyear_in_days&.to_s || UNKNOWN,
|
|
38
|
+
result.libyear_in_days ? "%.2f" % (result.libyear_in_days / 365.0) : UNKNOWN
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def calculate_widths(rows)
|
|
43
|
+
[HEADERS, *rows].transpose.map { |column| column.map(&:length).max + COLUMN_BUFFER }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format_row(values, widths)
|
|
47
|
+
values.zip(widths).map { |value, width| value.rjust(width) }.join(" ")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def divider(widths)
|
|
51
|
+
widths.map { |w| "." * w }.join(" ")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module LibyearRb
|
|
6
|
+
class Runner
|
|
7
|
+
def initialize(lockfile_parser:, gem_info_fetcher:, dependency_analyzer:, reporter:, logger: nil)
|
|
8
|
+
@lockfile_parser = lockfile_parser
|
|
9
|
+
@gem_info_fetcher = gem_info_fetcher
|
|
10
|
+
@dependency_analyzer = dependency_analyzer
|
|
11
|
+
@reporter = reporter
|
|
12
|
+
@logger = logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(lockfile_contents, as_of: nil)
|
|
16
|
+
results = []
|
|
17
|
+
lockfile = @lockfile_parser.parse(lockfile_contents)
|
|
18
|
+
lockfile.sources.each do |source|
|
|
19
|
+
unless source.type == :gem && !source.remote.nil?
|
|
20
|
+
@logger&.warn("Skipping source #{source.type}: unsupported source type or missing remote")
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
remote_host = URI.parse(source.remote).host
|
|
25
|
+
|
|
26
|
+
source.specs.each do |spec|
|
|
27
|
+
gem_name = spec.name
|
|
28
|
+
gem_version = spec.version
|
|
29
|
+
versions_metadata = @gem_info_fetcher.gem_versions_for(gem_name, remote_host)
|
|
30
|
+
.reject { |version| as_of && version.created_at > as_of }
|
|
31
|
+
.sort_by(&:number)
|
|
32
|
+
.reverse
|
|
33
|
+
|
|
34
|
+
if versions_metadata.empty?
|
|
35
|
+
@logger&.warn("Skipping #{gem_name}: no version metadata from #{remote_host}")
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result = @dependency_analyzer.calculate_dependency_freshness(gem_name, gem_version, versions_metadata)
|
|
40
|
+
results << result if result
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
@reporter.generate(results)
|
|
44
|
+
results
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/libyear_rb.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "libyear_rb/version"
|
|
4
|
+
require_relative "libyear_rb/models"
|
|
5
|
+
require_relative "libyear_rb/lockfile_parser"
|
|
6
|
+
require_relative "libyear_rb/gem_info_cacher"
|
|
7
|
+
require_relative "libyear_rb/gem_info_fetcher"
|
|
8
|
+
require_relative "libyear_rb/dependency_analyzer"
|
|
9
|
+
require_relative "libyear_rb/reporters/reporter"
|
|
10
|
+
require_relative "libyear_rb/reporters/plaintext_reporter"
|
|
11
|
+
require_relative "libyear_rb/runner"
|
|
12
|
+
require_relative "libyear_rb/cli"
|
|
13
|
+
|
|
14
|
+
module LibyearRb
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: libyear-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Thomas Countz
|
|
8
|
+
- Benjamin Quorning
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: gems
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: logger
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
description: libyear-rb analyzes your Gemfile.lock and tells you how out-of-date your
|
|
42
|
+
dependencies are, in libyears (the time between your installed version and the newest
|
|
43
|
+
version).
|
|
44
|
+
email:
|
|
45
|
+
- thomascountz@gmail.com
|
|
46
|
+
- bquorning@zendesk.com
|
|
47
|
+
executables:
|
|
48
|
+
- libyear-rb
|
|
49
|
+
extensions: []
|
|
50
|
+
extra_rdoc_files: []
|
|
51
|
+
files:
|
|
52
|
+
- ".github/CODEOWNERS"
|
|
53
|
+
- ".github/workflows/ci.yml"
|
|
54
|
+
- ".github/workflows/release.yml"
|
|
55
|
+
- ".rubocop.yml"
|
|
56
|
+
- ".ruby-version"
|
|
57
|
+
- CHANGELOG.md
|
|
58
|
+
- LICENSE.txt
|
|
59
|
+
- README.md
|
|
60
|
+
- Rakefile
|
|
61
|
+
- exe/libyear-rb
|
|
62
|
+
- lib/libyear_rb.rb
|
|
63
|
+
- lib/libyear_rb/cli.rb
|
|
64
|
+
- lib/libyear_rb/dependency_analyzer.rb
|
|
65
|
+
- lib/libyear_rb/gem_info_cacher.rb
|
|
66
|
+
- lib/libyear_rb/gem_info_fetcher.rb
|
|
67
|
+
- lib/libyear_rb/lockfile_parser.rb
|
|
68
|
+
- lib/libyear_rb/models.rb
|
|
69
|
+
- lib/libyear_rb/reporters/plaintext_reporter.rb
|
|
70
|
+
- lib/libyear_rb/reporters/reporter.rb
|
|
71
|
+
- lib/libyear_rb/runner.rb
|
|
72
|
+
- lib/libyear_rb/version.rb
|
|
73
|
+
homepage: https://github.com/thomascountz/libyear-rb
|
|
74
|
+
licenses:
|
|
75
|
+
- MIT
|
|
76
|
+
metadata:
|
|
77
|
+
allowed_push_host: https://rubygems.org
|
|
78
|
+
homepage_uri: https://github.com/thomascountz/libyear-rb
|
|
79
|
+
source_code_uri: https://github.com/thomascountz/libyear-rb
|
|
80
|
+
changelog_uri: https://github.com/thomascountz/libyear-rb/blob/main/CHANGELOG.md
|
|
81
|
+
rubygems_mfa_required: 'true'
|
|
82
|
+
rdoc_options: []
|
|
83
|
+
require_paths:
|
|
84
|
+
- lib
|
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 3.2.0
|
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
requirements: []
|
|
96
|
+
rubygems_version: 4.0.3
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: A simple measure of dependency freshness
|
|
99
|
+
test_files: []
|