git-pkgs 0.6.2 → 0.8.0

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +28 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +25 -0
  5. data/Dockerfile +18 -0
  6. data/Formula/git-pkgs.rb +28 -0
  7. data/README.md +90 -6
  8. data/lib/git/pkgs/analyzer.rb +142 -10
  9. data/lib/git/pkgs/cli.rb +20 -8
  10. data/lib/git/pkgs/commands/blame.rb +0 -18
  11. data/lib/git/pkgs/commands/diff.rb +122 -5
  12. data/lib/git/pkgs/commands/diff_driver.rb +30 -4
  13. data/lib/git/pkgs/commands/init.rb +5 -0
  14. data/lib/git/pkgs/commands/licenses.rb +378 -0
  15. data/lib/git/pkgs/commands/list.rb +60 -15
  16. data/lib/git/pkgs/commands/outdated.rb +312 -0
  17. data/lib/git/pkgs/commands/show.rb +126 -3
  18. data/lib/git/pkgs/commands/stale.rb +6 -2
  19. data/lib/git/pkgs/commands/update.rb +3 -0
  20. data/lib/git/pkgs/commands/vulns/base.rb +358 -0
  21. data/lib/git/pkgs/commands/vulns/blame.rb +276 -0
  22. data/lib/git/pkgs/commands/vulns/diff.rb +173 -0
  23. data/lib/git/pkgs/commands/vulns/exposure.rb +418 -0
  24. data/lib/git/pkgs/commands/vulns/history.rb +345 -0
  25. data/lib/git/pkgs/commands/vulns/log.rb +218 -0
  26. data/lib/git/pkgs/commands/vulns/praise.rb +238 -0
  27. data/lib/git/pkgs/commands/vulns/scan.rb +231 -0
  28. data/lib/git/pkgs/commands/vulns/show.rb +216 -0
  29. data/lib/git/pkgs/commands/vulns/sync.rb +110 -0
  30. data/lib/git/pkgs/commands/vulns.rb +50 -0
  31. data/lib/git/pkgs/config.rb +8 -1
  32. data/lib/git/pkgs/database.rb +151 -5
  33. data/lib/git/pkgs/ecosystems.rb +83 -0
  34. data/lib/git/pkgs/ecosystems_client.rb +96 -0
  35. data/lib/git/pkgs/models/dependency_change.rb +8 -0
  36. data/lib/git/pkgs/models/dependency_snapshot.rb +8 -0
  37. data/lib/git/pkgs/models/package.rb +92 -0
  38. data/lib/git/pkgs/models/version.rb +27 -0
  39. data/lib/git/pkgs/models/vulnerability.rb +300 -0
  40. data/lib/git/pkgs/models/vulnerability_package.rb +59 -0
  41. data/lib/git/pkgs/osv_client.rb +151 -0
  42. data/lib/git/pkgs/output.rb +22 -0
  43. data/lib/git/pkgs/purl_helper.rb +56 -0
  44. data/lib/git/pkgs/spinner.rb +46 -0
  45. data/lib/git/pkgs/version.rb +1 -1
  46. data/lib/git/pkgs.rb +12 -0
  47. metadata +72 -4
@@ -13,16 +13,13 @@ module Git
13
13
 
14
14
  def run
15
15
  repo = Repository.new
16
- require_database(repo)
16
+ use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
17
17
 
18
- Database.connect(repo.git_dir)
19
-
20
- commit_sha = @options[:commit] || repo.head_sha
21
- target_commit = Models::Commit.first(sha: commit_sha)
22
-
23
- error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
24
-
25
- deps = compute_dependencies_at_commit(target_commit, repo)
18
+ if use_stateless
19
+ deps = run_stateless(repo)
20
+ else
21
+ deps = run_with_database(repo)
22
+ end
26
23
 
27
24
  # Apply filters
28
25
  if @options[:manifest]
@@ -42,22 +39,64 @@ module Git
42
39
  return
