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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open-uri"
|
|
3
|
+
require "time"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Gemstar
|
|
7
|
+
class PyPIMetadata
|
|
8
|
+
def initialize(package_name)
|
|
9
|
+
@package_name = package_name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :package_name
|
|
13
|
+
|
|
14
|
+
alias_method :gem_name, :package_name
|
|
15
|
+
|
|
16
|
+
def cache_key
|
|
17
|
+
"pypi-#{package_name}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def meta(cache_only: false, force_refresh: false)
|
|
21
|
+
return @meta if !cache_only && defined?(@meta)
|
|
22
|
+
|
|
23
|
+
json = if cache_only
|
|
24
|
+
Cache.peek(cache_key)
|
|
25
|
+
else
|
|
26
|
+
url = "https://pypi.org/pypi/#{URI.encode_www_form_component(package_name)}/json"
|
|
27
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
28
|
+
URI.parse(url).open(read_timeout: 8).read
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
parsed = begin
|
|
33
|
+
JSON.parse(json) if json
|
|
34
|
+
rescue
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
normalized = normalize_meta(parsed)
|
|
39
|
+
@meta = normalized unless cache_only
|
|
40
|
+
normalized
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def repo_uri(cache_only: false, force_refresh: false)
|
|
44
|
+
resolved_meta = meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
45
|
+
return nil unless resolved_meta
|
|
46
|
+
|
|
47
|
+
return @repo_uri if !cache_only && defined?(@repo_uri)
|
|
48
|
+
|
|
49
|
+
uri = resolved_meta["source_code_uri"]
|
|
50
|
+
uri ||= resolved_meta["homepage_uri"] if resolved_meta["homepage_uri"].to_s.include?("github.com")
|
|
51
|
+
uri = normalize_repo_uri(uri)
|
|
52
|
+
|
|
53
|
+
@repo_uri = uri unless cache_only
|
|
54
|
+
uri
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
|
|
58
|
+
requested_versions = Array(versions).compact
|
|
59
|
+
changelog = Gemstar::ChangeLog.new(self)
|
|
60
|
+
if requested_versions.empty?
|
|
61
|
+
changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
62
|
+
else
|
|
63
|
+
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: use_github_cli)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def registry_release_dates(cache_only: false, force_refresh: false)
|
|
68
|
+
parsed = raw_meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
69
|
+
Array(parsed&.dig("releases")).each_with_object({}) do |(version, files), dates|
|
|
70
|
+
uploaded_at = Array(files).filter_map { |file| file["upload_time_iso_8601"] || file["upload_time"] }.min
|
|
71
|
+
next if version.to_s.empty? || uploaded_at.to_s.empty?
|
|
72
|
+
|
|
73
|
+
dates[version] = Time.parse(uploaded_at).utc.strftime("%b %-d, %Y")
|
|
74
|
+
end
|
|
75
|
+
rescue JSON::ParserError, ArgumentError
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def warm_cache(versions: nil)
|
|
80
|
+
meta
|
|
81
|
+
repo_uri
|
|
82
|
+
changelog_sections(versions: versions)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def discover_github_tag_sections?
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def github_tag_candidates(version)
|
|
90
|
+
raw = version.to_s
|
|
91
|
+
candidates = [raw, (raw.start_with?("v") ? raw : "v#{raw}")]
|
|
92
|
+
candidates << "release_#{raw}"
|
|
93
|
+
candidates << "release_v#{raw}" unless raw.start_with?("v")
|
|
94
|
+
|
|
95
|
+
if raw.match?(/\A\d+\.\d\z/)
|
|
96
|
+
padded_minor = "#{raw}0"
|
|
97
|
+
candidates << padded_minor
|
|
98
|
+
candidates << "v#{padded_minor}"
|
|
99
|
+
candidates << "release_#{padded_minor}"
|
|
100
|
+
candidates << "release_v#{padded_minor}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if raw.match?(/\A\d{4}\.\d{1,2}\.\d{1,2}\z/)
|
|
104
|
+
year, month, day = raw.split(".")
|
|
105
|
+
dotted_date = [year, month.rjust(2, "0"), day.rjust(2, "0")].join(".")
|
|
106
|
+
candidates << dotted_date
|
|
107
|
+
candidates << "v#{dotted_date}"
|
|
108
|
+
candidates << "release_#{dotted_date}"
|
|
109
|
+
candidates << "release_v#{dotted_date}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
candidates.uniq
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def github_tag_matches?(_tag_name)
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def raw_meta(cache_only: false, force_refresh: false)
|
|
122
|
+
json = if cache_only
|
|
123
|
+
Cache.peek(cache_key)
|
|
124
|
+
else
|
|
125
|
+
url = "https://pypi.org/pypi/#{URI.encode_www_form_component(package_name)}/json"
|
|
126
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
127
|
+
URI.parse(url).open(read_timeout: 8).read
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
JSON.parse(json) if json
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def normalize_meta(parsed)
|
|
135
|
+
return nil unless parsed.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
info = parsed["info"] || {}
|
|
138
|
+
project_urls = info["project_urls"] || {}
|
|
139
|
+
source_code_uri = project_url(project_urls, "Source", "Source Code", "Code", "Repository", "GitHub")
|
|
140
|
+
homepage_uri = info["home_page"].to_s.empty? ? nil : info["home_page"]
|
|
141
|
+
homepage_uri ||= project_url(project_urls, "Homepage", "Home", "homepage")
|
|
142
|
+
changelog_uri = project_url(project_urls, "Changelog", "Change Log", "Changes", "Release Notes", "Release notes", "History")
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
"name" => info["name"] || package_name,
|
|
146
|
+
"version" => info["version"],
|
|
147
|
+
"info" => info["summary"] || info["description"],
|
|
148
|
+
"homepage_uri" => homepage_uri,
|
|
149
|
+
"source_code_uri" => source_code_uri,
|
|
150
|
+
"project_uri" => info["package_url"] || "https://pypi.org/project/#{package_name}/",
|
|
151
|
+
"documentation_uri" => project_url(project_urls, "Documentation", "Docs", "documentation"),
|
|
152
|
+
"changelog_uri" => changelog_uri
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def project_url(project_urls, *names)
|
|
157
|
+
normalized_urls = project_urls.transform_keys { |key| key.to_s.downcase.gsub(/[\s_-]+/, "") }
|
|
158
|
+
names.each do |name|
|
|
159
|
+
value = normalized_urls[name.to_s.downcase.gsub(/[\s_-]+/, "")]
|
|
160
|
+
return value unless value.to_s.empty?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def normalize_repo_uri(uri)
|
|
167
|
+
value = uri.to_s
|
|
168
|
+
return "" if value.empty?
|
|
169
|
+
|
|
170
|
+
value = value.sub(/\Agit\+/, "")
|
|
171
|
+
value = value.sub(/\Agit:\/\//, "https://")
|
|
172
|
+
value = value.sub(/\Ahttp:\/\//, "https://")
|
|
173
|
+
value = value.gsub(/\.git\z/, "")
|
|
174
|
+
|
|
175
|
+
if value.include?("github.com")
|
|
176
|
+
value = value[%r{\Ahttps?://github\.com/[^/]+/[^/]+}] || value
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
value
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -97,6 +97,12 @@ module Gemstar
|
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
def correct_changelog_version(version, heading:)
|
|
101
|
+
overrides = package_metadata.dig("changelog", "version_overrides") || {}
|
|
102
|
+
release_date = heading.to_s[/\b\d{4}-\d{2}-\d{2}\b/]
|
|
103
|
+
overrides.fetch([version, release_date].compact.join("@"), version)
|
|
104
|
+
end
|
|
105
|
+
|
|
100
106
|
def registry_release_dates(cache_only: false, force_refresh: false)
|
|
101
107
|
cache_key = "rubygems-versions-#{gem_name}"
|
|
102
108
|
json = if cache_only
|
|
@@ -145,7 +151,11 @@ module Gemstar
|
|
|
145
151
|
override_branches = Array(override["branches"]).compact
|
|
146
152
|
override_branches = [""] if override_branches.empty? && override["raw_base"]
|
|
147
153
|
return {
|
|
148
|
-
base: expand_metadata_template(
|
|
154
|
+
base: expand_metadata_template(
|
|
155
|
+
override["raw_base"] || github_raw_base(repo_uri),
|
|
156
|
+
cache_only: cache_only,
|
|
157
|
+
force_refresh: force_refresh
|
|
158
|
+
),
|
|
149
159
|
paths: override_paths.empty? ? Gemstar::ChangeLog::DEFAULT_CHANGELOG_PATHS : override_paths,
|
|
150
160
|
branches: override_branches.empty? ? RemoteRepository.new(github_raw_base(repo_uri)).find_main_branch(cache_only: cache_only, force_refresh: force_refresh) : override_branches
|
|
151
161
|
}
|
|
@@ -171,8 +181,21 @@ module Gemstar
|
|
|
171
181
|
repo_uri.sub("https://github.com", "https://raw.githubusercontent.com").chomp("/")
|
|
172
182
|
end
|
|
173
183
|
|
|
174
|
-
def expand_metadata_template(value)
|
|
175
|
-
|
|
184
|
+
def expand_metadata_template(value, cache_only: false, force_refresh: false)
|
|
185
|
+
version = metadata_template_version(cache_only: cache_only, force_refresh: force_refresh)
|
|
186
|
+
value.to_s.gsub("{gem_name}", gem_name).gsub("{version}", version)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def metadata_template_version(cache_only: false, force_refresh: false)
|
|
190
|
+
version = meta(cache_only: cache_only, force_refresh: force_refresh)&.dig("version").to_s
|
|
191
|
+
return version unless version.empty?
|
|
192
|
+
|
|
193
|
+
override_versions = package_metadata.dig("changelog", "version_overrides")
|
|
194
|
+
Array(override_versions&.values).compact.map(&:to_s).reject(&:empty?).max_by do |override_version|
|
|
195
|
+
Gem::Version.new(override_version)
|
|
196
|
+
rescue ArgumentError
|
|
197
|
+
Gem::Version.new("0")
|
|
198
|
+
end.to_s
|
|
176
199
|
end
|
|
177
200
|
|
|
178
201
|
def format_registry_release_date(datetime)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module Gemstar
|
|
2
|
+
class UvLockFile
|
|
3
|
+
attr_reader :specs
|
|
4
|
+
attr_reader :spec_sources
|
|
5
|
+
|
|
6
|
+
def initialize(path: nil, content: nil)
|
|
7
|
+
parsed = parse_content(content || File.read(path))
|
|
8
|
+
@specs = parsed[:specs]
|
|
9
|
+
@spec_sources = parsed[:spec_sources]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def source_for(name)
|
|
13
|
+
spec_sources[name]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def parse_content(content)
|
|
19
|
+
specs = {}
|
|
20
|
+
spec_sources = {}
|
|
21
|
+
|
|
22
|
+
package_blocks(content).each do |block|
|
|
23
|
+
name = scalar_value(block, "name")
|
|
24
|
+
version = scalar_value(block, "version")
|
|
25
|
+
source = source_for_block(block, name, version)
|
|
26
|
+
next if name.to_s.empty? || version.to_s.empty?
|
|
27
|
+
next if source[:type] == :virtual
|
|
28
|
+
|
|
29
|
+
specs[name] = version
|
|
30
|
+
spec_sources[name] = source
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
specs: specs,
|
|
35
|
+
spec_sources: spec_sources
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def package_blocks(content)
|
|
40
|
+
content.to_s.split(/^\[\[package\]\]\s*$/).drop(1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def scalar_value(block, key)
|
|
44
|
+
block[/^#{Regexp.escape(key)}\s*=\s*"([^"]*)"/, 1]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def source_for_block(block, name, version)
|
|
48
|
+
source = inline_table(block[/^source\s*=\s*\{([^}]*)\}/, 1])
|
|
49
|
+
package_url = "https://pypi.org/project/#{name}/"
|
|
50
|
+
registry_url = source["registry"]
|
|
51
|
+
|
|
52
|
+
if source.key?("virtual")
|
|
53
|
+
return {
|
|
54
|
+
type: :virtual,
|
|
55
|
+
path: source["virtual"]
|
|
56
|
+
}.compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if source.key?("git")
|
|
60
|
+
return {
|
|
61
|
+
type: :git,
|
|
62
|
+
remote: source["git"],
|
|
63
|
+
revision: source["rev"] || source["commit"],
|
|
64
|
+
branch: source["branch"],
|
|
65
|
+
tag: source["tag"],
|
|
66
|
+
package_name: name,
|
|
67
|
+
package_version: version,
|
|
68
|
+
registry_url: package_url
|
|
69
|
+
}.compact
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if source.key?("path") || source.key?("editable") || source.key?("directory")
|
|
73
|
+
return {
|
|
74
|
+
type: :path,
|
|
75
|
+
path: source["path"] || source["editable"] || source["directory"],
|
|
76
|
+
package_name: name,
|
|
77
|
+
package_version: version,
|
|
78
|
+
registry_url: package_url
|
|
79
|
+
}.compact
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
type: :pypi,
|
|
84
|
+
remote: registry_url,
|
|
85
|
+
distribution_url: distribution_url(block),
|
|
86
|
+
package_name: name,
|
|
87
|
+
package_version: version,
|
|
88
|
+
registry_url: package_url
|
|
89
|
+
}.compact
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def distribution_url(block)
|
|
93
|
+
sdist = inline_table(block[/^sdist\s*=\s*\{([^}]*)\}/, 1])
|
|
94
|
+
return sdist["url"] unless sdist["url"].to_s.empty?
|
|
95
|
+
|
|
96
|
+
wheel = inline_table(block[/^\s*\{\s*url\s*=\s*"([^"]+)"/, 0])
|
|
97
|
+
wheel["url"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def inline_table(content)
|
|
101
|
+
content.to_s.scan(/([\w-]+)\s*=\s*("[^"]*"|true|false|[^,\s}]+)/).each_with_object({}) do |(key, raw_value), values|
|
|
102
|
+
values[key] = parse_inline_value(raw_value)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_inline_value(raw_value)
|
|
107
|
+
value = raw_value.to_s.strip
|
|
108
|
+
return true if value == "true"
|
|
109
|
+
return false if value == "false"
|
|
110
|
+
|
|
111
|
+
value.sub(/\A"/, "").sub(/"\z/, "")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|