gemstar 1.0.2 → 1.1
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 +39 -1
- data/README.md +28 -3
- data/bin/gemstar +5 -1
- data/lib/gemstar/cache_warmer.rb +93 -35
- data/lib/gemstar/change_log.rb +337 -31
- data/lib/gemstar/cli.rb +5 -1
- data/lib/gemstar/commands/diff.rb +197 -31
- data/lib/gemstar/commands/server.rb +94 -10
- data/lib/gemstar/data/importmap_package_metadata.json +22 -0
- data/lib/gemstar/data/ruby_gems_metadata.json +9 -0
- data/lib/gemstar/git_repo.rb +41 -3
- data/lib/gemstar/importmap_file.rb +193 -0
- data/lib/gemstar/lock_file.rb +91 -9
- data/lib/gemstar/npm_metadata.rb +159 -0
- data/lib/gemstar/outputs/html.rb +53 -4
- data/lib/gemstar/outputs/markdown.rb +29 -3
- data/lib/gemstar/package_lock_file.rb +101 -0
- data/lib/gemstar/project.rb +325 -37
- data/lib/gemstar/ruby_gems_metadata.rb +77 -2
- data/lib/gemstar/version.rb +1 -2
- data/lib/gemstar/web/app.rb +418 -70
- data/lib/gemstar/web/templates/app.css +40 -5
- data/lib/gemstar/web/templates/app.js.erb +119 -15
- data/lib/gemstar/web/templates/page.html.erb +2 -1
- data/lib/gemstar.rb +3 -0
- metadata +7 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
class ImportmapFile
|
|
5
|
+
Pin = Struct.new(:name, :target, :source, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
IMPORTMAP_PACKAGE_METADATA_PATH = File.expand_path("data/importmap_package_metadata.json", __dir__)
|
|
8
|
+
|
|
9
|
+
def self.package_metadata
|
|
10
|
+
@package_metadata ||= begin
|
|
11
|
+
JSON.parse(File.read(IMPORTMAP_PACKAGE_METADATA_PATH)).transform_values do |attributes|
|
|
12
|
+
attributes.each_with_object({}) do |(key, value), metadata|
|
|
13
|
+
metadata[key.to_sym] = value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
17
|
+
{}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(path: nil, content: nil, vendor_reader: nil)
|
|
22
|
+
@path = path
|
|
23
|
+
@vendor_reader = vendor_reader
|
|
24
|
+
@pins = parse_content(content || File.read(path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :pins
|
|
28
|
+
|
|
29
|
+
def specs
|
|
30
|
+
pins.transform_values(&:target)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def source_for(name)
|
|
34
|
+
pins[name]&.source
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def parse_content(content)
|
|
40
|
+
content.each_line.with_object({}) do |line, pins|
|
|
41
|
+
next unless line =~ /^\s*pin\s+["']([^"']+)["'](.*)$/
|
|
42
|
+
|
|
43
|
+
name = Regexp.last_match(1)
|
|
44
|
+
rest = Regexp.last_match(2).to_s
|
|
45
|
+
target = rest[/\bto:\s*["']([^"']+)["']/, 1] || name
|
|
46
|
+
inline_version = rest[/#\s*@([^\s]+)/, 1]
|
|
47
|
+
|
|
48
|
+
pins[name] = Pin.new(
|
|
49
|
+
name: name,
|
|
50
|
+
target: target,
|
|
51
|
+
source: build_source_for(name, target, inline_version: inline_version)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_source_for(name, target, inline_version: nil)
|
|
57
|
+
{
|
|
58
|
+
type: :importmap,
|
|
59
|
+
remote: target
|
|
60
|
+
}
|
|
61
|
+
.merge(default_package_metadata_for(name, inline_version: inline_version))
|
|
62
|
+
.merge(vendored_package_metadata_for(target))
|
|
63
|
+
.merge(cdn_package_metadata_for(target))
|
|
64
|
+
.merge(repo_source_for(name, target))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def default_package_metadata_for(name, inline_version: nil)
|
|
68
|
+
return {} unless package_like_pin_name?(name)
|
|
69
|
+
|
|
70
|
+
metadata = { package_name: name, registry_url: "https://www.npmjs.com/package/#{name}" }
|
|
71
|
+
unless inline_version.to_s.empty?
|
|
72
|
+
key = exact_package_version?(inline_version) ? :package_version : :package_requirement
|
|
73
|
+
metadata[key] = inline_version
|
|
74
|
+
end
|
|
75
|
+
metadata
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def vendored_package_metadata_for(target)
|
|
79
|
+
return {} unless local_javascript_target?(target)
|
|
80
|
+
|
|
81
|
+
first_line = vendored_file_first_line(target)
|
|
82
|
+
return {} if first_line.empty?
|
|
83
|
+
|
|
84
|
+
if first_line =~ %r{\A//\s+((?:@[^/]+/)?[^@\s]+(?:/[^@\s]+)?)@([^\s]+)\s+downloaded from\s+(https?://\S+)}
|
|
85
|
+
package_name = Regexp.last_match(1)
|
|
86
|
+
package_version = Regexp.last_match(2)
|
|
87
|
+
remote = Regexp.last_match(3)
|
|
88
|
+
remote_metadata = cdn_package_metadata_for(remote)
|
|
89
|
+
package_name = remote_metadata[:package_name] || package_name
|
|
90
|
+
package_version = remote_metadata[:package_version] || package_version
|
|
91
|
+
{
|
|
92
|
+
package_name: package_name,
|
|
93
|
+
package_version: package_version,
|
|
94
|
+
registry_url: "https://www.npmjs.com/package/#{package_name}",
|
|
95
|
+
remote: remote
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
{}
|
|
99
|
+
end
|
|
100
|
+
rescue EOFError
|
|
101
|
+
{}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def vendored_file_first_line(target)
|
|
105
|
+
if @vendor_reader
|
|
106
|
+
content = @vendor_reader.call(target.to_s)
|
|
107
|
+
return content.to_s.lines.first.to_s if content
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return "" unless @path
|
|
111
|
+
|
|
112
|
+
vendor_path = File.expand_path(File.join(File.dirname(@path), "..", "vendor", "javascript", target.to_s))
|
|
113
|
+
return "" unless File.file?(vendor_path)
|
|
114
|
+
|
|
115
|
+
File.open(vendor_path, &:readline).to_s
|
|
116
|
+
rescue EOFError
|
|
117
|
+
""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def cdn_package_metadata_for(target)
|
|
121
|
+
value = target.to_s
|
|
122
|
+
return {} if value.empty?
|
|
123
|
+
|
|
124
|
+
package_name, package_version =
|
|
125
|
+
case value
|
|
126
|
+
when %r{\Ahttps://esm\.sh/((?:@[^/]+/)?[^@/?]+)@([^/?]+)}
|
|
127
|
+
[Regexp.last_match(1), Regexp.last_match(2)]
|
|
128
|
+
when %r{\Ahttps://ga\.jspm\.io/npm:((?:@[^/]+/)?[^@/]+)@([^/]+)/}
|
|
129
|
+
[Regexp.last_match(1), Regexp.last_match(2)]
|
|
130
|
+
when %r{\Ahttps://cdn\.jsdelivr\.net/npm/((?:@[^/]+/)?[^@/]+)@([^/]+)/}
|
|
131
|
+
[Regexp.last_match(1), Regexp.last_match(2)]
|
|
132
|
+
when %r{\Ahttps://unpkg\.com/((?:@[^/]+/)?[^@/?]+)@([^/?]+)}
|
|
133
|
+
[Regexp.last_match(1), Regexp.last_match(2)]
|
|
134
|
+
else
|
|
135
|
+
[nil, nil]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return {} unless package_name
|
|
139
|
+
|
|
140
|
+
version_metadata = exact_package_version?(package_version) ? { package_version: package_version } : { package_requirement: package_version }
|
|
141
|
+
|
|
142
|
+
version_metadata.merge(
|
|
143
|
+
package_name: package_name,
|
|
144
|
+
registry_url: "https://www.npmjs.com/package/#{package_name}"
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def repo_source_for(name, target)
|
|
149
|
+
package_metadata = self.class.package_metadata[name]
|
|
150
|
+
repo_url = github_repo_url_for(target) || package_metadata&.dig(:repo_url)
|
|
151
|
+
metadata = {}
|
|
152
|
+
metadata[:repo_url] = repo_url if repo_url
|
|
153
|
+
metadata[:provider_gem] = package_metadata[:provider_gem] if package_metadata&.dig(:provider_gem)
|
|
154
|
+
metadata
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def github_repo_url_for(target)
|
|
158
|
+
value = target.to_s
|
|
159
|
+
return nil if value.empty?
|
|
160
|
+
|
|
161
|
+
case value
|
|
162
|
+
when %r{\Ahttps://raw\.githubusercontent\.com/([^/]+/[^/]+)/}
|
|
163
|
+
"https://github.com/#{Regexp.last_match(1)}"
|
|
164
|
+
when %r{\Ahttps://github\.com/([^/]+/[^/]+)}
|
|
165
|
+
"https://github.com/#{Regexp.last_match(1)}"
|
|
166
|
+
else
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def package_like_pin_name?(name)
|
|
172
|
+
value = name.to_s
|
|
173
|
+
return false if value.empty?
|
|
174
|
+
return false if value.start_with?("controllers/")
|
|
175
|
+
return false if value.include?("_controller")
|
|
176
|
+
return false if value.start_with?("./", "../")
|
|
177
|
+
|
|
178
|
+
value.start_with?("@") || value.match?(/\A[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*\z/i)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def exact_package_version?(version)
|
|
182
|
+
version.to_s.match?(/\Av?\d+(?:\.\d+)*(?:[-.][A-Za-z0-9]+)*\z/)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def local_javascript_target?(target)
|
|
186
|
+
value = target.to_s
|
|
187
|
+
return false if value.empty?
|
|
188
|
+
return false if value.match?(%r{\A[a-z]+://}i)
|
|
189
|
+
|
|
190
|
+
value.end_with?(".js", ".mjs")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/gemstar/lock_file.rb
CHANGED
|
@@ -5,26 +5,72 @@ module Gemstar
|
|
|
5
5
|
parsed = content ? parse_content(content) : parse_lockfile(path)
|
|
6
6
|
@specs = parsed[:specs]
|
|
7
7
|
@dependency_graph = parsed[:dependency_graph]
|
|
8
|
+
@dependency_requirements = parsed[:dependency_requirements]
|
|
8
9
|
@direct_dependencies = parsed[:direct_dependencies]
|
|
10
|
+
@direct_dependency_requirements = parsed[:direct_dependency_requirements]
|
|
11
|
+
@spec_sources = parsed[:spec_sources]
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
attr_reader :specs
|
|
12
15
|
attr_reader :dependency_graph
|
|
16
|
+
attr_reader :dependency_requirements
|
|
13
17
|
attr_reader :direct_dependencies
|
|
18
|
+
attr_reader :direct_dependency_requirements
|
|
19
|
+
attr_reader :spec_sources
|
|
14
20
|
|
|
15
21
|
def origins_for(gem_name)
|
|
16
|
-
|
|
22
|
+
if direct_dependencies.include?(gem_name)
|
|
23
|
+
return [{
|
|
24
|
+
type: :direct,
|
|
25
|
+
path: [gem_name],
|
|
26
|
+
requirement: direct_dependency_requirements[gem_name]
|
|
27
|
+
}]
|
|
28
|
+
end
|
|
17
29
|
|
|
18
30
|
direct_dependencies.filter_map do |root_dependency|
|
|
19
31
|
path = shortest_path_from(root_dependency, gem_name)
|
|
20
32
|
next if path.nil?
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
parent_name = path[-2]
|
|
35
|
+
{
|
|
36
|
+
type: :transitive,
|
|
37
|
+
path: path,
|
|
38
|
+
requirement: dependency_requirements.dig(parent_name, gem_name)
|
|
39
|
+
}
|
|
23
40
|
end
|
|
24
41
|
end
|
|
25
42
|
|
|
43
|
+
def source_for(gem_name)
|
|
44
|
+
spec_sources[gem_name]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def platform_for(gem_name)
|
|
48
|
+
version = specs[gem_name].to_s
|
|
49
|
+
parts = version.split("-")
|
|
50
|
+
return nil if parts.length < 2
|
|
51
|
+
|
|
52
|
+
1.upto(parts.length - 1) do |index|
|
|
53
|
+
candidate_version = parts[0...index].join("-")
|
|
54
|
+
candidate_platform = parts[index..].join("-")
|
|
55
|
+
next unless plausible_platform_suffix?(candidate_platform)
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
Gem::Version.new(candidate_version)
|
|
59
|
+
return candidate_platform
|
|
60
|
+
rescue ArgumentError
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
26
68
|
private
|
|
27
69
|
|
|
70
|
+
def plausible_platform_suffix?(suffix)
|
|
71
|
+
suffix.match?(/darwin|linux|mingw|mswin|musl|java|x86|arm|universal/i)
|
|
72
|
+
end
|
|
73
|
+
|
|
28
74
|
def shortest_path_from(root_dependency, target_gem)
|
|
29
75
|
queue = [[root_dependency, [root_dependency]]]
|
|
30
76
|
visited = {}
|
|
@@ -53,9 +99,13 @@ module Gemstar
|
|
|
53
99
|
def parse_content(content)
|
|
54
100
|
specs = {}
|
|
55
101
|
dependency_graph = Hash.new { |hash, key| hash[key] = [] }
|
|
102
|
+
dependency_requirements = Hash.new { |hash, key| hash[key] = {} }
|
|
56
103
|
direct_dependencies = []
|
|
104
|
+
direct_dependency_requirements = {}
|
|
105
|
+
spec_sources = {}
|
|
57
106
|
current_section = nil
|
|
58
107
|
current_spec = nil
|
|
108
|
+
current_source = nil
|
|
59
109
|
|
|
60
110
|
content.each_line do |line|
|
|
61
111
|
stripped = line.strip
|
|
@@ -68,12 +118,28 @@ module Gemstar
|
|
|
68
118
|
if stripped == "GEM"
|
|
69
119
|
current_section = :gem
|
|
70
120
|
current_spec = nil
|
|
121
|
+
current_source = { type: :rubygems }
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if stripped == "PATH"
|
|
126
|
+
current_section = :path
|
|
127
|
+
current_spec = nil
|
|
128
|
+
current_source = { type: :path }
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if stripped == "GIT"
|
|
133
|
+
current_section = :git
|
|
134
|
+
current_spec = nil
|
|
135
|
+
current_source = { type: :git }
|
|
71
136
|
next
|
|
72
137
|
end
|
|
73
138
|
|
|
74
139
|
if stripped == "DEPENDENCIES"
|
|
75
140
|
current_section = :dependencies
|
|
76
141
|
current_spec = nil
|
|
142
|
+
current_source = nil
|
|
77
143
|
next
|
|
78
144
|
end
|
|
79
145
|
|
|
@@ -83,17 +149,30 @@ module Gemstar
|
|
|
83
149
|
end
|
|
84
150
|
|
|
85
151
|
case current_section
|
|
86
|
-
when :gem
|
|
87
|
-
if line =~ /^\s{
|
|
152
|
+
when :gem, :path, :git
|
|
153
|
+
if line =~ /^\s{2}remote:\s+(.+)$/
|
|
154
|
+
current_source = (current_source || {}).merge(remote: Regexp.last_match(1))
|
|
155
|
+
elsif line =~ /^\s{2}(revision|branch|tag|ref|glob|submodules):\s+(.+)$/
|
|
156
|
+
current_source = (current_source || {}).merge(Regexp.last_match(1).to_sym => Regexp.last_match(2))
|
|
157
|
+
elsif line =~ /^\s{2}path:\s+(.+)$/
|
|
158
|
+
current_source = (current_source || {}).merge(path: Regexp.last_match(1))
|
|
159
|
+
elsif line =~ /^\s{4}(\S+) \((.+)\)/
|
|
88
160
|
name, version = Regexp.last_match(1), Regexp.last_match(2)
|
|
89
161
|
specs[name] = version
|
|
162
|
+
spec_sources[name] = (current_source || {}).dup
|
|
90
163
|
current_spec = name
|
|
91
|
-
elsif current_spec && line =~ /^\s{6}([^\s(]+)
|
|
92
|
-
|
|
164
|
+
elsif current_spec && line =~ /^\s{6}([^\s(]+)(?: \(([^)]+)\))?/
|
|
165
|
+
dependency_name = Regexp.last_match(1)
|
|
166
|
+
requirement = Regexp.last_match(2)
|
|
167
|
+
dependency_graph[current_spec] << dependency_name
|
|
168
|
+
dependency_requirements[current_spec][dependency_name] = requirement if requirement && !requirement.empty?
|
|
93
169
|
end
|
|
94
170
|
when :dependencies
|
|
95
|
-
if line =~ /^\s{2}([^\s!(]+)
|
|
96
|
-
|
|
171
|
+
if line =~ /^\s{2}([^\s!(]+)(?: \(([^)]+)\))?/
|
|
172
|
+
dependency_name = Regexp.last_match(1)
|
|
173
|
+
requirement = Regexp.last_match(2)
|
|
174
|
+
direct_dependencies << dependency_name
|
|
175
|
+
direct_dependency_requirements[dependency_name] = requirement if requirement && !requirement.empty?
|
|
97
176
|
end
|
|
98
177
|
end
|
|
99
178
|
end
|
|
@@ -101,7 +180,10 @@ module Gemstar
|
|
|
101
180
|
{
|
|
102
181
|
specs: specs,
|
|
103
182
|
dependency_graph: dependency_graph.transform_values(&:uniq),
|
|
104
|
-
|
|
183
|
+
dependency_requirements: dependency_requirements,
|
|
184
|
+
direct_dependencies: direct_dependencies.uniq,
|
|
185
|
+
direct_dependency_requirements: direct_dependency_requirements,
|
|
186
|
+
spec_sources: spec_sources
|
|
105
187
|
}
|
|
106
188
|
end
|
|
107
189
|
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
require "open-uri"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Gemstar
|
|
6
|
+
class NpmMetadata
|
|
7
|
+
def initialize(package_name)
|
|
8
|
+
@gem_name = package_name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :gem_name
|
|
12
|
+
|
|
13
|
+
def cache_key
|
|
14
|
+
"npm-#{gem_name}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def meta(cache_only: false, force_refresh: false)
|
|
18
|
+
return @meta if !cache_only && defined?(@meta)
|
|
19
|
+
|
|
20
|
+
json = if cache_only
|
|
21
|
+
Cache.peek(cache_key)
|
|
22
|
+
else
|
|
23
|
+
url = "https://registry.npmjs.org/#{URI.encode_www_form_component(gem_name)}"
|
|
24
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
25
|
+
URI.open(url, read_timeout: 8).read
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
parsed = begin
|
|
30
|
+
JSON.parse(json) if json
|
|
31
|
+
rescue
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
normalized = normalize_meta(parsed)
|
|
36
|
+
@meta = normalized unless cache_only
|
|
37
|
+
normalized
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def repo_uri(cache_only: false, force_refresh: false)
|
|
41
|
+
resolved_meta = meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
42
|
+
return nil unless resolved_meta
|
|
43
|
+
|
|
44
|
+
return @repo_uri if !cache_only && defined?(@repo_uri)
|
|
45
|
+
|
|
46
|
+
repo = begin
|
|
47
|
+
uri = resolved_meta["source_code_uri"]
|
|
48
|
+
uri ||= resolved_meta["homepage_uri"] if resolved_meta["homepage_uri"].to_s.include?("github.com")
|
|
49
|
+
uri ||= ""
|
|
50
|
+
|
|
51
|
+
uri = uri.sub(/\Agit\+/, "")
|
|
52
|
+
uri = uri.sub("git://", "https://")
|
|
53
|
+
uri = uri.sub("http://", "https://")
|
|
54
|
+
uri = uri.gsub(/\.git$/, "")
|
|
55
|
+
|
|
56
|
+
if uri.include?("github.com")
|
|
57
|
+
uri = uri[%r{\Ahttps?://github\.com/[^/]+/[^/]+}] || uri
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
uri
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@repo_uri = repo unless cache_only
|
|
64
|
+
repo
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def changelog_sections(versions: nil, cache_only: false, force_refresh: false)
|
|
68
|
+
requested_versions = Array(versions).compact
|
|
69
|
+
changelog = Gemstar::ChangeLog.new(self)
|
|
70
|
+
if requested_versions.empty?
|
|
71
|
+
changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
72
|
+
else
|
|
73
|
+
changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def warm_cache(versions: nil)
|
|
78
|
+
meta
|
|
79
|
+
repo_uri
|
|
80
|
+
changelog_sections(versions: versions)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def discover_github_tag_sections?
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def github_tag_candidates(version)
|
|
88
|
+
raw = version.to_s
|
|
89
|
+
candidates = [raw, (raw.start_with?("v") ? raw : "v#{raw}")]
|
|
90
|
+
|
|
91
|
+
package_name = gem_name.to_s
|
|
92
|
+
unless package_name.empty?
|
|
93
|
+
candidates << "#{package_name}@#{raw}"
|
|
94
|
+
candidates << "#{package_name}@v#{raw}" unless raw.start_with?("v")
|
|
95
|
+
|
|
96
|
+
short_name = package_name.split("/").last
|
|
97
|
+
if short_name && short_name != package_name
|
|
98
|
+
candidates << "#{short_name}@#{raw}"
|
|
99
|
+
candidates << "#{short_name}@v#{raw}" unless raw.start_with?("v")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
candidates.uniq
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def github_tag_matches?(tag_name)
|
|
107
|
+
decoded = URI.decode_www_form_component(tag_name.to_s.split("?").first.to_s)
|
|
108
|
+
package_name = gem_name.to_s
|
|
109
|
+
return true if package_name.empty?
|
|
110
|
+
|
|
111
|
+
if decoded.include?("@")
|
|
112
|
+
prefix = decoded.sub(/@v?\d[\w.\-]*\z/i, "")
|
|
113
|
+
return true if prefix == package_name
|
|
114
|
+
|
|
115
|
+
short_name = package_name.split("/").last
|
|
116
|
+
return true if short_name && prefix == short_name
|
|
117
|
+
|
|
118
|
+
return false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def normalize_meta(parsed)
|
|
127
|
+
return nil unless parsed.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
latest_version = parsed.dig("dist-tags", "latest").to_s
|
|
130
|
+
version_meta = parsed.dig("versions", latest_version)
|
|
131
|
+
source_code_uri = repository_url(version_meta || parsed)
|
|
132
|
+
homepage_uri = homepage_url(version_meta || parsed)
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
"name" => parsed["name"],
|
|
136
|
+
"version" => latest_version.empty? ? nil : latest_version,
|
|
137
|
+
"info" => (version_meta || parsed)["description"],
|
|
138
|
+
"homepage_uri" => homepage_uri,
|
|
139
|
+
"source_code_uri" => source_code_uri,
|
|
140
|
+
"project_uri" => "https://www.npmjs.com/package/#{parsed["name"]}",
|
|
141
|
+
"documentation_uri" => homepage_uri
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def repository_url(meta)
|
|
146
|
+
repository = meta["repository"]
|
|
147
|
+
case repository
|
|
148
|
+
when Hash
|
|
149
|
+
repository["url"]
|
|
150
|
+
when String
|
|
151
|
+
repository
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def homepage_url(meta)
|
|
156
|
+
meta["homepage"]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/gemstar/outputs/html.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "basic"
|
|
4
4
|
require "pathname"
|
|
5
|
+
require "cgi"
|
|
5
6
|
require "kramdown"
|
|
6
7
|
begin
|
|
7
8
|
require "kramdown-parser-gfm"
|
|
@@ -14,7 +15,7 @@ module Gemstar
|
|
|
14
15
|
class HTML < Basic
|
|
15
16
|
def render_diff(diff_command)
|
|
16
17
|
body = diff_command.updates.sort.map do |gem_name, info|
|
|
17
|
-
icon = info
|
|
18
|
+
icon = icon_for(info)
|
|
18
19
|
tooltip = info[:description] ? "title=\"#{info[:description].gsub('"', """)}\"" : ""
|
|
19
20
|
link = "<a href=\"#{info[:homepage_url]}\" #{tooltip} target=\"_blank\">#{gem_name}</a>"
|
|
20
21
|
html = if info[:sections]
|
|
@@ -38,14 +39,14 @@ module Gemstar
|
|
|
38
39
|
|
|
39
40
|
<<~HTML
|
|
40
41
|
<section>
|
|
41
|
-
<h2>#{icon} #{link}: #{info
|
|
42
|
+
<h2>#{icon} #{link}: #{version_label(info)}</h2>
|
|
42
43
|
#{"<p><a href='#{info[:release_page]}' target='_blank'>View all GitHub release notes</a></p>" if info[:release_page]}
|
|
43
44
|
#{html}
|
|
44
45
|
</section>
|
|
45
46
|
HTML
|
|
46
47
|
end.join("\n")
|
|
47
48
|
|
|
48
|
-
project_name =
|
|
49
|
+
project_name = diff_command.project_name
|
|
49
50
|
|
|
50
51
|
<<~HTML
|
|
51
52
|
<!DOCTYPE html>
|
|
@@ -63,13 +64,61 @@ module Gemstar
|
|
|
63
64
|
</style>
|
|
64
65
|
</head>
|
|
65
66
|
<body>
|
|
66
|
-
<h1>#{project_name}:
|
|
67
|
+
<h1>#{project_name}: Package Updates</h1>
|
|
67
68
|
<p><i>Showing changes from #{diff_command.from} to #{diff_command.to || "now"}, generated on #{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}.</i></p>
|
|
69
|
+
#{range_details(diff_command)}
|
|
68
70
|
#{body}
|
|
71
|
+
#{considered_commits(diff_command)}
|
|
69
72
|
</body>
|
|
70
73
|
</html>
|
|
71
74
|
HTML
|
|
72
75
|
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def icon_for(info)
|
|
80
|
+
return "📦" if info[:package_scope] == "js"
|
|
81
|
+
return "🐙" if info[:homepage_url]&.include?("github.com")
|
|
82
|
+
|
|
83
|
+
"💎"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def version_label(info)
|
|
87
|
+
info[:version_label] || "#{info[:old] || "new"} → #{info[:new]}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def range_details(diff_command)
|
|
91
|
+
return "" unless diff_command.since
|
|
92
|
+
|
|
93
|
+
cutoff = diff_command.format_commit(diff_command.since_cutoff_commit, fallback_revision: diff_command.from)
|
|
94
|
+
<<~HTML
|
|
95
|
+
<section>
|
|
96
|
+
<h2>Diff Range</h2>
|
|
97
|
+
<p>Since cutoff <code>#{h(diff_command.since)}</code> resolved to #{h(cutoff)}.</p>
|
|
98
|
+
</section>
|
|
99
|
+
HTML
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def considered_commits(diff_command)
|
|
103
|
+
commits = Array(diff_command.considered_commits)
|
|
104
|
+
items = commits.map do |commit|
|
|
105
|
+
%(<li><code>#{h(commit[:short_sha] || commit[:id])}</code> #{h(commit[:authored_at])} #{h(commit[:subject])}</li>)
|
|
106
|
+
end.join("\n")
|
|
107
|
+
items = "<li>No commits found in this range.</li>" if items.empty?
|
|
108
|
+
|
|
109
|
+
<<~HTML
|
|
110
|
+
<section>
|
|
111
|
+
<h2>Commits Considered</h2>
|
|
112
|
+
<ul>
|
|
113
|
+
#{items}
|
|
114
|
+
</ul>
|
|
115
|
+
</section>
|
|
116
|
+
HTML
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def h(value)
|
|
120
|
+
CGI.escapeHTML(value.to_s)
|
|
121
|
+
end
|
|
73
122
|
end
|
|
74
123
|
end
|
|
75
124
|
end
|
|
@@ -9,17 +9,21 @@ module Gemstar
|
|
|
9
9
|
module Outputs
|
|
10
10
|
class Markdown < Basic
|
|
11
11
|
def render_diff(diff_command)
|
|
12
|
-
project_name =
|
|
12
|
+
project_name = diff_command.project_name
|
|
13
13
|
body = diff_command.updates.sort.map do |gem_name, info|
|
|
14
14
|
render_entry(gem_name, info)
|
|
15
15
|
end.join("\n\n---\n\n")
|
|
16
16
|
|
|
17
17
|
<<~MARKDOWN
|
|
18
|
-
# #{project_name}:
|
|
18
|
+
# #{project_name}: Package Updates
|
|
19
19
|
|
|
20
20
|
_Showing changes from #{diff_command.from} to #{diff_command.to || "now"}, generated on #{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}._
|
|
21
21
|
|
|
22
|
+
#{range_details(diff_command)}
|
|
23
|
+
|
|
22
24
|
#{body}
|
|
25
|
+
|
|
26
|
+
#{considered_commits(diff_command)}
|
|
23
27
|
MARKDOWN
|
|
24
28
|
end
|
|
25
29
|
|
|
@@ -35,7 +39,7 @@ module Gemstar
|
|
|
35
39
|
parts = []
|
|
36
40
|
parts << title
|
|
37
41
|
parts << ""
|
|
38
|
-
parts << "*#{info[:old] || "new"} → #{info[:new]}*"
|
|
42
|
+
parts << "*#{info[:version_label] || "#{info[:old] || "new"} → #{info[:new]}"}*"
|
|
39
43
|
parts << ""
|
|
40
44
|
parts << info[:description].to_s unless info[:description].to_s.empty?
|
|
41
45
|
parts << "" unless info[:description].to_s.empty?
|
|
@@ -53,6 +57,28 @@ module Gemstar
|
|
|
53
57
|
parts.join("\n").strip
|
|
54
58
|
end
|
|
55
59
|
|
|
60
|
+
def range_details(diff_command)
|
|
61
|
+
return "" unless diff_command.since
|
|
62
|
+
|
|
63
|
+
cutoff = diff_command.format_commit(diff_command.since_cutoff_commit, fallback_revision: diff_command.from)
|
|
64
|
+
"## Diff Range\n\nSince cutoff `#{diff_command.since}` resolved to #{cutoff}."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def considered_commits(diff_command)
|
|
68
|
+
commits = Array(diff_command.considered_commits)
|
|
69
|
+
lines = ["## Commits Considered", ""]
|
|
70
|
+
|
|
71
|
+
if commits.empty?
|
|
72
|
+
lines << "No commits found in this range."
|
|
73
|
+
else
|
|
74
|
+
lines.concat(commits.map do |commit|
|
|
75
|
+
"- `#{commit[:short_sha] || commit[:id]}` #{commit[:authored_at]} #{commit[:subject]}"
|
|
76
|
+
end)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
def link_lines(info)
|
|
57
83
|
lines = []
|
|
58
84
|
lines << "[Compare changes](#{info[:compare_url]})" if info[:compare_url]
|