gemstar 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/gemstar/change_log.rb +220 -4
- data/lib/gemstar/commands/server.rb +1 -0
- data/lib/gemstar/lock_file.rb +91 -9
- data/lib/gemstar/project.rb +6 -2
- data/lib/gemstar/version.rb +1 -2
- data/lib/gemstar/web/app.rb +41 -3
- data/lib/gemstar/web/templates/app.css +5 -5
- data/lib/gemstar/web/templates/app.js.erb +75 -6
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bea143c3f9b89aca665d0b74402cb7f16ec65b6994139a187f406c3de800eca
|
|
4
|
+
data.tar.gz: f9bc3eeba8f76decc7dbbe215e377a3130e1ffbbaa5d22f405604974ab99741a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b857d1cc340eab567266177d927710b13d9f569d07bea5d59332a679c3f57e827c26d9ce6d8154968cd624ac1e1b41723778d9bbc3c6ed6fede5af147fdffbba
|
|
7
|
+
data.tar.gz: d2da645e58549938abf27c2c10a53c1eca1b2a8d842ef7f41ce85298e0a47e4b4d8a0bf152843ad080c0cf2b112f6882a5628a93cd617f1933e727d2e2992d39
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 1.0.4
|
|
4
|
+
|
|
5
|
+
- Server: Improve layout, detail panel state management, and initial gem selection
|
|
6
|
+
- Server: Gem details now in collapsible panel
|
|
7
|
+
- Server: Arrow left/right navigation improved
|
|
8
|
+
- Enhance changelog parsing with GitHub tag and release support
|
|
9
|
+
- Extend `LockFile` with dependency requirements, spec sources, and platform parsing
|
|
10
|
+
- Refactor dependency processing to include platform and source details
|
|
11
|
+
- Refactor changelog parsing to merge GitHub release and changelog sections
|
|
12
|
+
|
|
13
|
+
## 1.0.3
|
|
14
|
+
|
|
15
|
+
- Load our own WEBrick to avoid conflicts with hosting puma.rb etc.
|
|
16
|
+
|
|
3
17
|
## 1.0.2
|
|
4
18
|
|
|
5
19
|
- General server performance improvements.
|
data/lib/gemstar/change_log.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "cgi"
|
|
2
3
|
|
|
3
4
|
module Gemstar
|
|
4
5
|
class ChangeLog
|
|
@@ -22,10 +23,10 @@ module Gemstar
|
|
|
22
23
|
return @sections if !cache_only && defined?(@sections)
|
|
23
24
|
|
|
24
25
|
result = begin
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
changelog_sections = parse_changelog_sections(cache_only: cache_only, force_refresh: force_refresh) || {}
|
|
27
|
+
github_sections = parse_github_release_sections(cache_only: cache_only, force_refresh: force_refresh) || {}
|
|
28
|
+
|
|
29
|
+
s = merge_section_sources(changelog_sections, github_sections)
|
|
29
30
|
|
|
30
31
|
pp @@candidates_found if Gemstar.debug? && !cache_only
|
|
31
32
|
|
|
@@ -36,6 +37,13 @@ module Gemstar
|
|
|
36
37
|
result
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def merge_section_sources(changelog_sections, github_sections)
|
|
41
|
+
return github_sections if changelog_sections.nil? || changelog_sections.empty?
|
|
42
|
+
return changelog_sections if github_sections.nil? || github_sections.empty?
|
|
43
|
+
|
|
44
|
+
github_sections.merge(changelog_sections)
|
|
45
|
+
end
|
|
46
|
+
|
|
39
47
|
def extract_relevant_sections(old_version, new_version)
|
|
40
48
|
from = Gem::Version.new(old_version.gsub(/-[\w\-]+$/, "")) rescue nil if old_version
|
|
41
49
|
from ||= Gem::Version.new("0.0.0")
|
|
@@ -294,6 +302,22 @@ module Gemstar
|
|
|
294
302
|
end
|
|
295
303
|
end
|
|
296
304
|
|
|
305
|
+
if sections.empty?
|
|
306
|
+
current_version = @metadata&.meta(cache_only: cache_only, force_refresh: force_refresh)&.dig("version")
|
|
307
|
+
current_release_sections = parse_specific_github_release_pages(
|
|
308
|
+
repo_uri,
|
|
309
|
+
current_version,
|
|
310
|
+
cache_only: cache_only,
|
|
311
|
+
force_refresh: force_refresh
|
|
312
|
+
)
|
|
313
|
+
tag_sections = parse_github_tag_sections(
|
|
314
|
+
repo_uri,
|
|
315
|
+
cache_only: cache_only,
|
|
316
|
+
force_refresh: force_refresh
|
|
317
|
+
)
|
|
318
|
+
sections = tag_sections.merge(current_release_sections)
|
|
319
|
+
end
|
|
320
|
+
|
|
297
321
|
if Gemstar.debug?
|
|
298
322
|
puts "parse_github_release_sections #{@metadata.gem_name}:"
|
|
299
323
|
pp sections.keys
|
|
@@ -308,5 +332,197 @@ module Gemstar
|
|
|
308
332
|
return nil if repo.empty?
|
|
309
333
|
"#{repo}/releases"
|
|
310
334
|
end
|
|
335
|
+
|
|
336
|
+
def github_tags_url(repo_uri = @metadata&.repo_uri)
|
|
337
|
+
return nil unless repo_uri
|
|
338
|
+
repo = repo_uri.chomp("/")
|
|
339
|
+
return nil if repo.empty?
|
|
340
|
+
"#{repo}/tags"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def parse_specific_github_release_pages(repo_uri, version, cache_only:, force_refresh:)
|
|
344
|
+
return {} unless repo_uri&.include?("github.com")
|
|
345
|
+
return {} if version.to_s.empty?
|
|
346
|
+
|
|
347
|
+
github_release_tag_urls(repo_uri, version).each do |url|
|
|
348
|
+
html = if cache_only
|
|
349
|
+
Cache.peek("releases-#{url}")
|
|
350
|
+
else
|
|
351
|
+
Cache.fetch("releases-#{url}", force: force_refresh) do
|
|
352
|
+
begin
|
|
353
|
+
URI.open(url, read_timeout: 8)&.read
|
|
354
|
+
rescue => e
|
|
355
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
356
|
+
nil
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
next if html.nil? || html.strip.empty?
|
|
362
|
+
|
|
363
|
+
section = parse_single_github_release_page(html, version)
|
|
364
|
+
return { version => section } if section
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
{}
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def parse_github_tag_sections(repo_uri, cache_only:, force_refresh:)
|
|
371
|
+
return {} unless repo_uri&.include?("github.com")
|
|
372
|
+
|
|
373
|
+
url = github_tags_url(repo_uri)
|
|
374
|
+
return {} unless url
|
|
375
|
+
|
|
376
|
+
sections = {}
|
|
377
|
+
seen_urls = {}
|
|
378
|
+
|
|
379
|
+
while url && !seen_urls[url]
|
|
380
|
+
seen_urls[url] = true
|
|
381
|
+
html = if cache_only
|
|
382
|
+
Cache.peek("tags-#{url}")
|
|
383
|
+
else
|
|
384
|
+
Cache.fetch("tags-#{url}", force: force_refresh) do
|
|
385
|
+
begin
|
|
386
|
+
URI.open(url, read_timeout: 8)&.read
|
|
387
|
+
rescue => e
|
|
388
|
+
puts "#{url}: #{e}" if Gemstar.debug?
|
|
389
|
+
nil
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
break if html.nil? || html.strip.empty?
|
|
395
|
+
|
|
396
|
+
page_sections, next_url = parse_single_github_tags_page(html, repo_uri)
|
|
397
|
+
sections.merge!(page_sections) { |_version, existing, _new| existing }
|
|
398
|
+
url = next_url
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
sections.keys.each do |version|
|
|
402
|
+
specific_release = parse_specific_github_release_pages(
|
|
403
|
+
repo_uri,
|
|
404
|
+
version,
|
|
405
|
+
cache_only: cache_only,
|
|
406
|
+
force_refresh: force_refresh
|
|
407
|
+
)
|
|
408
|
+
next if specific_release.nil? || specific_release.empty?
|
|
409
|
+
|
|
410
|
+
sections[version] = specific_release[version] if specific_release[version]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
sections
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def parse_single_github_tags_page(html, repo_uri)
|
|
417
|
+
require "nokogiri"
|
|
418
|
+
|
|
419
|
+
doc = begin
|
|
420
|
+
Nokogiri::HTML5(html)
|
|
421
|
+
rescue => _
|
|
422
|
+
Nokogiri::HTML(html)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
sections = {}
|
|
426
|
+
repo_path = URI(repo_uri).path
|
|
427
|
+
release_prefix = "#{repo_path}/releases/tag/"
|
|
428
|
+
tree_prefix = "#{repo_path}/tree/"
|
|
429
|
+
|
|
430
|
+
doc.css("a[href]").each do |link|
|
|
431
|
+
href = link["href"].to_s
|
|
432
|
+
tag_name =
|
|
433
|
+
if href.start_with?(release_prefix)
|
|
434
|
+
href.delete_prefix(release_prefix)
|
|
435
|
+
elsif href.start_with?(tree_prefix)
|
|
436
|
+
href.delete_prefix(tree_prefix)
|
|
437
|
+
end
|
|
438
|
+
next if tag_name.to_s.empty?
|
|
439
|
+
|
|
440
|
+
version = normalize_github_tag_version(tag_name)
|
|
441
|
+
next if version.to_s.empty?
|
|
442
|
+
|
|
443
|
+
sections[version] ||= [
|
|
444
|
+
"## #{version}\n",
|
|
445
|
+
"<p>No release information available. Check the release page for more information.</p>"
|
|
446
|
+
]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
next_href =
|
|
450
|
+
doc.at_css('a[rel="next"], a.next_page')&.[]("href") ||
|
|
451
|
+
doc.css("a[href]").find do |link|
|
|
452
|
+
href = link["href"].to_s
|
|
453
|
+
text = link.text.to_s.gsub(/\s+/, " ").strip
|
|
454
|
+
href.include?("/tags?after=") && text == "Next"
|
|
455
|
+
end&.[]("href")
|
|
456
|
+
next_url = if next_href && !next_href.empty?
|
|
457
|
+
URI.join(repo_uri, next_href).to_s
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
[sections, next_url]
|
|
461
|
+
rescue LoadError
|
|
462
|
+
[{}, nil]
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def parse_single_github_release_page(html, version)
|
|
466
|
+
require "nokogiri"
|
|
467
|
+
|
|
468
|
+
doc = begin
|
|
469
|
+
Nokogiri::HTML5(html)
|
|
470
|
+
rescue => _
|
|
471
|
+
Nokogiri::HTML(html)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
body = doc.at_css('[data-test-selector="body-content"] .markdown-body') ||
|
|
475
|
+
doc.at_css('[data-test-selector="body-content"]') ||
|
|
476
|
+
doc.at_css('.markdown-body')
|
|
477
|
+
if body
|
|
478
|
+
html_chunk = body.inner_html.to_s.strip
|
|
479
|
+
return ["## #{version}\n", html_chunk] unless html_chunk.empty?
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
title = doc.at_css("title")&.text.to_s.strip
|
|
483
|
+
synthetic_title = normalize_github_release_title(title, version)
|
|
484
|
+
return nil if synthetic_title.nil? || synthetic_title.empty?
|
|
485
|
+
|
|
486
|
+
["## #{version}\n", "<p>#{CGI.escapeHTML(synthetic_title)}</p>"]
|
|
487
|
+
rescue LoadError
|
|
488
|
+
nil
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def github_release_tag_urls(repo_url, version)
|
|
492
|
+
github_tag_candidates(version).map do |tag|
|
|
493
|
+
"#{repo_url}/releases/tag/#{tag}"
|
|
494
|
+
end.uniq
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def github_tag_candidates(version)
|
|
498
|
+
raw = version.to_s
|
|
499
|
+
[raw, (raw.start_with?("v") ? raw : "v#{raw}")].uniq
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def normalize_github_tag_version(tag_name)
|
|
503
|
+
decoded = URI.decode_www_form_component(tag_name.to_s.split("?").first.to_s)
|
|
504
|
+
match = decoded.match(/\Av?(\d[\w.\-]*)\z/i)
|
|
505
|
+
match && match[1]
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def prefer_github_releases_first?(cache_only:, force_refresh:)
|
|
509
|
+
meta = @metadata.meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
510
|
+
repo_uri = @metadata.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
511
|
+
|
|
512
|
+
repo_uri.to_s.include?("github.com") && meta["changelog_uri"].to_s.empty?
|
|
513
|
+
rescue StandardError
|
|
514
|
+
false
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def normalize_github_release_title(title, version)
|
|
518
|
+
return nil if title.to_s.empty?
|
|
519
|
+
|
|
520
|
+
cleaned = title.sub(/\s*·\s*[^·]+\s*·\s*GitHub\z/, "")
|
|
521
|
+
cleaned = cleaned.sub(/\ARelease\s+/, "")
|
|
522
|
+
cleaned = cleaned.strip
|
|
523
|
+
return nil if cleaned.empty? || cleaned == version.to_s
|
|
524
|
+
|
|
525
|
+
cleaned
|
|
526
|
+
end
|
|
311
527
|
end
|
|
312
528
|
end
|
data/lib/gemstar/lock_file.rb
CHANGED
|
@@ -5,26 +5,72 @@ module Gemstar
|
|
|
5
5
|
parsed = content ? parse_content(content) : parse_lockfile(path)
|
|
6
6
|
@specs = parsed[:specs]
|
|
7
7
|
@dependency_graph = parsed[:dependency_graph]
|
|
8
|
+
@dependency_requirements = parsed[:dependency_requirements]
|
|
8
9
|
@direct_dependencies = parsed[:direct_dependencies]
|
|
10
|
+
@direct_dependency_requirements = parsed[:direct_dependency_requirements]
|
|
11
|
+
@spec_sources = parsed[:spec_sources]
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
attr_reader :specs
|
|
12
15
|
attr_reader :dependency_graph
|
|
16
|
+
attr_reader :dependency_requirements
|
|
13
17
|
attr_reader :direct_dependencies
|
|
18
|
+
attr_reader :direct_dependency_requirements
|
|
19
|
+
attr_reader :spec_sources
|
|
14
20
|
|
|
15
21
|
def origins_for(gem_name)
|
|
16
|
-
|
|
22
|
+
if direct_dependencies.include?(gem_name)
|
|
23
|
+
return [{
|
|
24
|
+
type: :direct,
|
|
25
|
+
path: [gem_name],
|
|
26
|
+
requirement: direct_dependency_requirements[gem_name]
|
|
27
|
+
}]
|
|
28
|
+
end
|
|
17
29
|
|
|
18
30
|
direct_dependencies.filter_map do |root_dependency|
|
|
19
31
|
path = shortest_path_from(root_dependency, gem_name)
|
|
20
32
|
next if path.nil?
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
parent_name = path[-2]
|
|
35
|
+
{
|
|
36
|
+
type: :transitive,
|
|
37
|
+
path: path,
|
|
38
|
+
requirement: dependency_requirements.dig(parent_name, gem_name)
|
|
39
|
+
}
|
|
23
40
|
end
|
|
24
41
|
end
|
|
25
42
|
|
|
43
|
+
def source_for(gem_name)
|
|
44
|
+
spec_sources[gem_name]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def platform_for(gem_name)
|
|
48
|
+
version = specs[gem_name].to_s
|
|
49
|
+
parts = version.split("-")
|
|
50
|
+
return nil if parts.length < 2
|
|
51
|
+
|
|
52
|
+
1.upto(parts.length - 1) do |index|
|
|
53
|
+
candidate_version = parts[0...index].join("-")
|
|
54
|
+
candidate_platform = parts[index..].join("-")
|
|
55
|
+
next unless plausible_platform_suffix?(candidate_platform)
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
Gem::Version.new(candidate_version)
|
|
59
|
+
return candidate_platform
|
|
60
|
+
rescue ArgumentError
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
26
68
|
private
|
|
27
69
|
|
|
70
|
+
def plausible_platform_suffix?(suffix)
|
|
71
|
+
suffix.match?(/darwin|linux|mingw|mswin|musl|java|x86|arm|universal/i)
|
|
72
|
+
end
|
|
73
|
+
|
|
28
74
|
def shortest_path_from(root_dependency, target_gem)
|
|
29
75
|
queue = [[root_dependency, [root_dependency]]]
|
|
30
76
|
visited = {}
|
|
@@ -53,9 +99,13 @@ module Gemstar
|
|
|
53
99
|
def parse_content(content)
|
|
54
100
|
specs = {}
|
|
55
101
|
dependency_graph = Hash.new { |hash, key| hash[key] = [] }
|
|
102
|
+
dependency_requirements = Hash.new { |hash, key| hash[key] = {} }
|
|
56
103
|
direct_dependencies = []
|
|
104
|
+
direct_dependency_requirements = {}
|
|
105
|
+
spec_sources = {}
|
|
57
106
|
current_section = nil
|
|
58
107
|
current_spec = nil
|
|
108
|
+
current_source = nil
|
|
59
109
|
|
|
60
110
|
content.each_line do |line|
|
|
61
111
|
stripped = line.strip
|
|
@@ -68,12 +118,28 @@ module Gemstar
|
|
|
68
118
|
if stripped == "GEM"
|
|
69
119
|
current_section = :gem
|
|
70
120
|
current_spec = nil
|
|
121
|
+
current_source = { type: :rubygems }
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if stripped == "PATH"
|
|
126
|
+
current_section = :path
|
|
127
|
+
current_spec = nil
|
|
128
|
+
current_source = { type: :path }
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if stripped == "GIT"
|
|
133
|
+
current_section = :git
|
|
134
|
+
current_spec = nil
|
|
135
|
+
current_source = { type: :git }
|
|
71
136
|
next
|
|
72
137
|
end
|
|
73
138
|
|
|
74
139
|
if stripped == "DEPENDENCIES"
|
|
75
140
|
current_section = :dependencies
|
|
76
141
|
current_spec = nil
|
|
142
|
+
current_source = nil
|
|
77
143
|
next
|
|
78
144
|
end
|
|
79
145
|
|
|
@@ -83,17 +149,30 @@ module Gemstar
|
|
|
83
149
|
end
|
|
84
150
|
|
|
85
151
|
case current_section
|
|
86
|
-
when :gem
|
|
87
|
-
if line =~ /^\s{
|
|
152
|
+
when :gem, :path, :git
|
|
153
|
+
if line =~ /^\s{2}remote:\s+(.+)$/
|
|
154
|
+
current_source = (current_source || {}).merge(remote: Regexp.last_match(1))
|
|
155
|
+
elsif line =~ /^\s{2}(revision|branch|tag|ref|glob|submodules):\s+(.+)$/
|
|
156
|
+
current_source = (current_source || {}).merge(Regexp.last_match(1).to_sym => Regexp.last_match(2))
|
|
157
|
+
elsif line =~ /^\s{2}path:\s+(.+)$/
|
|
158
|
+
current_source = (current_source || {}).merge(path: Regexp.last_match(1))
|
|
159
|
+
elsif line =~ /^\s{4}(\S+) \((.+)\)/
|
|
88
160
|
name, version = Regexp.last_match(1), Regexp.last_match(2)
|
|
89
161
|
specs[name] = version
|
|
162
|
+
spec_sources[name] = (current_source || {}).dup
|
|
90
163
|
current_spec = name
|
|
91
|
-
elsif current_spec && line =~ /^\s{6}([^\s(]+)
|
|
92
|
-
|
|
164
|
+
elsif current_spec && line =~ /^\s{6}([^\s(]+)(?: \(([^)]+)\))?/
|
|
165
|
+
dependency_name = Regexp.last_match(1)
|
|
166
|
+
requirement = Regexp.last_match(2)
|
|
167
|
+
dependency_graph[current_spec] << dependency_name
|
|
168
|
+
dependency_requirements[current_spec][dependency_name] = requirement if requirement && !requirement.empty?
|
|
93
169
|
end
|
|
94
170
|
when :dependencies
|
|
95
|
-
if line =~ /^\s{2}([^\s!(]+)
|
|
96
|
-
|
|
171
|
+
if line =~ /^\s{2}([^\s!(]+)(?: \(([^)]+)\))?/
|
|
172
|
+
dependency_name = Regexp.last_match(1)
|
|
173
|
+
requirement = Regexp.last_match(2)
|
|
174
|
+
direct_dependencies << dependency_name
|
|
175
|
+
direct_dependency_requirements[dependency_name] = requirement if requirement && !requirement.empty?
|
|
97
176
|
end
|
|
98
177
|
end
|
|
99
178
|
end
|
|
@@ -101,7 +180,10 @@ module Gemstar
|
|
|
101
180
|
{
|
|
102
181
|
specs: specs,
|
|
103
182
|
dependency_graph: dependency_graph.transform_values(&:uniq),
|
|
104
|
-
|
|
183
|
+
dependency_requirements: dependency_requirements,
|
|
184
|
+
direct_dependencies: direct_dependencies.uniq,
|
|
185
|
+
direct_dependency_requirements: direct_dependency_requirements,
|
|
186
|
+
spec_sources: spec_sources
|
|
105
187
|
}
|
|
106
188
|
end
|
|
107
189
|
end
|
data/lib/gemstar/project.rb
CHANGED
|
@@ -114,7 +114,8 @@ module Gemstar
|
|
|
114
114
|
@gem_states_cache[cache_key] = (from_specs.keys | to_specs.keys).map do |gem_name|
|
|
115
115
|
old_version = from_specs[gem_name]
|
|
116
116
|
new_version = to_specs[gem_name]
|
|
117
|
-
|
|
117
|
+
effective_lockfile = new_version ? to_lockfile : from_lockfile
|
|
118
|
+
bundle_origins = effective_lockfile&.origins_for(gem_name) || []
|
|
118
119
|
|
|
119
120
|
{
|
|
120
121
|
name: gem_name,
|
|
@@ -122,6 +123,8 @@ module Gemstar
|
|
|
122
123
|
new_version: new_version,
|
|
123
124
|
status: gem_status(old_version, new_version),
|
|
124
125
|
version_label: version_label(old_version, new_version),
|
|
126
|
+
platform: effective_lockfile&.platform_for(gem_name),
|
|
127
|
+
source: effective_lockfile&.source_for(gem_name),
|
|
125
128
|
bundle_origins: bundle_origins,
|
|
126
129
|
bundle_origin_labels: bundle_origin_labels(bundle_origins)
|
|
127
130
|
}
|
|
@@ -233,7 +236,8 @@ module Gemstar
|
|
|
233
236
|
Array(origins).map do |origin|
|
|
234
237
|
next "Gemfile" if origin[:type] == :direct
|
|
235
238
|
|
|
236
|
-
["Gemfile", *origin[:path]].join(" → ")
|
|
239
|
+
label = ["Gemfile", *origin[:path]].join(" → ")
|
|
240
|
+
origin[:requirement] ? "#{label} (#{origin[:requirement]})" : label
|
|
237
241
|
end.compact.uniq
|
|
238
242
|
end
|
|
239
243
|
|
data/lib/gemstar/version.rb
CHANGED
data/lib/gemstar/web/app.rb
CHANGED
|
@@ -227,7 +227,7 @@ module Gemstar
|
|
|
227
227
|
</div>
|
|
228
228
|
<div class="picker-row">
|
|
229
229
|
<label class="picker picker-project">
|
|
230
|
-
<span class="picker-prefix"
|
|
230
|
+
<span class="picker-prefix" data-text-label="true">Project:</span>
|
|
231
231
|
<select data-project-select>
|
|
232
232
|
#{project_options_html}
|
|
233
233
|
<option value="" disabled>────────</option>
|
|
@@ -516,7 +516,8 @@ module Gemstar
|
|
|
516
516
|
next if origin[:type] != :direct && display_path.empty?
|
|
517
517
|
|
|
518
518
|
linked_path = linked_gem_chain(["Gemfile", *display_path])
|
|
519
|
-
origin[:type] == :direct ? gemfile_link("Gemfile") : linked_path
|
|
519
|
+
label = origin[:type] == :direct ? gemfile_link("Gemfile") : linked_path
|
|
520
|
+
origin[:requirement] ? "#{label} (#{h(origin[:requirement])})" : label
|
|
520
521
|
end.uniq
|
|
521
522
|
end
|
|
522
523
|
|
|
@@ -540,14 +541,18 @@ module Gemstar
|
|
|
540
541
|
def render_dependency_details(bundle_origins, requirement_names, added_on)
|
|
541
542
|
required_by = dependency_origin_items(bundle_origins)
|
|
542
543
|
requires = Array(requirement_names).compact.uniq.map { |name| internal_gem_link(name) }
|
|
544
|
+
platforms = selected_gem_platform_items
|
|
545
|
+
source_items = selected_gem_source_items
|
|
543
546
|
added_markup = render_added_on(added_on)
|
|
544
|
-
return "" if required_by.empty? && requires.empty? && added_markup.empty?
|
|
547
|
+
return "" if required_by.empty? && requires.empty? && platforms.empty? && source_items.empty? && added_markup.empty?
|
|
545
548
|
|
|
546
549
|
<<~HTML
|
|
547
550
|
<details class="detail-disclosure">
|
|
548
551
|
<summary><span class="detail-disclosure-caret" aria-hidden="true"></span><h3>Details</h3></summary>
|
|
549
552
|
<div class="detail-disclosure-panel">
|
|
550
553
|
#{added_markup}
|
|
554
|
+
#{render_dependency_popover_section("Platforms", platforms)}
|
|
555
|
+
#{render_dependency_popover_section("Source", source_items)}
|
|
551
556
|
#{render_dependency_popover_section("Required by", required_by)}
|
|
552
557
|
#{render_dependency_popover_section("Requires", requires)}
|
|
553
558
|
</div>
|
|
@@ -584,6 +589,39 @@ module Gemstar
|
|
|
584
589
|
@selected_project&.gem_added_on(@selected_gem[:name], revision_id: revision_id)
|
|
585
590
|
end
|
|
586
591
|
|
|
592
|
+
def selected_gem_platform_items
|
|
593
|
+
platform = @selected_gem[:platform]
|
|
594
|
+
return [] if platform.to_s.empty?
|
|
595
|
+
|
|
596
|
+
[h(platform)]
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def selected_gem_source_items
|
|
600
|
+
source = @selected_gem[:source] || {}
|
|
601
|
+
source_type = source[:type]
|
|
602
|
+
|
|
603
|
+
case source_type
|
|
604
|
+
when :path
|
|
605
|
+
location = source[:path] || source[:remote]
|
|
606
|
+
return [] if location.to_s.empty?
|
|
607
|
+
|
|
608
|
+
["Path (#{h(location)})"]
|
|
609
|
+
when :git
|
|
610
|
+
remote = source[:remote]
|
|
611
|
+
pieces = ["Git"]
|
|
612
|
+
pieces << h(remote) unless remote.to_s.empty?
|
|
613
|
+
pieces << "@#{h(source[:branch])}" if source[:branch]
|
|
614
|
+
pieces << "##{h(source[:tag])}" if source[:tag]
|
|
615
|
+
pieces << h(source[:revision].to_s[0, 8]) if source[:revision]
|
|
616
|
+
[pieces.join(" ")]
|
|
617
|
+
when :rubygems
|
|
618
|
+
remote = source[:remote]
|
|
619
|
+
[remote.to_s.empty? ? "RubyGems" : "RubyGems (#{h(remote)})"]
|
|
620
|
+
else
|
|
621
|
+
[]
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
587
625
|
def linked_gem_chain(names)
|
|
588
626
|
Array(names).map.with_index do |name, index|
|
|
589
627
|
if index.zero?
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
align-items: center;
|
|
53
53
|
justify-content: space-between;
|
|
54
54
|
gap: 0.6rem;
|
|
55
|
-
padding: 0.
|
|
56
|
-
border-bottom: 1px solid #
|
|
57
|
-
background: #
|
|
55
|
+
padding: 0.55rem 0.75rem;
|
|
56
|
+
border-bottom: 1px solid #e6d5c2;
|
|
57
|
+
background: #fbf1e4;
|
|
58
58
|
position: sticky;
|
|
59
59
|
top: 0;
|
|
60
60
|
z-index: 2;
|
|
@@ -316,7 +316,7 @@
|
|
|
316
316
|
font-weight: 600;
|
|
317
317
|
}
|
|
318
318
|
.detail {
|
|
319
|
-
padding: 0.
|
|
319
|
+
padding: 0.62rem 0.8rem 0.5rem 0.5rem;
|
|
320
320
|
display: grid;
|
|
321
321
|
gap: 0.45rem;
|
|
322
322
|
align-content: start;
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
flex: 0 0 auto;
|
|
368
368
|
}
|
|
369
369
|
.detail-disclosure {
|
|
370
|
-
margin-top: 0.
|
|
370
|
+
margin-top: 0.38rem;
|
|
371
371
|
border: 1px solid #ece8df;
|
|
372
372
|
border-radius: 0.3rem;
|
|
373
373
|
background: #fff;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
let currentFilter = <%= selected_filter_json %>;
|
|
16
16
|
let currentSearch = "";
|
|
17
17
|
const emptyDetailHtml = <%= empty_detail_html_json %>;
|
|
18
|
+
const detailDisclosureStorageKey = "gemstar.detailDisclosureOpen";
|
|
18
19
|
|
|
19
20
|
const visibleGemLinks = () => gemLinks.filter((link) => !link.hidden);
|
|
20
21
|
const currentSelectedIndex = () => visibleGemLinks().findIndex((link) => link.classList.contains("is-selected"));
|
|
@@ -24,6 +25,15 @@
|
|
|
24
25
|
});
|
|
25
26
|
};
|
|
26
27
|
const requestedGemName = () => new URL(window.location.href).searchParams.get("gem");
|
|
28
|
+
const currentDetailGemName = () => {
|
|
29
|
+
if (!detailPanel || !detailPanel.dataset.detailUrl) return null;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return new URL(detailPanel.dataset.detailUrl, window.location.origin).searchParams.get("gem");
|
|
33
|
+
} catch (_error) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
27
37
|
const isSidebarFocused = () => document.activeElement === sidebarPanel;
|
|
28
38
|
const isDetailFocused = () => detailPanel && document.activeElement === detailPanel;
|
|
29
39
|
const focusSidebar = () => {
|
|
@@ -32,6 +42,31 @@
|
|
|
32
42
|
const focusDetail = () => {
|
|
33
43
|
if (detailPanel) detailPanel.focus({ preventScroll: true });
|
|
34
44
|
};
|
|
45
|
+
const storedDetailDisclosureOpen = () => {
|
|
46
|
+
try {
|
|
47
|
+
return window.localStorage.getItem(detailDisclosureStorageKey) === "true";
|
|
48
|
+
} catch (_error) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const persistDetailDisclosureOpen = (open) => {
|
|
53
|
+
try {
|
|
54
|
+
window.localStorage.setItem(detailDisclosureStorageKey, open ? "true" : "false");
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
// ignore storage failures
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const bindDetailDisclosure = () => {
|
|
60
|
+
if (!detailPanel) return;
|
|
61
|
+
|
|
62
|
+
const disclosure = detailPanel.querySelector(".detail-disclosure");
|
|
63
|
+
if (!disclosure) return;
|
|
64
|
+
|
|
65
|
+
disclosure.open = storedDetailDisclosureOpen();
|
|
66
|
+
disclosure.addEventListener("toggle", () => {
|
|
67
|
+
persistDetailDisclosureOpen(disclosure.open);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
35
70
|
|
|
36
71
|
const applyGemFilter = (filter) => {
|
|
37
72
|
currentFilter = filter;
|
|
@@ -94,6 +129,7 @@
|
|
|
94
129
|
detailPanel = document.querySelector("[data-detail-panel]");
|
|
95
130
|
if (detailPanel) detailPanel.scrollTop = 0;
|
|
96
131
|
activeDetailUrl = detailPanel ? detailPanel.dataset.detailUrl : null;
|
|
132
|
+
bindDetailDisclosure();
|
|
97
133
|
if (shouldRestoreDetailFocus) {
|
|
98
134
|
focusDetail();
|
|
99
135
|
}
|
|
@@ -122,7 +158,7 @@
|
|
|
122
158
|
</section>
|
|
123
159
|
`;
|
|
124
160
|
|
|
125
|
-
const fetchDetail = (url,
|
|
161
|
+
const fetchDetail = (url, historyMode = "push") => {
|
|
126
162
|
const normalizedUrl = new URL(url, window.location.origin).toString();
|
|
127
163
|
const requestToken = ++detailRequestToken;
|
|
128
164
|
activeDetailUrl = normalizedUrl;
|
|
@@ -139,12 +175,13 @@
|
|
|
139
175
|
if (requestToken !== detailRequestToken || normalizedUrl !== activeDetailUrl) return;
|
|
140
176
|
|
|
141
177
|
replaceDetail(html);
|
|
142
|
-
if (
|
|
178
|
+
if (historyMode !== "none") {
|
|
143
179
|
const pageUrl = new URL(window.location.href);
|
|
144
180
|
const detailUrl = new URL(url, window.location.origin);
|
|
145
181
|
pageUrl.search = detailUrl.search;
|
|
146
182
|
pageUrl.searchParams.set("filter", currentFilter);
|
|
147
|
-
|
|
183
|
+
const historyMethod = historyMode === "replace" ? "replaceState" : "pushState";
|
|
184
|
+
window.history[historyMethod]({}, "", pageUrl);
|
|
148
185
|
}
|
|
149
186
|
const detailUrl = new URL(url, window.location.origin);
|
|
150
187
|
syncSidebarSelection(detailUrl.searchParams.get("gem"));
|
|
@@ -154,17 +191,39 @@
|
|
|
154
191
|
});
|
|
155
192
|
};
|
|
156
193
|
|
|
157
|
-
const activateGemLink = (link,
|
|
194
|
+
const activateGemLink = (link, historyMode = "push", keepVisible = false) => {
|
|
158
195
|
if (!link) return;
|
|
159
196
|
|
|
160
197
|
stopDetailPoll();
|
|
161
198
|
stopDetailLoading();
|
|
162
199
|
syncSidebarSelection(link.dataset.gemName, keepVisible);
|
|
163
|
-
fetchDetail(link.dataset.detailUrl || link.href,
|
|
200
|
+
fetchDetail(link.dataset.detailUrl || link.href, historyMode);
|
|
164
201
|
|
|
165
202
|
focusSidebar();
|
|
166
203
|
};
|
|
167
204
|
|
|
205
|
+
const ensureInitialGemSelection = () => {
|
|
206
|
+
if (requestedGemName()) return;
|
|
207
|
+
|
|
208
|
+
const detailGemName = currentDetailGemName();
|
|
209
|
+
const detailLink = detailGemName ? visibleGemLinks().find((link) => link.dataset.gemName === detailGemName) : null;
|
|
210
|
+
|
|
211
|
+
if (detailLink) {
|
|
212
|
+
syncSidebarSelection(detailGemName, true);
|
|
213
|
+
const pageUrl = new URL(window.location.href);
|
|
214
|
+
const detailUrl = new URL(detailLink.dataset.detailUrl || detailLink.href, window.location.origin);
|
|
215
|
+
pageUrl.search = detailUrl.search;
|
|
216
|
+
pageUrl.searchParams.set("filter", currentFilter);
|
|
217
|
+
window.history.replaceState({}, "", pageUrl);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const firstVisibleLink = visibleGemLinks()[0];
|
|
222
|
+
if (firstVisibleLink) {
|
|
223
|
+
activateGemLink(firstVisibleLink, "replace", true);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
168
227
|
const scheduleDetailPoll = () => {
|
|
169
228
|
stopDetailPoll();
|
|
170
229
|
if (!detailPanel || detailPanel.dataset.detailPending !== "true") return;
|
|
@@ -176,11 +235,13 @@
|
|
|
176
235
|
|
|
177
236
|
if (detailPanel) {
|
|
178
237
|
detailPanel.scrollTop = 0;
|
|
238
|
+
bindDetailDisclosure();
|
|
179
239
|
scheduleDetailPoll();
|
|
180
240
|
}
|
|
181
241
|
|
|
182
242
|
syncSidebarSelection(null, true);
|
|
183
243
|
applyGemFilter(currentFilter);
|
|
244
|
+
ensureInitialGemSelection();
|
|
184
245
|
|
|
185
246
|
gemLinks.forEach((link) => {
|
|
186
247
|
link.addEventListener("click", (event) => {
|
|
@@ -277,6 +338,14 @@
|
|
|
277
338
|
}
|
|
278
339
|
|
|
279
340
|
if (!isSidebarFocused()) {
|
|
341
|
+
if (event.key === "ArrowLeft") {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
focusSidebar();
|
|
344
|
+
}
|
|
345
|
+
if (event.key === "ArrowRight") {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
focusDetail();
|
|
348
|
+
}
|
|
280
349
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
281
350
|
event.preventDefault();
|
|
282
351
|
focusSidebar();
|
|
@@ -296,7 +365,7 @@
|
|
|
296
365
|
|
|
297
366
|
if (nextIndex !== null && nextIndex !== currentIndex) {
|
|
298
367
|
event.preventDefault();
|
|
299
|
-
activateGemLink(links[nextIndex],
|
|
368
|
+
activateGemLink(links[nextIndex], "push", true);
|
|
300
369
|
}
|
|
301
370
|
});
|
|
302
371
|
|
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.0.
|
|
4
|
+
version: 1.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Dejako
|
|
@@ -281,7 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
281
281
|
- !ruby/object:Gem::Version
|
|
282
282
|
version: '0'
|
|
283
283
|
requirements: []
|
|
284
|
-
rubygems_version:
|
|
284
|
+
rubygems_version: 3.6.9
|
|
285
285
|
specification_version: 4
|
|
286
286
|
summary: Making sense of gems.
|
|
287
287
|
test_files: []
|