gemstar 1.1.1 → 1.3

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.
@@ -1,8 +1,29 @@
1
1
  require "time"
2
+ require_relative "cargo_lock_file"
3
+ require_relative "uv_lock_file"
2
4
 
3
5
  module Gemstar
4
6
  class Project
5
7
  REVISION_HISTORY_LIMIT = 100
8
+ SUPPORTED_PROJECT_FILES = [
9
+ "Gemfile",
10
+ "Gemfile.lock",
11
+ "config/importmap.rb",
12
+ "package.json",
13
+ "package-lock.json",
14
+ "uv.lock",
15
+ "Cargo.toml",
16
+ "Cargo.lock"
17
+ ].freeze
18
+
19
+ class UnsupportedProjectError < ArgumentError
20
+ def initialize(directory)
21
+ super(<<~MESSAGE.chomp)
22
+ Directory #{File.expand_path(directory)} does not contain a recognized project file.
23
+ Only #{Project.supported_project_files_sentence} are supported.
24
+ MESSAGE
25
+ end
26
+ end
6
27
 
7
28
  attr_reader :directory
8
29
  attr_reader :gemfile_path
@@ -10,6 +31,9 @@ module Gemstar
10
31
  attr_reader :importmap_path
11
32
  attr_reader :package_json_path
12
33
  attr_reader :package_lock_path
34
+ attr_reader :uv_lock_path
35
+ attr_reader :cargo_toml_path
36
+ attr_reader :cargo_lock_path
13
37
  attr_reader :name
14
38
 
15
39
  def self.from_cli_argument(input)
@@ -20,26 +44,36 @@ module Gemstar
20
44
  basename = File.basename(expanded_input)
21
45
  directory =
22
46
  case basename
23
- when "Gemfile", "package.json", "package-lock.json"
47
+ when "Gemfile", "Gemfile.lock", "package.json", "package-lock.json", "uv.lock", "Cargo.toml", "Cargo.lock"
24
48
  File.dirname(expanded_input)
25
49
  when "importmap.rb"
26
- File.dirname(File.dirname(expanded_input))
27
- else
28
- nil
50
+ File.dirname(expanded_input, 2)
29
51
  end
30
52
  end
31
53
 
32
- raise ArgumentError, "No supported project files found for #{input}" unless directory
33
- raise ArgumentError, "No supported project files found for #{input}" unless supported_project_directory?(directory)
54
+ unless directory
55
+ unsupported_directory = File.directory?(expanded_input) ? expanded_input : File.dirname(expanded_input)
56
+ raise UnsupportedProjectError, unsupported_directory
57
+ end
58
+
59
+ raise UnsupportedProjectError, directory unless supported_project_directory?(directory)
34
60
 
35
61
  new(directory: directory)
36
62
  end
37
63
 
38
64
  def self.supported_project_directory?(directory)
39
65
  File.file?(File.join(directory, "Gemfile")) ||
66
+ File.file?(File.join(directory, "Gemfile.lock")) ||
40
67
  File.file?(File.join(directory, "config", "importmap.rb")) ||
41
68
  File.file?(File.join(directory, "package.json")) ||
42
- File.file?(File.join(directory, "package-lock.json"))
69
+ File.file?(File.join(directory, "package-lock.json")) ||
70
+ File.file?(File.join(directory, "uv.lock")) ||
71
+ File.file?(File.join(directory, "Cargo.toml")) ||
72
+ File.file?(File.join(directory, "Cargo.lock"))
73
+ end
74
+
75
+ def self.supported_project_files_sentence
76
+ "#{SUPPORTED_PROJECT_FILES[0...-1].join(", ")}, and #{SUPPORTED_PROJECT_FILES[-1]}"
43
77
  end
44
78
 
45
79
  def initialize(directory:)
@@ -49,10 +83,15 @@ module Gemstar
49
83
  @importmap_path = File.join(@directory, "config", "importmap.rb")
50
84
  @package_json_path = File.join(@directory, "package.json")
