gemstar 1.2 → 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 +20 -0
- data/README.md +4 -2
- data/lib/gemstar/cache_warmer.rb +36 -1
- data/lib/gemstar/cargo_lock_file.rb +138 -0
- data/lib/gemstar/change_log.rb +127 -46
- data/lib/gemstar/cli.rb +8 -2
- data/lib/gemstar/commands/diff.rb +13 -2
- data/lib/gemstar/commands/server.rb +32 -11
- data/lib/gemstar/crates_io_metadata.rb +140 -0
- data/lib/gemstar/data/ruby_gems_metadata.json +10 -0
- 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 +26 -3
- data/lib/gemstar/uv_lock_file.rb +114 -0
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +452 -49
- data/lib/gemstar/web/templates/app.css +131 -4
- data/lib/gemstar/web/templates/app.js.erb +178 -21
- 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)
|
|
@@ -82,6 +87,23 @@ module Gemstar
|
|
|
82
87
|
File.read(project.gemfile_path)
|
|
83
88
|
end
|
|
84
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
|
+
|
|
85
107
|
r.on "projects", String do |project_id|
|
|
86
108
|
response.redirect "/?project=#{project_id}"
|
|
87
109
|
end
|
|
@@ -104,6 +126,7 @@ module Gemstar
|
|
|
104
126
|
importmap_stamp = File.file?(project.importmap_path) ? File.mtime(project.importmap_path).to_i : 0
|
|
105
127
|
importmap_vendor_stamp = importmap_vendor_mtime(project)
|
|
106
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
|
|
107
130
|
|
|
108
131
|
[
|
|
109
132
|
CACHE_VERSION,
|
|
@@ -113,10 +136,12 @@ module Gemstar
|
|
|
113
136
|
params["filter"],
|
|
114
137
|
params["scope"],
|
|
115
138
|
package_param(params),
|
|
139
|
+
params["package_id"],
|
|
116
140
|
lockfile_stamp,
|
|
117
141
|
importmap_stamp,
|
|
118
142
|
importmap_vendor_stamp,
|
|
119
|
-
package_lock_stamp
|
|
143
|
+
package_lock_stamp,
|
|
144
|
+
cargo_lock_stamp
|
|
120
145
|
]
|
|
121
146
|
end
|
|
122
147
|
|
|
@@ -156,10 +181,12 @@ module Gemstar
|
|
|
156
181
|
@selected_to_revision_id = selected_to_revision_id(@selected_to_revision_id)
|
|
157
182
|
@gem_states = @selected_project ? @selected_project.gem_states(from_revision_id: @selected_from_revision_id, to_revision_id: @selected_to_revision_id) : []
|
|
158
183
|
requested_package_name = package_param(params)
|
|
184
|
+
requested_package_id = params["package_id"]
|
|
159
185
|
@requested_gem_name = requested_package_name
|
|
160
|
-
@
|
|
161
|
-
@
|
|
162
|
-
@
|
|
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)
|
|
163
190
|
end
|
|
164
191
|
|
|
165
192
|
def package_param(params)
|
|
@@ -201,33 +228,36 @@ module Gemstar
|
|
|
201
228
|
valid_ids.include?(candidate) ? candidate : valid_ids.first || "worktree"
|
|
202
229
|
end
|
|
203
230
|
|
|
204
|
-
def selected_filter(raw_filter, raw_gem_name)
|
|
231
|
+
def selected_filter(raw_filter, raw_gem_name, raw_package_id = nil)
|
|
205
232
|
return "all" if @gem_states.empty?
|
|
233
|
+
return "all" unless @gem_states.any? { |gem| gem[:status] != :unchanged }
|
|
206
234
|
return raw_filter if %w[updated all].include?(raw_filter)
|
|
207
235
|
|
|
208
|
-
selected_gem =
|
|
236
|
+
selected_gem = requested_gem_state(raw_gem_name, raw_package_id)
|
|
209
237
|
return "all" if selected_gem && selected_gem[:status] == :unchanged
|
|
210
238
|
|
|
211
239
|
@gem_states.any? { |gem| gem[:status] != :unchanged } ? "updated" : "all"
|
|
212
240
|
end
|
|
213
241
|
|
|
214
|
-
def selected_package_scope(raw_scope, raw_gem_name)
|
|
242
|
+
def selected_package_scope(raw_scope, raw_gem_name, raw_package_id = nil)
|
|
215
243
|
return "all" if @gem_states.empty?
|
|
216
244
|
|
|
217
245
|
available_scopes = available_package_scopes
|
|
218
246
|
default_scope = available_scopes == ["gems"] ? "gems" : "all"
|
|
219
247
|
return raw_scope if raw_scope == "all" || available_scopes.include?(raw_scope)
|
|
220
248
|
|
|
221
|
-
selected_gem =
|
|
249
|
+
selected_gem = requested_gem_state(raw_gem_name, raw_package_id)
|
|
222
250
|
return selected_gem[:package_scope] if selected_gem
|
|
223
251
|
|
|
224
252
|
default_scope
|
|
225
253
|
end
|
|
226
254
|
|
|
227
|
-
def selected_gem_state(raw_gem_name)
|
|
255
|
+
def selected_gem_state(raw_gem_name, raw_package_id = nil)
|
|
228
256
|
return nil if @gem_states.empty?
|
|
229
257
|
|
|
230
|
-
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) }
|
|
231
261
|
exact_match ||= @gem_states.find { |gem| gem[:name] == raw_gem_name }
|
|
232
262
|
return exact_match if exact_match
|
|
233
263
|
|
|
@@ -237,6 +267,23 @@ module Gemstar
|
|
|
237
267
|
@gem_states.first
|
|
238
268
|
end
|
|
239
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
|
+
|
|
240
287
|
def gem_visible_in_selected_filter?(gem_state)
|
|
241
288
|
return false unless gem_visible_in_selected_scope?(gem_state)
|
|
242
289
|
return true if @selected_filter != "updated"
|
|
@@ -404,13 +451,231 @@ module Gemstar
|
|
|
404
451
|
<strong>#{@gem_states.count { |gem| gem[:status] != :unchanged }}</strong> changes from #{h(selected_from_revision_label)} to #{h(selected_to_revision_label)}
|
|
405
452
|
</div>
|
|
406
453
|
<div class="toolbar-actions">
|
|
407
|
-
|
|
408
|
-
<button type="button" class="action action-primary" disabled="disabled">bundle update</button>
|
|
454
|
+
#{render_project_action_buttons}
|
|
409
455
|
</div>
|
|
410
456
|
</section>
|
|
411
457
|
HTML
|
|
412
458
|
end
|
|
413
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
|
+
|
|
414
679
|
def selected_from_revision_label
|
|
415
680
|
@revision_options.find { |option| option[:id] == @selected_from_revision_id }&.dig(:label) || "worktree"
|
|
416
681
|
end
|
|
@@ -453,19 +718,22 @@ module Gemstar
|
|
|
453
718
|
HTML
|
|
454
719
|
|
|
455
720
|
items = @gem_states.map do |gem|
|
|
456
|
-
|
|
721
|
+
identity = package_identity(gem)
|
|
722
|
+
selected = identity == package_identity(@selected_gem) ? " is-selected" : ""
|
|
457
723
|
status_class = " status-#{gem[:status]}"
|
|
458
724
|
updated = gem[:status] != :unchanged
|
|
459
|
-
|
|
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"' : ""
|
|
460
727
|
<<~HTML
|
|
461
728
|
<a
|
|
462
729
|
class="gem-row#{selected}#{status_class}"
|
|
463
|
-
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)}"
|
|
464
731
|
data-gem-link="true"
|
|
465
732
|
data-gem-name="#{h(gem[:name])}"
|
|
733
|
+
data-package-id="#{h(identity)}"
|
|
466
734
|
data-gem-updated="#{updated}"
|
|
467
735
|
data-package-scope="#{h(gem[:package_scope])}"
|
|
468
|
-
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))}"
|
|
469
737
|
#{hidden}
|
|
470
738
|
>
|
|
471
739
|
<span class="gem-name-row">
|
|
@@ -518,8 +786,9 @@ module Gemstar
|
|
|
518
786
|
detail_pending = detail_pending?(@selected_gem[:name], metadata, groups)
|
|
519
787
|
|
|
520
788
|
detail_html = <<~HTML
|
|
521
|
-
<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)))}">
|
|
522
790
|
#{render_detail_hero(metadata)}
|
|
791
|
+
#{render_selected_dependency_details}
|
|
523
792
|
#{render_detail_loading_notice if detail_pending}
|
|
524
793
|
#{render_detail_revision_panel(groups)}
|
|
525
794
|
</section>
|
|
@@ -538,7 +807,8 @@ module Gemstar
|
|
|
538
807
|
to: @selected_to_revision_id,
|
|
539
808
|
filter: @selected_filter,
|
|
540
809
|
scope: @selected_package_scope,
|
|
541
|
-
package: @selected_gem[:name]
|
|
810
|
+
package: @selected_gem[:name],
|
|
811
|
+
package_id: package_identity(@selected_gem)
|
|
542
812
|
)
|
|
543
813
|
|
|
544
814
|
<<~HTML
|
|
@@ -551,11 +821,13 @@ module Gemstar
|
|
|
551
821
|
end
|
|
552
822
|
|
|
553
823
|
def empty_detail_html(loading: false)
|
|
824
|
+
package_label = @selected_project&.package_item_label || "package"
|
|
825
|
+
|
|
554
826
|
<<~HTML
|
|
555
827
|
<section class="detail" data-detail-panel tabindex="0">
|
|
556
828
|
<div class="empty-panel">
|
|
557
|
-
<h2>#{loading ? "Loading details" : "No
|
|
558
|
-
<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>
|
|
559
831
|
</div>
|
|
560
832
|
</section>
|
|
561
833
|
HTML
|
|
@@ -563,10 +835,7 @@ module Gemstar
|
|
|
563
835
|
|
|
564
836
|
def render_detail_hero(metadata)
|
|
565
837
|
description = metadata&.dig("info")
|
|
566
|
-
bundle_origins = Array(@selected_gem[:bundle_origins])
|
|
567
|
-
requirement_names = selected_gem_requirements
|
|
568
838
|
bundled_version = detail_bundled_version(metadata)
|
|
569
|
-
added_on = selected_gem_added_on
|
|
570
839
|
title_url = metadata&.dig("homepage_uri")
|
|
571
840
|
title_url = repo_url_for(@selected_gem, metadata: metadata) if title_url.to_s.empty?
|
|
572
841
|
title_markup = if title_url.to_s.empty?
|
|
@@ -580,17 +849,50 @@ module Gemstar
|
|
|
580
849
|
<div class="detail-hero-copy">
|
|
581
850
|
<div class="detail-title-row">
|
|
582
851
|
<div class="detail-title-lockup">
|
|
583
|
-
<
|
|
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>
|
|
584
856
|
</div>
|
|
585
857
|
#{render_detail_links(metadata)}
|
|
586
858
|
</div>
|
|
587
859
|
<div class="detail-subtitle">#{render_detail_subtitle(description)}</div>
|
|
588
|
-
#{render_dependency_details(bundle_origins, requirement_names, added_on)}
|
|
589
860
|
</div>
|
|
590
861
|
</section>
|
|
591
862
|
HTML
|
|
592
863
|
end
|
|
593
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
|
+
|
|
594
896
|
def render_detail_subtitle(description)
|
|
595
897
|
text = description.to_s.strip
|
|
596
898
|
return "<p>Metadata will appear here when package information is available.</p>" if text.empty?
|
|
@@ -707,6 +1009,7 @@ module Gemstar
|
|
|
707
1009
|
@selected_gem[:name],
|
|
708
1010
|
package_scope: @selected_gem[:package_scope],
|
|
709
1011
|
source_file: @selected_gem[:package_source_file],
|
|
1012
|
+
package_key: @selected_gem[:package_lock_key],
|
|
710
1013
|
revision_id: revision_id
|
|
711
1014
|
)
|
|
712
1015
|
end
|
|
@@ -764,6 +1067,26 @@ module Gemstar
|
|
|
764
1067
|
else
|
|
765
1068
|
["npm (#{h(remote)})"]
|
|
766
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
|
|
767
1090
|
else
|
|
768
1091
|
[]
|
|
769
1092
|
end
|
|
@@ -802,13 +1125,26 @@ module Gemstar
|
|
|
802
1125
|
def render_detail_revision_panel(groups)
|
|
803
1126
|
<<~HTML
|
|
804
1127
|
<section class="revision-panel">
|
|
805
|
-
#{render_revision_group("
|
|
806
|
-
#{
|
|
807
|
-
#{
|
|
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)}
|
|
808
1131
|
</section>
|
|
809
1132
|
HTML
|
|
810
1133
|
end
|
|
811
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
|
+
|
|
812
1148
|
def render_detail_loading_notice
|
|
813
1149
|
<<~HTML
|
|
814
1150
|
<section class="empty-panel">
|
|
@@ -932,7 +1268,7 @@ module Gemstar
|
|
|
932
1268
|
}
|
|
933
1269
|
end
|
|
934
1270
|
|
|
935
|
-
change_sections_cache[cache_key] = rendered_sections
|
|
1271
|
+
change_sections_cache[cache_key] = sort_change_sections(rendered_sections)
|
|
936
1272
|
rescue StandardError
|
|
937
1273
|
[]
|
|
938
1274
|
end
|
|
@@ -983,12 +1319,15 @@ module Gemstar
|
|
|
983
1319
|
value = version.to_s.strip
|
|
984
1320
|
return nil if value.empty?
|
|
985
1321
|
|
|
986
|
-
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
|
|
987
1326
|
end
|
|
988
1327
|
|
|
989
1328
|
def relevant_package_versions(gem_state, metadata)
|
|
990
1329
|
metadata_hash = metadata_for(gem_state) || {}
|
|
991
|
-
[
|
|
1330
|
+
explicit_versions = [
|
|
992
1331
|
gem_state[:old_version],
|
|
993
1332
|
gem_state[:new_version],
|
|
994
1333
|
gem_state[:raw_old_version],
|
|
@@ -996,6 +1335,27 @@ module Gemstar
|
|
|
996
1335
|
gem_state.dig(:source, :package_version),
|
|
997
1336
|
metadata_hash["version"]
|
|
998
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
|
+
[]
|
|
999
1359
|
end
|
|
1000
1360
|
|
|
1001
1361
|
def selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
@@ -1042,17 +1402,14 @@ module Gemstar
|
|
|
1042
1402
|
:previous if compare_versions(version, lower_bound) <= 0
|
|
1043
1403
|
end
|
|
1044
1404
|
|
|
1045
|
-
def
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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?
|
|
1049
1410
|
|
|
1050
|
-
|
|
1051
|
-
Gem::Version.new(version.to_s.gsub(/-[\w\-]+$/, "")).segments.take(6).each_with_index.sum do |segment, index|
|
|
1052
|
-
segment.to_i * (10**(10 - index * 2))
|
|
1411
|
+
compare_versions(right[:version], left[:version])
|
|
1053
1412
|
end
|
|
1054
|
-
rescue ArgumentError
|
|
1055
|
-
0
|
|
1056
1413
|
end
|
|
1057
1414
|
|
|
1058
1415
|
def changelog_content(lines, heading_version: nil)
|
|
@@ -1088,7 +1445,7 @@ module Gemstar
|
|
|
1088
1445
|
end
|
|
1089
1446
|
|
|
1090
1447
|
def strip_leading_version_heading(text, heading_version)
|
|
1091
|
-
stripped = text.sub(/\A\s*#+\s*(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b[^\n]*\n+/i, "")
|
|
1448
|
+
stripped = text.sub(/\A\s*#+\s*[\[(]?(?:Version\s+)?v?#{Regexp.escape(heading_version)}\b[^\n]*\n+/i, "")
|
|
1092
1449
|
return strip_leading_heading_separator(stripped) unless stripped == text
|
|
1093
1450
|
|
|
1094
1451
|
lines = text.lines
|
|
@@ -1107,7 +1464,7 @@ module Gemstar
|
|
|
1107
1464
|
end
|
|
1108
1465
|
|
|
1109
1466
|
def strip_leading_heading_separator(text)
|
|
1110
|
-
text.sub(/\A\s*(?:#{Regexp.escape("#")}{4,}|[
|
|
1467
|
+
text.sub(/\A\s*(?:#{Regexp.escape("#")}{4,}|[-=~`^"']{3,})\s*\n+/, "")
|
|
1111
1468
|
end
|
|
1112
1469
|
|
|
1113
1470
|
def compare_versions(left, right)
|
|
@@ -1154,6 +1511,34 @@ module Gemstar
|
|
|
1154
1511
|
package_version = source[:package_version].to_s
|
|
1155
1512
|
registry_url = source[:registry_url].to_s
|
|
1156
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
|
+
|
|
1157
1542
|
package_name = package_state[:name].to_s if package_name.empty? && source[:type] == :npm
|
|
1158
1543
|
package_version = (package_state[:new_version] || package_state[:old_version]).to_s if package_version.empty? && source[:type] == :npm
|
|
1159
1544
|
registry_url = "https://www.npmjs.com/package/#{package_name}" if registry_url.empty? && !package_name.empty?
|
|
@@ -1201,6 +1586,11 @@ module Gemstar
|
|
|
1201
1586
|
|
|
1202
1587
|
def metadata_adapter_for(package_state)
|
|
1203
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
|
|
1204
1594
|
return nil unless package_state[:package_scope] == "js"
|
|
1205
1595
|
|
|
1206
1596
|
provider_gem = package_state.dig(:source, :provider_gem)
|
|
@@ -1235,7 +1625,15 @@ module Gemstar
|
|
|
1235
1625
|
when :home
|
|
1236
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>'
|
|
1237
1627
|
when :rubygems
|
|
1238
|
-
'<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>'
|
|
1239
1637
|
when :info
|
|
1240
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>'
|
|
1241
1639
|
else
|
|
@@ -1251,6 +1649,10 @@ module Gemstar
|
|
|
1251
1649
|
end
|
|
1252
1650
|
end
|
|
1253
1651
|
|
|
1652
|
+
def unchanged_worktree_package?
|
|
1653
|
+
@selected_to_revision_id == "worktree" && @selected_gem && @selected_gem[:status] == :unchanged
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1254
1656
|
def range_label(gem_state)
|
|
1255
1657
|
old_version = gem_state[:old_version]
|
|
1256
1658
|
new_version = gem_state[:new_version]
|
|
@@ -1262,7 +1664,7 @@ module Gemstar
|
|
|
1262
1664
|
end
|
|
1263
1665
|
|
|
1264
1666
|
def previous_section_version(versions, current_version)
|
|
1265
|
-
ordered_versions = versions.
|
|
1667
|
+
ordered_versions = versions.sort { |left, right| compare_versions(right, left) }
|
|
1266
1668
|
current_index = ordered_versions.index(current_version)
|
|
1267
1669
|
return nil if current_index.nil?
|
|
1268
1670
|
|
|
@@ -1383,18 +1785,19 @@ module Gemstar
|
|
|
1383
1785
|
html
|
|
1384
1786
|
end
|
|
1385
1787
|
|
|
1386
|
-
def detail_query(project:, from:, to:, filter:, scope:, package:)
|
|
1387
|
-
"/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)}"
|
|
1388
1790
|
end
|
|
1389
1791
|
|
|
1390
|
-
def project_query(project:, from:, to:, filter:, scope:, package:)
|
|
1792
|
+
def project_query(project:, from:, to:, filter:, scope:, package:, package_id: nil)
|
|
1391
1793
|
params = {
|
|
1392
1794
|
project: project,
|
|
1393
1795
|
from: from,
|
|
1394
1796
|
to: to,
|
|
1395
1797
|
filter: filter,
|
|
1396
1798
|
scope: scope,
|
|
1397
|
-
package: package
|
|
1799
|
+
package: package,
|
|
1800
|
+
package_id: package_id
|
|
1398
1801
|
}.compact
|
|
1399
1802
|
|
|
1400
1803
|
"/?#{URI.encode_www_form(params)}"
|