43
40
  end
44
41
 
42
+ locked_versions = build_locked_versions(deps)
43
+
45
44
  if @options[:format] == "json"
46
45
  require "json"
47
- puts JSON.pretty_generate(deps)
46
+ deps_with_locked = deps.map do |dep|
47
+ if dep[:kind] == "manifest"
48
+ locked = locked_versions[[dep[:ecosystem], dep[:name]]]
49
+ locked ? dep.merge(locked_version: locked) : dep
50
+ else
51
+ dep
52
+ end
53
+ end
54
+ puts JSON.pretty_generate(deps_with_locked)
48
55
  else
49
- paginate { output_text(deps) }
56
+ paginate { output_text(deps, locked_versions) }
50
57
  end
51
58
  end
52
59
 
53
- def output_text(deps)
60
+ def run_stateless(repo)
61
+ commit_sha = @options[:commit] || repo.head_sha
62
+ rugged_commit = repo.lookup(repo.rev_parse(commit_sha))
63
+
64
+ error "Could not resolve '#{commit_sha}'. Check that the ref exists." unless rugged_commit
65
+
66
+ analyzer = Analyzer.new(repo)
67
+ analyzer.dependencies_at_commit(rugged_commit)
68
+ end
69
+
70
+ def run_with_database(repo)
71
+ Database.connect(repo.git_dir)
72
+
73
+ commit_sha = @options[:commit] || repo.head_sha
74
+ target_commit = Models::Commit.first(sha: commit_sha)
75
+
76
+ error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
77
+
78
+ compute_dependencies_at_commit(target_commit, repo)
79
+ end
80
+
81
+ def build_locked_versions(deps)
82
+ locked_versions = {}
83
+ deps.each do |d|
84
+ next unless d[:kind] == "lockfile"
85
+ locked_versions[[d[:ecosystem], d[:name]]] = d[:requirement]
86
+ end
87
+ locked_versions
88
+ end
89
+
90
+ def output_text(deps, locked_versions)
54
91
  grouped = deps.group_by { |d| [d[:manifest_path], d[:ecosystem]] }
55
92
 
56
93
  grouped.each do |(path, platform), manifest_deps|
57
94
  puts "#{path} (#{platform}):"
58
95
  manifest_deps.sort_by { |d| d[:name] }.each do |dep|
59
- type_suffix = dep[:dependency_type] ? " [#{dep[:dependency_type]}]" : ""
60
- puts " #{dep[:name]} #{dep[:requirement]}#{type_suffix}"
96
+ type_suffix = dep[:dependency_type] && dep[:dependency_type] != "runtime" ? " [#{dep[:dependency_type]}]" : ""
97
+ locked = locked_versions[[dep[:ecosystem], dep[:name]]] if dep[:kind] == "manifest"
98
+ locked_suffix = locked ? " [#{locked}]" : ""
99
+ puts " #{dep[:name]} #{dep[:requirement]}#{locked_suffix}#{type_suffix}"
61
100
  end
62
101
  puts
63
102
  end
@@ -85,6 +124,7 @@ module Git
85
124
  manifest_path: s.manifest.path,
86
125
  name: s.name,
87
126
  ecosystem: s.ecosystem,
127
+ kind: s.manifest.kind,
88
128
  requirement: s.requirement,
89
129
  dependency_type: s.dependency_type
90
130
  }
@@ -93,7 +133,7 @@ module Git
93
133
 
94
134
  # Replay changes from snapshot to target
95
135
  if snapshot_commit && snapshot_commit.id != target_commit.id
96
- commit_ids = branch.commits_dataset.select_map(:id)
136
+ commit_ids = branch.commits_dataset.select_map(Sequel[:commits][:id])
97
137
  changes = Models::DependencyChange
98
138
  .join(:commits, id: :commit_id)
99
139
  .where(Sequel[:commits][:id] => commit_ids)
