gemstar 0.0.2 → 1.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 +4 -4
- data/CHANGELOG.md +12 -14
- data/README.md +19 -0
- data/lib/gemstar/cache.rb +47 -10
- data/lib/gemstar/cache_cli.rb +12 -0
- data/lib/gemstar/cache_warmer.rb +120 -0
- data/lib/gemstar/change_log.rb +75 -44
- data/lib/gemstar/cli.rb +13 -0
- data/lib/gemstar/commands/cache.rb +12 -0
- data/lib/gemstar/commands/diff.rb +28 -5
- data/lib/gemstar/commands/server.rb +136 -0
- data/lib/gemstar/config.rb +15 -0
- data/lib/gemstar/git_repo.rb +74 -7
- data/lib/gemstar/lock_file.rb +86 -8
- data/lib/gemstar/outputs/markdown.rb +165 -0
- data/lib/gemstar/project.rb +245 -0
- data/lib/gemstar/request_logger.rb +31 -0
- data/lib/gemstar/ruby_gems_metadata.rb +49 -33
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +936 -0
- data/lib/gemstar/web/templates/app.css +523 -0
- data/lib/gemstar/web/templates/app.js.erb +226 -0
- data/lib/gemstar/web/templates/page.html.erb +15 -0
- data/lib/gemstar/webrick_logger.rb +22 -0
- data/lib/gemstar.rb +7 -1
- metadata +71 -3
- data/lib/gemstar/railtie.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c795357616121ccda76004c6fefa92194688472dd45950396c24a1924cf70a6
|
|
4
|
+
data.tar.gz: d4922547dc024c923d751164698da5b672c48fd4b2e61384345e0de24b1f5430
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6caf7c992dcccd533da3cbcf8ed1e98ee44538faffbdf43c55e1ecc1af899497c70100c62e1ba5bbe9331fba6c78d2a65a571c860f4035b99dc63f44d94cb682
|
|
7
|
+
data.tar.gz: 88e7f1557e01332a6d5dbd69301b0fcfb30548a80333d835476994a9b9ae92a5fdb8559bfa268d242ad11eaad266b432947277d52b185e40dcc66f166ee01854
|
data/CHANGELOG.md
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
# Change Log
|
|
2
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
|
-
- bundler itself
|
|
15
|
-
- use changelog files from installed gems where present
|
|
16
|
-
|
|
17
3
|
## Unreleased
|
|
18
4
|
|
|
5
|
+
## 1.0.1
|
|
6
|
+
|
|
7
|
+
- Added `--format markdown` to `gemstar diff` command.
|
|
8
|
+
|
|
9
|
+
## 1.0
|
|
10
|
+
|
|
11
|
+
- Added `gemstar server`, your interactive Gemfile.lock explorer and more.
|
|
12
|
+
- Default location for `diff` is now a tmp file.
|
|
13
|
+
- Removed Railtie from this gem.
|
|
14
|
+
- Improve how git root dir is determined.
|
|
15
|
+
|
|
16
|
+
|
|
19
17
|
## 0.0.2
|
|
20
18
|
|
|
21
19
|
- Diff: Fix regex warnings shown in terminal.
|
data/README.md
CHANGED
|
@@ -22,6 +22,19 @@ gem "gemstar", group: :development
|
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
+
### gemstar server
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Start the interactive web UI:
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
gemstar server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
By default, the server listens to http://127.0.0.1:2112/
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
### gemstar diff
|
|
26
39
|
|
|
27
40
|
Run this after you've updated your gems.
|
|
@@ -47,6 +60,12 @@ To examine a specific Gemfile.lock, pass it like this:
|
|
|
47
60
|
gemstar diff --lockfile=~/MyProject/Gemfile.lock
|
|
48
61
|
```
|
|
49
62
|
|
|
63
|
+
To write markdown instead of html:
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
gemstar diff --format markdown
|
|
67
|
+
```
|
|
68
|
+
|
|
50
69
|
## Contributing
|
|
51
70
|
|
|
52
71
|
Bug reports and pull requests are welcome on GitHub at [https://github.com/FDj/gemstar](https://github.com/FDj/gemstar).
|
data/lib/gemstar/cache.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
require_relative "config"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "digest"
|
|
3
4
|
|
|
4
5
|
module Gemstar
|
|
5
6
|
class Cache
|
|
6
7
|
MAX_CACHE_AGE = 60 * 60 * 24 * 7 # 1 week
|
|
7
|
-
CACHE_DIR =
|
|
8
|
+
CACHE_DIR = File.join(Gemstar::Config.home_directory, "cache")
|
|
8
9
|
|
|
9
10
|
@@initialized = false
|
|
10
11
|
|
|
@@ -18,15 +19,12 @@ module Gemstar
|
|
|
18
19
|
def self.fetch(key, &block)
|
|
19
20
|
init
|
|
20
21
|
|
|
21
|
-
path =
|
|
22
|
+
path = path_for(key)
|
|
22
23
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
return nil if content == "__404__"
|
|
28
|
-
return content
|
|
29
|
-
end
|
|
24
|
+
if fresh?(path)
|
|
25
|
+
content = File.read(path)
|
|
26
|
+
return nil if content == "__404__"
|
|
27
|
+
return content
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
begin
|
|
@@ -39,11 +37,50 @@ module Gemstar
|
|
|
39
37
|
end
|
|
40
38
|
end
|
|
41
39
|
|
|
40
|
+
def self.peek(key)
|
|
41
|
+
init
|
|
42
|
+
|
|
43
|
+
path = path_for(key)
|
|
44
|
+
return nil unless fresh?(path)
|
|
45
|
+
|
|
46
|
+
content = File.read(path)
|
|
47
|
+
return nil if content == "__404__"
|
|
48
|
+
|
|
49
|
+
content
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.path_for(key)
|
|
53
|
+
File.join(CACHE_DIR, Digest::SHA256.hexdigest(key))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.fresh?(path)
|
|
57
|
+
return false unless File.exist?(path)
|
|
58
|
+
|
|
59
|
+
(Time.now - File.mtime(path)) <= MAX_CACHE_AGE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.flush!
|
|
63
|
+
init
|
|
64
|
+
|
|
65
|
+
flush_directory(CACHE_DIR)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.flush_directory(directory)
|
|
69
|
+
return 0 unless Dir.exist?(directory)
|
|
70
|
+
|
|
71
|
+
entries = Dir.children(directory)
|
|
72
|
+
entries.each do |entry|
|
|
73
|
+
FileUtils.rm_rf(File.join(directory, entry))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
entries.count
|
|
77
|
+
end
|
|
78
|
+
|
|
42
79
|
end
|
|
43
80
|
|
|
44
81
|
def edit_gitignore
|
|
45
82
|
gitignore_path = ".gitignore"
|
|
46
|
-
ignore_entries = %w[
|
|
83
|
+
ignore_entries = %w[gem_update_changelog.html]
|
|
47
84
|
|
|
48
85
|
existing_lines = File.exist?(gitignore_path) ? File.read(gitignore_path).lines.map(&:chomp) : []
|
|
49
86
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
require "thread"
|
|
3
|
+
|
|
4
|
+
module Gemstar
|
|
5
|
+
class CacheWarmer
|
|
6
|
+
DEFAULT_THREADS = 10
|
|
7
|
+
|
|
8
|
+
def initialize(io: $stderr, debug: false, thread_count: DEFAULT_THREADS)
|
|
9
|
+
@io = io
|
|
10
|
+
@debug = debug
|
|
11
|
+
@thread_count = thread_count
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@condition = ConditionVariable.new
|
|
14
|
+
@queue = []
|
|
15
|
+
@queued = Set.new
|
|
16
|
+
@in_progress = Set.new
|
|
17
|
+
@completed = Set.new
|
|
18
|
+
@workers = []
|
|
19
|
+
@started = false
|
|
20
|
+
@total = 0
|
|
21
|
+
@completed_count = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enqueue_many(gem_names)
|
|
25
|
+
names = gem_names.uniq
|
|
26
|
+
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
names.each do |gem_name|
|
|
29
|
+
next if @completed.include?(gem_name) || @queued.include?(gem_name) || @in_progress.include?(gem_name)
|
|
30
|
+
|
|
31
|
+
@queue << gem_name
|
|
32
|
+
@queued << gem_name
|
|
33
|
+
end
|
|
34
|
+
@total += names.count
|
|
35
|
+
start_workers_unlocked unless @started
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
log "Background cache refresh queued for #{names.count} gems."
|
|
39
|
+
@condition.broadcast
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def prioritize(gem_name)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
return if @completed.include?(gem_name) || @in_progress.include?(gem_name)
|
|
46
|
+
|
|
47
|
+
if @queued.include?(gem_name)
|
|
48
|
+
@queue.delete(gem_name)
|
|
49
|
+
else
|
|
50
|
+
@queued << gem_name
|
|
51
|
+
@total += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@queue.unshift(gem_name)
|
|
55
|
+
start_workers_unlocked unless @started
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
log "Prioritized #{gem_name}"
|
|
59
|
+
@condition.broadcast
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def start_workers_unlocked
|
|
65
|
+
return if @started
|
|
66
|
+
|
|
67
|
+
@started = true
|
|
68
|
+
@thread_count.times do
|
|
69
|
+
@workers << Thread.new { worker_loop }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def worker_loop
|
|
74
|
+
Thread.current.name = "gemstar-cache-worker" if Thread.current.respond_to?(:name=)
|
|
75
|
+
|
|
76
|
+
loop do
|
|
77
|
+
gem_name = @mutex.synchronize do
|
|
78
|
+
while @queue.empty?
|
|
79
|
+
@condition.wait(@mutex)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
next_gem = @queue.shift
|
|
83
|
+
@queued.delete(next_gem)
|
|
84
|
+
@in_progress << next_gem
|
|
85
|
+
next_gem
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
warm_cache_for_gem(gem_name)
|
|
89
|
+
|
|
90
|
+
current = @mutex.synchronize do
|
|
91
|
+
@in_progress.delete(gem_name)
|
|
92
|
+
@completed << gem_name
|
|
93
|
+
@completed_count += 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
log_progress(gem_name, current)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def warm_cache_for_gem(gem_name)
|
|
101
|
+
metadata = Gemstar::RubyGemsMetadata.new(gem_name)
|
|
102
|
+
metadata.meta
|
|
103
|
+
metadata.repo_uri
|
|
104
|
+
Gemstar::ChangeLog.new(metadata).sections
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
log "Cache refresh failed for #{gem_name}: #{e.class}: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def log_progress(gem_name, current)
|
|
110
|
+
return unless @debug
|
|
111
|
+
return unless current <= 5 || (current % 25).zero?
|
|
112
|
+
|
|
113
|
+
log "Background cache refresh #{current}/#{@total}: #{gem_name}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def log(message)
|
|
117
|
+
@io.puts(message)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/gemstar/change_log.rb
CHANGED
|
@@ -10,21 +10,30 @@ module Gemstar
|
|
|
10
10
|
|
|
11
11
|
attr_reader :metadata
|
|
12
12
|
|
|
13
|
-
def content
|
|
14
|
-
@content
|
|
13
|
+
def content(cache_only: false)
|
|
14
|
+
return @content if !cache_only && defined?(@content)
|
|
15
|
+
|
|
16
|
+
result = fetch_changelog_content(cache_only: cache_only)
|
|
17
|
+
@content = result unless cache_only
|
|
18
|
+
result
|
|
15
19
|
end
|
|
16
20
|
|
|
17
|
-
def sections
|
|
18
|
-
@sections
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def sections(cache_only: false)
|
|
22
|
+
return @sections if !cache_only && defined?(@sections)
|
|
23
|
+
|
|
24
|
+
result = begin
|
|
25
|
+
s = parse_changelog_sections(cache_only: cache_only)
|
|
26
|
+
if s.nil? || s.empty?
|
|
27
|
+
s = parse_github_release_sections(cache_only: cache_only)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
pp @@candidates_found if Gemstar.debug? && !cache_only
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
s
|
|
33
|
+
end
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
@sections = result unless cache_only
|
|
36
|
+
result
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
def extract_relevant_sections(old_version, new_version)
|
|
@@ -52,26 +61,30 @@ module Gemstar
|
|
|
52
61
|
def extract_version_from_heading(line)
|
|
53
62
|
return nil unless line
|
|
54
63
|
heading = line.to_s
|
|
64
|
+
version_token = /(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)/
|
|
55
65
|
# 1) Prefer version inside parentheses after a date: "### 2025-11-07 (2.16.0)"
|
|
56
66
|
# Ensure we ONLY treat it as a version if it actually looks like a version (has a dot),
|
|
57
67
|
# so we don't capture dates like (2025-11-21).
|
|
58
|
-
return $1 if heading[/\(\s*v
|
|
68
|
+
return $1 if heading[/\(\s*v?#{version_token}(?![A-Za-z0-9])\s*\)/]
|
|
59
69
|
# 2) Version-first with optional leading markers/labels: "## v1.2.6 - 2025-10-21"
|
|
60
70
|
# Require a dot in the numeric token to avoid capturing dates like 2025-11-21.
|
|
61
|
-
return $1 if heading[/^\s*(?:#+|=+)?\s*(?:Version\s+)?\[
|
|
71
|
+
return $1 if heading[/^\s*(?:[-*]\s+)?(?:#+|=+)?\s*(?:Version\s+)?\[*v?#{version_token}(?![A-Za-z0-9])\]*/i]
|
|
62
72
|
# 3) Anywhere: first semver-like token with a dot
|
|
63
|
-
return $1 if heading[/\bv
|
|
73
|
+
return $1 if heading[/\bv?#{version_token}(?![A-Za-z0-9])\b/]
|
|
64
74
|
nil
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
def changelog_uri_candidates
|
|
77
|
+
def changelog_uri_candidates(cache_only: false)
|
|
68
78
|
candidates = []
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
repo_uri = @metadata.repo_uri(cache_only: cache_only)
|
|
81
|
+
return [] if repo_uri.nil? || repo_uri.empty?
|
|
82
|
+
|
|
83
|
+
if repo_uri =~ %r{https://github\.com/aws/aws-sdk-ruby}
|
|
71
84
|
base = "https://raw.githubusercontent.com/aws/aws-sdk-ruby/refs/heads/version-3/gems/#{@metadata.gem_name}"
|
|
72
85
|
aws_style = true
|
|
73
86
|
else
|
|
74
|
-
base =
|
|
87
|
+
base = repo_uri.sub("https://github.com", "https://raw.githubusercontent.com")
|
|
75
88
|
aws_style = false
|
|
76
89
|
end
|
|
77
90
|
|
|
@@ -94,7 +107,8 @@ module Gemstar
|
|
|
94
107
|
end
|
|
95
108
|
|
|
96
109
|
# Add the gem's changelog_uri last as it's usually not the most parsable:
|
|
97
|
-
|
|
110
|
+
meta = @metadata.meta(cache_only: cache_only)
|
|
111
|
+
candidates += [Gemstar::GitHub::github_blob_to_raw(meta["changelog_uri"])] if meta
|
|
98
112
|
|
|
99
113
|
candidates.flatten!
|
|
100
114
|
candidates.uniq!
|
|
@@ -103,15 +117,19 @@ module Gemstar
|
|
|
103
117
|
candidates
|
|
104
118
|
end
|
|
105
119
|
|
|
106
|
-
def fetch_changelog_content
|
|
120
|
+
def fetch_changelog_content(cache_only: false)
|
|
107
121
|
content = nil
|
|
108
122
|
|
|
109
|
-
changelog_uri_candidates.find do |candidate|
|
|
110
|
-
content =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
changelog_uri_candidates(cache_only: cache_only).find do |candidate|
|
|
124
|
+
content = if cache_only
|
|
125
|
+
Cache.peek("changelog-#{candidate}")
|
|
126
|
+
else
|
|
127
|
+
Cache.fetch("changelog-#{candidate}") do
|
|
128
|
+
URI.open(candidate, read_timeout: 8)&.read
|
|
129
|
+
rescue => e
|
|
130
|
+
puts "#{candidate}: #{e}" if Gemstar.debug?
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
115
133
|
end
|
|
116
134
|
|
|
117
135
|
# puts "fetch_changelog_content #{candidate}:\n#{content}" if Gemstar.debug?
|
|
@@ -127,18 +145,18 @@ module Gemstar
|
|
|
127
145
|
end
|
|
128
146
|
|
|
129
147
|
VERSION_PATTERNS = [
|
|
130
|
-
/^\s*(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d[
|
|
131
|
-
/^\s*(?:#+|=+)\s*\[
|
|
132
|
-
/^\s*(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[
|
|
133
|
-
/^\s*(?:#+|=+)\s*(?:Version\s+)?\[
|
|
134
|
-
/^\s*(?:Version\s+)?v?(\d+\.\d+(?:\.\d+)?[A-Za-z0-9
|
|
148
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\s*\)/, # prefer this
|
|
149
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\[*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\]*\s*(?:—|–|-)\s*\d{4}-\d{2}-\d{2}\b/,
|
|
150
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\]*(?:\s*[-(].*)?/i,
|
|
151
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?\[*v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])\]*(?:\s*[-(].*)?/i,
|
|
152
|
+
/^\s*(?:[-*]\s+)?(?:Version\s+)?v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])(?:\s*[-(].*)?/i
|
|
135
153
|
]
|
|
136
154
|
|
|
137
|
-
def parse_changelog_sections
|
|
155
|
+
def parse_changelog_sections(cache_only: false)
|
|
138
156
|
# If the fetched content looks like a GitHub Releases HTML page, return {}
|
|
139
157
|
# so that the GitHub releases scraper can handle it. This avoids
|
|
140
158
|
# accidentally parsing HTML from /releases pages as a markdown changelog.
|
|
141
|
-
c = content
|
|
159
|
+
c = content(cache_only: cache_only)
|
|
142
160
|
return {} if c.nil? || c.strip.empty?
|
|
143
161
|
if (c.include?("<html") || c.include?("<!DOCTYPE html")) &&
|
|
144
162
|
(c.include?('data-test-selector="body-content"') || c.include?("/releases/tag/"))
|
|
@@ -194,24 +212,37 @@ module Gemstar
|
|
|
194
212
|
sections
|
|
195
213
|
end
|
|
196
214
|
|
|
197
|
-
def parse_github_release_sections
|
|
215
|
+
def parse_github_release_sections(cache_only: false)
|
|
198
216
|
begin
|
|
199
217
|
require "nokogiri"
|
|
200
218
|
rescue LoadError
|
|
201
219
|
return {}
|
|
202
220
|
end
|
|
203
221
|
|
|
204
|
-
|
|
222
|
+
repo_uri = @metadata&.repo_uri(cache_only: cache_only)
|
|
223
|
+
return {} unless repo_uri&.include?("github.com")
|
|
205
224
|
|
|
206
|
-
url = github_releases_url
|
|
225
|
+
url = github_releases_url(repo_uri)
|
|
207
226
|
return {} unless url
|
|
208
227
|
|
|
209
|
-
html =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
html = if cache_only
|
|
229
|
+
Cache.peek("releases-#{url}")
|
|
230
|
+
else
|
|
231
|
+
Cache.fetch("releases-#{url}") do
|
|
232
|
+
begin
|
|
233
|
+
URI.open(url, read_timeout: 8)&.read
|
|
234
|
+
rescue => e
|
|
235
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if (html.nil? || html.strip.empty?) && cache_only
|
|
242
|
+
cached_content = content(cache_only: true)
|
|
243
|
+
if cached_content&.include?("<html") &&
|
|
244
|
+
(cached_content.include?('data-test-selector="body-content"') || cached_content.include?("/releases/tag/"))
|
|
245
|
+
html = cached_content
|
|
215
246
|
end
|
|
216
247
|
end
|
|
217
248
|
|
|
@@ -271,9 +302,9 @@ module Gemstar
|
|
|
271
302
|
sections
|
|
272
303
|
end
|
|
273
304
|
|
|
274
|
-
def github_releases_url
|
|
275
|
-
return nil unless
|
|
276
|
-
repo =
|
|
305
|
+
def github_releases_url(repo_uri = @metadata&.repo_uri)
|
|
306
|
+
return nil unless repo_uri
|
|
307
|
+
repo = repo_uri.chomp("/")
|
|
277
308
|
return nil if repo.empty?
|
|
278
309
|
"#{repo}/releases"
|
|
279
310
|
end
|
data/lib/gemstar/cli.rb
CHANGED
|
@@ -13,12 +13,25 @@ module Gemstar
|
|
|
13
13
|
desc "diff", "Show changelogs for updated gems"
|
|
14
14
|
method_option :from, type: :string, desc: "Git ref or lockfile"
|
|
15
15
|
method_option :to, type: :string, desc: "Git ref or lockfile"
|
|
16
|
+
method_option :format, type: :string, desc: "Output format (html or markdown)"
|
|
16
17
|
method_option :output_file, type: :string, desc: "Output file path"
|
|
17
18
|
method_option :debug_gem_regex, type: :string, desc: "Debug matching gems", hide: true
|
|
18
19
|
def diff
|
|
19
20
|
Gemstar::Commands::Diff.new(options).run
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
desc "server", "Start the interactive web server"
|
|
24
|
+
method_option :bind, type: :string, default: "127.0.0.1", desc: "Bind address"
|
|
25
|
+
method_option :port, type: :numeric, default: 2112, desc: "Port"
|
|
26
|
+
method_option :project, type: :string, repeatable: true, desc: "Project directories or Gemfile paths"
|
|
27
|
+
method_option :reload, type: :boolean, default: false, desc: "Restart automatically when files change"
|
|
28
|
+
def server
|
|
29
|
+
Gemstar::Commands::Server.new(options).run
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "cache SUBCOMMAND ...ARGS", "Manage gemstar caches"
|
|
33
|
+
subcommand "cache", Gemstar::CacheCLI
|
|
34
|
+
|
|
22
35
|
# desc "pick", "Interactively cherry-pick and upgrade gems"
|
|
23
36
|
# option :gem, type: :string, desc: "Gem name to cherry-pick"
|
|
24
37
|
# def pick
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require_relative "command"
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
module Commands
|
|
5
|
+
class Cache < Command
|
|
6
|
+
def flush
|
|
7
|
+
removed_entries = Gemstar::Cache.flush!
|
|
8
|
+
puts "Flushed #{removed_entries} cache entr#{removed_entries == 1 ? 'y' : 'ies'} from #{Gemstar::Cache::CACHE_DIR}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
4
|
require "concurrent-ruby"
|
|
5
|
-
require "
|
|
5
|
+
require "tmpdir"
|
|
6
6
|
|
|
7
7
|
module Gemstar
|
|
8
8
|
module Commands
|
|
@@ -15,6 +15,7 @@ module Gemstar
|
|
|
15
15
|
attr_reader :git_repo
|
|
16
16
|
attr_reader :lockfile_full_path
|
|
17
17
|
attr_reader :output_file
|
|
18
|
+
attr_reader :output_format
|
|
18
19
|
|
|
19
20
|
def initialize(options)
|
|
20
21
|
super
|
|
@@ -24,7 +25,8 @@ module Gemstar
|
|
|
24
25
|
@from = options[:from] || "HEAD"
|
|
25
26
|
@to = options[:to]
|
|
26
27
|
@lockfile = options[:lockfile] || "Gemfile.lock"
|
|
27
|
-
@
|
|
28
|
+
@output_format = normalize_output_format(options[:format] || options[:output_format])
|
|
29
|
+
@output_file = options[:output_file] || default_output_file
|
|
28
30
|
|
|
29
31
|
@git_repo = Gemstar::GitRepo.new(File.dirname(@lockfile))
|
|
30
32
|
end
|
|
@@ -44,9 +46,9 @@ module Gemstar
|
|
|
44
46
|
|
|
45
47
|
collect_updates(new_lockfile: new, old_lockfile: old)
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
File.write(output_file,
|
|
49
|
-
puts "✅
|
|
49
|
+
rendered_output = output_renderer.render_diff(self)
|
|
50
|
+
File.write(output_file, rendered_output)
|
|
51
|
+
puts "✅ Changelog report created: #{File.expand_path(output_file)}"
|
|
50
52
|
|
|
51
53
|
if failed.any?
|
|
52
54
|
puts "\n⚠️ The following gems failed to process:"
|
|
@@ -56,6 +58,27 @@ module Gemstar
|
|
|
56
58
|
|
|
57
59
|
private
|
|
58
60
|
|
|
61
|
+
def normalize_output_format(value)
|
|
62
|
+
format = value.to_s.strip.downcase
|
|
63
|
+
return :markdown if %w[md markdown].include?(format)
|
|
64
|
+
|
|
65
|
+
:html
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def default_output_file
|
|
69
|
+
extension = output_format == :markdown ? "md" : "html"
|
|
70
|
+
File.join(Dir.tmpdir, "gem_update_changelog.#{extension}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def output_renderer
|
|
74
|
+
@output_renderer ||= case output_format
|
|
75
|
+
when :markdown
|
|
76
|
+
Outputs::Markdown.new
|
|
77
|
+
else
|
|
78
|
+
Outputs::HTML.new
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
59
82
|
def build_entry(gem_name:, old_version:, new_version:)
|
|
60
83
|
metadata = Gemstar::RubyGemsMetadata.new(gem_name)
|
|
61
84
|
repo_url = metadata.repo_uri
|