git-pkgs 0.6.1 → 0.7.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 +23 -0
- data/Dockerfile +18 -0
- data/Formula/git-pkgs.rb +28 -0
- data/README.md +69 -5
- data/lib/git/pkgs/analyzer.rb +140 -8
- data/lib/git/pkgs/cli.rb +16 -6
- data/lib/git/pkgs/commands/blame.rb +0 -18
- data/lib/git/pkgs/commands/diff.rb +181 -7
- data/lib/git/pkgs/commands/diff_driver.rb +25 -5
- data/lib/git/pkgs/commands/init.rb +5 -0
- data/lib/git/pkgs/commands/list.rb +68 -15
- data/lib/git/pkgs/commands/show.rb +126 -3
- data/lib/git/pkgs/commands/stale.rb +38 -4
- data/lib/git/pkgs/commands/tree.rb +44 -2
- data/lib/git/pkgs/commands/update.rb +3 -0
- data/lib/git/pkgs/commands/vulns/base.rb +354 -0
- data/lib/git/pkgs/commands/vulns/blame.rb +276 -0
- data/lib/git/pkgs/commands/vulns/diff.rb +172 -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 +108 -0
- data/lib/git/pkgs/commands/vulns.rb +50 -0
- data/lib/git/pkgs/commands/why.rb +40 -1
- data/lib/git/pkgs/config.rb +10 -2
- data/lib/git/pkgs/database.rb +135 -5
- data/lib/git/pkgs/ecosystems.rb +83 -0
- data/lib/git/pkgs/models/package.rb +54 -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/version.rb +1 -1
- data/lib/git/pkgs.rb +77 -0
- metadata +66 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f9ff177f3dd7cbb4f5591a0c0f2b936d5fb98629329294dae9f57380a69709e
|
|
4
|
+
data.tar.gz: e8760e948aea3b7252176fc6a0b4bb0d74432ad05e4fa20693468b9440b126bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a670a1b046b7d0953aefe8bd57a6a03fa69624eb3bdb4e8867a647521a88c936e559973b6470989cca45714e79de19c0f6d2a2c85fa7340c3b677e07923cb97a
|
|
7
|
+
data.tar.gz: dcbb08683dfe053e70801ae36fe74f3d1f33ce0a6f74a7df3b6cb1a6d5df70d5a7c5db9ca9c7fc8efb6ee94463fbc96a68ef7e09f20808fd6b41b1c2d2e6b873
|
data/.gitattributes
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# git-pkgs textconv for lockfiles
|
|
2
|
+
Brewfile.lock.json diff=pkgs
|
|
3
|
+
Cargo.lock diff=pkgs
|
|
4
|
+
Cartfile.resolved diff=pkgs
|
|
5
|
+
Gemfile.lock diff=pkgs
|
|
6
|
+
Gopkg.lock diff=pkgs
|
|
7
|
+
Package.resolved diff=pkgs
|
|
8
|
+
Pipfile.lock diff=pkgs
|
|
9
|
+
Podfile.lock diff=pkgs
|
|
10
|
+
Project.lock.json diff=pkgs
|
|
11
|
+
bun.lock diff=pkgs
|
|
12
|
+
composer.lock diff=pkgs
|
|
13
|
+
gems.locked diff=pkgs
|
|
14
|
+
glide.lock diff=pkgs
|
|
15
|
+
go.mod diff=pkgs
|
|
16
|
+
mix.lock diff=pkgs
|
|
17
|
+
npm-shrinkwrap.json diff=pkgs
|
|
18
|
+
package-lock.json diff=pkgs
|
|
19
|
+
packages.lock.json diff=pkgs
|
|
20
|
+
paket.lock diff=pkgs
|
|
21
|
+
pnpm-lock.yaml diff=pkgs
|
|
22
|
+
poetry.lock diff=pkgs
|
|
23
|
+
project.assets.json diff=pkgs
|
|
24
|
+
pubspec.lock diff=pkgs
|
|
25
|
+
pylock.toml diff=pkgs
|
|
26
|
+
shard.lock diff=pkgs
|
|
27
|
+
uv.lock diff=pkgs
|
|
28
|
+
yarn.lock diff=pkgs
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-01-09
|
|
4
|
+
|
|
5
|
+
- `git pkgs vulns` subcommand for vulnerability scanning via OSV API
|
|
6
|
+
- `git pkgs vulns scan` to scan dependencies for known vulnerabilities
|
|
7
|
+
- `git pkgs vulns show` to display details for a specific vulnerability
|
|
8
|
+
- `git pkgs vulns sync` to prefetch vulnerability data for all packages
|
|
9
|
+
- `git pkgs vulns exposure` to analyze vulnerability exposure over time
|
|
10
|
+
- `git pkgs vulns praise` to show resolved vulnerabilities with attribution
|
|
11
|
+
- SARIF output format for CI integration (`--format=sarif`)
|
|
12
|
+
- Docker container support for running git-pkgs without local Ruby installation
|
|
13
|
+
- `list` command now shows locked versions and manifest kind
|
|
14
|
+
- `--stateless` flag for `list`, `show`, and `diff` commands (auto-enabled when no database exists)
|
|
15
|
+
- Update ecosystems-bibliothecary to ~> 15.2
|
|
16
|
+
- Fix `-f` flag conflict in `diff` command (was defined for both `--from` and `--format`)
|
|
17
|
+
|
|
18
|
+
## [0.6.2] - 2026-01-06
|
|
19
|
+
|
|
20
|
+
- `--format=json` support for `diff`, `tree`, `stale`, and `why` commands
|
|
21
|
+
- Ignore go.sum (checksums only), treat go.mod as lockfile
|
|
22
|
+
- Update ecosystems-bibliothecary to ~> 15.1
|
|
23
|
+
- `--manifest` filter for `list` command to filter by manifest path
|
|
24
|
+
- Stateless parsing API for forge integration (`Git::Pkgs.parse_file`, `parse_files`, `diff_file`)
|
|
25
|
+
|
|
3
26
|
## [0.6.1] - 2026-01-05
|
|
4
27
|
|
|
5
28
|
- Fix `stats` command crash on most changed dependencies query
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM ruby:4.0-alpine
|
|
2
|
+
|
|
3
|
+
RUN apk add --no-cache \
|
|
4
|
+
build-base \
|
|
5
|
+
git \
|
|
6
|
+
libgit2-dev \
|
|
7
|
+
cmake
|
|
8
|
+
|
|
9
|
+
RUN gem install git-pkgs
|
|
10
|
+
|
|
11
|
+
# The git repository (the mounted directory) has different ownership that the root user of the container.
|
|
12
|
+
# Due to an update in git, a different user cannot perform git operations on the mounted directory.
|
|
13
|
+
# This command allows the any user to perform git operations on the mounted directory.
|
|
14
|
+
RUN git config --system --add safe.directory /mnt
|
|
15
|
+
|
|
16
|
+
WORKDIR /mnt
|
|
17
|
+
|
|
18
|
+
ENTRYPOINT ["git", "pkgs"]
|
data/Formula/git-pkgs.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class GitPkgs < Formula
|
|
2
|
+
desc "Track package dependencies across git history"
|
|
3
|
+
homepage "https://github.com/andrew/git-pkgs"
|
|
4
|
+
url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.6.2.tar.gz"
|
|
5
|
+
sha256 "ccd7a8a5b9cb21c52cc488923ed1318387a9fefa4baff2057bd96b27591577aa"
|
|
6
|
+
license "AGPL-3.0"
|
|
7
|
+
|
|
8
|
+
depends_on "cmake" => :build
|
|
9
|
+
depends_on "pkg-config" => :build
|
|
10
|
+
depends_on "libgit2"
|
|
11
|
+
depends_on "ruby"
|
|
12
|
+
|
|
13
|
+
def install
|
|
14
|
+
ENV["GEM_HOME"] = libexec
|
|
15
|
+
|
|
16
|
+
system "git", "init"
|
|
17
|
+
system "git", "add", "."
|
|
18
|
+
|
|
19
|
+
system "gem", "build", "git-pkgs.gemspec"
|
|
20
|
+
system "gem", "install", "--no-document", "git-pkgs-#{version}.gem"
|
|
21
|
+
bin.install libexec/"bin/git-pkgs"
|
|
22
|
+
bin.env_script_all_files(libexec/"bin", GEM_HOME: ENV.fetch("GEM_HOME", nil))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
test do
|
|
26
|
+
system bin/"git-pkgs", "--version"
|
|
27
|
+
end
|
|
28
|
+
end
|
data/README.md
CHANGED
|
@@ -2,18 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
A git subcommand for tracking package dependencies across git history. Analyzes your repository to show when dependencies were added, modified, or removed, who made those changes, and why.
|
|
4
4
|
|
|
5
|
+
[Installation](#installation) · [Quick start](#quick-start) · [Commands](#commands) · [Configuration](#configuration) · [Ruby API](#ruby-api) · [Contributing](#contributing)
|
|
6
|
+
|
|
5
7
|
## Why this exists
|
|
6
8
|
|
|
7
9
|
Your lockfile shows what dependencies you have, but it doesn't show how you got here, and `git log Gemfile.lock` is useless noise. git-pkgs indexes your dependency history into a queryable database so you can ask questions like: when did we add this? who added it? what changed between these two releases? has anyone touched this in the last year?
|
|
8
10
|
|
|
11
|
+
For best results, commit your lockfiles. Manifests show version ranges but lockfiles show what actually got installed, including transitive dependencies.
|
|
12
|
+
|
|
9
13
|
It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem. Everything runs locally and offline with no external services or network calls, and the database lives in your `.git` directory where you can use it in CI to catch dependency changes in pull requests.
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
12
16
|
|
|
17
|
+
```bash
|
|
18
|
+
brew tap andrew/git-pkgs https://github.com/andrew/git-pkgs
|
|
19
|
+
brew install git-pkgs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with RubyGems:
|
|
23
|
+
|
|
13
24
|
```bash
|
|
14
25
|
gem install git-pkgs
|
|
15
26
|
```
|
|
16
27
|
|
|
28
|
+
Or using Docker:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
docker build -t git-pkgs .
|
|
32
|
+
docker run -it --rm -v $(pwd):/mnt git-pkgs <subcommand>
|
|
33
|
+
```
|
|
34
|
+
|
|
17
35
|
## Quick start
|
|
18
36
|
|
|
19
37
|
```bash
|
|
@@ -95,6 +113,8 @@ Snapshot Coverage
|
|
|
95
113
|
git pkgs list
|
|
96
114
|
git pkgs list --commit=abc123
|
|
97
115
|
git pkgs list --ecosystem=rubygems
|
|
116
|
+
git pkgs list --manifest=Gemfile
|
|
117
|
+
git pkgs list --stateless # parse manifests directly, no database needed
|
|
98
118
|
```
|
|
99
119
|
|
|
100
120
|
Example output:
|
|
@@ -239,11 +259,24 @@ git pkgs outdated # alias for stale
|
|
|
239
259
|
|
|
240
260
|
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.
|
|
241
261
|
|
|
262
|
+
### Vulnerability scanning
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
git pkgs vulns # scan current dependencies for known CVEs
|
|
266
|
+
git pkgs vulns -s high # only critical and high severity
|
|
267
|
+
git pkgs vulns blame # who introduced each vulnerability
|
|
268
|
+
git pkgs vulns praise # who fixed vulnerabilities
|
|
269
|
+
git pkgs vulns exposure --all-time --summary # remediation metrics
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Uses the [OSV database](https://osv.dev) to check your dependencies against known security advisories. Because git-pkgs tracks the full history, it can show who introduced and fixed each vulnerability. See [docs/vulns.md](docs/vulns.md) for full documentation.
|
|
273
|
+
|
|
242
274
|
### Diff between commits
|
|
243
275
|
|
|
244
276
|
```bash
|
|
245
277
|
git pkgs diff --from=abc123 --to=def456
|
|
246
278
|
git pkgs diff --from=HEAD~10
|
|
279
|
+
git pkgs diff main..feature --stateless # no database needed
|
|
247
280
|
```
|
|
248
281
|
|
|
249
282
|
This shows added, removed, and modified packages with version info.
|
|
@@ -254,6 +287,7 @@ This shows added, removed, and modified packages with version info.
|
|
|
254
287
|
git pkgs show # show dependency changes in HEAD
|
|
255
288
|
git pkgs show abc123 # specific commit
|
|
256
289
|
git pkgs show HEAD~5 # relative ref
|
|
290
|
+
git pkgs show --stateless # no database needed
|
|
257
291
|
```
|
|
258
292
|
|
|
259
293
|
Like `git show` but for dependencies. Shows what was added, modified, or removed in a single commit.
|
|
@@ -324,7 +358,7 @@ Useful for understanding the [database structure](docs/schema.md) or generating
|
|
|
324
358
|
|
|
325
359
|
### CI usage
|
|
326
360
|
|
|
327
|
-
You can run git-pkgs in CI to show dependency changes in pull requests:
|
|
361
|
+
You can run git-pkgs in CI to show dependency changes in pull requests. Use `--stateless` to skip database initialization for faster runs:
|
|
328
362
|
|
|
329
363
|
```yaml
|
|
330
364
|
# .github/workflows/deps.yml
|
|
@@ -343,8 +377,7 @@ jobs:
|
|
|
343
377
|
with:
|
|
344
378
|
ruby-version: '3.3'
|
|
345
379
|
- run: gem install git-pkgs
|
|
346
|
-
- run: git pkgs
|
|
347
|
-
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD
|
|
380
|
+
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD --stateless
|
|
348
381
|
```
|
|
349
382
|
|
|
350
383
|
### Diff driver
|
|
@@ -416,7 +449,7 @@ git config --add pkgs.ignoredFiles test/fixtures/package.json
|
|
|
416
449
|
|
|
417
450
|
## Performance
|
|
418
451
|
|
|
419
|
-
Benchmarked on an M1 MacBook Pro analyzing [octobox](https://github.com/octobox/octobox) (
|
|
452
|
+
Benchmarked on an M1 MacBook Pro analyzing [octobox](https://github.com/octobox/octobox) (5193 commits, 8 years of history): init takes about 5 seconds at roughly 1000 commits/sec, producing a 4.8 MB database. About half the commits (2439) had dependency changes.
|
|
420
453
|
|
|
421
454
|
Optimizations:
|
|
422
455
|
- Bulk inserts with transaction batching (500 commits per transaction)
|
|
@@ -429,10 +462,41 @@ Optimizations:
|
|
|
429
462
|
|
|
430
463
|
git-pkgs uses [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary) for parsing, supporting:
|
|
431
464
|
|
|
432
|
-
Actions, BentoML, Bower, Cargo, Carthage, Clojars, CocoaPods, Cog, Conda, CPAN, CRAN, Docker, Dub, DVC, Elm, Go, Hackage, Haxelib, Hex, Homebrew, Julia, Maven, Meteor, MLflow, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, SwiftPM, Vcpkg
|
|
465
|
+
Actions, BentoML, Bower, Cargo, Carthage, Clojars, CocoaPods, Cog, Conan, Conda, CPAN, CRAN, Deno, Docker, Dub, DVC, Elm, Go, Hackage, Haxelib, Hex, Homebrew, Julia, LuaRocks, Maven, Meteor, MLflow, Nimble, Nix, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, SwiftPM, Vcpkg
|
|
433
466
|
|
|
434
467
|
SBOM formats (CycloneDX, SPDX) are not supported as they duplicate information from the actual lockfiles.
|
|
435
468
|
|
|
469
|
+
## Ruby API
|
|
470
|
+
|
|
471
|
+
For embedding in other tools (like forges), git-pkgs provides a stateless parsing API that doesn't require initializing a database:
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
require "git/pkgs"
|
|
475
|
+
|
|
476
|
+
# Parse a single manifest file
|
|
477
|
+
result = Git::Pkgs.parse_file("Gemfile", content)
|
|
478
|
+
# => { platform: "rubygems", kind: "manifest", dependencies: [...] }
|
|
479
|
+
|
|
480
|
+
# Parse multiple files at once
|
|
481
|
+
results = Git::Pkgs.parse_files({
|
|
482
|
+
"Gemfile" => gemfile_content,
|
|
483
|
+
"package.json" => package_json_content
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
# Diff two versions of a manifest
|
|
487
|
+
diff = Git::Pkgs.diff_file("Gemfile", old_content, new_content)
|
|
488
|
+
# => { path: "Gemfile", platform: "rubygems", added: [...], modified: [...], removed: [...] }
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The diff_file method returns modified dependencies with a `previous_requirement` field showing the old version.
|
|
492
|
+
|
|
493
|
+
For database queries, connect to an existing database and use the Sequel models directly:
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
Git::Pkgs::Database.connect(repo_git_dir)
|
|
497
|
+
Git::Pkgs::Models::DependencyChange.where(name: "rails").all
|
|
498
|
+
```
|
|
499
|
+
|
|
436
500
|
## Contributing
|
|
437
501
|
|
|
438
502
|
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
|
@@ -12,10 +12,10 @@ module Git
|
|
|
12
12
|
QUICK_MANIFEST_PATTERNS = %w[
|
|
13
13
|
Gemfile Gemfile.lock gems.rb gems.locked *.gemspec
|
|
14
14
|
package.json package-lock.json yarn.lock npm-shrinkwrap.json pnpm-lock.yaml bun.lock npm-ls.json
|
|
15
|
-
setup.py req*.txt req*.pip requirements/*.txt requirements/*.pip requirements.frozen
|
|
16
|
-
Pipfile Pipfile.lock pyproject.toml poetry.lock uv.lock pylock.toml
|
|
15
|
+
setup.py req*.txt req*.pip requirements/*.txt requirements/*.pip requirements*.in requirements.frozen
|
|
16
|
+
Pipfile Pipfile.lock pyproject.toml poetry.lock uv.lock pylock.toml pdm.lock
|
|
17
17
|
pip-resolved-dependencies.txt pip-dependency-graph.json
|
|
18
|
-
pom.xml ivy.xml build.gradle build.gradle.kts gradle-dependencies-q.txt
|
|
18
|
+
pom.xml ivy.xml build.gradle build.gradle.kts gradle-dependencies-q.txt gradle.lockfile verification-metadata.xml
|
|
19
19
|
maven-resolved-dependencies.txt sbt-update-full.txt maven-dependency-tree.txt maven-dependency-tree.dot
|
|
20
20
|
Cargo.toml Cargo.lock
|
|
21
21
|
go.mod go.sum glide.yaml glide.lock Godeps Godeps/Godeps.json
|
|
@@ -23,22 +23,32 @@ module Git
|
|
|
23
23
|
composer.json composer.lock
|
|
24
24
|
Podfile Podfile.lock *.podspec *.podspec.json
|
|
25
25
|
packages.config packages.lock.json Project.json Project.lock.json
|
|
26
|
-
*.nuspec paket.lock *.csproj project.assets.json
|
|
26
|
+
*.nuspec paket.lock *.csproj project.assets.json *.deps.json
|
|
27
27
|
bower.json bentofile.yaml
|
|
28
|
-
META.json META.yml
|
|
28
|
+
META.json META.yml cpanfile cpanfile.snapshot Makefile.PL Build.PL
|
|
29
29
|
environment.yml environment.yaml
|
|
30
|
-
cog.yaml versions.json MLmodel DESCRIPTION
|
|
30
|
+
cog.yaml versions.json MLmodel DESCRIPTION renv.lock
|
|
31
31
|
pubspec.yaml pubspec.lock
|
|
32
32
|
dub.json dub.sdl
|
|
33
|
-
REQUIRE
|
|
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
36
|
haxelib.json
|
|
37
37
|
action.yml action.yaml .github/workflows/*.yml .github/workflows/*.yaml
|
|
38
38
|
Dockerfile docker-compose*.yml docker-compose*.yaml
|
|
39
|
-
dvc.yaml vcpkg.json
|
|
39
|
+
dvc.yaml vcpkg.json _generated-vcpkg-list.json
|
|
40
40
|
Brewfile Brewfile.lock.json
|
|
41
41
|
Modelfile
|
|
42
|
+
deno.json deno.jsonc deno.lock
|
|
43
|
+
conanfile.py conanfile.txt conan.lock
|
|
44
|
+
*.rockspec
|
|
45
|
+
*.nimble
|
|
46
|
+
*.cabal *cabal.config stack.yaml.lock cabal.project.freeze
|
|
47
|
+
Cartfile Cartfile.private Cartfile.resolved
|
|
48
|
+
project.clj
|
|
49
|
+
Package.swift Package.resolved
|
|
50
|
+
mix.exs mix.lock gleam.toml manifest.toml rebar.lock
|
|
51
|
+
flake.nix flake.lock nix/sources.json npins/sources.json
|
|
42
52
|
].freeze
|
|
43
53
|
|
|
44
54
|
QUICK_MANIFEST_REGEX = Regexp.union(
|
|
@@ -58,6 +68,10 @@ module Git
|
|
|
58
68
|
Config.configure_bibliothecary
|
|
59
69
|
end
|
|
60
70
|
|
|
71
|
+
def generate_purl(ecosystem, name)
|
|
72
|
+
Ecosystems.generate_purl(ecosystem, name)
|
|
73
|
+
end
|
|
74
|
+
|
|
61
75
|
# Quick check if any paths might be manifests (fast regex check)
|
|
62
76
|
def might_have_manifests?(paths)
|
|
63
77
|
paths.any? { |p| p.match?(QUICK_MANIFEST_REGEX) }
|
|
@@ -124,6 +138,7 @@ module Git
|
|
|
124
138
|
ecosystem: result[:platform],
|
|
125
139
|
kind: result[:kind],
|
|
126
140
|
name: dep[:name],
|
|
141
|
+
purl: generate_purl(result[:platform], dep[:name]),
|
|
127
142
|
change_type: "added",
|
|
128
143
|
requirement: dep[:requirement],
|
|
129
144
|
dependency_type: dep[:type]
|
|
@@ -133,6 +148,7 @@ module Git
|
|
|
133
148
|
new_snapshot[key] = {
|
|
134
149
|
ecosystem: result[:platform],
|
|
135
150
|
kind: result[:kind],
|
|
151
|
+
purl: generate_purl(result[:platform], dep[:name]),
|
|
136
152
|
requirement: dep[:requirement],
|
|
137
153
|
dependency_type: dep[:type]
|
|
138
154
|
}
|
|
@@ -160,6 +176,7 @@ module Git
|
|
|
160
176
|
ecosystem: after_result[:platform],
|
|
161
177
|
kind: after_result[:kind],
|
|
162
178
|
name: name,
|
|
179
|
+
purl: generate_purl(after_result[:platform], name),
|
|
163
180
|
change_type: "added",
|
|
164
181
|
requirement: dep[:requirement],
|
|
165
182
|
dependency_type: dep[:type]
|
|
@@ -169,6 +186,7 @@ module Git
|
|
|
169
186
|
new_snapshot[key] = {
|
|
170
187
|
ecosystem: after_result[:platform],
|
|
171
188
|
kind: after_result[:kind],
|
|
189
|
+
purl: generate_purl(after_result[:platform], name),
|
|
172
190
|
requirement: dep[:requirement],
|
|
173
191
|
dependency_type: dep[:type]
|
|
174
192
|
}
|
|
@@ -181,6 +199,7 @@ module Git
|
|
|
181
199
|
ecosystem: before_result[:platform],
|
|
182
200
|
kind: before_result[:kind],
|
|
183
201
|
name: name,
|
|
202
|
+
purl: generate_purl(before_result[:platform], name),
|
|
184
203
|
change_type: "removed",
|
|
185
204
|
requirement: dep[:requirement],
|
|
186
205
|
dependency_type: dep[:type]
|
|
@@ -200,6 +219,7 @@ module Git
|
|
|
200
219
|
ecosystem: after_result[:platform],
|
|
201
220
|
kind: after_result[:kind],
|
|
202
221
|
name: name,
|
|
222
|
+
purl: generate_purl(after_result[:platform], name),
|
|
203
223
|
change_type: "modified",
|
|
204
224
|
requirement: after_dep[:requirement],
|
|
205
225
|
previous_requirement: before_dep[:requirement],
|
|
@@ -210,6 +230,7 @@ module Git
|
|
|
210
230
|
new_snapshot[key] = {
|
|
211
231
|
ecosystem: after_result[:platform],
|
|
212
232
|
kind: after_result[:kind],
|
|
233
|
+
purl: generate_purl(after_result[:platform], name),
|
|
213
234
|
requirement: after_dep[:requirement],
|
|
214
235
|
dependency_type: after_dep[:type]
|
|
215
236
|
}
|
|
@@ -228,6 +249,7 @@ module Git
|
|
|
228
249
|
ecosystem: result[:platform],
|
|
229
250
|
kind: result[:kind],
|
|
230
251
|
name: dep[:name],
|
|
252
|
+
purl: generate_purl(result[:platform], dep[:name]),
|
|
231
253
|
change_type: "removed",
|
|
232
254
|
requirement: dep[:requirement],
|
|
233
255
|
dependency_type: dep[:type]
|
|
@@ -288,6 +310,116 @@ module Git
|
|
|
288
310
|
@blob_cache[cache_key] = { result: result, hits: 0 }
|
|
289
311
|
result
|
|
290
312
|
end
|
|
313
|
+
|
|
314
|
+
# Parse all manifest files at a given commit (stateless)
|
|
315
|
+
def dependencies_at_commit(rugged_commit)
|
|
316
|
+
deps = []
|
|
317
|
+
manifest_paths = find_manifest_paths_in_tree(rugged_commit.tree)
|
|
318
|
+
|
|
319
|
+
manifest_paths.each do |path|
|
|
320
|
+
result = parse_manifest_at_commit(rugged_commit, path)
|
|
321
|
+
next unless result && result[:dependencies]
|
|
322
|
+
|
|
323
|
+
result[:dependencies].each do |dep|
|
|
324
|
+
deps << {
|
|
325
|
+
manifest_path: path,
|
|
326
|
+
manifest_kind: result[:kind],
|
|
327
|
+
name: dep[:name],
|
|
328
|
+
ecosystem: result[:platform],
|
|
329
|
+
kind: result[:kind],
|
|
330
|
+
purl: generate_purl(result[:platform], dep[:name]),
|
|
331
|
+
requirement: dep[:requirement],
|
|
332
|
+
dependency_type: dep[:type]
|
|
333
|
+
}
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
deps
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Compute changes between two commits (stateless)
|
|
341
|
+
def diff_commits(from_commit, to_commit)
|
|
342
|
+
from_deps = dependencies_at_commit(from_commit).group_by { |d| [d[:manifest_path], d[:name]] }
|
|
343
|
+
to_deps = dependencies_at_commit(to_commit).group_by { |d| [d[:manifest_path], d[:name]] }
|
|
344
|
+
|
|
345
|
+
added = []
|
|
346
|
+
modified = []
|
|
347
|
+
removed = []
|
|
348
|
+
|
|
349
|
+
# Find added and modified
|
|
350
|
+
to_deps.each do |key, to_list|
|
|
351
|
+
to_dep = to_list.first
|
|
352
|
+
if from_deps[key]
|
|
353
|
+
from_dep = from_deps[key].first
|
|
354
|
+
if from_dep[:requirement] != to_dep[:requirement]
|
|
355
|
+
modified << to_dep.merge(previous_requirement: from_dep[:requirement])
|
|
356
|
+
end
|
|
357
|
+
else
|
|
358
|
+
added << to_dep
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Find removed
|
|
363
|
+
from_deps.each do |key, from_list|
|
|
364
|
+
removed << from_list.first unless to_deps[key]
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
{ added: added, modified: modified, removed: removed }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def find_manifest_paths_in_tree(tree, prefix = "")
|
|
371
|
+
paths = []
|
|
372
|
+
|
|
373
|
+
tree.each do |entry|
|
|
374
|
+
full_path = prefix.empty? ? entry[:name] : "#{prefix}/#{entry[:name]}"
|
|
375
|
+
|
|
376
|
+
if entry[:type] == :tree
|
|
377
|
+
subtree = repository.lookup(entry[:oid])
|
|
378
|
+
paths.concat(find_manifest_paths_in_tree(subtree, full_path))
|
|
379
|
+
elsif entry[:type] == :blob && full_path.match?(QUICK_MANIFEST_REGEX)
|
|
380
|
+
paths << full_path
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
identify_manifests_cached(paths)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def lookup(oid)
|
|
388
|
+
repository.lookup(oid)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Pair manifest dependencies with their corresponding lockfile versions.
|
|
392
|
+
# Groups by directory + ecosystem + name, preferring lockfile over manifest.
|
|
393
|
+
# Can be called as instance method or class method.
|
|
394
|
+
def pair_manifests_with_lockfiles(deps)
|
|
395
|
+
self.class.pair_manifests_with_lockfiles(deps)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def self.pair_manifests_with_lockfiles(deps)
|
|
399
|
+
# Group by (directory, ecosystem, name)
|
|
400
|
+
groups = {}
|
|
401
|
+
deps.each do |dep|
|
|
402
|
+
dir = File.dirname(dep[:manifest_path])
|
|
403
|
+
dir = "" if dir == "."
|
|
404
|
+
key = [dir, dep[:ecosystem], dep[:name]]
|
|
405
|
+
groups[key] ||= []
|
|
406
|
+
groups[key] << dep
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# For each group, pick the best entry (lockfile preferred)
|
|
410
|
+
groups.values.map do |group_deps|
|
|
411
|
+
lockfile_dep = group_deps.find { |d| d[:manifest_kind] == "lockfile" }
|
|
412
|
+
manifest_dep = group_deps.find { |d| d[:manifest_kind] == "manifest" }
|
|
413
|
+
|
|
414
|
+
# Prefer lockfile version, fall back to manifest
|
|
415
|
+
lockfile_dep || manifest_dep || group_deps.first
|
|
416
|
+
end.compact
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Filter to only lockfile dependencies
|
|
420
|
+
def self.lockfile_dependencies(deps)
|
|
421
|
+
deps.select { |d| d[:manifest_kind] == "lockfile" }
|
|
422
|
+
end
|
|
291
423
|
end
|
|
292
424
|
end
|
|
293
425
|
end
|
data/lib/git/pkgs/cli.rb
CHANGED
|
@@ -34,6 +34,9 @@ module Git
|
|
|
34
34
|
"Analysis" => {
|
|
35
35
|
"stats" => "Show dependency statistics",
|
|
36
36
|
"stale" => "Show dependencies that haven't been updated"
|
|
37
|
+
},
|
|
38
|
+
"Security" => {
|
|
39
|
+
"vulns" => "Scan for known vulnerabilities"
|
|
37
40
|
}
|
|
38
41
|
}.freeze
|
|
39
42
|
|
|
@@ -99,13 +102,20 @@ module Git
|
|
|
99
102
|
command = ALIASES.fetch(command, command)
|
|
100
103
|
# Convert kebab-case or snake_case to PascalCase
|
|
101
104
|
class_name = command.split(/[-_]/).map(&:capitalize).join
|
|
102
|
-
|
|
105
|
+
|
|
106
|
+
# Try with Command suffix first (e.g., VulnsCommand), then bare name
|
|
107
|
+
command_class = begin
|
|
108
|
+
Commands.const_get("#{class_name}Command")
|
|
109
|
+
rescue NameError
|
|
110
|
+
begin
|
|
111
|
+
Commands.const_get(class_name)
|
|
112
|
+
rescue NameError
|
|
113
|
+
$stderr.puts "Command '#{command}' not yet implemented"
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
103
118
|
command_class.new(@args).run
|
|
104
|
-
rescue NameError => e
|
|
105
|
-
# Only catch NameError for missing command class, not NoMethodError
|
|
106
|
-
raise unless e.is_a?(NameError) && !e.is_a?(NoMethodError)
|
|
107
|
-
$stderr.puts "Command '#{command}' not yet implemented"
|
|
108
|
-
exit 1
|
|
109
119
|
end
|
|
110
120
|
|
|
111
121
|
def print_help
|
|
@@ -113,24 +113,6 @@ module Git
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
def best_author(commit)
|
|
117
|
-
authors = [commit.author_name] + parse_coauthors(commit.message)
|
|
118
|
-
|
|
119
|
-
# Prefer human authors over bots
|
|
120
|
-
human = authors.find { |a| !bot_author?(a) }
|
|
121
|
-
human || authors.first
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def parse_coauthors(message)
|
|
125
|
-
return [] unless message
|
|
126
|
-
|
|
127
|
-
message.scan(/^Co-authored-by:([^<]+)<[^>]+>/i).flatten.map(&:strip)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def bot_author?(name)
|
|
131
|
-
name =~ /\[bot\]$|^dependabot|^renovate|^github-actions/i
|
|
132
|
-
end
|
|
133
|
-
|
|
134
116
|
def parse_options
|
|
135
117
|
options = {}
|
|
136
118
|
|