gem_changelog_diff 0.7.0 → 0.8.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/CHANGELOG.md +31 -1
- data/README.md +16 -1
- data/ROADMAP.md +0 -14
- data/lib/gem_changelog_diff/cache.rb +4 -1
- data/lib/gem_changelog_diff/changelog_parser.rb +21 -8
- data/lib/gem_changelog_diff/cli.rb +36 -10
- data/lib/gem_changelog_diff/concurrent_fetcher.rb +11 -2
- data/lib/gem_changelog_diff/configuration.rb +6 -2
- data/lib/gem_changelog_diff/github_client.rb +74 -21
- data/lib/gem_changelog_diff/interactive.rb +2 -1
- data/lib/gem_changelog_diff/rubygems_client.rb +12 -18
- data/lib/gem_changelog_diff/tag_matcher.rb +44 -0
- data/lib/gem_changelog_diff/uri_resolver.rb +101 -0
- data/lib/gem_changelog_diff/version.rb +1 -1
- data/lib/gem_changelog_diff.rb +2 -0
- data/sig/gem_changelog_diff.rbs +44 -6
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c69caa73eea6c508419eff5debb1b5b75bd56edf29140519fb451a433ed67272
|
|
4
|
+
data.tar.gz: 310b2803925afeb1a107d5805d8f20e681c3257962bb762fcf0db11fb5c78c60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 919f9f8da6bd23c1b2e5210ac0d4bd7f66217f4dd897b735006605858552c757eeffdf962acb23f97ec307fc2c1957964acd787ee9704ccf9061758cde4adb16
|
|
7
|
+
data.tar.gz: 1808e80f094a1035fc34245dd21a3965fd63a35d794e66c9a66d74b4405deba9fb84e0f15360419561b160a1b21807d82f170141aa2a666060b0d0777f8a7a8b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-06-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `UriResolver` class for source URI resolution with non-GitHub host detection
|
|
15
|
+
- Informative skip messages for gems hosted on GitLab, Codeberg, Bitbucket, and SourceHut
|
|
16
|
+
- Automatic redirect following for renamed GitHub repositories (up to 3 hops)
|
|
17
|
+
- `RepoNotFoundError` raised with descriptive messages for non-GitHub gems
|
|
18
|
+
- `TagMatcher` class for tag format normalization (`v1.2.3`, `1.2.3`, `gem_name-1.2.3`, `release-1.2.3`)
|
|
19
|
+
- GitHub API pagination for gems with 100+ releases (up to 1000 releases)
|
|
20
|
+
- Early termination when paginated releases pass the current version
|
|
21
|
+
- Per-request timeout (10s default) and total timeout (120s default)
|
|
22
|
+
- `--timeout` flag to set per-request timeout
|
|
23
|
+
- `request_timeout` and `total_timeout` config file options
|
|
24
|
+
- Interactive mode help hint: "(Space to select, Enter to confirm)"
|
|
25
|
+
- `JSON::ParserError` handling in gem report builder to prevent worker thread crashes
|
|
26
|
+
- Wider network error handling: `Errno::ETIMEDOUT`, `Errno::ECONNRESET`, `OpenSSL::SSL::SSLError`
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `RubygemsClient` delegates URI resolution to `UriResolver` (extracted `extract_github_repo`)
|
|
31
|
+
- Error message for missing repo changed from "Could not find GitHub repository" to "Could not determine source repository"
|
|
32
|
+
- `GithubClient` uses `TagMatcher` for version extraction (replaced `TAG_VERSION_REGEX`)
|
|
33
|
+
- `GithubClient` fetches 100 releases per page (was 30) with pagination support
|
|
34
|
+
- `GithubClient` returns empty array for HTTP 301 responses (redirects handled by `UriResolver`)
|
|
35
|
+
- All `Gem::Version.new` calls wrapped in `safe_gem_version` helper to prevent `ArgumentError` crashes
|
|
36
|
+
- `ConcurrentFetcher` wrapped in total timeout to prevent runaway operations
|
|
37
|
+
- HTTP requests in `Cache`, `RubygemsClient`, and `ChangelogParser` use configurable timeouts
|
|
38
|
+
|
|
10
39
|
## [0.7.0] - 2026-06-18
|
|
11
40
|
|
|
12
41
|
### Added
|
|
@@ -89,7 +118,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
89
118
|
- Plain text formatter for changelog output
|
|
90
119
|
- Full end-to-end pipeline: detect → lookup → fetch → format
|
|
91
120
|
|
|
92
|
-
[Unreleased]: https://github.com/eclectic-coding/gem_changelog_diff/compare/v0.
|
|
121
|
+
[Unreleased]: https://github.com/eclectic-coding/gem_changelog_diff/compare/v0.8.0...HEAD
|
|
122
|
+
[0.8.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.8.0
|
|
93
123
|
[0.7.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.7.0
|
|
94
124
|
[0.6.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.6.0
|
|
95
125
|
[0.5.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.5.0
|
data/README.md
CHANGED
|
@@ -21,7 +21,8 @@ CLI that shows you the changelog diff for each gem before you `bundle update`, p
|
|
|
21
21
|
- [Show Subcommand](#show-subcommand)
|
|
22
22
|
- [Caching](#caching)
|
|
23
23
|
- [Dry Run](#dry-run)
|
|
24
|
-
- [
|
|
24
|
+
- [Timeouts](#timeouts)
|
|
25
|
+
- [Configuration File](#configuration-file)
|
|
25
26
|
- [Development](#development)
|
|
26
27
|
- [Contributing](#contributing)
|
|
27
28
|
- [License](#license)
|
|
@@ -55,6 +56,10 @@ gem_changelog_diff version # Print version
|
|
|
55
56
|
gem_changelog_diff --version # Same as above
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
### Non-GitHub Gems
|
|
60
|
+
|
|
61
|
+
Gems hosted on GitHub are fully supported. Gems on other platforms (GitLab, Codeberg, Bitbucket, SourceHut) are detected and skipped with an informative message. Renamed GitHub repositories are followed automatically via redirect.
|
|
62
|
+
|
|
58
63
|
### GitHub Authentication
|
|
59
64
|
|
|
60
65
|
To avoid the 60 requests/hour unauthenticated rate limit, provide a GitHub personal access token:
|
|
@@ -157,6 +162,14 @@ gem_changelog_diff --dry-run --format json # JSON array of gem objects
|
|
|
157
162
|
gem_changelog_diff --dry-run --format markdown # Markdown bullet list
|
|
158
163
|
```
|
|
159
164
|
|
|
165
|
+
### Timeouts
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gem_changelog_diff --timeout 30 # Per-request timeout in seconds (default: 10)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The total operation timeout (default: 120s) limits how long concurrent fetching can run. Both values are configurable via the config file.
|
|
172
|
+
|
|
160
173
|
### Configuration File
|
|
161
174
|
|
|
162
175
|
Generate a config file template:
|
|
@@ -181,6 +194,8 @@ ignore_gems:
|
|
|
181
194
|
- rake
|
|
182
195
|
- bundler
|
|
183
196
|
no_color: false
|
|
197
|
+
request_timeout: 10 # per-request timeout in seconds
|
|
198
|
+
total_timeout: 120 # total operation timeout in seconds
|
|
184
199
|
```
|
|
185
200
|
|
|
186
201
|
CLI flags always take priority over config file values.
|
data/ROADMAP.md
CHANGED
|
@@ -2,20 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Feature roadmap for gem_changelog_diff. Each section is auto-pruned by `bin/release` when that version ships.
|
|
4
4
|
|
|
5
|
-
## 0.8.0 -- Robustness & Edge Cases
|
|
6
|
-
|
|
7
|
-
Handle the long tail of real-world gem repository patterns.
|
|
8
|
-
|
|
9
|
-
- Source URI resolution: detect and skip GitLab/Codeberg gracefully, follow redirects for renamed repos, handle monorepo subdirectory URIs
|
|
10
|
-
- Tag format normalization: `v1.2.3`, `1.2.3`, `gem_name-1.2.3`, `release-1.2.3`
|
|
11
|
-
- Proper version comparison via `Gem::Version` (handles pre-release: `1.0.0.rc1`, `1.0.0.beta2`)
|
|
12
|
-
- GitHub API pagination for gems with 100+ releases
|
|
13
|
-
- Per-request timeout (10s default), total timeout (120s default), configurable via `--timeout`
|
|
14
|
-
|
|
15
|
-
**New files:** `tag_matcher.rb`, `uri_resolver.rb`
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
5
|
## 0.9.0 -- Pre-1.0 Stabilization
|
|
20
6
|
|
|
21
7
|
Freeze the public API. Harden the test suite. Prepare documentation for stable release.
|
|
@@ -84,10 +84,13 @@ module GemChangelogDiff
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def fetch_from_network(uri, headers)
|
|
87
|
+
timeout = GemChangelogDiff.configuration.request_timeout
|
|
87
88
|
request = Net::HTTP::Get.new(uri)
|
|
88
89
|
headers.each { |k, v| request[k] = v }
|
|
89
90
|
|
|
90
|
-
Net::HTTP.start(uri.hostname, uri.port,
|
|
91
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
92
|
+
use_ssl: uri.scheme == "https",
|
|
93
|
+
open_timeout: timeout, read_timeout: timeout) do |http|
|
|
91
94
|
http.request(request)
|
|
92
95
|
end
|
|
93
96
|
end
|
|
@@ -19,7 +19,9 @@ module GemChangelogDiff
|
|
|
19
19
|
|
|
20
20
|
parse_entries(content, current_version, newest_version)
|
|
21
21
|
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
|
22
|
-
|
|
22
|
+
Errno::ETIMEDOUT, Errno::ECONNRESET,
|
|
23
|
+
Net::OpenTimeout, Net::ReadTimeout,
|
|
24
|
+
OpenSSL::SSL::SSLError => e
|
|
23
25
|
raise NetworkError, "GitHub API request failed: #{e.message}"
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -47,9 +49,13 @@ module GemChangelogDiff
|
|
|
47
49
|
headers = request_headers
|
|
48
50
|
return @cache.get(uri, headers: headers) if @cache
|
|
49
51
|
|
|
52
|
+
timeout = GemChangelogDiff.configuration.request_timeout
|
|
50
53
|
request = Net::HTTP::Get.new(uri)
|
|
51
54
|
headers.each { |k, v| request[k] = v }
|
|
52
|
-
Net::HTTP.start(uri.hostname, uri.port,
|
|
55
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
56
|
+
use_ssl: true, open_timeout: timeout, read_timeout: timeout) do |http|
|
|
57
|
+
http.request(request)
|
|
58
|
+
end
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
def request_headers
|
|
@@ -63,21 +69,28 @@ module GemChangelogDiff
|
|
|
63
69
|
end
|
|
64
70
|
|
|
65
71
|
def parse_entries(content, current_version, newest_version)
|
|
66
|
-
current =
|
|
67
|
-
newest =
|
|
68
|
-
|
|
72
|
+
current = safe_gem_version(current_version)
|
|
73
|
+
newest = safe_gem_version(newest_version)
|
|
74
|
+
return [] unless current && newest
|
|
69
75
|
|
|
76
|
+
sections = split_sections(content)
|
|
70
77
|
matched = sections.filter_map { |v, body| build_entry(v, body, current, newest) }
|
|
71
|
-
matched.sort_by { |e|
|
|
78
|
+
matched.sort_by { |e| safe_gem_version(e[:tag_name]) || Gem::Version.new("0") }.reverse
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
def build_entry(version_str, body, current, newest)
|
|
75
|
-
gem_version =
|
|
76
|
-
return unless gem_version > current && gem_version <= newest
|
|
82
|
+
gem_version = safe_gem_version(version_str)
|
|
83
|
+
return unless gem_version && gem_version > current && gem_version <= newest
|
|
77
84
|
|
|
78
85
|
{ tag_name: version_str, name: version_str, published_at: nil, body: body.strip }
|
|
79
86
|
end
|
|
80
87
|
|
|
88
|
+
def safe_gem_version(version_str)
|
|
89
|
+
Gem::Version.new(version_str)
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
81
94
|
def split_sections(content)
|
|
82
95
|
sections = []
|
|
83
96
|
current_version = nil
|
|
@@ -26,13 +26,12 @@ module GemChangelogDiff
|
|
|
26
26
|
class_option :interactive, type: :boolean, default: false, aliases: "-i",
|
|
27
27
|
desc: "Interactively select gems to check"
|
|
28
28
|
class_option :dry_run, type: :boolean, default: false, desc: "Show which gems would be checked"
|
|
29
|
+
class_option :timeout, type: :numeric, desc: "Per-request timeout in seconds (default: 10)"
|
|
29
30
|
|
|
30
31
|
desc "check [GEM...]", "Show changelog diffs for outdated gems"
|
|
31
32
|
def check(*gem_names)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
gems = detect_gems
|
|
35
|
-
gems = filter_gems(gems, gem_names)
|
|
33
|
+
setup_environment
|
|
34
|
+
gems = filter_gems(detect_gems, gem_names)
|
|
36
35
|
return say("All gems are up to date!") if gems.empty?
|
|
37
36
|
|
|
38
37
|
gems = Interactive.new(gems: gems).select if options[:interactive]
|
|
@@ -44,8 +43,7 @@ module GemChangelogDiff
|
|
|
44
43
|
|
|
45
44
|
desc "show GEM FROM_VERSION TO_VERSION", "Show changelog between two versions of a gem"
|
|
46
45
|
def show(gem_name, from_version, to_version)
|
|
47
|
-
|
|
48
|
-
configure_token
|
|
46
|
+
setup_environment
|
|
49
47
|
gem = OutdatedGem.new(name: gem_name, current_version: from_version, newest_version: to_version)
|
|
50
48
|
report = build_single_report(gem)
|
|
51
49
|
formatter = Formatters.build(format: resolved_format, color: color_enabled?)
|
|
@@ -84,6 +82,12 @@ module GemChangelogDiff
|
|
|
84
82
|
|
|
85
83
|
private
|
|
86
84
|
|
|
85
|
+
def setup_environment
|
|
86
|
+
load_config
|
|
87
|
+
configure_token
|
|
88
|
+
configure_timeout
|
|
89
|
+
end
|
|
90
|
+
|
|
87
91
|
def load_config
|
|
88
92
|
config = ConfigLoader.new.load
|
|
89
93
|
GemChangelogDiff.configuration.apply(config)
|
|
@@ -96,6 +100,11 @@ module GemChangelogDiff
|
|
|
96
100
|
GemChangelogDiff.configuration.github_token = token if token
|
|
97
101
|
end
|
|
98
102
|
|
|
103
|
+
def configure_timeout
|
|
104
|
+
timeout = options[:timeout] || GemChangelogDiff.configuration.request_timeout
|
|
105
|
+
GemChangelogDiff.configuration.request_timeout = timeout
|
|
106
|
+
end
|
|
107
|
+
|
|
99
108
|
def rails_credentials_token
|
|
100
109
|
return unless defined?(Rails) && Rails.application.respond_to?(:credentials)
|
|
101
110
|
|
|
@@ -157,15 +166,26 @@ module GemChangelogDiff
|
|
|
157
166
|
|
|
158
167
|
def build_gem_report(gem, rubygems_client, source_resolver)
|
|
159
168
|
log "Checking #{gem.name}..."
|
|
169
|
+
fetch_gem_releases(gem, rubygems_client, source_resolver)
|
|
170
|
+
rescue GemChangelogDiff::Error => e
|
|
171
|
+
log_warning " Skipping #{gem.name}: #{e.message}"
|
|
172
|
+
gem_error(gem, e.message)
|
|
173
|
+
rescue JSON::ParserError => e
|
|
174
|
+
log_warning " Skipping #{gem.name}: malformed API response"
|
|
175
|
+
gem_error(gem, "Malformed API response: #{e.message}")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def fetch_gem_releases(gem, rubygems_client, source_resolver)
|
|
160
179
|
repo = rubygems_client.repo_url(gem.name)
|
|
161
|
-
return
|
|
180
|
+
return gem_error(gem, "Could not determine source repository.") unless repo
|
|
162
181
|
|
|
163
182
|
log " Found repo: #{repo}"
|
|
164
183
|
releases = source_resolver.resolve(repo, gem.current_version, gem.newest_version)
|
|
165
184
|
{ gem: gem, releases: releases }
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def gem_error(gem, message)
|
|
188
|
+
{ gem: gem, releases: [], error: " #{message}" }
|
|
169
189
|
end
|
|
170
190
|
|
|
171
191
|
def with_spinner
|
|
@@ -257,6 +277,12 @@ module GemChangelogDiff
|
|
|
257
277
|
|
|
258
278
|
# Disable colored output
|
|
259
279
|
# no_color: false
|
|
280
|
+
|
|
281
|
+
# Per-request timeout in seconds (default: 10)
|
|
282
|
+
# request_timeout: 10
|
|
283
|
+
|
|
284
|
+
# Total timeout in seconds (default: 120)
|
|
285
|
+
# total_timeout: 120
|
|
260
286
|
YAML
|
|
261
287
|
end
|
|
262
288
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
3
5
|
module GemChangelogDiff
|
|
4
6
|
class ConcurrentFetcher
|
|
5
7
|
def initialize(concurrency: 4)
|
|
@@ -9,6 +11,15 @@ module GemChangelogDiff
|
|
|
9
11
|
def fetch_all(items, &)
|
|
10
12
|
return items.map(&) if @concurrency <= 1
|
|
11
13
|
|
|
14
|
+
total_timeout = GemChangelogDiff.configuration.total_timeout
|
|
15
|
+
Timeout.timeout(total_timeout, NetworkError, "Total timeout of #{total_timeout}s exceeded") do
|
|
16
|
+
run_workers(items, &)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def run_workers(items, &)
|
|
12
23
|
results = Array.new(items.size)
|
|
13
24
|
queue = Queue.new
|
|
14
25
|
items.each_with_index { |item, i| queue << [item, i] }
|
|
@@ -18,8 +29,6 @@ module GemChangelogDiff
|
|
|
18
29
|
results
|
|
19
30
|
end
|
|
20
31
|
|
|
21
|
-
private
|
|
22
|
-
|
|
23
32
|
def spawn_workers(queue, results, &block)
|
|
24
33
|
worker_count = [@concurrency, queue.size].min
|
|
25
34
|
worker_count.times.map do
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module GemChangelogDiff
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :github_token, :cache_enabled, :cache_ttl,
|
|
6
|
-
:default_format, :concurrency, :ignore_gems, :no_color
|
|
6
|
+
:default_format, :concurrency, :ignore_gems, :no_color,
|
|
7
|
+
:request_timeout, :total_timeout
|
|
7
8
|
|
|
8
|
-
VALID_KEYS = %i[github_token cache_enabled cache_ttl default_format concurrency
|
|
9
|
+
VALID_KEYS = %i[github_token cache_enabled cache_ttl default_format concurrency
|
|
10
|
+
ignore_gems no_color request_timeout total_timeout].freeze
|
|
9
11
|
|
|
10
12
|
def initialize
|
|
11
13
|
@cache_enabled = true
|
|
@@ -14,6 +16,8 @@ module GemChangelogDiff
|
|
|
14
16
|
@concurrency = 4
|
|
15
17
|
@ignore_gems = []
|
|
16
18
|
@no_color = false
|
|
19
|
+
@request_timeout = 10
|
|
20
|
+
@total_timeout = 120
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def apply(hash)
|
|
@@ -6,39 +6,88 @@ require "json"
|
|
|
6
6
|
module GemChangelogDiff
|
|
7
7
|
class GithubClient
|
|
8
8
|
RELEASES_URL = "https://api.github.com/repos/%<repo>s/releases"
|
|
9
|
-
TAG_VERSION_REGEX = /\Av?(\d+\..+)\z/
|
|
10
9
|
RATE_LIMIT_WARNING_THRESHOLD = 10
|
|
10
|
+
MAX_PAGES = 10
|
|
11
11
|
|
|
12
12
|
def initialize(cache: nil)
|
|
13
13
|
@cache = cache
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def releases_between(repo, current_version, newest_version)
|
|
17
|
-
|
|
17
|
+
gem_name = repo.split("/").last
|
|
18
|
+
@active_matcher = TagMatcher.new(gem_name: gem_name)
|
|
19
|
+
releases = fetch_releases(repo, current_version)
|
|
18
20
|
filter_releases(releases, current_version, newest_version)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
|
-
def fetch_releases(repo)
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
def fetch_releases(repo, current_version = nil)
|
|
26
|
+
current = current_version ? safe_gem_version(current_version) : nil
|
|
27
|
+
paginate_releases(repo, current)
|
|
28
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
|
29
|
+
Errno::ETIMEDOUT, Errno::ECONNRESET,
|
|
30
|
+
Net::OpenTimeout, Net::ReadTimeout,
|
|
31
|
+
OpenSSL::SSL::SSLError => e
|
|
32
|
+
raise NetworkError, "GitHub API request failed: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def paginate_releases(repo, current)
|
|
36
|
+
all_releases = []
|
|
37
|
+
|
|
38
|
+
(1..MAX_PAGES).each do |page|
|
|
39
|
+
response, page_releases = fetch_release_page(repo, page)
|
|
40
|
+
break if page_releases.empty?
|
|
26
41
|
|
|
42
|
+
all_releases.concat(page_releases)
|
|
43
|
+
break if current && oldest_before_current?(page_releases, current)
|
|
44
|
+
break unless next_page?(response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
all_releases
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fetch_release_page(repo, page)
|
|
51
|
+
uri = build_releases_uri(repo, page)
|
|
27
52
|
response = execute_request(uri)
|
|
28
53
|
check_rate_limit(response)
|
|
29
|
-
handle_response(response)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
[response, handle_response(response)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_releases_uri(repo, page)
|
|
58
|
+
uri = URI(format(RELEASES_URL, repo: repo))
|
|
59
|
+
uri.query = URI.encode_www_form(per_page: 100, page: page)
|
|
60
|
+
uri
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def next_page?(response)
|
|
64
|
+
link = response["Link"]
|
|
65
|
+
return false unless link
|
|
66
|
+
|
|
67
|
+
link.include?('rel="next"')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def oldest_before_current?(releases, current)
|
|
71
|
+
releases.any? do |r|
|
|
72
|
+
version = @active_matcher.extract_version(r["tag_name"])
|
|
73
|
+
next false unless version
|
|
74
|
+
|
|
75
|
+
gem_version = safe_gem_version(version)
|
|
76
|
+
gem_version && gem_version < current
|
|
77
|
+
end
|
|
33
78
|
end
|
|
34
79
|
|
|
35
80
|
def execute_request(uri)
|
|
36
81
|
headers = request_headers
|
|
37
82
|
return @cache.get(uri, headers: headers) if @cache
|
|
38
83
|
|
|
84
|
+
timeout = GemChangelogDiff.configuration.request_timeout
|
|
39
85
|
request = Net::HTTP::Get.new(uri)
|
|
40
86
|
headers.each { |k, v| request[k] = v }
|
|
41
|
-
Net::HTTP.start(uri.hostname, uri.port,
|
|
87
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
88
|
+
use_ssl: true, open_timeout: timeout, read_timeout: timeout) do |http|
|
|
89
|
+
http.request(request)
|
|
90
|
+
end
|
|
42
91
|
end
|
|
43
92
|
|
|
44
93
|
def request_headers
|
|
@@ -52,7 +101,7 @@ module GemChangelogDiff
|
|
|
52
101
|
end
|
|
53
102
|
|
|
54
103
|
def handle_response(response)
|
|
55
|
-
return [] if response.code
|
|
104
|
+
return [] if %w[301 404].include?(response.code)
|
|
56
105
|
|
|
57
106
|
if response.code == "403" && response["X-RateLimit-Remaining"] == "0"
|
|
58
107
|
raise RateLimitError, "GitHub API rate limit exceeded. Use --token to authenticate."
|
|
@@ -71,31 +120,35 @@ module GemChangelogDiff
|
|
|
71
120
|
end
|
|
72
121
|
|
|
73
122
|
def filter_releases(releases, current_version, newest_version)
|
|
74
|
-
current =
|
|
75
|
-
newest =
|
|
123
|
+
current = safe_gem_version(current_version)
|
|
124
|
+
newest = safe_gem_version(newest_version)
|
|
125
|
+
return [] unless current && newest
|
|
76
126
|
|
|
77
127
|
matched = releases.filter_map { |r| build_release(r, current, newest) }
|
|
78
128
|
sort_releases(matched)
|
|
79
129
|
end
|
|
80
130
|
|
|
81
131
|
def build_release(release, current, newest)
|
|
82
|
-
|
|
83
|
-
return unless
|
|
132
|
+
version_str = @active_matcher.extract_version(release["tag_name"])
|
|
133
|
+
return unless version_str
|
|
84
134
|
|
|
85
|
-
gem_version =
|
|
86
|
-
return unless gem_version > current && gem_version <= newest
|
|
135
|
+
gem_version = safe_gem_version(version_str)
|
|
136
|
+
return unless gem_version && gem_version > current && gem_version <= newest
|
|
87
137
|
|
|
88
138
|
{ tag_name: release["tag_name"], name: release["name"],
|
|
89
139
|
published_at: release["published_at"], body: release["body"] }
|
|
90
140
|
end
|
|
91
141
|
|
|
92
142
|
def sort_releases(releases)
|
|
93
|
-
releases.sort_by
|
|
143
|
+
releases.sort_by do |r|
|
|
144
|
+
safe_gem_version(@active_matcher.extract_version(r[:tag_name])) || Gem::Version.new("0")
|
|
145
|
+
end.reverse
|
|
94
146
|
end
|
|
95
147
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
148
|
+
def safe_gem_version(version_str)
|
|
149
|
+
Gem::Version.new(version_str)
|
|
150
|
+
rescue ArgumentError
|
|
151
|
+
nil
|
|
99
152
|
end
|
|
100
153
|
end
|
|
101
154
|
end
|
|
@@ -10,7 +10,8 @@ module GemChangelogDiff
|
|
|
10
10
|
|
|
11
11
|
def select
|
|
12
12
|
prompt = TTY::Prompt.new
|
|
13
|
-
prompt.multi_select("Select gems to check:",
|
|
13
|
+
prompt.multi_select("Select gems to check:",
|
|
14
|
+
per_page: 15, help: "(Space to select, Enter to confirm)") do |menu|
|
|
14
15
|
@gems.each do |gem|
|
|
15
16
|
menu.choice "#{gem.name} (#{gem.current_version} → #{gem.newest_version})", gem
|
|
16
17
|
end
|
|
@@ -6,17 +6,17 @@ require "json"
|
|
|
6
6
|
module GemChangelogDiff
|
|
7
7
|
class RubygemsClient
|
|
8
8
|
RUBYGEMS_API = "https://rubygems.org/api/v1/gems/%<name>s.json"
|
|
9
|
-
GITHUB_REPO_REGEX = %r{github\.com/([^/]+)/([^/]+)}
|
|
10
9
|
|
|
11
|
-
def initialize(cache: nil)
|
|
10
|
+
def initialize(cache: nil, uri_resolver: UriResolver.new)
|
|
12
11
|
@cache = cache
|
|
12
|
+
@uri_resolver = uri_resolver
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def repo_url(gem_name)
|
|
16
16
|
data = fetch_gem_data(gem_name)
|
|
17
17
|
return nil unless data
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
@uri_resolver.resolve(data)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def latest_version(gem_name)
|
|
@@ -28,29 +28,23 @@ module GemChangelogDiff
|
|
|
28
28
|
|
|
29
29
|
def fetch_gem_data(gem_name)
|
|
30
30
|
uri = URI(format(RUBYGEMS_API, name: gem_name))
|
|
31
|
-
response = @cache ? @cache.get(uri) :
|
|
31
|
+
response = @cache ? @cache.get(uri) : fetch_from_api(uri)
|
|
32
32
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
33
33
|
|
|
34
34
|
JSON.parse(response.body)
|
|
35
35
|
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
|
36
|
-
|
|
36
|
+
Errno::ETIMEDOUT, Errno::ECONNRESET,
|
|
37
|
+
Net::OpenTimeout, Net::ReadTimeout,
|
|
38
|
+
OpenSSL::SSL::SSLError, JSON::ParserError => e
|
|
37
39
|
raise NetworkError, "RubyGems API request failed: #{e.message}"
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
match = url.match(GITHUB_REPO_REGEX)
|
|
46
|
-
next unless match
|
|
47
|
-
|
|
48
|
-
owner = match[1]
|
|
49
|
-
repo = match[2].sub(/\.git\z/, "").sub(%r{/.*}, "")
|
|
50
|
-
return "#{owner}/#{repo}"
|
|
42
|
+
def fetch_from_api(uri)
|
|
43
|
+
timeout = GemChangelogDiff.configuration.request_timeout
|
|
44
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
45
|
+
use_ssl: true, open_timeout: timeout, read_timeout: timeout) do |http|
|
|
46
|
+
http.request(Net::HTTP::Get.new(uri))
|
|
51
47
|
end
|
|
52
|
-
|
|
53
|
-
nil
|
|
54
48
|
end
|
|
55
49
|
end
|
|
56
50
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemChangelogDiff
|
|
4
|
+
class TagMatcher
|
|
5
|
+
STANDARD_PATTERN = /\A(?:release-)?v?(\d+\..+)\z/
|
|
6
|
+
|
|
7
|
+
def initialize(gem_name: nil)
|
|
8
|
+
@gem_name = gem_name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def extract_version(tag)
|
|
12
|
+
return nil if tag.nil? || tag.strip.empty?
|
|
13
|
+
|
|
14
|
+
version = try_gem_prefixed(tag) || try_standard_pattern(tag)
|
|
15
|
+
return nil unless version
|
|
16
|
+
|
|
17
|
+
validate_version(version)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def try_gem_prefixed(tag)
|
|
23
|
+
return nil unless @gem_name
|
|
24
|
+
|
|
25
|
+
prefix = "#{@gem_name}-"
|
|
26
|
+
return nil unless tag.start_with?(prefix)
|
|
27
|
+
|
|
28
|
+
raw = tag.delete_prefix(prefix)
|
|
29
|
+
raw.start_with?("v") ? raw[1..] : raw
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def try_standard_pattern(tag)
|
|
33
|
+
match = tag.match(STANDARD_PATTERN)
|
|
34
|
+
match ? match[1] : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_version(version_str)
|
|
38
|
+
Gem::Version.new(version_str)
|
|
39
|
+
version_str
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module GemChangelogDiff
|
|
7
|
+
class UriResolver
|
|
8
|
+
GITHUB_REGEX = %r{github\.com/([^/]+)/([^/]+)}
|
|
9
|
+
NON_GITHUB_HOSTS = {
|
|
10
|
+
"gitlab.com" => "GitLab",
|
|
11
|
+
"codeberg.org" => "Codeberg",
|
|
12
|
+
"bitbucket.org" => "Bitbucket",
|
|
13
|
+
"sr.ht" => "SourceHut"
|
|
14
|
+
}.freeze
|
|
15
|
+
URI_FIELDS = %w[source_code_uri homepage_uri bug_tracker_uri].freeze
|
|
16
|
+
MAX_REDIRECTS = 3
|
|
17
|
+
|
|
18
|
+
def resolve(gem_data)
|
|
19
|
+
uris = extract_uris(gem_data)
|
|
20
|
+
return nil if uris.empty?
|
|
21
|
+
|
|
22
|
+
non_github = detect_non_github(uris)
|
|
23
|
+
raise RepoNotFoundError, "hosted on #{non_github} (not supported)" if non_github
|
|
24
|
+
|
|
25
|
+
slug = extract_github_slug(uris)
|
|
26
|
+
return nil unless slug
|
|
27
|
+
|
|
28
|
+
follow_redirects(slug)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def extract_uris(data)
|
|
34
|
+
URI_FIELDS.filter_map do |field|
|
|
35
|
+
value = data[field]
|
|
36
|
+
value unless value.nil? || value.to_s.strip.empty?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def detect_non_github(uris)
|
|
41
|
+
uris.each do |url|
|
|
42
|
+
host = URI.parse(url).host&.downcase
|
|
43
|
+
NON_GITHUB_HOSTS.each do |domain, name|
|
|
44
|
+
return name if host&.end_with?(domain)
|
|
45
|
+
end
|
|
46
|
+
rescue URI::InvalidURIError
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_github_slug(uris)
|
|
53
|
+
uris.each do |url|
|
|
54
|
+
match = url.match(GITHUB_REGEX)
|
|
55
|
+
next unless match
|
|
56
|
+
|
|
57
|
+
owner = match[1]
|
|
58
|
+
repo = match[2].sub(/\.git\z/, "").sub(%r{/.*}, "")
|
|
59
|
+
return "#{owner}/#{repo}"
|
|
60
|
+
end
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def follow_redirects(slug, remaining = MAX_REDIRECTS)
|
|
65
|
+
return slug if remaining <= 0
|
|
66
|
+
|
|
67
|
+
response = execute_head_request(URI("https://api.github.com/repos/#{slug}"))
|
|
68
|
+
return slug unless response.is_a?(Net::HTTPRedirection)
|
|
69
|
+
|
|
70
|
+
new_slug = extract_slug_from_location(response["Location"])
|
|
71
|
+
new_slug ? follow_redirects(new_slug, remaining - 1) : slug
|
|
72
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
|
73
|
+
Errno::ETIMEDOUT, Errno::ECONNRESET,
|
|
74
|
+
Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
|
|
75
|
+
slug
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def extract_slug_from_location(location)
|
|
79
|
+
location&.match(%r{repos/([^/]+/[^/]+)})&.then { |m| m[1] }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def execute_head_request(uri)
|
|
83
|
+
request = Net::HTTP::Head.new(uri)
|
|
84
|
+
request_headers.each { |k, v| request[k] = v }
|
|
85
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
86
|
+
use_ssl: true, open_timeout: 5, read_timeout: 5) do |http|
|
|
87
|
+
http.request(request)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def request_headers
|
|
92
|
+
headers = {
|
|
93
|
+
"Accept" => "application/vnd.github.v3+json",
|
|
94
|
+
"User-Agent" => "gem_changelog_diff/#{VERSION}"
|
|
95
|
+
}
|
|
96
|
+
token = GemChangelogDiff.configuration.github_token
|
|
97
|
+
headers["Authorization"] = "token #{token}" if token
|
|
98
|
+
headers
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/gem_changelog_diff.rb
CHANGED
|
@@ -12,6 +12,8 @@ require_relative "gem_changelog_diff/errors"
|
|
|
12
12
|
require_relative "gem_changelog_diff/cache"
|
|
13
13
|
require_relative "gem_changelog_diff/outdated_gem"
|
|
14
14
|
require_relative "gem_changelog_diff/detector"
|
|
15
|
+
require_relative "gem_changelog_diff/uri_resolver"
|
|
16
|
+
require_relative "gem_changelog_diff/tag_matcher"
|
|
15
17
|
require_relative "gem_changelog_diff/rubygems_client"
|
|
16
18
|
require_relative "gem_changelog_diff/lockfile_parser"
|
|
17
19
|
require_relative "gem_changelog_diff/github_client"
|
data/sig/gem_changelog_diff.rbs
CHANGED
|
@@ -30,6 +30,8 @@ module GemChangelogDiff
|
|
|
30
30
|
attr_accessor concurrency: Integer
|
|
31
31
|
attr_accessor ignore_gems: Array[String]
|
|
32
32
|
attr_accessor no_color: bool
|
|
33
|
+
attr_accessor request_timeout: Integer
|
|
34
|
+
attr_accessor total_timeout: Integer
|
|
33
35
|
|
|
34
36
|
VALID_KEYS: Array[Symbol]
|
|
35
37
|
|
|
@@ -102,18 +104,48 @@ module GemChangelogDiff
|
|
|
102
104
|
def parse: (String output) -> Array[OutdatedGem]
|
|
103
105
|
end
|
|
104
106
|
|
|
107
|
+
class UriResolver
|
|
108
|
+
GITHUB_REGEX: Regexp
|
|
109
|
+
NON_GITHUB_HOSTS: Hash[String, String]
|
|
110
|
+
URI_FIELDS: Array[String]
|
|
111
|
+
MAX_REDIRECTS: Integer
|
|
112
|
+
|
|
113
|
+
def resolve: (Hash[String, untyped] gem_data) -> String?
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def extract_uris: (Hash[String, untyped] data) -> Array[String]
|
|
118
|
+
def detect_non_github: (Array[String] uris) -> String?
|
|
119
|
+
def extract_github_slug: (Array[String] uris) -> String?
|
|
120
|
+
def follow_redirects: (String slug, ?Integer remaining) -> String
|
|
121
|
+
def execute_head_request: (URI::Generic uri) -> (Net::HTTPResponse)
|
|
122
|
+
def request_headers: () -> Hash[String, String]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class TagMatcher
|
|
126
|
+
STANDARD_PATTERN: Regexp
|
|
127
|
+
|
|
128
|
+
def initialize: (?gem_name: String?) -> void
|
|
129
|
+
def extract_version: (String? tag) -> String?
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def try_gem_prefixed: (String tag) -> String?
|
|
134
|
+
def try_standard_pattern: (String tag) -> String?
|
|
135
|
+
def validate_version: (String version_str) -> String?
|
|
136
|
+
end
|
|
137
|
+
|
|
105
138
|
class RubygemsClient
|
|
106
139
|
RUBYGEMS_API: String
|
|
107
|
-
GITHUB_REPO_REGEX: Regexp
|
|
108
140
|
|
|
109
|
-
def initialize: (?cache: Cache?) -> void
|
|
141
|
+
def initialize: (?cache: Cache?, ?uri_resolver: UriResolver) -> void
|
|
110
142
|
def repo_url: (String gem_name) -> String?
|
|
111
143
|
def latest_version: (String gem_name) -> String?
|
|
112
144
|
|
|
113
145
|
private
|
|
114
146
|
|
|
115
147
|
def fetch_gem_data: (String gem_name) -> Hash[String, untyped]?
|
|
116
|
-
def
|
|
148
|
+
def fetch_from_api: (URI::Generic uri) -> Net::HTTPResponse
|
|
117
149
|
end
|
|
118
150
|
|
|
119
151
|
class LockfileParser
|
|
@@ -153,6 +185,7 @@ module GemChangelogDiff
|
|
|
153
185
|
def process_line: (String line, Array[[String, String]] sections, String? current_version, Array[String] current_body) -> [String?, Array[String]]
|
|
154
186
|
def flush_section: (Array[[String, String]] sections, String? version, Array[String] body) -> void
|
|
155
187
|
def clean_version: (String raw) -> String
|
|
188
|
+
def safe_gem_version: (String version_str) -> Gem::Version?
|
|
156
189
|
end
|
|
157
190
|
|
|
158
191
|
class SourceResolver
|
|
@@ -162,15 +195,18 @@ module GemChangelogDiff
|
|
|
162
195
|
|
|
163
196
|
class GithubClient
|
|
164
197
|
RELEASES_URL: String
|
|
165
|
-
TAG_VERSION_REGEX: Regexp
|
|
166
198
|
RATE_LIMIT_WARNING_THRESHOLD: Integer
|
|
199
|
+
MAX_PAGES: Integer
|
|
167
200
|
|
|
168
201
|
def initialize: (?cache: Cache?) -> void
|
|
169
202
|
def releases_between: (String repo, String current_version, String newest_version) -> Array[release_hash]
|
|
170
203
|
|
|
171
204
|
private
|
|
172
205
|
|
|
173
|
-
def fetch_releases: (String repo) -> Array[Hash[String, untyped]]
|
|
206
|
+
def fetch_releases: (String repo, ?String? current_version) -> Array[Hash[String, untyped]]
|
|
207
|
+
def build_releases_uri: (String repo, Integer page) -> URI::Generic
|
|
208
|
+
def next_page?: ((Net::HTTPResponse | CachedResponse) response) -> bool
|
|
209
|
+
def oldest_before_current?: (Array[Hash[String, untyped]] releases, Gem::Version current) -> bool
|
|
174
210
|
def execute_request: (URI::Generic uri) -> (Net::HTTPResponse | CachedResponse)
|
|
175
211
|
def request_headers: () -> Hash[String, String]
|
|
176
212
|
def handle_response: (Net::HTTPResponse | CachedResponse response) -> Array[Hash[String, untyped]]
|
|
@@ -178,7 +214,7 @@ module GemChangelogDiff
|
|
|
178
214
|
def filter_releases: (Array[Hash[String, untyped]] releases, String current_version, String newest_version) -> Array[release_hash]
|
|
179
215
|
def build_release: (Hash[String, untyped] release, Gem::Version current, Gem::Version newest) -> release_hash?
|
|
180
216
|
def sort_releases: (Array[release_hash] releases) -> Array[release_hash]
|
|
181
|
-
def
|
|
217
|
+
def safe_gem_version: (String version_str) -> Gem::Version?
|
|
182
218
|
end
|
|
183
219
|
|
|
184
220
|
class ConcurrentFetcher
|
|
@@ -187,6 +223,7 @@ module GemChangelogDiff
|
|
|
187
223
|
|
|
188
224
|
private
|
|
189
225
|
|
|
226
|
+
def run_workers: [T, U] (Array[T], &) -> Array[U]
|
|
190
227
|
def spawn_workers: (Queue[untyped], Array[untyped], &) -> Array[Thread]
|
|
191
228
|
def process_queue: (Queue[untyped], Array[untyped], &) -> void
|
|
192
229
|
end
|
|
@@ -270,6 +307,7 @@ module GemChangelogDiff
|
|
|
270
307
|
def load_config: () -> void
|
|
271
308
|
def dry_run_output: (Array[OutdatedGem] gems) -> void
|
|
272
309
|
def format_dry_run: (Array[OutdatedGem] gems) -> String
|
|
310
|
+
def configure_timeout: () -> void
|
|
273
311
|
def rails_credentials_token: () -> String?
|
|
274
312
|
def output_results: (Array[OutdatedGem] gems) -> void
|
|
275
313
|
def resolved_format: () -> String
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gem_changelog_diff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -104,6 +104,8 @@ files:
|
|
|
104
104
|
- lib/gem_changelog_diff/outdated_gem.rb
|
|
105
105
|
- lib/gem_changelog_diff/rubygems_client.rb
|
|
106
106
|
- lib/gem_changelog_diff/source_resolver.rb
|
|
107
|
+
- lib/gem_changelog_diff/tag_matcher.rb
|
|
108
|
+
- lib/gem_changelog_diff/uri_resolver.rb
|
|
107
109
|
- lib/gem_changelog_diff/version.rb
|
|
108
110
|
- sig/gem_changelog_diff.rbs
|
|
109
111
|
homepage: https://github.com/eclectic-coding/gem_changelog_diff
|