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,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
|
data/lib/gemstar/project.rb
CHANGED
|
@@ -2,31 +2,57 @@ require "time"
|
|
|
2
2
|
|
|
3
3
|
module Gemstar
|
|
4
4
|
class Project
|
|
5
|
+
REVISION_HISTORY_LIMIT = 100
|
|
6
|
+
|
|
5
7
|
attr_reader :directory
|
|
6
8
|
attr_reader :gemfile_path
|
|
7
9
|
attr_reader :lockfile_path
|
|
10
|
+
attr_reader :importmap_path
|
|
11
|
+
attr_reader :package_json_path
|
|
12
|
+
attr_reader :package_lock_path
|
|
8
13
|
attr_reader :name
|
|
9
14
|
|
|
10
15
|
def self.from_cli_argument(input)
|
|
11
16
|
expanded_input = File.expand_path(input)
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
if File.directory?(expanded_input)
|
|
18
|
+
directory = expanded_input
|
|
14
19
|
else
|
|
15
|
-
expanded_input
|
|
20
|
+
basename = File.basename(expanded_input)
|
|
21
|
+
directory =
|
|
22
|
+
case basename
|
|
23
|
+
when "Gemfile", "package.json", "package-lock.json"
|
|
24
|
+
File.dirname(expanded_input)
|
|
25
|
+
when "importmap.rb"
|
|
26
|
+
File.dirname(File.dirname(expanded_input))
|
|
27
|
+
else
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
16
30
|
end
|
|
17
31
|
|
|
18
|
-
raise ArgumentError, "No
|
|
19
|
-
raise ArgumentError, "
|
|
32
|
+
raise ArgumentError, "No supported project files found for #{input}" unless directory
|
|
33
|
+
raise ArgumentError, "No supported project files found for #{input}" unless supported_project_directory?(directory)
|
|
34
|
+
|
|
35
|
+
new(directory: directory)
|
|
36
|
+
end
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
def self.supported_project_directory?(directory)
|
|
39
|
+
File.file?(File.join(directory, "Gemfile")) ||
|
|
40
|
+
File.file?(File.join(directory, "config", "importmap.rb")) ||
|
|
41
|
+
File.file?(File.join(directory, "package.json")) ||
|
|
42
|
+
File.file?(File.join(directory, "package-lock.json"))
|
|
22
43
|
end
|
|
23
44
|
|
|
24
|
-
def initialize(
|
|
25
|
-
@
|
|
26
|
-
@
|
|
45
|
+
def initialize(directory:)
|
|
46
|
+
@directory = File.expand_path(directory)
|
|
47
|
+
@gemfile_path = File.join(@directory, "Gemfile")
|
|
27
48
|
@lockfile_path = File.join(@directory, "Gemfile.lock")
|
|
49
|
+
@importmap_path = File.join(@directory, "config", "importmap.rb")
|
|
50
|
+
@package_json_path = File.join(@directory, "package.json")
|
|
51
|
+
@package_lock_path = File.join(@directory, "package-lock.json")
|
|
28
52
|
@name = File.basename(@directory)
|
|
29
53
|
@lockfile_cache = {}
|
|
54
|
+
@importmap_cache = {}
|
|
55
|
+
@package_lock_cache = {}
|
|
30
56
|
@gem_states_cache = {}
|
|
31
57
|
@gem_added_on_cache = {}
|
|
32
58
|
@history_cache = {}
|
|
@@ -50,11 +76,39 @@ module Gemstar
|
|
|
50
76
|
@current_lockfile ||= Gemstar::LockFile.new(path: lockfile_path)
|
|
51
77
|
end
|
|
52
78
|
|
|
53
|
-
def
|
|
79
|
+
def gemfile?
|
|
80
|
+
File.file?(gemfile_path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def importmap?
|
|
84
|
+
File.file?(importmap_path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def current_importmap
|
|
88
|
+
return nil unless importmap?
|
|
89
|
+
|
|
90
|
+
@current_importmap ||= Gemstar::ImportmapFile.new(path: importmap_path, vendor_reader: importmap_vendor_reader("worktree"))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def package_lock?
|
|
94
|
+
File.file?(package_lock_path)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def package_json?
|
|
98
|
+
File.file?(package_json_path)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def current_package_lock
|
|
102
|
+
return nil unless package_lock?
|
|
103
|
+
|
|
104
|
+
@current_package_lock ||= Gemstar::PackageLockFile.new(path: package_lock_path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def revision_history(limit: REVISION_HISTORY_LIMIT)
|
|
54
108
|
history_for_paths(tracked_git_paths, limit: limit)
|
|
55
109
|
end
|
|
56
110
|
|
|
57
|
-
def lockfile_revision_history(limit:
|
|
111
|
+
def lockfile_revision_history(limit: REVISION_HISTORY_LIMIT)
|
|
58
112
|
return [] unless lockfile?
|
|
59
113
|
|
|
60
114
|
relative_path = git_repo.relative_path(lockfile_path)
|
|
@@ -63,7 +117,9 @@ module Gemstar
|
|
|
63
117
|
history_for_paths([relative_path], limit: limit)
|
|
64
118
|
end
|
|
65
119
|
|
|
66
|
-
def gemfile_revision_history(limit:
|
|
120
|
+
def gemfile_revision_history(limit: REVISION_HISTORY_LIMIT)
|
|
121
|
+
return [] unless gemfile?
|
|
122
|
+
|
|
67
123
|
relative_path = git_repo.relative_path(gemfile_path)
|
|
68
124
|
return [] if relative_path.nil?
|
|
69
125
|
|
|
@@ -71,12 +127,12 @@ module Gemstar
|
|
|
71
127
|
end
|
|
72
128
|
|
|
73
129
|
def default_from_revision_id
|
|
74
|
-
|
|
130
|
+
default_changed_revision_id ||
|
|
75
131
|
gemfile_revision_history(limit: 1).first&.dig(:id) ||
|
|
76
132
|
"worktree"
|
|
77
133
|
end
|
|
78
134
|
|
|
79
|
-
def revision_options(limit:
|
|
135
|
+
def revision_options(limit: REVISION_HISTORY_LIMIT)
|
|
80
136
|
[{ id: "worktree", label: "Worktree", description: "Current Gemfile.lock in the working tree" }] +
|
|
81
137
|
revision_history(limit: limit).map do |revision|
|
|
82
138
|
{
|
|
@@ -87,6 +143,26 @@ module Gemstar
|
|
|
87
143
|
end
|
|
88
144
|
end
|
|
89
145
|
|
|
146
|
+
def package_scopes
|
|
147
|
+
scopes = []
|
|
148
|
+
scopes << :gems if gemfile? || lockfile?
|
|
149
|
+
scopes << :js if importmap? || package_lock? || package_json?
|
|
150
|
+
scopes
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def package_scope_options
|
|
154
|
+
package_scopes.map do |scope|
|
|
155
|
+
{
|
|
156
|
+
id: package_scope_id(scope),
|
|
157
|
+
label: package_scope_label(scope)
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def package_collection_label
|
|
163
|
+
package_scopes == [:gems] ? "Gems" : "Packages"
|
|
164
|
+
end
|
|
165
|
+
|
|
90
166
|
def lockfile_for_revision(revision_id)
|
|
91
167
|
cache_key = revision_id || "worktree"
|
|
92
168
|
return @lockfile_cache[cache_key] if @lockfile_cache.key?(cache_key)
|
|
@@ -102,6 +178,36 @@ module Gemstar
|
|
|
102
178
|
@lockfile_cache[cache_key] = Gemstar::LockFile.new(content: content)
|
|
103
179
|
end
|
|
104
180
|
|
|
181
|
+
def importmap_for_revision(revision_id)
|
|
182
|
+
cache_key = revision_id || "worktree"
|
|
183
|
+
return @importmap_cache[cache_key] if @importmap_cache.key?(cache_key)
|
|
184
|
+
return @importmap_cache[cache_key] = current_importmap if revision_id.nil? || revision_id == "worktree"
|
|
185
|
+
return nil unless importmap?
|
|
186
|
+
|
|
187
|
+
relative_importmap_path = git_repo.relative_path(importmap_path)
|
|
188
|
+
return nil if relative_importmap_path.nil?
|
|
189
|
+
|
|
190
|
+
content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_importmap_path}"])
|
|
191
|
+
return nil if content.nil? || content.empty?
|
|
192
|
+
|
|
193
|
+
@importmap_cache[cache_key] = Gemstar::ImportmapFile.new(content: content, vendor_reader: importmap_vendor_reader(revision_id))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def package_lock_for_revision(revision_id)
|
|
197
|
+
cache_key = revision_id || "worktree"
|
|
198
|
+
return @package_lock_cache[cache_key] if @package_lock_cache.key?(cache_key)
|
|
199
|
+
return @package_lock_cache[cache_key] = current_package_lock if revision_id.nil? || revision_id == "worktree"
|
|
200
|
+
return nil unless package_lock?
|
|
201
|
+
|
|
202
|
+
relative_package_lock_path = git_repo.relative_path(package_lock_path)
|
|
203
|
+
return nil if relative_package_lock_path.nil?
|
|
204
|
+
|
|
205
|
+
content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_package_lock_path}"])
|
|
206
|
+
return nil if content.nil? || content.empty?
|
|
207
|
+
|
|
208
|
+
@package_lock_cache[cache_key] = Gemstar::PackageLockFile.new(content: content)
|
|
209
|
+
end
|
|
210
|
+
|
|
105
211
|
def gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree")
|
|
106
212
|
cache_key = [from_revision_id, to_revision_id]
|
|
107
213
|
return @gem_states_cache[cache_key] if @gem_states_cache.key?(cache_key)
|
|
@@ -111,40 +217,118 @@ module Gemstar
|
|
|
111
217
|
from_specs = from_lockfile&.specs || {}
|
|
112
218
|
to_specs = to_lockfile&.specs || {}
|
|
113
219
|
|
|
114
|
-
|
|
220
|
+
gem_states = (from_specs.keys | to_specs.keys).map do |gem_name|
|
|
115
221
|
old_version = from_specs[gem_name]
|
|
116
222
|
new_version = to_specs[gem_name]
|
|
117
|
-
|
|
223
|
+
effective_lockfile = new_version ? to_lockfile : from_lockfile
|
|
224
|
+
bundle_origins = effective_lockfile&.origins_for(gem_name) || []
|
|
118
225
|
|
|
119
226
|
{
|
|
120
227
|
name: gem_name,
|
|
228
|
+
package_scope: "gems",
|
|
229
|
+
package_type_label: "Gem",
|
|
121
230
|
old_version: old_version,
|
|
122
231
|
new_version: new_version,
|
|
123
232
|
status: gem_status(old_version, new_version),
|
|
124
233
|
version_label: version_label(old_version, new_version),
|
|
234
|
+
platform: effective_lockfile&.platform_for(gem_name),
|
|
235
|
+
source: effective_lockfile&.source_for(gem_name),
|
|
125
236
|
bundle_origins: bundle_origins,
|
|
126
237
|
bundle_origin_labels: bundle_origin_labels(bundle_origins)
|
|
127
238
|
}
|
|
128
|
-
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
from_importmap = importmap_for_revision(from_revision_id)
|
|
242
|
+
to_importmap = importmap_for_revision(to_revision_id)
|
|
243
|
+
from_js_specs = from_importmap&.specs || {}
|
|
244
|
+
to_js_specs = to_importmap&.specs || {}
|
|
245
|
+
js_states = (from_js_specs.keys | to_js_specs.keys).map do |package_name|
|
|
246
|
+
old_target = from_js_specs[package_name]
|
|
247
|
+
new_target = to_js_specs[package_name]
|
|
248
|
+
old_source = from_importmap&.source_for(package_name) || {}
|
|
249
|
+
new_source = to_importmap&.source_for(package_name) || {}
|
|
250
|
+
old_source = enrich_importmap_source(old_source, from_lockfile)
|
|
251
|
+
new_source = enrich_importmap_source(new_source, to_lockfile)
|
|
252
|
+
effective_source = new_target ? new_source : old_source
|
|
253
|
+
old_package_version = js_package_version(old_source)
|
|
254
|
+
new_package_version = js_package_version(new_source)
|
|
255
|
+
comparison_old = old_package_version || old_target
|
|
256
|
+
comparison_new = new_package_version || new_target
|
|
257
|
+
|
|
258
|
+
{
|
|
259
|
+
name: package_name,
|
|
260
|
+
package_scope: "js",
|
|
261
|
+
package_type_label: "JS",
|
|
262
|
+
package_source_file: :importmap,
|
|
263
|
+
old_version: old_package_version,
|
|
264
|
+
new_version: new_package_version,
|
|
265
|
+
raw_old_version: old_target,
|
|
266
|
+
raw_new_version: new_target,
|
|
267
|
+
status: gem_status(comparison_old, comparison_new),
|
|
268
|
+
version_label: js_version_label(old_target, new_target, old_source, new_source),
|
|
269
|
+
platform: nil,
|
|
270
|
+
source: effective_source,
|
|
271
|
+
bundle_origins: [],
|
|
272
|
+
bundle_origin_labels: []
|
|
273
|
+
}
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
from_package_lock = package_lock_for_revision(from_revision_id)
|
|
277
|
+
to_package_lock = package_lock_for_revision(to_revision_id)
|
|
278
|
+
from_npm_specs = from_package_lock&.specs || {}
|
|
279
|
+
to_npm_specs = to_package_lock&.specs || {}
|
|
280
|
+
npm_states = (from_npm_specs.keys | to_npm_specs.keys).map do |package_name|
|
|
281
|
+
old_version = from_npm_specs[package_name]
|
|
282
|
+
new_version = to_npm_specs[package_name]
|
|
283
|
+
effective_package_lock = new_version ? to_package_lock : from_package_lock
|
|
284
|
+
|
|
285
|
+
{
|
|
286
|
+
name: package_name,
|
|
287
|
+
package_scope: "js",
|
|
288
|
+
package_type_label: "JS",
|
|
289
|
+
package_source_file: :package_lock,
|
|
290
|
+
old_version: old_version,
|
|
291
|
+
new_version: new_version,
|
|
292
|
+
status: gem_status(old_version, new_version),
|
|
293
|
+
version_label: version_label(old_version, new_version),
|
|
294
|
+
platform: nil,
|
|
295
|
+
source: effective_package_lock&.source_for(package_name),
|
|
296
|
+
bundle_origins: [],
|
|
297
|
+
bundle_origin_labels: []
|
|
298
|
+
}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
@gem_states_cache[cache_key] = (gem_states + js_states + npm_states).sort_by { |gem| [gem[:name], gem[:package_scope], gem[:package_source_file].to_s] }
|
|
129
302
|
end
|
|
130
303
|
|
|
131
|
-
def
|
|
132
|
-
cache_key = [
|
|
304
|
+
def package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil)
|
|
305
|
+
cache_key = [package_name, package_scope, source_file, revision_id]
|
|
133
306
|
return @gem_added_on_cache[cache_key] if @gem_added_on_cache.key?(cache_key)
|
|
134
|
-
return nil unless lockfile?
|
|
135
307
|
|
|
136
|
-
|
|
137
|
-
|
|
308
|
+
tracked_file, reader =
|
|
309
|
+
if source_file == :importmap
|
|
310
|
+
[importmap_path, method(:importmap_for_revision)]
|
|
311
|
+
elsif source_file == :package_lock
|
|
312
|
+
[package_lock_path, method(:package_lock_for_revision)]
|
|
313
|
+
elsif package_scope == "js"
|
|
314
|
+
[importmap_path, method(:importmap_for_revision)]
|
|
315
|
+
else
|
|
316
|
+
[lockfile_path, method(:lockfile_for_revision)]
|
|
317
|
+
end
|
|
318
|
+
return @gem_added_on_cache[cache_key] = nil unless File.file?(tracked_file)
|
|
138
319
|
|
|
139
|
-
|
|
320
|
+
target_snapshot = reader.call(revision_id)
|
|
321
|
+
return @gem_added_on_cache[cache_key] = nil unless target_snapshot&.specs&.key?(package_name)
|
|
322
|
+
|
|
323
|
+
relative_path = git_repo.relative_path(tracked_file)
|
|
140
324
|
return @gem_added_on_cache[cache_key] = nil if relative_path.nil?
|
|
141
325
|
|
|
142
326
|
first_seen_revision = history_for_paths([relative_path], limit: nil, reverse: true).find do |revision|
|
|
143
|
-
|
|
144
|
-
|
|
327
|
+
snapshot = reader.call(revision[:id])
|
|
328
|
+
snapshot&.specs&.key?(package_name)
|
|
145
329
|
end
|
|
146
330
|
|
|
147
|
-
return @gem_added_on_cache[cache_key] = worktree_added_on_info if first_seen_revision.nil? && revision_id == "worktree"
|
|
331
|
+
return @gem_added_on_cache[cache_key] = worktree_added_on_info(tracked_file) if first_seen_revision.nil? && revision_id == "worktree"
|
|
148
332
|
return @gem_added_on_cache[cache_key] = nil unless first_seen_revision
|
|
149
333
|
|
|
150
334
|
@gem_added_on_cache[cache_key] = {
|
|
@@ -158,18 +342,22 @@ module Gemstar
|
|
|
158
342
|
|
|
159
343
|
private
|
|
160
344
|
|
|
161
|
-
def
|
|
162
|
-
return nil unless lockfile?
|
|
163
|
-
|
|
345
|
+
def default_changed_revision_id
|
|
164
346
|
current_specs = current_lockfile&.specs || {}
|
|
347
|
+
current_importmap_specs = current_importmap&.specs || {}
|
|
348
|
+
current_package_lock_specs = current_package_lock&.specs || {}
|
|
165
349
|
|
|
166
|
-
|
|
350
|
+
revision_history(limit: REVISION_HISTORY_LIMIT).find do |revision|
|
|
167
351
|
revision_lockfile = lockfile_for_revision(revision[:id])
|
|
168
|
-
|
|
352
|
+
revision_importmap = importmap_for_revision(revision[:id])
|
|
353
|
+
revision_package_lock = package_lock_for_revision(revision[:id])
|
|
354
|
+
(revision_lockfile && revision_lockfile.specs != current_specs) ||
|
|
355
|
+
(revision_importmap && revision_importmap.specs != current_importmap_specs) ||
|
|
356
|
+
(revision_package_lock && revision_package_lock.specs != current_package_lock_specs)
|
|
169
357
|
end&.dig(:id)
|
|
170
358
|
end
|
|
171
359
|
|
|
172
|
-
def history_for_paths(paths, limit:
|
|
360
|
+
def history_for_paths(paths, limit: REVISION_HISTORY_LIMIT, reverse: false)
|
|
173
361
|
return [] if git_root.nil? || git_root.empty?
|
|
174
362
|
return [] if paths.empty?
|
|
175
363
|
|
|
@@ -196,13 +384,23 @@ module Gemstar
|
|
|
196
384
|
end
|
|
197
385
|
|
|
198
386
|
def tracked_git_paths
|
|
199
|
-
[gemfile_path, lockfile_path].filter_map do |path|
|
|
387
|
+
[gemfile_path, lockfile_path, importmap_path, package_json_path, package_lock_path, *importmap_vendor_paths].filter_map do |path|
|
|
200
388
|
next unless File.file?(path)
|
|
201
389
|
|
|
202
390
|
git_repo.relative_path(path)
|
|
203
391
|
end.uniq
|
|
204
392
|
end
|
|
205
393
|
|
|
394
|
+
def importmap_vendor_paths
|
|
395
|
+
return [] unless current_importmap
|
|
396
|
+
|
|
397
|
+
current_importmap.specs.values.filter_map do |target|
|
|
398
|
+
next unless target.to_s.end_with?(".js", ".mjs")
|
|
399
|
+
|
|
400
|
+
File.join(directory, "vendor", "javascript", target.to_s)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
206
404
|
def gem_status(old_version, new_version)
|
|
207
405
|
return :added if old_version.nil? && !new_version.nil?
|
|
208
406
|
return :removed if !old_version.nil? && new_version.nil?
|
|
@@ -223,6 +421,77 @@ module Gemstar
|
|
|
223
421
|
"#{old_version} → #{new_version}"
|
|
224
422
|
end
|
|
225
423
|
|
|
424
|
+
def importmap_version_label(old_target, new_target)
|
|
425
|
+
old_label = importmap_target_label(old_target)
|
|
426
|
+
new_label = importmap_target_label(new_target)
|
|
427
|
+
return "new → #{new_label}" if old_target.nil? && !new_target.nil?
|
|
428
|
+
return "#{old_label} → removed" if !old_target.nil? && new_target.nil?
|
|
429
|
+
return new_label.to_s if old_target == new_target
|
|
430
|
+
|
|
431
|
+
"#{old_label} → #{new_label}"
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def js_version_label(old_target, new_target, old_source, new_source)
|
|
435
|
+
old_label = js_version_label_part(old_target, old_source)
|
|
436
|
+
new_label = js_version_label_part(new_target, new_source)
|
|
437
|
+
return "new → #{new_label}" if old_target.nil? && !new_target.nil?
|
|
438
|
+
return "#{old_label} → removed" if !old_target.nil? && new_target.nil?
|
|
439
|
+
return new_label.to_s if old_label == new_label && old_target == new_target
|
|
440
|
+
return "#{new_label} (source changed)" if old_label == new_label
|
|
441
|
+
|
|
442
|
+
"#{old_label} → #{new_label}"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def js_version_label_part(target, source)
|
|
446
|
+
version = js_package_version(source)
|
|
447
|
+
return version if version && !version.empty?
|
|
448
|
+
|
|
449
|
+
requirement = source&.dig(:package_requirement)
|
|
450
|
+
return "range #{requirement}" if requirement && !requirement.empty?
|
|
451
|
+
|
|
452
|
+
importmap_target_label(target)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def js_package_version(source)
|
|
456
|
+
source && source[:package_version].to_s.empty? ? nil : source&.dig(:package_version)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def enrich_importmap_source(source, lockfile)
|
|
460
|
+
source = (source || {}).dup
|
|
461
|
+
provider_gem = source[:provider_gem]
|
|
462
|
+
return source if provider_gem.to_s.empty?
|
|
463
|
+
|
|
464
|
+
provider_version = lockfile&.specs&.[](provider_gem)
|
|
465
|
+
source[:provider_version] = provider_version unless provider_version.to_s.empty?
|
|
466
|
+
source[:package_version] ||= provider_version unless provider_version.to_s.empty?
|
|
467
|
+
source
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def importmap_target_label(target)
|
|
471
|
+
return "" if target.nil?
|
|
472
|
+
|
|
473
|
+
version = target.to_s[/@(\d+(?:\.\d+)*[\w.-]*)/, 1]
|
|
474
|
+
return version if version
|
|
475
|
+
|
|
476
|
+
target.to_s.sub(%r{\Ahttps?://}, "").slice(0, 36)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def importmap_vendor_reader(revision_id)
|
|
480
|
+
lambda do |target|
|
|
481
|
+
next nil unless target.to_s.end_with?(".js", ".mjs")
|
|
482
|
+
|
|
483
|
+
vendor_path = File.join(directory, "vendor", "javascript", target.to_s)
|
|
484
|
+
if revision_id.nil? || revision_id == "worktree"
|
|
485
|
+
File.file?(vendor_path) ? File.read(vendor_path) : nil
|
|
486
|
+
else
|
|
487
|
+
relative_path = git_repo.relative_path(vendor_path)
|
|
488
|
+
next nil if relative_path.nil?
|
|
489
|
+
|
|
490
|
+
git_repo.try_git_command(["show", "#{revision_id}:#{relative_path}"])
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
226
495
|
def compare_versions(left, right)
|
|
227
496
|
Gem::Version.new(left.to_s.gsub(/-[\w\-]+$/, "")) <=> Gem::Version.new(right.to_s.gsub(/-[\w\-]+$/, ""))
|
|
228
497
|
rescue ArgumentError
|
|
@@ -233,16 +502,17 @@ module Gemstar
|
|
|
233
502
|
Array(origins).map do |origin|
|
|
234
503
|
next "Gemfile" if origin[:type] == :direct
|
|
235
504
|
|
|
236
|
-
["Gemfile", *origin[:path]].join(" → ")
|
|
505
|
+
label = ["Gemfile", *origin[:path]].join(" → ")
|
|
506
|
+
origin[:requirement] ? "#{label} (#{origin[:requirement]})" : label
|
|
237
507
|
end.compact.uniq
|
|
238
508
|
end
|
|
239
509
|
|
|
240
|
-
def worktree_added_on_info
|
|
241
|
-
return nil unless File.file?(
|
|
510
|
+
def worktree_added_on_info(path)
|
|
511
|
+
return nil unless File.file?(path)
|
|
242
512
|
|
|
243
513
|
{
|
|
244
514
|
project_name: name,
|
|
245
|
-
date: File.mtime(
|
|
515
|
+
date: File.mtime(path).strftime("%Y-%m-%d"),
|
|
246
516
|
revision: "Worktree",
|
|
247
517
|
revision_url: nil,
|
|
248
518
|
worktree: true
|
|
@@ -255,5 +525,23 @@ module Gemstar
|
|
|
255
525
|
|
|
256
526
|
"#{repo_url}/commit/#{full_sha}"
|
|
257
527
|
end
|
|
528
|
+
|
|
529
|
+
def package_scope_id(scope)
|
|
530
|
+
case scope
|
|
531
|
+
when :gems then "gems"
|
|
532
|
+
when :js then "js"
|
|
533
|
+
when :python then "python"
|
|
534
|
+
else scope.to_s
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def package_scope_label(scope)
|
|
539
|
+
case scope
|
|
540
|
+
when :gems then "Gems"
|
|
541
|
+
when :js then "JS"
|
|
542
|
+
when :python then "Python"
|
|
543
|
+
else scope.to_s.capitalize
|
|
544
|
+
end
|
|
545
|
+
end
|
|
258
546
|
end
|
|
259
547
|
end
|