51
85
  @package_lock_path = File.join(@directory, "package-lock.json")
86
+ @uv_lock_path = File.join(@directory, "uv.lock")
87
+ @cargo_toml_path = File.join(@directory, "Cargo.toml")
88
+ @cargo_lock_path = File.join(@directory, "Cargo.lock")
52
89
  @name = File.basename(@directory)
53
90
  @lockfile_cache = {}
54
91
  @importmap_cache = {}
55
92
  @package_lock_cache = {}
93
+ @uv_lock_cache = {}
94
+ @cargo_lock_cache = {}
56
95
  @gem_states_cache = {}
57
96
  @gem_added_on_cache = {}
58
97
  @history_cache = {}
@@ -104,6 +143,46 @@ module Gemstar
104
143
  @current_package_lock ||= Gemstar::PackageLockFile.new(path: package_lock_path)
105
144
  end
106
145
 
146
+ def uv_lock?
147
+ File.file?(uv_lock_path)
148
+ end
149
+
150
+ def current_uv_lock
151
+ return nil unless uv_lock?
152
+
153
+ @current_uv_lock ||= Gemstar::UvLockFile.new(path: uv_lock_path)
154
+ end
155
+
156
+ def cargo_toml?
157
+ File.file?(cargo_toml_path)
158
+ end
159
+
160
+ def cargo_lock?
161
+ File.file?(cargo_lock_path)
162
+ end
163
+
164
+ def current_cargo_lock
165
+ return nil unless cargo_lock?
166
+
167
+ @current_cargo_lock ||= Gemstar::CargoLockFile.new(path: cargo_lock_path)
168
+ end
169
+
170
+ def clear_cache!
171
+ @current_lockfile = nil
172
+ @current_importmap = nil
173
+ @current_package_lock = nil
174
+ @current_uv_lock = nil
175
+ @current_cargo_lock = nil
176
+ @lockfile_cache.clear
177
+ @importmap_cache.clear
178
+ @package_lock_cache.clear
179
+ @uv_lock_cache.clear
180
+ @cargo_lock_cache.clear
181
+ @gem_states_cache.clear
182
+ @gem_added_on_cache.clear
183
+ @history_cache.clear
184
+ end
185
+
107
186
  def revision_history(limit: REVISION_HISTORY_LIMIT)
108
187
  history_for_paths(tracked_git_paths, limit: limit)
109
188
  end
@@ -133,7 +212,7 @@ module Gemstar
133
212
  end
134
213
 
135
214
  def revision_options(limit: REVISION_HISTORY_LIMIT)
136
- [{ id: "worktree", label: "Worktree", description: "Current Gemfile.lock in the working tree" }] +
215
+ [{ id: "worktree", label: "Worktree", description: "Current project files in the working tree" }] +
137
216
  revision_history(limit: limit).map do |revision|
