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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +4 -2
- data/lib/gemstar/cache_warmer.rb +51 -4
- data/lib/gemstar/cargo_lock_file.rb +138 -0
- data/lib/gemstar/change_log.rb +287 -58
- data/lib/gemstar/cli.rb +8 -2
- data/lib/gemstar/commands/diff.rb +13 -2
- data/lib/gemstar/commands/server.rb +99 -14
- data/lib/gemstar/crates_io_metadata.rb +140 -0
- data/lib/gemstar/data/ruby_gems_metadata.json +10 -0
- data/lib/gemstar/npm_metadata.rb +2 -2
- data/lib/gemstar/outputs/html.rb +2 -0
- data/lib/gemstar/project.rb +208 -15
- data/lib/gemstar/pypi_metadata.rb +182 -0
- data/lib/gemstar/ruby_gems_metadata.rb +33 -5
- data/lib/gemstar/uv_lock_file.rb +114 -0
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +504 -79
- data/lib/gemstar/web/templates/app.css +137 -4
- data/lib/gemstar/web/templates/app.js.erb +209 -23
- data/lib/gemstar.rb +4 -0
- metadata +8 -4
data/lib/gemstar/change_log.rb
CHANGED
|
@@ -2,17 +2,31 @@
|
|
|
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
|
|
15
|
+
MAX_CHANGELOG_REDIRECTS = 3
|
|
16
|
+
CHANGELOG_REDIRECT_PATTERN = /\b(?:change\s*log|changelog|release\s+notes?)\b.{0,160}\b(?:moved|relocated|now\s+lives|lives\s+here)\b/im
|
|
10
17
|
DEFAULT_CHANGELOG_PATHS = %w[
|
|
11
|
-
CHANGELOG.md releases.md CHANGES.md
|
|
18
|
+
CHANGELOG.md CHANGELOG releases.md CHANGES.md
|
|
12
19
|
Changelog.md changelog.md ChangeLog.md
|
|
13
20
|
Changes.md changes.md
|
|
14
21
|
HISTORY.md History.md history.md
|
|
15
|
-
History
|
|
22
|
+
History
|
|
23
|
+
RELEASE_NOTES.md Release_Notes.md release_notes.md
|
|
24
|
+
RELEASE-NOTES.md Release-Notes.md release-notes.md
|
|
25
|
+
CHANGELOG.rst Changelog.rst changelog.rst ChangeLog.rst
|
|
26
|
+
CHANGES.rst Changes.rst changes.rst
|
|
27
|
+
HISTORY.rst History.rst history.rst
|
|
28
|
+
CHANGES Changes
|
|
29
|
+
CHANGELOG.rdoc
|
|
16
30
|
].freeze
|
|
17
31
|
|
|
18
32
|
def initialize(metadata)
|
|
@@ -33,7 +47,7 @@ module Gemstar
|
|
|
33
47
|
return @sections if !cache_only && defined?(@sections) && !force_refresh
|
|
34
48
|
|
|
35
49
|
metadata_key = @metadata.respond_to?(:cache_key) ? @metadata.cache_key : @metadata.gem_name
|
|
36
|
-
cache_key = "sections-
|
|
50
|
+
cache_key = "sections-v10-#{metadata_key}"
|
|
37
51
|
serialized = if cache_only
|
|
38
52
|
Cache.peek(cache_key)
|
|
39
53
|
else
|
|
@@ -54,7 +68,7 @@ module Gemstar
|
|
|
54
68
|
result
|
|
55
69
|
end
|
|
56
70
|
|
|
57
|
-
def sections_for_versions(versions, cache_only: false, force_refresh: false)
|
|
71
|
+
def sections_for_versions(versions, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
58
72
|
requested_versions = normalize_requested_versions(versions)
|
|
59
73
|
return {} if requested_versions.empty?
|
|
60
74
|
|
|
@@ -75,7 +89,8 @@ module Gemstar
|
|
|
75
89
|
repo_uri,
|
|
76
90
|
version,
|
|
77
91
|
cache_only: false,
|
|
78
|
-
force_refresh: force_refresh
|
|
92
|
+
force_refresh: force_refresh,
|
|
93
|
+
use_github_cli: use_github_cli
|
|
79
94
|
)
|
|
80
95
|
result.merge!(specific_release) if specific_release
|
|
81
96
|
end
|
|
@@ -176,25 +191,10 @@ module Gemstar
|
|
|
176
191
|
value = version.to_s.strip
|
|
177
192
|
return nil if value.empty?
|
|
178
193
|
|
|
179
|
-
value.sub(/\Av/i, "")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# and avoiding returning a date string when both are present.
|
|
184
|
-
def extract_version_from_heading(line)
|
|
185
|
-
return nil unless line
|
|
186
|
-
heading = line.to_s
|
|
187
|
-
version_token = /(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)/
|
|
188
|
-
# 1) Prefer version inside parentheses after a date: "### 2025-11-07 (2.16.0)"
|
|
189
|
-
# Ensure we ONLY treat it as a version if it actually looks like a version (has a dot),
|
|
190
|
-
# so we don't capture dates like (2025-11-21).
|
|
191
|
-
return $1 if heading[/\(\s*v?#{version_token}(?![A-Za-z0-9])\s*\)/]
|
|
192
|
-
# 2) Version-first with optional leading markers/labels: "## v1.2.6 - 2025-10-21"
|
|
193
|
-
# Require a dot in the numeric token to avoid capturing dates like 2025-11-21.
|
|
194
|
-
return $1 if heading[/^\s*(?:[-*]\s+)?(?:#+|=+)?\s*(?:Version\s+)?\[*v?#{version_token}(?![A-Za-z0-9])\]*/i]
|
|
195
|
-
# 3) Anywhere: first semver-like token with a dot
|
|
196
|
-
return $1 if heading[/\bv?#{version_token}(?![A-Za-z0-9])\b/]
|
|
197
|
-
nil
|
|
194
|
+
value = value.sub(/\Av/i, "")
|
|
195
|
+
Gem::Version.new(value.gsub(/-[\w\-]+$/, "")).canonical_segments.join(".")
|
|
196
|
+
rescue ArgumentError
|
|
197
|
+
value
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
def extract_release_date_from_heading(line)
|
|
@@ -205,23 +205,24 @@ module Gemstar
|
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
def changelog_uri_candidates(cache_only: false, force_refresh: false)
|
|
208
|
-
candidates = []
|
|
209
|
-
|
|
210
208
|
repo_uri = @metadata.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
211
209
|
return [] if repo_uri.nil? || repo_uri.empty?
|
|
212
210
|
|
|
213
211
|
meta = @metadata.meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
214
|
-
|
|
212
|
+
explicit_candidates = meta ? changelog_uri_markdown_candidates(meta["changelog_uri"]) : []
|
|
215
213
|
|
|
216
214
|
changelog_source = metadata_changelog_source(repo_uri, cache_only: cache_only, force_refresh: force_refresh)
|
|
217
215
|
return [] unless changelog_source
|
|
218
216
|
|
|
219
|
-
|
|
217
|
+
repository_candidates = changelog_source[:paths].product(changelog_source[:branches]).map do |file, branch|
|
|
220
218
|
[changelog_source[:base], branch, file].reject { |segment| segment.to_s.empty? }.join("/")
|
|
221
219
|
end
|
|
222
220
|
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
candidates = if version_pinned_changelog_uri?(meta)
|
|
222
|
+
repository_candidates + explicit_candidates
|
|
223
|
+
else
|
|
224
|
+
explicit_candidates + repository_candidates
|
|
225
|
+
end
|
|
225
226
|
|
|
226
227
|
candidates.flatten!
|
|
227
228
|
candidates.uniq!
|
|
@@ -230,12 +231,24 @@ module Gemstar
|
|
|
230
231
|
candidates
|
|
231
232
|
end
|
|
232
233
|
|
|
234
|
+
def version_pinned_changelog_uri?(meta)
|
|
235
|
+
changelog_uri = meta&.fetch("changelog_uri", nil).to_s
|
|
236
|
+
version = meta&.fetch("version", nil).to_s.sub(/\Av/i, "")
|
|
237
|
+
return false if changelog_uri.empty? || version.empty?
|
|
238
|
+
|
|
239
|
+
raw_uri = Gemstar::GitHub::github_blob_to_raw(changelog_uri)
|
|
240
|
+
path_segments = URI(raw_uri).path.split("/")
|
|
241
|
+
path_segments.include?(version) || path_segments.include?("v#{version}")
|
|
242
|
+
rescue URI::InvalidURIError
|
|
243
|
+
false
|
|
244
|
+
end
|
|
245
|
+
|
|
233
246
|
def changelog_uri_markdown_candidates(changelog_uri)
|
|
234
247
|
raw_uri = Gemstar::GitHub::github_blob_to_raw(changelog_uri)
|
|
235
248
|
return [] if raw_uri.to_s.empty?
|
|
236
249
|
|
|
237
250
|
candidates = []
|
|
238
|
-
candidates << raw_uri if raw_uri.match?(/\.(?:md|markdown|rdoc|txt)\z/i)
|
|
251
|
+
candidates << raw_uri if raw_uri.match?(/\.(?:md|markdown|rdoc|rst|txt)\z/i)
|
|
239
252
|
|
|
240
253
|
begin
|
|
241
254
|
uri = URI(raw_uri)
|
|
@@ -243,9 +256,14 @@ module Gemstar
|
|
|
243
256
|
if path.end_with?("/")
|
|
244
257
|
uri.path = "#{path.chomp("/")}.md"
|
|
245
258
|
candidates << uri.to_s
|
|
259
|
+
uri.path = "#{path.chomp("/")}.rst"
|
|
260
|
+
candidates << uri.to_s
|
|
246
261
|
elsif File.extname(path).empty?
|
|
262
|
+
candidates << raw_uri if uri.host == "raw.githubusercontent.com"
|
|
247
263
|
uri.path = "#{path}.md"
|
|
248
264
|
candidates << uri.to_s
|
|
265
|
+
uri.path = "#{path}.rst"
|
|
266
|
+
candidates << uri.to_s
|
|
249
267
|
end
|
|
250
268
|
rescue URI::InvalidURIError
|
|
251
269
|
nil
|
|
@@ -271,15 +289,14 @@ module Gemstar
|
|
|
271
289
|
content = nil
|
|
272
290
|
|
|
273
291
|
changelog_uri_candidates(cache_only: cache_only, force_refresh: force_refresh).find do |candidate|
|
|
274
|
-
content =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
end
|
|
292
|
+
content = fetch_changelog_uri(candidate, cache_only: cache_only, force_refresh: force_refresh)
|
|
293
|
+
if content
|
|
294
|
+
content = follow_changelog_redirects(
|
|
295
|
+
content,
|
|
296
|
+
source_uri: candidate,
|
|
297
|
+
cache_only: cache_only,
|
|
298
|
+
force_refresh: force_refresh
|
|
299
|
+
)
|
|
283
300
|
end
|
|
284
301
|
|
|
285
302
|
# puts "fetch_changelog_content #{candidate}:\n#{content}" if Gemstar.debug?
|
|
@@ -294,13 +311,74 @@ module Gemstar
|
|
|
294
311
|
content
|
|
295
312
|
end
|
|
296
313
|
|
|
314
|
+
def fetch_changelog_uri(uri, cache_only:, force_refresh:)
|
|
315
|
+
if cache_only
|
|
316
|
+
Cache.peek("changelog-#{uri}")
|
|
317
|
+
else
|
|
318
|
+
Cache.fetch("changelog-#{uri}", force: force_refresh) do
|
|
319
|
+
URI.open(uri, read_timeout: 8)&.read
|
|
320
|
+
rescue => e
|
|
321
|
+
puts "#{uri}: #{e}" if Gemstar.debug?
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def follow_changelog_redirects(content, source_uri:, cache_only:, force_refresh:)
|
|
328
|
+
current_content = content
|
|
329
|
+
current_uri = source_uri
|
|
330
|
+
visited = {source_uri => true}
|
|
331
|
+
|
|
332
|
+
MAX_CHANGELOG_REDIRECTS.times do
|
|
333
|
+
redirect_uri = changelog_redirect_uri(current_content, current_uri)
|
|
334
|
+
break if redirect_uri.nil? || visited[redirect_uri]
|
|
335
|
+
|
|
336
|
+
redirected_content = fetch_changelog_uri(
|
|
337
|
+
redirect_uri,
|
|
338
|
+
cache_only: cache_only,
|
|
339
|
+
force_refresh: force_refresh
|
|
340
|
+
)
|
|
341
|
+
break if redirected_content.nil? || redirected_content.strip.empty?
|
|
342
|
+
|
|
343
|
+
puts "Following changelog redirect: #{current_uri} -> #{redirect_uri}" if Gemstar.debug?
|
|
344
|
+
visited[redirect_uri] = true
|
|
345
|
+
current_uri = redirect_uri
|
|
346
|
+
current_content = redirected_content
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
current_content
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def changelog_redirect_uri(content, source_uri)
|
|
353
|
+
text = content.to_s
|
|
354
|
+
return nil if text.bytesize > 4096 || text.lines.count > 12
|
|
355
|
+
return nil unless text.match?(CHANGELOG_REDIRECT_PATTERN)
|
|
356
|
+
|
|
357
|
+
links = text.scan(/\[([^\]]+)\]\(\s*<?([^\s)>]+)>?(?:\s+["'][^"']*["'])?\s*\)/)
|
|
358
|
+
redirect_link = links.find { |link_label, _| link_label.match?(/\A(?:here|changelog|change\s*log|release\s+notes?)\z/i) }
|
|
359
|
+
redirect_link ||= links.find { |_, link_href| link_href.match?(%r{(?:change(?:log|s)|history|releases?)}i) }
|
|
360
|
+
href = redirect_link&.last
|
|
361
|
+
return nil if href.nil?
|
|
362
|
+
|
|
363
|
+
resolved = URI.join(source_uri, CGI.unescapeHTML(href)).to_s
|
|
364
|
+
uri = URI(Gemstar::GitHub::github_blob_to_raw(resolved))
|
|
365
|
+
return nil unless %w[http https].include?(uri.scheme)
|
|
366
|
+
|
|
367
|
+
uri.fragment = nil
|
|
368
|
+
uri.to_s
|
|
369
|
+
rescue URI::InvalidURIError
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
|
|
297
373
|
VERSION_PATTERNS = [
|
|
298
|
-
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d
|
|
299
|
-
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\[*v?(\d
|
|
300
|
-
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[*v?(\d
|
|
301
|
-
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?\[*v?(\d
|
|
302
|
-
/^\s*(?:[
|
|
374
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\d{4}-\d{2}-\d{2}\s*\(\s*v?(\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9]|\s*%)\s*\)/, # prefer this
|
|
375
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*\[*v?(\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9]|\s*%)\]*\s*(?:—|–|-)\s*\d{4}-\d{2}-\d{2}\b/,
|
|
376
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?(?:(?:[^\s\d][^\s]*\s+)+)\[*v?(\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9]|\s*%)\]*(?:\s*[-(].*)?/i,
|
|
377
|
+
/^\s*(?:[-*]\s+)?(?:#+|=+)\s*(?:Version\s+)?\[*v?(\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9]|\s*%)\]*(?:\s*[-(].*)?/i,
|
|
378
|
+
/^\s*(?:[-+*]\s+)?Starting with version\s+v?(\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9]|\s*%)/i,
|
|
379
|
+
/^\s*(?:[-+*]\s+)?(?:Version\s+)?v?(\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9]|\s*%)(?:\s*[-(].*)?\s*$/i
|
|
303
380
|
]
|
|
381
|
+
RST_ADORNMENT_PATTERN = /^\s*[=\-~`^"']{3,}\s*$/
|
|
304
382
|
|
|
305
383
|
def parse_changelog_sections(cache_only: false, force_refresh: false)
|
|
306
384
|
# If the fetched content looks like a GitHub Releases HTML page, return {}
|
|
@@ -330,11 +408,13 @@ module Gemstar
|
|
|
330
408
|
lines.each do |line|
|
|
331
409
|
# Convert rdoc to markdown:
|
|
332
410
|
line = line.gsub(/^=+/) { |m| "#" * m.length }
|
|
411
|
+
next if line.match?(/^\s*\.\.\s+_v?[\w.-]+:/)
|
|
412
|
+
next if line.match?(RST_ADORNMENT_PATTERN)
|
|
333
413
|
|
|
334
414
|
m = VERSION_PATTERNS.lazy.map { |re| line.match(re) }.find(&:itself)
|
|
335
415
|
|
|
336
416
|
if m
|
|
337
|
-
new_key =
|
|
417
|
+
new_key = corrected_changelog_version(m[1], line)
|
|
338
418
|
|
|
339
419
|
if current_key
|
|
340
420
|
sections[current_key] ||= []
|
|
@@ -369,27 +449,37 @@ module Gemstar
|
|
|
369
449
|
|
|
370
450
|
c.lines.each_with_object({}) do |line, dates|
|
|
371
451
|
line = line.gsub(/^=+/) { |m| "#" * m.length }
|
|
372
|
-
|
|
452
|
+
match = VERSION_PATTERNS.lazy.map { |pattern| line.match(pattern) }.find(&:itself)
|
|
453
|
+
next unless match
|
|
373
454
|
|
|
374
|
-
version =
|
|
455
|
+
version = corrected_changelog_version(match[1], line)
|
|
375
456
|
date = extract_release_date_from_heading(line)
|
|
376
457
|
dates[version] ||= date if version && date
|
|
377
458
|
end
|
|
378
459
|
end
|
|
379
460
|
|
|
380
|
-
def
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
end
|
|
461
|
+
def corrected_changelog_version(version, heading)
|
|
462
|
+
return version unless @metadata.respond_to?(:correct_changelog_version)
|
|
463
|
+
|
|
464
|
+
@metadata.correct_changelog_version(version, heading: heading) || version
|
|
465
|
+
end
|
|
386
466
|
|
|
467
|
+
def parse_github_release_sections(cache_only: false, force_refresh: false)
|
|
387
468
|
repo_uri = @metadata&.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
388
469
|
return {} unless repo_uri&.include?("github.com")
|
|
389
470
|
|
|
390
471
|
url = github_releases_url(repo_uri)
|
|
391
472
|
return {} unless url
|
|
392
473
|
|
|
474
|
+
api_sections = parse_github_api_release_sections(repo_uri, cache_only: cache_only, force_refresh: force_refresh)
|
|
475
|
+
return api_sections unless api_sections.empty?
|
|
476
|
+
|
|
477
|
+
begin
|
|
478
|
+
require "nokogiri"
|
|
479
|
+
rescue LoadError
|
|
480
|
+
return {}
|
|
481
|
+
end
|
|
482
|
+
|
|
393
483
|
html = if cache_only
|
|
394
484
|
Cache.peek("releases-#{url}")
|
|
395
485
|
else
|
|
@@ -506,11 +596,90 @@ module Gemstar
|
|
|
506
596
|
"#{repo}/tags"
|
|
507
597
|
end
|
|
508
598
|
|
|
509
|
-
def
|
|
599
|
+
def github_releases_api_url(repo_uri = @metadata&.repo_uri)
|
|
600
|
+
repo_path = github_repo_path(repo_uri)
|
|
601
|
+
return nil unless repo_path
|
|
602
|
+
|
|
603
|
+
"https://api.github.com/repos/#{repo_path}/releases"
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def github_release_api_url(repo_uri, tag)
|
|
607
|
+
repo_path = github_repo_path(repo_uri)
|
|
608
|
+
return nil unless repo_path
|
|
609
|
+
|
|
610
|
+
"https://api.github.com/repos/#{repo_path}/releases/tags/#{URI.encode_www_form_component(tag)}"
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def github_repo_path(repo_uri)
|
|
614
|
+
uri = URI(repo_uri.to_s)
|
|
615
|
+
return nil unless uri.host.to_s.end_with?("github.com")
|
|
616
|
+
|
|
617
|
+
segments = uri.path.to_s.split("/").reject(&:empty?)
|
|
618
|
+
return nil if segments.length < 2
|
|
619
|
+
|
|
620
|
+
segments.take(2).join("/")
|
|
621
|
+
rescue URI::InvalidURIError
|
|
622
|
+
nil
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def parse_github_api_release_sections(repo_uri, cache_only:, force_refresh:)
|
|
626
|
+
url = github_releases_api_url(repo_uri)
|
|
627
|
+
return {} unless url
|
|
628
|
+
|
|
629
|
+
json = if cache_only
|
|
630
|
+
Cache.peek("releases-api-#{url}")
|
|
631
|
+
else
|
|
632
|
+
Cache.fetch("releases-api-#{url}", force: force_refresh) do
|
|
633
|
+
begin
|
|
634
|
+
URI.open(url, "Accept" => "application/vnd.github+json", read_timeout: 8)&.read
|
|
635
|
+
rescue => e
|
|
636
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
637
|
+
nil
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
parse_github_api_releases(json)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def parse_github_api_releases(json)
|
|
646
|
+
releases = JSON.parse(json.to_s)
|
|
647
|
+
releases = [releases] if releases.is_a?(Hash)
|
|
648
|
+
return {} unless releases.is_a?(Array)
|
|
649
|
+
|
|
650
|
+
releases.each_with_object({}) do |release, sections|
|
|
651
|
+
next unless release.is_a?(Hash)
|
|
652
|
+
|
|
653
|
+
tag_name = (release["tag_name"] || release["tagName"]).to_s
|
|
654
|
+
next if tag_name.empty?
|
|
655
|
+
next unless github_tag_matches_metadata?(tag_name)
|
|
656
|
+
|
|
657
|
+
version = normalize_github_tag_version(tag_name)
|
|
658
|
+
next if version.to_s.empty?
|
|
659
|
+
|
|
660
|
+
body = release["body"].to_s.strip
|
|
661
|
+
title = release["name"].to_s.strip
|
|
662
|
+
content = body.empty? ? title : body
|
|
663
|
+
next if content.empty?
|
|
664
|
+
|
|
665
|
+
sections[version] ||= ["## #{version}\n", content]
|
|
666
|
+
end
|
|
667
|
+
rescue JSON::ParserError
|
|
668
|
+
{}
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def parse_specific_github_release_pages(repo_uri, version, cache_only:, force_refresh:, use_github_cli: false)
|
|
510
672
|
return {} unless repo_uri&.include?("github.com")
|
|
511
673
|
return {} if version.to_s.empty?
|
|
512
674
|
|
|
513
|
-
|
|
675
|
+
github_tag_candidates(version).each do |tag|
|
|
676
|
+
cli_section = parse_specific_github_cli_release(repo_uri, tag, cache_only: cache_only, force_refresh: force_refresh) if use_github_cli
|
|
677
|
+
return cli_section if cli_section && !cli_section.empty?
|
|
678
|
+
|
|
679
|
+
api_section = parse_specific_github_api_release(repo_uri, tag, cache_only: cache_only, force_refresh: force_refresh)
|
|
680
|
+
return api_section unless api_section.empty?
|
|
681
|
+
|
|
682
|
+
url = github_release_tag_url(repo_uri, tag)
|
|
514
683
|
html = if cache_only
|
|
515
684
|
Cache.peek("releases-#{url}")
|
|
516
685
|
else
|
|
@@ -533,6 +702,62 @@ module Gemstar
|
|
|
533
702
|
{}
|
|
534
703
|
end
|
|
535
704
|
|
|
705
|
+
def parse_specific_github_cli_release(repo_uri, tag_name, cache_only:, force_refresh:)
|
|
706
|
+
repo_path = github_repo_path(repo_uri)
|
|
707
|
+
return {} unless repo_path
|
|
708
|
+
|
|
709
|
+
cache_key = "releases-gh-#{repo_path}-#{tag_name}"
|
|
710
|
+
json = if cache_only
|
|
711
|
+
Cache.peek(cache_key)
|
|
712
|
+
else
|
|
713
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
714
|
+
fetch_github_cli_release_json(repo_path, tag_name)
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
parse_github_api_releases(json)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def fetch_github_cli_release_json(repo_path, tag_name)
|
|
722
|
+
stdout, _stderr, status = nil
|
|
723
|
+
Timeout.timeout(GITHUB_CLI_TIMEOUT) do
|
|
724
|
+
stdout, _stderr, status = Open3.capture3(
|
|
725
|
+
"gh",
|
|
726
|
+
"release",
|
|
727
|
+
"view",
|
|
728
|
+
tag_name,
|
|
729
|
+
"--repo",
|
|
730
|
+
repo_path,
|
|
731
|
+
"--json",
|
|
732
|
+
"body,name,tagName"
|
|
733
|
+
)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
status.success? ? stdout : nil
|
|
737
|
+
rescue Errno::ENOENT, Timeout::Error
|
|
738
|
+
nil
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def parse_specific_github_api_release(repo_uri, tag_name, cache_only:, force_refresh:)
|
|
742
|
+
url = github_release_api_url(repo_uri, URI.decode_www_form_component(tag_name.to_s))
|
|
743
|
+
return {} unless url
|
|
744
|
+
|
|
745
|
+
json = if cache_only
|
|
746
|
+
Cache.peek("releases-api-#{url}")
|
|
747
|
+
else
|
|
748
|
+
Cache.fetch("releases-api-#{url}", force: force_refresh) do
|
|
749
|
+
begin
|
|
750
|
+
URI.open(url, "Accept" => "application/vnd.github+json", read_timeout: 8)&.read
|
|
751
|
+
rescue => e
|
|
752
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
753
|
+
nil
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
parse_github_api_releases(json)
|
|
759
|
+
end
|
|
760
|
+
|
|
536
761
|
def parse_github_tag_sections(repo_uri, cache_only:, force_refresh:)
|
|
537
762
|
return {} unless repo_uri&.include?("github.com")
|
|
538
763
|
|
|
@@ -760,11 +985,15 @@ module Gemstar
|
|
|
760
985
|
|
|
761
986
|
def github_release_tag_urls(repo_url, version)
|
|
762
987
|
github_tag_candidates(version).map do |tag|
|
|
763
|
-
|
|
764
|
-
"#{repo_url}/releases/tag/#{encoded_tag}"
|
|
988
|
+
github_release_tag_url(repo_url, tag)
|
|
765
989
|
end.uniq
|
|
766
990
|
end
|
|
767
991
|
|
|
992
|
+
def github_release_tag_url(repo_url, tag)
|
|
993
|
+
encoded_tag = URI.encode_www_form_component(tag)
|
|
994
|
+
"#{repo_url}/releases/tag/#{encoded_tag}"
|
|
995
|
+
end
|
|
996
|
+
|
|
768
997
|
def github_tag_candidates(version)
|
|
769
998
|
return @metadata.github_tag_candidates(version) if @metadata.respond_to?(:github_tag_candidates)
|
|
770
999
|
|
data/lib/gemstar/cli.rb
CHANGED
|
@@ -5,7 +5,8 @@ module Gemstar
|
|
|
5
5
|
class CLI < Thor
|
|
6
6
|
package_name "gemstar"
|
|
7
7
|
|
|
8
|
-
map "-D" => "diff"
|
|
8
|
+
map "-D" => "diff",
|
|
9
|
+
"--version" => "version"
|
|
9
10
|
|
|
10
11
|
class_option :verbose, type: :boolean, default: false, desc: "Enable verbose output"
|
|
11
12
|
class_option :lockfile, type: :string, default: "Gemfile.lock", desc: "Lockfile path"
|
|
@@ -15,7 +16,7 @@ module Gemstar
|
|
|
15
16
|
method_option :to, type: :string, desc: "Git ref or lockfile"
|
|
16
17
|
method_option :since, type: :string, desc: "Set --from to the latest commit before this relative time (for example: '3 weeks')"
|
|
17
18
|
method_option :project, type: :string, desc: "Project directory or supported project file path"
|
|
18
|
-
method_option :ecosystem, type: :string, desc: "Filter packages by ecosystem (all, gems, js)"
|
|
19
|
+
method_option :ecosystem, type: :string, desc: "Filter packages by ecosystem (all, gems, js, python, cargo)"
|
|
19
20
|
method_option :format, type: :string, desc: "Output format (html or markdown)"
|
|
20
21
|
method_option :output_file, type: :string, desc: "Output file path"
|
|
21
22
|
method_option :debug_gem_regex, type: :string, desc: "Debug matching gems", hide: true
|
|
@@ -36,6 +37,11 @@ module Gemstar
|
|
|
36
37
|
desc "cache SUBCOMMAND ...ARGS", "Manage gemstar caches"
|
|
37
38
|
subcommand "cache", Gemstar::CacheCLI
|
|
38
39
|
|
|
40
|
+
desc "version", "Print gemstar version"
|
|
41
|
+
def version
|
|
42
|
+
puts Gemstar::VERSION
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
# desc "pick", "Interactively cherry-pick and upgrade gems"
|
|
40
46
|
# option :gem, type: :string, desc: "Gem name to cherry-pick"
|
|
41
47
|
# def pick
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "command"
|
|
4
|
+
require_relative "../pypi_metadata"
|
|
5
|
+
require_relative "../crates_io_metadata"
|
|
4
6
|
require "concurrent-ruby"
|
|
5
7
|
require "tmpdir"
|
|
6
8
|
require "pathname"
|
|
@@ -70,9 +72,9 @@ module Gemstar
|
|
|
70
72
|
def normalize_ecosystem(value)
|
|
71
73
|
normalized = value.to_s.strip.downcase
|
|
72
74
|
return "all" if normalized.empty?
|
|
73
|
-
return normalized if %w[all gems js].include?(normalized)
|
|
75
|
+
return normalized if %w[all gems js python cargo].include?(normalized)
|
|
74
76
|
|
|
75
|
-
raise Thor::Error, "Unsupported ecosystem #{value.inspect}. Expected one of: all, gems, js"
|
|
77
|
+
raise Thor::Error, "Unsupported ecosystem #{value.inspect}. Expected one of: all, gems, js, python, cargo"
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def normalize_since(value)
|
|
@@ -210,6 +212,11 @@ module Gemstar
|
|
|
210
212
|
def metadata_for(package_state)
|
|
211
213
|
if package_state[:package_scope] == "js"
|
|
212
214
|
Gemstar::NpmMetadata.new(package_state[:name])
|
|
215
|
+
elsif package_state[:package_scope] == "python"
|
|
216
|
+
Gemstar::PyPIMetadata.new(package_state[:name])
|
|
217
|
+
elsif package_state[:package_scope] == "cargo"
|
|
218
|
+
package_name = package_state[:metadata_package_name] || package_state.dig(:source, :package_name) || package_state[:name]
|
|
219
|
+
Gemstar::CratesIOMetadata.new(package_name)
|
|
213
220
|
else
|
|
214
221
|
Gemstar::RubyGemsMetadata.new(package_state[:name])
|
|
215
222
|
end
|
|
@@ -315,6 +322,10 @@ module Gemstar
|
|
|
315
322
|
"importmap"
|
|
316
323
|
when :package_lock
|
|
317
324
|
"package-lock"
|
|
325
|
+
when :uv_lock
|
|
326
|
+
"uv.lock"
|
|
327
|
+
when :cargo_lock
|
|
328
|
+
"Cargo.lock"
|
|
318
329
|
else
|
|
319
330
|
package_state[:package_scope]
|
|
320
331
|
end
|