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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6eab7d36aeffe59f5863dffb07696c7ed8aab64dfb8828ae962122d607ad0b28
|
|
4
|
+
data.tar.gz: 9120441dae108fb6ff9d9a009cb9e6988349b884f011c40037460a86c45ef42a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://github.com/FDj/gemstar/actions)
|
|
4
4
|
|
|
5
5
|
# Gemstar
|
|
6
|
-
Helps you keep track of your gems and
|
|
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
|
|
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:
|
data/lib/gemstar/cache_warmer.rb
CHANGED
|
@@ -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
|
-
|
|
154
|
+
metadata&.warm_cache(versions: package_versions(package_state))
|
|
124
155
|
|
|
125
|
-
|
|
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
|