@@ -111,6 +151,7 @@ module Git
111
151
  manifest_path: change.manifest.path,
112
152
  name: change.name,
113
153
  ecosystem: change.ecosystem,
154
+ kind: change.manifest.kind,
114
155
  requirement: change.requirement,
115
156
  dependency_type: change.dependency_type
116
157
  }
@@ -157,6 +198,10 @@ module Git
157
198
  options[:no_pager] = true
158
199
  end
159
200
 
201
+ opts.on("--stateless", "Parse manifests directly without database") do
202
+ options[:stateless] = true
203
+ end
204
+
160
205
  opts.on("-h", "--help", "Show this help") do
161
206
  puts opts
162
207
  exit
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Git
6
+ module Pkgs
7
+ module Commands
8
+ class Outdated
9
+ include Output
10
+
11
+ def self.description
12
+ "Show packages with newer versions available"
13
+ end
14
+
15
+ def initialize(args)
16
+ @args = args.dup
17
+ @options = parse_options
18
+ end
19
+
20
+ def parse_options
21
+ options = {}
22
+
23
+ parser = OptionParser.new do |opts|
24
+ opts.banner = "Usage: git pkgs outdated [options]"
25
+ opts.separator ""
26
+ opts.separator "Show packages that have newer versions available in their registries."
27
+ opts.separator ""
28
+ opts.separator "Options:"
29
+
30
+ opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
31
+ options[:ecosystem] = v
32
+ end
33
+
34
+ opts.on("-r", "--ref=REF", "Git ref to check (default: HEAD)") do |v|
35
+ options[:ref] = v
36
+ end
37
+
38
+ opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
39
+ options[:format] = v
40
+ end
41
+
42
+ opts.on("--major", "Show only major version updates") do
43
+ options[:major_only] = true
44
+ end
45
+
46
+ opts.on("--minor", "Show only minor or major updates (skip patch)") do
47
+ options[:minor_only] = true
48
+ end
49
+
50
+ opts.on("--stateless", "Parse manifests directly without database") do
51
+ options[:stateless] = true
52
+ end
53
+
54
+ opts.on("-h", "--help", "Show this help") do
55
+ puts opts
56
+ exit
57
+ end
58
+ end
59
+
60
+ parser.parse!(@args)
61
+ options
62
+ end
63
+
64
+ def run
65
+ repo = Repository.new
66
+ use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
67
+
68
+ if use_stateless
69
+ Database.connect_memory
70
+ deps = get_dependencies_stateless(repo)
71
+ else
72
+ Database.connect(repo.git_dir)
73
+ deps = get_dependencies_with_database(repo)
74
+ end
75
+
76
+ if deps.empty?
77
+ empty_result "No dependencies found"
78
+ return
79
+ end
80
+
81
+ if @options[:ecosystem]
82
+ deps = deps.select { |d| d[:ecosystem].downcase == @options[:ecosystem].downcase }
83
+ end
84
+
85
+ deps_with_versions = Analyzer.lockfile_dependencies(deps).select do |dep|
86
+ dep[:requirement] && !dep[:requirement].match?(/[<>=~^]/)
87
+ end
88
+
89
+ if deps_with_versions.empty?
90
+ empty_result "No dependencies with pinned versions found"
91
+ return
92
+ end
93
+
94
+ packages_to_check = deps_with_versions.map do |dep|
95
+ purl = PurlHelper.build_purl(ecosystem: dep[:ecosystem], name: dep[:name]).to_s
96
+ {
97
+ purl: purl,
98
+ name: dep[:name],
99
+ ecosystem: dep[:ecosystem],
100
+ current_version: dep[:requirement],
101
+ manifest_path: dep[:manifest_path]
102
+ }
103
+ end.uniq { |p| p[:purl] }
104
+
105
+ enrich_packages(packages_to_check.map { |p| p[:purl] })
106
+
107
+ outdated = []
108
+ packages_to_check.each do |pkg|
109
+ db_pkg = Models::Package.first(purl: pkg[:purl])
110
+ next unless db_pkg&.latest_version
111
+
112
+ latest = db_pkg.latest_version
113
+ current = pkg[:current_version]
114
+
115
+ next if current == latest
116
+
117
+ update_type = classify_update(current, latest)
118
+ next if @options[:major_only] && update_type != :major
119
+ next if @options[:minor_only] && update_type == :patch
120
+
121
+ outdated << pkg.merge(
122
+ latest_version: latest,
123
+ update_type: update_type
124
+ )
125
+ end
126
+
127
+ if outdated.empty?
128
+ puts "All packages are up to date"
129
+ return
130
+ end
131
+
132
+ type_order = { major: 0, minor: 1, patch: 2, unknown: 3 }
133
+ outdated.sort_by! { |o| [type_order[o[:update_type]], o[:name]] }
134
+
135
+ if @options[:format] == "json"
136
+ require "json"
137
+ puts JSON.pretty_generate(outdated)
138
+ else
139
+ output_text(outdated)
140
+ end
141
+ end
142
+
143
+ def enrich_packages(purls)
144
+ packages_by_purl = {}
145
+ purls.each do |purl|
146
+ parsed = Purl::PackageURL.parse(purl)
147
+ ecosystem = PurlHelper::ECOSYSTEM_TO_PURL_TYPE.invert[parsed.type] || parsed.type
148
+ pkg = Models::Package.find_or_create_by_purl(
149
+ purl: purl,
150
+ ecosystem: ecosystem,
151
+ name: parsed.name
152
+ )
153
+ packages_by_purl[purl] = pkg
154
+ end
155
+
156
+ stale_purls = packages_by_purl.select { |_, pkg| pkg.needs_enrichment? }.keys
157
+ return if stale_purls.empty?
158
+
159
+ client = EcosystemsClient.new
160
+ begin
161
+ results = Spinner.with_spinner("Fetching package metadata...") do
162
+ client.bulk_lookup(stale_purls)
163
+ end
164
+ results.each do |purl, data|
165
+ packages_by_purl[purl]&.enrich_from_api(data)
166
+ end
167
+ rescue EcosystemsClient::ApiError => e
168
+ $stderr.puts "Warning: Could not fetch package data: #{e.message}" unless Git::Pkgs.quiet
169
+ end
170
+ end
171
+
172
+ def classify_update(current, latest)
173
+ current_parts = parse_version(current)
174
+ latest_parts = parse_version(latest)
175
+
176
+ return :unknown if current_parts.nil? || latest_parts.nil?
177
+
178
+ if latest_parts[0] > current_parts[0]
179
+ :major
180
+ elsif latest_parts[1] > current_parts[1]
181
+ :minor
182
+ elsif latest_parts[2] > current_parts[2]
183
+ :patch
184
+ else
185
+ :unknown
186
+ end
187
+ end
188
+
189
+ def parse_version(version)
190
+ cleaned = version.to_s.sub(/^v/i, "")
191
+ parts = cleaned.split(".").first(3).map { |p| p.to_i }
192
+ return nil if parts.empty?
193
+
194
+ parts + [0] * (3 - parts.length)
195
+ end
196
+
197
+ def output_text(outdated)
198
+ max_name = outdated.map { |o| o[:name].length }.max || 20
199
+ max_current = outdated.map { |o| o[:current_version].length }.max || 10
200
+ max_latest = outdated.map { |o| o[:latest_version].length }.max || 10
201
+
202
+ outdated.each do |pkg|
203
+ name = pkg[:name].ljust(max_name)
204
+ current = pkg[:current_version].ljust(max_current)
205
+ latest = pkg[:latest_version].ljust(max_latest)
206
+ update = pkg[:update_type].to_s
207
+
208
+ line = "#{name} #{current} -> #{latest} (#{update})"
209
+
210
+ colored = case pkg[:update_type]
211
+ when :major then Color.red(line)
212
+ when :minor then Color.yellow(line)
213
+ when :patch then Color.cyan(line)
214
+ else line
215
+ end
216
+
217
+ puts colored
218
+ end
219
+
220
+ puts ""
221
+ summary = "#{outdated.size} outdated package#{"s" if outdated.size != 1}"
222
+ by_type = outdated.group_by { |o| o[:update_type] }
223
+ parts = []
224
+ parts << "#{by_type[:major].size} major" if by_type[:major]&.any?
225
+ parts << "#{by_type[:minor].size} minor" if by_type[:minor]&.any?
226
+ parts << "#{by_type[:patch].size} patch" if by_type[:patch]&.any?
227
+ puts "#{summary}: #{parts.join(", ")}" if parts.any?
228
+ end
229
+
230
+ def get_dependencies_stateless(repo)
231
+ ref = @options[:ref] || "HEAD"
232
+ commit_sha = repo.rev_parse(ref)
233
+ rugged_commit = repo.lookup(commit_sha)
234
+
235
+ error "Could not resolve '#{ref}'" unless rugged_commit
236
+
237
+ analyzer = Analyzer.new(repo)
238
+ analyzer.dependencies_at_commit(rugged_commit)
239
+ end
240
+
241
+ def get_dependencies_with_database(repo)
242
+ ref = @options[:ref] || "HEAD"
243
+ commit_sha = repo.rev_parse(ref)
244
+ target_commit = Models::Commit.first(sha: commit_sha)
245
+
246
+ return get_dependencies_stateless(repo) unless target_commit
247
+
248
+ branch_name = repo.default_branch
249
+ branch = Models::Branch.first(name: branch_name)
250
+ return [] unless branch
251
+
252
+ compute_dependencies_at_commit(target_commit, branch)
253
+ end
254
+
255
+ def compute_dependencies_at_commit(target_commit, branch)
256
+ snapshot_commit = branch.commits_dataset
257
+ .join(:dependency_snapshots, commit_id: :id)
258
+ .where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
259
+ .order(Sequel.desc(Sequel[:commits][:committed_at]))
260
+ .distinct
261
+ .first
262
+
263
+ deps = {}
264
+ if snapshot_commit
265
+ snapshot_commit.dependency_snapshots.each do |s|
266
+ key = [s.manifest.path, s.name]
267
+ deps[key] = {
268
+ manifest_path: s.manifest.path,
269
+ manifest_kind: s.manifest.kind,
270
+ name: s.name,
271
+ ecosystem: s.ecosystem,
272
+ requirement: s.requirement,
273
+ dependency_type: s.dependency_type
274
+ }
275
+ end
276
+ end
277
+
278
+ if snapshot_commit && snapshot_commit.id != target_commit.id
279
+ commit_ids = branch.commits_dataset.select_map(Sequel[:commits][:id])
280
+ changes = Models::DependencyChange
281
+ .join(:commits, id: :commit_id)
282
+ .where(Sequel[:commits][:id] => commit_ids)
283
+ .where { Sequel[:commits][:committed_at] > snapshot_commit.committed_at }
284
+ .where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
285
+ .order(Sequel[:commits][:committed_at])
286
+ .eager(:manifest)
287
+ .all
288
+
289
+ changes.each do |change|
290
+ key = [change.manifest.path, change.name]
291
+ case change.change_type
292
+ when "added", "modified"
293
+ deps[key] = {
294
+ manifest_path: change.manifest.path,
295
+ manifest_kind: change.manifest.kind,
296
+ name: change.name,
297
+ ecosystem: change.ecosystem,
298
+ requirement: change.requirement,
299
+ dependency_type: change.dependency_type
300
+ }
301
+ when "removed"
302
+ deps.delete(key)
303
+ end
304
+ end
305
+ end
306
+
307
+ deps.values
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
@@ -15,13 +15,70 @@ module Git
15
15
  ref = @args.shift || "HEAD"
