gemstar 1.1 → 1.2
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 +14 -0
- data/lib/gemstar/cache_warmer.rb +15 -3
- data/lib/gemstar/change_log.rb +357 -16
- data/lib/gemstar/commands/server.rb +67 -3
- data/lib/gemstar/git_hub.rb +3 -2
- data/lib/gemstar/npm_metadata.rb +2 -2
- data/lib/gemstar/ruby_gems_metadata.rb +43 -2
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +99 -33
- data/lib/gemstar/web/templates/app.css +19 -0
- data/lib/gemstar/web/templates/app.js.erb +33 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6157dce9498afdbda3d014af6e1e4e20e7477d73bd3b12129c0d754df28f4f7
|
|
4
|
+
data.tar.gz: c7fcc69688c21fea79ead9b9f762d026a5b57e5c597206171fe6faaeb5555db1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89b29cc2ff749b326e1e8dfd2cc98279b45d05a961b1d1aab8ea0cfcc9e4c2acab238fbaf40c4b947b0c9201a6254a80beb5102aa8c28c348d1afde67ced305a
|
|
7
|
+
data.tar.gz: 55182c82a720bd6a5baf9ab26e7369df6c834c24ae959b05301a40da16ac0e447a6ada98ac2461064ba137f9e944191026c48084d1ec2f95e923c0cda6b19265
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 1.2
|
|
4
|
+
|
|
5
|
+
- Server: Cache warmer now pre-fetches package detail pages so browsing warmed gems is faster.
|
|
6
|
+
- Server: Manual browser reloads now refresh the selected package details, including metadata and release notes caches.
|
|
7
|
+
- Server: Fetch GitHub release bodies through the GitHub Releases API before falling back to HTML scraping.
|
|
8
|
+
- Server: Add explicit "Use GitHub CLI" fallback for GitHub-backed release notes when automatic release discovery misses a page.
|
|
9
|
+
|
|
10
|
+
## 1.1.1
|
|
11
|
+
|
|
12
|
+
- Server: Show release dates for changelog entries.
|
|
13
|
+
- Fix GitHub release discovery with direct tag-page fallback and paginated GitHub tags support.
|
|
14
|
+
- Add changelog_uri fallback for GitHub repos with no changelog_uri metadata.
|
|
15
|
+
- Fix change log display for aws, herb, and pagy gems.
|
|
16
|
+
|
|
3
17
|
## 1.1
|
|
4
18
|
|
|
5
19
|
- Server: Add **JavaScript package support** to browse change logs for your project's packages. This currently
|
data/lib/gemstar/cache_warmer.rb
CHANGED
|
@@ -5,10 +5,11 @@ module Gemstar
|
|
|
5
5
|
class CacheWarmer
|
|
6
6
|
DEFAULT_THREADS = 10
|
|
7
7
|
|
|
8
|
-
def initialize(io: $stderr, debug: false, thread_count: DEFAULT_THREADS)
|
|
8
|
+
def initialize(io: $stderr, debug: false, thread_count: DEFAULT_THREADS, detail_cache_fetcher: nil)
|
|
9
9
|
@io = io
|
|
10
10
|
@debug = debug
|
|
11
11
|
@thread_count = thread_count
|
|
12
|
+
@detail_cache_fetcher = detail_cache_fetcher
|
|
12
13
|
@mutex = Mutex.new
|
|
13
14
|
@condition = ConditionVariable.new
|
|
14
15
|
@queue = []
|
|
@@ -21,6 +22,8 @@ module Gemstar
|
|
|
21
22
|
@completed_count = 0
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
attr_writer :detail_cache_fetcher
|
|
26
|
+
|
|
24
27
|
def enqueue_many(package_states)
|
|
25
28
|
states = normalize_package_states(package_states)
|
|
26
29
|
|
|
@@ -120,13 +123,22 @@ module Gemstar
|
|
|
120
123
|
|
|
121
124
|
def warm_cache_for_package(package_state)
|
|
122
125
|
metadata = metadata_adapter_for(package_state)
|
|
123
|
-
|
|
126
|
+
metadata&.warm_cache(versions: package_versions(package_state))
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
warm_detail_cache_for_package(package_state)
|
|
126
129
|
rescue StandardError => e
|
|
127
130
|
log "Cache refresh failed for #{package_label(package_state)}: #{e.class}: #{e.message}"
|
|
128
131
|
end
|
|
129
132
|
|
|
133
|
+
def warm_detail_cache_for_package(package_state)
|
|
134
|
+
fetcher = @detail_cache_fetcher
|
|
135
|
+
return unless fetcher
|
|
136
|
+
|
|
137
|
+
Array(package_state[:detail_cache_contexts]).each do |context|
|
|
138
|
+
fetcher.call(package_state, context)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
130
142
|
def log_progress(package_name, current)
|
|
131
143
|
return unless @debug
|
|
132
144
|
return unless current <= 5 || (current % 25).zero?
|
data/lib/gemstar/change_log.rb
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "cgi"
|
|
3
|
+
require "date"
|
|
3
4
|
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "time"
|
|
7
|
+
require "timeout"
|
|
8
|
+
require "uri"
|
|
9
|
+
require_relative "cache"
|
|
4
10
|
|
|
5
11
|
module Gemstar
|
|
6
12
|
class ChangeLog
|
|
7
13
|
@@candidates_found = Hash.new(0)
|
|
14
|
+
GITHUB_CLI_TIMEOUT = 8
|
|
8
15
|
DEFAULT_CHANGELOG_PATHS = %w[
|
|
9
16
|
CHANGELOG.md releases.md CHANGES.md
|
|
10
17
|
Changelog.md changelog.md ChangeLog.md
|
|
@@ -31,7 +38,7 @@ module Gemstar
|
|
|
31
38
|
return @sections if !cache_only && defined?(@sections) && !force_refresh
|
|
32
39
|
|
|
33
40
|
metadata_key = @metadata.respond_to?(:cache_key) ? @metadata.cache_key : @metadata.gem_name
|
|
34
|
-
cache_key = "sections-
|
|
41
|
+
cache_key = "sections-v5-#{metadata_key}"
|
|
35
42
|
serialized = if cache_only
|
|
36
43
|
Cache.peek(cache_key)
|
|
37
44
|
else
|
|
@@ -52,7 +59,7 @@ module Gemstar
|
|
|
52
59
|
result
|
|
53
60
|
end
|
|
54
61
|
|
|
55
|
-
def sections_for_versions(versions, cache_only: false, force_refresh: false)
|
|
62
|
+
def sections_for_versions(versions, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
56
63
|
requested_versions = normalize_requested_versions(versions)
|
|
57
64
|
return {} if requested_versions.empty?
|
|
58
65
|
|
|
@@ -73,7 +80,8 @@ module Gemstar
|
|
|
73
80
|
repo_uri,
|
|
74
81
|
version,
|
|
75
82
|
cache_only: false,
|
|
76
|
-
force_refresh: force_refresh
|
|
83
|
+
force_refresh: force_refresh,
|
|
84
|
+
use_github_cli: use_github_cli
|
|
77
85
|
)
|
|
78
86
|
result.merge!(specific_release) if specific_release
|
|
79
87
|
end
|
|
@@ -82,6 +90,31 @@ module Gemstar
|
|
|
82
90
|
result
|
|
83
91
|
end
|
|
84
92
|
|
|
93
|
+
def release_dates(versions: nil, cache_only: false, force_refresh: false)
|
|
94
|
+
requested_versions = normalize_requested_versions(versions)
|
|
95
|
+
metadata_key = @metadata.respond_to?(:cache_key) ? @metadata.cache_key : @metadata.gem_name
|
|
96
|
+
cache_key = "release-dates-v2-#{metadata_key}"
|
|
97
|
+
serialized = if cache_only
|
|
98
|
+
Cache.peek(cache_key)
|
|
99
|
+
else
|
|
100
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
101
|
+
JSON.generate(compute_release_dates(force_refresh: force_refresh))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
dates = if serialized
|
|
106
|
+
decode_sections(serialized) || {}
|
|
107
|
+
elsif cache_only
|
|
108
|
+
{}
|
|
109
|
+
else
|
|
110
|
+
compute_release_dates(force_refresh: force_refresh)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
return dates if requested_versions.empty?
|
|
114
|
+
|
|
115
|
+
dates.select { |version, _date| requested_versions.include?(normalize_version_key(version)) }
|
|
116
|
+
end
|
|
117
|
+
|
|
85
118
|
def compute_sections(force_refresh: false)
|
|
86
119
|
changelog_sections = parse_changelog_sections(cache_only: false, force_refresh: force_refresh) || {}
|
|
87
120
|
github_sections = parse_github_release_sections(cache_only: false, force_refresh: force_refresh) || {}
|
|
@@ -93,6 +126,23 @@ module Gemstar
|
|
|
93
126
|
sections
|
|
94
127
|
end
|
|
95
128
|
|
|
129
|
+
def compute_release_dates(force_refresh: false)
|
|
130
|
+
registry_dates = if @metadata.respond_to?(:registry_release_dates)
|
|
131
|
+
@metadata.registry_release_dates(cache_only: false, force_refresh: force_refresh)
|
|
132
|
+
else
|
|
133
|
+
{}
|
|
134
|
+
end
|
|
135
|
+
return registry_dates unless registry_dates.nil? || registry_dates.empty?
|
|
136
|
+
|
|
137
|
+
changelog_dates = parse_changelog_release_dates(cache_only: false, force_refresh: force_refresh)
|
|
138
|
+
return changelog_dates unless changelog_dates.nil? || changelog_dates.empty?
|
|
139
|
+
|
|
140
|
+
repo_uri = @metadata&.repo_uri(cache_only: false, force_refresh: force_refresh)
|
|
141
|
+
return {} unless repo_uri&.include?("github.com")
|
|
142
|
+
|
|
143
|
+
parse_github_tag_dates(repo_uri, cache_only: false, force_refresh: force_refresh)
|
|
144
|
+
end
|
|
145
|
+
|
|
96
146
|
def decode_sections(serialized)
|
|
97
147
|
JSON.parse(serialized)
|
|
98
148
|
rescue JSON::ParserError
|
|
@@ -101,9 +151,7 @@ module Gemstar
|
|
|
101
151
|
|
|
102
152
|
def merge_section_sources(changelog_sections, github_sections)
|
|
103
153
|
return github_sections if changelog_sections.nil? || changelog_sections.empty?
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
github_sections.merge(changelog_sections)
|
|
154
|
+
changelog_sections
|
|
107
155
|
end
|
|
108
156
|
|
|
109
157
|
def extract_relevant_sections(old_version, new_version)
|
|
@@ -155,12 +203,22 @@ module Gemstar
|
|
|
155
203
|
nil
|
|
156
204
|
end
|
|
157
205
|
|
|
206
|
+
def extract_release_date_from_heading(line)
|
|
207
|
+
return nil unless line
|
|
208
|
+
|
|
209
|
+
raw_date = line.to_s[/\b(\d{4}-\d{2}-\d{2})\b/, 1]
|
|
210
|
+
format_release_date(raw_date)
|
|
211
|
+
end
|
|
212
|
+
|
|
158
213
|
def changelog_uri_candidates(cache_only: false, force_refresh: false)
|
|
159
214
|
candidates = []
|
|
160
215
|
|
|
161
216
|
repo_uri = @metadata.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
162
217
|
return [] if repo_uri.nil? || repo_uri.empty?
|
|
163
218
|
|
|
219
|
+
meta = @metadata.meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
220
|
+
candidates += changelog_uri_markdown_candidates(meta["changelog_uri"]) if meta
|
|
221
|
+
|
|
164
222
|
changelog_source = metadata_changelog_source(repo_uri, cache_only: cache_only, force_refresh: force_refresh)
|
|
165
223
|
return [] unless changelog_source
|
|
166
224
|
|
|
@@ -169,7 +227,6 @@ module Gemstar
|
|
|
169
227
|
end
|
|
170
228
|
|
|
171
229
|
# Add the gem's changelog_uri last as it's usually not the most parsable:
|
|
172
|
-
meta = @metadata.meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
173
230
|
candidates += [Gemstar::GitHub::github_blob_to_raw(meta["changelog_uri"])] if meta
|
|
174
231
|
|
|
175
232
|
candidates.flatten!
|
|
@@ -179,6 +236,30 @@ module Gemstar
|
|
|
179
236
|
candidates
|
|
180
237
|
end
|
|
181
238
|
|
|
239
|
+
def changelog_uri_markdown_candidates(changelog_uri)
|
|
240
|
+
raw_uri = Gemstar::GitHub::github_blob_to_raw(changelog_uri)
|
|
241
|
+
return [] if raw_uri.to_s.empty?
|
|
242
|
+
|
|
243
|
+
candidates = []
|
|
244
|
+
candidates << raw_uri if raw_uri.match?(/\.(?:md|markdown|rdoc|txt)\z/i)
|
|
245
|
+
|
|
246
|
+
begin
|
|
247
|
+
uri = URI(raw_uri)
|
|
248
|
+
path = uri.path.to_s
|
|
249
|
+
if path.end_with?("/")
|
|
250
|
+
uri.path = "#{path.chomp("/")}.md"
|
|
251
|
+
candidates << uri.to_s
|
|
252
|
+
elsif File.extname(path).empty?
|
|
253
|
+
uri.path = "#{path}.md"
|
|
254
|
+
candidates << uri.to_s
|
|
255
|
+
end
|
|
256
|
+
rescue URI::InvalidURIError
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
candidates
|
|
261
|
+
end
|
|
262
|
+
|
|
182
263
|
def metadata_changelog_source(repo_uri, cache_only:, force_refresh:)
|
|
183
264
|
if @metadata.respond_to?(:changelog_source)
|
|
184
265
|
return @metadata.changelog_source(repo_uri: repo_uri, cache_only: cache_only, force_refresh: force_refresh)
|
|
@@ -287,19 +368,37 @@ module Gemstar
|
|
|
287
368
|
sections
|
|
288
369
|
end
|
|
289
370
|
|
|
290
|
-
def
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
371
|
+
def parse_changelog_release_dates(cache_only: false, force_refresh: false)
|
|
372
|
+
c = content(cache_only: cache_only, force_refresh: force_refresh)
|
|
373
|
+
return {} if c.nil? || c.strip.empty?
|
|
374
|
+
return {} if c.include?("<html") || c.include?("<!DOCTYPE html")
|
|
375
|
+
|
|
376
|
+
c.lines.each_with_object({}) do |line, dates|
|
|
377
|
+
line = line.gsub(/^=+/) { |m| "#" * m.length }
|
|
378
|
+
next unless VERSION_PATTERNS.any? { |re| line.match?(re) }
|
|
379
|
+
|
|
380
|
+
version = extract_version_from_heading(line)
|
|
381
|
+
date = extract_release_date_from_heading(line)
|
|
382
|
+
dates[version] ||= date if version && date
|
|
295
383
|
end
|
|
384
|
+
end
|
|
296
385
|
|
|
386
|
+
def parse_github_release_sections(cache_only: false, force_refresh: false)
|
|
297
387
|
repo_uri = @metadata&.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
298
388
|
return {} unless repo_uri&.include?("github.com")
|
|
299
389
|
|
|
300
390
|
url = github_releases_url(repo_uri)
|
|
301
391
|
return {} unless url
|
|
302
392
|
|
|
393
|
+
api_sections = parse_github_api_release_sections(repo_uri, cache_only: cache_only, force_refresh: force_refresh)
|
|
394
|
+
return api_sections unless api_sections.empty?
|
|
395
|
+
|
|
396
|
+
begin
|
|
397
|
+
require "nokogiri"
|
|
398
|
+
rescue LoadError
|
|
399
|
+
return {}
|
|
400
|
+
end
|
|
401
|
+
|
|
303
402
|
html = if cache_only
|
|
304
403
|
Cache.peek("releases-#{url}")
|
|
305
404
|
else
|
|
@@ -416,11 +515,90 @@ module Gemstar
|
|
|
416
515
|
"#{repo}/tags"
|
|
417
516
|
end
|
|
418
517
|
|
|
419
|
-
def
|
|
518
|
+
def github_releases_api_url(repo_uri = @metadata&.repo_uri)
|
|
519
|
+
repo_path = github_repo_path(repo_uri)
|
|
520
|
+
return nil unless repo_path
|
|
521
|
+
|
|
522
|
+
"https://api.github.com/repos/#{repo_path}/releases"
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def github_release_api_url(repo_uri, tag)
|
|
526
|
+
repo_path = github_repo_path(repo_uri)
|
|
527
|
+
return nil unless repo_path
|
|
528
|
+
|
|
529
|
+
"https://api.github.com/repos/#{repo_path}/releases/tags/#{URI.encode_www_form_component(tag)}"
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def github_repo_path(repo_uri)
|
|
533
|
+
uri = URI(repo_uri.to_s)
|
|
534
|
+
return nil unless uri.host.to_s.end_with?("github.com")
|
|
535
|
+
|
|
536
|
+
segments = uri.path.to_s.split("/").reject(&:empty?)
|
|
537
|
+
return nil if segments.length < 2
|
|
538
|
+
|
|
539
|
+
segments.take(2).join("/")
|
|
540
|
+
rescue URI::InvalidURIError
|
|
541
|
+
nil
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def parse_github_api_release_sections(repo_uri, cache_only:, force_refresh:)
|
|
545
|
+
url = github_releases_api_url(repo_uri)
|
|
546
|
+
return {} unless url
|
|
547
|
+
|
|
548
|
+
json = if cache_only
|
|
549
|
+
Cache.peek("releases-api-#{url}")
|
|
550
|
+
else
|
|
551
|
+
Cache.fetch("releases-api-#{url}", force: force_refresh) do
|
|
552
|
+
begin
|
|
553
|
+
URI.open(url, "Accept" => "application/vnd.github+json", read_timeout: 8)&.read
|
|
554
|
+
rescue => e
|
|
555
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
556
|
+
nil
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
parse_github_api_releases(json)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def parse_github_api_releases(json)
|
|
565
|
+
releases = JSON.parse(json.to_s)
|
|
566
|
+
releases = [releases] if releases.is_a?(Hash)
|
|
567
|
+
return {} unless releases.is_a?(Array)
|
|
568
|
+
|
|
569
|
+
releases.each_with_object({}) do |release, sections|
|
|
570
|
+
next unless release.is_a?(Hash)
|
|
571
|
+
|
|
572
|
+
tag_name = (release["tag_name"] || release["tagName"]).to_s
|
|
573
|
+
next if tag_name.empty?
|
|
574
|
+
next unless github_tag_matches_metadata?(tag_name)
|
|
575
|
+
|
|
576
|
+
version = normalize_github_tag_version(tag_name)
|
|
577
|
+
next if version.to_s.empty?
|
|
578
|
+
|
|
579
|
+
body = release["body"].to_s.strip
|
|
580
|
+
title = release["name"].to_s.strip
|
|
581
|
+
content = body.empty? ? title : body
|
|
582
|
+
next if content.empty?
|
|
583
|
+
|
|
584
|
+
sections[version] ||= ["## #{version}\n", content]
|
|
585
|
+
end
|
|
586
|
+
rescue JSON::ParserError
|
|
587
|
+
{}
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def parse_specific_github_release_pages(repo_uri, version, cache_only:, force_refresh:, use_github_cli: false)
|
|
420
591
|
return {} unless repo_uri&.include?("github.com")
|
|
421
592
|
return {} if version.to_s.empty?
|
|
422
593
|
|
|
423
|
-
|
|
594
|
+
github_tag_candidates(version).each do |tag|
|
|
595
|
+
cli_section = parse_specific_github_cli_release(repo_uri, tag, cache_only: cache_only, force_refresh: force_refresh) if use_github_cli
|
|
596
|
+
return cli_section if cli_section && !cli_section.empty?
|
|
597
|
+
|
|
598
|
+
api_section = parse_specific_github_api_release(repo_uri, tag, cache_only: cache_only, force_refresh: force_refresh)
|
|
599
|
+
return api_section unless api_section.empty?
|
|
600
|
+
|
|
601
|
+
url = github_release_tag_url(repo_uri, tag)
|
|
424
602
|
html = if cache_only
|
|
425
603
|
Cache.peek("releases-#{url}")
|
|
426
604
|
else
|
|
@@ -443,6 +621,62 @@ module Gemstar
|
|
|
443
621
|
{}
|
|
444
622
|
end
|
|
445
623
|
|
|
624
|
+
def parse_specific_github_cli_release(repo_uri, tag_name, cache_only:, force_refresh:)
|
|
625
|
+
repo_path = github_repo_path(repo_uri)
|
|
626
|
+
return {} unless repo_path
|
|
627
|
+
|
|
628
|
+
cache_key = "releases-gh-#{repo_path}-#{tag_name}"
|
|
629
|
+
json = if cache_only
|
|
630
|
+
Cache.peek(cache_key)
|
|
631
|
+
else
|
|
632
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
633
|
+
fetch_github_cli_release_json(repo_path, tag_name)
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
parse_github_api_releases(json)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def fetch_github_cli_release_json(repo_path, tag_name)
|
|
641
|
+
stdout, _stderr, status = nil
|
|
642
|
+
Timeout.timeout(GITHUB_CLI_TIMEOUT) do
|
|
643
|
+
stdout, _stderr, status = Open3.capture3(
|
|
644
|
+
"gh",
|
|
645
|
+
"release",
|
|
646
|
+
"view",
|
|
647
|
+
tag_name,
|
|
648
|
+
"--repo",
|
|
649
|
+
repo_path,
|
|
650
|
+
"--json",
|
|
651
|
+
"body,name,tagName"
|
|
652
|
+
)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
status.success? ? stdout : nil
|
|
656
|
+
rescue Errno::ENOENT, Timeout::Error
|
|
657
|
+
nil
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def parse_specific_github_api_release(repo_uri, tag_name, cache_only:, force_refresh:)
|
|
661
|
+
url = github_release_api_url(repo_uri, URI.decode_www_form_component(tag_name.to_s))
|
|
662
|
+
return {} unless url
|
|
663
|
+
|
|
664
|
+
json = if cache_only
|
|
665
|
+
Cache.peek("releases-api-#{url}")
|
|
666
|
+
else
|
|
667
|
+
Cache.fetch("releases-api-#{url}", force: force_refresh) do
|
|
668
|
+
begin
|
|
669
|
+
URI.open(url, "Accept" => "application/vnd.github+json", read_timeout: 8)&.read
|
|
670
|
+
rescue => e
|
|
671
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
672
|
+
nil
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
parse_github_api_releases(json)
|
|
678
|
+
end
|
|
679
|
+
|
|
446
680
|
def parse_github_tag_sections(repo_uri, cache_only:, force_refresh:)
|
|
447
681
|
return {} unless repo_uri&.include?("github.com")
|
|
448
682
|
|
|
@@ -489,6 +723,40 @@ module Gemstar
|
|
|
489
723
|
sections
|
|
490
724
|
end
|
|
491
725
|
|
|
726
|
+
def parse_github_tag_dates(repo_uri, cache_only:, force_refresh:)
|
|
727
|
+
return {} unless repo_uri&.include?("github.com")
|
|
728
|
+
|
|
729
|
+
url = github_tags_url(repo_uri)
|
|
730
|
+
return {} unless url
|
|
731
|
+
|
|
732
|
+
dates = {}
|
|
733
|
+
seen_urls = {}
|
|
734
|
+
|
|
735
|
+
while url && !seen_urls[url]
|
|
736
|
+
seen_urls[url] = true
|
|
737
|
+
html = if cache_only
|
|
738
|
+
Cache.peek("tags-#{url}")
|
|
739
|
+
else
|
|
740
|
+
Cache.fetch("tags-#{url}", force: force_refresh) do
|
|
741
|
+
begin
|
|
742
|
+
URI.open(url, read_timeout: 8)&.read
|
|
743
|
+
rescue => e
|
|
744
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
745
|
+
nil
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
break if html.nil? || html.strip.empty?
|
|
751
|
+
|
|
752
|
+
page_dates, next_url = parse_single_github_tag_dates_page(html, repo_uri)
|
|
753
|
+
dates.merge!(page_dates) { |_version, existing, _new| existing }
|
|
754
|
+
url = next_url
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
dates
|
|
758
|
+
end
|
|
759
|
+
|
|
492
760
|
def parse_single_github_tags_page(html, repo_uri)
|
|
493
761
|
require "nokogiri"
|
|
494
762
|
|
|
@@ -539,6 +807,75 @@ module Gemstar
|
|
|
539
807
|
[{}, nil]
|
|
540
808
|
end
|
|
541
809
|
|
|
810
|
+
def parse_single_github_tag_dates_page(html, repo_uri)
|
|
811
|
+
require "nokogiri"
|
|
812
|
+
|
|
813
|
+
doc = begin
|
|
814
|
+
Nokogiri::HTML5(html)
|
|
815
|
+
rescue => _
|
|
816
|
+
Nokogiri::HTML(html)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
dates = {}
|
|
820
|
+
repo_path = URI(repo_uri).path
|
|
821
|
+
release_prefix = "#{repo_path}/releases/tag/"
|
|
822
|
+
tree_prefix = "#{repo_path}/tree/"
|
|
823
|
+
|
|
824
|
+
doc.css("a[href]").each do |link|
|
|
825
|
+
href = link["href"].to_s
|
|
826
|
+
tag_name =
|
|
827
|
+
if href.start_with?(release_prefix)
|
|
828
|
+
href.delete_prefix(release_prefix)
|
|
829
|
+
elsif href.start_with?(tree_prefix)
|
|
830
|
+
href.delete_prefix(tree_prefix)
|
|
831
|
+
end
|
|
832
|
+
next if tag_name.to_s.empty?
|
|
833
|
+
next unless github_tag_matches_metadata?(tag_name)
|
|
834
|
+
|
|
835
|
+
version = normalize_github_tag_version(tag_name)
|
|
836
|
+
next if version.to_s.empty?
|
|
837
|
+
|
|
838
|
+
datetime = github_tag_datetime_for(link)
|
|
839
|
+
next if datetime.to_s.empty?
|
|
840
|
+
|
|
841
|
+
dates[version] ||= format_release_date(datetime)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
next_href =
|
|
845
|
+
doc.at_css('a[rel="next"], a.next_page')&.[]("href") ||
|
|
846
|
+
doc.css("a[href]").find do |link|
|
|
847
|
+
href = link["href"].to_s
|
|
848
|
+
text = link.text.to_s.gsub(/\s+/, " ").strip
|
|
849
|
+
href.include?("/tags?after=") && text == "Next"
|
|
850
|
+
end&.[]("href")
|
|
851
|
+
next_url = if next_href && !next_href.empty?
|
|
852
|
+
URI.join(repo_uri, next_href).to_s
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
[dates.compact, next_url]
|
|
856
|
+
rescue LoadError
|
|
857
|
+
[{}, nil]
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def github_tag_datetime_for(link)
|
|
861
|
+
container = link.at_xpath('ancestor::*[self::li or self::div][.//relative-time or .//time-ago][1]')
|
|
862
|
+
time_node = container&.at_css("relative-time[datetime], time-ago[datetime]") ||
|
|
863
|
+
link.xpath('following::relative-time[@datetime] | following::time-ago[@datetime]').first
|
|
864
|
+
time_node&.[]("datetime")
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def format_release_date(datetime)
|
|
868
|
+
if datetime.to_s.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
869
|
+
date = Date.strptime(datetime.to_s, "%Y-%m-%d")
|
|
870
|
+
return date.strftime("%b #{date.day}, %Y")
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
time = Time.parse(datetime.to_s).utc
|
|
874
|
+
time.strftime("%b #{time.day}, %Y")
|
|
875
|
+
rescue ArgumentError
|
|
876
|
+
nil
|
|
877
|
+
end
|
|
878
|
+
|
|
542
879
|
def parse_single_github_release_page(html, version)
|
|
543
880
|
require "nokogiri"
|
|
544
881
|
|
|
@@ -567,11 +904,15 @@ module Gemstar
|
|
|
567
904
|
|
|
568
905
|
def github_release_tag_urls(repo_url, version)
|
|
569
906
|
github_tag_candidates(version).map do |tag|
|
|
570
|
-
|
|
571
|
-
"#{repo_url}/releases/tag/#{encoded_tag}"
|
|
907
|
+
github_release_tag_url(repo_url, tag)
|
|
572
908
|
end.uniq
|
|
573
909
|
end
|
|
574
910
|
|
|
911
|
+
def github_release_tag_url(repo_url, tag)
|
|
912
|
+
encoded_tag = URI.encode_www_form_component(tag)
|
|
913
|
+
"#{repo_url}/releases/tag/#{encoded_tag}"
|
|
914
|
+
end
|
|
915
|
+
|
|
575
916
|
def github_tag_candidates(version)
|
|
576
917
|
return @metadata.github_tag_candidates(version) if @metadata.respond_to?(:github_tag_candidates)
|
|
577
918
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require_relative "command"
|
|
2
|
+
require "rack/mock"
|
|
2
3
|
require "socket"
|
|
3
4
|
require "shellwords"
|
|
5
|
+
require "uri"
|
|
4
6
|
require "rbconfig"
|
|
5
7
|
|
|
6
8
|
module Gemstar
|
|
@@ -45,7 +47,9 @@ module Gemstar
|
|
|
45
47
|
projects = load_projects
|
|
46
48
|
log_loaded_projects(projects)
|
|
47
49
|
cache_warmer = build_cache_warmer
|
|
48
|
-
|
|
50
|
+
web_app = Gemstar::Web::App.build(projects: projects, config_home: Gemstar::Config.home_directory, cache_warmer: cache_warmer)
|
|
51
|
+
cache_warmer.detail_cache_fetcher = build_detail_cache_fetcher(web_app)
|
|
52
|
+
app = web_app
|
|
49
53
|
app = Gemstar::RequestLogger.new(app, io: $stderr) if debug_request_logging?
|
|
50
54
|
|
|
51
55
|
puts "Gemstar server listening on http://#{bind}:#{port}"
|
|
@@ -183,8 +187,21 @@ module Gemstar
|
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
def start_background_cache_refresh(projects, cache_warmer)
|
|
186
|
-
package_states = projects.flat_map do |project|
|
|
187
|
-
|
|
190
|
+
package_states = projects.flat_map.with_index do |project, project_index|
|
|
191
|
+
from_revision_id = project.default_from_revision_id
|
|
192
|
+
to_revision_id = "worktree"
|
|
193
|
+
states = project.gem_states(from_revision_id: from_revision_id, to_revision_id: to_revision_id)
|
|
194
|
+
detail_cache_contexts = detail_cache_contexts_for(
|
|
195
|
+
project: project,
|
|
196
|
+
project_index: project_index,
|
|
197
|
+
from_revision_id: from_revision_id,
|
|
198
|
+
to_revision_id: to_revision_id,
|
|
199
|
+
package_states: states
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
states.map do |state|
|
|
203
|
+
state.merge(detail_cache_contexts: detail_cache_contexts_for_package(state, detail_cache_contexts))
|
|
204
|
+
end
|
|
188
205
|
end
|
|
189
206
|
|
|
190
207
|
return nil if package_states.empty?
|
|
@@ -192,6 +209,53 @@ module Gemstar
|
|
|
192
209
|
cache_warmer.enqueue_many(package_states)
|
|
193
210
|
end
|
|
194
211
|
|
|
212
|
+
def detail_cache_contexts_for(project:, project_index:, from_revision_id:, to_revision_id:, package_states:)
|
|
213
|
+
default_filter = package_states.any? { |state| state[:status] != :unchanged } ? "updated" : "all"
|
|
214
|
+
default_scope = project.package_scope_options.map { |option| option[:id] } == ["gems"] ? "gems" : "all"
|
|
215
|
+
|
|
216
|
+
[
|
|
217
|
+
{
|
|
218
|
+
project: project_index,
|
|
219
|
+
from: from_revision_id,
|
|
220
|
+
to: to_revision_id,
|
|
221
|
+
filter: default_filter,
|
|
222
|
+
scope: default_scope
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
project: project_index,
|
|
226
|
+
from: from_revision_id,
|
|
227
|
+
to: to_revision_id,
|
|
228
|
+
filter: "all",
|
|
229
|
+
scope: default_scope
|
|
230
|
+
}
|
|
231
|
+
].uniq
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def detail_cache_contexts_for_package(package_state, base_contexts)
|
|
235
|
+
package_scope = package_state[:package_scope]
|
|
236
|
+
|
|
237
|
+
base_contexts.flat_map do |context|
|
|
238
|
+
[
|
|
239
|
+
context,
|
|
240
|
+
context.merge(scope: package_scope)
|
|
241
|
+
]
|
|
242
|
+
end.uniq
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def build_detail_cache_fetcher(app)
|
|
246
|
+
lambda do |package_state, context|
|
|
247
|
+
params = context.merge(package: package_state[:name])
|
|
248
|
+
env = Rack::MockRequest.env_for(
|
|
249
|
+
"/detail?#{URI.encode_www_form(params)}",
|
|
250
|
+
"REQUEST_METHOD" => "GET",
|
|
251
|
+
"gemstar.detail_cache_warm" => true
|
|
252
|
+
)
|
|
253
|
+
response = app.call(env)
|
|
254
|
+
body = response[2]
|
|
255
|
+
body.close if body.respond_to?(:close)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
195
259
|
def server_start_callback(projects, cache_warmer)
|
|
196
260
|
proc do
|
|
197
261
|
Thread.new do
|
data/lib/gemstar/git_hub.rb
CHANGED
|
@@ -8,11 +8,12 @@ module Gemstar
|
|
|
8
8
|
uri = URI(url)
|
|
9
9
|
return url unless uri.host == "github.com"
|
|
10
10
|
|
|
11
|
-
owner, repo,
|
|
12
|
-
return url unless blob
|
|
11
|
+
owner, repo, view, *rest = uri.path.split("/")[1..]
|
|
12
|
+
return url unless %w[blob tree].include?(view)
|
|
13
13
|
|
|
14
14
|
ref = rest.shift
|
|
15
15
|
path = rest.join("/")
|
|
16
|
+
return url if path.empty?
|
|
16
17
|
|
|
17
18
|
ref_prefix = ref_is_tag ? "refs/tags/" : ""
|
|
18
19
|
|
data/lib/gemstar/npm_metadata.rb
CHANGED
|
@@ -64,13 +64,13 @@ module Gemstar
|
|
|
64
64
|
repo
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def changelog_sections(versions: nil, cache_only: false, force_refresh: false)
|
|
67
|
+
def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
68
68
|
requested_versions = Array(versions).compact
|
|
69
69
|
changelog = Gemstar::ChangeLog.new(self)
|
|
70
70
|
if requested_versions.empty?
|
|
71
71
|
changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
72
72
|
else
|
|
73
|
-
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh)
|
|
73
|
+
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "open-uri"
|
|
2
2
|
require "uri"
|
|
3
3
|
require "json"
|
|
4
|
+
require "date"
|
|
5
|
+
require "time"
|
|
4
6
|
require_relative "remote_repository"
|
|
5
7
|
|
|
6
8
|
module Gemstar
|
|
@@ -86,8 +88,35 @@ module Gemstar
|
|
|
86
88
|
repo
|
|
87
89
|
end
|
|
88
90
|
|
|
89
|
-
def changelog_sections(versions: nil, cache_only: false, force_refresh: false)
|
|
90
|
-
Gemstar::ChangeLog.new(self)
|
|
91
|
+
def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
92
|
+
changelog = Gemstar::ChangeLog.new(self)
|
|
93
|
+
if use_github_cli && Array(versions).compact.any?
|
|
94
|
+
changelog.sections_for_versions(versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: true)
|
|
95
|
+
else
|
|
96
|
+
changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def registry_release_dates(cache_only: false, force_refresh: false)
|
|
101
|
+
cache_key = "rubygems-versions-#{gem_name}"
|
|
102
|
+
json = if cache_only
|
|
103
|
+
Cache.peek(cache_key)
|
|
104
|
+
else
|
|
105
|
+
url = "https://rubygems.org/api/v1/versions/#{URI.encode_www_form_component(gem_name)}.json"
|
|
106
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
107
|
+
URI.open(url, read_timeout: 8).read
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Array(JSON.parse(json)).each_with_object({}) do |version, dates|
|
|
112
|
+
number = version["number"].to_s
|
|
113
|
+
created_at = version["created_at"].to_s
|
|
114
|
+
next if number.empty? || created_at.empty?
|
|
115
|
+
|
|
116
|
+
dates[number] = format_registry_release_date(created_at)
|
|
117
|
+
end.compact
|
|
118
|
+
rescue JSON::ParserError
|
|
119
|
+
{}
|
|
91
120
|
end
|
|
92
121
|
|
|
93
122
|
def warm_cache(versions: nil)
|
|
@@ -146,5 +175,17 @@ module Gemstar
|
|
|
146
175
|
value.to_s.gsub("{gem_name}", gem_name)
|
|
147
176
|
end
|
|
148
177
|
|
|
178
|
+
def format_registry_release_date(datetime)
|
|
179
|
+
if datetime.to_s.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
180
|
+
date = Date.strptime(datetime.to_s, "%Y-%m-%d")
|
|
181
|
+
return date.strftime("%b #{date.day}, %Y")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
time = Time.parse(datetime.to_s).utc
|
|
185
|
+
time.strftime("%b #{time.day}, %Y")
|
|
186
|
+
rescue ArgumentError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
149
190
|
end
|
|
150
191
|
end
|
data/lib/gemstar/version.rb
CHANGED
data/lib/gemstar/web/app.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "cgi"
|
|
|
2
2
|
require "erb"
|
|
3
3
|
require "uri"
|
|
4
4
|
require "kramdown"
|
|
5
|
+
require "nokogiri"
|
|
5
6
|
require "roda"
|
|
6
7
|
|
|
7
8
|
begin
|
|
@@ -13,7 +14,7 @@ module Gemstar
|
|
|
13
14
|
module Web
|
|
14
15
|
class App < Roda
|
|
15
16
|
MISSING_METADATA = Object.new
|
|
16
|
-
CACHE_VERSION = "
|
|
17
|
+
CACHE_VERSION = "v7"
|
|
17
18
|
|
|
18
19
|
class << self
|
|
19
20
|
def build(projects:, config_home:, cache_warmer: nil)
|
|
@@ -58,13 +59,15 @@ module Gemstar
|
|
|
58
59
|
r.get "detail" do
|
|
59
60
|
request_cache_key = detail_request_cache_key(r.params)
|
|
60
61
|
request_cache = self.class.opts[:detail_request_cache]
|
|
61
|
-
|
|
62
|
+
refresh_detail = detail_refresh_requested?(r.params)
|
|
63
|
+
if request_cache_key && !refresh_detail && request_cache.key?(request_cache_key)
|
|
62
64
|
next request_cache[request_cache_key]
|
|
63
65
|
end
|
|
66
|
+
request_cache.delete(request_cache_key) if request_cache_key && refresh_detail
|
|
64
67
|
|
|
65
68
|
load_state(r.params)
|
|
66
|
-
prioritize_selected_gem
|
|
67
|
-
detail_html = render_detail
|
|
69
|
+
prioritize_selected_gem unless r.env["gemstar.detail_cache_warm"]
|
|
70
|
+
detail_html = render_detail(force_refresh: refresh_detail, use_github_cli: detail_use_github_cli_requested?(r.params))
|
|
68
71
|
request_cache[request_cache_key] = detail_html if request_cache_key
|
|
69
72
|
detail_html
|
|
70
73
|
end
|
|
@@ -130,6 +133,14 @@ module Gemstar
|
|
|
130
133
|
0
|
|
131
134
|
end
|
|
132
135
|
|
|
136
|
+
def detail_refresh_requested?(params)
|
|
137
|
+
%w[1 true yes].include?(params["refresh"].to_s.downcase)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def detail_use_github_cli_requested?(params)
|
|
141
|
+
%w[1 true yes].include?(params["use_gh"].to_s.downcase)
|
|
142
|
+
end
|
|
143
|
+
|
|
133
144
|
def page_title
|
|
134
145
|
return "Gemstar" unless @selected_project
|
|
135
146
|
|
|
@@ -479,10 +490,10 @@ module Gemstar
|
|
|
479
490
|
HTML
|
|
480
491
|
end
|
|
481
492
|
|
|
482
|
-
def render_detail
|
|
493
|
+
def render_detail(force_refresh: false, use_github_cli: false)
|
|
483
494
|
return empty_detail_html unless @selected_gem
|
|
484
495
|
|
|
485
|
-
metadata = metadata_for(@selected_gem, refresh_if_missing: true)
|
|
496
|
+
metadata = metadata_for(@selected_gem, refresh_if_missing: true, force_refresh: force_refresh)
|
|
486
497
|
cache_key = [
|
|
487
498
|
CACHE_VERSION,
|
|
488
499
|
@selected_project_index,
|
|
@@ -501,9 +512,9 @@ module Gemstar
|
|
|
501
512
|
@selected_gem[:status]
|
|
502
513
|
]
|
|
503
514
|
detail_cache = self.class.opts[:detail_html_cache]
|
|
504
|
-
return detail_cache[cache_key] if detail_cache.key?(cache_key)
|
|
515
|
+
return detail_cache[cache_key] if !force_refresh && detail_cache.key?(cache_key)
|
|
505
516
|
|
|
506
|
-
groups = grouped_change_sections(@selected_gem)
|
|
517
|
+
groups = grouped_change_sections(@selected_gem, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
507
518
|
detail_pending = detail_pending?(@selected_gem[:name], metadata, groups)
|
|
508
519
|
|
|
509
520
|
detail_html = <<~HTML
|
|
@@ -837,7 +848,10 @@ module Gemstar
|
|
|
837
848
|
<article class="revision-card revision-#{section[:kind]}#{status_class}">
|
|
838
849
|
<header class="revision-card-header">
|
|
839
850
|
<div class="revision-card-titlebar">
|
|
840
|
-
<
|
|
851
|
+
<div class="revision-card-heading">
|
|
852
|
+
<h5>#{h(section[:title] || section[:version])}</h5>
|
|
853
|
+
#{render_release_date(section[:release_date])}
|
|
854
|
+
</div>
|
|
841
855
|
<div class="revision-card-actions">
|
|
842
856
|
#{title_links.join}
|
|
843
857
|
</div>
|
|
@@ -850,8 +864,14 @@ module Gemstar
|
|
|
850
864
|
HTML
|
|
851
865
|
end
|
|
852
866
|
|
|
853
|
-
def
|
|
854
|
-
|
|
867
|
+
def render_release_date(release_date)
|
|
868
|
+
return "" if release_date.to_s.empty?
|
|
869
|
+
|
|
870
|
+
%(<span class="revision-release-date" title="Estimated release date">#{h(release_date)}</span>)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
def grouped_change_sections(gem_state, force_refresh: false, use_github_cli: false)
|
|
874
|
+
sections = change_sections(gem_state, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
855
875
|
latest = sections.select { |section| section[:kind] == :future }
|
|
856
876
|
current = sections.select { |section| section[:kind] == :current }
|
|
857
877
|
previous = sections.select { |section| section[:kind] == :previous }
|
|
@@ -868,8 +888,8 @@ module Gemstar
|
|
|
868
888
|
}
|
|
869
889
|
end
|
|
870
890
|
|
|
871
|
-
def change_sections(gem_state)
|
|
872
|
-
metadata_hash = metadata_for(gem_state) || {}
|
|
891
|
+
def change_sections(gem_state, force_refresh: false, use_github_cli: false)
|
|
892
|
+
metadata_hash = metadata_for(gem_state, refresh_if_missing: force_refresh, force_refresh: force_refresh) || {}
|
|
873
893
|
current_version = effective_package_version(gem_state, metadata_hash)
|
|
874
894
|
cache_key = [
|
|
875
895
|
CACHE_VERSION,
|
|
@@ -884,17 +904,18 @@ module Gemstar
|
|
|
884
904
|
gem_state[:status]
|
|
885
905
|
]
|
|
886
906
|
change_sections_cache = self.class.opts[:change_sections_cache]
|
|
887
|
-
return change_sections_cache[cache_key] if change_sections_cache.key?(cache_key)
|
|
907
|
+
return change_sections_cache[cache_key] if !force_refresh && change_sections_cache.key?(cache_key)
|
|
888
908
|
|
|
889
909
|
return [] if gem_state[:new_version].nil? &&
|
|
890
910
|
gem_state[:old_version].nil? &&
|
|
891
911
|
(current_version.nil? || current_version.to_s.empty?)
|
|
892
912
|
metadata = metadata_adapter_for(gem_state)
|
|
893
913
|
return change_sections_cache[cache_key] = [] unless metadata
|
|
894
|
-
sections = resolved_sections(metadata, gem_state)
|
|
914
|
+
sections = resolved_sections(metadata, gem_state, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
895
915
|
return change_sections_cache[cache_key] = [] if sections.nil? || sections.empty?
|
|
896
916
|
|
|
897
917
|
previous_version = gem_state[:old_version]
|
|
918
|
+
release_dates = resolved_release_dates(metadata, sections.keys, gem_state, force_refresh: force_refresh)
|
|
898
919
|
|
|
899
920
|
rendered_sections = sections.keys.filter_map do |version|
|
|
900
921
|
kind = section_kind(version, previous_version, current_version, gem_state[:status])
|
|
@@ -906,6 +927,7 @@ module Gemstar
|
|
|
906
927
|
title: content[:title],
|
|
907
928
|
kind: kind,
|
|
908
929
|
previous_version: previous_section_version(sections.keys, version),
|
|
930
|
+
release_date: release_date_for(release_dates, version),
|
|
909
931
|
html: content[:html]
|
|
910
932
|
}
|
|
911
933
|
end
|
|
@@ -915,10 +937,10 @@ module Gemstar
|
|
|
915
937
|
[]
|
|
916
938
|
end
|
|
917
939
|
|
|
918
|
-
def resolved_sections(metadata, gem_state)
|
|
940
|
+
def resolved_sections(metadata, gem_state, force_refresh: false, use_github_cli: false)
|
|
919
941
|
changelog = Gemstar::ChangeLog.new(metadata)
|
|
920
|
-
cached_sections = changelog.sections(cache_only: true) || {}
|
|
921
|
-
return cached_sections unless selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
942
|
+
cached_sections = force_refresh ? {} : changelog.sections(cache_only: true) || {}
|
|
943
|
+
return cached_sections unless force_refresh || selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
922
944
|
|
|
923
945
|
@metadata_cache.delete([gem_state[:package_scope], gem_state[:name]])
|
|
924
946
|
metadata.meta(cache_only: false, force_refresh: true)
|
|
@@ -926,11 +948,44 @@ module Gemstar
|
|
|
926
948
|
refreshed_sections = metadata.changelog_sections(
|
|
927
949
|
versions: relevant_package_versions(gem_state, metadata),
|
|
928
950
|
cache_only: false,
|
|
929
|
-
force_refresh: true
|
|
951
|
+
force_refresh: true,
|
|
952
|
+
use_github_cli: use_github_cli
|
|
930
953
|
)
|
|
931
954
|
cached_sections.merge(refreshed_sections || {})
|
|
932
955
|
end
|
|
933
956
|
|
|
957
|
+
def resolved_release_dates(metadata, versions, gem_state, force_refresh: false)
|
|
958
|
+
changelog = Gemstar::ChangeLog.new(metadata)
|
|
959
|
+
cached_dates = force_refresh ? {} : changelog.release_dates(versions: versions, cache_only: true) || {}
|
|
960
|
+
return cached_dates unless selected_gem_missing_release_dates?(gem_state, versions, cached_dates)
|
|
961
|
+
|
|
962
|
+
fetched_dates = changelog.release_dates(versions: versions, cache_only: false, force_refresh: force_refresh) || {}
|
|
963
|
+
cached_dates.merge(fetched_dates)
|
|
964
|
+
rescue StandardError
|
|
965
|
+
{}
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def release_date_for(release_dates, version)
|
|
969
|
+
release_dates.find do |candidate_version, _date|
|
|
970
|
+
normalize_release_version_key(candidate_version) == normalize_release_version_key(version)
|
|
971
|
+
end&.last
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def selected_gem_missing_release_dates?(gem_state, versions, release_dates)
|
|
975
|
+
return false unless @selected_gem && gem_state[:name] == @selected_gem[:name]
|
|
976
|
+
|
|
977
|
+
requested_versions = Array(versions).map { |version| normalize_release_version_key(version) }.compact
|
|
978
|
+
dated_versions = release_dates.keys.map { |version| normalize_release_version_key(version) }.compact
|
|
979
|
+
(requested_versions - dated_versions).any?
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
def normalize_release_version_key(version)
|
|
983
|
+
value = version.to_s.strip
|
|
984
|
+
return nil if value.empty?
|
|
985
|
+
|
|
986
|
+
value.sub(/\Av/i, "")
|
|
987
|
+
end
|
|
988
|
+
|
|
934
989
|
def relevant_package_versions(gem_state, metadata)
|
|
935
990
|
metadata_hash = metadata_for(gem_state) || {}
|
|
936
991
|
[
|
|
@@ -1033,26 +1088,26 @@ module Gemstar
|
|
|
1033
1088
|
end
|
|
1034
1089
|
|
|
1035
1090
|
def strip_leading_version_heading(text, heading_version)
|
|
1036
|
-
stripped = text.sub(/\A\s*#+\s*v?#{Regexp.escape(heading_version)}\
|
|
1037
|
-
return
|
|
1091
|
+
stripped = text.sub(/\A\s*#+\s*(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b[^\n]*\n+/i, "")
|
|
1092
|
+
return strip_leading_heading_separator(stripped) unless stripped == text
|
|
1038
1093
|
|
|
1039
1094
|
lines = text.lines
|
|
1040
1095
|
return text if lines.empty?
|
|
1041
1096
|
|
|
1042
1097
|
first_line = lines.first.to_s
|
|
1043
1098
|
heading_like =
|
|
1044
|
-
first_line.match?(/\A\s*v?#{Regexp.escape(heading_version)}\b/i) ||
|
|
1045
|
-
first_line.match?(/\A\s*[\[(]?v?#{Regexp.escape(heading_version)}\b/i)
|
|
1099
|
+
first_line.match?(/\A\s*(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b/i) ||
|
|
1100
|
+
first_line.match?(/\A\s*[\[(]?(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b/i)
|
|
1046
1101
|
|
|
1047
1102
|
return text unless heading_like
|
|
1048
1103
|
|
|
1049
1104
|
remaining = lines.drop(1)
|
|
1050
1105
|
remaining.shift while remaining.first&.strip&.empty?
|
|
1051
|
-
|
|
1106
|
+
strip_leading_heading_separator(remaining.join)
|
|
1052
1107
|
end
|
|
1053
1108
|
|
|
1054
|
-
def
|
|
1055
|
-
text.sub(/\A\s
|
|
1109
|
+
def strip_leading_heading_separator(text)
|
|
1110
|
+
text.sub(/\A\s*(?:#{Regexp.escape("#")}{4,}|[-=]{3,})\s*\n+/, "")
|
|
1056
1111
|
end
|
|
1057
1112
|
|
|
1058
1113
|
def compare_versions(left, right)
|
|
@@ -1061,26 +1116,26 @@ module Gemstar
|
|
|
1061
1116
|
left.to_s <=> right.to_s
|
|
1062
1117
|
end
|
|
1063
1118
|
|
|
1064
|
-
def metadata_for(package_state_or_name, refresh_if_missing: false)
|
|
1119
|
+
def metadata_for(package_state_or_name, refresh_if_missing: false, force_refresh: false)
|
|
1065
1120
|
package_state = package_state_or_name.is_a?(Hash) ? package_state_or_name : { name: package_state_or_name, package_scope: "gems" }
|
|
1066
1121
|
gem_name = package_state[:name]
|
|
1067
1122
|
cache_key = [package_state[:package_scope], gem_name]
|
|
1068
1123
|
cached = @metadata_cache[cache_key]
|
|
1069
|
-
return nil if cached.equal?(MISSING_METADATA)
|
|
1070
|
-
return cached if cached
|
|
1124
|
+
return nil if !force_refresh && cached.equal?(MISSING_METADATA)
|
|
1125
|
+
return cached if !force_refresh && cached
|
|
1071
1126
|
|
|
1072
1127
|
if package_state[:package_scope] != "gems"
|
|
1073
1128
|
metadata = local_package_metadata(package_state)
|
|
1074
1129
|
adapter = metadata_adapter_for(package_state)
|
|
1075
|
-
remote_metadata = adapter&.meta(cache_only: true)
|
|
1130
|
+
remote_metadata = force_refresh ? nil : adapter&.meta(cache_only: true)
|
|
1076
1131
|
remote_metadata = adapter&.meta(cache_only: false, force_refresh: true) if remote_metadata.nil? && refresh_if_missing
|
|
1077
1132
|
metadata = metadata.compact.merge(remote_metadata || {})
|
|
1078
1133
|
@metadata_cache[cache_key] = metadata || MISSING_METADATA
|
|
1079
1134
|
return metadata
|
|
1080
1135
|
end
|
|
1081
1136
|
|
|
1082
|
-
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
1083
|
-
if metadata.nil? && refresh_if_missing
|
|
1137
|
+
metadata = force_refresh ? nil : Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
1138
|
+
if metadata.nil? && (refresh_if_missing || force_refresh)
|
|
1084
1139
|
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: false, force_refresh: true)
|
|
1085
1140
|
end
|
|
1086
1141
|
|
|
@@ -1261,16 +1316,27 @@ module Gemstar
|
|
|
1261
1316
|
else
|
|
1262
1317
|
%(<a href="#{h(fallback_url)}" target="_blank" rel="noreferrer">#{h(fallback_label)}</a>)
|
|
1263
1318
|
end
|
|
1319
|
+
github_cli_action = render_github_cli_release_button(version, repo_url)
|
|
1264
1320
|
|
|
1265
1321
|
{
|
|
1266
1322
|
version: version,
|
|
1267
1323
|
title: version,
|
|
1268
1324
|
kind: :current,
|
|
1269
1325
|
previous_version: fallback_previous_version_for(gem_state, previous_sections),
|
|
1270
|
-
html: "<p>No release information available. Check #{fallback_link} for more information.</p
|
|
1326
|
+
html: "<p>No release information available. Check #{fallback_link} for more information.</p>#{github_cli_action}"
|
|
1271
1327
|
}
|
|
1272
1328
|
end
|
|
1273
1329
|
|
|
1330
|
+
def render_github_cli_release_button(version, repo_url)
|
|
1331
|
+
return "" unless repo_url.to_s.include?("github.com")
|
|
1332
|
+
|
|
1333
|
+
<<~HTML
|
|
1334
|
+
<div class="detail-inline-actions">
|
|
1335
|
+
<button type="button" class="action" data-detail-use-gh data-release-version="#{h(version)}">Use GitHub CLI</button>
|
|
1336
|
+
</div>
|
|
1337
|
+
HTML
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1274
1340
|
def fallback_previous_version_for(gem_state, previous_sections)
|
|
1275
1341
|
return gem_state[:old_version] if gem_state[:new_version]
|
|
1276
1342
|
return previous_sections.first[:version] if previous_sections.any?
|
|
@@ -455,6 +455,12 @@
|
|
|
455
455
|
background: #fff;
|
|
456
456
|
padding: 0.5rem;
|
|
457
457
|
}
|
|
458
|
+
.detail-inline-actions {
|
|
459
|
+
display: flex;
|
|
460
|
+
flex-wrap: wrap;
|
|
461
|
+
gap: 0.35rem;
|
|
462
|
+
margin-top: 0.55rem;
|
|
463
|
+
}
|
|
458
464
|
.detail-loading-shell {
|
|
459
465
|
min-height: 14rem;
|
|
460
466
|
display: grid;
|
|
@@ -566,6 +572,13 @@
|
|
|
566
572
|
flex: 0 0 auto;
|
|
567
573
|
margin-left: auto;
|
|
568
574
|
}
|
|
575
|
+
.revision-card-heading {
|
|
576
|
+
display: inline-flex;
|
|
577
|
+
align-items: baseline;
|
|
578
|
+
flex-wrap: wrap;
|
|
579
|
+
gap: 0.45rem;
|
|
580
|
+
min-width: 0;
|
|
581
|
+
}
|
|
569
582
|
.revision-card h5 {
|
|
570
583
|
margin: 0;
|
|
571
584
|
font-size: 1.55rem;
|
|
@@ -573,6 +586,12 @@
|
|
|
573
586
|
color: var(--ink);
|
|
574
587
|
min-width: 0;
|
|
575
588
|
}
|
|
589
|
+
.revision-release-date {
|
|
590
|
+
color: var(--grey);
|
|
591
|
+
font-size: 0.86rem;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
white-space: nowrap;
|
|
594
|
+
}
|
|
576
595
|
.revision-markup {
|
|
577
596
|
padding: 0.7rem;
|
|
578
597
|
}
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
const emptyDetailHtml = <%= empty_detail_html_json %>;
|
|
20
20
|
const packageCollectionLabel = <%= (@selected_project&.package_collection_label || "Packages").downcase.dump %>;
|
|
21
21
|
const detailDisclosureStorageKey = "gemstar.detailDisclosureOpen";
|
|
22
|
+
const pageWasReloaded = (() => {
|
|
23
|
+
const navigationEntry = performance.getEntriesByType && performance.getEntriesByType("navigation")[0];
|
|
24
|
+
if (navigationEntry) return navigationEntry.type === "reload";
|
|
25
|
+
return performance.navigation && performance.navigation.type === 1;
|
|
26
|
+
})();
|
|
27
|
+
let pendingManualDetailRefresh = pageWasReloaded;
|
|
22
28
|
|
|
23
29
|
const visibleGemLinks = () => gemLinks.filter((link) => !link.hidden);
|
|
24
30
|
const currentSelectedIndex = () => visibleGemLinks().findIndex((link) => link.classList.contains("is-selected"));
|
|
@@ -170,7 +176,15 @@
|
|
|
170
176
|
</section>
|
|
171
177
|
`;
|
|
172
178
|
|
|
173
|
-
const fetchDetail = (url, historyMode = "push") => {
|
|
179
|
+
const fetchDetail = (url, historyMode = "push", options = {}) => {
|
|
180
|
+
const requestUrl = new URL(url, window.location.origin);
|
|
181
|
+
if (options.refresh) {
|
|
182
|
+
requestUrl.searchParams.set("refresh", "1");
|
|
183
|
+
}
|
|
184
|
+
if (options.useGh) {
|
|
185
|
+
requestUrl.searchParams.set("use_gh", "1");
|
|
186
|
+
requestUrl.searchParams.set("refresh", "1");
|
|
187
|
+
}
|
|
174
188
|
const normalizedUrl = new URL(url, window.location.origin).toString();
|
|
175
189
|
const requestToken = ++detailRequestToken;
|
|
176
190
|
activeDetailUrl = normalizedUrl;
|
|
@@ -180,7 +194,7 @@
|
|
|
180
194
|
replaceDetail(loadingDetailHtml(normalizedUrl));
|
|
181
195
|
}, 1000);
|
|
182
196
|
|
|
183
|
-
fetch(
|
|
197
|
+
fetch(requestUrl.toString(), { headers: { "X-Requested-With": "gemstar-detail" } })
|
|
184
198
|
.then((response) => response.text())
|
|
185
199
|
.then((html) => {
|
|
186
200
|
stopDetailLoading();
|
|
@@ -232,7 +246,8 @@
|
|
|
232
246
|
window.history.replaceState({}, "", pageUrl);
|
|
233
247
|
}
|
|
234
248
|
if (detailNeedsInitialFetch()) {
|
|
235
|
-
fetchDetail(detailLink.dataset.detailUrl || detailLink.href, requested ? "none" : "replace");
|
|
249
|
+
fetchDetail(detailLink.dataset.detailUrl || detailLink.href, requested ? "none" : "replace", { refresh: pendingManualDetailRefresh });
|
|
250
|
+
pendingManualDetailRefresh = false;
|
|
236
251
|
}
|
|
237
252
|
return;
|
|
238
253
|
}
|
|
@@ -241,7 +256,11 @@
|
|
|
241
256
|
|
|
242
257
|
const firstVisibleLink = visibleGemLinks()[0];
|
|
243
258
|
if (firstVisibleLink) {
|
|
244
|
-
|
|
259
|
+
const refresh = pendingManualDetailRefresh;
|
|
260
|
+
pendingManualDetailRefresh = false;
|
|
261
|
+
syncSidebarSelection(firstVisibleLink.dataset.gemName, true);
|
|
262
|
+
fetchDetail(firstVisibleLink.dataset.detailUrl || firstVisibleLink.href, "replace", { refresh });
|
|
263
|
+
focusSidebar();
|
|
245
264
|
}
|
|
246
265
|
};
|
|
247
266
|
|
|
@@ -309,6 +328,16 @@
|
|
|
309
328
|
});
|
|
310
329
|
}
|
|
311
330
|
|
|
331
|
+
document.addEventListener("click", (event) => {
|
|
332
|
+
const button = event.target.closest("[data-detail-use-gh]");
|
|
333
|
+
if (!button || !detailPanel || !detailPanel.dataset.detailUrl) return;
|
|
334
|
+
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
button.disabled = true;
|
|
337
|
+
button.textContent = "Checking...";
|
|
338
|
+
fetchDetail(detailPanel.dataset.detailUrl, "none", { refresh: true, useGh: true });
|
|
339
|
+
});
|
|
340
|
+
|
|
312
341
|
const navigate = (params) => {
|
|
313
342
|
const url = new URL(window.location.href);
|
|
314
343
|
Object.entries(params).forEach(([key, value]) => {
|