gemstar 0.0.1 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -13
- data/README.md +28 -13
- 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 +105 -128
- data/lib/gemstar/cli.rb +12 -0
- data/lib/gemstar/commands/cache.rb +12 -0
- data/lib/gemstar/commands/diff.rb +3 -3
- 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/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 +6 -1
- metadata +70 -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: 65596efbc0ff5c10ad950ca3cc8bb19e9ca9030db58b73d046ebce447200aa07
|
|
4
|
+
data.tar.gz: e5ab0cbb7f3db4b3d9949011f3b6446aecb725ee1863fd61dd196660332606de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef75cdf526345f289eff5f0a8bff44520eb1d0aea55eff617de71d585832721655fd70c680e0e27ef7584c15dd4f304d9ee3b079c70fbafc7e4ccc5c8360c4ad
|
|
7
|
+
data.tar.gz: 13e3d871e1772c7f8ec183a3801504123896e8bd5365bba2a3e6fae5668415dcf2a40ef29f3494a2e9bef47251a3b913c37ae122879c3e8367cba9cf4490af1e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
##
|
|
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 ```
|
|
3
|
+
## Unreleased
|
|
15
4
|
|
|
16
|
-
|
|
5
|
+
- Added `gemstar server`, your interactive Gemfile.lock explorer and more.
|
|
6
|
+
- Default location for `diff` is now a tmp file.
|
|
7
|
+
- Removed Railtie from this gem.
|
|
8
|
+
- Improve how git root dir is determined.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## 0.0.2
|
|
12
|
+
|
|
13
|
+
- Diff: Fix regex warnings shown in terminal.
|
|
14
|
+
- Diff: Simplify and fix change log section parsing.
|
|
15
|
+
|
|
16
|
+
## 0.0.1
|
|
17
17
|
|
|
18
18
|
- Initial release
|
|
19
19
|
- Add GEMSTAR_DEBUG_GEM_REGEX to debug specific gems.
|
data/README.md
CHANGED
|
@@ -7,32 +7,41 @@ A very preliminary gem to help you keep track of your gems.
|
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The easiest way to install gemstar is to use Bundler:
|
|
11
11
|
|
|
12
12
|
```shell
|
|
13
13
|
# Shell
|
|
14
|
-
gem install
|
|
15
|
-
gem specific_install -l https://github.com/FDj/gemstar.git
|
|
14
|
+
gem install gemstar
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Alternatively, add it to the development group in your Gemfile:
|
|
19
18
|
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
group :development do
|
|
23
|
-
gem "gemstar", github: "FDj/gemstar"
|
|
24
|
-
end
|
|
19
|
+
```
|
|
20
|
+
gem "gemstar", group: :development
|
|
25
21
|
```
|
|
26
22
|
|
|
27
23
|
## Usage
|
|
28
24
|
|
|
29
|
-
###
|
|
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
|
+
|
|
38
|
+
### gemstar diff
|
|
30
39
|
|
|
31
40
|
Run this after you've updated your gems.
|
|
32
41
|
|
|
33
42
|
```shell
|
|
34
|
-
# in your project directory:
|
|
35
|
-
|
|
43
|
+
# in your project directory, after bundle update:
|
|
44
|
+
gemstar diff
|
|
36
45
|
```
|
|
37
46
|
|
|
38
47
|
This will generate an html diff report with changelog entries for each gem that was updated:
|
|
@@ -42,7 +51,13 @@ This will generate an html diff report with changelog entries for each gem that
|
|
|
42
51
|
You can also specify from and to hashes or tags to generate a diff report for a specific range of commits:
|
|
43
52
|
|
|
44
53
|
```shell
|
|
45
|
-
|
|
54
|
+
gemstar diff --from 8e3aa96b7027834cdbabc0d8cbd5f9455165e930 --to HEAD
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To examine a specific Gemfile.lock, pass it like this:
|
|
58
|
+
|
|
59
|
+
```shell
|
|
60
|
+
gemstar diff --lockfile=~/MyProject/Gemfile.lock
|
|
46
61
|
```
|
|
47
62
|
|
|
48
63
|
## Contributing
|
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
|
-
|
|
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)
|
|
20
26
|
if s.nil? || s.empty?
|
|
21
|
-
s = parse_github_release_sections
|
|
27
|
+
s = parse_github_release_sections(cache_only: cache_only)
|
|
22
28
|
end
|
|
23
29
|
|
|
24
|
-
pp @@candidates_found if Gemstar.debug?
|
|
30
|
+
pp @@candidates_found if Gemstar.debug? && !cache_only
|
|
25
31
|
|
|
26
32
|
s
|
|
27
33
|
end
|
|
34
|
+
|
|
35
|
+
@sections = result unless cache_only
|
|
36
|
+
result
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
def extract_relevant_sections(old_version, new_version)
|
|
@@ -52,23 +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),
|
|
67
|
+
# so we don't capture dates like (2025-11-21).
|
|
68
|
+
return $1 if heading[/\(\s*v?#{version_token}(?![A-Za-z0-9])\s*\)/]
|
|
57
69
|
# 2) Version-first with optional leading markers/labels: "## v1.2.6 - 2025-10-21"
|
|
58
|
-
|
|
70
|
+
# Require a dot in the numeric token to avoid capturing dates like 2025-11-21.
|
|
71
|
+
return $1 if heading[/^\s*(?:[-*]\s+)?(?:#+|=+)?\s*(?:Version\s+)?\[*v?#{version_token}(?![A-Za-z0-9])\]*/i]
|
|
59
72
|
# 3) Anywhere: first semver-like token with a dot
|
|
60
|
-
return $1 if heading[/\bv
|
|
73
|
+
return $1 if heading[/\bv?#{version_token}(?![A-Za-z0-9])\b/]
|
|
61
74
|
nil
|
|
62
75
|
end
|
|
63
76
|
|
|
64
|
-
def changelog_uri_candidates
|
|
77
|
+
def changelog_uri_candidates(cache_only: false)
|
|
65
78
|
candidates = []
|
|
66
79
|
|
|
67
|
-
|
|
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}
|
|
68
84
|
base = "https://raw.githubusercontent.com/aws/aws-sdk-ruby/refs/heads/version-3/gems/#{@metadata.gem_name}"
|
|
69
85
|
aws_style = true
|
|
70
86
|
else
|
|
71
|
-
base =
|
|
87
|
+
base = repo_uri.sub("https://github.com", "https://raw.githubusercontent.com")
|
|
72
88
|
aws_style = false
|
|
73
89
|
end
|
|
74
90
|
|
|
@@ -91,7 +107,8 @@ module Gemstar
|
|
|
91
107
|
end
|
|
92
108
|
|
|
93
109
|
# Add the gem's changelog_uri last as it's usually not the most parsable:
|
|
94
|
-
|
|
110
|
+
meta = @metadata.meta(cache_only: cache_only)
|
|
111
|
+
candidates += [Gemstar::GitHub::github_blob_to_raw(meta["changelog_uri"])] if meta
|
|
95
112
|
|
|
96
113
|
candidates.flatten!
|
|
97
114
|
candidates.uniq!
|
|
@@ -100,15 +117,19 @@ module Gemstar
|
|
|
100
117
|
candidates
|
|
101
118
|
end
|
|
102
119
|
|
|
103
|
-
def fetch_changelog_content
|
|
120
|
+
def fetch_changelog_content(cache_only: false)
|
|
104
121
|
content = nil
|
|
105
122
|
|
|
106
|
-
changelog_uri_candidates.find do |candidate|
|
|
107
|
-
content =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
112
133
|
end
|
|
113
134
|
|
|
114
135
|
# puts "fetch_changelog_content #{candidate}:\n#{content}" if Gemstar.debug?
|
|
@@ -123,12 +144,19 @@ module Gemstar
|
|
|
123
144
|
content
|
|
124
145
|
end
|
|
125
146
|
|
|
147
|
+
VERSION_PATTERNS = [
|
|
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
|
|
153
|
+
]
|
|
126
154
|
|
|
127
|
-
def parse_changelog_sections
|
|
155
|
+
def parse_changelog_sections(cache_only: false)
|
|
128
156
|
# If the fetched content looks like a GitHub Releases HTML page, return {}
|
|
129
157
|
# so that the GitHub releases scraper can handle it. This avoids
|
|
130
158
|
# accidentally parsing HTML from /releases pages as a markdown changelog.
|
|
131
|
-
c = content
|
|
159
|
+
c = content(cache_only: cache_only)
|
|
132
160
|
return {} if c.nil? || c.strip.empty?
|
|
133
161
|
if (c.include?("<html") || c.include?("<!DOCTYPE html")) &&
|
|
134
162
|
(c.include?('data-test-selector="body-content"') || c.include?("/releases/tag/"))
|
|
@@ -136,108 +164,44 @@ module Gemstar
|
|
|
136
164
|
return {}
|
|
137
165
|
end
|
|
138
166
|
|
|
167
|
+
lines = c.lines
|
|
168
|
+
|
|
169
|
+
if lines.count < 4
|
|
170
|
+
# Skip changelog files that are too short to be useful
|
|
171
|
+
# This is sometimes the case with changelogs just saying "please see GitHub releases"
|
|
172
|
+
puts "parse_changelog_sections #{@metadata.gem_name}: Changelog too short; skipping" if Gemstar.debug?
|
|
173
|
+
return {}
|
|
174
|
+
end
|
|
175
|
+
|
|
139
176
|
sections = {}
|
|
140
177
|
current_key = nil
|
|
141
178
|
current_lines = []
|
|
142
179
|
|
|
143
|
-
|
|
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|
|
|
180
|
+
lines.each do |line|
|
|
160
181
|
# Convert rdoc to markdown:
|
|
161
182
|
line = line.gsub(/^=+/) { |m| "#" * m.length }
|
|
162
183
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if
|
|
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)"
|
|
184
|
+
m = VERSION_PATTERNS.lazy.map { |re| line.match(re) }.find(&:itself)
|
|
185
|
+
|
|
186
|
+
if m
|
|
177
187
|
new_key = extract_version_from_heading(line) || $1
|
|
178
|
-
end
|
|
179
188
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
if current_key
|
|
190
|
+
sections[current_key] ||= []
|
|
191
|
+
sections[current_key] << current_lines
|
|
192
|
+
current_lines = []
|
|
193
|
+
end
|
|
194
|
+
|
|
183
195
|
current_key = new_key
|
|
184
|
-
current_lines = [line]
|
|
185
|
-
elsif current_key
|
|
186
|
-
current_lines << line
|
|
187
196
|
end
|
|
188
|
-
end
|
|
189
197
|
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
current_lines << line if current_key
|
|
199
|
+
end
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
sections
|
|
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?
|
|
201
|
+
if current_key
|
|
202
|
+
# Flush last section
|
|
203
|
+
sections[current_key] ||= []
|
|
204
|
+
sections[current_key] << current_lines
|
|
241
205
|
end
|
|
242
206
|
|
|
243
207
|
if Gemstar.debug?
|
|
@@ -248,34 +212,47 @@ module Gemstar
|
|
|
248
212
|
sections
|
|
249
213
|
end
|
|
250
214
|
|
|
251
|
-
def parse_github_release_sections
|
|
215
|
+
def parse_github_release_sections(cache_only: false)
|
|
252
216
|
begin
|
|
253
217
|
require "nokogiri"
|
|
254
218
|
rescue LoadError
|
|
255
219
|
return {}
|
|
256
220
|
end
|
|
257
221
|
|
|
258
|
-
|
|
222
|
+
repo_uri = @metadata&.repo_uri(cache_only: cache_only)
|
|
223
|
+
return {} unless repo_uri&.include?("github.com")
|
|
259
224
|
|
|
260
|
-
url = github_releases_url
|
|
225
|
+
url = github_releases_url(repo_uri)
|
|
261
226
|
return {} unless url
|
|
262
227
|
|
|
263
|
-
html =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
269
246
|
end
|
|
270
247
|
end
|
|
271
248
|
|
|
272
249
|
return {} if html.nil? || html.strip.empty?
|
|
273
250
|
|
|
274
251
|
doc = begin
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
252
|
+
Nokogiri::HTML5(html)
|
|
253
|
+
rescue => _
|
|
254
|
+
Nokogiri::HTML(html)
|
|
255
|
+
end
|
|
279
256
|
|
|
280
257
|
sections = {}
|
|
281
258
|
|
|
@@ -325,9 +302,9 @@ module Gemstar
|
|
|
325
302
|
sections
|
|
326
303
|
end
|
|
327
304
|
|
|
328
|
-
def github_releases_url
|
|
329
|
-
return nil unless
|
|
330
|
-
repo =
|
|
305
|
+
def github_releases_url(repo_uri = @metadata&.repo_uri)
|
|
306
|
+
return nil unless repo_uri
|
|
307
|
+
repo = repo_uri.chomp("/")
|
|
331
308
|
return nil if repo.empty?
|
|
332
309
|
"#{repo}/releases"
|
|
333
310
|
end
|