16
16
 
17
17
  repo = Repository.new
18
- require_database(repo)
19
-
20
- Database.connect(repo.git_dir)
18
+ use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
21
19
 
22
20
  sha = repo.rev_parse(ref)
23
21
  error "Could not resolve '#{ref}'. Check that the ref exists with 'git rev-parse #{ref}'." unless sha
24
22
 
23
+ if use_stateless
24
+ run_stateless(repo, sha)
25
+ else
26
+ run_with_database(repo, sha)
27
+ end
28
+ end
29
+
30
+ def run_stateless(repo, sha)
31
+ rugged_commit = repo.lookup(sha)
32
+ analyzer = Analyzer.new(repo)
33
+
34
+ if rugged_commit.parents.empty?
35
+ # First commit - all deps are "added"
36
+ deps = analyzer.dependencies_at_commit(rugged_commit)
37
+ changes = deps.map do |dep|
38
+ {
39
+ name: dep[:name],
40
+ change_type: "added",
41
+ requirement: dep[:requirement],
42
+ ecosystem: dep[:ecosystem],
43
+ manifest_path: dep[:manifest_path]
44
+ }
45
+ end
46
+ else
47
+ diff = analyzer.diff_commits(rugged_commit.parents[0], rugged_commit)
48
+ changes = []
49
+ diff[:added].each { |d| changes << d.merge(change_type: "added") }
50
+ diff[:modified].each { |d| changes << d.merge(change_type: "modified") }
51
+ diff[:removed].each { |d| changes << d.merge(change_type: "removed") }
52
+ end
53
+
54
+ if @options[:ecosystem]
55
+ changes = changes.select { |c| c[:ecosystem] == @options[:ecosystem] }
56
+ end
57
+
58
+ commit_info = {
59
+ sha: sha,
60
+ short_sha: sha[0..7],
61
+ message: rugged_commit.message,
62
+ author_name: rugged_commit.author[:name],
63
+ author_email: rugged_commit.author[:email],
64
+ committed_at: rugged_commit.time
65
+ }
66
+
67
+ if changes.empty?
68
+ empty_result "No dependency changes in #{commit_info[:short_sha]}"
69
+ return
70
+ end
71
+
72
+ if @options[:format] == "json"
73
+ output_json_stateless(commit_info, changes)
74
+ else
75
+ paginate { output_text_stateless(commit_info, changes) }
76
+ end
77
+ end
78
+
79
+ def run_with_database(repo, sha)
80
+ Database.connect(repo.git_dir)
81
+
25
82
  commit = Models::Commit.find_or_create_from_repo(repo, sha)
