Class: Gemstar::Project

Inherits:
Object
  • Object
show all
Defined in:
lib/gemstar/project.rb

Constant Summary collapse

REVISION_HISTORY_LIMIT =
100

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(directory:) ⇒ Project

Returns a new instance of Project.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/gemstar/project.rb', line 45

def initialize(directory:)
  @directory = File.expand_path(directory)
  @gemfile_path = File.join(@directory, "Gemfile")
  @lockfile_path = File.join(@directory, "Gemfile.lock")
  @importmap_path = File.join(@directory, "config", "importmap.rb")
  @package_json_path = File.join(@directory, "package.json")
  @package_lock_path = File.join(@directory, "package-lock.json")
  @name = File.basename(@directory)
  @lockfile_cache = {}
  @importmap_cache = {}
  @package_lock_cache = {}
  @gem_states_cache = {}
  @gem_added_on_cache = {}
  @history_cache = {}
end

Instance Attribute Details

#directoryObject (readonly)

Returns the value of attribute directory.



7
8
9
# File 'lib/gemstar/project.rb', line 7

def directory
  @directory
end

#gemfile_pathObject (readonly)

Returns the value of attribute gemfile_path.



8
9
10
# File 'lib/gemstar/project.rb', line 8

def gemfile_path
  @gemfile_path
end

#importmap_pathObject (readonly)

Returns the value of attribute importmap_path.



10
11
12
# File 'lib/gemstar/project.rb', line 10

def importmap_path
  @importmap_path
end

#lockfile_pathObject (readonly)

Returns the value of attribute lockfile_path.



9
10
11
# File 'lib/gemstar/project.rb', line 9

def lockfile_path
  @lockfile_path
end

#nameObject (readonly)

Returns the value of attribute name.



13
14
15
# File 'lib/gemstar/project.rb', line 13

def name
  @name
end

#package_json_pathObject (readonly)

Returns the value of attribute package_json_path.



11
12
13
# File 'lib/gemstar/project.rb', line 11

def package_json_path
  @package_json_path
end

#package_lock_pathObject (readonly)

Returns the value of attribute package_lock_path.



12
13
14
# File 'lib/gemstar/project.rb', line 12

def package_lock_path
  @package_lock_path
end

Class Method Details

.from_cli_argument(input) ⇒ Object

Raises:

  • (ArgumentError)


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/gemstar/project.rb', line 15

def self.from_cli_argument(input)
  expanded_input = File.expand_path(input)
  if File.directory?(expanded_input)
    directory = expanded_input
  else
    basename = File.basename(expanded_input)
    directory =
      case basename
      when "Gemfile", "package.json", "package-lock.json"
        File.dirname(expanded_input)
      when "importmap.rb"
        File.dirname(File.dirname(expanded_input))
      else
        nil
      end
  end

  raise ArgumentError, "No supported project files found for #{input}" unless directory
  raise ArgumentError, "No supported project files found for #{input}" unless supported_project_directory?(directory)

  new(directory: directory)
end

.supported_project_directory?(directory) ⇒ Boolean

Returns:

  • (Boolean)


38
39
40
41
42
43
# File 'lib/gemstar/project.rb', line 38

def self.supported_project_directory?(directory)
  File.file?(File.join(directory, "Gemfile")) ||
    File.file?(File.join(directory, "config", "importmap.rb")) ||
    File.file?(File.join(directory, "package.json")) ||
    File.file?(File.join(directory, "package-lock.json"))
end

Instance Method Details

#current_importmapObject



87
88
89
90
91
# File 'lib/gemstar/project.rb', line 87

def current_importmap
  return nil unless importmap?

  @current_importmap ||= Gemstar::ImportmapFile.new(path: importmap_path, vendor_reader: importmap_vendor_reader("worktree"))
end

#current_lockfileObject



73
74
75
76
77
# File 'lib/gemstar/project.rb', line 73

def current_lockfile
  return nil unless lockfile?

  @current_lockfile ||= Gemstar::LockFile.new(path: lockfile_path)
end

#current_package_lockObject



101
102
103
104
105
# File 'lib/gemstar/project.rb', line 101

def current_package_lock
  return nil unless package_lock?

  @current_package_lock ||= Gemstar::PackageLockFile.new(path: package_lock_path)
end

#default_from_revision_idObject



129
130
131
132
133
# File 'lib/gemstar/project.rb', line 129

def default_from_revision_id
  default_changed_revision_id ||
    gemfile_revision_history(limit: 1).first&.dig(:id) ||
    "worktree"
end

#gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree") ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/gemstar/project.rb', line 211

def gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree")
  cache_key = [from_revision_id, to_revision_id]
  return @gem_states_cache[cache_key] if @gem_states_cache.key?(cache_key)

  from_lockfile = lockfile_for_revision(from_revision_id)
  to_lockfile = lockfile_for_revision(to_revision_id)
  from_specs = from_lockfile&.specs || {}
  to_specs = to_lockfile&.specs || {}

  gem_states = (from_specs.keys | to_specs.keys).map do |gem_name|
    old_version = from_specs[gem_name]
    new_version = to_specs[gem_name]
    effective_lockfile = new_version ? to_lockfile : from_lockfile
    bundle_origins = effective_lockfile&.origins_for(gem_name) || []

    {
      name: gem_name,
      package_scope: "gems",
      package_type_label: "Gem",
      old_version: old_version,
      new_version: new_version,
      status: gem_status(old_version, new_version),
      version_label: version_label(old_version, new_version),
      platform: effective_lockfile&.platform_for(gem_name),
      source: effective_lockfile&.source_for(gem_name),
      bundle_origins: bundle_origins,
      bundle_origin_labels: bundle_origin_labels(bundle_origins)
    }
  end

  from_importmap = importmap_for_revision(from_revision_id)
  to_importmap = importmap_for_revision(to_revision_id)
  from_js_specs = from_importmap&.specs || {}
  to_js_specs = to_importmap&.specs || {}
  js_states = (from_js_specs.keys | to_js_specs.keys).map do |package_name|
    old_target = from_js_specs[package_name]
    new_target = to_js_specs[package_name]
    old_source = from_importmap&.source_for(package_name) || {}
    new_source = to_importmap&.source_for(package_name) || {}
    old_source = enrich_importmap_source(old_source, from_lockfile)
    new_source = enrich_importmap_source(new_source, to_lockfile)
    effective_source = new_target ? new_source : old_source
    old_package_version = js_package_version(old_source)
    new_package_version = js_package_version(new_source)
    comparison_old = old_package_version || old_target
    comparison_new = new_package_version || new_target

    {
      name: package_name,
      package_scope: "js",
      package_type_label: "JS",
      package_source_file: :importmap,
      old_version: old_package_version,
      new_version: new_package_version,
      raw_old_version: old_target,
      raw_new_version: new_target,
      status: gem_status(comparison_old, comparison_new),
      version_label: js_version_label(old_target, new_target, old_source, new_source),
      platform: nil,
      source: effective_source,
      bundle_origins: [],
      bundle_origin_labels: []
    }
  end

  from_package_lock = package_lock_for_revision(from_revision_id)
  to_package_lock = package_lock_for_revision(to_revision_id)
  from_npm_specs = from_package_lock&.specs || {}
  to_npm_specs = to_package_lock&.specs || {}
  npm_states = (from_npm_specs.keys | to_npm_specs.keys).map do |package_name|
    old_version = from_npm_specs[package_name]
    new_version = to_npm_specs[package_name]
    effective_package_lock = new_version ? to_package_lock : from_package_lock

    {
      name: package_name,
      package_scope: "js",
      package_type_label: "JS",
      package_source_file: :package_lock,
      old_version: old_version,
      new_version: new_version,
      status: gem_status(old_version, new_version),
      version_label: version_label(old_version, new_version),
      platform: nil,
      source: effective_package_lock&.source_for(package_name),
      bundle_origins: [],
      bundle_origin_labels: []
    }
  end

  @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] }
end

#gemfile?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/gemstar/project.rb', line 79

def gemfile?
  File.file?(gemfile_path)
end

#gemfile_revision_history(limit: REVISION_HISTORY_LIMIT) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/gemstar/project.rb', line 120

def gemfile_revision_history(limit: REVISION_HISTORY_LIMIT)
  return [] unless gemfile?

  relative_path = git_repo.relative_path(gemfile_path)
  return [] if relative_path.nil?

  history_for_paths([relative_path], limit: limit)
end

#git_repoObject



61
62
63
# File 'lib/gemstar/project.rb', line 61

def git_repo
  @git_repo ||= Gemstar::GitRepo.new(directory)
end

#git_rootObject



65
66
67
# File 'lib/gemstar/project.rb', line 65

def git_root
  git_repo.tree_root_directory
end

#importmap?Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/gemstar/project.rb', line 83

def importmap?
  File.file?(importmap_path)
end

#importmap_for_revision(revision_id) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/gemstar/project.rb', line 181

def importmap_for_revision(revision_id)
  cache_key = revision_id || "worktree"
  return @importmap_cache[cache_key] if @importmap_cache.key?(cache_key)
  return @importmap_cache[cache_key] = current_importmap if revision_id.nil? || revision_id == "worktree"
  return nil unless importmap?

  relative_importmap_path = git_repo.relative_path(importmap_path)
  return nil if relative_importmap_path.nil?

  content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_importmap_path}"])
  return nil if content.nil? || content.empty?

  @importmap_cache[cache_key] = Gemstar::ImportmapFile.new(content: content, vendor_reader: importmap_vendor_reader(revision_id))
end

#lockfile?Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/gemstar/project.rb', line 69

def lockfile?
  File.file?(lockfile_path)
end

#lockfile_for_revision(revision_id) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/gemstar/project.rb', line 166

def lockfile_for_revision(revision_id)
  cache_key = revision_id || "worktree"
  return @lockfile_cache[cache_key] if @lockfile_cache.key?(cache_key)
  return @lockfile_cache[cache_key] = current_lockfile if revision_id.nil? || revision_id == "worktree"
  return nil unless lockfile?

  relative_lockfile_path = git_repo.relative_path(lockfile_path)
  return nil if relative_lockfile_path.nil?

  content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_lockfile_path}"])
  return nil if content.nil? || content.empty?

  @lockfile_cache[cache_key] = Gemstar::LockFile.new(content: content)
