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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
require_relative "command"
|
|
2
|
+
require_relative "../project"
|
|
3
|
+
require "rack/mock"
|
|
2
4
|
require "socket"
|
|
3
5
|
require "shellwords"
|
|
6
|
+
require "uri"
|
|
4
7
|
require "rbconfig"
|
|
5
8
|
|
|
6
9
|
module Gemstar
|
|
@@ -31,6 +34,8 @@ module Gemstar
|
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
def run
|
|
37
|
+
projects = load_projects
|
|
38
|
+
|
|
34
39
|
restart_with_rerun if reload_requested?
|
|
35
40
|
|
|
36
41
|
require "rackup"
|
|
@@ -42,23 +47,28 @@ module Gemstar
|
|
|
42
47
|
Gemstar::Config.ensure_home_directory!
|
|
43
48
|
@port = resolve_port
|
|
44
49
|
|
|
45
|
-
projects = load_projects
|
|
46
50
|
log_loaded_projects(projects)
|
|
47
51
|
cache_warmer = build_cache_warmer
|
|
48
|
-
|
|
52
|
+
web_app = Gemstar::Web::App.build(projects: projects, config_home: Gemstar::Config.home_directory, cache_warmer: cache_warmer)
|
|
53
|
+
cache_warmer.detail_cache_fetcher = build_detail_cache_fetcher(web_app)
|
|
54
|
+
app = web_app
|
|
49
55
|
app = Gemstar::RequestLogger.new(app, io: $stderr) if debug_request_logging?
|
|
50
56
|
|
|
51
57
|
puts "Gemstar server listening on http://#{bind}:#{port}"
|
|
52
58
|
puts "Config home: #{Gemstar::Config.home_directory}"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
begin
|
|
60
|
+
Rackup::Server.start(
|
|
61
|
+
app: app,
|
|
62
|
+
server: "webrick",
|
|
63
|
+
Host: bind,
|
|
64
|
+
Port: port,
|
|
65
|
+
AccessLog: [],
|
|
66
|
+
Logger: Gemstar::WEBrickLogger.new($stderr, WEBrick::BasicLog::INFO),
|
|
67
|
+
StartCallback: server_start_callback(projects, cache_warmer)
|
|
68
|
+
)
|
|
69
|
+
ensure
|
|
70
|
+
stop_background_cache_refresh(cache_warmer)
|
|
71
|
+
end
|
|
62
72
|
end
|
|
63
73
|
|
|
64
74
|
private
|
|
@@ -143,6 +153,9 @@ module Gemstar
|
|
|
143
153
|
|
|
144
154
|
def load_projects
|
|
145
155
|
project_inputs.map { |input| Gemstar::Project.from_cli_argument(input) }
|
|
156
|
+
rescue Gemstar::Project::UnsupportedProjectError => e
|
|
157
|
+
warn e.message
|
|
158
|
+
exit 1
|
|
146
159
|
end
|
|
147
160
|
|
|
148
161
|
def log_loaded_projects(projects)
|
|
@@ -183,8 +196,21 @@ module Gemstar
|
|
|
183
196
|
end
|
|
184
197
|
|
|
185
198
|
def start_background_cache_refresh(projects, cache_warmer)
|
|
186
|
-
package_states = projects.flat_map do |project|
|
|
187
|
-
|
|
199
|
+
package_states = projects.flat_map.with_index do |project, project_index|
|
|
200
|
+
from_revision_id = project.default_from_revision_id
|
|
201
|
+
to_revision_id = "worktree"
|
|
202
|
+
states = project.gem_states(from_revision_id: from_revision_id, to_revision_id: to_revision_id)
|
|
203
|
+
detail_cache_contexts = detail_cache_contexts_for(
|
|
204
|
+
project: project,
|
|
205
|
+
project_index: project_index,
|
|
206
|
+
from_revision_id: from_revision_id,
|
|
207
|
+
to_revision_id: to_revision_id,
|
|
208
|
+
package_states: states
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
states.map do |state|
|
|
212
|
+
state.merge(detail_cache_contexts: detail_cache_contexts_for_package(state, detail_cache_contexts))
|
|
213
|
+
end
|
|
188
214
|
end
|
|
189
215
|
|
|
190
216
|
return nil if package_states.empty?
|
|
@@ -192,16 +218,75 @@ module Gemstar
|
|
|
192
218
|
cache_warmer.enqueue_many(package_states)
|
|
193
219
|
end
|
|
194
220
|
|
|
221
|
+
def detail_cache_contexts_for(project:, project_index:, from_revision_id:, to_revision_id:, package_states:)
|
|
222
|
+
default_filter = package_states.any? { |state| state[:status] != :unchanged } ? "updated" : "all"
|
|
223
|
+
default_scope = project.package_scope_options.map { |option| option[:id] } == ["gems"] ? "gems" : "all"
|
|
224
|
+
|
|
225
|
+
[
|
|
226
|
+
{
|
|
227
|
+
project: project_index,
|
|
228
|
+
from: from_revision_id,
|
|
229
|
+
to: to_revision_id,
|
|
230
|
+
filter: default_filter,
|
|
231
|
+
scope: default_scope
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
project: project_index,
|
|
235
|
+
from: from_revision_id,
|
|
236
|
+
to: to_revision_id,
|
|
237
|
+
filter: "all",
|
|
238
|
+
scope: default_scope
|
|
239
|
+
}
|
|
240
|
+
].uniq
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def detail_cache_contexts_for_package(package_state, base_contexts)
|
|
244
|
+
package_scope = package_state[:package_scope]
|
|
245
|
+
|
|
246
|
+
base_contexts.flat_map do |context|
|
|
247
|
+
[
|
|
248
|
+
context,
|
|
249
|
+
context.merge(scope: package_scope)
|
|
250
|
+
]
|
|
251
|
+
end.uniq
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def build_detail_cache_fetcher(app)
|
|
255
|
+
lambda do |package_state, context|
|
|
256
|
+
params = context.merge(package: package_state[:name])
|
|
257
|
+
env = Rack::MockRequest.env_for(
|
|
258
|
+
"/detail?#{URI.encode_www_form(params)}",
|
|
259
|
+
"REQUEST_METHOD" => "GET",
|
|
260
|
+
"gemstar.detail_cache_warm" => true
|
|
261
|
+
)
|
|
262
|
+
response = app.call(env)
|
|
263
|
+
body = response[2]
|
|
264
|
+
body.close if body.respond_to?(:close)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
195
268
|
def server_start_callback(projects, cache_warmer)
|
|
196
269
|
proc do
|
|
197
|
-
Thread.new do
|
|
270
|
+
@server_start_thread = Thread.new do
|
|
271
|
+
Thread.current.name = "gemstar-server-start" if Thread.current.respond_to?(:name=)
|
|
198
272
|
sleep 0.15
|
|
199
273
|
start_background_cache_refresh(projects, cache_warmer)
|
|
200
274
|
launch_browser
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
warn "Background cache refresh failed: #{e.class}: #{e.message}"
|
|
201
277
|
end
|
|
202
278
|
end
|
|
203
279
|
end
|
|
204
280
|
|
|
281
|
+
def stop_background_cache_refresh(cache_warmer)
|
|
282
|
+
cache_warmer.shutdown if cache_warmer.respond_to?(:shutdown)
|
|
283
|
+
|
|
284
|
+
return unless @server_start_thread&.alive?
|
|
285
|
+
|
|
286
|
+
@server_start_thread.kill
|
|
287
|
+
@server_start_thread.join(0.5)
|
|
288
|
+
end
|
|
289
|
+
|
|
205
290
|
def launch_browser
|
|
206
291
|
return unless open_browser
|
|
207
292
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
require_relative "version"
|
|
8
|
+
|
|
9
|
+
module Gemstar
|
|
10
|
+
class CratesIOMetadata
|
|
11
|
+
USER_AGENT = "gemstar/#{Gemstar::VERSION} (+https://github.com/FDj/gemstar)"
|
|
12
|
+
|
|
13
|
+
def initialize(crate_name)
|
|
14
|
+
@crate_name = crate_name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :crate_name
|
|
18
|
+
|
|
19
|
+
alias_method :gem_name, :crate_name
|
|
20
|
+
|
|
21
|
+
def cache_key
|
|
22
|
+
"crates-io-#{crate_name}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def meta(cache_only: false, force_refresh: false)
|
|
26
|
+
return @meta if !cache_only && defined?(@meta)
|
|
27
|
+
|
|
28
|
+
parsed = raw_meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
29
|
+
normalized = normalize_meta(parsed)
|
|
30
|
+
@meta = normalized unless cache_only
|
|
31
|
+
normalized
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def repo_uri(cache_only: false, force_refresh: false)
|
|
35
|
+
resolved_meta = meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
36
|
+
return nil unless resolved_meta
|
|
37
|
+
return @repo_uri if !cache_only && defined?(@repo_uri)
|
|
38
|
+
|
|
39
|
+
uri = normalize_repo_uri(resolved_meta["source_code_uri"])
|
|
40
|
+
@repo_uri = uri unless cache_only
|
|
41
|
+
uri
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
45
|
+
requested_versions = Array(versions).compact
|
|
46
|
+
changelog = Gemstar::ChangeLog.new(self)
|
|
47
|
+
if requested_versions.empty?
|
|
48
|
+
changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
49
|
+
else
|
|
50
|
+
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def registry_release_dates(cache_only: false, force_refresh: false)
|
|
55
|
+
parsed = raw_meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
56
|
+
Array(parsed&.dig("versions")).each_with_object({}) do |version, dates|
|
|
57
|
+
number = version["num"]
|
|
58
|
+
created_at = version["created_at"]
|
|
59
|
+
next if number.to_s.empty? || created_at.to_s.empty?
|
|
60
|
+
|
|
61
|
+
dates[number] = Time.parse(created_at).utc.strftime("%b %-d, %Y")
|
|
62
|
+
end
|
|
63
|
+
rescue JSON::ParserError, ArgumentError
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def warm_cache(versions: nil)
|
|
68
|
+
meta
|
|
69
|
+
repo_uri
|
|
70
|
+
changelog_sections(versions: versions)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def discover_github_tag_sections?
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def github_tag_candidates(version)
|
|
78
|
+
raw = version.to_s
|
|
79
|
+
[
|
|
80
|
+
raw,
|
|
81
|
+
(raw.start_with?("v") ? raw : "v#{raw}"),
|
|
82
|
+
"#{crate_name}-#{raw}",
|
|
83
|
+
"#{crate_name}-v#{raw}",
|
|
84
|
+
"#{crate_name}@#{raw}"
|
|
85
|
+
].uniq
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def github_tag_matches?(tag_name)
|
|
89
|
+
decoded = URI.decode_www_form_component(tag_name.to_s.split("?").first.to_s)
|
|
90
|
+
return true if decoded.match?(/\Av?\d/)
|
|
91
|
+
|
|
92
|
+
decoded.match?(/\A#{Regexp.escape(crate_name)}(?:-|@)v?\d/i)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def raw_meta(cache_only: false, force_refresh: false)
|
|
98
|
+
json = if cache_only
|
|
99
|
+
Cache.peek(cache_key)
|
|
100
|
+
else
|
|
101
|
+
url = "https://crates.io/api/v1/crates/#{URI.encode_www_form_component(crate_name)}"
|
|
102
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
103
|
+
options = {read_timeout: 8}
|
|
104
|
+
options["User-Agent"] = USER_AGENT
|
|
105
|
+
URI.parse(url).open(options).read
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
JSON.parse(json) if json
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalize_meta(parsed)
|
|
113
|
+
return nil unless parsed.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
crate = parsed["crate"] || {}
|
|
116
|
+
name = crate["name"] || crate["id"] || crate_name
|
|
117
|
+
{
|
|
118
|
+
"name" => name,
|
|
119
|
+
"version" => crate["max_version"] || crate["newest_version"],
|
|
120
|
+
"info" => crate["description"],
|
|
121
|
+
"homepage_uri" => crate["homepage"],
|
|
122
|
+
"source_code_uri" => crate["repository"],
|
|
123
|
+
"project_uri" => "https://crates.io/crates/#{name}",
|
|
124
|
+
"documentation_uri" => crate["documentation"]
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def normalize_repo_uri(uri)
|
|
129
|
+
value = uri.to_s
|
|
130
|
+
return "" if value.empty?
|
|
131
|
+
|
|
132
|
+
value = value.sub(/\Agit\+/, "")
|
|
133
|
+
value = value.sub(/\Agit:\/\//, "https://")
|
|
134
|
+
value = value.sub(/\Ahttp:\/\//, "https://")
|
|
135
|
+
value = value.gsub(/\.git\z/, "")
|
|
136
|
+
value = value[%r{\Ahttps?://github\.com/[^/]+/[^/]+}] || value if value.include?("github.com")
|
|
137
|
+
value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -5,5 +5,15 @@
|
|
|
5
5
|
"paths": ["CHANGELOG.md"],
|
|
6
6
|
"branches": [""]
|
|
7
7
|
}
|
|
8
|
+
},
|
|
9
|
+
"thruster": {
|
|
10
|
+
"changelog": {
|
|
11
|
+
"raw_base": "https://raw.githubusercontent.com/basecamp/thruster/v{version}",
|
|
12
|
+
"paths": ["CHANGELOG.md"],
|
|
13
|
+
"branches": [""],
|
|
14
|
+
"version_overrides": {
|
|
15
|
+
"0.1.22@2026-07-16": "0.1.23"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
8
18
|
}
|
|
9
19
|
}
|
data/lib/gemstar/npm_metadata.rb
CHANGED
|
@@ -64,13 +64,13 @@ module Gemstar
|
|
|
64
64
|
repo
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def changelog_sections(versions: nil, cache_only: false, force_refresh: false)
|
|
67
|
+
def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
68
68
|
requested_versions = Array(versions).compact
|
|
69
69
|
changelog = Gemstar::ChangeLog.new(self)
|
|
70
70
|
if requested_versions.empty?
|
|
71
71
|
changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
72
72
|
else
|
|
73
|
-
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh)
|
|
73
|
+
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
data/lib/gemstar/outputs/html.rb
CHANGED