26
83
  error "Commit '#{sha[0..7]}' not in database. Run 'git pkgs update' to index new commits." unless commit
27
84
 
@@ -109,6 +166,68 @@ module Git
109
166
  puts JSON.pretty_generate(data)
110
167
  end
111
168
 
169
+ def output_text_stateless(commit_info, changes)
170
+ puts "Commit: #{commit_info[:short_sha]} #{commit_info[:message]&.lines&.first&.strip}"
171
+ puts "Author: #{commit_info[:author_name]} <#{commit_info[:author_email]}>"
172
+ puts "Date: #{commit_info[:committed_at].strftime("%Y-%m-%d")}"
173
+ puts
174
+
175
+ added = changes.select { |c| c[:change_type] == "added" }
176
+ modified = changes.select { |c| c[:change_type] == "modified" }
177
+ removed = changes.select { |c| c[:change_type] == "removed" }
178
+
179
+ if added.any?
180
+ puts Color.green("Added:")
181
+ added.each do |change|
182
+ puts Color.green(" + #{change[:name]} #{change[:requirement]} (#{change[:ecosystem]}, #{change[:manifest_path]})")
183
+ end
184
+ puts
185
+ end
186
+
187
+ if modified.any?
188
+ puts Color.yellow("Modified:")
189
+ modified.each do |change|
190
+ puts Color.yellow(" ~ #{change[:name]} #{change[:previous_requirement]} -> #{change[:requirement]} (#{change[:ecosystem]}, #{change[:manifest_path]})")
191
+ end
192
+ puts
193
+ end
194
+
195
+ if removed.any?
196
+ puts Color.red("Removed:")
197
+ removed.each do |change|
198
+ puts Color.red(" - #{change[:name]} #{change[:requirement]} (#{change[:ecosystem]}, #{change[:manifest_path]})")
199
+ end
200
+ puts
201
+ end
202
+ end
203
+
204
+ def output_json_stateless(commit_info, changes)
205
+ require "json"
206
+
207
+ data = {
208
+ commit: {
209
+ sha: commit_info[:sha],
210
+ short_sha: commit_info[:short_sha],
211
+ message: commit_info[:message]&.lines&.first&.strip,
212
+ author_name: commit_info[:author_name],
213
+ author_email: commit_info[:author_email],
214
+ date: commit_info[:committed_at].iso8601
215
+ },
216
+ changes: changes.map do |change|
217
+ {
218
+ name: change[:name],
219
+ change_type: change[:change_type],
220
+ requirement: change[:requirement],
221
+ previous_requirement: change[:previous_requirement],
222
+ ecosystem: change[:ecosystem],
223
+ manifest: change[:manifest_path]
224
+ }
225
+ end
226
+ }
227
+
228
+ puts JSON.pretty_generate(data)
229
+ end
230
+
112
231
  def parse_options
