gemstar 0.0.1
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/CHANGELOG.md +25 -0
- data/LICENSE.txt +23 -0
- data/README.md +58 -0
- data/bin/gemstar +7 -0
- data/lib/gemstar/cache.rb +60 -0
- data/lib/gemstar/change_log.rb +335 -0
- data/lib/gemstar/cli.rb +50 -0
- data/lib/gemstar/commands/command.rb +13 -0
- data/lib/gemstar/commands/diff.rb +147 -0
- data/lib/gemstar/git_hub.rb +26 -0
- data/lib/gemstar/git_repo.rb +55 -0
- data/lib/gemstar/lock_file.rb +30 -0
- data/lib/gemstar/outputs/basic.rb +7 -0
- data/lib/gemstar/outputs/html.rb +75 -0
- data/lib/gemstar/railtie.rb +6 -0
- data/lib/gemstar/remote_repository.rb +28 -0
- data/lib/gemstar/ruby_gems_metadata.rb +59 -0
- data/lib/gemstar/utils.rb +28 -0
- data/lib/gemstar/version.rb +10 -0
- data/lib/gemstar.rb +17 -0
- metadata +205 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 80dede87ff46bc8aefd962ccd46dffcb199357eca04173b9610f85e74c74111a
|
|
4
|
+
data.tar.gz: dfd221d5ec0cdd3c3511fdd1e7980fb8bc00050afab9d69cdb81c43d1673fee6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4d160d053c3e60ef38d7b129876159a2d696015857d917f1dceb82afcd3e648d8e5736232535a9d79ef84e11681591019245317f5f15c3d7b24d06292e6c15f7
|
|
7
|
+
data.tar.gz: bcadfa71fac1e0be9c6777217cba5797e729ead523383a7f049984e47f9b9c24952cb9ae2d7a4eb7f03615cc8f4aeaafa13961cbedcfde9dd94684d601bcb0f2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
## TODO
|
|
4
|
+
- Diff:
|
|
5
|
+
- Gems:
|
|
6
|
+
- benchmark
|
|
7
|
+
- brakeman
|
|
8
|
+
- json (not using CHANGES.md?)
|
|
9
|
+
- nio4r not using releases.md?
|
|
10
|
+
- parser not using CHANGELOG.md?
|
|
11
|
+
- actioncable-next uses release tag names?
|
|
12
|
+
- paper_trail not using CHANGELOG.md?
|
|
13
|
+
- playwright-ruby-client uses release tags?
|
|
14
|
+
- support ```ruby ```
|
|
15
|
+
|
|
16
|
+
## 0.1
|
|
17
|
+
|
|
18
|
+
- Initial release
|
|
19
|
+
- Add GEMSTAR_DEBUG_GEM_REGEX to debug specific gems.
|
|
20
|
+
- Refactor to work correctly with more gems.
|
|
21
|
+
- Diff: More flexible changelog parsing.
|
|
22
|
+
- Diff: Fetch raw GitHub changelogs, not html.
|
|
23
|
+
- Diff: Support GitHub releases.
|
|
24
|
+
- Diff: Improved Markup rendering (with code samples)
|
|
25
|
+
- Diff: Try release notes in order of match frequency.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Copyright (c) 2025 Florian Dejako
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[](https://rubygems.org/gems/gemstar)
|
|
2
|
+
[](https://github.com/palkan/gemstar/actions)
|
|
3
|
+
[](https://github.com/FDj/gemstar/actions)
|
|
4
|
+
|
|
5
|
+
# Gemstar
|
|
6
|
+
A very preliminary gem to help you keep track of your gems.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Until it's released on RubyGems, you can install it from GitHub:
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
# Shell
|
|
14
|
+
gem install specific_install
|
|
15
|
+
gem specific_install -l https://github.com/FDj/gemstar.git
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or adding to your project:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Gemfile
|
|
22
|
+
group :development do
|
|
23
|
+
gem "gemstar", github: "FDj/gemstar"
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### `gemstar diff`
|
|
30
|
+
|
|
31
|
+
Run this after you've updated your gems.
|
|
32
|
+
|
|
33
|
+
```shell
|
|
34
|
+
# in your project directory:
|
|
35
|
+
bundle exec gemstar diff
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This will generate an html diff report with changelog entries for each gem that was updated:
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
You can also specify from and to hashes or tags to generate a diff report for a specific range of commits:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
bundle exec gemstar diff --from 8e3aa96b7027834cdbabc0d8cbd5f9455165e930 --to HEAD
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Contributing
|
|
49
|
+
|
|
50
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/FDj/gemstar](https://github.com/FDj/gemstar).
|
|
51
|
+
|
|
52
|
+
## Credits
|
|
53
|
+
|
|
54
|
+
This gem is generated via [`newgem` template](https://github.com/palkan/newgem) by [@palkan](https://github.com/palkan).
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/bin/gemstar
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "digest"
|
|
3
|
+
|
|
4
|
+
module Gemstar
|
|
5
|
+
class Cache
|
|
6
|
+
MAX_CACHE_AGE = 60 * 60 * 24 * 7 # 1 week
|
|
7
|
+
CACHE_DIR = ".gem_changelog_cache"
|
|
8
|
+
|
|
9
|
+
@@initialized = false
|
|
10
|
+
|
|
11
|
+
def self.init
|
|
12
|
+
return if @@initialized
|
|
13
|
+
|
|
14
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
15
|
+
@@initialized = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.fetch(key, &block)
|
|
19
|
+
init
|
|
20
|
+
|
|
21
|
+
path = File.join(CACHE_DIR, Digest::SHA256.hexdigest(key))
|
|
22
|
+
|
|
23
|
+
if File.exist?(path)
|
|
24
|
+
age = Time.now - File.mtime(path)
|
|
25
|
+
if age <= MAX_CACHE_AGE
|
|
26
|
+
content = File.read(path)
|
|
27
|
+
return nil if content == "__404__"
|
|
28
|
+
return content
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
data = block.call
|
|
34
|
+
File.write(path, data || "__404__")
|
|
35
|
+
data
|
|
36
|
+
rescue
|
|
37
|
+
File.write(path, "__404__")
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def edit_gitignore
|
|
45
|
+
gitignore_path = ".gitignore"
|
|
46
|
+
ignore_entries = %w[.gem_changelog_cache/ gem_update_changelog.html]
|
|
47
|
+
|
|
48
|
+
existing_lines = File.exist?(gitignore_path) ? File.read(gitignore_path).lines.map(&:chomp) : []
|
|
49
|
+
|
|
50
|
+
new_lines = ignore_entries.reject { |entry| existing_lines.include?(entry) }
|
|
51
|
+
|
|
52
|
+
unless new_lines.empty?
|
|
53
|
+
File.open(gitignore_path, "a") do |f|
|
|
54
|
+
f.puts "\n# Cache/output from gem changelog tool" if (existing_lines & ignore_entries).empty?
|
|
55
|
+
new_lines.each { |entry| f.puts entry }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
class ChangeLog
|
|
5
|
+
@@candidates_found = Hash.new(0)
|
|
6
|
+
|
|
7
|
+
def initialize(metadata)
|
|
8
|
+
@metadata = metadata
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :metadata
|
|
12
|
+
|
|
13
|
+
def content
|
|
14
|
+
@content ||= fetch_changelog_content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sections
|
|
18
|
+
@sections ||= begin
|
|
19
|
+
s = parse_changelog_sections
|
|
20
|
+
if s.nil? || s.empty?
|
|
21
|
+
s = parse_github_release_sections
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
pp @@candidates_found if Gemstar.debug?
|
|
25
|
+
|
|
26
|
+
s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def extract_relevant_sections(old_version, new_version)
|
|
31
|
+
from = Gem::Version.new(old_version.gsub(/-[\w\-]+$/, "")) rescue nil if old_version
|
|
32
|
+
from ||= Gem::Version.new("0.0.0")
|
|
33
|
+
to = Gem::Version.new(new_version.gsub(/-[\w\-]+$/, "")) rescue nil if new_version
|
|
34
|
+
to ||= Gem::Version.new("9999.9999.9999")
|
|
35
|
+
|
|
36
|
+
sections.select do |version, _|
|
|
37
|
+
v = Gem::Version.new(version.gsub(/-[\w\-]+$/, ""))
|
|
38
|
+
v > from && v <= to
|
|
39
|
+
rescue => e
|
|
40
|
+
false
|
|
41
|
+
end.sort_by do |k, _|
|
|
42
|
+
Gem::Version.new(k.gsub(/-[\w\-]+$/, ""))
|
|
43
|
+
rescue => e
|
|
44
|
+
Gem::Version.new("0.0.0")
|
|
45
|
+
end.reverse.to_h
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Extract a version token from a heading line, preferring explicit version forms
|
|
51
|
+
# and avoiding returning a date string when both are present.
|
|
52
|
+
def extract_version_from_heading(line)
|
|
53
|
+
return nil unless line
|
|
54
|
+
heading = line.to_s
|
|
55
|
+
# 1) Prefer version inside parentheses after a date: "### 2025-11-07 (2.16.0)"
|
|
56
|
+
return $1 if heading[/\(\s*v?(\d[\w.\-]+)\s*\)/]
|
|
57
|
+
# 2) Version-first with optional leading markers/labels: "## v1.2.6 - 2025-10-21"
|
|
58
|
+
return $1 if heading[/^\s*(?:#+|=+)?\s*(?:Version\s+)?\[?v?(\d[\w.\-]+)\]?/i]
|
|
59
|
+
# 3) Anywhere: first semver-like token with a dot
|
|
60
|
+
return $1 if heading[/\bv?(\d+\.\d+(?:\.\d+)?(?:[A-Za-z0-9.\-]+)?)\b/]
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def changelog_uri_candidates
|
|
65
|
+
candidates = []
|
|
66
|
+
|
|
67
|
+
if @metadata.repo_uri =~ %r{https://github\.com/aws/aws-sdk-ruby}
|
|
68
|
+
base = "https://raw.githubusercontent.com/aws/aws-sdk-ruby/refs/heads/version-3/gems/#{@metadata.gem_name}"
|
|
69
|
+
aws_style = true
|
|
70
|
+
else
|
|
71
|
+
base = @metadata.repo_uri.sub("https://github.com", "https://raw.githubusercontent.com")
|
|
72
|
+
aws_style = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
base = base.chomp("/")
|
|
76
|
+
|
|
77
|
+
paths = aws_style ? ["CHANGELOG.md"] : %w[
|
|
78
|
+
CHANGELOG.md releases.md CHANGES.md
|
|
79
|
+
Changelog.md changelog.md ChangeLog.md
|
|
80
|
+
Changes.md changes.md
|
|
81
|
+
HISTORY.md History.md history.md
|
|
82
|
+
History CHANGELOG.rdoc
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
remote_repository = RemoteRepository.new(base)
|
|
86
|
+
|
|
87
|
+
branches = aws_style ? [""] : remote_repository.find_main_branch
|
|
88
|
+
|
|
89
|
+
candidates += paths.product(branches).map do |file, branch|
|
|
90
|
+
uri = aws_style ? "#{base}/#{file}" : "#{base}/#{branch}/#{file}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Add the gem's changelog_uri last as it's usually not the most parsable:
|
|
94
|
+
candidates += [Gemstar::GitHub::github_blob_to_raw(@metadata.meta["changelog_uri"])]
|
|
95
|
+
|
|
96
|
+
candidates.flatten!
|
|
97
|
+
candidates.uniq!
|
|
98
|
+
candidates.compact!
|
|
99
|
+
|
|
100
|
+
candidates
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fetch_changelog_content
|
|
104
|
+
content = nil
|
|
105
|
+
|
|
106
|
+
changelog_uri_candidates.find do |candidate|
|
|
107
|
+
content = Cache.fetch("changelog-#{candidate}") do
|
|
108
|
+
URI.open(candidate, read_timeout: 8)&.read
|
|
109
|
+
rescue => e
|
|
110
|
+
puts "#{candidate}: #{e}" if Gemstar.debug?
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# puts "fetch_changelog_content #{candidate}:\n#{content}" if Gemstar.debug?
|
|
115
|
+
|
|
116
|
+
if content
|
|
117
|
+
@@candidates_found[candidate.split("/").last] += 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
!content.nil?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
content
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_changelog_sections
|
|
128
|
+
# If the fetched content looks like a GitHub Releases HTML page, return {}
|
|
129
|
+
# so that the GitHub releases scraper can handle it. This avoids
|
|
130
|
+
# accidentally parsing HTML from /releases pages as a markdown changelog.
|
|
131
|
+
c = content
|
|
132
|
+
return {} if c.nil? || c.strip.empty?
|
|
133
|
+
if (c.include?("<html") || c.include?("<!DOCTYPE html")) &&
|
|
134
|
+
(c.include?('data-test-selector="body-content"') || c.include?("/releases/tag/"))
|
|
135
|
+
puts "parse_changelog_sections #{@metadata.gem_name}: Detected GitHub Releases HTML; skipping to fallback" if Gemstar.debug?
|
|
136
|
+
return {}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sections = {}
|
|
140
|
+
current_key = nil
|
|
141
|
+
current_lines = []
|
|
142
|
+
|
|
143
|
+
flush_current = lambda do
|
|
144
|
+
return unless current_key && !current_lines.empty?
|
|
145
|
+
key = current_key
|
|
146
|
+
# If key looks like a date or non-version, try to extract a proper version
|
|
147
|
+
if key =~ /\A\d{4}-\d{2}-\d{2}\z/ || key !~ /\A\d[\w.\-]*\z/
|
|
148
|
+
v = extract_version_from_heading(current_lines.first)
|
|
149
|
+
key = v if v
|
|
150
|
+
end
|
|
151
|
+
if sections.key?(key)
|
|
152
|
+
# Collision: merge by appending with a separator to avoid losing data
|
|
153
|
+
sections[key] += ["\n"] + current_lines
|
|
154
|
+
else
|
|
155
|
+
sections[key] = current_lines.dup
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
c.each_line do |line|
|
|
160
|
+
# Convert rdoc to markdown:
|
|
161
|
+
line = line.gsub(/^=+/) { |m| "#" * m.length }
|
|
162
|
+
|
|
163
|
+
new_key = nil
|
|
164
|
+
# Keep-a-Changelog style: version first with trailing date, e.g. "## v1.2.6 - 2025-10-21"
|
|
165
|
+
if line =~ /^\s*(?:#+|=+)\s*\[?v?(\d[\w.\-]+)\]?\s*(?:—|–|-)\s*\d{4}-\d{2}-\d{2}\b/
|
|
166
|
+
new_key = extract_version_from_heading(line) || $1
|
|
167
|
+
elsif line =~ /^\s*(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[?v?(\d[\w.\-]+)\]?(?:\s*[-(].*)?/i
|
|
168
|
+
new_key = extract_version_from_heading(line) || $1
|
|
169
|
+
elsif line =~ /^\s*(?:#+|=+)\s*(?:Version\s+)?\[?v?(\d[\w.\-]+)\]?(?:\s*[-(].*)?/i
|
|
170
|
+
# header without label words before the version
|
|
171
|
+
new_key = extract_version_from_heading(line) || $1
|
|
172
|
+
elsif line =~ /^\s*(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d[\w.\-]+)\s*\)/
|
|
173
|
+
# headings like "### 2025-11-07 (2.16.0)" — prefer the version in parentheses over the leading date
|
|
174
|
+
new_key = extract_version_from_heading(line) || $1
|
|
175
|
+
elsif line =~ /^\s*(?:Version\s+)?v?(\d[\w.\-]+)(?:\s*[-(].*)?/i
|
|
176
|
+
# fallback for lines like "1.4.0 (2025-06-02)"
|
|
177
|
+
new_key = extract_version_from_heading(line) || $1
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if new_key
|
|
181
|
+
# Flush previous section before starting a new one
|
|
182
|
+
flush_current.call
|
|
183
|
+
current_key = new_key
|
|
184
|
+
current_lines = [line]
|
|
185
|
+
elsif current_key
|
|
186
|
+
current_lines << line
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Flush the last captured section
|
|
191
|
+
flush_current.call
|
|
192
|
+
|
|
193
|
+
# Normalize keys: ensure all keys are versions; fix any leftover date-like keys conservatively
|
|
194
|
+
begin
|
|
195
|
+
normalized = {}
|
|
196
|
+
sections.each do |k, lines|
|
|
197
|
+
if k =~ /\A\d{4}-\d{2}-\d{2}\z/ || k !~ /\A\d[\w.\-]*\z/
|
|
198
|
+
heading = lines.first.to_s
|
|
199
|
+
# 1) Prefer version inside parentheses, e.g., "### 2025-11-07 (2.16.0)"
|
|
200
|
+
if heading[/\(\s*v?(\d[\w.\-]+)\s*\)/]
|
|
201
|
+
key = $1
|
|
202
|
+
normalized[key] = if normalized.key?(key)
|
|
203
|
+
normalized[key] + ["\n"] + lines
|
|
204
|
+
else
|
|
205
|
+
lines
|
|
206
|
+
end
|
|
207
|
+
next
|
|
208
|
+
end
|
|
209
|
+
# 2) Headings like "## v1.2.5 - 2025-10-21" or "## 1.2.5 — 2025-10-21"
|
|
210
|
+
if heading[/^\s*(?:#+|=+)\s*(?:Version\s+)?\[?v?(\d+\.\d+(?:\.\d+)?(?:[A-Za-z0-9.\-]+)?)\]?/]
|
|
211
|
+
key = $1
|
|
212
|
+
normalized[key] = if normalized.key?(key)
|
|
213
|
+
normalized[key] + ["\n"] + lines
|
|
214
|
+
else
|
|
215
|
+
lines
|
|
216
|
+
end
|
|
217
|
+
next
|
|
218
|
+
end
|
|
219
|
+
# 3) Anywhere in the heading, pick the first semver-like token with a dot
|
|
220
|
+
if heading[/\bv?(\d+\.\d+(?:\.\d+)?(?:[A-Za-z0-9.\-]+)?)\b/]
|
|
221
|
+
key = $1
|
|
222
|
+
normalized[key] = if normalized.key?(key)
|
|
223
|
+
normalized[key] + ["\n"] + lines
|
|
224
|
+
else
|
|
225
|
+
lines
|
|
226
|
+
end
|
|
227
|
+
next
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
# Default: carry over, merging on collision to avoid loss
|
|
231
|
+
if normalized.key?(k)
|
|
232
|
+
normalized[k] += ["\n"] + lines
|
|
233
|
+
else
|
|
234
|
+
normalized[k] = lines
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
sections = normalized unless normalized.empty?
|
|
238
|
+
rescue => e
|
|
239
|
+
# Be conservative; if normalization fails for any reason, keep original sections
|
|
240
|
+
puts "Normalization error in parse_changelog_sections: #{e}" if Gemstar.debug?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
if Gemstar.debug?
|
|
244
|
+
puts "parse_changelog_sections #{@metadata.gem_name}:"
|
|
245
|
+
pp sections
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
sections
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def parse_github_release_sections
|
|
252
|
+
begin
|
|
253
|
+
require "nokogiri"
|
|
254
|
+
rescue LoadError
|
|
255
|
+
return {}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
return {} unless @metadata&.repo_uri&.include?("github.com")
|
|
259
|
+
|
|
260
|
+
url = github_releases_url
|
|
261
|
+
return {} unless url
|
|
262
|
+
|
|
263
|
+
html = Cache.fetch("releases-#{url}") do
|
|
264
|
+
begin
|
|
265
|
+
URI.open(url, read_timeout: 8)&.read
|
|
266
|
+
rescue => e
|
|
267
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
return {} if html.nil? || html.strip.empty?
|
|
273
|
+
|
|
274
|
+
doc = begin
|
|
275
|
+
Nokogiri::HTML5(html)
|
|
276
|
+
rescue => _
|
|
277
|
+
Nokogiri::HTML(html)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
sections = {}
|
|
281
|
+
|
|
282
|
+
# Preferred: iterate release sections that have an accessible h2 with the version (sr-only)
|
|
283
|
+
doc.css('section[aria-labelledby]').each do |sec|
|
|
284
|
+
heading = sec.at_css('h2.sr-only')
|
|
285
|
+
next unless heading
|
|
286
|
+
text = heading.text.to_s.strip
|
|
287
|
+
next unless text[/v?(\d[\w.\-]+)/i]
|
|
288
|
+
version = $1
|
|
289
|
+
|
|
290
|
+
body = sec.at_css('[data-test-selector="body-content"] .markdown-body') ||
|
|
291
|
+
sec.at_css('[data-test-selector="body-content"]') ||
|
|
292
|
+
sec.at_css('.markdown-body')
|
|
293
|
+
next unless body
|
|
294
|
+
|
|
295
|
+
html_chunk = body.inner_html.to_s.strip
|
|
296
|
+
next if html_chunk.empty?
|
|
297
|
+
|
|
298
|
+
lines = ["## #{version}\n", html_chunk]
|
|
299
|
+
sections[version] = lines
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Fallback: look for any body-content blocks across the page and try to infer nearby tag links
|
|
303
|
+
if sections.empty?
|
|
304
|
+
doc.css('[data-test-selector="body-content"]').each do |container|
|
|
305
|
+
body = container.at_css('.markdown-body') || container
|
|
306
|
+
# find a tag link near this container
|
|
307
|
+
link = container.at_xpath('ancestor::*[self::section or self::div][.//a[contains(@href, "/releases/tag/")]][1]//a[contains(@href, "/releases/tag/")]')
|
|
308
|
+
text = link&.text.to_s
|
|
309
|
+
text = File.basename(URI(link["href"]).path) if (text.nil? || text.empty?) && link
|
|
310
|
+
next unless text && text[/v?(\d[\w.\-]+)/i]
|
|
311
|
+
version = $1
|
|
312
|
+
|
|
313
|
+
html_chunk = body.inner_html.to_s.strip
|
|
314
|
+
next if html_chunk.empty?
|
|
315
|
+
lines = ["## #{version}\n", html_chunk]
|
|
316
|
+
sections[version] = lines
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
if Gemstar.debug?
|
|
321
|
+
puts "parse_github_release_sections #{@metadata.gem_name}:"
|
|
322
|
+
pp sections.keys
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
sections
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def github_releases_url
|
|
329
|
+
return nil unless @metadata&.repo_uri
|
|
330
|
+
repo = @metadata.repo_uri.chomp("/")
|
|
331
|
+
return nil if repo.empty?
|
|
332
|
+
"#{repo}/releases"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
data/lib/gemstar/cli.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# lib/gemstar/cli.rb
|
|
2
|
+
require "thor"
|
|
3
|
+
|
|
4
|
+
module Gemstar
|
|
5
|
+
class CLI < Thor
|
|
6
|
+
package_name "gemstar"
|
|
7
|
+
|
|
8
|
+
map "-D" => "diff"
|
|
9
|
+
|
|
10
|
+
class_option :verbose, type: :boolean, default: false, desc: "Enable verbose output"
|
|
11
|
+
class_option :lockfile, type: :string, default: "Gemfile.lock", desc: "Lockfile path"
|
|
12
|
+
|
|
13
|
+
desc "diff", "Show changelogs for updated gems"
|
|
14
|
+
method_option :from, type: :string, desc: "Git ref or lockfile"
|
|
15
|
+
method_option :to, type: :string, desc: "Git ref or lockfile"
|
|
16
|
+
method_option :output_file, type: :string, desc: "Output file path"
|
|
17
|
+
method_option :debug_gem_regex, type: :string, desc: "Debug matching gems", hide: true
|
|
18
|
+
def diff
|
|
19
|
+
Gemstar::Commands::Diff.new(options).run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# desc "pick", "Interactively cherry-pick and upgrade gems"
|
|
23
|
+
# option :gem, type: :string, desc: "Gem name to cherry-pick"
|
|
24
|
+
# def pick
|
|
25
|
+
# Gemstar::Commands::Pick.new(options).run
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# desc "audit", "Run security and vulnerability checks"
|
|
29
|
+
# def audit
|
|
30
|
+
# Gemstar::Commands::Audit.new.run
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# desc "diff", "Show lockfile diff or GitHub comparison"
|
|
34
|
+
# option :from, type: :string
|
|
35
|
+
# option :to, type: :string
|
|
36
|
+
# def diff
|
|
37
|
+
# Gemstar::Commands::Diff.new(options).run
|
|
38
|
+
# end
|
|
39
|
+
|
|
40
|
+
# desc "init", "Setup gem hygiene for a project"
|
|
41
|
+
# def init
|
|
42
|
+
# Gemstar::Commands::Init.new.run
|
|
43
|
+
# end
|
|
44
|
+
|
|
45
|
+
def self.exit_on_failure?
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command"
|
|
4
|
+
require "concurrent-ruby"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
module Gemstar
|
|
8
|
+
module Commands
|
|
9
|
+
class Diff < Command
|
|
10
|
+
attr_reader :updates
|
|
11
|
+
attr_reader :failed
|
|
12
|
+
attr_reader :from
|
|
13
|
+
attr_reader :to
|
|
14
|
+
attr_reader :lockfile
|
|
15
|
+
attr_reader :git_repo
|
|
16
|
+
attr_reader :lockfile_full_path
|
|
17
|
+
attr_reader :output_file
|
|
18
|
+
|
|
19
|
+
def initialize(options)
|
|
20
|
+
super
|
|
21
|
+
|
|
22
|
+
@debug_gem_regex = Regexp.new(options[:debug_gem_regex] || ENV["GEMSTAR_DEBUG_GEM_REGEX"] || ".*")
|
|
23
|
+
|
|
24
|
+
@from = options[:from] || "HEAD"
|
|
25
|
+
@to = options[:to]
|
|
26
|
+
@lockfile = options[:lockfile] || "Gemfile.lock"
|
|
27
|
+
@output_file = options[:output_file] || "gem_update_changelog.html"
|
|
28
|
+
|
|
29
|
+
@git_repo = Gemstar::GitRepo.new(File.dirname(@lockfile))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
# logic to diff from/to, find updated gems, fetch changelogs
|
|
34
|
+
|
|
35
|
+
#+++ edit_gitignore?
|
|
36
|
+
|
|
37
|
+
@lockfile_full_path = git_repo.get_full_path(File.basename(lockfile))
|
|
38
|
+
puts "Lockfile path: #{lockfile_full_path}"
|
|
39
|
+
|
|
40
|
+
old = LockFile.new(content: git_repo.show_blob_at(@from, lockfile_full_path))
|
|
41
|
+
new = @to ?
|
|
42
|
+
LockFile.new(content: git_repo.show_blob_at(@to, lockfile_full_path)) :
|
|
43
|
+
LockFile.new(path: lockfile)
|
|
44
|
+
|
|
45
|
+
collect_updates(new_lockfile: new, old_lockfile: old)
|
|
46
|
+
|
|
47
|
+
html = Outputs::HTML.new.render_diff(self)
|
|
48
|
+
File.write(output_file, html)
|
|
49
|
+
puts "✅ gem_update_changelog.html created."
|
|
50
|
+
|
|
51
|
+
if failed.any?
|
|
52
|
+
puts "\n⚠️ The following gems failed to process:"
|
|
53
|
+
failed.each { |gem, msg| puts " - #{gem}: #{msg}" }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def build_entry(gem_name:, old_version:, new_version:)
|
|
60
|
+
metadata = Gemstar::RubyGemsMetadata.new(gem_name)
|
|
61
|
+
repo_url = metadata.repo_uri
|
|
62
|
+
changelog = Gemstar::ChangeLog.new(metadata)
|
|
63
|
+
sections = changelog.extract_relevant_sections(old_version, new_version)
|
|
64
|
+
|
|
65
|
+
compare_url = if repo_url && old_version
|
|
66
|
+
tag_from_v = "v#{old_version}"
|
|
67
|
+
tag_to_v = "v#{new_version}"
|
|
68
|
+
tag_from_raw = old_version
|
|
69
|
+
tag_to_raw = new_version
|
|
70
|
+
|
|
71
|
+
url_v = "#{repo_url}/compare/#{tag_from_v}...#{tag_to_v}"
|
|
72
|
+
url_raw = "#{repo_url}/compare/#{tag_from_raw}...#{tag_to_raw}"
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
URI.open(url_v, read_timeout: 4) # TODO use a real HTTP client
|
|
76
|
+
url_v
|
|
77
|
+
rescue
|
|
78
|
+
url_raw
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
homepage_url = metadata.meta["homepage_uri"] || metadata.meta["source_code_uri"] || "https://rubygems.org/gems/#{gem_name}"
|
|
83
|
+
description = metadata.meta["info"]
|
|
84
|
+
|
|
85
|
+
entry = {
|
|
86
|
+
old: old_version,
|
|
87
|
+
new: new_version,
|
|
88
|
+
homepage_url: homepage_url,
|
|
89
|
+
description: description
|
|
90
|
+
}
|
|
91
|
+
entry[:sections] = sections unless sections.nil? || sections.empty?
|
|
92
|
+
entry[:compare_url] = compare_url if compare_url
|
|
93
|
+
|
|
94
|
+
if entry[:sections].nil? && repo_url && new_version
|
|
95
|
+
entry[:release_url] = "#{repo_url}/releases/tag/#{new_version}"
|
|
96
|
+
end
|
|
97
|
+
entry[:release_page] = "#{repo_url}/releases" if repo_url && (!sections || sections.empty?)
|
|
98
|
+
|
|
99
|
+
if repo_url && new_version
|
|
100
|
+
version_list = sections ? sections.keys : []
|
|
101
|
+
if version_list.empty?
|
|
102
|
+
version_list = [new_version]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
entry[:release_urls] = version_list.map do |ver|
|
|
106
|
+
"#{repo_url}/releases/tag/#{ver}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
entry
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def collect_updates(new_lockfile:, old_lockfile:)
|
|
114
|
+
@updates = {}
|
|
115
|
+
@failed = []
|
|
116
|
+
mutex = Mutex.new
|
|
117
|
+
pool = Concurrent::FixedThreadPool.new(10)
|
|
118
|
+
|
|
119
|
+
new_lockfile.specs.keys.sort.each do |gem_name|
|
|
120
|
+
pool.post do
|
|
121
|
+
next unless @debug_gem_regex.match?(gem_name)
|
|
122
|
+
|
|
123
|
+
old_version = old_lockfile.specs[gem_name]
|
|
124
|
+
new_version = new_lockfile.specs[gem_name]
|
|
125
|
+
next if old_version == new_version
|
|
126
|
+
|
|
127
|
+
puts "#{gem_name} (#{old_version || "new"} → #{new_version})..."
|
|
128
|
+
|
|
129
|
+
begin
|
|
130
|
+
entry = build_entry(gem_name: gem_name, old_version: old_version, new_version: new_version)
|
|
131
|
+
|
|
132
|
+
mutex.synchronize { updates[gem_name] = entry }
|
|
133
|
+
rescue => e
|
|
134
|
+
mutex.synchronize { failed << [gem_name, e.message] }
|
|
135
|
+
puts "⚠️ Failed to process #{gem_name}: #{e.message}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
pool.shutdown
|
|
141
|
+
pool.wait_for_termination
|
|
142
|
+
|
|
143
|
+
@updates = updates
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
class GitHub
|
|
5
|
+
def self.github_blob_to_raw(url, ref_is_tag: false)
|
|
6
|
+
return nil unless url
|
|
7
|
+
|
|
8
|
+
uri = URI(url)
|
|
9
|
+
return url unless uri.host == "github.com"
|
|
10
|
+
|
|
11
|
+
owner, repo, blob, *rest = uri.path.split("/")[1..]
|
|
12
|
+
return url unless blob == "blob"
|
|
13
|
+
|
|
14
|
+
ref = rest.shift
|
|
15
|
+
path = rest.join("/")
|
|
16
|
+
|
|
17
|
+
ref_prefix = ref_is_tag ? "refs/tags/" : ""
|
|
18
|
+
|
|
19
|
+
uri.scheme = "https"
|
|
20
|
+
uri.host = "raw.githubusercontent.com"
|
|
21
|
+
uri.path = "/#{owner}/#{repo}/#{ref_prefix}#{ref}/#{path}"
|
|
22
|
+
uri.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Gemstar
|
|
2
|
+
class GitRepo
|
|
3
|
+
def initialize(specified_directory)
|
|
4
|
+
@specified_directory = specified_directory || Dir.pwd
|
|
5
|
+
@tree_root_directory = find_git_root(File.dirname(@specified_directory))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def find_git_root(directory)
|
|
9
|
+
# return directory if File.directory?(File.join(directory, ".git"))
|
|
10
|
+
# find_git_root(File.dirname(directory))
|
|
11
|
+
|
|
12
|
+
run_git_command(%W[rev-parse --show-toplevel])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def git_client
|
|
16
|
+
"git"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run_git_command(command, in_directory: @specified_directory, strip: true)
|
|
20
|
+
git_command = [git_client]
|
|
21
|
+
git_command += ["-C", in_directory] if in_directory
|
|
22
|
+
git_command += command
|
|
23
|
+
|
|
24
|
+
puts %[run_git_command (joined): #{git_command.join(" ")}] if Gemstar.debug?
|
|
25
|
+
|
|
26
|
+
output = IO.popen(git_command, err: [:child, :out],
|
|
27
|
+
&:read)
|
|
28
|
+
strip ? output.strip : output
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve_commit(revish, default_branch: "HEAD")
|
|
32
|
+
# If it looks like a pure date (or you want to support "date only"),
|
|
33
|
+
# map it to "latest commit before date on default_branch".
|
|
34
|
+
if revish =~ /\d{4}-\d{2}-\d{2}/ || revish =~ /\d{1,2}:\d{2}/i
|
|
35
|
+
sha = run_git_command(["rev-list", "-1", "--before", revish, default_branch])
|
|
36
|
+
raise "No commit before #{revish} on #{default_branch}" if sha.empty?
|
|
37
|
+
return sha
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Otherwise let Git parse whatever the user typed.
|
|
41
|
+
sha = run_git_command(%W[rev-parse --verify #{revish}^{commit}])
|
|
42
|
+
raise "Unknown revision: #{revish}" if sha.empty?
|
|
43
|
+
sha
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show_blob_at(revish, path)
|
|
47
|
+
commit = resolve_commit(revish)
|
|
48
|
+
run_git_command(["show", "#{commit}:#{path}"])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def get_full_path(path)
|
|
52
|
+
run_git_command(["ls-files", "--full-name", "--", path])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Gemstar
|
|
2
|
+
class LockFile
|
|
3
|
+
def initialize(path: nil, content: nil)
|
|
4
|
+
@path = path
|
|
5
|
+
@specs = content ? parse_content(content) : parse_lockfile(path)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_reader :specs
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def parse_lockfile(path)
|
|
13
|
+
parse_content(File.read(path))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse_content(content)
|
|
17
|
+
specs = {}
|
|
18
|
+
in_specs = false
|
|
19
|
+
content.each_line do |line|
|
|
20
|
+
in_specs = true if line.strip == "GEM"
|
|
21
|
+
next unless in_specs
|
|
22
|
+
if line =~ /^\s{4}(\S+) \((.+)\)/
|
|
23
|
+
name, version = $1, $2
|
|
24
|
+
specs[name] = version
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
specs
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "basic"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "kramdown"
|
|
6
|
+
begin
|
|
7
|
+
require "kramdown-parser-gfm"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
# Optional dependency: if not available, we'll gracefully fall back to the default parser
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Gemstar
|
|
13
|
+
module Outputs
|
|
14
|
+
class HTML < Basic
|
|
15
|
+
def render_diff(diff_command)
|
|
16
|
+
body = diff_command.updates.sort.map do |gem_name, info|
|
|
17
|
+
icon = info[:homepage_url]&.include?("github.com") ? "🐙" : "💎"
|
|
18
|
+
tooltip = info[:description] ? "title=\"#{info[:description].gsub('"', """)}\"" : ""
|
|
19
|
+
link = "<a href=\"#{info[:homepage_url]}\" #{tooltip} target=\"_blank\">#{gem_name}</a>"
|
|
20
|
+
html = if info[:sections]
|
|
21
|
+
info[:sections].map do |_version, lines|
|
|
22
|
+
html_chunk = begin
|
|
23
|
+
opts = { hard_wrap: false }
|
|
24
|
+
opts[:input] = "GFM" if defined?(Kramdown::Parser::GFM)
|
|
25
|
+
Kramdown::Document.new(lines.join, opts).to_html
|
|
26
|
+
rescue Kramdown::Error
|
|
27
|
+
Kramdown::Document.new(lines.join, { hard_wrap: false }).to_html
|
|
28
|
+
end
|
|
29
|
+
<<~HTML
|
|
30
|
+
#{html_chunk}
|
|
31
|
+
HTML
|
|
32
|
+
end.join("\n")
|
|
33
|
+
elsif info[:release_urls]
|
|
34
|
+
"" # the changelog wasn't found, but we have release links — skip the message
|
|
35
|
+
else
|
|
36
|
+
"<p><strong>#{gem_name}:</strong> No changelog entries found</p>"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
<<~HTML
|
|
40
|
+
<section>
|
|
41
|
+
<h2>#{icon} #{link}: #{info[:old] || "new"} → #{info[:new]}</h2>
|
|
42
|
+
#{"<p><a href='#{info[:release_page]}' target='_blank'>View all GitHub release notes</a></p>" if info[:release_page]}
|
|
43
|
+
#{html}
|
|
44
|
+
</section>
|
|
45
|
+
HTML
|
|
46
|
+
end.join("\n")
|
|
47
|
+
|
|
48
|
+
project_name = Pathname.getwd.basename.to_s
|
|
49
|
+
|
|
50
|
+
<<~HTML
|
|
51
|
+
<!DOCTYPE html>
|
|
52
|
+
<html>
|
|
53
|
+
<head>
|
|
54
|
+
<meta charset="UTF-8">
|
|
55
|
+
<title>#{project_name}: Gem Updates with Changelogs</title>
|
|
56
|
+
<style>
|
|
57
|
+
body { font-family: sans-serif; padding: 2em; background: #fdfdfd; }
|
|
58
|
+
section { margin-bottom: 3em; border-bottom: 1px solid #ccc; padding-bottom: 1em; }
|
|
59
|
+
h2 { color: #333; }
|
|
60
|
+
h3 { margin-top: 1em; color: #444; }
|
|
61
|
+
pre { background: #eee; padding: 1em; overflow-x: auto; }
|
|
62
|
+
a { color: #0645ad; text-decoration: none; }
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<h1>#{project_name}: Gem Updates</h1>
|
|
67
|
+
<p><i>Showing changes from #{diff_command.from} to #{diff_command.to || "now"}, generated on #{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}.</i></p>
|
|
68
|
+
#{body}
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
71
|
+
HTML
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
class RemoteRepository
|
|
5
|
+
def initialize(repository_uri)
|
|
6
|
+
@repository_uri = repository_uri
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def find_main_branch
|
|
10
|
+
# Attempt loading .gitignore (assumed to be present in all repos) from either
|
|
11
|
+
# main or master branch:
|
|
12
|
+
%w[main master].each do |branch|
|
|
13
|
+
Cache.fetch("gitignore-#{branch}") do
|
|
14
|
+
content = begin
|
|
15
|
+
URI.open("#{@repository_uri}/#{branch}/.gitignore", read_timeout: 8)&.read
|
|
16
|
+
rescue
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
return [branch] unless content.nil?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# No .gitignore found, have to search for changelogs in both branches:
|
|
24
|
+
%w[main master]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "open-uri"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Gemstar
|
|
6
|
+
class RubyGemsMetadata
|
|
7
|
+
def initialize(gem_name)
|
|
8
|
+
@gem_name = gem_name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :gem_name
|
|
12
|
+
|
|
13
|
+
def meta
|
|
14
|
+
@meta ||=
|
|
15
|
+
begin
|
|
16
|
+
url = "https://rubygems.org/api/v1/gems/#{URI.encode_www_form_component(gem_name)}.json"
|
|
17
|
+
Cache.fetch("rubygems-#{gem_name}") do
|
|
18
|
+
URI.open(url).read
|
|
19
|
+
end.then { |json|
|
|
20
|
+
begin
|
|
21
|
+
JSON.parse(json) if json
|
|
22
|
+
rescue
|
|
23
|
+
nil
|
|
24
|
+
end }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def repo_uri
|
|
29
|
+
return nil unless meta
|
|
30
|
+
|
|
31
|
+
@repo_uri ||= begin
|
|
32
|
+
uri = meta["source_code_uri"]
|
|
33
|
+
|
|
34
|
+
if uri.nil?
|
|
35
|
+
uri = meta["homepage_uri"]
|
|
36
|
+
if uri.include?("github.com")
|
|
37
|
+
uri = uri[%r{http[s?]://github\.com/[^/]+/[^/]+}]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
uri ||= ""
|
|
42
|
+
|
|
43
|
+
uri = uri.sub("http://", "https://")
|
|
44
|
+
|
|
45
|
+
uri = uri.gsub(/\.git$/, "")
|
|
46
|
+
|
|
47
|
+
if uri.include?("github.io")
|
|
48
|
+
# Convert e.g. https://socketry.github.io/console/ to https://github.com/socketry/console/
|
|
49
|
+
uri = uri.sub(%r{\Ahttps?://([\w-]+)\.github\.io/([^/]+)}) do
|
|
50
|
+
"https://github.com/#{$1}/#{$2}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
uri
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Gemstar
|
|
2
|
+
module Utils
|
|
3
|
+
def generate_version_range(from_str, to_str)
|
|
4
|
+
from = Gem::Version.new(from_str.gsub(/-[\w\-]+$/, ''))
|
|
5
|
+
to = Gem::Version.new(to_str.gsub(/-[\w\-]+$/, ''))
|
|
6
|
+
result = Set.new
|
|
7
|
+
|
|
8
|
+
# Generate known version steps up to 2000 iterations max (safety limit)
|
|
9
|
+
queue = [from]
|
|
10
|
+
2000.times do
|
|
11
|
+
v = queue.pop
|
|
12
|
+
break if v.nil? || v >= to
|
|
13
|
+
|
|
14
|
+
patch = Gem::Version.new("#{v.segments[0]}.#{v.segments[1]}.#{v.segments[2] + 1}")
|
|
15
|
+
minor = Gem::Version.new("#{v.segments[0]}.#{v.segments[1] + 1}.0")
|
|
16
|
+
major = Gem::Version.new("#{v.segments[0] + 1}.0.0")
|
|
17
|
+
|
|
18
|
+
[patch, minor, major].each do |next_v|
|
|
19
|
+
next if next_v > to || result&.include?(next_v)
|
|
20
|
+
result << next_v
|
|
21
|
+
queue << next_v
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
result.select { |v| v > from && v <= to }.sort.map(&:to_s)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/gemstar.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gemstar/version"
|
|
4
|
+
require "gemstar/railtie" if defined?(Rails::Railtie)
|
|
5
|
+
require "gemstar/cli"
|
|
6
|
+
require "gemstar/commands/command"
|
|
7
|
+
require "gemstar/commands/diff"
|
|
8
|
+
require "gemstar/outputs/basic"
|
|
9
|
+
require "gemstar/outputs/html"
|
|
10
|
+
require "gemstar/cache"
|
|
11
|
+
require "gemstar/change_log"
|
|
12
|
+
require "gemstar/git_hub"
|
|
13
|
+
require "gemstar/lock_file"
|
|
14
|
+
require "gemstar/remote_repository"
|
|
15
|
+
require "gemstar/utils"
|
|
16
|
+
require "gemstar/ruby_gems_metadata"
|
|
17
|
+
require "gemstar/git_repo"
|
metadata
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemstar
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Florian Dejako
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 2.6.8
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 2.6.8
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: combustion
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.5'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.5'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: minitest
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '5.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '5.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: kramdown
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: kramdown-parser-gfm
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rouge
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '4'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '4'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: concurrent-ruby
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '1.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '1.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: thor
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '1.4'
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '1.4'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: nokogiri
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '1.18'
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '1.18'
|
|
152
|
+
description: Gem changelog viewer and more, but starting as a changelog viewer for
|
|
153
|
+
bundled gems.
|
|
154
|
+
email:
|
|
155
|
+
- fdejako@gmail.com
|
|
156
|
+
executables:
|
|
157
|
+
- gemstar
|
|
158
|
+
extensions: []
|
|
159
|
+
extra_rdoc_files: []
|
|
160
|
+
files:
|
|
161
|
+
- CHANGELOG.md
|
|
162
|
+
- LICENSE.txt
|
|
163
|
+
- README.md
|
|
164
|
+
- bin/gemstar
|
|
165
|
+
- lib/gemstar.rb
|
|
166
|
+
- lib/gemstar/cache.rb
|
|
167
|
+
- lib/gemstar/change_log.rb
|
|
168
|
+
- lib/gemstar/cli.rb
|
|
169
|
+
- lib/gemstar/commands/command.rb
|
|
170
|
+
- lib/gemstar/commands/diff.rb
|
|
171
|
+
- lib/gemstar/git_hub.rb
|
|
172
|
+
- lib/gemstar/git_repo.rb
|
|
173
|
+
- lib/gemstar/lock_file.rb
|
|
174
|
+
- lib/gemstar/outputs/basic.rb
|
|
175
|
+
- lib/gemstar/outputs/html.rb
|
|
176
|
+
- lib/gemstar/railtie.rb
|
|
177
|
+
- lib/gemstar/remote_repository.rb
|
|
178
|
+
- lib/gemstar/ruby_gems_metadata.rb
|
|
179
|
+
- lib/gemstar/utils.rb
|
|
180
|
+
- lib/gemstar/version.rb
|
|
181
|
+
homepage: https://github.com/FDj/gemstar
|
|
182
|
+
licenses:
|
|
183
|
+
- MIT
|
|
184
|
+
metadata:
|
|
185
|
+
bug_tracker_uri: https://github.com/FDj/gemstar/issues
|
|
186
|
+
changelog_uri: https://github.com/FDj/gemstar/blob/master/CHANGELOG.md
|
|
187
|
+
source_code_uri: https://github.com/FDj/gemstar
|
|
188
|
+
rdoc_options: []
|
|
189
|
+
require_paths:
|
|
190
|
+
- lib
|
|
191
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
192
|
+
requirements:
|
|
193
|
+
- - ">="
|
|
194
|
+
- !ruby/object:Gem::Version
|
|
195
|
+
version: '3.3'
|
|
196
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - ">="
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: '0'
|
|
201
|
+
requirements: []
|
|
202
|
+
rubygems_version: 3.7.0
|
|
203
|
+
specification_version: 4
|
|
204
|
+
summary: Making sense of gems.
|
|
205
|
+
test_files: []
|