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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aba5f74f93dc292f713a6f5d1a824cc04d7601fffac736f075c75f08ea769ff7
4
- data.tar.gz: f13a06cba387d9ae4ec744ed6e9703bbe8c1aea95cd8f27ab2f4efd816af33d6
3
+ metadata.gz: 6eab7d36aeffe59f5863dffb07696c7ed8aab64dfb8828ae962122d607ad0b28
4
+ data.tar.gz: 9120441dae108fb6ff9d9a009cb9e6988349b884f011c40037460a86c45ef42a
5
5
  SHA512:
6
- metadata.gz: e45c7adfbfae8779801141084c0097d57be63415398c1a1b695a090042aa1c8fc6c94764275963aca7ef361de4e63f38f39a172b5158814282cd43ce865f5556
7
- data.tar.gz: ad082b18db97c9f7a1a1c74de309fad18ca282cdd41c42918b27818b7bcc6da895fc9320660f35a0546028b75ed56726a5a545b7e709f4eb82d9a49237f938f5
6
+ metadata.gz: 5857f33b3a6b36759b2978b768749d7240f8ce54c73e57c48bce10ca407db846a955b80ba7c93d5db2a03bccbc6cb964ee55759ed4d75deb6982e23da681f5c8
7
+ data.tar.gz: 61dc580cfbb69aca3535428107a299833343ad132bb5e437cc01380ae4d58d90d525498818aed462793b036348e9f3666d83d8c6a7d2b821b62313f52c289381
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.3
4
+
5
+ - **Python uv projects** are now supported with our new `uv.lock` file parser and pypi package support!
6
+ - **Rust Cargo projects** are now supported with a new `Cargo.lock` parser and crates.io package support!
7
+ - Changelogs in rst format are now supported.
8
+ - Changelogs: Fix parsing of documentation percentages, linked version headings, and version-first headings with dotted dates.
9
+ - Changelogs: Do not treat version-looking prose inside release notes as a new release heading.
10
+ - Changelogs: Prefer repository-branch changelogs when gem metadata points to a release-pinned file that may omit its own release.
11
+ - Changelogs: Discover extensionless changelog files and direct GitHub changelog links, including Roda and Selenium release histories.
12
+ - Changelogs: Correct known upstream release-heading typos through package metadata, including Thruster 0.1.23.
13
+ - Server: Sort changelog cards with semantic version comparison so Selenium's `4.x` releases appear ahead of `3.142.x` and `3.141.x`.
14
+ - Server: Render linked changelog version headings without Markdown brackets.
15
+ - Server: Keep the details focus ring visible around its sticky header.
16
+ - Server: Keep keyboard-selected sidebar packages visible below the sticky filter header.
17
+ - Documentation: List Python package support in the README and gem metadata.
18
+ - Server: Details heading is now sticky.
19
+ - Server: Changed display of available versions to be more clear and concise.
20
+ - CLI: Improve message shown when no supported project files are found.
21
+ - CLI: Add `--version` to print the Gemstar version.
22
+
23
+ ## 1.2
24
+
25
+ - Server: Cache warmer now pre-fetches package detail pages so browsing warmed gems is faster.
26
+ - Server: Manual browser reloads now refresh the selected package details, including metadata and release notes caches.
27
+ - Server: Fetch GitHub release bodies through the GitHub Releases API before falling back to HTML scraping.
28
+ - Server: Add explicit "Use GitHub CLI" fallback for GitHub-backed release notes when automatic release discovery misses a page.
29
+
3
30
  ## 1.1.1
4
31
 
