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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aba5f74f93dc292f713a6f5d1a824cc04d7601fffac736f075c75f08ea769ff7
4
- data.tar.gz: f13a06cba387d9ae4ec744ed6e9703bbe8c1aea95cd8f27ab2f4efd816af33d6
3
+ metadata.gz: c6157dce9498afdbda3d014af6e1e4e20e7477d73bd3b12129c0d754df28f4f7
4
+ data.tar.gz: c7fcc69688c21fea79ead9b9f762d026a5b57e5c597206171fe6faaeb5555db1
5
5
  SHA512:
6
- metadata.gz: e45c7adfbfae8779801141084c0097d57be63415398c1a1b695a090042aa1c8fc6c94764275963aca7ef361de4e63f38f39a172b5158814282cd43ce865f5556
7
- data.tar.gz: ad082b18db97c9f7a1a1c74de309fad18ca282cdd41c42918b27818b7bcc6da895fc9320660f35a0546028b75ed56726a5a545b7e709f4eb82d9a49237f938f5
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.
@@ -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
- return unless metadata
126
+ metadata&.warm_cache(versions: package_versions(package_state))
124
127
 
125
- metadata.warm_cache(versions: package_versions(package_state))
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?
@@ -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 parse_specific_github_release_pages(repo_uri, version, cache_only:, force_refresh:)
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
- github_release_tag_urls(repo_uri, version).each do |url|
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
- encoded_tag = URI.encode_www_form_component(tag)
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
- app = Gemstar::Web::App.build(projects: projects, config_home: Gemstar::Config.home_directory, cache_warmer: cache_warmer)
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
- project.gem_states(from_revision_id: "worktree", to_revision_id: "worktree")
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
@@ -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).sections(cache_only: cache_only, force_refresh: force_refresh)
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)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemstar # :nodoc:
4
- VERSION = "1.1.1"
4
+ VERSION = "1.2"
5
5
  def self.debug?
6
6
  return @debug if defined?(@debug)
7
7
  @debug = ENV["GEMSTAR_DEBUG"] == "true"
@@ -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
- if request_cache_key && request_cache.key?(request_cache_key)
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)}\s*\n+/i, "")
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?
@@ -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;
@@ -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(url, { headers: { "X-Requested-With": "gemstar-detail" } })
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
- activateGemLink(firstVisibleLink, "replace", true);
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]) => {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemstar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: '1.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Dejako