138
217
  {
139
218
  id: revision[:id],
@@ -147,6 +226,8 @@ module Gemstar
147
226
  scopes = []
148
227
  scopes << :gems if gemfile? || lockfile?
149
228
  scopes << :js if importmap? || package_lock? || package_json?
229
+ scopes << :python if uv_lock?
230
+ scopes << :cargo if cargo_toml? || cargo_lock?
150
231
  scopes
151
232
  end
152
233
 
@@ -163,6 +244,16 @@ module Gemstar
163
244
  package_scopes == [:gems] ? "Gems" : "Packages"
164
245
  end
165
246
 
247
+ def package_item_label
248
+ case package_scopes
249
+ when [:gems] then "gem"
250
+ when [:js] then "JavaScript package"
251
+ when [:python] then "Python package"
252
+ when [:cargo] then "Rust crate"
253
+ else "package"
254
+ end
255
+ end
256
+
166
257
  def lockfile_for_revision(revision_id)
167
258
  cache_key = revision_id || "worktree"
168
259
  return @lockfile_cache[cache_key] if @lockfile_cache.key?(cache_key)
@@ -208,6 +299,36 @@ module Gemstar
208
299
  @package_lock_cache[cache_key] = Gemstar::PackageLockFile.new(content: content)
209
300
  end
210
301
 
302
+ def uv_lock_for_revision(revision_id)
303
+ cache_key = revision_id || "worktree"
304
+ return @uv_lock_cache[cache_key] if @uv_lock_cache.key?(cache_key)
305
+ return @uv_lock_cache[cache_key] = current_uv_lock if revision_id.nil? || revision_id == "worktree"
306
+ return nil unless uv_lock?
307
+
308
+ relative_uv_lock_path = git_repo.relative_path(uv_lock_path)
309
+ return nil if relative_uv_lock_path.nil?
310
+
311
+ content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_uv_lock_path}"])
312
+ return nil if content.nil? || content.empty?
313
+
314
+ @uv_lock_cache[cache_key] = Gemstar::UvLockFile.new(content: content)
315
+ end
316
+
317
+ def cargo_lock_for_revision(revision_id)
318
+ cache_key = revision_id || "worktree"
319
+ return @cargo_lock_cache[cache_key] if @cargo_lock_cache.key?(cache_key)
320
+ return @cargo_lock_cache[cache_key] = current_cargo_lock if revision_id.nil? || revision_id == "worktree"
321
+ return nil unless cargo_lock?
322
+
323
+ relative_cargo_lock_path = git_repo.relative_path(cargo_lock_path)
324
+ return nil if relative_cargo_lock_path.nil?
325
+
326
+ content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_cargo_lock_path}"])
327
+ return nil if content.nil? || content.empty?
328
+
329
+ @cargo_lock_cache[cache_key] = Gemstar::CargoLockFile.new(content: content)
330
+ end
331
+
211
332
  def gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree")
212
333
  cache_key = [from_revision_id, to_revision_id]
213
334
  return @gem_states_cache[cache_key] if @gem_states_cache.key?(cache_key)
@@ -298,11 +419,67 @@ module Gemstar
298
419
  }
299
420
  end
300
421
 
301
- @gem_states_cache[cache_key] = (gem_states + js_states + npm_states).sort_by { |gem| [gem[:name], gem[:package_scope], gem[:package_source_file].to_s] }
422
+ from_uv_lock = uv_lock_for_revision(from_revision_id)
423
+ to_uv_lock = uv_lock_for_revision(to_revision_id)
424
+ from_python_specs = from_uv_lock&.specs || {}
425
+ to_python_specs = to_uv_lock&.specs || {}
426
+ python_states = (from_python_specs.keys | to_python_specs.keys).map do |package_name|
427
+ old_version = from_python_specs[package_name]
428
+ new_version = to_python_specs[package_name]
429
+ effective_uv_lock = new_version ? to_uv_lock : from_uv_lock
430
+
431
+ {
432
+ name: package_name,
433
+ package_scope: "python",
434
+ package_type_label: "Python",
435
+ package_source_file: :uv_lock,
436
+ old_version: old_version,
437
+ new_version: new_version,
438
+ status: gem_status(old_version, new_version),
439
+ version_label: version_label(old_version, new_version),
440
+ platform: nil,
441
+ source: effective_uv_lock&.source_for(package_name),
442
+ bundle_origins: [],
443
+ bundle_origin_labels: []
444
+ }
445
+ end
446
+
447
+ from_cargo_lock = cargo_lock_for_revision(from_revision_id)
448
+ to_cargo_lock = cargo_lock_for_revision(to_revision_id)
449
+ from_cargo_specs = from_cargo_lock&.specs || {}
450
+ to_cargo_specs = to_cargo_lock&.specs || {}
451
+ cargo_states = (from_cargo_specs.keys | to_cargo_specs.keys).map do |package_key|
452
+ old_version = from_cargo_specs[package_key]
453
+ new_version = to_cargo_specs[package_key]
454
+ effective_cargo_lock = new_version ? to_cargo_lock : from_cargo_lock
455
+ source = effective_cargo_lock&.source_for(package_key)
456
+ package_name = source&.dig(:package_name) || package_key
457
+ display_name = package_key == package_name ? package_name : "#{package_name} @ #{new_version || old_version}"
458
+
459
+ {
460
+ name: display_name,
461
+ metadata_package_name: package_name,
462
+ package_lock_key: package_key,
463
+ package_scope: "cargo",
464
+ package_type_label: "Crate",
465
+ package_source_file: :cargo_lock,
466
+ old_version: old_version,
467
+ new_version: new_version,
468
+ status: gem_status(old_version, new_version),
469
+ version_label: version_label(old_version, new_version),
470
+ platform: nil,
471
+ source: source,
472
+ bundle_origins: [],
473
+ bundle_origin_labels: []
474
+ }
475
+ end
476
+
477
+ @gem_states_cache[cache_key] = (gem_states + js_states + npm_states + python_states + cargo_states).sort_by { |gem| [gem[:name], gem[:package_scope], gem[:package_source_file].to_s] }
302
478
  end
