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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "command"
|
|
2
|
+
require_relative "../project"
|
|
2
3
|
require "rack/mock"
|
|
3
4
|
require "socket"
|
|
4
5
|
require "shellwords"
|
|
@@ -33,6 +34,8 @@ module Gemstar
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def run
|
|
37
|
+
projects = load_projects
|
|
38
|
+
|
|
36
39
|
restart_with_rerun if reload_requested?
|
|
37
40
|
|
|
38
41
|
require "rackup"
|
|
@@ -44,7 +47,6 @@ module Gemstar
|
|
|
44
47
|
Gemstar::Config.ensure_home_directory!
|
|
45
48
|
@port = resolve_port
|
|
46
49
|
|
|
47
|
-
projects = load_projects
|
|
48
50
|
log_loaded_projects(projects)
|
|
49
51
|
cache_warmer = build_cache_warmer
|
|
50
52
|
web_app = Gemstar::Web::App.build(projects: projects, config_home: Gemstar::Config.home_directory, cache_warmer: cache_warmer)
|
|
@@ -54,15 +56,19 @@ module Gemstar
|
|
|
54
56
|
|
|
55
57
|
puts "Gemstar server listening on http://#{bind}:#{port}"
|
|
56
58
|
puts "Config home: #{Gemstar::Config.home_directory}"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
72
|
end
|
|
67
73
|
|
|
68
74
|
private
|
|
@@ -147,6 +153,9 @@ module Gemstar
|
|
|
147
153
|
|
|
148
154
|
def load_projects
|
|
149
155
|
project_inputs.map { |input| Gemstar::Project.from_cli_argument(input) }
|
|
156
|
+
rescue Gemstar::Project::UnsupportedProjectError => e
|
|
157
|
+
warn e.message
|
|
158
|
+
exit 1
|
|
150
159
|
end
|
|
151
160
|
|
|
152
161
|
def log_loaded_projects(projects)
|
|
@@ -258,14 +267,26 @@ module Gemstar
|
|
|
258
267
|
|
|
259
268
|
def server_start_callback(projects, cache_warmer)
|
|
260
269
|
proc do
|
|
261
|
-
Thread.new do
|
|
270
|
+
@server_start_thread = Thread.new do
|
|
271
|
+
Thread.current.name = "gemstar-server-start" if Thread.current.respond_to?(:name=)
|
|
262
272
|
sleep 0.15
|
|
263
273
|
start_background_cache_refresh(projects, cache_warmer)
|
|
264
274
|
launch_browser
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
warn "Background cache refresh failed: #{e.class}: #{e.message}"
|
|
265
277
|
end
|
|
266
278
|
end
|
|
267
279
|
end
|
|
268
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
|
+
|
|
269
290
|
def launch_browser
|
|
270
291
|
return unless open_browser
|
|
271
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/outputs/html.rb
CHANGED
data/lib/gemstar/project.rb
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
require "time"
|
|
2
|
+
require_relative "cargo_lock_file"
|
|
3
|
+
require_relative "uv_lock_file"
|
|
2
4
|
|
|
3
5
|
module Gemstar
|
|
4
6
|
class Project
|
|
5
7
|
REVISION_HISTORY_LIMIT = 100
|
|
8
|
+
SUPPORTED_PROJECT_FILES = [
|
|
9
|
+
"Gemfile",
|
|
10
|
+
"Gemfile.lock",
|
|
11
|
+
"config/importmap.rb",
|
|
12
|
+
"package.json",
|
|
13
|
+
"package-lock.json",
|
|
14
|
+
"uv.lock",
|
|
15
|
+
"Cargo.toml",
|
|
16
|
+
"Cargo.lock"
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
class UnsupportedProjectError < ArgumentError
|
|
20
|
+
def initialize(directory)
|
|
21
|
+
super(<<~MESSAGE.chomp)
|
|
22
|
+
Directory #{File.expand_path(directory)} does not contain a recognized project file.
|
|
23
|
+
Only #{Project.supported_project_files_sentence} are supported.
|
|
24
|
+
MESSAGE
|
|
25
|
+
end
|
|
26
|
+
end
|
|
6
27
|
|
|
7
28
|
attr_reader :directory
|
|
8
29
|
attr_reader :gemfile_path
|
|
@@ -10,6 +31,9 @@ module Gemstar
|
|
|
10
31
|
attr_reader :importmap_path
|
|
11
32
|
attr_reader :package_json_path
|
|
12
33
|
attr_reader :package_lock_path
|
|
34
|
+
attr_reader :uv_lock_path
|
|
35
|
+
attr_reader :cargo_toml_path
|
|
36
|
+
attr_reader :cargo_lock_path
|
|
13
37
|
attr_reader :name
|
|
14
38
|
|
|
15
39
|
def self.from_cli_argument(input)
|
|
@@ -20,26 +44,36 @@ module Gemstar
|
|
|
20
44
|
basename = File.basename(expanded_input)
|
|
21
45
|
directory =
|
|
22
46
|
case basename
|
|
23
|
-
when "Gemfile", "package.json", "package-lock.json"
|
|
47
|
+
when "Gemfile", "Gemfile.lock", "package.json", "package-lock.json", "uv.lock", "Cargo.toml", "Cargo.lock"
|
|
24
48
|
File.dirname(expanded_input)
|
|
25
49
|
when "importmap.rb"
|
|
26
|
-
File.dirname(
|
|
27
|
-
else
|
|
28
|
-
nil
|
|
50
|
+
File.dirname(expanded_input, 2)
|
|
29
51
|
end
|
|
30
52
|
end
|
|
31
53
|
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
unless directory
|
|
55
|
+
unsupported_directory = File.directory?(expanded_input) ? expanded_input : File.dirname(expanded_input)
|
|
56
|
+
raise UnsupportedProjectError, unsupported_directory
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
raise UnsupportedProjectError, directory unless supported_project_directory?(directory)
|
|
34
60
|
|
|
35
61
|
new(directory: directory)
|
|
36
62
|
end
|
|
37
63
|
|
|
38
64
|
def self.supported_project_directory?(directory)
|
|
39
65
|
File.file?(File.join(directory, "Gemfile")) ||
|
|
66
|
+
File.file?(File.join(directory, "Gemfile.lock")) ||
|
|
40
67
|
File.file?(File.join(directory, "config", "importmap.rb")) ||
|
|
41
68
|
File.file?(File.join(directory, "package.json")) ||
|
|
42
|
-
File.file?(File.join(directory, "package-lock.json"))
|
|
69
|
+
File.file?(File.join(directory, "package-lock.json")) ||
|
|
70
|
+
File.file?(File.join(directory, "uv.lock")) ||
|
|
71
|
+
File.file?(File.join(directory, "Cargo.toml")) ||
|
|
72
|
+
File.file?(File.join(directory, "Cargo.lock"))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.supported_project_files_sentence
|
|
76
|
+
"#{SUPPORTED_PROJECT_FILES[0...-1].join(", ")}, and #{SUPPORTED_PROJECT_FILES[-1]}"
|
|
43
77
|
end
|
|
44
78
|
|
|
45
79
|
def initialize(directory:)
|
|
@@ -49,10 +83,15 @@ module Gemstar
|
|
|
49
83
|
@importmap_path = File.join(@directory, "config", "importmap.rb")
|
|
50
84
|
@package_json_path = File.join(@directory, "package.json")
|
|
51
85
|
@package_lock_path = File.join(@directory, "package-lock.json")
|
|
86
|
+
@uv_lock_path = File.join(@directory, "uv.lock")
|
|
87
|
+
@cargo_toml_path = File.join(@directory, "Cargo.toml")
|
|
88
|
+
@cargo_lock_path = File.join(@directory, "Cargo.lock")
|
|
52
89
|
@name = File.basename(@directory)
|
|
53
90
|
@lockfile_cache = {}
|
|
54
91
|
@importmap_cache = {}
|
|
55
92
|
@package_lock_cache = {}
|
|
93
|
+
@uv_lock_cache = {}
|
|
94
|
+
@cargo_lock_cache = {}
|
|
56
95
|
@gem_states_cache = {}
|
|
57
96
|
@gem_added_on_cache = {}
|
|
58
97
|
@history_cache = {}
|
|
@@ -104,6 +143,46 @@ module Gemstar
|
|
|
104
143
|
@current_package_lock ||= Gemstar::PackageLockFile.new(path: package_lock_path)
|
|
105
144
|
end
|
|
106
145
|
|
|
146
|
+
def uv_lock?
|
|
147
|
+
File.file?(uv_lock_path)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def current_uv_lock
|
|
151
|
+
return nil unless uv_lock?
|
|
152
|
+
|
|
153
|
+
@current_uv_lock ||= Gemstar::UvLockFile.new(path: uv_lock_path)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def cargo_toml?
|
|
157
|
+
File.file?(cargo_toml_path)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def cargo_lock?
|
|
161
|
+
File.file?(cargo_lock_path)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def current_cargo_lock
|
|
165
|
+
return nil unless cargo_lock?
|
|
166
|
+
|
|
167
|
+
@current_cargo_lock ||= Gemstar::CargoLockFile.new(path: cargo_lock_path)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def clear_cache!
|
|
171
|
+
@current_lockfile = nil
|
|
172
|
+
@current_importmap = nil
|
|
173
|
+
@current_package_lock = nil
|
|
174
|
+
@current_uv_lock = nil
|
|
175
|
+
@current_cargo_lock = nil
|
|
176
|
+
@lockfile_cache.clear
|
|
177
|
+
@importmap_cache.clear
|
|
178
|
+
@package_lock_cache.clear
|
|
179
|
+
@uv_lock_cache.clear
|
|
180
|
+
@cargo_lock_cache.clear
|
|
181
|
+
@gem_states_cache.clear
|
|
182
|
+
@gem_added_on_cache.clear
|
|
183
|
+
@history_cache.clear
|
|
184
|
+
end
|
|
185
|
+
|
|
107
186
|
def revision_history(limit: REVISION_HISTORY_LIMIT)
|
|
108
187
|
history_for_paths(tracked_git_paths, limit: limit)
|
|
109
188
|
end
|
|
@@ -133,7 +212,7 @@ module Gemstar
|
|
|
133
212
|
end
|
|
134
213
|
|
|
135
214
|
def revision_options(limit: REVISION_HISTORY_LIMIT)
|
|
136
|
-
[{ id: "worktree", label: "Worktree", description: "Current
|
|
215
|
+
[{ id: "worktree", label: "Worktree", description: "Current project files in the working tree" }] +
|
|
137
216
|
revision_history(limit: limit).map do |revision|
|
|
138
217
|
{
|
|
139
218
|
id: revision[:id],
|
|
@@ -147,6 +226,8 @@ module Gemstar
|
|
|
147
226
|
scopes = []
|
|
148
227
|
scopes << :gems if gemfile? || lockfile?
|
|
149
228
|
scopes << :js if importmap? || package_lock? || package_json?
|
|
229
|
+
scopes << :python if uv_lock?
|
|
230
|
+
scopes << :cargo if cargo_toml? || cargo_lock?
|
|
150
231
|
scopes
|
|
151
232
|
end
|
|
152
233
|
|
|
@@ -163,6 +244,16 @@ module Gemstar
|
|
|
163
244
|
package_scopes == [:gems] ? "Gems" : "Packages"
|
|
164
245
|
end
|
|
165
246
|
|
|
247
|
+
def package_item_label
|
|
248
|
+
case package_scopes
|
|
249
|
+
when [:gems] then "gem"
|
|
250
|
+
when [:js] then "JavaScript package"
|
|
251
|
+
when [:python] then "Python package"
|
|
252
|
+
when [:cargo] then "Rust crate"
|
|
253
|
+
else "package"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
166
257
|
def lockfile_for_revision(revision_id)
|
|
167
258
|
cache_key = revision_id || "worktree"
|
|
168
259
|
return @lockfile_cache[cache_key] if @lockfile_cache.key?(cache_key)
|
|
@@ -208,6 +299,36 @@ module Gemstar
|
|
|
208
299
|
@package_lock_cache[cache_key] = Gemstar::PackageLockFile.new(content: content)
|
|
209
300
|
end
|
|
210
301
|
|
|
302
|
+
def uv_lock_for_revision(revision_id)
|
|
303
|
+
cache_key = revision_id || "worktree"
|
|
304
|
+
return @uv_lock_cache[cache_key] if @uv_lock_cache.key?(cache_key)
|
|
305
|
+
return @uv_lock_cache[cache_key] = current_uv_lock if revision_id.nil? || revision_id == "worktree"
|
|
306
|
+
return nil unless uv_lock?
|
|
307
|
+
|
|
308
|
+
relative_uv_lock_path = git_repo.relative_path(uv_lock_path)
|
|
309
|
+
return nil if relative_uv_lock_path.nil?
|
|
310
|
+
|
|
311
|
+
content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_uv_lock_path}"])
|
|
312
|
+
return nil if content.nil? || content.empty?
|
|
313
|
+
|
|
314
|
+
@uv_lock_cache[cache_key] = Gemstar::UvLockFile.new(content: content)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def cargo_lock_for_revision(revision_id)
|
|
318
|
+
cache_key = revision_id || "worktree"
|
|
319
|
+
return @cargo_lock_cache[cache_key] if @cargo_lock_cache.key?(cache_key)
|
|
320
|
+
return @cargo_lock_cache[cache_key] = current_cargo_lock if revision_id.nil? || revision_id == "worktree"
|
|
321
|
+
return nil unless cargo_lock?
|
|
322
|
+
|
|
323
|
+
relative_cargo_lock_path = git_repo.relative_path(cargo_lock_path)
|
|
324
|
+
return nil if relative_cargo_lock_path.nil?
|
|
325
|
+
|
|
326
|
+
content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_cargo_lock_path}"])
|
|
327
|
+
return nil if content.nil? || content.empty?
|
|
328
|
+
|
|
329
|
+
@cargo_lock_cache[cache_key] = Gemstar::CargoLockFile.new(content: content)
|
|
330
|
+
end
|
|
331
|
+
|
|
211
332
|
def gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree")
|
|
212
333
|
cache_key = [from_revision_id, to_revision_id]
|
|
213
334
|
return @gem_states_cache[cache_key] if @gem_states_cache.key?(cache_key)
|
|
@@ -298,11 +419,67 @@ module Gemstar
|
|
|
298
419
|
}
|
|
299
420
|
end
|
|
300
421
|
|
|
301
|
-
|
|
422
|
+
from_uv_lock = uv_lock_for_revision(from_revision_id)
|
|
423
|
+
to_uv_lock = uv_lock_for_revision(to_revision_id)
|
|
424
|
+
from_python_specs = from_uv_lock&.specs || {}
|
|
425
|
+
to_python_specs = to_uv_lock&.specs || {}
|
|
426
|
+
python_states = (from_python_specs.keys | to_python_specs.keys).map do |package_name|
|
|
427
|
+
old_version = from_python_specs[package_name]
|
|
428
|
+
new_version = to_python_specs[package_name]
|
|
429
|
+
effective_uv_lock = new_version ? to_uv_lock : from_uv_lock
|
|
430
|
+
|
|
431
|
+
{
|
|
432
|
+
name: package_name,
|
|
433
|
+
package_scope: "python",
|
|
434
|
+
package_type_label: "Python",
|
|
435
|
+
package_source_file: :uv_lock,
|
|
436
|
+
old_version: old_version,
|
|
437
|
+
new_version: new_version,
|
|
438
|
+
status: gem_status(old_version, new_version),
|
|
439
|
+
version_label: version_label(old_version, new_version),
|
|
440
|
+
platform: nil,
|
|
441
|
+
source: effective_uv_lock&.source_for(package_name),
|
|
442
|
+
bundle_origins: [],
|
|
443
|
+
bundle_origin_labels: []
|
|
444
|
+
}
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
from_cargo_lock = cargo_lock_for_revision(from_revision_id)
|
|
448
|
+
to_cargo_lock = cargo_lock_for_revision(to_revision_id)
|
|
449
|
+
from_cargo_specs = from_cargo_lock&.specs || {}
|
|
450
|
+
to_cargo_specs = to_cargo_lock&.specs || {}
|
|
451
|
+
cargo_states = (from_cargo_specs.keys | to_cargo_specs.keys).map do |package_key|
|
|
452
|
+
old_version = from_cargo_specs[package_key]
|
|
453
|
+
new_version = to_cargo_specs[package_key]
|
|
454
|
+
effective_cargo_lock = new_version ? to_cargo_lock : from_cargo_lock
|
|
455
|
+
source = effective_cargo_lock&.source_for(package_key)
|
|
456
|
+
package_name = source&.dig(:package_name) || package_key
|
|
457
|
+
display_name = package_key == package_name ? package_name : "#{package_name} @ #{new_version || old_version}"
|
|
458
|
+
|
|
459
|
+
{
|
|
460
|
+
name: display_name,
|
|
461
|
+
metadata_package_name: package_name,
|
|
462
|
+
package_lock_key: package_key,
|
|
463
|
+
package_scope: "cargo",
|
|
464
|
+
package_type_label: "Crate",
|
|
465
|
+
package_source_file: :cargo_lock,
|
|
466
|
+
old_version: old_version,
|
|
467
|
+
new_version: new_version,
|
|
468
|
+
status: gem_status(old_version, new_version),
|
|
469
|
+
version_label: version_label(old_version, new_version),
|
|
470
|
+
platform: nil,
|
|
471
|
+
source: source,
|
|
472
|
+
bundle_origins: [],
|
|
473
|
+
bundle_origin_labels: []
|
|
474
|
+
}
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
@gem_states_cache[cache_key] = (gem_states + js_states + npm_states + python_states + cargo_states).sort_by { |gem| [gem[:name], gem[:package_scope], gem[:package_source_file].to_s] }
|
|
302
478
|
end
|
|
303
479
|
|
|
304
|
-
def package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil)
|
|
305
|
-
|
|
480
|
+
def package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil, package_key: nil)
|
|
481
|
+
lookup_name = package_key || package_name
|
|
482
|
+
cache_key = [lookup_name, package_scope, source_file, revision_id]
|
|
306
483
|
return @gem_added_on_cache[cache_key] if @gem_added_on_cache.key?(cache_key)
|
|
307
484
|
|
|
308
485
|
tracked_file, reader =
|
|
@@ -310,22 +487,30 @@ module Gemstar
|
|
|
310
487
|
[importmap_path, method(:importmap_for_revision)]
|
|
311
488
|
elsif source_file == :package_lock
|
|
312
489
|
[package_lock_path, method(:package_lock_for_revision)]
|
|
490
|
+
elsif source_file == :uv_lock
|
|
491
|
+
[uv_lock_path, method(:uv_lock_for_revision)]
|
|
492
|
+
elsif source_file == :cargo_lock
|
|
493
|
+
[cargo_lock_path, method(:cargo_lock_for_revision)]
|
|
313
494
|
elsif package_scope == "js"
|
|
314
495
|
[importmap_path, method(:importmap_for_revision)]
|
|
496
|
+
elsif package_scope == "python"
|
|
497
|
+
[uv_lock_path, method(:uv_lock_for_revision)]
|
|
498
|
+
elsif package_scope == "cargo"
|
|
499
|
+
[cargo_lock_path, method(:cargo_lock_for_revision)]
|
|
315
500
|
else
|
|
316
501
|
[lockfile_path, method(:lockfile_for_revision)]
|
|
317
502
|
end
|
|
318
503
|
return @gem_added_on_cache[cache_key] = nil unless File.file?(tracked_file)
|
|
319
504
|
|
|
320
505
|
target_snapshot = reader.call(revision_id)
|
|
321
|
-
return @gem_added_on_cache[cache_key] = nil unless target_snapshot&.specs&.key?(
|
|
506
|
+
return @gem_added_on_cache[cache_key] = nil unless target_snapshot&.specs&.key?(lookup_name)
|
|
322
507
|
|
|
323
508
|
relative_path = git_repo.relative_path(tracked_file)
|
|
324
509
|
return @gem_added_on_cache[cache_key] = nil if relative_path.nil?
|
|
325
510
|
|
|
326
511
|
first_seen_revision = history_for_paths([relative_path], limit: nil, reverse: true).find do |revision|
|
|
327
512
|
snapshot = reader.call(revision[:id])
|
|
328
|
-
snapshot&.specs&.key?(
|
|
513
|
+
snapshot&.specs&.key?(lookup_name)
|
|
329
514
|
end
|
|
330
515
|
|
|
331
516
|
return @gem_added_on_cache[cache_key] = worktree_added_on_info(tracked_file) if first_seen_revision.nil? && revision_id == "worktree"
|
|
@@ -346,14 +531,20 @@ module Gemstar
|
|
|
346
531
|
current_specs = current_lockfile&.specs || {}
|
|
347
532
|
current_importmap_specs = current_importmap&.specs || {}
|
|
348
533
|
current_package_lock_specs = current_package_lock&.specs || {}
|
|
534
|
+
current_uv_lock_specs = current_uv_lock&.specs || {}
|
|
535
|
+
current_cargo_lock_specs = current_cargo_lock&.specs || {}
|
|
349
536
|
|
|
350
537
|
revision_history(limit: REVISION_HISTORY_LIMIT).find do |revision|
|
|
351
538
|
revision_lockfile = lockfile_for_revision(revision[:id])
|
|
352
539
|
revision_importmap = importmap_for_revision(revision[:id])
|
|
353
540
|
revision_package_lock = package_lock_for_revision(revision[:id])
|
|
541
|
+
revision_uv_lock = uv_lock_for_revision(revision[:id])
|
|
542
|
+
revision_cargo_lock = cargo_lock_for_revision(revision[:id])
|
|
354
543
|
(revision_lockfile && revision_lockfile.specs != current_specs) ||
|
|
355
544
|
(revision_importmap && revision_importmap.specs != current_importmap_specs) ||
|
|
356
|
-
(revision_package_lock && revision_package_lock.specs != current_package_lock_specs)
|
|
545
|
+
(revision_package_lock && revision_package_lock.specs != current_package_lock_specs) ||
|
|
546
|
+
(revision_uv_lock && revision_uv_lock.specs != current_uv_lock_specs) ||
|
|
547
|
+
(revision_cargo_lock && revision_cargo_lock.specs != current_cargo_lock_specs)
|
|
357
548
|
end&.dig(:id)
|
|
358
549
|
end
|
|
359
550
|
|
|
@@ -384,7 +575,7 @@ module Gemstar
|
|
|
384
575
|
end
|
|
385
576
|
|
|
386
577
|
def tracked_git_paths
|
|
387
|
-
[gemfile_path, lockfile_path, importmap_path, package_json_path, package_lock_path, *importmap_vendor_paths].filter_map do |path|
|
|
578
|
+
[gemfile_path, lockfile_path, importmap_path, package_json_path, package_lock_path, uv_lock_path, cargo_toml_path, cargo_lock_path, *importmap_vendor_paths].filter_map do |path|
|
|
388
579
|
next unless File.file?(path)
|
|
389
580
|
|
|
390
581
|
git_repo.relative_path(path)
|
|
@@ -531,6 +722,7 @@ module Gemstar
|
|
|
531
722
|
when :gems then "gems"
|
|
532
723
|
when :js then "js"
|
|
533
724
|
when :python then "python"
|
|
725
|
+
when :cargo then "cargo"
|
|
534
726
|
else scope.to_s
|
|
535
727
|
end
|
|
536
728
|
end
|
|
@@ -540,6 +732,7 @@ module Gemstar
|
|
|
540
732
|
when :gems then "Gems"
|
|
541
733
|
when :js then "JS"
|
|
542
734
|
when :python then "Python"
|
|
735
|
+
when :cargo then "Cargo"
|
|
543
736
|
else scope.to_s.capitalize
|
|
544
737
|
end
|
|
545
738
|
end
|