5
32
  - Server: Show release dates for changelog entries.
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![JRuby Build](https://github.com/FDj/gemstar/workflows/JRuby%20Build/badge.svg)](https://github.com/FDj/gemstar/actions)
4
4
 
5
5
  # Gemstar
6
- Helps you keep track of your gems and JavaScript packages.
6
+ Helps you keep track of your Ruby gems, JavaScript packages, Python packages, and Rust crates.
7
7
 
8
8
  ## Installation
9
9
 
@@ -72,7 +72,7 @@ To examine a specific Gemfile.lock, pass it like this:
72
72
  gemstar diff --lockfile=~/MyProject/Gemfile.lock
73
73
  ```
74
74
 
75
- To diff a project from anywhere, pass the project directory or a supported project file. In project mode, gemstar includes Ruby gems plus JS packages from `importmap.rb` and `package-lock.json` when present:
75
+ To diff a project from anywhere, pass the project directory or a supported project file. In project mode, gemstar includes Ruby gems, JS packages from `importmap.rb` and `package-lock.json`, Python packages from `uv.lock`, and Rust crates from `Cargo.lock` when present:
76
76
 
77
77
  ```shell
78
78
  gemstar diff --project ~/Code/my-app
@@ -83,6 +83,8 @@ To filter a project diff down to one ecosystem:
83
83
  ```shell
84
84
  gemstar diff --project ~/Code/my-app --ecosystem js
85
85
  gemstar diff --project ~/Code/my-app --ecosystem gems
86
+ gemstar diff --project ~/Code/my-app --ecosystem python
87
+ gemstar diff --project ~/Code/my-app --ecosystem cargo
86
88
  ```
87
89
 
88
90
  To write markdown instead of html:
@@ -1,14 +1,17 @@
1
1
  require "set"
2
2
  require "thread"
3
+ require_relative "crates_io_metadata"
4
+ require_relative "pypi_metadata"
3
5
 
4
6
  module Gemstar
5
7
  class CacheWarmer
6
8
  DEFAULT_THREADS = 10
7
9
 
8
- def initialize(io: $stderr, debug: false, thread_count: DEFAULT_THREADS)
10
+ def initialize(io: $stderr, debug: false, thread_count: DEFAULT_THREADS, detail_cache_fetcher: nil)
9
11
  @io = io
10
12
  @debug = debug
11
13
  @thread_count = thread_count
14
+ @detail_cache_fetcher = detail_cache_fetcher
12
15
  @mutex = Mutex.new
13
16
  @condition = ConditionVariable.new
14
17
  @queue = []
@@ -17,14 +20,19 @@ module Gemstar
17
20
  @completed = Set.new
18
21
  @workers = []
19
22
  @started = false
23
+ @stopping = false
20
24
  @total = 0
21
25
  @completed_count = 0
22
26
  end
23
27
 
28
+ attr_writer :detail_cache_fetcher
29
+
24
30
  def enqueue_many(package_states)
25
31
  states = normalize_package_states(package_states)
26
32
 
27
33
  @mutex.synchronize do
34
+ return self if @stopping
35
+
28
36
  states.each do |package_state|
29
37
  key = package_key(package_state)
30
38
  next if @completed.include?(key) || @queued.include?(key) || @in_progress.include?(key)
@@ -71,6 +79,28 @@ module Gemstar
71
79
  @condition.broadcast
72
80
  end
73
81
 
82
+ def shutdown(timeout: 0.5)
83
+ workers = nil
84
+
85
+ @mutex.synchronize do
86
+ @stopping = true
87
+ @queue.clear
88
+ @queued.clear
89
+ @condition.broadcast
90
+ workers = @workers.dup
91
+ end
92
+
93
+ workers.each do |worker|
94
+ worker.join(timeout)
95
+ worker.kill if worker.alive?
96
+ end
97
+
98
+ @mutex.synchronize do
99
+ @workers.clear
100
+ @started = false
101
+ end
102
+ end
103
+
74
104
  def pending?(package_name)
75
105
  @mutex.synchronize do
76
106
  @queue.any? { |item| item[:name] == package_name || item.dig(:source, :package_name) == package_name } ||
@@ -94,9 +124,10 @@ module Gemstar
94
124
 
95
125
  loop do
96
126
  package_state = @mutex.synchronize do
97
- while @queue.empty?
127
+ while @queue.empty? && !@stopping
98
128
  @condition.wait(@mutex)
99
129
  end
130
+ return if @stopping && @queue.empty?
100
131
 
101
132
  next_package = @queue.shift
102
133
  key = package_key(next_package)
@@ -120,13 +151,22 @@ module Gemstar
120
151
 
121
152
  def warm_cache_for_package(package_state)
122
153
  metadata = metadata_adapter_for(package_state)
123
- return unless metadata
154
+ metadata&.warm_cache(versions: package_versions(package_state))
124
155
 
125
- metadata.warm_cache(versions: package_versions(package_state))
156
+ warm_detail_cache_for_package(package_state)
126
157
  rescue StandardError => e
127
158
  log "Cache refresh failed for #{package_label(package_state)}: #{e.class}: #{e.message}"
128
159
  end
129
160
 
161
+ def warm_detail_cache_for_package(package_state)
162
+ fetcher = @detail_cache_fetcher
163
+ return unless fetcher
164
+
165
+ Array(package_state[:detail_cache_contexts]).each do |context|
166
+ fetcher.call(package_state, context)
167
+ end
168
+ end
169
+
130
170
  def log_progress(package_name, current)
131
171
  return unless @debug
132
172
  return unless current <= 5 || (current % 25).zero?
@@ -168,6 +208,13 @@ module Gemstar
168
208
  end
169
209
 
170
210
  def metadata_adapter_for(package_state)
211
+ if package_state[:package_scope] == "cargo"
212
+ package_name = package_state[:metadata_package_name] || package_state.dig(:source, :package_name) || package_state[:name]
213
+ return Gemstar::CratesIOMetadata.new(package_name)
214
+ end
215
+
216
+ return Gemstar::PyPIMetadata.new(package_state[:name]) if package_state[:package_scope] == "python"
217
+
171
218
  if package_state[:package_scope] == "js"
172
219
  provider_gem = package_state.dig(:source, :provider_gem)
173
220
  return Gemstar::RubyGemsMetadata.new(provider_gem) unless provider_gem.to_s.empty?
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Gemstar
6
+ class CargoLockFile
7
+ attr_reader :specs
8
+ attr_reader :spec_sources
9
+
10
+ def initialize(path: nil, content: nil)
11
+ parsed = parse_content(content || File.read(path))
12
+ @specs = parsed[:specs]
13
+ @spec_sources = parsed[:spec_sources]
14
+ end
15
+
16
+ def source_for(name)
17
+ spec_sources[name]
18
+ end
19
+
20
+ private
21
+
22
+ def parse_content(content)
23
+ packages = package_blocks(content).filter_map do |block|
24
+ name = scalar_value(block, "name")
25
+ version = scalar_value(block, "version")
26
+ source = scalar_value(block, "source")
27
+ next if name.to_s.empty? || version.to_s.empty?
28
+ next if source.to_s.empty?
29
+
30
+ {
31
+ name: name,
32
+ version: version,
33
+ source: source_for_package(source, name, version)
34
+ }
35
+ end
36
+
37
+ specs = {}
38
+ spec_sources = {}
39
+ packages.group_by { |package| package[:name] }.each_value do |named_packages|
40
+ sorted_packages(named_packages).each_with_index do |package, index|
41
+ key = package_key(package, index, named_packages.length)
42
+ specs[key] = package[:version]
43
+ spec_sources[key] = package[:source]
44
+ end
45
+ end
46
+
47
+ {specs: specs, spec_sources: spec_sources}
48
+ end
49
+
50
+ def package_blocks(content)
51
+ content.to_s.split(/^\[\[package\]\]\s*$/).drop(1)
52
+ end
53
+
54
+ def scalar_value(block, key)
55
+ block[/^#{Regexp.escape(key)}\s*=\s*"((?:\\.|[^"])*)"/, 1]&.gsub('\\"', '"')
56
+ end
57
+
58
+ def source_for_package(raw_source, name, version)
59
+ if raw_source.start_with?("git+")
60
+ git_source(raw_source, name, version)
61
+ else
62
+ registry_source(raw_source, name, version)
63
+ end
64
+ end
65
+
66
+ def git_source(raw_source, name, version)
67
+ remote_with_query, revision = raw_source.delete_prefix("git+").split("#", 2)
68
+ remote, query = remote_with_query.split("?", 2)
69
+ params = URI.decode_www_form(query.to_s).to_h
70
+
71
+ {
72
+ type: :git,
73
+ remote: remote,
74
+ revision: revision,
75
+ branch: params["branch"],
76
+ tag: params["tag"],
77
+ package_name: name,
78
+ package_version: version
79
+ }.compact
80
+ rescue ArgumentError
81
+ {
82
+ type: :git,
83
+ remote: raw_source.delete_prefix("git+").split(/[?#]/, 2).first,
84
+ revision: raw_source.split("#", 2)[1],
85
+ package_name: name,
86
+ package_version: version
87
+ }.compact
88
+ end
89
+
90
+ def registry_source(raw_source, name, version)
91
+ registry = raw_source.sub(/\A(?:registry|sparse)\+/, "")
92
+ crates_io = crates_io_registry?(registry)
93
+
94
+ {
95
+ type: :cargo,
96
+ remote: registry,
97
+ package_name: name,
98
+ package_version: version,
99
+ registry_url: crates_io ? "https://crates.io/crates/#{name}" : registry,
100
+ crates_io: crates_io
101
+ }
102
+ end
103
+
104
+ def crates_io_registry?(registry)
105
+ registry.include?("crates.io-index") || registry.include?("index.crates.io")
106
+ end
107
+
108
+ def sorted_packages(packages)
109
+ packages.sort do |left, right|
110
+ comparison = compare_versions(right[:version], left[:version])
111
+ comparison.zero? ? left[:source].to_s <=> right[:source].to_s : comparison
112
+ end
113
+ end
114
+
115
+ def compare_versions(left, right)
116
+ Gem::Version.new(left) <=> Gem::Version.new(right)
117
+ rescue ArgumentError
118
+ left.to_s <=> right.to_s
119
+ end
120
+
121
+ def package_key(package, index, package_count)
122
+ return package[:name] if package_count == 1
123
+
124
+ key = "#{package[:name]}@#{package[:version]}"
125
+ if index.zero? || !@used_package_keys&.include?(key)
126
+ remember_package_key(key)
127
+ else
128
+ remember_package_key("#{key}:#{index + 1}")
129
+ end
130
+ end
131
+
132
+ def remember_package_key(key)
133
+ @used_package_keys ||= []
134
+ @used_package_keys << key
135
+ key
136
+ end
137
+ end
138
+ end