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.
- checksums.yaml +4 -4
- data/.gitattributes +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +25 -0
- data/Dockerfile +18 -0
- data/Formula/git-pkgs.rb +28 -0
- data/README.md +90 -6
- data/lib/git/pkgs/analyzer.rb +142 -10
- data/lib/git/pkgs/cli.rb +20 -8
- data/lib/git/pkgs/commands/blame.rb +0 -18
- data/lib/git/pkgs/commands/diff.rb +122 -5
- data/lib/git/pkgs/commands/diff_driver.rb +30 -4
- data/lib/git/pkgs/commands/init.rb +5 -0
- data/lib/git/pkgs/commands/licenses.rb +378 -0
- data/lib/git/pkgs/commands/list.rb +60 -15
- data/lib/git/pkgs/commands/outdated.rb +312 -0
- data/lib/git/pkgs/commands/show.rb +126 -3
- data/lib/git/pkgs/commands/stale.rb +6 -2
- data/lib/git/pkgs/commands/update.rb +3 -0
- data/lib/git/pkgs/commands/vulns/base.rb +358 -0
- data/lib/git/pkgs/commands/vulns/blame.rb +276 -0
- data/lib/git/pkgs/commands/vulns/diff.rb +173 -0
- data/lib/git/pkgs/commands/vulns/exposure.rb +418 -0
- data/lib/git/pkgs/commands/vulns/history.rb +345 -0
- data/lib/git/pkgs/commands/vulns/log.rb +218 -0
- data/lib/git/pkgs/commands/vulns/praise.rb +238 -0
- data/lib/git/pkgs/commands/vulns/scan.rb +231 -0
- data/lib/git/pkgs/commands/vulns/show.rb +216 -0
- data/lib/git/pkgs/commands/vulns/sync.rb +110 -0
- data/lib/git/pkgs/commands/vulns.rb +50 -0
- data/lib/git/pkgs/config.rb +8 -1
- data/lib/git/pkgs/database.rb +151 -5
- data/lib/git/pkgs/ecosystems.rb +83 -0
- data/lib/git/pkgs/ecosystems_client.rb +96 -0
- data/lib/git/pkgs/models/dependency_change.rb +8 -0
- data/lib/git/pkgs/models/dependency_snapshot.rb +8 -0
- data/lib/git/pkgs/models/package.rb +92 -0
- data/lib/git/pkgs/models/version.rb +27 -0
- data/lib/git/pkgs/models/vulnerability.rb +300 -0
- data/lib/git/pkgs/models/vulnerability_package.rb +59 -0
- data/lib/git/pkgs/osv_client.rb +151 -0
- data/lib/git/pkgs/output.rb +22 -0
- data/lib/git/pkgs/purl_helper.rb +56 -0
- data/lib/git/pkgs/spinner.rb +46 -0
- data/lib/git/pkgs/version.rb +1 -1
- data/lib/git/pkgs.rb +12 -0
- metadata +72 -4
|
@@ -13,16 +13,13 @@ module Git
|
|
|
13
13
|
|
|
14
14
|
def run
|
|
15
15
|
repo = Repository.new
|
|
16
|
-
|
|
16
|
+
use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|