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.
@@ -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
@@ -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
- gemfile_path = if File.directory?(expanded_input)
13
- File.join(expanded_input, "Gemfile")
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 Gemfile found for #{input}" unless File.file?(gemfile_path)
19
- raise ArgumentError, "#{gemfile_path} is not a Gemfile" unless File.basename(gemfile_path) == "Gemfile"
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
- new(gemfile_path)
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(gemfile_path)
25
- @gemfile_path = File.expand_path(gemfile_path)
26
- @directory = File.dirname(@gemfile_path)
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 revision_history(limit: 20)
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: 20)
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: 20)
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
- default_changed_lockfile_revision_id ||
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: 20)
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
- @gem_states_cache[cache_key] = (from_specs.keys | to_specs.keys).map do |gem_name|
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
- bundle_origins = to_lockfile&.origins_for(gem_name) || []
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.sort_by { |gem| gem[:name] }
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 gem_added_on(gem_name, revision_id: "worktree")
132
- cache_key = [gem_name, revision_id]
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
- target_lockfile = lockfile_for_revision(revision_id)
137
- return @gem_added_on_cache[cache_key] = nil unless target_lockfile&.specs&.key?(gem_name)
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
- relative_path = git_repo.relative_path(lockfile_path)
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
- lockfile = lockfile_for_revision(revision[:id])
144
- lockfile&.specs&.key?(gem_name)
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 default_changed_lockfile_revision_id
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
- lockfile_revision_history(limit: 20).find do |revision|
350
+ revision_history(limit: REVISION_HISTORY_LIMIT).find do |revision|
167
351
  revision_lockfile = lockfile_for_revision(revision[:id])
168
- revision_lockfile && revision_lockfile.specs != current_specs
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: 20, reverse: false)
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?(lockfile_path)
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(lockfile_path).strftime("%Y-%m-%d"),
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