gemstar 1.0.4 → 1.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 +32 -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 +319 -36
- data/lib/gemstar/cli.rb +5 -1
- data/lib/gemstar/commands/diff.rb +197 -31
- data/lib/gemstar/commands/server.rb +93 -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_hub.rb +3 -2
- data/lib/gemstar/git_repo.rb +41 -3
- data/lib/gemstar/importmap_file.rb +193 -0
- 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 +319 -35
- data/lib/gemstar/ruby_gems_metadata.rb +113 -2
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +422 -68
- data/lib/gemstar/web/templates/app.css +48 -0
- data/lib/gemstar/web/templates/app.js.erb +51 -16
- data/lib/gemstar/web/templates/page.html.erb +2 -1
- data/lib/gemstar.rb +3 -0
- metadata +6 -1
|
@@ -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
|
|
@@ -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]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Gemstar
|
|
4
|
+
class PackageLockFile
|
|
5
|
+
def initialize(path: nil, content: nil)
|
|
6
|
+
@path = path
|
|
7
|
+
parsed = parse_content(content || File.read(path))
|
|
8
|
+
@specs = parsed[:specs]
|
|
9
|
+
@spec_sources = parsed[:spec_sources]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :specs
|
|
13
|
+
attr_reader :spec_sources
|
|
14
|
+
|
|
15
|
+
def source_for(name)
|
|
16
|
+
spec_sources[name]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_content(content)
|
|
22
|
+
parsed = JSON.parse(content)
|
|
23
|
+
specs = {}
|
|
24
|
+
spec_sources = {}
|
|
25
|
+
|
|
26
|
+
if parsed["packages"].is_a?(Hash)
|
|
27
|
+
parse_packages_map(parsed["packages"], specs, spec_sources)
|
|
28
|
+
elsif parsed["dependencies"].is_a?(Hash)
|
|
29
|
+
parse_dependencies_hash(parsed["dependencies"], specs, spec_sources)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
specs: specs,
|
|
34
|
+
spec_sources: spec_sources
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_packages_map(packages, specs, spec_sources)
|
|
39
|
+
packages.each do |path, package|
|
|
40
|
+
next if path.to_s.empty?
|
|
41
|
+
|
|
42
|
+
name = package["name"] || package_name_from_path(path)
|
|
43
|
+
version = package["version"]
|
|
44
|
+
next if name.to_s.empty? || version.to_s.empty?
|
|
45
|
+
|
|
46
|
+
specs[name] = version
|
|
47
|
+
spec_sources[name] = {
|
|
48
|
+
type: :npm,
|
|
49
|
+
remote: package["resolved"],
|
|
50
|
+
integrity: package["integrity"],
|
|
51
|
+
registry_url: "https://www.npmjs.com/package/#{name}"
|
|
52
|
+
}.compact
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_dependencies_hash(dependencies, specs, spec_sources)
|
|
57
|
+
dependencies.each do |name, package|
|
|
58
|
+
version = package["version"]
|
|
59
|
+
next if name.to_s.empty? || version.to_s.empty?
|
|
60
|
+
|
|
61
|
+
specs[name] = version
|
|
62
|
+
spec_sources[name] = {
|
|
63
|
+
type: :npm,
|
|
64
|
+
remote: package["resolved"],
|
|
65
|
+
integrity: package["integrity"],
|
|
66
|
+
registry_url: "https://www.npmjs.com/package/#{name}"
|
|
67
|
+
}.compact
|
|
68
|
+
|
|
69
|
+
child_dependencies = package["dependencies"]
|
|
70
|
+
parse_dependencies_hash(child_dependencies, specs, spec_sources) if child_dependencies.is_a?(Hash)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def package_name_from_path(path)
|
|
75
|
+
value = path.to_s
|
|
76
|
+
return nil if value.empty?
|
|
77
|
+
|
|
78
|
+
segments = value.split("/").reject(&:empty?)
|
|
79
|
+
package_segments = []
|
|
80
|
+
index = 0
|
|
81
|
+
while index < segments.length
|
|
82
|
+
if segments[index] == "node_modules"
|
|
83
|
+
index += 1
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if segments[index].start_with?("@") && segments[index + 1]
|
|
88
|
+
package_segments = [segments[index], segments[index + 1]]
|
|
89
|
+
index += 2
|
|
90
|
+
else
|
|
91
|
+
package_segments = [segments[index]]
|
|
92
|
+
index += 1
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return nil if package_segments.empty?
|
|
97
|
+
|
|
98
|
+
package_segments.join("/")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|