303
479
 
304
- def package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil)
305
- cache_key = [package_name, package_scope, source_file, revision_id]
480
+ def package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil, package_key: nil)
481
+ lookup_name = package_key || package_name
482
+ cache_key = [lookup_name, package_scope, source_file, revision_id]
306
483
  return @gem_added_on_cache[cache_key] if @gem_added_on_cache.key?(cache_key)
307
484
 
308
485
  tracked_file, reader =
@@ -310,22 +487,30 @@ module Gemstar
310
487
  [importmap_path, method(:importmap_for_revision)]
311
488
  elsif source_file == :package_lock
312
489
  [package_lock_path, method(:package_lock_for_revision)]
490
+ elsif source_file == :uv_lock
491
+ [uv_lock_path, method(:uv_lock_for_revision)]
492
+ elsif source_file == :cargo_lock
493
+ [cargo_lock_path, method(:cargo_lock_for_revision)]
313
494
  elsif package_scope == "js"
314
495
  [importmap_path, method(:importmap_for_revision)]
496
+ elsif package_scope == "python"
497
+ [uv_lock_path, method(:uv_lock_for_revision)]
498
+ elsif package_scope == "cargo"
499
+ [cargo_lock_path, method(:cargo_lock_for_revision)]
315
500
  else
316
501
  [lockfile_path, method(:lockfile_for_revision)]
317
502
  end
318
503
  return @gem_added_on_cache[cache_key] = nil unless File.file?(tracked_file)
319
504
 
320
505
  target_snapshot = reader.call(revision_id)
321
- return @gem_added_on_cache[cache_key] = nil unless target_snapshot&.specs&.key?(package_name)
506
+ return @gem_added_on_cache[cache_key] = nil unless target_snapshot&.specs&.key?(lookup_name)
322
507
 
323
508
  relative_path = git_repo.relative_path(tracked_file)
324
509
  return @gem_added_on_cache[cache_key] = nil if relative_path.nil?
325
510
 
326
511
  first_seen_revision = history_for_paths([relative_path], limit: nil, reverse: true).find do |revision|
327
512
  snapshot = reader.call(revision[:id])
328
- snapshot&.specs&.key?(package_name)
513
+ snapshot&.specs&.key?(lookup_name)
329
514
  end
330
515
 
331
516
  return @gem_added_on_cache[cache_key] = worktree_added_on_info(tracked_file) if first_seen_revision.nil? && revision_id == "worktree"
@@ -346,14 +531,20 @@ module Gemstar
346
531
  current_specs = current_lockfile&.specs || {}
347
532
  current_importmap_specs = current_importmap&.specs || {}
348
533
  current_package_lock_specs = current_package_lock&.specs || {}
534
+ current_uv_lock_specs = current_uv_lock&.specs || {}
535
+ current_cargo_lock_specs = current_cargo_lock&.specs || {}
349
536
 
350
537
  revision_history(limit: REVISION_HISTORY_LIMIT).find do |revision|