end

#lockfile_revision_history(limit: REVISION_HISTORY_LIMIT) ⇒ Object



111
112
113
114
115
116
117
118
# File 'lib/gemstar/project.rb', line 111

def lockfile_revision_history(limit: REVISION_HISTORY_LIMIT)
  return [] unless lockfile?

  relative_path = git_repo.relative_path(lockfile_path)
  return [] if relative_path.nil?

  history_for_paths([relative_path], limit: limit)
end

#package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/gemstar/project.rb', line 304

def package_added_on(package_name, package_scope:, revision_id: "worktree", source_file: nil)
  cache_key = [package_name, package_scope, source_file, revision_id]
  return @gem_added_on_cache[cache_key] if @gem_added_on_cache.key?(cache_key)

  tracked_file, reader =
    if source_file == :importmap
      [importmap_path, method(:importmap_for_revision)]
    elsif source_file == :package_lock
      [package_lock_path, method(:package_lock_for_revision)]
    elsif package_scope == "js"
      [importmap_path, method(:importmap_for_revision)]
    else
      [lockfile_path, method(:lockfile_for_revision)]
    end
  return @gem_added_on_cache[cache_key] = nil unless File.file?(tracked_file)

  target_snapshot = reader.call(revision_id)
  return @gem_added_on_cache[cache_key] = nil unless target_snapshot&.specs&.key?(package_name)

  relative_path = git_repo.relative_path(tracked_file)
  return @gem_added_on_cache[cache_key] = nil if relative_path.nil?

  first_seen_revision = history_for_paths([relative_path], limit: nil, reverse: true).find do |revision|
    snapshot = reader.call(revision[:id])
    snapshot&.specs&.key?(package_name)
  end

  return @gem_added_on_cache[cache_key] = worktree_added_on_info(tracked_file) if first_seen_revision.nil? && revision_id == "worktree"
  return @gem_added_on_cache[cache_key] = nil unless first_seen_revision

  @gem_added_on_cache[cache_key] = {
    project_name: name,
    date: first_seen_revision[:authored_at].strftime("%Y-%m-%d"),
    revision: first_seen_revision[:short_sha],
    revision_url: revision_url(first_seen_revision[:id]),
    worktree: false
  }
end

#package_collection_labelObject



162
163
164
# File 'lib/gemstar/project.rb', line 162

def package_collection_label
  package_scopes == [:gems] ? "Gems" : "Packages"
end

#package_json?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/gemstar/project.rb', line 97

def package_json?
  File.file?(package_json_path)
end

#package_lock?Boolean

Returns:

  • (Boolean)


93
94
95
# File 'lib/gemstar/project.rb', line 93

def package_lock?
  File.file?(package_lock_path)
end

#package_lock_for_revision(revision_id) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/gemstar/project.rb', line 196

def package_lock_for_revision(revision_id)
  cache_key = revision_id || "worktree"
  return @package_lock_cache[cache_key] if @package_lock_cache.key?(cache_key)
  return @package_lock_cache[cache_key] = current_package_lock if revision_id.nil? || revision_id == "worktree"
  return nil unless package_lock?

  relative_package_lock_path = git_repo.relative_path(package_lock_path)
  return nil if relative_package_lock_path.nil?

  content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_package_lock_path}"])
  return nil if content.nil? || content.empty?

  @package_lock_cache[cache_key] = Gemstar::PackageLockFile.new(content: content)
end

#package_scope_optionsObject



153
154
155
156
157
158
159
160
# File 'lib/gemstar/project.rb', line 153

def package_scope_options
  package_scopes.map do |scope|
    {
      id: package_scope_id(scope),
      label: package_scope_label(scope)
    }
  end
end

#package_scopesObject



146
147
148
149
150
151
# File 'lib/gemstar/project.rb', line 146

def package_scopes
  scopes = []
  scopes << :gems if gemfile? || lockfile?
  scopes << :js if importmap? || package_lock? || package_json?
  scopes
end

#revision_history(limit: REVISION_HISTORY_LIMIT) ⇒ Object



107
108
109
# File 'lib/gemstar/project.rb', line 107

def revision_history(limit: REVISION_HISTORY_LIMIT)
  history_for_paths(tracked_git_paths, limit: limit)
end

#revision_options(limit: REVISION_HISTORY_LIMIT) ⇒ Object



135
136
137
138
139
140
141
142
143
144
# File 'lib/gemstar/project.rb', line 135

def revision_options(limit: REVISION_HISTORY_LIMIT)
  [{ id: "worktree", label: "Worktree", description: "Current Gemfile.lock in the working tree" }] +
    revision_history(limit: limit).map do |revision|
      {
        id: revision[:id],
        label: revision[:short_sha],
        description: "#{revision[:subject]} (#{revision[:authored_at].strftime("%Y-%m-%d %H:%M")})"
      }
    end
end