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.
@@ -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
- Rackup::Server.start(
58
- app: app,
59
- server: "webrick",
60
- Host: bind,
61
- Port: port,
62
- AccessLog: [],
63
- Logger: Gemstar::WEBrickLogger.new($stderr, WEBrick::BasicLog::INFO),
64
- StartCallback: server_start_callback(projects, cache_warmer)
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
  }
@@ -78,6 +78,8 @@ module Gemstar
78
78
 
79
79
  def icon_for(info)
80
80
  return "📦" if info[:package_scope] == "js"
81
+ return "🐍" if info[:package_scope] == "python"
82
+ return "🦀" if info[:package_scope] == "cargo"
81
83
  return "🐙" if info[:homepage_url]&.include?("github.com")
82
84
 
83
85
  "💎"
@@ -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(File.dirname(expanded_input))
27
- else
28
- nil
50
+ File.dirname(expanded_input, 2)
29
51
  end
30
52
  end
31
53
 
32
- raise ArgumentError, "No supported project files found for #{input}" unless directory
33
- raise ArgumentError, "No supported project files found for #{input}" unless supported_project_directory?(directory)
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 Gemfile.lock in the working tree" }] +
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
- @gem_states_cache[cache_key] = (gem_states + js_states + npm_states).sort_by { |gem| [gem[:name], gem[:package_scope], gem[:package_source_file].to_s] }
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
- cache_key = [package_name, package_scope, source_file, revision_id]
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?(package_name)
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?(package_name)
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