git-pkgs 0.7.0 → 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/CHANGELOG.md +10 -0
- data/Formula/git-pkgs.rb +2 -2
- data/README.md +57 -5
- data/lib/git/pkgs/analyzer.rb +1 -1
- data/lib/git/pkgs/cli.rb +4 -2
- data/lib/git/pkgs/commands/diff_driver.rb +6 -0
- data/lib/git/pkgs/commands/licenses.rb +378 -0
- data/lib/git/pkgs/commands/outdated.rb +312 -0
- data/lib/git/pkgs/commands/vulns/base.rb +16 -12
- data/lib/git/pkgs/commands/vulns/diff.rb +3 -2
- data/lib/git/pkgs/commands/vulns/sync.rb +30 -28
- data/lib/git/pkgs/database.rb +17 -1
- 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 +38 -0
- data/lib/git/pkgs/models/version.rb +27 -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 +6 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e23d09227a873e67670fa403a6c81c4f93a301cfda036f62d8b02d4d7f1e0ce5
|
|
4
|
+
data.tar.gz: ae4c878fa0cca631cb5d0485ab4a5caa999300ba1afc564c6d23461527a487cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a675b67669d345dc39419219adfe51d2c36b1167be446b7f1b937eab42b92c0d935ebd8102e534585760c5944de6059ac722818558090c504758fc4ca7d9388
|
|
7
|
+
data.tar.gz: 7cb84ae314e29ed3cbf0c360ddcd79f43174ce153fca35af476693a6f37a98c84e297b8b3bf3543150e546df2459e69fbcae371959cd4dac04d2aa7d139f1969
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-01-14
|
|
4
|
+
|
|
5
|
+
- `git pkgs outdated` command to find dependencies with newer versions available in registries
|
|
6
|
+
- `git pkgs licenses` command to check dependency licenses with compliance options (--permissive, --allow, --deny)
|
|
7
|
+
- ecosyste.ms client for fetching package metadata (latest versions, licenses)
|
|
8
|
+
- Package and Version models for storing enrichment data
|
|
9
|
+
- Spinner utility for progress feedback during network operations
|
|
10
|
+
- PURL helper for standardized package URLs
|
|
11
|
+
- `outdated` is no longer an alias for `stale` (now a separate command)
|
|
12
|
+
|
|
3
13
|
## [0.7.0] - 2026-01-09
|
|
4
14
|
|
|
5
15
|
- `git pkgs vulns` subcommand for vulnerability scanning via OSV API
|
data/Formula/git-pkgs.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class GitPkgs < Formula
|
|
2
2
|
desc "Track package dependencies across git history"
|
|
3
3
|
homepage "https://github.com/andrew/git-pkgs"
|
|
4
|
-
url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.
|
|
5
|
-
sha256 "
|
|
4
|
+
url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.7.0.tar.gz"
|
|
5
|
+
sha256 "5c5aebf75e9570945b324777e5fa33cd5e35d31f6172c2415a4bd91db02477cc"
|
|
6
6
|
license "AGPL-3.0"
|
|
7
7
|
|
|
8
8
|
depends_on "cmake" => :build
|
data/README.md
CHANGED
|
@@ -10,7 +10,9 @@ Your lockfile shows what dependencies you have, but it doesn't show how you got
|
|
|
10
10
|
|
|
11
11
|
For best results, commit your lockfiles. Manifests show version ranges but lockfiles show what actually got installed, including transitive dependencies.
|
|
12
12
|
|
|
13
|
-
It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem.
|
|
13
|
+
It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem. The database lives in your `.git` directory where you can use it in CI to catch dependency changes in pull requests.
|
|
14
|
+
|
|
15
|
+
The core commands (`list`, `history`, `blame`, `diff`, `stale`, etc.) work entirely from your git history with no network access. Additional commands fetch external data: `vulns` checks [OSV](https://osv.dev) for known CVEs, while `outdated` and `licenses` query [ecosyste.ms](https://packages.ecosyste.ms/) for registry metadata. See [docs/enrichment.md](docs/enrichment.md) for details on external data.
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
@@ -45,6 +47,8 @@ git pkgs history rails # track a specific package
|
|
|
45
47
|
git pkgs why rails # why was this added?
|
|
46
48
|
git pkgs diff --from=HEAD~10 # what changed recently?
|
|
47
49
|
git pkgs diff --from=main --to=feature # compare branches
|
|
50
|
+
git pkgs vulns # scan for known CVEs
|
|
51
|
+
git pkgs vulns blame # who introduced each vulnerability
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
## Commands
|
|
@@ -254,22 +258,69 @@ This shows dependencies grouped by type (runtime, development, etc).
|
|
|
254
258
|
git pkgs stale # list deps by how long since last touched
|
|
255
259
|
git pkgs stale --days=365 # only show deps untouched for a year
|
|
256
260
|
git pkgs stale --ecosystem=npm # filter by ecosystem
|
|
257
|
-
git pkgs outdated # alias for stale
|
|
258
261
|
```
|
|
259
262
|
|
|
260
263
|
Shows dependencies sorted by how long since they were last changed in your repo. Useful for finding packages that may have been forgotten or need review.
|
|
261
264
|
|
|
265
|
+
### Find outdated dependencies
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
git pkgs outdated # show packages with newer versions available
|
|
269
|
+
git pkgs outdated --major # only major version updates
|
|
270
|
+
git pkgs outdated --minor # minor and major updates (skip patch)
|
|
271
|
+
git pkgs outdated --stateless # no database needed
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Checks package registries (via [ecosyste.ms](https://packages.ecosyste.ms/)) to find dependencies with newer versions available. Major updates are shown in red, minor in yellow, patch in cyan.
|
|
275
|
+
|
|
276
|
+
### Check licenses
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
git pkgs licenses # show license for each dependency
|
|
280
|
+
git pkgs licenses --permissive # flag copyleft licenses
|
|
281
|
+
git pkgs licenses --allow=MIT,Apache-2.0 # explicit allow list
|
|
282
|
+
git pkgs licenses --group # group output by license
|
|
283
|
+
git pkgs licenses --stateless # no database needed
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Fetches license information from package registries. Exits with code 1 if violations are found, making it suitable for CI. See [docs/enrichment.md](docs/enrichment.md) for all options.
|
|
287
|
+
|
|
262
288
|
### Vulnerability scanning
|
|
263
289
|
|
|
290
|
+
Scan dependencies for known CVEs using the [OSV database](https://osv.dev). Because git-pkgs tracks the full history of every dependency change, it provides context that static scanners can't: who introduced a vulnerability, when it was fixed, and how long you were exposed.
|
|
291
|
+
|
|
264
292
|
```bash
|
|
265
|
-
git pkgs vulns # scan current dependencies
|
|
293
|
+
git pkgs vulns # scan current dependencies
|
|
294
|
+
git pkgs vulns v1.0.0 # scan at a tag, branch, or commit
|
|
266
295
|
git pkgs vulns -s high # only critical and high severity
|
|
296
|
+
git pkgs vulns -e npm # filter by ecosystem
|
|
297
|
+
git pkgs vulns -f sarif # output for GitHub code scanning
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Subcommands for historical analysis:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
267
303
|
git pkgs vulns blame # who introduced each vulnerability
|
|
304
|
+
git pkgs vulns blame --all-time # include fixed vulnerabilities
|
|
268
305
|
git pkgs vulns praise # who fixed vulnerabilities
|
|
269
|
-
git pkgs vulns
|
|
306
|
+
git pkgs vulns praise --summary # author leaderboard
|
|
307
|
+
git pkgs vulns exposure # remediation metrics (CRA compliance)
|
|
308
|
+
git pkgs vulns diff main feature # compare vulnerability state between refs
|
|
309
|
+
git pkgs vulns log # commits that introduced or fixed vulns
|
|
310
|
+
git pkgs vulns history lodash # vulnerability timeline for a package
|
|
311
|
+
git pkgs vulns show CVE-2024-1234 # details about a specific CVE
|
|
270
312
|
```
|
|
271
313
|
|
|
272
|
-
|
|
314
|
+
Output formats: `text` (default), `json`, and `sarif`. SARIF integrates with GitHub Advanced Security:
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
- run: git pkgs vulns --stateless -f sarif > results.sarif
|
|
318
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
319
|
+
with:
|
|
320
|
+
sarif_file: results.sarif
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Vulnerability data is cached locally and refreshed automatically when stale (>24h). Use `git pkgs vulns sync --refresh` to force an update. See [docs/vulns.md](docs/vulns.md) for full documentation.
|
|
273
324
|
|
|
274
325
|
### Diff between commits
|
|
275
326
|
|
|
@@ -497,6 +548,7 @@ Git::Pkgs::Database.connect(repo_git_dir)
|
|
|
497
548
|
Git::Pkgs::Models::DependencyChange.where(name: "rails").all
|
|
498
549
|
```
|
|
499
550
|
|
|
551
|
+
|
|
500
552
|
## Contributing
|
|
501
553
|
|
|
502
554
|
Bug reports, feature requests, and pull requests are welcome. If you're unsure about a change, open an issue first to discuss it.
|
data/lib/git/pkgs/analyzer.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Git
|
|
|
33
33
|
REQUIRE Project.toml Manifest.toml
|
|
34
34
|
shard.yml shard.lock
|
|
35
35
|
elm-package.json elm_dependencies.json elm-stuff/exact-dependencies.json
|
|
36
|
-
haxelib.json
|
|
36
|
+
haxelib.json stack.yaml stack.yaml.lock
|
|
37
37
|
action.yml action.yaml .github/workflows/*.yml .github/workflows/*.yaml
|
|
38
38
|
Dockerfile docker-compose*.yml docker-compose*.yaml
|
|
39
39
|
dvc.yaml vcpkg.json _generated-vcpkg-list.json
|
data/lib/git/pkgs/cli.rb
CHANGED
|
@@ -33,7 +33,9 @@ module Git
|
|
|
33
33
|
},
|
|
34
34
|
"Analysis" => {
|
|
35
35
|
"stats" => "Show dependency statistics",
|
|
36
|
-
"stale" => "Show dependencies that haven't been updated"
|
|
36
|
+
"stale" => "Show dependencies that haven't been updated",
|
|
37
|
+
"outdated" => "Show packages with newer versions available",
|
|
38
|
+
"licenses" => "Show licenses for dependencies"
|
|
37
39
|
},
|
|
38
40
|
"Security" => {
|
|
39
41
|
"vulns" => "Scan for known vulnerabilities"
|
|
@@ -42,7 +44,7 @@ module Git
|
|
|
42
44
|
|
|
43
45
|
COMMANDS = COMMAND_GROUPS.values.flat_map(&:keys).freeze
|
|
44
46
|
COMMAND_DESCRIPTIONS = COMMAND_GROUPS.values.reduce({}, :merge).freeze
|
|
45
|
-
ALIASES = { "praise" => "blame"
|
|
47
|
+
ALIASES = { "praise" => "blame" }.freeze
|
|
46
48
|
|
|
47
49
|
def self.run(args)
|
|
48
50
|
new(args).run
|
|
@@ -24,18 +24,24 @@ module Git
|
|
|
24
24
|
gems.locked
|
|
25
25
|
glide.lock
|
|
26
26
|
go.mod
|
|
27
|
+
go.sum
|
|
28
|
+
gradle.lockfile
|
|
27
29
|
mix.lock
|
|
28
30
|
npm-shrinkwrap.json
|
|
29
31
|
package-lock.json
|
|
30
32
|
packages.lock.json
|
|
31
33
|
paket.lock
|
|
34
|
+
pdm.lock
|
|
32
35
|
pnpm-lock.yaml
|
|
33
36
|
poetry.lock
|
|
34
37
|
project.assets.json
|
|
35
38
|
pubspec.lock
|
|
36
39
|
pylock.toml
|
|
40
|
+
renv.lock
|
|
37
41
|
shard.lock
|
|
42
|
+
stack.yaml.lock
|
|
38
43
|
uv.lock
|
|
44
|
+
verification-metadata.xml
|
|
39
45
|
yarn.lock
|
|
40
46
|
].freeze
|
|
41
47
|
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Pkgs
|
|
7
|
+
module Commands
|
|
8
|
+
class Licenses
|
|
9
|
+
include Output
|
|
10
|
+
|
|
11
|
+
PERMISSIVE = %w[
|
|
12
|
+
MIT Apache-2.0 BSD-2-Clause BSD-3-Clause ISC Unlicense CC0-1.0
|
|
13
|
+
0BSD WTFPL Zlib BSL-1.0
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
COPYLEFT = %w[
|
|
17
|
+
GPL-2.0 GPL-3.0 LGPL-2.1 LGPL-3.0 AGPL-3.0 MPL-2.0
|
|
18
|
+
GPL-2.0-only GPL-2.0-or-later GPL-3.0-only GPL-3.0-or-later
|
|
19
|
+
LGPL-2.1-only LGPL-2.1-or-later LGPL-3.0-only LGPL-3.0-or-later
|
|
20
|
+
AGPL-3.0-only AGPL-3.0-or-later
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def self.description
|
|
24
|
+
"Show licenses for dependencies"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(args)
|
|
28
|
+
@args = args.dup
|
|
29
|
+
@options = parse_options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse_options
|
|
33
|
+
options = { allow: [], deny: [] }
|
|
34
|
+
|
|
35
|
+
parser = OptionParser.new do |opts|
|
|
36
|
+
opts.banner = "Usage: git pkgs licenses [options]"
|
|
37
|
+
opts.separator ""
|
|
38
|
+
opts.separator "Show licenses for dependencies with optional compliance checks."
|
|
39
|
+
opts.separator ""
|
|
40
|
+
opts.separator "Options:"
|
|
41
|
+
|
|
42
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
43
|
+
options[:ecosystem] = v
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.on("-r", "--ref=REF", "Git ref to check (default: HEAD)") do |v|
|
|
47
|
+
options[:ref] = v
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json, csv)") do |v|
|
|
51
|
+
options[:format] = v
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
opts.on("--allow=LICENSES", "Comma-separated list of allowed licenses") do |v|
|
|
55
|
+
options[:allow] = v.split(",").map(&:strip)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
opts.on("--deny=LICENSES", "Comma-separated list of denied licenses") do |v|
|
|
59
|
+
options[:deny] = v.split(",").map(&:strip)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on("--permissive", "Only allow permissive licenses (MIT, Apache, BSD, etc.)") do
|
|
63
|
+
options[:permissive] = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on("--copyleft", "Flag copyleft licenses (GPL, AGPL, etc.)") do
|
|
67
|
+
options[:copyleft] = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
opts.on("--unknown", "Flag packages with unknown/missing licenses") do
|
|
71
|
+
options[:unknown] = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
opts.on("--group", "Group output by license") do
|
|
75
|
+
options[:group] = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opts.on("--stateless", "Parse manifests directly without database") do
|
|
79
|
+
options[:stateless] = true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
opts.on("-h", "--help", "Show this help") do
|
|
83
|
+
puts opts
|
|
84
|
+
exit
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
parser.parse!(@args)
|
|
89
|
+
options
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run
|
|
93
|
+
repo = Repository.new
|
|
94
|
+
use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
|
|
95
|
+
|
|
96
|
+
if use_stateless
|
|
97
|
+
Database.connect_memory
|
|
98
|
+
deps = get_dependencies_stateless(repo)
|
|
99
|
+
else
|
|
100
|
+
Database.connect(repo.git_dir)
|
|
101
|
+
deps = get_dependencies_with_database(repo)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if deps.empty?
|
|
105
|
+
empty_result "No dependencies found"
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if @options[:ecosystem]
|
|
110
|
+
deps = deps.select { |d| d[:ecosystem].downcase == @options[:ecosystem].downcase }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
deps = Analyzer.pair_manifests_with_lockfiles(deps)
|
|
114
|
+
|
|
115
|
+
if deps.empty?
|
|
116
|
+
empty_result "No dependencies found"
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
packages = deps.map do |dep|
|
|
121
|
+
purl = PurlHelper.build_purl(ecosystem: dep[:ecosystem], name: dep[:name]).to_s
|
|
122
|
+
{
|
|
123
|
+
purl: purl,
|
|
124
|
+
name: dep[:name],
|
|
125
|
+
ecosystem: dep[:ecosystem],
|
|
126
|
+
version: dep[:requirement],
|
|
127
|
+
manifest_path: dep[:manifest_path]
|
|
128
|
+
}
|
|
129
|
+
end.uniq { |p| p[:purl] }
|
|
130
|
+
|
|
131
|
+
enrich_packages(packages.map { |p| p[:purl] })
|
|
132
|
+
|
|
133
|
+
packages.each do |pkg|
|
|
134
|
+
db_pkg = Models::Package.first(purl: pkg[:purl])
|
|
135
|
+
pkg[:license] = db_pkg&.license
|
|
136
|
+
pkg[:violation] = check_violation(pkg[:license])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
violations = packages.select { |p| p[:violation] }
|
|
140
|
+
|
|
141
|
+
case @options[:format]
|
|
142
|
+
when "json"
|
|
143
|
+
output_json(packages, violations)
|
|
144
|
+
when "csv"
|
|
145
|
+
output_csv(packages)
|
|
146
|
+
else
|
|
147
|
+
if @options[:group]
|
|
148
|
+
output_grouped(packages, violations)
|
|
149
|
+
else
|
|
150
|
+
output_text(packages, violations)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
exit 1 if violations.any?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def check_violation(license)
|
|
158
|
+
return "unknown" if @options[:unknown] && (license.nil? || license.empty?)
|
|
159
|
+
|
|
160
|
+
return nil if license.nil? || license.empty?
|
|
161
|
+
|
|
162
|
+
if @options[:permissive]
|
|
163
|
+
return "copyleft" if COPYLEFT.any? { |l| license_matches?(license, l) }
|
|
164
|
+
return "not-permissive" unless PERMISSIVE.any? { |l| license_matches?(license, l) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if @options[:copyleft]
|
|
168
|
+
return "copyleft" if COPYLEFT.any? { |l| license_matches?(license, l) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if @options[:allow].any?
|
|
172
|
+
return "not-allowed" unless @options[:allow].any? { |l| license_matches?(license, l) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if @options[:deny].any?
|
|
176
|
+
return "denied" if @options[:deny].any? { |l| license_matches?(license, l) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def license_matches?(license, pattern)
|
|
183
|
+
license.downcase.include?(pattern.downcase)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def enrich_packages(purls)
|
|
187
|
+
packages_by_purl = {}
|
|
188
|
+
purls.each do |purl|
|
|
189
|
+
parsed = Purl::PackageURL.parse(purl)
|
|
190
|
+
ecosystem = PurlHelper::ECOSYSTEM_TO_PURL_TYPE.invert[parsed.type] || parsed.type
|
|
191
|
+
pkg = Models::Package.find_or_create_by_purl(
|
|
192
|
+
purl: purl,
|
|
193
|
+
ecosystem: ecosystem,
|
|
194
|
+
name: parsed.name
|
|
195
|
+
)
|
|
196
|
+
packages_by_purl[purl] = pkg
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
stale_purls = packages_by_purl.select { |_, pkg| pkg.needs_enrichment? }.keys
|
|
200
|
+
return if stale_purls.empty?
|
|
201
|
+
|
|
202
|
+
client = EcosystemsClient.new
|
|
203
|
+
begin
|
|
204
|
+
results = Spinner.with_spinner("Fetching package metadata...") do
|
|
205
|
+
client.bulk_lookup(stale_purls)
|
|
206
|
+
end
|
|
207
|
+
results.each do |purl, data|
|
|
208
|
+
packages_by_purl[purl]&.enrich_from_api(data)
|
|
209
|
+
end
|
|
210
|
+
rescue EcosystemsClient::ApiError => e
|
|
211
|
+
$stderr.puts "Warning: Could not fetch package data: #{e.message}" unless Git::Pkgs.quiet
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def output_text(packages, violations)
|
|
216
|
+
max_name = packages.map { |p| p[:name].length }.max || 20
|
|
217
|
+
max_license = packages.map { |p| (p[:license] || "").length }.max || 10
|
|
218
|
+
max_license = [max_license, 20].min
|
|
219
|
+
|
|
220
|
+
packages.sort_by { |p| [p[:license] || "zzz", p[:name]] }.each do |pkg|
|
|
221
|
+
name = pkg[:name].ljust(max_name)
|
|
222
|
+
license = (pkg[:license] || "unknown").ljust(max_license)[0, max_license]
|
|
223
|
+
ecosystem = pkg[:ecosystem]
|
|
224
|
+
|
|
225
|
+
line = "#{name} #{license} (#{ecosystem})"
|
|
226
|
+
|
|
227
|
+
colored = if pkg[:violation]
|
|
228
|
+
Color.red("#{line} [#{pkg[:violation]}]")
|
|
229
|
+
else
|
|
230
|
+
line
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
puts colored
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
output_summary(packages, violations)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def output_grouped(packages, violations)
|
|
240
|
+
by_license = packages.group_by { |p| p[:license] || "unknown" }
|
|
241
|
+
|
|
242
|
+
by_license.sort_by { |license, _| license.downcase }.each do |license, pkgs|
|
|
243
|
+
has_violation = pkgs.any? { |p| p[:violation] }
|
|
244
|
+
header = "#{license} (#{pkgs.size})"
|
|
245
|
+
puts has_violation ? Color.red(header) : Color.bold(header)
|
|
246
|
+
|
|
247
|
+
pkgs.sort_by { |p| p[:name] }.each do |pkg|
|
|
248
|
+
puts " #{pkg[:name]}"
|
|
249
|
+
end
|
|
250
|
+
puts ""
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
output_summary(packages, violations)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def output_summary(packages, violations)
|
|
257
|
+
return unless violations.any?
|
|
258
|
+
|
|
259
|
+
puts ""
|
|
260
|
+
puts Color.red("#{violations.size} license violation#{"s" if violations.size != 1} found")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def output_json(packages, violations)
|
|
264
|
+
require "json"
|
|
265
|
+
puts JSON.pretty_generate({
|
|
266
|
+
packages: packages,
|
|
267
|
+
summary: {
|
|
268
|
+
total: packages.size,
|
|
269
|
+
violations: violations.size,
|
|
270
|
+
by_license: packages.group_by { |p| p[:license] || "unknown" }.transform_values(&:size)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def output_csv(packages)
|
|
276
|
+
puts "name,ecosystem,version,license,violation"
|
|
277
|
+
packages.sort_by { |p| p[:name] }.each do |pkg|
|
|
278
|
+
puts [
|
|
279
|
+
pkg[:name],
|
|
280
|
+
pkg[:ecosystem],
|
|
281
|
+
pkg[:version],
|
|
282
|
+
pkg[:license] || "",
|
|
283
|
+
pkg[:violation] || ""
|
|
284
|
+
].map { |v| csv_escape(v) }.join(",")
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def csv_escape(value)
|
|
289
|
+
if value.to_s.include?(",") || value.to_s.include?('"')
|
|
290
|
+
"\"#{value.to_s.gsub('"', '""')}\""
|
|
291
|
+
else
|
|
292
|
+
value.to_s
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def get_dependencies_stateless(repo)
|
|
297
|
+
ref = @options[:ref] || "HEAD"
|
|
298
|
+
commit_sha = repo.rev_parse(ref)
|
|
299
|
+
rugged_commit = repo.lookup(commit_sha)
|
|
300
|
+
|
|
301
|
+
error "Could not resolve '#{ref}'" unless rugged_commit
|
|
302
|
+
|
|
303
|
+
analyzer = Analyzer.new(repo)
|
|
304
|
+
analyzer.dependencies_at_commit(rugged_commit)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def get_dependencies_with_database(repo)
|
|
308
|
+
ref = @options[:ref] || "HEAD"
|
|
309
|
+
commit_sha = repo.rev_parse(ref)
|
|
310
|
+
target_commit = Models::Commit.first(sha: commit_sha)
|
|
311
|
+
|
|
312
|
+
return get_dependencies_stateless(repo) unless target_commit
|
|
313
|
+
|
|
314
|
+
branch_name = repo.default_branch
|
|
315
|
+
branch = Models::Branch.first(name: branch_name)
|
|
316
|
+
return [] unless branch
|
|
317
|
+
|
|
318
|
+
compute_dependencies_at_commit(target_commit, branch)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def compute_dependencies_at_commit(target_commit, branch)
|
|
322
|
+
snapshot_commit = branch.commits_dataset
|
|
323
|
+
.join(:dependency_snapshots, commit_id: :id)
|
|
324
|
+
.where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
|
|
325
|
+
.order(Sequel.desc(Sequel[:commits][:committed_at]))
|
|
326
|
+
.distinct
|
|
327
|
+
.first
|
|
328
|
+
|
|
329
|
+
deps = {}
|
|
330
|
+
if snapshot_commit
|
|
331
|
+
snapshot_commit.dependency_snapshots.each do |s|
|
|
332
|
+
key = [s.manifest.path, s.name]
|
|
333
|
+
deps[key] = {
|
|
334
|
+
manifest_path: s.manifest.path,
|
|
335
|
+
manifest_kind: s.manifest.kind,
|
|
336
|
+
name: s.name,
|
|
337
|
+
ecosystem: s.ecosystem,
|
|
338
|
+
requirement: s.requirement,
|
|
339
|
+
dependency_type: s.dependency_type
|
|
340
|
+
}
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
if snapshot_commit && snapshot_commit.id != target_commit.id
|
|
345
|
+
commit_ids = branch.commits_dataset.select_map(Sequel[:commits][:id])
|
|
346
|
+
changes = Models::DependencyChange
|
|
347
|
+
.join(:commits, id: :commit_id)
|
|
348
|
+
.where(Sequel[:commits][:id] => commit_ids)
|
|
349
|
+
.where { Sequel[:commits][:committed_at] > snapshot_commit.committed_at }
|
|
350
|
+
.where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
|
|
351
|
+
.order(Sequel[:commits][:committed_at])
|
|
352
|
+
.eager(:manifest)
|
|
353
|
+
.all
|
|
354
|
+
|
|
355
|
+
changes.each do |change|
|
|
356
|
+
key = [change.manifest.path, change.name]
|
|
357
|
+
case change.change_type
|
|
358
|
+
when "added", "modified"
|
|
359
|
+
deps[key] = {
|
|
360
|
+
manifest_path: change.manifest.path,
|
|
361
|
+
manifest_kind: change.manifest.kind,
|
|
362
|
+
name: change.name,
|
|
363
|
+
ecosystem: change.ecosystem,
|
|
364
|
+
requirement: change.requirement,
|
|
365
|
+
dependency_type: change.dependency_type
|
|
366
|
+
}
|
|
367
|
+
when "removed"
|
|
368
|
+
deps.delete(key)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
deps.values
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|