git-pkgs 0.4.0 → 0.5.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 +15 -0
- data/README.md +48 -39
- data/lib/git/pkgs/analyzer.rb +35 -12
- data/lib/git/pkgs/cli.rb +83 -29
- data/lib/git/pkgs/commands/blame.rb +25 -7
- data/lib/git/pkgs/commands/branch.rb +27 -15
- data/lib/git/pkgs/commands/completions.rb +234 -0
- data/lib/git/pkgs/commands/diff.rb +34 -31
- data/lib/git/pkgs/commands/diff_driver.rb +9 -7
- data/lib/git/pkgs/commands/history.rb +0 -7
- data/lib/git/pkgs/commands/hooks.rb +8 -8
- data/lib/git/pkgs/commands/info.rb +65 -1
- data/lib/git/pkgs/commands/init.rb +31 -17
- data/lib/git/pkgs/commands/list.rb +2 -2
- data/lib/git/pkgs/commands/log.rb +5 -12
- data/lib/git/pkgs/commands/show.rb +3 -23
- data/lib/git/pkgs/commands/stale.rb +26 -7
- data/lib/git/pkgs/commands/stats.rb +9 -12
- data/lib/git/pkgs/commands/tree.rb +1 -1
- data/lib/git/pkgs/commands/update.rb +9 -7
- data/lib/git/pkgs/commands/upgrade.rb +4 -4
- data/lib/git/pkgs/config.rb +73 -0
- data/lib/git/pkgs/database.rb +7 -7
- data/lib/git/pkgs/models/commit.rb +19 -0
- data/lib/git/pkgs/output.rb +13 -0
- data/lib/git/pkgs/repository.rb +35 -1
- data/lib/git/pkgs/version.rb +1 -1
- data/lib/git/pkgs.rb +34 -0
- metadata +3 -10
- data/CODE_OF_CONDUCT.md +0 -10
- data/CONTRIBUTING.md +0 -27
- data/Rakefile +0 -8
- data/SECURITY.md +0 -7
- data/benchmark_bulk.rb +0 -167
- data/benchmark_db.rb +0 -138
- data/benchmark_detailed.rb +0 -151
- data/benchmark_full.rb +0 -131
- data/docs/schema.md +0 -129
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ff583fee83b937717da50de39743a8cfa5a75ce987df064edeaf89ea713f90f
|
|
4
|
+
data.tar.gz: eea893e5bacae2af0f2fdf8a8c1736f1d080a4b4bb111a2b94891f481f65c805
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95795186a511d176679a98a4f6e8e246bb1548456f041c3e5379d9705d3382deac69df2d81f0d187c294ba9af164f8c8372724b1f228326784a32a2952c98b98
|
|
7
|
+
data.tar.gz: 8fffd3620a9bfe95b4aa46e51c80dad6db2984829f653f6f28eccfc7d5a7c19aa480a20f7c7be30a929d1fe23cdfe6e368a2a529c9b2204319102ff9d1bbd9cd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-01-04
|
|
4
|
+
|
|
5
|
+
- `git pkgs init` now installs git hooks by default (use `--no-hooks` to skip)
|
|
6
|
+
- Parallel prefetching of git diffs for ~2x speedup on large repositories (1500+ commits)
|
|
7
|
+
- Performance tuning via environment variables: `GIT_PKGS_BATCH_SIZE`, `GIT_PKGS_SNAPSHOT_INTERVAL`, `GIT_PKGS_THREADS`
|
|
8
|
+
- `git pkgs completions` command for bash/zsh tab completion
|
|
9
|
+
- Fix N+1 queries in `blame`, `stale`, `stats`, and `log` commands
|
|
10
|
+
- Configuration via git config: `pkgs.ecosystems`, `pkgs.ignoredDirs`, `pkgs.ignoredFiles`
|
|
11
|
+
- `git pkgs info --ecosystems` to show available ecosystems and their status
|
|
12
|
+
- `-q, --quiet` flag to suppress informational messages
|
|
13
|
+
- `git pkgs diff` now supports `commit..commit` range syntax
|
|
14
|
+
- `--git-dir` and `--work-tree` global options (also respects `GIT_WORK_TREE` env var)
|
|
15
|
+
- Grouped commands by category in help output
|
|
16
|
+
- Fix crash when parsing manifests that return no dependencies
|
|
17
|
+
|
|
3
18
|
## [0.4.0] - 2026-01-04
|
|
4
19
|
|
|
5
20
|
- `git pkgs where` command to find where a package is declared in manifest files
|
data/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Options:
|
|
|
43
43
|
- `--branch=NAME` - analyze a specific branch (default: default branch)
|
|
44
44
|
- `--since=SHA` - start analysis from a specific commit
|
|
45
45
|
- `--force` - rebuild the database from scratch
|
|
46
|
-
- `--hooks` -
|
|
46
|
+
- `--no-hooks` - skip installing git hooks (hooks are installed by default)
|
|
47
47
|
|
|
48
48
|
Example output:
|
|
49
49
|
```
|
|
@@ -52,7 +52,7 @@ Processing commit 5191/5191...
|
|
|
52
52
|
Done!
|
|
53
53
|
Analyzed 5191 commits
|
|
54
54
|
Found 2531 commits with dependency changes
|
|
55
|
-
Stored 28239 snapshots (every
|
|
55
|
+
Stored 28239 snapshots (every 50 changes)
|
|
56
56
|
Blob cache: 3141 unique blobs, 2349 had cache hits
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -86,7 +86,7 @@ Snapshot Coverage
|
|
|
86
86
|
----------------------------------------
|
|
87
87
|
Commits with dependency changes: 2531
|
|
88
88
|
Commits with snapshots: 127
|
|
89
|
-
Coverage:
|
|
89
|
+
Coverage: 2.0% (1 snapshot per ~50 changes)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
### List dependencies
|
|
@@ -287,16 +287,18 @@ Like `git log` but only shows commits that changed dependencies, with the change
|
|
|
287
287
|
|
|
288
288
|
### Keep database updated
|
|
289
289
|
|
|
290
|
-
After the initial analysis,
|
|
290
|
+
After the initial analysis, the database updates automatically via git hooks installed during init. You can also update manually:
|
|
291
291
|
|
|
292
292
|
```bash
|
|
293
293
|
git pkgs update
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
To manage hooks separately:
|
|
297
297
|
|
|
298
298
|
```bash
|
|
299
|
-
git pkgs hooks
|
|
299
|
+
git pkgs hooks # show hook status
|
|
300
|
+
git pkgs hooks --install # install hooks
|
|
301
|
+
git pkgs hooks --uninstall # remove hooks
|
|
300
302
|
```
|
|
301
303
|
|
|
302
304
|
### Upgrading
|
|
@@ -369,6 +371,21 @@ diff --git a/Gemfile.lock b/Gemfile.lock
|
|
|
369
371
|
|
|
370
372
|
Use `git diff --no-textconv` to see the raw lockfile diff. To remove: `git pkgs diff-driver --uninstall`
|
|
371
373
|
|
|
374
|
+
### Shell completions
|
|
375
|
+
|
|
376
|
+
Enable tab completion for commands:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
# Bash: add to ~/.bashrc
|
|
380
|
+
eval "$(git pkgs completions bash)"
|
|
381
|
+
|
|
382
|
+
# Zsh: add to ~/.zshrc
|
|
383
|
+
eval "$(git pkgs completions zsh)"
|
|
384
|
+
|
|
385
|
+
# Or auto-install to standard completion directories
|
|
386
|
+
git pkgs completions install
|
|
387
|
+
```
|
|
388
|
+
|
|
372
389
|
## Configuration
|
|
373
390
|
|
|
374
391
|
git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-config).
|
|
@@ -377,6 +394,21 @@ git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-conf
|
|
|
377
394
|
|
|
378
395
|
**Pager** follows git's precedence: `GIT_PAGER` env, `core.pager` config, `PAGER` env, then `less -FRSX`. Use `--no-pager` flag or `git config core.pager cat` to disable.
|
|
379
396
|
|
|
397
|
+
**Ecosystem filtering** lets you limit which package ecosystems are tracked:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
git config --add pkgs.ecosystems rubygems
|
|
401
|
+
git config --add pkgs.ecosystems npm
|
|
402
|
+
git pkgs info --ecosystems # show enabled/disabled ecosystems
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Ignored paths** let you skip directories or files from analysis:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
git config --add pkgs.ignoredDirs third_party
|
|
409
|
+
git config --add pkgs.ignoredFiles test/fixtures/package.json
|
|
410
|
+
```
|
|
411
|
+
|
|
380
412
|
**Environment variables:**
|
|
381
413
|
|
|
382
414
|
- `GIT_DIR` - git directory location (standard git variable)
|
|
@@ -384,55 +416,32 @@ git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-conf
|
|
|
384
416
|
|
|
385
417
|
## Performance
|
|
386
418
|
|
|
387
|
-
Benchmarked on
|
|
419
|
+
Benchmarked on an M1 MacBook Pro analyzing [octobox](https://github.com/octobox/octobox) (5191 commits, 8 years of history): init takes about 18 seconds at roughly 300 commits/sec, producing an 8.3 MB database. About half the commits (2531) had dependency changes.
|
|
388
420
|
|
|
389
421
|
Optimizations:
|
|
390
|
-
- Bulk inserts with transaction batching (
|
|
422
|
+
- Bulk inserts with transaction batching (500 commits per transaction)
|
|
391
423
|
- Blob SHA caching (75% cache hit rate for repeated manifest content)
|
|
392
424
|
- Deferred index creation during bulk load
|
|
393
|
-
- Sparse snapshots (every
|
|
425
|
+
- Sparse snapshots (every 50 dependency-changing commits) for storage efficiency
|
|
394
426
|
- SQLite WAL mode for write performance
|
|
395
427
|
|
|
396
428
|
## Supported ecosystems
|
|
397
429
|
|
|
398
430
|
git-pkgs uses [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary) for parsing, supporting:
|
|
399
431
|
|
|
400
|
-
Actions,
|
|
401
|
-
|
|
402
|
-
## How it works
|
|
432
|
+
Actions, BentoML, Bower, Cargo, CocoaPods, Cog, Conda, CPAN, CRAN, Docker, Dub, DVC, Elm, Go, Haxelib, Homebrew, Julia, Maven, Meteor, MLflow, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, Vcpkg
|
|
403
433
|
|
|
404
|
-
|
|
434
|
+
Some ecosystems require remote parsing services and are disabled by default: Carthage, Clojars, Hackage, Hex, SwiftPM. Enable with `git config --add pkgs.ecosystems <name>`.
|
|
405
435
|
|
|
406
|
-
|
|
407
|
-
- Commits with dependency changes
|
|
408
|
-
- Dependency changes (added/modified/removed) with before/after versions
|
|
409
|
-
- Periodic snapshots of full dependency state for efficient point-in-time queries
|
|
436
|
+
SBOM formats (CycloneDX, SPDX) are not supported as they duplicate information from the actual lockfiles.
|
|
410
437
|
|
|
411
|
-
|
|
438
|
+
## Contributing
|
|
412
439
|
|
|
413
|
-
|
|
440
|
+
Bug reports, feature requests, and pull requests are welcome. If you're unsure about a change, open an issue first to discuss it.
|
|
414
441
|
|
|
415
|
-
|
|
416
|
-
sqlite3 .git/pkgs.sqlite3 "
|
|
417
|
-
-- who added the most dependencies?
|
|
418
|
-
SELECT c.author_name, COUNT(*) as deps_added
|
|
419
|
-
FROM dependency_changes dc
|
|
420
|
-
JOIN commits c ON dc.commit_id = c.id
|
|
421
|
-
WHERE dc.change_type = 'added'
|
|
422
|
-
GROUP BY c.author_name
|
|
423
|
-
ORDER BY deps_added DESC
|
|
424
|
-
LIMIT 10;
|
|
425
|
-
"
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
## Development
|
|
442
|
+
Good first contributions: adding tests, improving error messages, or supporting new manifest formats via [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary).
|
|
429
443
|
|
|
430
|
-
|
|
431
|
-
git clone https://github.com/andrew/git-pkgs
|
|
432
|
-
cd git-pkgs
|
|
433
|
-
bin/setup
|
|
434
|
-
rake test
|
|
435
|
-
```
|
|
444
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and architecture docs.
|
|
436
445
|
|
|
437
446
|
## License
|
|
438
447
|
|
data/lib/git/pkgs/analyzer.rb
CHANGED
|
@@ -24,8 +24,6 @@ module Git
|
|
|
24
24
|
Podfile Podfile.lock *.podspec *.podspec.json
|
|
25
25
|
packages.config packages.lock.json Project.json Project.lock.json
|
|
26
26
|
*.nuspec paket.lock *.csproj project.assets.json
|
|
27
|
-
cyclonedx.xml cyclonedx.json *.cdx.xml *.cdx.json
|
|
28
|
-
*.spdx *.spdx.json
|
|
29
27
|
bower.json bentofile.yaml
|
|
30
28
|
META.json META.yml
|
|
31
29
|
environment.yml environment.yaml
|
|
@@ -56,6 +54,8 @@ module Git
|
|
|
56
54
|
def initialize(repository)
|
|
57
55
|
@repository = repository
|
|
58
56
|
@blob_cache = {}
|
|
57
|
+
@manifest_path_cache = {}
|
|
58
|
+
Config.configure_bibliothecary
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Quick check if any paths might be manifests (fast regex check)
|
|
@@ -63,6 +63,23 @@ module Git
|
|
|
63
63
|
paths.any? { |p| p.match?(QUICK_MANIFEST_REGEX) }
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Cached version of Bibliothecary.identify_manifests
|
|
67
|
+
def identify_manifests_cached(paths)
|
|
68
|
+
uncached = paths.reject { |p| @manifest_path_cache.key?(p) }
|
|
69
|
+
|
|
70
|
+
if uncached.any?
|
|
71
|
+
# Call Bibliothecary only for uncached paths
|
|
72
|
+
manifests = Bibliothecary.identify_manifests(uncached)
|
|
73
|
+
manifest_set = manifests.to_set
|
|
74
|
+
|
|
75
|
+
uncached.each do |path|
|
|
76
|
+
@manifest_path_cache[path] = manifest_set.include?(path)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
paths.select { |p| @manifest_path_cache[p] }
|
|
81
|
+
end
|
|
82
|
+
|
|
66
83
|
# Quick check if a commit touches any manifest files
|
|
67
84
|
def has_manifest_changes?(rugged_commit)
|
|
68
85
|
return false if repository.merge_commit?(rugged_commit)
|
|
@@ -72,7 +89,7 @@ module Git
|
|
|
72
89
|
|
|
73
90
|
return false unless might_have_manifests?(all_paths)
|
|
74
91
|
|
|
75
|
-
|
|
92
|
+
identify_manifests_cached(all_paths).any?
|
|
76
93
|
end
|
|
77
94
|
|
|
78
95
|
def analyze_commit(rugged_commit, previous_snapshot = {})
|
|
@@ -87,9 +104,9 @@ module Git
|
|
|
87
104
|
all_paths = added_paths + modified_paths + removed_paths
|
|
88
105
|
return nil unless might_have_manifests?(all_paths)
|
|
89
106
|
|
|
90
|
-
added_manifests =
|
|
91
|
-
modified_manifests =
|
|
92
|
-
removed_manifests =
|
|
107
|
+
added_manifests = identify_manifests_cached(added_paths)
|
|
108
|
+
modified_manifests = identify_manifests_cached(modified_paths)
|
|
109
|
+
removed_manifests = identify_manifests_cached(removed_paths)
|
|
93
110
|
|
|
94
111
|
return nil if added_manifests.empty? && modified_manifests.empty? && removed_manifests.empty?
|
|
95
112
|
|
|
@@ -99,7 +116,7 @@ module Git
|
|
|
99
116
|
# Process added manifest files
|
|
100
117
|
added_manifests.each do |manifest_path|
|
|
101
118
|
result = parse_manifest_at_commit(rugged_commit, manifest_path)
|
|
102
|
-
next unless result
|
|
119
|
+
next unless result && result[:dependencies]
|
|
103
120
|
|
|
104
121
|
result[:dependencies].each do |dep|
|
|
105
122
|
changes << {
|
|
@@ -203,7 +220,7 @@ module Git
|
|
|
203
220
|
# Process removed manifest files
|
|
204
221
|
removed_manifests.each do |manifest_path|
|
|
205
222
|
result = parse_manifest_before_commit(rugged_commit, manifest_path)
|
|
206
|
-
next unless result
|
|
223
|
+
next unless result && result[:dependencies]
|
|
207
224
|
|
|
208
225
|
result[:dependencies].each do |dep|
|
|
209
226
|
changes << {
|
|
@@ -229,9 +246,14 @@ module Git
|
|
|
229
246
|
|
|
230
247
|
# Cache stats for debugging
|
|
231
248
|
def cache_stats
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
249
|
+
blob_hits = @blob_cache.values.count { |v| v[:hits] > 0 }
|
|
250
|
+
blob_total = @blob_cache.size
|
|
251
|
+
manifest_paths = @manifest_path_cache.size
|
|
252
|
+
{
|
|
253
|
+
cached_blobs: blob_total,
|
|
254
|
+
blobs_with_hits: blob_hits,
|
|
255
|
+
cached_paths: manifest_paths
|
|
256
|
+
}
|
|
235
257
|
end
|
|
236
258
|
|
|
237
259
|
def parse_manifest_at_commit(rugged_commit, manifest_path)
|
|
@@ -251,7 +273,7 @@ module Git
|
|
|
251
273
|
end
|
|
252
274
|
|
|
253
275
|
def parse_manifest_by_oid(blob_oid, manifest_path)
|
|
254
|
-
cache_key =
|
|
276
|
+
cache_key = [blob_oid, manifest_path]
|
|
255
277
|
|
|
256
278
|
if @blob_cache.key?(cache_key)
|
|
257
279
|
@blob_cache[cache_key][:hits] += 1
|
|
@@ -262,6 +284,7 @@ module Git
|
|
|
262
284
|
return nil unless content
|
|
263
285
|
|
|
264
286
|
result = Bibliothecary.analyse_file(manifest_path, content).first
|
|
287
|
+
result = nil if result && Config.filter_ecosystem?(result[:platform])
|
|
265
288
|
@blob_cache[cache_key] = { result: result, hits: 0 }
|
|
266
289
|
result
|
|
267
290
|
end
|
data/lib/git/pkgs/cli.rb
CHANGED
|
@@ -5,7 +5,40 @@ require "optparse"
|
|
|
5
5
|
module Git
|
|
6
6
|
module Pkgs
|
|
7
7
|
class CLI
|
|
8
|
-
|
|
8
|
+
COMMAND_GROUPS = {
|
|
9
|
+
"Setup" => {
|
|
10
|
+
"init" => "Initialize the package database for this repository",
|
|
11
|
+
"update" => "Update the database with new commits",
|
|
12
|
+
"hooks" => "Manage git hooks for auto-updating",
|
|
13
|
+
"upgrade" => "Upgrade database after git-pkgs update",
|
|
14
|
+
"info" => "Show database size and row counts",
|
|
15
|
+
"branch" => "Manage tracked branches",
|
|
16
|
+
"schema" => "Show database schema",
|
|
17
|
+
"diff-driver" => "Install git textconv driver for lockfile diffs",
|
|
18
|
+
"completions" => "Generate shell completions"
|
|
19
|
+
},
|
|
20
|
+
"Query" => {
|
|
21
|
+
"list" => "List dependencies at a commit",
|
|
22
|
+
"tree" => "Show dependency tree grouped by type",
|
|
23
|
+
"search" => "Find a dependency across all history",
|
|
24
|
+
"where" => "Show where a package appears in manifest files",
|
|
25
|
+
"why" => "Explain why a dependency exists"
|
|
26
|
+
},
|
|
27
|
+
"History" => {
|
|
28
|
+
"history" => "Show the history of a package",
|
|
29
|
+
"blame" => "Show who added each dependency",
|
|
30
|
+
"log" => "List commits with dependency changes",
|
|
31
|
+
"show" => "Show dependency changes in a commit",
|
|
32
|
+
"diff" => "Show dependency changes between commits"
|
|
33
|
+
},
|
|
34
|
+
"Analysis" => {
|
|
35
|
+
"stats" => "Show dependency statistics",
|
|
36
|
+
"stale" => "Show dependencies that haven't been updated"
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
COMMANDS = COMMAND_GROUPS.values.flat_map(&:keys).freeze
|
|
41
|
+
COMMAND_DESCRIPTIONS = COMMAND_GROUPS.values.reduce({}, :merge).freeze
|
|
9
42
|
ALIASES = { "praise" => "blame", "outdated" => "stale" }.freeze
|
|
10
43
|
|
|
11
44
|
def self.run(args)
|
|
@@ -18,6 +51,9 @@ module Git
|
|
|
18
51
|
end
|
|
19
52
|
|
|
20
53
|
def run
|
|
54
|
+
Git::Pkgs.configure_from_env
|
|
55
|
+
parse_global_options
|
|
56
|
+
|
|
21
57
|
command = @args.shift
|
|
22
58
|
|
|
23
59
|
case command
|
|
@@ -34,48 +70,66 @@ module Git
|
|
|
34
70
|
end
|
|
35
71
|
end
|
|
36
72
|
|
|
73
|
+
def parse_global_options
|
|
74
|
+
while @args.first&.start_with?("-")
|
|
75
|
+
arg = @args.first
|
|
76
|
+
case arg
|
|
77
|
+
when "-q", "--quiet"
|
|
78
|
+
Git::Pkgs.quiet = true
|
|
79
|
+
@args.shift
|
|
80
|
+
when /^--git-dir=(.+)$/
|
|
81
|
+
Git::Pkgs.git_dir = $1
|
|
82
|
+
@args.shift
|
|
83
|
+
when "--git-dir"
|
|
84
|
+
@args.shift
|
|
85
|
+
Git::Pkgs.git_dir = @args.shift
|
|
86
|
+
when /^--work-tree=(.+)$/
|
|
87
|
+
Git::Pkgs.work_tree = $1
|
|
88
|
+
@args.shift
|
|
89
|
+
when "--work-tree"
|
|
90
|
+
@args.shift
|
|
91
|
+
Git::Pkgs.work_tree = @args.shift
|
|
92
|
+
else
|
|
93
|
+
break
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
37
98
|
def run_command(command)
|
|
38
99
|
command = ALIASES.fetch(command, command)
|
|
39
100
|
# Convert kebab-case or snake_case to PascalCase
|
|
40
101
|
class_name = command.split(/[-_]/).map(&:capitalize).join
|
|
41
102
|
command_class = Commands.const_get(class_name)
|
|
42
103
|
command_class.new(@args).run
|
|
43
|
-
rescue NameError
|
|
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)
|
|
44
107
|
$stderr.puts "Command '#{command}' not yet implemented"
|
|
45
108
|
exit 1
|
|
46
109
|
end
|
|
47
110
|
|
|
48
111
|
def print_help
|
|
49
|
-
puts
|
|
50
|
-
|
|
112
|
+
puts "Usage: git pkgs <command> [options]"
|
|
113
|
+
puts
|
|
51
114
|
|
|
52
|
-
|
|
53
|
-
init Initialize the package database for this repository
|
|
54
|
-
update Update the database with new commits
|
|
55
|
-
hooks Manage git hooks for auto-updating
|
|
56
|
-
info Show database size and row counts
|
|
57
|
-
branch Manage tracked branches
|
|
58
|
-
list List dependencies at a commit
|
|
59
|
-
tree Show dependency tree grouped by type
|
|
60
|
-
history Show the history of a package
|
|
61
|
-
search Find a dependency across all history
|
|
62
|
-
where Show where a package appears in manifest files
|
|
63
|
-
why Explain why a dependency exists
|
|
64
|
-
blame Show who added each dependency
|
|
65
|
-
stale Show dependencies that haven't been updated
|
|
66
|
-
stats Show dependency statistics
|
|
67
|
-
diff Show dependency changes between commits
|
|
68
|
-
show Show dependency changes in a commit
|
|
69
|
-
log List commits with dependency changes
|
|
70
|
-
upgrade Upgrade database after git-pkgs update
|
|
71
|
-
schema Show database schema
|
|
115
|
+
max_cmd_len = COMMANDS.map(&:length).max
|
|
72
116
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
COMMAND_GROUPS.each do |group, commands|
|
|
118
|
+
puts "#{group}:"
|
|
119
|
+
commands.each do |cmd, desc|
|
|
120
|
+
puts " #{cmd.ljust(max_cmd_len)} #{desc}"
|
|
121
|
+
end
|
|
122
|
+
puts
|
|
123
|
+
end
|
|
76
124
|
|
|
77
|
-
|
|
78
|
-
|
|
125
|
+
puts "Options:"
|
|
126
|
+
puts " -h, --help Show this help message"
|
|
127
|
+
puts " -v, --version Show version"
|
|
128
|
+
puts " -q, --quiet Suppress informational messages"
|
|
129
|
+
puts " --git-dir=<path> Path to the git directory"
|
|
130
|
+
puts " --work-tree=<path> Path to the working tree"
|
|
131
|
+
puts
|
|
132
|
+
puts "Run 'git pkgs <command> -h' for command-specific options."
|
|
79
133
|
end
|
|
80
134
|
end
|
|
81
135
|
end
|
|
@@ -21,7 +21,7 @@ module Git
|
|
|
21
21
|
branch_name = @options[:branch] || repo.default_branch
|
|
22
22
|
branch = Models::Branch.find_by(name: branch_name)
|
|
23
23
|
|
|
24
|
-
error "No analysis found for branch '#{branch_name}'" unless branch&.last_analyzed_sha
|
|
24
|
+
error "No analysis found for branch '#{branch_name}'. Run 'git pkgs init' first." unless branch&.last_analyzed_sha
|
|
25
25
|
|
|
26
26
|
current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
|
|
27
27
|
snapshots = current_commit&.dependency_snapshots&.includes(:manifest) || []
|
|
@@ -35,16 +35,34 @@ module Git
|
|
|
35
35
|
return
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Batch fetch all "added" changes for current dependencies
|
|
39
|
+
snapshot_keys = snapshots.map { |s| [s.name, s.manifest_id] }.to_set
|
|
40
|
+
manifest_ids = snapshots.map(&:manifest_id).uniq
|
|
41
|
+
names = snapshots.map(&:name).uniq
|
|
42
|
+
|
|
43
|
+
all_added_changes = Models::DependencyChange
|
|
44
|
+
.includes(:commit)
|
|
45
|
+
.added
|
|
46
|
+
.where(manifest_id: manifest_ids, name: names)
|
|
47
|
+
.to_a
|
|
48
|
+
|
|
49
|
+
# Group by (name, manifest_id) and find earliest by committed_at
|
|
50
|
+
added_by_key = {}
|
|
51
|
+
all_added_changes.each do |change|
|
|
52
|
+
key = [change.name, change.manifest_id]
|
|
53
|
+
next unless snapshot_keys.include?(key)
|
|
54
|
+
|
|
55
|
+
existing = added_by_key[key]
|
|
56
|
+
if existing.nil? || change.commit.committed_at < existing.commit.committed_at
|
|
57
|
+
added_by_key[key] = change
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
38
61
|
# For each current dependency, find who added it
|
|
39
62
|
blame_data = []
|
|
40
63
|
|
|
41
64
|
snapshots.each do |snapshot|
|
|
42
|
-
added_change =
|
|
43
|
-
.includes(:commit)
|
|
44
|
-
.where(name: snapshot.name, manifest: snapshot.manifest)
|
|
45
|
-
.added
|
|
46
|
-
.order("commits.committed_at ASC")
|
|
47
|
-
.first
|
|
65
|
+
added_change = added_by_key[[snapshot.name, snapshot.manifest_id]]
|
|
48
66
|
|
|
49
67
|
next unless added_change
|
|
50
68
|
|
|
@@ -6,8 +6,16 @@ module Git
|
|
|
6
6
|
class Branch
|
|
7
7
|
include Output
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
DEFAULT_BATCH_SIZE = 500
|
|
10
|
+
DEFAULT_SNAPSHOT_INTERVAL = 50
|
|
11
|
+
|
|
12
|
+
def batch_size
|
|
13
|
+
Git::Pkgs.batch_size || DEFAULT_BATCH_SIZE
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def snapshot_interval
|
|
17
|
+
Git::Pkgs.snapshot_interval || DEFAULT_SNAPSHOT_INTERVAL
|
|
18
|
+
end
|
|
11
19
|
|
|
12
20
|
def initialize(args)
|
|
13
21
|
@args = args
|
|
@@ -40,12 +48,12 @@ module Git
|
|
|
40
48
|
|
|
41
49
|
Database.connect(repo.git_dir)
|
|
42
50
|
|
|
43
|
-
error "Branch '#{branch_name}' not found" unless repo.branch_exists?(branch_name)
|
|
51
|
+
error "Branch '#{branch_name}' not found. Check 'git branch -a' for available branches." unless repo.branch_exists?(branch_name)
|
|
44
52
|
|
|
45
53
|
existing = Models::Branch.find_by(name: branch_name)
|
|
46
54
|
if existing
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
info "Branch '#{branch_name}' already tracked (#{existing.commits.count} commits)"
|
|
56
|
+
info "Use 'git pkgs update' to refresh"
|
|
49
57
|
return
|
|
50
58
|
end
|
|
51
59
|
|
|
@@ -54,11 +62,15 @@ module Git
|
|
|
54
62
|
branch = Models::Branch.create!(name: branch_name)
|
|
55
63
|
analyzer = Analyzer.new(repo)
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
info "Analyzing branch: #{branch_name}"
|
|
58
66
|
|
|
67
|
+
print "Loading commits..." unless Git::Pkgs.quiet
|
|
59
68
|
walker = repo.walk(branch_name)
|
|
60
69
|
commits = walker.to_a
|
|
61
70
|
total = commits.size
|
|
71
|
+
print "\rPrefetching diffs..." unless Git::Pkgs.quiet
|
|
72
|
+
repo.prefetch_blob_paths(commits)
|
|
73
|
+
print "\r#{' ' * 20}\r" unless Git::Pkgs.quiet
|
|
62
74
|
|
|
63
75
|
stats = bulk_process_commits(commits, branch, analyzer, total, repo)
|
|
64
76
|
|
|
@@ -66,10 +78,10 @@ module Git
|
|
|
66
78
|
|
|
67
79
|
Database.optimize_for_reads
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
info "\rDone!#{' ' * 20}"
|
|
82
|
+
info "Analyzed #{total} commits"
|
|
83
|
+
info "Found #{stats[:dependency_commits]} commits with dependency changes"
|
|
84
|
+
info "Stored #{stats[:snapshots_stored]} snapshots"
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
def list_branches
|
|
@@ -104,14 +116,14 @@ module Git
|
|
|
104
116
|
Database.connect(repo.git_dir)
|
|
105
117
|
|
|
106
118
|
branch = Models::Branch.find_by(name: branch_name)
|
|
107
|
-
error "Branch '#{branch_name}' not tracked" unless branch
|
|
119
|
+
error "Branch '#{branch_name}' not tracked. Run 'git pkgs branch list' to see tracked branches." unless branch
|
|
108
120
|
|
|
109
121
|
# Only delete branch_commits, keep shared commits
|
|
110
122
|
count = branch.branch_commits.count
|
|
111
123
|
branch.branch_commits.delete_all
|
|
112
124
|
branch.destroy
|
|
113
125
|
|
|
114
|
-
|
|
126
|
+
info "Removed branch '#{branch_name}' (#{count} branch-commit links)"
|
|
115
127
|
end
|
|
116
128
|
|
|
117
129
|
def bulk_process_commits(commits, branch, analyzer, total, repo)
|
|
@@ -196,7 +208,7 @@ module Git
|
|
|
196
208
|
|
|
197
209
|
commits.each do |rugged_commit|
|
|
198
210
|
processed += 1
|
|
199
|
-
print "\rProcessing commit #{processed}/#{total}..." if processed % 50 == 0 || processed == total
|
|
211
|
+
print "\rProcessing commit #{processed}/#{total}..." if !Git::Pkgs.quiet && (processed % 50 == 0 || processed == total)
|
|
200
212
|
|
|
201
213
|
next if rugged_commit.parents.length > 1
|
|
202
214
|
|
|
@@ -247,7 +259,7 @@ module Git
|
|
|
247
259
|
|
|
248
260
|
snapshot = result[:snapshot]
|
|
249
261
|
|
|
250
|
-
if dependency_commit_count %
|
|
262
|
+
if dependency_commit_count % snapshot_interval == 0
|
|
251
263
|
snapshot.each do |(manifest_path, name), dep_info|
|
|
252
264
|
pending_snapshots << {
|
|
253
265
|
sha: rugged_commit.oid,
|
|
@@ -262,7 +274,7 @@ module Git
|
|
|
262
274
|
end
|
|
263
275
|
end
|
|
264
276
|
|
|
265
|
-
flush.call if pending_commits.size >=
|
|
277
|
+
flush.call if pending_commits.size >= batch_size
|
|
266
278
|
end
|
|
267
279
|
|
|
268
280
|
if snapshot.any?
|