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
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/.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,30 @@
|
|
|
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
|
+
|
|
13
|
+
## [0.7.0] - 2026-01-09
|
|
14
|
+
|
|
15
|
+
- `git pkgs vulns` subcommand for vulnerability scanning via OSV API
|
|
16
|
+
- `git pkgs vulns scan` to scan dependencies for known vulnerabilities
|
|
17
|
+
- `git pkgs vulns show` to display details for a specific vulnerability
|
|
18
|
+
- `git pkgs vulns sync` to prefetch vulnerability data for all packages
|
|
19
|
+
- `git pkgs vulns exposure` to analyze vulnerability exposure over time
|
|
20
|
+
- `git pkgs vulns praise` to show resolved vulnerabilities with attribution
|
|
21
|
+
- SARIF output format for CI integration (`--format=sarif`)
|
|
22
|
+
- Docker container support for running git-pkgs without local Ruby installation
|
|
23
|
+
- `list` command now shows locked versions and manifest kind
|
|
24
|
+
- `--stateless` flag for `list`, `show`, and `diff` commands (auto-enabled when no database exists)
|
|
25
|
+
- Update ecosystems-bibliothecary to ~> 15.2
|
|
26
|
+
- Fix `-f` flag conflict in `diff` command (was defined for both `--from` and `--format`)
|
|
27
|
+
|
|
3
28
|
## [0.6.2] - 2026-01-06
|
|
4
29
|
|
|
5
30
|
- `--format=json` support for `diff`, `tree`, `stale`, and `why` commands
|
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.7.0.tar.gz"
|
|
5
|
+
sha256 "5c5aebf75e9570945b324777e5fa33cd5e35d31f6172c2415a4bd91db02477cc"
|
|
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,38 @@
|
|
|
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
|
|
|
9
|
-
|
|
11
|
+
For best results, commit your lockfiles. Manifests show version ranges but lockfiles show what actually got installed, including transitive dependencies.
|
|
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. 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.
|
|
10
16
|
|
|
11
17
|
## Installation
|
|
12
18
|
|
|
19
|
+
```bash
|
|
20
|
+
brew tap andrew/git-pkgs https://github.com/andrew/git-pkgs
|
|
21
|
+
brew install git-pkgs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or with RubyGems:
|
|
25
|
+
|
|
13
26
|
```bash
|
|
14
27
|
gem install git-pkgs
|
|
15
28
|
```
|
|
16
29
|
|
|
30
|
+
Or using Docker:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
docker build -t git-pkgs .
|
|
34
|
+
docker run -it --rm -v $(pwd):/mnt git-pkgs <subcommand>
|
|
35
|
+
```
|
|
36
|
+
|
|
17
37
|
## Quick start
|
|
18
38
|
|
|
19
39
|
```bash
|
|
@@ -27,6 +47,8 @@ git pkgs history rails # track a specific package
|
|
|
27
47
|
git pkgs why rails # why was this added?
|
|
28
48
|
git pkgs diff --from=HEAD~10 # what changed recently?
|
|
29
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
|
|
30
52
|
```
|
|
31
53
|
|
|
32
54
|
## Commands
|
|
@@ -96,6 +118,7 @@ git pkgs list
|
|
|
96
118
|
git pkgs list --commit=abc123
|
|
97
119
|
git pkgs list --ecosystem=rubygems
|
|
98
120
|
git pkgs list --manifest=Gemfile
|
|
121
|
+
git pkgs list --stateless # parse manifests directly, no database needed
|
|
99
122
|
```
|
|
100
123
|
|
|
101
124
|
Example output:
|
|
@@ -235,16 +258,76 @@ This shows dependencies grouped by type (runtime, development, etc).
|
|
|
235
258
|
git pkgs stale # list deps by how long since last touched
|
|
236
259
|
git pkgs stale --days=365 # only show deps untouched for a year
|
|
237
260
|
git pkgs stale --ecosystem=npm # filter by ecosystem
|
|
238
|
-
git pkgs outdated # alias for stale
|
|
239
261
|
```
|
|
240
262
|
|
|
241
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.
|
|
242
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
|
+
|
|
288
|
+
### Vulnerability scanning
|
|
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
|
+
|
|
292
|
+
```bash
|
|
293
|
+
git pkgs vulns # scan current dependencies
|
|
294
|
+
git pkgs vulns v1.0.0 # scan at a tag, branch, or commit
|
|
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
|
|
303
|
+
git pkgs vulns blame # who introduced each vulnerability
|
|
304
|
+
git pkgs vulns blame --all-time # include fixed vulnerabilities
|
|
305
|
+
git pkgs vulns praise # who fixed vulnerabilities
|
|
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
|
|
312
|
+
```
|
|
313
|
+
|
|
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.
|
|
324
|
+
|
|
243
325
|
### Diff between commits
|
|
244
326
|
|
|
245
327
|
```bash
|
|
246
328
|
git pkgs diff --from=abc123 --to=def456
|
|
247
329
|
git pkgs diff --from=HEAD~10
|
|
330
|
+
git pkgs diff main..feature --stateless # no database needed
|
|
248
331
|
```
|
|
249
332
|
|
|
250
333
|
This shows added, removed, and modified packages with version info.
|
|
@@ -255,6 +338,7 @@ This shows added, removed, and modified packages with version info.
|
|
|
255
338
|
git pkgs show # show dependency changes in HEAD
|
|
256
339
|
git pkgs show abc123 # specific commit
|
|
257
340
|
git pkgs show HEAD~5 # relative ref
|
|
341
|
+
git pkgs show --stateless # no database needed
|
|
258
342
|
```
|
|
259
343
|
|
|
260
344
|
Like `git show` but for dependencies. Shows what was added, modified, or removed in a single commit.
|
|
@@ -325,7 +409,7 @@ Useful for understanding the [database structure](docs/schema.md) or generating
|
|
|
325
409
|
|
|
326
410
|
### CI usage
|
|
327
411
|
|
|
328
|
-
You can run git-pkgs in CI to show dependency changes in pull requests:
|
|
412
|
+
You can run git-pkgs in CI to show dependency changes in pull requests. Use `--stateless` to skip database initialization for faster runs:
|
|
329
413
|
|
|
330
414
|
```yaml
|
|
331
415
|
# .github/workflows/deps.yml
|
|
@@ -344,8 +428,7 @@ jobs:
|
|
|
344
428
|
with:
|
|
345
429
|
ruby-version: '3.3'
|
|
346
430
|
- run: gem install git-pkgs
|
|
347
|
-
- run: git pkgs
|
|
348
|
-
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD
|
|
431
|
+
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD --stateless
|
|
349
432
|
```
|
|
350
433
|
|
|
351
434
|
### Diff driver
|
|
@@ -430,7 +513,7 @@ Optimizations:
|
|
|
430
513
|
|
|
431
514
|
git-pkgs uses [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary) for parsing, supporting:
|
|
432
515
|
|
|
433
|
-
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
|
|
516
|
+
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
|
|
434
517
|
|
|
435
518
|
SBOM formats (CycloneDX, SPDX) are not supported as they duplicate information from the actual lockfiles.
|
|
436
519
|
|
|
@@ -465,6 +548,7 @@ Git::Pkgs::Database.connect(repo_git_dir)
|
|
|
465
548
|
Git::Pkgs::Models::DependencyChange.where(name: "rails").all
|
|
466
549
|
```
|
|
467
550
|
|
|
551
|
+
|
|
468
552
|
## Contributing
|
|
469
553
|
|
|
470
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
|
@@ -12,33 +12,43 @@ 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
|
-
go.mod glide.yaml glide.lock Godeps Godeps/Godeps.json
|
|
21
|
+
go.mod go.sum glide.yaml glide.lock Godeps Godeps/Godeps.json
|
|
22
22
|
vendor/manifest vendor/vendor.json Gopkg.toml Gopkg.lock go-resolved-dependencies.json
|
|
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
|
-
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
|
-
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
|
@@ -33,13 +33,18 @@ 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"
|
|
39
|
+
},
|
|
40
|
+
"Security" => {
|
|
41
|
+
"vulns" => "Scan for known vulnerabilities"
|
|
37
42
|
}
|
|
38
43
|
}.freeze
|
|
39
44
|
|
|
40
45
|
COMMANDS = COMMAND_GROUPS.values.flat_map(&:keys).freeze
|
|
41
46
|
COMMAND_DESCRIPTIONS = COMMAND_GROUPS.values.reduce({}, :merge).freeze
|
|
42
|
-
ALIASES = { "praise" => "blame"
|
|
47
|
+
ALIASES = { "praise" => "blame" }.freeze
|
|
43
48
|
|
|
44
49
|
def self.run(args)
|
|
45
50
|
new(args).run
|
|
@@ -99,13 +104,20 @@ module Git
|
|
|
99
104
|
command = ALIASES.fetch(command, command)
|
|
100
105
|
# Convert kebab-case or snake_case to PascalCase
|
|
101
106
|
class_name = command.split(/[-_]/).map(&:capitalize).join
|
|
102
|
-
|
|
107
|
+
|
|
108
|
+
# Try with Command suffix first (e.g., VulnsCommand), then bare name
|
|
109
|
+
command_class = begin
|
|
110
|
+
Commands.const_get("#{class_name}Command")
|
|
111
|
+
rescue NameError
|
|
112
|
+
begin
|
|
113
|
+
Commands.const_get(class_name)
|
|
114
|
+
rescue NameError
|
|
115
|
+
$stderr.puts "Command '#{command}' not yet implemented"
|
|
116
|
+
exit 1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
103
120
|
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
121
|
end
|
|
110
122
|
|
|
111
123
|
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
|
|