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/web/app.rb
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
require "cgi"
|
|
2
2
|
require "erb"
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "shellwords"
|
|
3
6
|
require "uri"
|
|
4
7
|
require "kramdown"
|
|
5
8
|
require "nokogiri"
|
|
6
9
|
require "roda"
|
|
10
|
+
require_relative "../crates_io_metadata"
|
|
11
|
+
require_relative "../pypi_metadata"
|
|
7
12
|
|
|
8
13
|
begin
|
|
9
14
|
require "kramdown-parser-gfm"
|
|
@@ -14,7 +19,7 @@ module Gemstar
|
|
|
14
19
|
module Web
|
|
15
20
|
class App < Roda
|
|
16
21
|
MISSING_METADATA = Object.new
|
|
17
|
-
CACHE_VERSION = "
|
|
22
|
+
CACHE_VERSION = "v11"
|
|
18
23
|
|
|
19
24
|
class << self
|
|
20
25
|
def build(projects:, config_home:, cache_warmer: nil)
|
|
@@ -59,13 +64,15 @@ module Gemstar
|
|
|
59
64
|
r.get "detail" do
|
|
60
65
|
request_cache_key = detail_request_cache_key(r.params)
|
|
61
66
|
request_cache = self.class.opts[:detail_request_cache]
|
|
62
|
-
|
|
67
|
+
refresh_detail = detail_refresh_requested?(r.params)
|
|
68
|
+
if request_cache_key && !refresh_detail && request_cache.key?(request_cache_key)
|
|
63
69
|
next request_cache[request_cache_key]
|
|
64
70
|
end
|
|
71
|
+
request_cache.delete(request_cache_key) if request_cache_key && refresh_detail
|
|
65
72
|
|
|
66
73
|
load_state(r.params)
|
|
67
|
-
prioritize_selected_gem
|
|
68
|
-
detail_html = render_detail
|
|
74
|
+
prioritize_selected_gem unless r.env["gemstar.detail_cache_warm"]
|
|
75
|
+
detail_html = render_detail(force_refresh: refresh_detail, use_github_cli: detail_use_github_cli_requested?(r.params))
|
|
69
76
|
request_cache[request_cache_key] = detail_html if request_cache_key
|
|
70
77
|
detail_html
|
|
71
78
|
end
|
|
@@ -80,6 +87,23 @@ module Gemstar
|
|
|
80
87
|
File.read(project.gemfile_path)
|
|
81
88
|
end
|
|
82
89
|
|
|
90
|
+
r.post "actions", String do |action_id|
|
|
91
|
+
response["Content-Type"] = "application/json; charset=utf-8"
|
|
92
|
+
|
|
93
|
+
project_index = selected_project_index(r.params["project"])
|
|
94
|
+
project = @projects[project_index]
|
|
95
|
+
action = project_action(action_id, project)
|
|
96
|
+
unless project && action
|
|
97
|
+
response.status = 404
|
|
98
|
+
next JSON.generate({ ok: false, error: "Unknown project action." })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
result = run_project_action(project, action)
|
|
102
|
+
clear_runtime_state!(project)
|
|
103
|
+
response.status = result[:ok] ? 200 : 500
|
|
104
|
+
JSON.generate(result)
|
|
105
|
+
end
|
|
106
|
+
|
|
83
107
|
r.on "projects", String do |project_id|
|
|
84
108
|
response.redirect "/?project=#{project_id}"
|
|
85
109
|
end
|
|
@@ -102,6 +126,7 @@ module Gemstar
|
|
|
102
126
|
importmap_stamp = File.file?(project.importmap_path) ? File.mtime(project.importmap_path).to_i : 0
|
|
103
127
|
importmap_vendor_stamp = importmap_vendor_mtime(project)
|
|
104
128
|
package_lock_stamp = File.file?(project.package_lock_path) ? File.mtime(project.package_lock_path).to_i : 0
|
|
129
|
+
cargo_lock_stamp = File.file?(project.cargo_lock_path) ? File.mtime(project.cargo_lock_path).to_i : 0
|
|
105
130
|
|
|
106
131
|
[
|
|
107
132
|
CACHE_VERSION,
|
|
@@ -111,10 +136,12 @@ module Gemstar
|
|
|
111
136
|
params["filter"],
|
|
112
137
|
params["scope"],
|
|
113
138
|
package_param(params),
|
|
139
|
+
params["package_id"],
|
|
114
140
|
lockfile_stamp,
|
|
115
141
|
importmap_stamp,
|
|
116
142
|
importmap_vendor_stamp,
|
|
117
|
-
package_lock_stamp
|
|
143
|
+
package_lock_stamp,
|
|
144
|
+
cargo_lock_stamp
|
|
118
145
|
]
|
|
119
146
|
end
|
|
120
147
|
|
|
@@ -131,6 +158,14 @@ module Gemstar
|
|
|
131
158
|
0
|
|
132
159
|
end
|
|
133
160
|
|
|
161
|
+
def detail_refresh_requested?(params)
|
|
162
|
+
%w[1 true yes].include?(params["refresh"].to_s.downcase)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def detail_use_github_cli_requested?(params)
|
|
166
|
+
%w[1 true yes].include?(params["use_gh"].to_s.downcase)
|
|
167
|
+
end
|
|
168
|
+
|
|
134
169
|
def page_title
|
|
135
170
|
return "Gemstar" unless @selected_project
|
|
136
171
|
|
|
@@ -146,10 +181,12 @@ module Gemstar
|
|
|
146
181
|
@selected_to_revision_id = selected_to_revision_id(@selected_to_revision_id)
|
|
147
182
|
@gem_states = @selected_project ? @selected_project.gem_states(from_revision_id: @selected_from_revision_id, to_revision_id: @selected_to_revision_id) : []
|
|
148
183
|
requested_package_name = package_param(params)
|
|
184
|
+
requested_package_id = params["package_id"]
|
|
149
185
|
@requested_gem_name = requested_package_name
|
|
150
|
-
@
|
|
151
|
-
@
|
|
152
|
-
@
|
|
186
|
+
@requested_package_id = requested_package_id
|
|
187
|
+
@selected_package_scope = selected_package_scope(params["scope"], requested_package_name, requested_package_id)
|
|
188
|
+
@selected_filter = selected_filter(params["filter"], requested_package_name, requested_package_id)
|
|
189
|
+
@selected_gem = selected_gem_state(requested_package_name, requested_package_id)
|
|
153
190
|
end
|
|
154
191
|
|
|
155
192
|
def package_param(params)
|
|
@@ -191,33 +228,36 @@ module Gemstar
|
|
|
191
228
|
valid_ids.include?(candidate) ? candidate : valid_ids.first || "worktree"
|
|
192
229
|
end
|
|
193
230
|
|
|
194
|
-
def selected_filter(raw_filter, raw_gem_name)
|
|
231
|
+
def selected_filter(raw_filter, raw_gem_name, raw_package_id = nil)
|
|
195
232
|
return "all" if @gem_states.empty?
|
|
233
|
+
return "all" unless @gem_states.any? { |gem| gem[:status] != :unchanged }
|
|
196
234
|
return raw_filter if %w[updated all].include?(raw_filter)
|
|
197
235
|
|
|
198
|
-
selected_gem =
|
|
236
|
+
selected_gem = requested_gem_state(raw_gem_name, raw_package_id)
|
|
199
237
|
return "all" if selected_gem && selected_gem[:status] == :unchanged
|
|
200
238
|
|
|
201
239
|
@gem_states.any? { |gem| gem[:status] != :unchanged } ? "updated" : "all"
|
|
202
240
|
end
|
|
203
241
|
|
|
204
|
-
def selected_package_scope(raw_scope, raw_gem_name)
|
|
242
|
+
def selected_package_scope(raw_scope, raw_gem_name, raw_package_id = nil)
|
|
205
243
|
return "all" if @gem_states.empty?
|
|
206
244
|
|
|
207
245
|
available_scopes = available_package_scopes
|
|
208
246
|
default_scope = available_scopes == ["gems"] ? "gems" : "all"
|
|
209
247
|
return raw_scope if raw_scope == "all" || available_scopes.include?(raw_scope)
|
|
210
248
|
|
|
211
|
-
selected_gem =
|
|
249
|
+
selected_gem = requested_gem_state(raw_gem_name, raw_package_id)
|
|
212
250
|
return selected_gem[:package_scope] if selected_gem
|
|
213
251
|
|
|
214
252
|
default_scope
|
|
215
253
|
end
|
|
216
254
|
|
|
217
|
-
def selected_gem_state(raw_gem_name)
|
|
255
|
+
def selected_gem_state(raw_gem_name, raw_package_id = nil)
|
|
218
256
|
return nil if @gem_states.empty?
|
|
219
257
|
|
|
220
|
-
exact_match = @gem_states.find { |gem| gem
|
|
258
|
+
exact_match = @gem_states.find { |gem| package_identity(gem) == raw_package_id && gem_visible_in_selected_scope?(gem) } unless raw_package_id.to_s.empty?
|
|
259
|
+
exact_match ||= @gem_states.find { |gem| package_identity(gem) == raw_package_id } unless raw_package_id.to_s.empty?
|
|
260
|
+
exact_match ||= @gem_states.find { |gem| gem[:name] == raw_gem_name && gem_visible_in_selected_scope?(gem) }
|
|
221
261
|
exact_match ||= @gem_states.find { |gem| gem[:name] == raw_gem_name }
|
|
222
262
|
return exact_match if exact_match
|
|
223
263
|
|
|
@@ -227,6 +267,23 @@ module Gemstar
|
|
|
227
267
|
@gem_states.first
|
|
228
268
|
end
|
|
229
269
|
|
|
270
|
+
def requested_gem_state(raw_gem_name, raw_package_id)
|
|
271
|
+
unless raw_package_id.to_s.empty?
|
|
272
|
+
identity_match = @gem_states.find { |gem| package_identity(gem) == raw_package_id }
|
|
273
|
+
return identity_match if identity_match
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
@gem_states.find { |gem| gem[:name] == raw_gem_name }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def package_identity(gem_state)
|
|
280
|
+
[
|
|
281
|
+
gem_state[:package_scope],
|
|
282
|
+
gem_state[:package_source_file],
|
|
283
|
+
gem_state[:package_lock_key] || gem_state[:name]
|
|
284
|
+
].join(":")
|
|
285
|
+
end
|
|
286
|
+
|
|
230
287
|
def gem_visible_in_selected_filter?(gem_state)
|
|
231
288
|
return false unless gem_visible_in_selected_scope?(gem_state)
|
|
232
289
|
return true if @selected_filter != "updated"
|
|
@@ -394,13 +451,231 @@ module Gemstar
|
|
|
394
451
|
<strong>#{@gem_states.count { |gem| gem[:status] != :unchanged }}</strong> changes from #{h(selected_from_revision_label)} to #{h(selected_to_revision_label)}
|
|
395
452
|
</div>
|
|
396
453
|
<div class="toolbar-actions">
|
|
397
|
-
|
|
398
|
-
<button type="button" class="action action-primary" disabled="disabled">bundle update</button>
|
|
454
|
+
#{render_project_action_buttons}
|
|
399
455
|
</div>
|
|
400
456
|
</section>
|
|
401
457
|
HTML
|
|
402
458
|
end
|
|
403
459
|
|
|
460
|
+
def render_project_action_buttons
|
|
461
|
+
actions = project_actions
|
|
462
|
+
actions.map do |action|
|
|
463
|
+
primary = action[:primary] ? " action-primary" : ""
|
|
464
|
+
command_label = project_action_display_command(@selected_project, action)
|
|
465
|
+
<<~HTML
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
class="action#{primary}"
|
|
469
|
+
data-project-action="#{h(action[:id])}"
|
|
470
|
+
data-project-action-command="#{h(command_label)}"
|
|
471
|
+
>#{h(action[:label])}</button>
|
|
472
|
+
HTML
|
|
473
|
+
end.join
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def project_actions(project = @selected_project)
|
|
477
|
+
return [] unless project
|
|
478
|
+
|
|
479
|
+
actions = []
|
|
480
|
+
if project.gemfile? || project.lockfile?
|
|
481
|
+
actions << {
|
|
482
|
+
id: "bundle_install",
|
|
483
|
+
label: "#{bundle_command(project).join(" ")} install",
|
|
484
|
+
command: [*bundle_command(project), "install"]
|
|
485
|
+
}
|
|
486
|
+
actions << {
|
|
487
|
+
id: "bundle_update",
|
|
488
|
+
label: "#{bundle_command(project).join(" ")} update",
|
|
489
|
+
command: [*bundle_command(project), "update"],
|
|
490
|
+
primary: true
|
|
491
|
+
}
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
if project.uv_lock?
|
|
495
|
+
actions << {
|
|
496
|
+
id: "uv_sync",
|
|
497
|
+
label: "uv sync",
|
|
498
|
+
command: %w[uv sync]
|
|
499
|
+
}
|
|
500
|
+
actions << {
|
|
501
|
+
id: "uv_lock_upgrade",
|
|
502
|
+
label: "uv lock --upgrade",
|
|
503
|
+
command: %w[uv lock --upgrade],
|
|
504
|
+
primary: true
|
|
505
|
+
}
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
if project.cargo_toml? || project.cargo_lock?
|
|
510
|
+
actions << {
|
|
511
|
+
id: "cargo_fetch",
|
|
512
|
+
label: "cargo fetch",
|
|
513
|
+
command: %w[cargo fetch]
|
|
514
|
+
}
|
|
515
|
+
actions << {
|
|
516
|
+
id: "cargo_update",
|
|
517
|
+
label: "cargo update",
|
|
518
|
+
command: %w[cargo update],
|
|
519
|
+
primary: true
|
|
520
|
+
}
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
if project.package_lock?
|
|
524
|
+
actions << {
|
|
525
|
+
id: "npm_install",
|
|
526
|
+
label: "npm install",
|
|
527
|
+
command: %w[npm install]
|
|
528
|
+
}
|
|
529
|
+
actions << {
|
|
530
|
+
id: "npm_update",
|
|
531
|
+
label: "npm update",
|
|
532
|
+
command: %w[npm update],
|
|
533
|
+
primary: true
|
|
534
|
+
}
|
|
535
|
+
elsif project.package_json?
|
|
536
|
+
actions << {
|
|
537
|
+
id: "npm_install",
|
|
538
|
+
label: "npm install",
|
|
539
|
+
command: %w[npm install]
|
|
540
|
+
}
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if project.importmap?
|
|
544
|
+
actions << {
|
|
545
|
+
id: "importmap_update",
|
|
546
|
+
label: "bin/importmap update",
|
|
547
|
+
command: %w[bin/importmap update]
|
|
548
|
+
}
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
actions
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def project_action(action_id, project)
|
|
555
|
+
project_actions(project).find { |action| action[:id] == action_id }
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def run_project_action(project, action)
|
|
559
|
+
command = project_action_command(project, action)
|
|
560
|
+
shell_command = project_action_shell_command(project, command)
|
|
561
|
+
stdout, stderr, status = Open3.capture3(project_action_environment, user_shell, "-lc", shell_command)
|
|
562
|
+
{
|
|
563
|
+
ok: status.success?,
|
|
564
|
+
action: action[:id],
|
|
565
|
+
command: shell_command,
|
|
566
|
+
output: truncated_action_output([stdout, stderr].reject(&:empty?).join("\n").strip)
|
|
567
|
+
}
|
|
568
|
+
rescue Errno::ENOENT => e
|
|
569
|
+
{
|
|
570
|
+
ok: false,
|
|
571
|
+
action: action[:id],
|
|
572
|
+
command: project_action_display_command(project, action),
|
|
573
|
+
output: e.message
|
|
574
|
+
}
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def project_action_display_command(project, action)
|
|
578
|
+
project_action_shell_command(project, project_action_command(project, action))
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def project_action_shell_command(project, command)
|
|
582
|
+
"cd #{Shellwords.escape(project.directory)} && #{Shellwords.join(command)}"
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def project_action_command(project, action)
|
|
586
|
+
return action[:command] unless project_uses_mise?(project)
|
|
587
|
+
|
|
588
|
+
mise = mise_executable
|
|
589
|
+
return action[:command] unless mise
|
|
590
|
+
|
|
591
|
+
[mise, "exec", "--", *mise_command(action[:command])]
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def mise_command(command)
|
|
595
|
+
executable, *arguments = command
|
|
596
|
+
case executable
|
|
597
|
+
when /\Abin\//
|
|
598
|
+
["ruby", executable, *arguments]
|
|
599
|
+
when "bundle"
|
|
600
|
+
["ruby", "-S", executable, *arguments]
|
|
601
|
+
else
|
|
602
|
+
command
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def project_action_environment
|
|
607
|
+
{
|
|
608
|
+
"BUNDLE_BIN_PATH" => nil,
|
|
609
|
+
"BUNDLE_APP_CONFIG" => nil,
|
|
610
|
+
"BUNDLE_GEMFILE" => nil,
|
|
611
|
+
"BUNDLE_PATH" => nil,
|
|
612
|
+
"BUNDLE_WITHOUT" => nil,
|
|
613
|
+
"BUNDLER_VERSION" => nil,
|
|
614
|
+
"GEM_HOME" => nil,
|
|
615
|
+
"GEM_PATH" => nil,
|
|
616
|
+
"GEM_ROOT" => nil,
|
|
617
|
+
"MY_RUBY_HOME" => nil,
|
|
618
|
+
"RBENV_VERSION" => nil,
|
|
619
|
+
"RUBYLIB" => nil,
|
|
620
|
+
"RUBYGEMS_GEMDEPS" => nil,
|
|
621
|
+
"RUBYOPT" => nil,
|
|
622
|
+
"rvm_path" => nil,
|
|
623
|
+
"rvm_ruby_string" => nil
|
|
624
|
+
}
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def user_shell
|
|
628
|
+
shell = ENV["SHELL"].to_s
|
|
629
|
+
shell.empty? ? "/bin/sh" : shell
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def bundle_command(project)
|
|
633
|
+
File.file?(File.join(project.directory, "bin", "bundle")) ? %w[bin/bundle] : %w[bundle]
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def project_uses_mise?(project)
|
|
637
|
+
directory = project.directory
|
|
638
|
+
loop do
|
|
639
|
+
return true if File.file?(File.join(directory, "mise.toml")) ||
|
|
640
|
+
File.file?(File.join(directory, ".mise.toml")) ||
|
|
641
|
+
File.file?(File.join(directory, ".ruby-version"))
|
|
642
|
+
|
|
643
|
+
parent = File.dirname(directory)
|
|
644
|
+
break if parent == directory
|
|
645
|
+
|
|
646
|
+
directory = parent
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
false
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def mise_executable
|
|
653
|
+
ENV["PATH"].to_s.split(File::PATH_SEPARATOR).each do |directory|
|
|
654
|
+
path = File.join(directory, "mise")
|
|
655
|
+
return path if File.executable?(path)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
[
|
|
659
|
+
"/opt/homebrew/bin/mise",
|
|
660
|
+
"/usr/local/bin/mise",
|
|
661
|
+
File.expand_path("~/.local/bin/mise")
|
|
662
|
+
].find { |path| File.executable?(path) }
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def truncated_action_output(output)
|
|
666
|
+
return output if output.length <= 8_000
|
|
667
|
+
|
|
668
|
+
"#{output[0, 8_000]}\n\n... output truncated ..."
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def clear_runtime_state!(project)
|
|
672
|
+
project&.clear_cache! if project.respond_to?(:clear_cache!)
|
|
673
|
+
self.class.opts[:change_sections_cache].clear
|
|
674
|
+
self.class.opts[:detail_html_cache].clear
|
|
675
|
+
self.class.opts[:detail_request_cache].clear
|
|
676
|
+
self.class.opts[:metadata_cache].clear
|
|
677
|
+
end
|
|
678
|
+
|
|
404
679
|
def selected_from_revision_label
|
|
405
680
|
@revision_options.find { |option| option[:id] == @selected_from_revision_id }&.dig(:label) || "worktree"
|
|
406
681
|
end
|
|
@@ -443,19 +718,22 @@ module Gemstar
|
|
|
443
718
|
HTML
|
|
444
719
|
|
|
445
720
|
items = @gem_states.map do |gem|
|
|
446
|
-
|
|
721
|
+
identity = package_identity(gem)
|
|
722
|
+
selected = identity == package_identity(@selected_gem) ? " is-selected" : ""
|
|
447
723
|
status_class = " status-#{gem[:status]}"
|
|
448
724
|
updated = gem[:status] != :unchanged
|
|
449
|
-
|
|
725
|
+
requested = identity == @requested_package_id || (@requested_package_id.to_s.empty? && gem[:name] == @requested_gem_name)
|
|
726
|
+
hidden = (!gem_visible_in_selected_scope?(gem) || (@selected_filter == "updated" && !updated && !requested)) ? ' hidden="hidden"' : ""
|
|
450
727
|
<<~HTML
|
|
451
728
|
<a
|
|
452
729
|
class="gem-row#{selected}#{status_class}"
|
|
453
|
-
href="#{project_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: gem[:name])}"
|
|
730
|
+
href="#{project_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: gem[:name], package_id: identity)}"
|
|
454
731
|
data-gem-link="true"
|
|
455
732
|
data-gem-name="#{h(gem[:name])}"
|
|
733
|
+
data-package-id="#{h(identity)}"
|
|
456
734
|
data-gem-updated="#{updated}"
|
|
457
735
|
data-package-scope="#{h(gem[:package_scope])}"
|
|
458
|
-
data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: gem[:name]))}"
|
|
736
|
+
data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: gem[:name], package_id: identity))}"
|
|
459
737
|
#{hidden}
|
|
460
738
|
>
|
|
461
739
|
<span class="gem-name-row">
|
|
@@ -480,10 +758,10 @@ module Gemstar
|
|
|
480
758
|
HTML
|
|
481
759
|
end
|
|
482
760
|
|
|
483
|
-
def render_detail
|
|
761
|
+
def render_detail(force_refresh: false, use_github_cli: false)
|
|
484
762
|
return empty_detail_html unless @selected_gem
|
|
485
763
|
|
|
486
|
-
metadata = metadata_for(@selected_gem, refresh_if_missing: true)
|
|
764
|
+
metadata = metadata_for(@selected_gem, refresh_if_missing: true, force_refresh: force_refresh)
|
|
487
765
|
cache_key = [
|
|
488
766
|
CACHE_VERSION,
|
|
489
767
|
@selected_project_index,
|
|
@@ -502,14 +780,15 @@ module Gemstar
|
|
|
502
780
|
@selected_gem[:status]
|
|
503
781
|
]
|
|
504
782
|
detail_cache = self.class.opts[:detail_html_cache]
|
|
505
|
-
return detail_cache[cache_key] if detail_cache.key?(cache_key)
|
|
783
|
+
return detail_cache[cache_key] if !force_refresh && detail_cache.key?(cache_key)
|
|
506
784
|
|
|
507
|
-
groups = grouped_change_sections(@selected_gem)
|
|
785
|
+
groups = grouped_change_sections(@selected_gem, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
508
786
|
detail_pending = detail_pending?(@selected_gem[:name], metadata, groups)
|
|
509
787
|
|
|
510
788
|
detail_html = <<~HTML
|
|
511
|
-
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="#{detail_pending}" data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: @selected_gem[:name]))}">
|
|
789
|
+
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="#{detail_pending}" data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, scope: @selected_package_scope, package: @selected_gem[:name], package_id: package_identity(@selected_gem)))}">
|
|
512
790
|
#{render_detail_hero(metadata)}
|
|
791
|
+
#{render_selected_dependency_details}
|
|
513
792
|
#{render_detail_loading_notice if detail_pending}
|
|
514
793
|
#{render_detail_revision_panel(groups)}
|
|
515
794
|
</section>
|
|
@@ -528,7 +807,8 @@ module Gemstar
|
|
|
528
807
|
to: @selected_to_revision_id,
|
|
529
808
|
filter: @selected_filter,
|
|
530
809
|
scope: @selected_package_scope,
|
|
531
|
-
package: @selected_gem[:name]
|
|
810
|
+
package: @selected_gem[:name],
|
|
811
|
+
package_id: package_identity(@selected_gem)
|
|
532
812
|
)
|
|
533
813
|
|
|
534
814
|
<<~HTML
|
|
@@ -541,11 +821,13 @@ module Gemstar
|
|
|
541
821
|
end
|
|
542
822
|
|
|
543
823
|
def empty_detail_html(loading: false)
|
|
824
|
+
package_label = @selected_project&.package_item_label || "package"
|
|
825
|
+
|
|
544
826
|
<<~HTML
|
|
545
827
|
<section class="detail" data-detail-panel tabindex="0">
|
|
546
828
|
<div class="empty-panel">
|
|
547
|
-
<h2>#{loading ? "Loading details" : "No
|
|
548
|
-
<p>#{loading ? "Preparing the selected
|
|
829
|
+
<h2>#{loading ? "Loading details" : "No #{h(package_label)} selected"}</h2>
|
|
830
|
+
<p>#{loading ? "Preparing the selected #{h(package_label)} details." : "Choose a #{h(package_label)} from the list to inspect its current version and changelog revisions."}</p>
|
|
549
831
|
</div>
|
|
550
832
|
</section>
|
|
551
833
|
HTML
|
|
@@ -553,10 +835,7 @@ module Gemstar
|
|
|
553
835
|
|
|
554
836
|
def render_detail_hero(metadata)
|
|
555
837
|
description = metadata&.dig("info")
|
|
556
|
-
bundle_origins = Array(@selected_gem[:bundle_origins])
|
|
557
|
-
requirement_names = selected_gem_requirements
|
|
558
838
|
bundled_version = detail_bundled_version(metadata)
|
|
559
|
-
added_on = selected_gem_added_on
|
|
560
839
|
title_url = metadata&.dig("homepage_uri")
|
|
561
840
|
title_url = repo_url_for(@selected_gem, metadata: metadata) if title_url.to_s.empty?
|
|
562
841
|
title_markup = if title_url.to_s.empty?
|
|
@@ -570,17 +849,50 @@ module Gemstar
|
|
|
570
849
|
<div class="detail-hero-copy">
|
|
571
850
|
<div class="detail-title-row">
|
|
572
851
|
<div class="detail-title-lockup">
|
|
573
|
-
<
|
|
852
|
+
<div class="detail-heading">
|
|
853
|
+
<h2>#{title_markup}#{bundled_version ? %(<span class="detail-title-version"> #{h(bundled_version)}</span>) : ""}</h2>
|
|
854
|
+
#{render_package_type_badge(@selected_gem)}
|
|
855
|
+
</div>
|
|
574
856
|
</div>
|
|
575
857
|
#{render_detail_links(metadata)}
|
|
576
858
|
</div>
|
|
577
859
|
<div class="detail-subtitle">#{render_detail_subtitle(description)}</div>
|
|
578
|
-
#{render_dependency_details(bundle_origins, requirement_names, added_on)}
|
|
579
860
|
</div>
|
|
580
861
|
</section>
|
|
581
862
|
HTML
|
|
582
863
|
end
|
|
583
864
|
|
|
865
|
+
def render_package_type_badge(package_state)
|
|
866
|
+
scope = package_state[:package_scope].to_s
|
|
867
|
+
label, accessible_label, icon_type = case scope
|
|
868
|
+
when "gems"
|
|
869
|
+
["Gem", "Ruby gem", :rubygems]
|
|
870
|
+
when "cargo"
|
|
871
|
+
["Crate", "Rust crate", :cargo]
|
|
872
|
+
when "js"
|
|
873
|
+
["JS", "JavaScript package", :javascript]
|
|
874
|
+
when "python"
|
|
875
|
+
["Python", "Python package", :python]
|
|
876
|
+
else
|
|
877
|
+
[package_state[:package_type_label] || "Package", "Package", :package]
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
<<~HTML.chomp
|
|
881
|
+
<span class="detail-package-type detail-package-type-#{h(scope)}" aria-label="#{h(accessible_label)}" title="#{h(accessible_label)}">
|
|
882
|
+
<span class="detail-package-type-icon" aria-hidden="true">#{icon_svg(icon_type)}</span>
|
|
883
|
+
<span>#{h(label)}</span>
|
|
884
|
+
</span>
|
|
885
|
+
HTML
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def render_selected_dependency_details
|
|
889
|
+
render_dependency_details(
|
|
890
|
+
Array(@selected_gem[:bundle_origins]),
|
|
891
|
+
selected_gem_requirements,
|
|
892
|
+
selected_gem_added_on
|
|
893
|
+
)
|
|
894
|
+
end
|
|
895
|
+
|
|
584
896
|
def render_detail_subtitle(description)
|
|
585
897
|
text = description.to_s.strip
|
|
586
898
|
return "<p>Metadata will appear here when package information is available.</p>" if text.empty?
|
|
@@ -697,6 +1009,7 @@ module Gemstar
|
|
|
697
1009
|
@selected_gem[:name],
|
|
698
1010
|
package_scope: @selected_gem[:package_scope],
|
|
699
1011
|
source_file: @selected_gem[:package_source_file],
|
|
1012
|
+
package_key: @selected_gem[:package_lock_key],
|
|
700
1013
|
revision_id: revision_id
|
|
701
1014
|
)
|
|
702
1015
|
end
|
|
@@ -754,6 +1067,26 @@ module Gemstar
|
|
|
754
1067
|
else
|
|
755
1068
|
["npm (#{h(remote)})"]
|
|
756
1069
|
end
|
|
1070
|
+
when :pypi
|
|
1071
|
+
remote = source[:remote]
|
|
1072
|
+
registry_url = source[:registry_url]
|
|
1073
|
+
if remote.to_s.empty? && registry_url.to_s.empty?
|
|
1074
|
+
["PyPI"]
|
|
1075
|
+
elsif remote.to_s.empty?
|
|
1076
|
+
["PyPI (#{h(registry_url)})"]
|
|
1077
|
+
else
|
|
1078
|
+
["PyPI (#{h(remote)})"]
|
|
1079
|
+
end
|
|
1080
|
+
when :cargo
|
|
1081
|
+
remote = source[:remote]
|
|
1082
|
+
registry_url = source[:registry_url]
|
|
1083
|
+
if remote.to_s.empty? && registry_url.to_s.empty?
|
|
1084
|
+
["Cargo registry"]
|
|
1085
|
+
elsif remote.to_s.empty?
|
|
1086
|
+
["Cargo registry (#{h(registry_url)})"]
|
|
1087
|
+
else
|
|
1088
|
+
["Cargo registry (#{h(remote)})"]
|
|
1089
|
+
end
|
|
757
1090
|
else
|
|
758
1091
|
[]
|
|
759
1092
|
end
|
|
@@ -792,13 +1125,26 @@ module Gemstar
|
|
|
792
1125
|
def render_detail_revision_panel(groups)
|
|
793
1126
|
<<~HTML
|
|
794
1127
|
<section class="revision-panel">
|
|
795
|
-
#{render_revision_group("
|
|
796
|
-
#{
|
|
797
|
-
#{
|
|
1128
|
+
#{render_revision_group("Available", groups[:latest], empty_message: nil) if groups[:latest].any?}
|
|
1129
|
+
#{render_current_revision_group(groups)}
|
|
1130
|
+
#{render_previous_revision_group(groups)}
|
|
798
1131
|
</section>
|
|
799
1132
|
HTML
|
|
800
1133
|
end
|
|
801
1134
|
|
|
1135
|
+
def render_current_revision_group(groups)
|
|
1136
|
+
return "" if unchanged_worktree_package? && groups[:previous].any?
|
|
1137
|
+
|
|
1138
|
+
render_revision_group(current_section_title, groups[:current], empty_message: "No changelog entries in this revision range.")
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def render_previous_revision_group(groups)
|
|
1142
|
+
return "" if groups[:previous].empty?
|
|
1143
|
+
|
|
1144
|
+
title = unchanged_worktree_package? ? "Included in #{selected_from_revision_label}" : "Earlier changes"
|
|
1145
|
+
render_revision_group(title, groups[:previous], empty_message: nil)
|
|
1146
|
+
end
|
|
1147
|
+
|
|
802
1148
|
def render_detail_loading_notice
|
|
803
1149
|
<<~HTML
|
|
804
1150
|
<section class="empty-panel">
|
|
@@ -860,8 +1206,8 @@ module Gemstar
|
|
|
860
1206
|
%(<span class="revision-release-date" title="Estimated release date">#{h(release_date)}</span>)
|
|
861
1207
|
end
|
|
862
1208
|
|
|
863
|
-
def grouped_change_sections(gem_state)
|
|
864
|
-
sections = change_sections(gem_state)
|
|
1209
|
+
def grouped_change_sections(gem_state, force_refresh: false, use_github_cli: false)
|
|
1210
|
+
sections = change_sections(gem_state, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
865
1211
|
latest = sections.select { |section| section[:kind] == :future }
|
|
866
1212
|
current = sections.select { |section| section[:kind] == :current }
|
|
867
1213
|
previous = sections.select { |section| section[:kind] == :previous }
|
|
@@ -878,8 +1224,8 @@ module Gemstar
|
|
|
878
1224
|
}
|
|
879
1225
|
end
|
|
880
1226
|
|
|
881
|
-
def change_sections(gem_state)
|
|
882
|
-
metadata_hash = metadata_for(gem_state) || {}
|
|
1227
|
+
def change_sections(gem_state, force_refresh: false, use_github_cli: false)
|
|
1228
|
+
metadata_hash = metadata_for(gem_state, refresh_if_missing: force_refresh, force_refresh: force_refresh) || {}
|
|
883
1229
|
current_version = effective_package_version(gem_state, metadata_hash)
|
|
884
1230
|
cache_key = [
|
|
885
1231
|
CACHE_VERSION,
|
|
@@ -894,18 +1240,18 @@ module Gemstar
|
|
|
894
1240
|
gem_state[:status]
|
|
895
1241
|
]
|
|
896
1242
|
change_sections_cache = self.class.opts[:change_sections_cache]
|
|
897
|
-
return change_sections_cache[cache_key] if change_sections_cache.key?(cache_key)
|
|
1243
|
+
return change_sections_cache[cache_key] if !force_refresh && change_sections_cache.key?(cache_key)
|
|
898
1244
|
|
|
899
1245
|
return [] if gem_state[:new_version].nil? &&
|
|
900
1246
|
gem_state[:old_version].nil? &&
|
|
901
1247
|
(current_version.nil? || current_version.to_s.empty?)
|
|
902
1248
|
metadata = metadata_adapter_for(gem_state)
|
|
903
1249
|
return change_sections_cache[cache_key] = [] unless metadata
|
|
904
|
-
sections = resolved_sections(metadata, gem_state)
|
|
1250
|
+
sections = resolved_sections(metadata, gem_state, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
905
1251
|
return change_sections_cache[cache_key] = [] if sections.nil? || sections.empty?
|
|
906
1252
|
|
|
907
1253
|
previous_version = gem_state[:old_version]
|
|
908
|
-
release_dates = resolved_release_dates(metadata, sections.keys, gem_state)
|
|
1254
|
+
release_dates = resolved_release_dates(metadata, sections.keys, gem_state, force_refresh: force_refresh)
|
|
909
1255
|
|
|
910
1256
|
rendered_sections = sections.keys.filter_map do |version|
|
|
911
1257
|
kind = section_kind(version, previous_version, current_version, gem_state[:status])
|
|
@@ -922,15 +1268,15 @@ module Gemstar
|
|
|
922
1268
|
}
|
|
923
1269
|
end
|
|
924
1270
|
|
|
925
|
-
change_sections_cache[cache_key] = rendered_sections
|
|
1271
|
+
change_sections_cache[cache_key] = sort_change_sections(rendered_sections)
|
|
926
1272
|
rescue StandardError
|
|
927
1273
|
[]
|
|
928
1274
|
end
|
|
929
1275
|
|
|
930
|
-
def resolved_sections(metadata, gem_state)
|
|
1276
|
+
def resolved_sections(metadata, gem_state, force_refresh: false, use_github_cli: false)
|
|
931
1277
|
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)
|
|
1278
|
+
cached_sections = force_refresh ? {} : changelog.sections(cache_only: true) || {}
|
|
1279
|
+
return cached_sections unless force_refresh || selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
934
1280
|
|
|
935
1281
|
@metadata_cache.delete([gem_state[:package_scope], gem_state[:name]])
|
|
936
1282
|
metadata.meta(cache_only: false, force_refresh: true)
|
|
@@ -938,17 +1284,18 @@ module Gemstar
|
|
|
938
1284
|
refreshed_sections = metadata.changelog_sections(
|
|
939
1285
|
versions: relevant_package_versions(gem_state, metadata),
|
|
940
1286
|
cache_only: false,
|
|
941
|
-
force_refresh: true
|
|
1287
|
+
force_refresh: true,
|
|
1288
|
+
use_github_cli: use_github_cli
|
|
942
1289
|
)
|
|
943
1290
|
cached_sections.merge(refreshed_sections || {})
|
|
944
1291
|
end
|
|
945
1292
|
|
|
946
|
-
def resolved_release_dates(metadata, versions, gem_state)
|
|
1293
|
+
def resolved_release_dates(metadata, versions, gem_state, force_refresh: false)
|
|
947
1294
|
changelog = Gemstar::ChangeLog.new(metadata)
|
|
948
|
-
cached_dates = changelog.release_dates(versions: versions, cache_only: true) || {}
|
|
1295
|
+
cached_dates = force_refresh ? {} : changelog.release_dates(versions: versions, cache_only: true) || {}
|
|
949
1296
|
return cached_dates unless selected_gem_missing_release_dates?(gem_state, versions, cached_dates)
|
|
950
1297
|
|
|
951
|
-
fetched_dates = changelog.release_dates(versions: versions, cache_only: false) || {}
|
|
1298
|
+
fetched_dates = changelog.release_dates(versions: versions, cache_only: false, force_refresh: force_refresh) || {}
|
|
952
1299
|
cached_dates.merge(fetched_dates)
|
|
953
1300
|
rescue StandardError
|
|
954
1301
|
{}
|
|
@@ -972,12 +1319,15 @@ module Gemstar
|
|
|
972
1319
|
value = version.to_s.strip
|
|
973
1320
|
return nil if value.empty?
|
|
974
1321
|
|
|
975
|
-
value.sub(/\Av/i, "")
|
|
1322
|
+
value = value.sub(/\Av/i, "")
|
|
1323
|
+
Gem::Version.new(value.gsub(/-[\w\-]+$/, "")).canonical_segments.join(".")
|
|
1324
|
+
rescue ArgumentError
|
|
1325
|
+
value
|
|
976
1326
|
end
|
|
977
1327
|
|
|
978
1328
|
def relevant_package_versions(gem_state, metadata)
|
|
979
1329
|
metadata_hash = metadata_for(gem_state) || {}
|
|
980
|
-
[
|
|
1330
|
+
explicit_versions = [
|
|
981
1331
|
gem_state[:old_version],
|
|
982
1332
|
gem_state[:new_version],
|
|
983
1333
|
gem_state[:raw_old_version],
|
|
@@ -985,6 +1335,27 @@ module Gemstar
|
|
|
985
1335
|
gem_state.dig(:source, :package_version),
|
|
986
1336
|
metadata_hash["version"]
|
|
987
1337
|
].compact
|
|
1338
|
+
|
|
1339
|
+
(explicit_versions + registry_versions_between(metadata, gem_state, metadata_hash)).uniq
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
def registry_versions_between(metadata, gem_state, metadata_hash)
|
|
1343
|
+
return [] unless metadata.respond_to?(:registry_release_dates)
|
|
1344
|
+
|
|
1345
|
+
lower_bound = gem_state[:old_version] || gem_state[:raw_old_version] || gem_state.dig(:source, :package_version)
|
|
1346
|
+
current_version = effective_package_version(gem_state, metadata_hash)
|
|
1347
|
+
upper_bound = metadata_hash["version"] || current_version
|
|
1348
|
+
return [] if lower_bound.to_s.empty? || upper_bound.to_s.empty?
|
|
1349
|
+
|
|
1350
|
+
release_dates = metadata.registry_release_dates(cache_only: true)
|
|
1351
|
+
release_dates = metadata.registry_release_dates(cache_only: false) if release_dates.nil? || release_dates.empty?
|
|
1352
|
+
|
|
1353
|
+
release_dates.keys.select do |version|
|
|
1354
|
+
compare_versions(version, lower_bound) == 1 &&
|
|
1355
|
+
compare_versions(version, upper_bound) <= 0
|
|
1356
|
+
end
|
|
1357
|
+
rescue StandardError
|
|
1358
|
+
[]
|
|
988
1359
|
end
|
|
989
1360
|
|
|
990
1361
|
def selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
@@ -1031,17 +1402,14 @@ module Gemstar
|
|
|
1031
1402
|
:previous if compare_versions(version, lower_bound) <= 0
|
|
1032
1403
|
end
|
|
1033
1404
|
|
|
1034
|
-
def
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1405
|
+
def sort_change_sections(sections)
|
|
1406
|
+
kind_ranks = { future: 0, current: 1, previous: 2 }
|
|
1407
|
+
sections.sort do |left, right|
|
|
1408
|
+
kind_comparison = kind_ranks.fetch(left[:kind], 9) <=> kind_ranks.fetch(right[:kind], 9)
|
|
1409
|
+
next kind_comparison unless kind_comparison.zero?
|
|
1038
1410
|
|
|
1039
|
-
|
|
1040
|
-
Gem::Version.new(version.to_s.gsub(/-[\w\-]+$/, "")).segments.take(6).each_with_index.sum do |segment, index|
|
|
1041
|
-
segment.to_i * (10**(10 - index * 2))
|
|
1411
|
+
compare_versions(right[:version], left[:version])
|
|
1042
1412
|
end
|
|
1043
|
-
rescue ArgumentError
|
|
1044
|
-
0
|
|
1045
1413
|
end
|
|
1046
1414
|
|
|
1047
1415
|
def changelog_content(lines, heading_version: nil)
|
|
@@ -1077,7 +1445,7 @@ module Gemstar
|
|
|
1077
1445
|
end
|
|
1078
1446
|
|
|
1079
1447
|
def strip_leading_version_heading(text, heading_version)
|
|
1080
|
-
stripped = text.sub(/\A\s*#+\s*v?#{Regexp.escape(heading_version)}\
|
|
1448
|
+
stripped = text.sub(/\A\s*#+\s*[\[(]?(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b[^\n]*\n+/i, "")
|
|
1081
1449
|
return strip_leading_heading_separator(stripped) unless stripped == text
|
|
1082
1450
|
|
|
1083
1451
|
lines = text.lines
|
|
@@ -1085,8 +1453,8 @@ module Gemstar
|
|
|
1085
1453
|
|
|
1086
1454
|
first_line = lines.first.to_s
|
|
1087
1455
|
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)
|
|
1456
|
+
first_line.match?(/\A\s*(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b/i) ||
|
|
1457
|
+
first_line.match?(/\A\s*[\[(]?(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b/i)
|
|
1090
1458
|
|
|
1091
1459
|
return text unless heading_like
|
|
1092
1460
|
|
|
@@ -1096,7 +1464,7 @@ module Gemstar
|
|
|
1096
1464
|
end
|
|
1097
1465
|
|
|
1098
1466
|
def strip_leading_heading_separator(text)
|
|
1099
|
-
text.sub(/\A\s*(?:#{Regexp.escape("#")}{4,}|[
|
|
1467
|
+
text.sub(/\A\s*(?:#{Regexp.escape("#")}{4,}|[-=~`^"']{3,})\s*\n+/, "")
|
|
1100
1468
|
end
|
|
1101
1469
|
|
|
1102
1470
|
def compare_versions(left, right)
|
|
@@ -1105,26 +1473,26 @@ module Gemstar
|
|
|
1105
1473
|
left.to_s <=> right.to_s
|
|
1106
1474
|
end
|
|
1107
1475
|
|
|
1108
|
-
def metadata_for(package_state_or_name, refresh_if_missing: false)
|
|
1476
|
+
def metadata_for(package_state_or_name, refresh_if_missing: false, force_refresh: false)
|
|
1109
1477
|
package_state = package_state_or_name.is_a?(Hash) ? package_state_or_name : { name: package_state_or_name, package_scope: "gems" }
|
|
1110
1478
|
gem_name = package_state[:name]
|
|
1111
1479
|
cache_key = [package_state[:package_scope], gem_name]
|
|
1112
1480
|
cached = @metadata_cache[cache_key]
|
|
1113
|
-
return nil if cached.equal?(MISSING_METADATA)
|
|
1114
|
-
return cached if cached
|
|
1481
|
+
return nil if !force_refresh && cached.equal?(MISSING_METADATA)
|
|
1482
|
+
return cached if !force_refresh && cached
|
|
1115
1483
|
|
|
1116
1484
|
if package_state[:package_scope] != "gems"
|
|
1117
1485
|
metadata = local_package_metadata(package_state)
|
|
1118
1486
|
adapter = metadata_adapter_for(package_state)
|
|
1119
|
-
remote_metadata = adapter&.meta(cache_only: true)
|
|
1487
|
+
remote_metadata = force_refresh ? nil : adapter&.meta(cache_only: true)
|
|
1120
1488
|
remote_metadata = adapter&.meta(cache_only: false, force_refresh: true) if remote_metadata.nil? && refresh_if_missing
|
|
1121
1489
|
metadata = metadata.compact.merge(remote_metadata || {})
|
|
1122
1490
|
@metadata_cache[cache_key] = metadata || MISSING_METADATA
|
|
1123
1491
|
return metadata
|
|
1124
1492
|
end
|
|
1125
1493
|
|
|
1126
|
-
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
1127
|
-
if metadata.nil? && refresh_if_missing
|
|
1494
|
+
metadata = force_refresh ? nil : Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
1495
|
+
if metadata.nil? && (refresh_if_missing || force_refresh)
|
|
1128
1496
|
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: false, force_refresh: true)
|
|
1129
1497
|
end
|
|
1130
1498
|
|
|
@@ -1143,6 +1511,34 @@ module Gemstar
|
|
|
1143
1511
|
package_version = source[:package_version].to_s
|
|
1144
1512
|
registry_url = source[:registry_url].to_s
|
|
1145
1513
|
provider_gem = source[:provider_gem].to_s
|
|
1514
|
+
if package_state[:package_scope] == "python"
|
|
1515
|
+
package_name = package_state[:name].to_s if package_name.empty?
|
|
1516
|
+
package_version = (package_state[:new_version] || package_state[:old_version]).to_s if package_version.empty?
|
|
1517
|
+
registry_url = "https://pypi.org/project/#{package_name}/" if registry_url.empty? && !package_name.empty?
|
|
1518
|
+
|
|
1519
|
+
return {
|
|
1520
|
+
"info" => "Python package `#{package_name}` locked to `#{package_version}` in uv.lock",
|
|
1521
|
+
"project_uri" => registry_url.empty? ? nil : registry_url,
|
|
1522
|
+
"homepage_uri" => absolute_url?(remote) ? remote : nil,
|
|
1523
|
+
"source_code_uri" => repo_url.empty? ? nil : repo_url
|
|
1524
|
+
}
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
if package_state[:package_scope] == "cargo"
|
|
1528
|
+
package_name = package_state[:metadata_package_name].to_s
|
|
1529
|
+
package_name = source[:package_name].to_s if package_name.empty?
|
|
1530
|
+
package_name = package_state[:name].to_s if package_name.empty?
|
|
1531
|
+
package_version = (package_state[:new_version] || package_state[:old_version]).to_s if package_version.empty?
|
|
1532
|
+
registry_url = "https://crates.io/crates/#{package_name}" if registry_url.empty? && !package_name.empty?
|
|
1533
|
+
|
|
1534
|
+
return {
|
|
1535
|
+
"info" => "Rust crate `#{package_name}` locked to `#{package_version}` in Cargo.lock",
|
|
1536
|
+
"project_uri" => registry_url.empty? ? nil : registry_url,
|
|
1537
|
+
"homepage_uri" => absolute_url?(remote) ? remote : nil,
|
|
1538
|
+
"source_code_uri" => repo_url.empty? ? nil : repo_url
|
|
1539
|
+
}
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1146
1542
|
package_name = package_state[:name].to_s if package_name.empty? && source[:type] == :npm
|
|
1147
1543
|
package_version = (package_state[:new_version] || package_state[:old_version]).to_s if package_version.empty? && source[:type] == :npm
|
|
1148
1544
|
registry_url = "https://www.npmjs.com/package/#{package_name}" if registry_url.empty? && !package_name.empty?
|
|
@@ -1190,6 +1586,11 @@ module Gemstar
|
|
|
1190
1586
|
|
|
1191
1587
|
def metadata_adapter_for(package_state)
|
|
1192
1588
|
return Gemstar::RubyGemsMetadata.new(package_state[:name]) if package_state[:package_scope] == "gems"
|
|
1589
|
+
return Gemstar::PyPIMetadata.new(package_state[:name]) if package_state[:package_scope] == "python"
|
|
1590
|
+
if package_state[:package_scope] == "cargo"
|
|
1591
|
+
package_name = package_state[:metadata_package_name] || package_state.dig(:source, :package_name) || package_state[:name]
|
|
1592
|
+
return Gemstar::CratesIOMetadata.new(package_name)
|
|
1593
|
+
end
|
|
1193
1594
|
return nil unless package_state[:package_scope] == "js"
|
|
1194
1595
|
|
|
1195
1596
|
provider_gem = package_state.dig(:source, :provider_gem)
|
|
@@ -1224,7 +1625,15 @@ module Gemstar
|
|
|
1224
1625
|
when :home
|
|
1225
1626
|
'<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 .8 1.2 6.3v8.9h4.3V10h5v5.2h4.3V6.3L8 .8Zm5.2 13.3h-1.8V8.9H4.6v5.2H2.8V6.8L8 2.6l5.2 4.2v7.3Z"/></svg>'
|
|
1226
1627
|
when :rubygems
|
|
1227
|
-
'<svg viewBox="0 0 16 16" aria-hidden="true"><
|
|
1628
|
+
'<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="#cc342d" d="M4.2 2.2h7.6l2.7 4.1L8 14.2 1.5 6.3l2.7-4.1Z"/><path fill="#ef5b55" d="m4.2 2.2 1.5 4.1H1.5l2.7-4.1Zm7.6 0 2.7 4.1h-4.2l1.5-4.1Z"/><path fill="#a91f1a" d="M5.7 6.3h4.6L8 14.2 5.7 6.3Z"/><path fill="#fff" fill-opacity=".42" d="m5 3.15.7 2H3.3l1.3-2h.4Z"/></svg>'
|
|
1629
|
+
when :cargo
|
|
1630
|
+
'<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" d="m2.2 5 5.8-3 5.8 3v6L8 14l-5.8-3V5Z"/><path fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" d="M2.4 5 8 8l5.6-3M8 8v6M5.2 3.45 11 6.5"/></svg>'
|
|
1631
|
+
when :javascript
|
|
1632
|
+
'<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.5" y="1.5" width="13" height="13" rx="2" fill="none" stroke="currentColor" stroke-width="1.2"/><text x="8" y="10.8" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, monospace" font-size="6.3" font-weight="700" fill="currentColor">JS</text></svg>'
|
|
1633
|
+
when :python
|
|
1634
|
+
'<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" d="M8 1.8H5.6c-1.7 0-2.4.8-2.4 2.3v2.1h5.6v1.6H4.5C2.8 7.8 2 8.7 2 10.2c0 1.7 1 2.5 2.6 2.5H7"/><path fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" d="M8 14.2h2.4c1.7 0 2.4-.8 2.4-2.3V9.8H7.2V8.2h4.3c1.7 0 2.5-.9 2.5-2.4 0-1.7-1-2.5-2.6-2.5H9"/><circle cx="6" cy="4.2" r=".7" fill="currentColor"/><circle cx="10" cy="11.8" r=".7" fill="currentColor"/></svg>'
|
|
1635
|
+
when :package
|
|
1636
|
+
'<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" d="m2.2 5 5.8-3 5.8 3v6L8 14l-5.8-3V5ZM2.4 5 8 8l5.6-3M8 8v6"/></svg>'
|
|
1228
1637
|
when :info
|
|
1229
1638
|
'<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="4.5" r="0.9" fill="currentColor"/><path d="M8 7v4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>'
|
|
1230
1639
|
else
|
|
@@ -1240,6 +1649,10 @@ module Gemstar
|
|
|
1240
1649
|
end
|
|
1241
1650
|
end
|
|
1242
1651
|
|
|
1652
|
+
def unchanged_worktree_package?
|
|
1653
|
+
@selected_to_revision_id == "worktree" && @selected_gem && @selected_gem[:status] == :unchanged
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1243
1656
|
def range_label(gem_state)
|
|
1244
1657
|
old_version = gem_state[:old_version]
|
|
1245
1658
|
new_version = gem_state[:new_version]
|
|
@@ -1251,7 +1664,7 @@ module Gemstar
|
|
|
1251
1664
|
end
|
|
1252
1665
|
|
|
1253
1666
|
def previous_section_version(versions, current_version)
|
|
1254
|
-
ordered_versions = versions.
|
|
1667
|
+
ordered_versions = versions.sort { |left, right| compare_versions(right, left) }
|
|
1255
1668
|
current_index = ordered_versions.index(current_version)
|
|
1256
1669
|
return nil if current_index.nil?
|
|
1257
1670
|
|
|
@@ -1305,16 +1718,27 @@ module Gemstar
|
|
|
1305
1718
|
else
|
|
1306
1719
|
%(<a href="#{h(fallback_url)}" target="_blank" rel="noreferrer">#{h(fallback_label)}</a>)
|
|
1307
1720
|
end
|
|
1721
|
+
github_cli_action = render_github_cli_release_button(version, repo_url)
|
|
1308
1722
|
|
|
1309
1723
|
{
|
|
1310
1724
|
version: version,
|
|
1311
1725
|
title: version,
|
|
1312
1726
|
kind: :current,
|
|
1313
1727
|
previous_version: fallback_previous_version_for(gem_state, previous_sections),
|
|
1314
|
-
html: "<p>No release information available. Check #{fallback_link} for more information.</p
|
|
1728
|
+
html: "<p>No release information available. Check #{fallback_link} for more information.</p>#{github_cli_action}"
|
|
1315
1729
|
}
|
|
1316
1730
|
end
|
|
1317
1731
|
|
|
1732
|
+
def render_github_cli_release_button(version, repo_url)
|
|
1733
|
+
return "" unless repo_url.to_s.include?("github.com")
|
|
1734
|
+
|
|
1735
|
+
<<~HTML
|
|
1736
|
+
<div class="detail-inline-actions">
|
|
1737
|
+
<button type="button" class="action" data-detail-use-gh data-release-version="#{h(version)}">Use GitHub CLI</button>
|
|
1738
|
+
</div>
|
|
1739
|
+
HTML
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1318
1742
|
def fallback_previous_version_for(gem_state, previous_sections)
|
|
1319
1743
|
return gem_state[:old_version] if gem_state[:new_version]
|
|
1320
1744
|
return previous_sections.first[:version] if previous_sections.any?
|
|
@@ -1361,18 +1785,19 @@ module Gemstar
|
|
|
1361
1785
|
html
|
|
1362
1786
|
end
|
|
1363
1787
|
|
|
1364
|
-
def detail_query(project:, from:, to:, filter:, scope:, package:)
|
|
1365
|
-
"/detail?#{URI.encode_www_form(project: project, from: from, to: to, filter: filter, scope: scope, package: package)}"
|
|
1788
|
+
def detail_query(project:, from:, to:, filter:, scope:, package:, package_id: nil)
|
|
1789
|
+
"/detail?#{URI.encode_www_form({project: project, from: from, to: to, filter: filter, scope: scope, package: package, package_id: package_id}.compact)}"
|
|
1366
1790
|
end
|
|
1367
1791
|
|
|
1368
|
-
def project_query(project:, from:, to:, filter:, scope:, package:)
|
|
1792
|
+
def project_query(project:, from:, to:, filter:, scope:, package:, package_id: nil)
|
|
1369
1793
|
params = {
|
|
1370
1794
|
project: project,
|
|
1371
1795
|
from: from,
|
|
1372
1796
|
to: to,
|
|
1373
1797
|
filter: filter,
|
|
1374
1798
|
scope: scope,
|
|
1375
|
-
package: package
|
|
1799
|
+
package: package,
|
|
1800
|
+
package_id: package_id
|
|
1376
1801
|
}.compact
|
|
1377
1802
|
|
|
1378
1803
|
"/?#{URI.encode_www_form(params)}"
|