113
232
  options = {}
114
233
 
@@ -127,6 +246,10 @@ module Git
127
246
  options[:no_pager] = true
128
247
  end
129
248
 
249
+ opts.on("--stateless", "Parse manifests directly without database") do
250
+ options[:stateless] = true
251
+ end
252
+
130
253
  opts.on("-h", "--help", "Show this help") do
131
254
  puts opts
132
255
  exit
@@ -26,10 +26,14 @@ module Git
26
26
 
27
27
  return empty_result("No dependencies found") unless current_commit
28
28
 
29
- snapshots = current_commit.dependency_snapshots_dataset.eager(:manifest)
29
+ # Only look at lockfile dependencies (actual resolved versions, not constraints)
30
+ snapshots = current_commit.dependency_snapshots_dataset
31
+ .eager(:manifest)
32
+ .join(:manifests, id: :manifest_id)
33
+ .where(Sequel[:manifests][:kind] => "lockfile")
30
34
 
31
35
  if @options[:ecosystem]
32
- snapshots = snapshots.where(ecosystem: @options[:ecosystem])
36
+ snapshots = snapshots.where(Sequel[:dependency_snapshots][:ecosystem] => @options[:ecosystem])
33
37
  end
34
38
 
35
39
  snapshots = snapshots.all
@@ -41,6 +41,7 @@ module Git
41
41
  key = [s.manifest.path, s.name]
42
42
  snapshot[key] = {
43
43
  ecosystem: s.ecosystem,
44
+ purl: s.purl,
44
45
  requirement: s.requirement,
45
46
  dependency_type: s.dependency_type
46
47
  }
@@ -88,6 +89,7 @@ module Git
88
89
  manifest: manifest,
89
90
  name: change[:name],
90
91
  ecosystem: change[:ecosystem],
92
+ purl: change[:purl],
91
93
  change_type: change[:change_type],
92
94
  requirement: change[:requirement],
93
95
  previous_requirement: change[:previous_requirement],
@@ -105,6 +107,7 @@ module Git
105
107
  name: name
106
108
  ) do |s|
107
109
  s.ecosystem = dep_info[:ecosystem]
110
+ s.purl = dep_info[:purl]
108
111
  s.requirement = dep_info[:requirement]
109
112
  s.dependency_type = dep_info[:dependency_type]
110
113
  end