351
538
  revision_lockfile = lockfile_for_revision(revision[:id])
352
539
  revision_importmap = importmap_for_revision(revision[:id])
353
540
  revision_package_lock = package_lock_for_revision(revision[:id])
541
+ revision_uv_lock = uv_lock_for_revision(revision[:id])
542
+ revision_cargo_lock = cargo_lock_for_revision(revision[:id])
354
543
  (revision_lockfile && revision_lockfile.specs != current_specs) ||
355
544
  (revision_importmap && revision_importmap.specs != current_importmap_specs) ||
356
- (revision_package_lock && revision_package_lock.specs != current_package_lock_specs)
545
+ (revision_package_lock && revision_package_lock.specs != current_package_lock_specs) ||
546
+ (revision_uv_lock && revision_uv_lock.specs != current_uv_lock_specs) ||
547
+ (revision_cargo_lock && revision_cargo_lock.specs != current_cargo_lock_specs)
357
548
  end&.dig(:id)
358
549
  end
359
550
 
@@ -384,7 +575,7 @@ module Gemstar
384
575
  end
385
576
 
386
577
  def tracked_git_paths
387
- [gemfile_path, lockfile_path, importmap_path, package_json_path, package_lock_path, *importmap_vendor_paths].filter_map do |path|
578
+ [gemfile_path, lockfile_path, importmap_path, package_json_path, package_lock_path, uv_lock_path, cargo_toml_path, cargo_lock_path, *importmap_vendor_paths].filter_map do |path|
388
579
  next unless File.file?(path)
389
580
 
390
581
  git_repo.relative_path(path)
@@ -531,6 +722,7 @@ module Gemstar
531
722
  when :gems then "gems"
532
723
  when :js then "js"
533
724
  when :python then "python"
725
+ when :cargo then "cargo"
534
726
  else scope.to_s
535
727
  end
536
728
  end
@@ -540,6 +732,7 @@ module Gemstar
540
732
  when :gems then "Gems"
541
733
  when :js then "JS"
542
734
  when :python then "Python"
735
+ when :cargo then "Cargo"
543
736
  else scope.to_s.capitalize
544
737
  end
545
738
  end
