libyear-rb 0.1.0 → 0.2.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 +4 -4
- data/.github/workflows/release.yml +3 -2
- data/CHANGELOG.md +20 -1
- data/README.md +95 -47
- data/exe/libyear-rb +4 -0
- data/lib/libyear_rb/cli.rb +30 -23
- data/lib/libyear_rb/dependency_analyzer.rb +6 -14
- data/lib/libyear_rb/file_cache.rb +51 -0
- data/lib/libyear_rb/formatters/formatter.rb +31 -0
- data/lib/libyear_rb/formatters/json_formatter.rb +39 -0
- data/lib/libyear_rb/{reporters/plaintext_reporter.rb → formatters/plaintext_formatter.rb} +10 -6
- data/lib/libyear_rb/gem_info_fetcher.rb +28 -56
- data/lib/libyear_rb/lockfile_parser.rb +11 -0
- data/lib/libyear_rb/models.rb +5 -1
- data/lib/libyear_rb/runner.rb +65 -29
- data/lib/libyear_rb/sorter.rb +50 -0
- data/lib/libyear_rb/version.rb +1 -1
- data/lib/libyear_rb.rb +17 -4
- metadata +9 -7
- data/lib/libyear_rb/gem_info_cacher.rb +0 -47
- data/lib/libyear_rb/reporters/reporter.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 199716d9e9d674a6b897277931b9cecaa3a762807da3341ff13bdd3b865d1587
|
|
4
|
+
data.tar.gz: e430a6dbb744e921f27a3218e1c31eaa5de9726beece1a07e7dfdc6890013d27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43697c73f71080fceeec2b4c433f326b693c17e47a9b2496e714ae9e58b1ee74f7a0efbeec7dbd70ea329ed09646e81b248df0ba01dc7d1372ebd7845f0aab33
|
|
7
|
+
data.tar.gz: 1c7be9d76720da7a6992f7b0d028d0e6d7285209c80d657a8fba2611a831a7e1203265bff85cfa8d0e8aa6c399fc6f9aa487415fc39c555575391a67cccfed5c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.2.0] - 2026-08-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- The report can now be generated in JSON with the `--format json` flag
|
|
10
|
+
- The `--sort` flag orders the output by column values (default: `libyear,name`)
|
|
11
|
+
- The `--[no-]indirect` flag includes (default) or excludes indirect dependencies in the output
|
|
12
|
+
- A library API (`LibyearRb::Runner`) has been extracted for programmatic/scripting use
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- All results are now sent to formatters, so the report can include both outdated and up-to-date gems
|
|
17
|
+
- Gem version metadata is fetched in parallel with per-host worker pools
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- When `XDG_CACHE_HOME` is set, the cache directory resolves as a `Pathname` instead of a `String`
|
|
22
|
+
- Fetch failures are rescued per dependency, so one error no longer crashes the report
|
|
23
|
+
|
|
24
|
+
## [0.1.0] - 2026-01-09
|
|
6
25
|
|
|
7
26
|
### Added
|
|
8
27
|
|
data/README.md
CHANGED
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
# libyear-rb
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
|
3
|
+
A measure of dependency freshness for Gemfile.lock. Tells you how out-of-date your dependencies are, in [libyears](https://libyear.com/) by comparing the release date of the locked version to the latest available version.
|
|
25
4
|
|
|
26
5
|
## Installation
|
|
27
6
|
|
|
7
|
+
To use the CLI, install the gem:
|
|
8
|
+
|
|
28
9
|
```bash
|
|
29
10
|
gem install libyear-rb
|
|
30
11
|
```
|
|
31
12
|
|
|
32
|
-
|
|
13
|
+
```bash
|
|
14
|
+
mise install gem:libyear-rb
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To use programmatically, add to your Gemfile:
|
|
33
18
|
|
|
34
19
|
```ruby
|
|
35
20
|
gem "libyear-rb"
|
|
36
21
|
```
|
|
37
22
|
|
|
23
|
+
```ruby
|
|
24
|
+
require "bundler/inline"
|
|
25
|
+
gemfile do
|
|
26
|
+
source "https://rubygems.org"
|
|
27
|
+
gem "libyear-rb"
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
38
31
|
## Usage
|
|
39
32
|
|
|
33
|
+
### As a CLI
|
|
34
|
+
|
|
40
35
|
Run `libyear-rb` in a directory with a Gemfile.lock, or provide a path:
|
|
41
36
|
|
|
42
37
|
```bash
|
|
@@ -44,65 +39,118 @@ libyear-rb # Uses ./Gemfile.lock
|
|
|
44
39
|
libyear-rb path/to/Gemfile.lock # Uses specified lockfile
|
|
45
40
|
```
|
|
46
41
|
|
|
47
|
-
|
|
42
|
+
```
|
|
43
|
+
Gem Current Current Date Latest Latest Date Versions Days Years
|
|
44
|
+
.................... ....... ............ ........ ........... ........ .... .....
|
|
45
|
+
addressable 2.8.7 2024-06-21 2.8.8 2025-11-25 1 522 1.43
|
|
46
|
+
bigdecimal 3.3.1 2025-10-09 4.0.1 2025-12-17 4 69 0.19
|
|
47
|
+
rails 7.0.0 2021-12-15 7.2.0 2024-08-10 15 968 2.65
|
|
48
|
+
System is 4.27 libyears behind
|
|
49
|
+
Total releases behind: 20
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Options:
|
|
48
53
|
|
|
49
54
|
```
|
|
50
|
-
--as-of DATE
|
|
51
|
-
--
|
|
52
|
-
--
|
|
53
|
-
--
|
|
55
|
+
--as-of DATE Only consider versions released before DATE (YYYY-MM-DD)
|
|
56
|
+
--format FORMATTER Choose an output formatter (plaintext, json) (default: plaintext)
|
|
57
|
+
--sort SPEC Sort by comma-separated columns (default: libyear,name)
|
|
58
|
+
--[no-]indirect Include indirect dependencies in output (on by default)
|
|
59
|
+
--verbose Run with logs
|
|
60
|
+
--help Show help
|
|
61
|
+
--version Show version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Sorting
|
|
65
|
+
|
|
66
|
+
The `--sort` flag accepts a comma-separated list of columns. Each column has a
|
|
67
|
+
fixed direction: `name` is ascending, while `latest-date`, `versions`, and
|
|
68
|
+
`libyear` are descending. Later columns are used to break ties.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
libyear-rb --sort versions,name # most releases behind first, then by name
|
|
72
|
+
libyear-rb --sort libyear # most libyears behind first
|
|
54
73
|
```
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
#### Formatters
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
- `plaintext` (default) — prints a human-readable table to stdout, listing only outdated gems.
|
|
78
|
+
- `json` — prints a machine-readable JSON document to stdout, listing every gem along with a summary.
|
|
59
79
|
|
|
60
80
|
```bash
|
|
61
|
-
libyear-rb --
|
|
81
|
+
libyear-rb --format json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### As a Library
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require "libyear_rb"
|
|
88
|
+
|
|
89
|
+
results = LibyearRb::Runner.new.run(File.read("Gemfile.lock"))
|
|
90
|
+
|
|
91
|
+
results.each do |r|
|
|
92
|
+
puts "#{r.name}: #{r.version_distance} versions, #{r.libyear_in_days} days behind"
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Configure the cache or logger before use:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
LibyearRb.cache = LibyearRb::FileCache.new(skip_cache: true)
|
|
100
|
+
LibyearRb.logger = Logger.new($stderr)
|
|
62
101
|
```
|
|
63
102
|
|
|
64
103
|
## Private Gem Servers
|
|
65
104
|
|
|
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.
|
|
105
|
+
`libyear-rb` supports any gem server with a RubyGems.org-compatible API. It uses your configured gem sources from `~/.gemrc` (`gem sources`) or environment, so private gems hosted on servers like Gemfury, Artifactory, or self-hosted solutions work automatically.
|
|
67
106
|
|
|
68
107
|
## Caching
|
|
69
108
|
|
|
70
109
|
To reduce API requests and improve performance, `libyear-rb` caches gem version metadata for 24 hours.
|
|
71
110
|
|
|
72
|
-
|
|
73
|
-
-
|
|
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.
|
|
111
|
+
Cache location:
|
|
112
|
+
- Gem info is cached under `$XDG_CACHE_HOME/libyear-rb/` (or `~/.cache/libyear-rb/` if `XDG_CACHE_HOME` is not set), with a subdirectory for each gem host.
|
|
77
113
|
|
|
78
|
-
|
|
114
|
+
To skip the cache:
|
|
79
115
|
```bash
|
|
80
116
|
SKIP_CACHE=1 libyear-rb
|
|
81
117
|
```
|
|
82
118
|
|
|
83
|
-
|
|
119
|
+
To clear the cache:
|
|
84
120
|
```bash
|
|
85
121
|
rm -rf ~/.cache/libyear-rb
|
|
86
122
|
```
|
|
87
123
|
|
|
88
|
-
##
|
|
124
|
+
## Development
|
|
89
125
|
|
|
90
|
-
|
|
126
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
91
127
|
|
|
92
|
-
|
|
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.
|
|
128
|
+
### Commands
|
|
95
129
|
|
|
96
|
-
|
|
130
|
+
```bash
|
|
131
|
+
# Setup development environment
|
|
132
|
+
bin/setup
|
|
97
133
|
|
|
98
|
-
|
|
134
|
+
# Run tests
|
|
135
|
+
rake test
|
|
99
136
|
|
|
100
|
-
|
|
137
|
+
# Run linter
|
|
138
|
+
rake rubocop
|
|
139
|
+
|
|
140
|
+
# Install gem locally
|
|
141
|
+
bundle exec rake install
|
|
142
|
+
```
|
|
101
143
|
|
|
102
144
|
## Acknowledgements
|
|
103
145
|
|
|
104
146
|
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
147
|
|
|
148
|
+
## Alternatives
|
|
149
|
+
|
|
150
|
+
- [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.
|
|
151
|
+
- [bundler-audit](https://github.com/rubysec/bundler-audit) - Focuses on security vulnerabilities rather than freshness.
|
|
152
|
+
- [bundle outdated](https://bundler.io/man/bundle-outdated.1.html) - Built into Bundler. Shows outdated gems but doesn't calculate libyears.
|
|
153
|
+
|
|
106
154
|
## License
|
|
107
155
|
|
|
108
156
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/libyear-rb
CHANGED
data/lib/libyear_rb/cli.rb
CHANGED
|
@@ -8,38 +8,26 @@ module LibyearRb
|
|
|
8
8
|
class CLI
|
|
9
9
|
def initialize(argv = ARGV)
|
|
10
10
|
@argv = argv.dup
|
|
11
|
-
@options = {
|
|
11
|
+
@options = {
|
|
12
|
+
formatter: Formatter.for("plaintext"),
|
|
13
|
+
indirect: true,
|
|
14
|
+
sorters: Sorter.parse(Sorter::DEFAULT_SPEC)
|
|
15
|
+
}
|
|
12
16
|
parse_options!
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
def run
|
|
20
|
+
LibyearRb.logger = Logger.new($stderr) if @options[:verbose]
|
|
21
|
+
|
|
16
22
|
lockfile_contents = read_lockfile
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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])
|
|
23
|
+
results = Runner.new.run(lockfile_contents, as_of: @options[:as_of])
|
|
24
|
+
sorted_results = Sorter.sort(results, @options.fetch(:sorters))
|
|
25
|
+
|
|
26
|
+
@options.fetch(:formatter).new(indirect: @options.fetch(:indirect)).generate(sorted_results)
|
|
33
27
|
end
|
|
34
28
|
|
|
35
29
|
private
|
|
36
30
|
|
|
37
|
-
def build_logger
|
|
38
|
-
return nil unless @options[:verbose]
|
|
39
|
-
|
|
40
|
-
Logger.new($stderr)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
31
|
def parse_options!
|
|
44
32
|
OptionParser.new do |opts|
|
|
45
33
|
opts.banner = "Usage: libyear-rb [Gemfile.lock] [options]"
|
|
@@ -63,6 +51,25 @@ module LibyearRb
|
|
|
63
51
|
exit 1
|
|
64
52
|
end
|
|
65
53
|
|
|
54
|
+
opts.on("-f", "--format FORMATTER", "Choose an output formatter (#{Formatter::NAMES.join(", ")}) (default: plaintext).") do |name|
|
|
55
|
+
@options[:formatter] = Formatter.for(name)
|
|
56
|
+
rescue ArgumentError => e
|
|
57
|
+
warn e.message
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.on("--sort SPEC",
|
|
62
|
+
"Sort by comma-separated columns (#{Sorter::COLUMNS.keys.join(", ")}) (default: #{Sorter::DEFAULT_SPEC}).") do |spec|
|
|
63
|
+
@options[:sorters] = Sorter.parse(spec)
|
|
64
|
+
rescue Sorter::InvalidSpec => e
|
|
65
|
+
warn e.message
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
opts.on("--[no-]indirect", "Include indirect dependencies in output (on by default).") do |value|
|
|
70
|
+
@options[:indirect] = value
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
opts.on("--verbose", "Run with verbose logs") do
|
|
67
74
|
@options[:verbose] = true
|
|
68
75
|
end
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module LibyearRb
|
|
4
4
|
class DependencyAnalyzer
|
|
5
|
-
def
|
|
6
|
-
|
|
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 }
|
|
5
|
+
def self.freshness(spec, versions_metadata)
|
|
6
|
+
current_version_info = versions_metadata.find { |version| version.number == spec.version }
|
|
11
7
|
|
|
12
8
|
if current_version_info.nil?
|
|
13
|
-
|
|
9
|
+
LibyearRb.logger.warn("Skipping #{spec.name}: installed version #{spec.version} not found in metadata")
|
|
14
10
|
return
|
|
15
11
|
end
|
|
16
12
|
|
|
@@ -23,10 +19,6 @@ module LibyearRb
|
|
|
23
19
|
latest_version = latest_version_info.number
|
|
24
20
|
current_version = current_version_info.number
|
|
25
21
|
|
|
26
|
-
if latest_version == current_version
|
|
27
|
-
return nil
|
|
28
|
-
end
|
|
29
|
-
|
|
30
22
|
latest_release_date = latest_version_info.created_at
|
|
31
23
|
current_release_date = current_version_info.created_at
|
|
32
24
|
|
|
@@ -34,14 +26,14 @@ module LibyearRb
|
|
|
34
26
|
libyear_in_days = [(latest_release_date - current_release_date).to_i, 0].max
|
|
35
27
|
|
|
36
28
|
Result.new(
|
|
37
|
-
name:
|
|
38
|
-
current_version:
|
|
29
|
+
name: spec.name,
|
|
30
|
+
current_version: spec.version,
|
|
39
31
|
current_version_release_date: current_release_date,
|
|
40
32
|
latest_version: latest_version,
|
|
41
33
|
latest_version_release_date: latest_release_date,
|
|
42
34
|
version_distance: version_distance,
|
|
43
35
|
libyear_in_days: libyear_in_days,
|
|
44
|
-
is_direct:
|
|
36
|
+
is_direct: spec.direct
|
|
45
37
|
)
|
|
46
38
|
end
|
|
47
39
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module LibyearRb
|
|
7
|
+
class FileCache
|
|
8
|
+
CACHE_EXPIRATION = 86_400 # 24 hours in seconds
|
|
9
|
+
CACHE_DIR_ROOT = ENV.fetch("XDG_CACHE_HOME", File.join(Dir.home, ".cache"))
|
|
10
|
+
CACHE_DIR_DEFAULT = File.join(CACHE_DIR_ROOT, "libyear-rb")
|
|
11
|
+
SKIP_CACHE = ENV.fetch("SKIP_CACHE", "0") == "1"
|
|
12
|
+
|
|
13
|
+
attr_reader :cache_dir
|
|
14
|
+
|
|
15
|
+
def initialize(cache_dir: CACHE_DIR_DEFAULT, skip_cache: SKIP_CACHE)
|
|
16
|
+
@cache_dir = Pathname.new(cache_dir)
|
|
17
|
+
@skip_cache = skip_cache
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch(remote_host, gem_name)
|
|
21
|
+
return yield if @skip_cache
|
|
22
|
+
|
|
23
|
+
path = cache_file_path(remote_host, gem_name)
|
|
24
|
+
if cache_valid?(path)
|
|
25
|
+
JSON.parse(path.read)
|
|
26
|
+
else
|
|
27
|
+
data = yield
|
|
28
|
+
return data if data.empty?
|
|
29
|
+
|
|
30
|
+
path.dirname.mkpath
|
|
31
|
+
path.write(JSON.dump(data))
|
|
32
|
+
path.utime(Time.now, Time.now)
|
|
33
|
+
data
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def cache_valid?(path)
|
|
40
|
+
return false unless path.exist?
|
|
41
|
+
|
|
42
|
+
cache_age = Time.now - path.mtime
|
|
43
|
+
cache_age < CACHE_EXPIRATION
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cache_file_path(remote_host, gem_name)
|
|
47
|
+
host_key = remote_host.gsub(/\W/, "_")
|
|
48
|
+
cache_dir.join(host_key, "#{gem_name}.json")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibyearRb
|
|
4
|
+
class Formatter
|
|
5
|
+
NAMES = %w[plaintext json].freeze
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def for(name)
|
|
9
|
+
case name
|
|
10
|
+
when "plaintext"
|
|
11
|
+
require "libyear_rb/formatters/plaintext_formatter"
|
|
12
|
+
PlaintextFormatter
|
|
13
|
+
when "json"
|
|
14
|
+
require "libyear_rb/formatters/json_formatter"
|
|
15
|
+
JsonFormatter
|
|
16
|
+
else
|
|
17
|
+
raise ArgumentError, "Unknown formatter: #{name.inspect}. Available formatters: #{NAMES.join(", ")}."
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(io: $stdout, indirect: true)
|
|
23
|
+
@io = io
|
|
24
|
+
@indirect = indirect
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def generate(results)
|
|
28
|
+
raise NotImplementedError, "Subclasses must implement the generate method"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LibyearRb
|
|
6
|
+
class JsonFormatter < Formatter
|
|
7
|
+
def generate(results)
|
|
8
|
+
total_days = results.sum { |r| r.libyear_in_days || 0 }
|
|
9
|
+
total_versions = results.sum { |r| r.version_distance || 0 }
|
|
10
|
+
|
|
11
|
+
visible_results = @indirect ? results : results.select(&:is_direct)
|
|
12
|
+
|
|
13
|
+
payload = {
|
|
14
|
+
gems: visible_results.map { |result| gem_hash(result) },
|
|
15
|
+
summary: {
|
|
16
|
+
libyears_behind: (total_days / 365.0).round(2),
|
|
17
|
+
total_releases_behind: total_versions
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@io.puts JSON.pretty_generate(payload)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def gem_hash(result)
|
|
27
|
+
{
|
|
28
|
+
name: result.name.to_s,
|
|
29
|
+
current_version: result.current_version&.to_s,
|
|
30
|
+
current_version_release_date: result.current_version_release_date&.strftime("%Y-%m-%d"),
|
|
31
|
+
latest_version: result.latest_version&.to_s,
|
|
32
|
+
latest_version_release_date: result.latest_version_release_date&.strftime("%Y-%m-%d"),
|
|
33
|
+
version_distance: result.version_distance,
|
|
34
|
+
libyear_in_days: result.libyear_in_days,
|
|
35
|
+
direct: result.is_direct
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module LibyearRb
|
|
4
|
-
class
|
|
4
|
+
class PlaintextFormatter < Formatter
|
|
5
5
|
UNKNOWN = "Unknown"
|
|
6
6
|
COLUMN_BUFFER = 3
|
|
7
7
|
HEADERS = ["Gem", "Current", "Current Date", "Latest", "Latest Date", "Versions", "Days", "Years"].freeze
|
|
8
8
|
|
|
9
9
|
def generate(results)
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
total_days = results.sum { |r| r.libyear_in_days || 0 }
|
|
11
|
+
total_versions = results.sum { |r| r.version_distance || 0 }
|
|
12
|
+
|
|
13
|
+
visible_results = @indirect ? results : results.select(&:is_direct)
|
|
14
|
+
visible_results = visible_results.reject do |result|
|
|
15
|
+
result.version_distance.zero?
|
|
12
16
|
end
|
|
13
17
|
|
|
14
|
-
return if
|
|
18
|
+
return if visible_results.empty?
|
|
19
|
+
|
|
20
|
+
rows = visible_results.map { |result| row_for(result) }
|
|
15
21
|
|
|
16
22
|
widths = calculate_widths(rows)
|
|
17
23
|
@io.puts format_row(HEADERS, widths)
|
|
18
24
|
@io.puts divider(widths)
|
|
19
25
|
rows.each { |row| @io.puts format_row(row, widths) }
|
|
20
26
|
|
|
21
|
-
total_days = results.sum { |r| r.libyear_in_days || 0 }
|
|
22
|
-
total_versions = results.sum { |r| r.version_distance || 0 }
|
|
23
27
|
@io.puts "System is %.2f libyears behind" % (total_days / 365.0)
|
|
24
28
|
@io.puts "Total releases behind: #{total_versions}"
|
|
25
29
|
end
|
|
@@ -1,80 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "gems"
|
|
4
3
|
require "date"
|
|
4
|
+
require "gems"
|
|
5
5
|
require "rubygems"
|
|
6
6
|
|
|
7
7
|
module LibyearRb
|
|
8
8
|
class GemInfoFetcher
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
def initialize(remote_host, rate_limiter: -> {})
|
|
10
|
+
@remote_host = remote_host
|
|
11
|
+
@rate_limiter = rate_limiter
|
|
12
|
+
source = Gem.sources.each_source.find { |s| s.uri.host == remote_host }
|
|
13
|
+
if source
|
|
14
|
+
uri = source.uri
|
|
15
|
+
@client = Gems::Client.new(
|
|
16
|
+
host: uri.origin + uri.request_uri,
|
|
17
|
+
username: uri.user,
|
|
18
|
+
password: uri.password
|
|
19
|
+
)
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
return [] unless client
|
|
23
|
+
def versions_for(gem_name)
|
|
24
|
+
return [] unless @client
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
raw = fetch_raw(gem_name)
|
|
27
|
+
parse_versions(gem_name, raw)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
28
31
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
client.versions(gem_name)
|
|
32
|
+
def fetch_raw(gem_name)
|
|
33
|
+
LibyearRb.cache.fetch(@remote_host, gem_name) do
|
|
34
|
+
@rate_limiter.call
|
|
35
|
+
Array(@client.versions(gem_name))
|
|
34
36
|
rescue Gems::GemError, Gems::NotFound
|
|
35
37
|
[]
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
def
|
|
40
|
-
Array(raw_versions)
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
41
|
+
def parse_versions(gem_name, raw_versions)
|
|
42
|
+
Array(raw_versions).map do |attrs|
|
|
43
|
+
GemVersion.new(
|
|
44
|
+
name: gem_name,
|
|
45
|
+
number: Gem::Version.new(attrs["number"]),
|
|
46
|
+
created_at: Date.parse(attrs["created_at"]),
|
|
47
|
+
prerelease?: attrs["prerelease"]
|
|
63
48
|
)
|
|
64
49
|
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
50
|
end
|
|
79
51
|
end
|
|
80
52
|
end
|
|
@@ -25,6 +25,10 @@ module LibyearRb
|
|
|
25
25
|
VERSION_LINE = /^ ?([^ ].+)$/
|
|
26
26
|
BUNDLED_VERSION = /^ ?([^ ].+)$/
|
|
27
27
|
|
|
28
|
+
def self.parse(lockfile_content)
|
|
29
|
+
new.parse(lockfile_content)
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
def parse(lockfile_content)
|
|
29
33
|
lines = lockfile_content.lines(chomp: true)
|
|
30
34
|
|
|
@@ -58,6 +62,13 @@ module LibyearRb
|
|
|
58
62
|
end
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
direct_names = dependencies.map(&:name)
|
|
66
|
+
sources = sources.map do |source|
|
|
67
|
+
source.with(specs: source.specs.map { |spec|
|
|
68
|
+
spec.with(direct: direct_names.include?(spec.name))
|
|
69
|
+
})
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
Lockfile.new(
|
|
62
73
|
sources: sources,
|
|
63
74
|
platforms: platforms,
|
data/lib/libyear_rb/models.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
module LibyearRb
|
|
4
4
|
Lockfile = Data.define(:sources, :platforms, :dependencies, :ruby_version, :bundled_with)
|
|
5
5
|
Source = Data.define(:type, :remote, :revision, :specs, :options)
|
|
6
|
-
Spec = Data.define(:name, :version, :dependencies)
|
|
6
|
+
Spec = Data.define(:name, :version, :dependencies, :direct) do
|
|
7
|
+
def initialize(name:, version:, dependencies: [], direct: false)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
end
|
|
7
11
|
Dependency = Data.define(:name, :version_requirements)
|
|
8
12
|
Platform = Data.define(:name)
|
|
9
13
|
RubyVersion = Data.define(:version, :engine, :patchlevel)
|
data/lib/libyear_rb/runner.rb
CHANGED
|
@@ -4,44 +4,80 @@ require "uri"
|
|
|
4
4
|
|
|
5
5
|
module LibyearRb
|
|
6
6
|
class Runner
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@gem_info_fetcher = gem_info_fetcher
|
|
10
|
-
@dependency_analyzer = dependency_analyzer
|
|
11
|
-
@reporter = reporter
|
|
12
|
-
@logger = logger
|
|
13
|
-
end
|
|
7
|
+
MAX_WORKERS_PER_HOST = 10
|
|
8
|
+
RATE_LIMIT = 10 # requests per second per host
|
|
14
9
|
|
|
15
10
|
def run(lockfile_contents, as_of: nil)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
11
|
+
specs_by_host = parse_specs(lockfile_contents)
|
|
12
|
+
threads = []
|
|
13
|
+
replenishers = []
|
|
23
14
|
|
|
24
|
-
|
|
15
|
+
specs_by_host.each do |host, specs|
|
|
16
|
+
token_queue = Thread::SizedQueue.new(RATE_LIMIT)
|
|
17
|
+
rate_limiter = -> { token_queue.pop }
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
19
|
+
work_queue = Thread::Queue.new
|
|
20
|
+
specs.each { |spec| work_queue << spec }
|
|
21
|
+
work_queue.close
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
replenishers << Thread.new do
|
|
24
|
+
loop do
|
|
25
|
+
sleep(1.0 / RATE_LIMIT)
|
|
26
|
+
token_queue << :token
|
|
37
27
|
end
|
|
28
|
+
end
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
[specs.size, MAX_WORKERS_PER_HOST].min.times do
|
|
31
|
+
threads << Thread.new do
|
|
32
|
+
fetcher = GemInfoFetcher.new(host, rate_limiter: rate_limiter)
|
|
33
|
+
thread_results = []
|
|
34
|
+
while (spec = work_queue.pop)
|
|
35
|
+
begin
|
|
36
|
+
versions = fetcher.versions_for(spec.name)
|
|
37
|
+
thread_results << [spec, versions]
|
|
38
|
+
rescue => e
|
|
39
|
+
LibyearRb.logger.error("Error fetching #{spec.name}: #{e.message}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
thread_results
|
|
43
|
+
end
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
|
|
47
|
+
spec_versions = threads.flat_map(&:value)
|
|
48
|
+
spec_versions.filter_map { |spec, versions| analyze(spec, versions, as_of: as_of) }
|
|
49
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
50
|
+
threads.each { |t| t.kill unless t == Thread.current }
|
|
51
|
+
raise
|
|
52
|
+
ensure
|
|
53
|
+
replenishers.each(&:kill)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def parse_specs(lockfile_contents)
|
|
59
|
+
lockfile = LockfileParser.parse(lockfile_contents)
|
|
60
|
+
lockfile.sources
|
|
61
|
+
.select { |source| source.type == :gem && source.remote }
|
|
62
|
+
.group_by { |source| URI.parse(source.remote).host }
|
|
63
|
+
.transform_values { |sources| sources.flat_map(&:specs).uniq(&:name) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def analyze(spec, versions, as_of:)
|
|
67
|
+
filtered = versions
|
|
68
|
+
.reject { |v| as_of && v.created_at > as_of }
|
|
69
|
+
.sort_by(&:number)
|
|
70
|
+
.reverse
|
|
71
|
+
|
|
72
|
+
if filtered.empty?
|
|
73
|
+
LibyearRb.logger.warn("Skipping #{spec.name}: no version metadata")
|
|
74
|
+
return nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
DependencyAnalyzer.freshness(spec, filtered)
|
|
78
|
+
rescue => e
|
|
79
|
+
LibyearRb.logger.error("Error processing #{spec.name}: #{e.message}")
|
|
80
|
+
nil
|
|
45
81
|
end
|
|
46
82
|
end
|
|
47
83
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibyearRb
|
|
4
|
+
SortColumn = Data.define(:field, :direction) do
|
|
5
|
+
def compare(a, b)
|
|
6
|
+
av = a.public_send(field)
|
|
7
|
+
bv = b.public_send(field)
|
|
8
|
+
return 0 if av.nil? && bv.nil?
|
|
9
|
+
return 1 if av.nil?
|
|
10
|
+
return -1 if bv.nil?
|
|
11
|
+
(direction == :desc) ? bv <=> av : av <=> bv
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Sorter
|
|
16
|
+
DEFAULT_SPEC = "libyear,name"
|
|
17
|
+
|
|
18
|
+
COLUMNS = {
|
|
19
|
+
"name" => SortColumn.new(field: :name, direction: :asc),
|
|
20
|
+
"latest-date" => SortColumn.new(field: :latest_version_release_date, direction: :desc),
|
|
21
|
+
"versions" => SortColumn.new(field: :version_distance, direction: :desc),
|
|
22
|
+
"libyear" => SortColumn.new(field: :libyear_in_days, direction: :desc)
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
InvalidSpec = Class.new(ArgumentError)
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def parse(spec)
|
|
29
|
+
spec.to_s.split(",").map do |part|
|
|
30
|
+
column = part.strip
|
|
31
|
+
unless COLUMNS.key?(column)
|
|
32
|
+
raise InvalidSpec, "Unknown sort column: #{column.inspect}. Available columns: #{COLUMNS.keys.join(", ")}."
|
|
33
|
+
end
|
|
34
|
+
COLUMNS.fetch(column)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sort(results, sorters)
|
|
39
|
+
results.sort do |a, b|
|
|
40
|
+
cmp = 0
|
|
41
|
+
sorters.each do |sorter|
|
|
42
|
+
cmp = sorter.compare(a, b)
|
|
43
|
+
break unless cmp.zero?
|
|
44
|
+
end
|
|
45
|
+
cmp
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/libyear_rb/version.rb
CHANGED
data/lib/libyear_rb.rb
CHANGED
|
@@ -3,14 +3,27 @@
|
|
|
3
3
|
require_relative "libyear_rb/version"
|
|
4
4
|
require_relative "libyear_rb/models"
|
|
5
5
|
require_relative "libyear_rb/lockfile_parser"
|
|
6
|
-
require_relative "libyear_rb/
|
|
6
|
+
require_relative "libyear_rb/file_cache"
|
|
7
7
|
require_relative "libyear_rb/gem_info_fetcher"
|
|
8
8
|
require_relative "libyear_rb/dependency_analyzer"
|
|
9
|
-
require_relative "libyear_rb/
|
|
10
|
-
require_relative "libyear_rb/
|
|
9
|
+
require_relative "libyear_rb/formatters/formatter"
|
|
10
|
+
require_relative "libyear_rb/sorter"
|
|
11
11
|
require_relative "libyear_rb/runner"
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
require "logger"
|
|
13
14
|
|
|
14
15
|
module LibyearRb
|
|
15
16
|
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_writer :logger, :cache
|
|
20
|
+
|
|
21
|
+
def logger
|
|
22
|
+
@logger ||= Logger.new(File::NULL)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cache
|
|
26
|
+
@cache ||= FileCache.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
16
29
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: libyear-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Countz
|
|
@@ -14,14 +14,14 @@ dependencies:
|
|
|
14
14
|
name: gems
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '1.3'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.3'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
@@ -62,13 +62,15 @@ files:
|
|
|
62
62
|
- lib/libyear_rb.rb
|
|
63
63
|
- lib/libyear_rb/cli.rb
|
|
64
64
|
- lib/libyear_rb/dependency_analyzer.rb
|
|
65
|
-
- lib/libyear_rb/
|
|
65
|
+
- lib/libyear_rb/file_cache.rb
|
|
66
|
+
- lib/libyear_rb/formatters/formatter.rb
|
|
67
|
+
- lib/libyear_rb/formatters/json_formatter.rb
|
|
68
|
+
- lib/libyear_rb/formatters/plaintext_formatter.rb
|
|
66
69
|
- lib/libyear_rb/gem_info_fetcher.rb
|
|
67
70
|
- lib/libyear_rb/lockfile_parser.rb
|
|
68
71
|
- lib/libyear_rb/models.rb
|
|
69
|
-
- lib/libyear_rb/reporters/plaintext_reporter.rb
|
|
70
|
-
- lib/libyear_rb/reporters/reporter.rb
|
|
71
72
|
- lib/libyear_rb/runner.rb
|
|
73
|
+
- lib/libyear_rb/sorter.rb
|
|
72
74
|
- lib/libyear_rb/version.rb
|
|
73
75
|
homepage: https://github.com/thomascountz/libyear-rb
|
|
74
76
|
licenses:
|
|
@@ -93,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
93
95
|
- !ruby/object:Gem::Version
|
|
94
96
|
version: '0'
|
|
95
97
|
requirements: []
|
|
96
|
-
rubygems_version: 4.0.
|
|
98
|
+
rubygems_version: 4.0.16
|
|
97
99
|
specification_version: 4
|
|
98
100
|
summary: A simple measure of dependency freshness
|
|
99
101
|
test_files: []
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|