gemstar 1.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 +7 -0
- data/lib/gemstar/cache_warmer.rb +15 -3
- data/lib/gemstar/change_log.rb +160 -12
- data/lib/gemstar/commands/server.rb +67 -3
- data/lib/gemstar/npm_metadata.rb +2 -2
- data/lib/gemstar/ruby_gems_metadata.rb +7 -2
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +53 -31
- data/lib/gemstar/web/templates/app.css +6 -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,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 1.1.1
|
|
4
11
|
|
|
5
12
|
- Server: Show release dates for changelog entries.
|
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
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
require "cgi"
|
|
3
3
|
require "date"
|
|
4
4
|
require "json"
|
|
5
|
+
require "open3"
|
|
5
6
|
require "time"
|
|
7
|
+
require "timeout"
|
|
8
|
+
require "uri"
|
|
9
|
+
require_relative "cache"
|
|
6
10
|
|
|
7
11
|
module Gemstar
|
|
8
12
|
class ChangeLog
|
|
9
13
|
@@candidates_found = Hash.new(0)
|
|
14
|
+
GITHUB_CLI_TIMEOUT = 8
|
|
10
15
|
DEFAULT_CHANGELOG_PATHS = %w[
|
|
11
16
|
CHANGELOG.md releases.md CHANGES.md
|
|
12
17
|
Changelog.md changelog.md ChangeLog.md
|
|
@@ -54,7 +59,7 @@ module Gemstar
|
|
|
54
59
|
result
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
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)
|
|
58
63
|
requested_versions = normalize_requested_versions(versions)
|
|
59
64
|
return {} if requested_versions.empty?
|
|
60
65
|
|
|
@@ -75,7 +80,8 @@ module Gemstar
|
|
|
75
80
|
repo_uri,
|
|
76
81
|
version,
|
|
77
82
|
cache_only: false,
|
|
78
|
-
force_refresh: force_refresh
|
|
83
|
+
force_refresh: force_refresh,
|
|
84
|
+
use_github_cli: use_github_cli
|
|
79
85
|
)
|
|
80
86
|
result.merge!(specific_release) if specific_release
|
|
81
87
|
end
|
|
@@ -378,18 +384,21 @@ module Gemstar
|
|
|
378
384
|
end
|
|
379
385
|
|
|
380
386
|
def parse_github_release_sections(cache_only: false, force_refresh: false)
|
|
381
|
-
begin
|
|
382
|
-
require "nokogiri"
|
|
383
|
-
rescue LoadError
|
|
384
|
-
return {}
|
|
385
|
-
end
|
|
386
|
-
|
|
387
387
|
repo_uri = @metadata&.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
388
388
|
return {} unless repo_uri&.include?("github.com")
|
|
389
389
|
|
|
390
390
|
url = github_releases_url(repo_uri)
|
|
391
391
|
return {} unless url
|
|
392
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
|
+
|
|
393
402
|
html = if cache_only
|
|
394
403
|
Cache.peek("releases-#{url}")
|
|
395
404
|
else
|
|
@@ -506,11 +515,90 @@ module Gemstar
|
|
|
506
515
|
"#{repo}/tags"
|
|
507
516
|
end
|
|
508
517
|
|
|
509
|
-
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)
|
|
510
591
|
return {} unless repo_uri&.include?("github.com")
|
|
511
592
|
return {} if version.to_s.empty?
|
|
512
593
|
|
|
513
|
-
|
|
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)
|
|
514
602
|
html = if cache_only
|
|
515
603
|
Cache.peek("releases-#{url}")
|
|
516
604
|
else
|
|
@@ -533,6 +621,62 @@ module Gemstar
|
|
|
533
621
|
{}
|
|
534
622
|
end
|
|
535
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
|
+
|
|
536
680
|
def parse_github_tag_sections(repo_uri, cache_only:, force_refresh:)
|
|
537
681
|
return {} unless repo_uri&.include?("github.com")
|
|
538
682
|
|
|
@@ -760,11 +904,15 @@ module Gemstar
|
|
|
760
904
|
|
|
761
905
|
def github_release_tag_urls(repo_url, version)
|
|
762
906
|
github_tag_candidates(version).map do |tag|
|
|
763
|
-
|
|
764
|
-
"#{repo_url}/releases/tag/#{encoded_tag}"
|
|
907
|
+
github_release_tag_url(repo_url, tag)
|
|
765
908
|
end.uniq
|
|
766
909
|
end
|
|
767
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
|
+
|
|
768
916
|
def github_tag_candidates(version)
|
|
769
917
|
return @metadata.github_tag_candidates(version) if @metadata.respond_to?(:github_tag_candidates)
|
|
770
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/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
|
|
|
@@ -88,8 +88,13 @@ module Gemstar
|
|
|
88
88
|
repo
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
def changelog_sections(versions: nil, cache_only: false, force_refresh: false)
|
|
92
|
-
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
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
def registry_release_dates(cache_only: false, force_refresh: false)
|
data/lib/gemstar/version.rb
CHANGED
data/lib/gemstar/web/app.rb
CHANGED
|
@@ -59,13 +59,15 @@ module Gemstar
|
|
|
59
59
|
r.get "detail" do
|
|
60
60
|
request_cache_key = detail_request_cache_key(r.params)
|
|
61
61
|
request_cache = self.class.opts[:detail_request_cache]
|
|
62
|
-
|
|
62
|
+
refresh_detail = detail_refresh_requested?(r.params)
|
|
63
|
+
if request_cache_key && !refresh_detail && request_cache.key?(request_cache_key)
|
|
63
64
|
next request_cache[request_cache_key]
|
|
64
65
|
end
|
|
66
|
+
request_cache.delete(request_cache_key) if request_cache_key && refresh_detail
|
|
65
67
|
|
|
66
68
|
load_state(r.params)
|
|
67
|
-
prioritize_selected_gem
|
|
68
|
-
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))
|
|
69
71
|
request_cache[request_cache_key] = detail_html if request_cache_key
|
|
70
72
|
detail_html
|
|
71
73
|
end
|
|
@@ -131,6 +133,14 @@ module Gemstar
|
|
|
131
133
|
0
|
|
132
134
|
end
|
|
133
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
|
+
|
|
134
144
|
def page_title
|
|
135
145
|
return "Gemstar" unless @selected_project
|
|
136
146
|
|
|
@@ -480,10 +490,10 @@ module Gemstar
|
|
|
480
490
|
HTML
|
|
481
491
|
end
|
|
482
492
|
|
|
483
|
-
def render_detail
|
|
493
|
+
def render_detail(force_refresh: false, use_github_cli: false)
|
|
484
494
|
return empty_detail_html unless @selected_gem
|
|
485
495
|
|
|
486
|
-
metadata = metadata_for(@selected_gem, refresh_if_missing: true)
|
|
496
|
+
metadata = metadata_for(@selected_gem, refresh_if_missing: true, force_refresh: force_refresh)
|
|
487
497
|
cache_key = [
|
|
488
498
|
CACHE_VERSION,
|
|
489
499
|
@selected_project_index,
|
|
@@ -502,9 +512,9 @@ module Gemstar
|
|
|
502
512
|
@selected_gem[:status]
|
|
503
513
|
]
|
|
504
514
|
detail_cache = self.class.opts[:detail_html_cache]
|
|
505
|
-
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)
|
|
506
516
|
|
|
507
|
-
groups = grouped_change_sections(@selected_gem)
|
|
517
|
+
groups = grouped_change_sections(@selected_gem, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
508
518
|
detail_pending = detail_pending?(@selected_gem[:name], metadata, groups)
|
|
509
519
|
|
|
510
520
|
detail_html = <<~HTML
|
|
@@ -860,8 +870,8 @@ module Gemstar
|
|
|
860
870
|
%(<span class="revision-release-date" title="Estimated release date">#{h(release_date)}</span>)
|
|
861
871
|
end
|
|
862
872
|
|
|
863
|
-
def grouped_change_sections(gem_state)
|
|
864
|
-
sections = change_sections(gem_state)
|
|
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)
|
|
865
875
|
latest = sections.select { |section| section[:kind] == :future }
|
|
866
876
|
current = sections.select { |section| section[:kind] == :current }
|
|
867
877
|
previous = sections.select { |section| section[:kind] == :previous }
|
|
@@ -878,8 +888,8 @@ module Gemstar
|
|
|
878
888
|
}
|
|
879
889
|
end
|
|
880
890
|
|
|
881
|
-
def change_sections(gem_state)
|
|
882
|
-
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) || {}
|
|
883
893
|
current_version = effective_package_version(gem_state, metadata_hash)
|
|
884
894
|
cache_key = [
|
|
885
895
|
CACHE_VERSION,
|
|
@@ -894,18 +904,18 @@ module Gemstar
|
|
|
894
904
|
gem_state[:status]
|
|
895
905
|
]
|
|
896
906
|
change_sections_cache = self.class.opts[:change_sections_cache]
|
|
897
|
-
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)
|
|
898
908
|
|
|
899
909
|
return [] if gem_state[:new_version].nil? &&
|
|
900
910
|
gem_state[:old_version].nil? &&
|
|
901
911
|
(current_version.nil? || current_version.to_s.empty?)
|
|
902
912
|
metadata = metadata_adapter_for(gem_state)
|
|
903
913
|
return change_sections_cache[cache_key] = [] unless metadata
|
|
904
|
-
sections = resolved_sections(metadata, gem_state)
|
|
914
|
+
sections = resolved_sections(metadata, gem_state, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
905
915
|
return change_sections_cache[cache_key] = [] if sections.nil? || sections.empty?
|
|
906
916
|
|
|
907
917
|
previous_version = gem_state[:old_version]
|
|
908
|
-
release_dates = resolved_release_dates(metadata, sections.keys, gem_state)
|
|
918
|
+
release_dates = resolved_release_dates(metadata, sections.keys, gem_state, force_refresh: force_refresh)
|
|
909
919
|
|
|
910
920
|
rendered_sections = sections.keys.filter_map do |version|
|
|
911
921
|
kind = section_kind(version, previous_version, current_version, gem_state[:status])
|
|
@@ -927,10 +937,10 @@ module Gemstar
|
|
|
927
937
|
[]
|
|
928
938
|
end
|
|
929
939
|
|
|
930
|
-
def resolved_sections(metadata, gem_state)
|
|
940
|
+
def resolved_sections(metadata, gem_state, force_refresh: false, use_github_cli: false)
|
|
931
941
|
changelog = Gemstar::ChangeLog.new(metadata)
|
|
932
|
-
cached_sections = changelog.sections(cache_only: true) || {}
|
|
933
|
-
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)
|
|
934
944
|
|
|
935
945
|
@metadata_cache.delete([gem_state[:package_scope], gem_state[:name]])
|
|
936
946
|
metadata.meta(cache_only: false, force_refresh: true)
|
|
@@ -938,17 +948,18 @@ module Gemstar
|
|
|
938
948
|
refreshed_sections = metadata.changelog_sections(
|
|
939
949
|
versions: relevant_package_versions(gem_state, metadata),
|
|
940
950
|
cache_only: false,
|
|
941
|
-
force_refresh: true
|
|
951
|
+
force_refresh: true,
|
|
952
|
+
use_github_cli: use_github_cli
|
|
942
953
|
)
|
|
943
954
|
cached_sections.merge(refreshed_sections || {})
|
|
944
955
|
end
|
|
945
956
|
|
|
946
|
-
def resolved_release_dates(metadata, versions, gem_state)
|
|
957
|
+
def resolved_release_dates(metadata, versions, gem_state, force_refresh: false)
|
|
947
958
|
changelog = Gemstar::ChangeLog.new(metadata)
|
|
948
|
-
cached_dates = changelog.release_dates(versions: versions, cache_only: true) || {}
|
|
959
|
+
cached_dates = force_refresh ? {} : changelog.release_dates(versions: versions, cache_only: true) || {}
|
|
949
960
|
return cached_dates unless selected_gem_missing_release_dates?(gem_state, versions, cached_dates)
|
|
950
961
|
|
|
951
|
-
fetched_dates = changelog.release_dates(versions: versions, cache_only: false) || {}
|
|
962
|
+
fetched_dates = changelog.release_dates(versions: versions, cache_only: false, force_refresh: force_refresh) || {}
|
|
952
963
|
cached_dates.merge(fetched_dates)
|
|
953
964
|
rescue StandardError
|
|
954
965
|
{}
|
|
@@ -1077,7 +1088,7 @@ module Gemstar
|
|
|
1077
1088
|
end
|
|
1078
1089
|
|
|
1079
1090
|
def strip_leading_version_heading(text, heading_version)
|
|
1080
|
-
stripped = text.sub(/\A\s*#+\s*v?#{Regexp.escape(heading_version)}\
|
|
1091
|
+
stripped = text.sub(/\A\s*#+\s*(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b[^\n]*\n+/i, "")
|
|
1081
1092
|
return strip_leading_heading_separator(stripped) unless stripped == text
|
|
1082
1093
|
|
|
1083
1094
|
lines = text.lines
|
|
@@ -1085,8 +1096,8 @@ module Gemstar
|
|
|
1085
1096
|
|
|
1086
1097
|
first_line = lines.first.to_s
|
|
1087
1098
|
heading_like =
|
|
1088
|
-
first_line.match?(/\A\s*v?#{Regexp.escape(heading_version)}\b/i) ||
|
|
1089
|
-
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)
|
|
1090
1101
|
|
|
1091
1102
|
return text unless heading_like
|
|
1092
1103
|
|
|
@@ -1105,26 +1116,26 @@ module Gemstar
|
|
|
1105
1116
|
left.to_s <=> right.to_s
|
|
1106
1117
|
end
|
|
1107
1118
|
|
|
1108
|
-
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)
|
|
1109
1120
|
package_state = package_state_or_name.is_a?(Hash) ? package_state_or_name : { name: package_state_or_name, package_scope: "gems" }
|
|
1110
1121
|
gem_name = package_state[:name]
|
|
1111
1122
|
cache_key = [package_state[:package_scope], gem_name]
|
|
1112
1123
|
cached = @metadata_cache[cache_key]
|
|
1113
|
-
return nil if cached.equal?(MISSING_METADATA)
|
|
1114
|
-
return cached if cached
|
|
1124
|
+
return nil if !force_refresh && cached.equal?(MISSING_METADATA)
|
|
1125
|
+
return cached if !force_refresh && cached
|
|
1115
1126
|
|
|
1116
1127
|
if package_state[:package_scope] != "gems"
|
|
1117
1128
|
metadata = local_package_metadata(package_state)
|
|
1118
1129
|
adapter = metadata_adapter_for(package_state)
|
|
1119
|
-
remote_metadata = adapter&.meta(cache_only: true)
|
|
1130
|
+
remote_metadata = force_refresh ? nil : adapter&.meta(cache_only: true)
|
|
1120
1131
|
remote_metadata = adapter&.meta(cache_only: false, force_refresh: true) if remote_metadata.nil? && refresh_if_missing
|
|
1121
1132
|
metadata = metadata.compact.merge(remote_metadata || {})
|
|
1122
1133
|
@metadata_cache[cache_key] = metadata || MISSING_METADATA
|
|
1123
1134
|
return metadata
|
|
1124
1135
|
end
|
|
1125
1136
|
|
|
1126
|
-
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
1127
|
-
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)
|
|
1128
1139
|
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: false, force_refresh: true)
|
|
1129
1140
|
end
|
|
1130
1141
|
|
|
@@ -1305,16 +1316,27 @@ module Gemstar
|
|
|
1305
1316
|
else
|
|
1306
1317
|
%(<a href="#{h(fallback_url)}" target="_blank" rel="noreferrer">#{h(fallback_label)}</a>)
|
|
1307
1318
|
end
|
|
1319
|
+
github_cli_action = render_github_cli_release_button(version, repo_url)
|
|
1308
1320
|
|
|
1309
1321
|
{
|
|
1310
1322
|
version: version,
|
|
1311
1323
|
title: version,
|
|
1312
1324
|
kind: :current,
|
|
1313
1325
|
previous_version: fallback_previous_version_for(gem_state, previous_sections),
|
|
1314
|
-
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}"
|
|
1315
1327
|
}
|
|
1316
1328
|
end
|
|
1317
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
|
+
|
|
1318
1340
|
def fallback_previous_version_for(gem_state, previous_sections)
|
|
1319
1341
|
return gem_state[:old_version] if gem_state[:new_version]
|
|
1320
1342
|
return previous_sections.first[:version] if previous_sections.any?
|
|
@@ -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]) => {
|