@@ -0,0 +1,182 @@
1
+ require "json"
2
+ require "open-uri"
3
+ require "time"
4
+ require "uri"
5
+
6
+ module Gemstar
7
+ class PyPIMetadata
8
+ def initialize(package_name)
9
+ @package_name = package_name
10
+ end
11
+
12
+ attr_reader :package_name
13
+
14
+ alias_method :gem_name, :package_name
15
+
16
+ def cache_key
17
+ "pypi-#{package_name}"
18
+ end
19
+
20
+ def meta(cache_only: false, force_refresh: false)
21
+ return @meta if !cache_only && defined?(@meta)
22
+
23
+ json = if cache_only
24
+ Cache.peek(cache_key)
25
+ else
26
+ url = "https://pypi.org/pypi/#{URI.encode_www_form_component(package_name)}/json"
27
+ Cache.fetch(cache_key, force: force_refresh) do
28
+ URI.parse(url).open(read_timeout: 8).read
29
+ end
30
+ end
31
+
32
+ parsed = begin
33
+ JSON.parse(json) if json
34
+ rescue
35
+ nil
36
+ end
37
+
38
+ normalized = normalize_meta(parsed)
39
+ @meta = normalized unless cache_only
40
+ normalized
41
+ end
42
+
43
+ def repo_uri(cache_only: false, force_refresh: false)
44
+ resolved_meta = meta(cache_only: cache_only, force_refresh: force_refresh)
45
+ return nil unless resolved_meta
46
+
47
+ return @repo_uri if !cache_only && defined?(@repo_uri)
48
+
49
+ uri = resolved_meta["source_code_uri"]
50
+ uri ||= resolved_meta["homepage_uri"] if resolved_meta["homepage_uri"].to_s.include?("github.com")
51
+ uri = normalize_repo_uri(uri)
52
+
53
+ @repo_uri = uri unless cache_only
54
+ uri
55
+ end
56
+
57
+ def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
58
+ requested_versions = Array(versions).compact
59
+ changelog = Gemstar::ChangeLog.new(self)
60
+ if requested_versions.empty?
61
+ changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
62
+ else
63
+ changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: use_github_cli)
64
+ end
65
+ end
66
+
67
+ def registry_release_dates(cache_only: false, force_refresh: false)
68
+ parsed = raw_meta(cache_only: cache_only, force_refresh: force_refresh)
69
+ Array(parsed&.dig("releases")).each_with_object({}) do |(version, files), dates|
70
+ uploaded_at = Array(files).filter_map { |file| file["upload_time_iso_8601"] || file["upload_time"] }.min
71
+ next if version.to_s.empty? || uploaded_at.to_s.empty?
72
+
73
+ dates[version] = Time.parse(uploaded_at).utc.strftime("%b %-d, %Y")
74
+ end
75
+ rescue JSON::ParserError, ArgumentError
76
+ {}
77
+ end
78
+
79
+ def warm_cache(versions: nil)
80
+ meta
81
+ repo_uri
82
+ changelog_sections(versions: versions)
83
+ end
84
+
85
+ def discover_github_tag_sections?
86
+ true
87
+ end
88
+
89
+ def github_tag_candidates(version)
90
+ raw = version.to_s
91
+ candidates = [raw, (raw.start_with?("v") ? raw : "v#{raw}")]
92
+ candidates << "release_#{raw}"
93
+ candidates << "release_v#{raw}" unless raw.start_with?("v")
94
+
95
+ if raw.match?(/\A\d+\.\d\z/)
96
+ padded_minor = "#{raw}0"
97
+ candidates << padded_minor
98
+ candidates << "v#{padded_minor}"
99
+ candidates << "release_#{padded_minor}"
100
+ candidates << "release_v#{padded_minor}"
101
+ end
102
+
103
+ if raw.match?(/\A\d{4}\.\d{1,2}\.\d{1,2}\z/)
104
+ year, month, day = raw.split(".")
105
+ dotted_date = [year, month.rjust(2, "0"), day.rjust(2, "0")].join(".")
106
+ candidates << dotted_date
107
+ candidates << "v#{dotted_date}"
108
+ candidates << "release_#{dotted_date}"
109
+ candidates << "release_v#{dotted_date}"
110
+ end
111
+
112
+ candidates.uniq
113
+ end
114
+
115
+ def github_tag_matches?(_tag_name)
116
+ true
117
+ end
118
+
119
+ private
120
+
121
+ def raw_meta(cache_only: false, force_refresh: false)
122
+ json = if cache_only
123
+ Cache.peek(cache_key)
124
+ else
125
+ url = "https://pypi.org/pypi/#{URI.encode_www_form_component(package_name)}/json"
126
+ Cache.fetch(cache_key, force: force_refresh) do
127
+ URI.parse(url).open(read_timeout: 8).read
128
+ end
129
+ end
130
+
131
+ JSON.parse(json) if json
132
+ end
133
+
134
+ def normalize_meta(parsed)
135
+ return nil unless parsed.is_a?(Hash)
136
+
137
+ info = parsed["info"] || {}
138
+ project_urls = info["project_urls"] || {}
139
+ source_code_uri = project_url(project_urls, "Source", "Source Code", "Code", "Repository", "GitHub")
140
+ homepage_uri = info["home_page"].to_s.empty? ? nil : info["home_page"]
141
+ homepage_uri ||= project_url(project_urls, "Homepage", "Home", "homepage")
142
+ changelog_uri = project_url(project_urls, "Changelog", "Change Log", "Changes", "Release Notes", "Release notes", "History")
143
+
144
+ {
145
+ "name" => info["name"] || package_name,
146
+ "version" => info["version"],
147
+ "info" => info["summary"] || info["description"],
148
+ "homepage_uri" => homepage_uri,
149
+ "source_code_uri" => source_code_uri,
150
+ "project_uri" => info["package_url"] || "https://pypi.org/project/#{package_name}/",
151
+ "documentation_uri" => project_url(project_urls, "Documentation", "Docs", "documentation"),
152
+ "changelog_uri" => changelog_uri
153
+ }
154
+ end
155
+
156
+ def project_url(project_urls, *names)
157
+ normalized_urls = project_urls.transform_keys { |key| key.to_s.downcase.gsub(/[\s_-]+/, "") }
158
+ names.each do |name|
159
+ value = normalized_urls[name.to_s.downcase.gsub(/[\s_-]+/, "")]
160
+ return value unless value.to_s.empty?
161
+ end
162
+
163
+ nil
164
+ end
165
+
166
+ def normalize_repo_uri(uri)
167
+ value = uri.to_s
168
+ return "" if value.empty?
169
+
170
+ value = value.sub(/\Agit\+/, "")
171
+ value = value.sub(/\Agit:\/\//, "https://")
172
+ value = value.sub(/\Ahttp:\/\//, "https://")
173
+ value = value.gsub(/\.git\z/, "")
174
+
175
+ if value.include?("github.com")
176
+ value = value[%r{\Ahttps?://github\.com/[^/]+/[^/]+}] || value
177
+ end
178
+
179
+ value
180
+ end
181
+ end
182
+ end
@@ -88,8 +88,19 @@ 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
98
+ end
99
+
100
+ def correct_changelog_version(version, heading:)
101
+ overrides = package_metadata.dig("changelog", "version_overrides") || {}
102
+ release_date = heading.to_s[/\b\d{4}-\d{2}-\d{2}\b/]
103
+ overrides.fetch([version, release_date].compact.join("@"), version)
93
104
  end
94
105
 
95
106
  def registry_release_dates(cache_only: false, force_refresh: false)
@@ -140,7 +151,11 @@ module Gemstar
140
151
  override_branches = Array(override["branches"]).compact
141
152
  override_branches = [""] if override_branches.empty? && override["raw_base"]
142
153
  return {
143
- base: expand_metadata_template(override["raw_base"] || github_raw_base(repo_uri)),
154
+ base: expand_metadata_template(
155
+ override["raw_base"] || github_raw_base(repo_uri),
156
+ cache_only: cache_only,
157
+ force_refresh: force_refresh
158
+ ),
144
159
  paths: override_paths.empty? ? Gemstar::ChangeLog::DEFAULT_CHANGELOG_PATHS : override_paths,
145
160
  branches: override_branches.empty? ? RemoteRepository.new(github_raw_base(repo_uri)).find_main_branch(cache_only: cache_only, force_refresh: force_refresh) : override_branches
146
161
  }
@@ -166,8 +181,21 @@ module Gemstar
166
181
  repo_uri.sub("https://github.com", "https://raw.githubusercontent.com").chomp("/")
167
182
  end
168
183
 
169
- def expand_metadata_template(value)
170
- value.to_s.gsub("{gem_name}", gem_name)
184
+ def expand_metadata_template(value, cache_only: false, force_refresh: false)
185
+ version = metadata_template_version(cache_only: cache_only, force_refresh: force_refresh)
186
+ value.to_s.gsub("{gem_name}", gem_name).gsub("{version}", version)
187
+ end
188
+
189
+ def metadata_template_version(cache_only: false, force_refresh: false)
190
+ version = meta(cache_only: cache_only, force_refresh: force_refresh)&.dig("version").to_s
191
+ return version unless version.empty?
192
+
193
+ override_versions = package_metadata.dig("changelog", "version_overrides")
194
+ Array(override_versions&.values).compact.map(&:to_s).reject(&:empty?).max_by do |override_version|
195
+ Gem::Version.new(override_version)
196
+ rescue ArgumentError
197
+ Gem::Version.new("0")
198
+ end.to_s
171
199
  end
172
200
 
173
201
  def format_registry_release_date(datetime)