git-pkgs 0.1.0 → 0.2.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 +76 -15
- data/lib/git/pkgs/cli.rb +2 -1
- data/lib/git/pkgs/commands/diff.rb +45 -12
- data/lib/git/pkgs/commands/history.rb +55 -10
- data/lib/git/pkgs/commands/show.rb +166 -0
- data/lib/git/pkgs/commands/stats.rb +71 -12
- data/lib/git/pkgs/repository.rb +6 -0
- data/lib/git/pkgs/version.rb +1 -1
- data/lib/git/pkgs.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ff2285e54475944f6c8a9cb98ae9f5efcb78ee0aa74dbea6d46553bbf71caa2
|
|
4
|
+
data.tar.gz: 81aee90f3e0b9f573e1f9e442dc4c487f07957c320d70ca24a809929248279a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffdcb5fe7cc217b105f10018ba24101131120fdfcc8305f3142fcc11f29e7f77114e95c0a2abbe661fcac3a74e4ea0662031dc6a997020225bdabcc9b6ab754b
|
|
7
|
+
data.tar.gz: 204dc587c61ccb128957910784ccab0cb84c667627dff1fb81d18a6b6779abc34cfff3a838f06146c954d1a50b9ac095a036a67c898d12699c530476b9b3d31c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-01-02
|
|
4
|
+
|
|
5
|
+
- `git pkgs show` command to display dependency changes in a single commit
|
|
6
|
+
- `git pkgs history` now supports `--author`, `--since`, and `--until` filters
|
|
7
|
+
- `git pkgs stats --by-author` shows who added the most dependencies
|
|
8
|
+
- `git pkgs stats --ecosystem=X` filters statistics by ecosystem
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2026-01-01
|
|
11
|
+
|
|
12
|
+
- `git pkgs history` now works without a package argument to show all dependency changes
|
|
13
|
+
- `git pkgs diff` supports git refs (HEAD~10, branch names, tags) not just SHAs
|
|
14
|
+
- `git pkgs diff` lazily inserts commits not found in the database
|
|
15
|
+
- Expanded manifest file pattern matching for all supported ecosystems
|
|
16
|
+
- Switched to ecosystems-bibliothecary
|
|
17
|
+
|
|
3
18
|
## [0.1.0] - 2026-01-01
|
|
4
19
|
|
|
5
20
|
- Initial release
|
data/README.md
CHANGED
|
@@ -4,11 +4,9 @@ A git subcommand for tracking package dependencies across git history. Analyzes
|
|
|
4
4
|
|
|
5
5
|
## Why this exists
|
|
6
6
|
|
|
7
|
-
Your lockfile shows what dependencies you have
|
|
7
|
+
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
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
It works across ecosystems. Gemfile, package.json, Dockerfile, GitHub Actions workflows - one unified history instead of separate tools per ecosystem.
|
|
9
|
+
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.
|
|
12
10
|
|
|
13
11
|
## Installation
|
|
14
12
|
|
|
@@ -20,10 +18,15 @@ gem install git-pkgs
|
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
cd your-repo
|
|
23
|
-
git pkgs init
|
|
24
|
-
git pkgs
|
|
25
|
-
git pkgs
|
|
26
|
-
git pkgs
|
|
21
|
+
git pkgs init # analyze history (one-time, ~300 commits/sec)
|
|
22
|
+
git pkgs list # show current dependencies
|
|
23
|
+
git pkgs stats # see overview
|
|
24
|
+
git pkgs blame # who added each dependency
|
|
25
|
+
git pkgs history # all dependency changes over time
|
|
26
|
+
git pkgs history rails # track a specific package
|
|
27
|
+
git pkgs why rails # why was this added?
|
|
28
|
+
git pkgs diff --from=HEAD~10 # what changed recently?
|
|
29
|
+
git pkgs diff --from=main --to=feature # compare branches
|
|
27
30
|
```
|
|
28
31
|
|
|
29
32
|
## Commands
|
|
@@ -105,13 +108,17 @@ Gemfile (rubygems):
|
|
|
105
108
|
...
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
### View
|
|
111
|
+
### View dependency history
|
|
109
112
|
|
|
110
113
|
```bash
|
|
111
|
-
git pkgs history
|
|
114
|
+
git pkgs history # all dependency changes
|
|
115
|
+
git pkgs history rails # changes for a specific package
|
|
116
|
+
git pkgs history --author=alice # filter by author
|
|
117
|
+
git pkgs history --since=2024-01-01 # changes after date
|
|
118
|
+
git pkgs history --ecosystem=rubygems # filter by ecosystem
|
|
112
119
|
```
|
|
113
120
|
|
|
114
|
-
Shows when
|
|
121
|
+
Shows when packages were added, updated, or removed:
|
|
115
122
|
|
|
116
123
|
```
|
|
117
124
|
History for rails:
|
|
@@ -160,6 +167,8 @@ Gemfile (rubygems):
|
|
|
160
167
|
|
|
161
168
|
```bash
|
|
162
169
|
git pkgs stats
|
|
170
|
+
git pkgs stats --by-author # who added the most dependencies
|
|
171
|
+
git pkgs stats --ecosystem=npm # filter by ecosystem
|
|
163
172
|
```
|
|
164
173
|
|
|
165
174
|
Example output:
|
|
@@ -205,7 +214,7 @@ Manifest Files
|
|
|
205
214
|
git pkgs why rails
|
|
206
215
|
```
|
|
207
216
|
|
|
208
|
-
|
|
217
|
+
This shows the commit that added the dependency along with the author and message.
|
|
209
218
|
|
|
210
219
|
### Dependency tree
|
|
211
220
|
|
|
@@ -214,7 +223,7 @@ git pkgs tree
|
|
|
214
223
|
git pkgs tree --ecosystem=rubygems
|
|
215
224
|
```
|
|
216
225
|
|
|
217
|
-
|
|
226
|
+
This shows dependencies grouped by type (runtime, development, etc).
|
|
218
227
|
|
|
219
228
|
### Diff between commits
|
|
220
229
|
|
|
@@ -223,20 +232,57 @@ git pkgs diff --from=abc123 --to=def456
|
|
|
223
232
|
git pkgs diff --from=HEAD~10
|
|
224
233
|
```
|
|
225
234
|
|
|
226
|
-
|
|
235
|
+
This shows added, removed, and modified packages with version info.
|
|
236
|
+
|
|
237
|
+
### Show changes in a commit
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
git pkgs show # show dependency changes in HEAD
|
|
241
|
+
git pkgs show abc123 # specific commit
|
|
242
|
+
git pkgs show HEAD~5 # relative ref
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Like `git show` but for dependencies. Shows what was added, modified, or removed in a single commit.
|
|
227
246
|
|
|
228
247
|
### Keep database updated
|
|
229
248
|
|
|
249
|
+
After the initial analysis, you can incrementally update the database with new commits:
|
|
250
|
+
|
|
230
251
|
```bash
|
|
231
252
|
git pkgs update
|
|
232
253
|
```
|
|
233
254
|
|
|
234
|
-
|
|
255
|
+
You can also install git hooks to update automatically after commits and merges:
|
|
235
256
|
|
|
236
257
|
```bash
|
|
237
258
|
git pkgs hooks --install
|
|
238
259
|
```
|
|
239
260
|
|
|
261
|
+
### CI usage
|
|
262
|
+
|
|
263
|
+
You can run git-pkgs in CI to show dependency changes in pull requests:
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
# .github/workflows/deps.yml
|
|
267
|
+
name: Dependencies
|
|
268
|
+
|
|
269
|
+
on: pull_request
|
|
270
|
+
|
|
271
|
+
jobs:
|
|
272
|
+
diff:
|
|
273
|
+
runs-on: ubuntu-latest
|
|
274
|
+
steps:
|
|
275
|
+
- uses: actions/checkout@v4
|
|
276
|
+
with:
|
|
277
|
+
fetch-depth: 0
|
|
278
|
+
- uses: ruby/setup-ruby@v1
|
|
279
|
+
with:
|
|
280
|
+
ruby-version: '3.3'
|
|
281
|
+
- run: gem install git-pkgs
|
|
282
|
+
- run: git pkgs init
|
|
283
|
+
- run: git pkgs diff --from=origin/${{ github.base_ref }} --to=HEAD
|
|
284
|
+
```
|
|
285
|
+
|
|
240
286
|
## Performance
|
|
241
287
|
|
|
242
288
|
Benchmarked on a 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.
|
|
@@ -265,6 +311,21 @@ The database schema stores:
|
|
|
265
311
|
|
|
266
312
|
See [docs/schema.md](docs/schema.md) for full schema documentation.
|
|
267
313
|
|
|
314
|
+
Since the database is just SQLite, you can query it directly for ad-hoc analysis:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
sqlite3 .git/pkgs.sqlite3 "
|
|
318
|
+
-- who added the most dependencies?
|
|
319
|
+
SELECT c.author_name, COUNT(*) as deps_added
|
|
320
|
+
FROM dependency_changes dc
|
|
321
|
+
JOIN commits c ON dc.commit_id = c.id
|
|
322
|
+
WHERE dc.change_type = 'added'
|
|
323
|
+
GROUP BY c.author_name
|
|
324
|
+
ORDER BY deps_added DESC
|
|
325
|
+
LIMIT 10;
|
|
326
|
+
"
|
|
327
|
+
```
|
|
328
|
+
|
|
268
329
|
## Development
|
|
269
330
|
|
|
270
331
|
```bash
|
data/lib/git/pkgs/cli.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "optparse"
|
|
|
5
5
|
module Git
|
|
6
6
|
module Pkgs
|
|
7
7
|
class CLI
|
|
8
|
-
COMMANDS = %w[init update hooks info list tree history search why blame outdated stats diff branch].freeze
|
|
8
|
+
COMMANDS = %w[init update hooks info list tree history search why blame outdated stats diff branch show].freeze
|
|
9
9
|
|
|
10
10
|
def self.run(args)
|
|
11
11
|
new(args).run
|
|
@@ -60,6 +60,7 @@ module Git
|
|
|
60
60
|
outdated Show dependencies that haven't been updated
|
|
61
61
|
stats Show dependency statistics
|
|
62
62
|
diff Show dependency changes between commits
|
|
63
|
+
show Show dependency changes in a commit
|
|
63
64
|
|
|
64
65
|
Options:
|
|
65
66
|
-h, --help Show this help message
|
|
@@ -19,26 +19,38 @@ module Git
|
|
|
19
19
|
|
|
20
20
|
Database.connect(repo.git_dir)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
from_ref = @options[:from]
|
|
23
|
+
to_ref = @options[:to] || "HEAD"
|
|
24
|
+
|
|
25
|
+
unless from_ref
|
|
26
|
+
$stderr.puts "Usage: git pkgs diff --from=REF [--to=REF]"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Resolve git refs (like HEAD~10) to SHAs
|
|
31
|
+
from_sha = repo.rev_parse(from_ref)
|
|
32
|
+
to_sha = repo.rev_parse(to_ref)
|
|
24
33
|
|
|
25
34
|
unless from_sha
|
|
26
|
-
$stderr.puts "
|
|
35
|
+
$stderr.puts "Could not resolve '#{from_ref}'"
|
|
27
36
|
exit 1
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
unless to_sha
|
|
40
|
+
$stderr.puts "Could not resolve '#{to_ref}'"
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
from_commit = find_or_create_commit(repo, from_sha)
|
|
45
|
+
to_commit = find_or_create_commit(repo, to_sha)
|
|
34
46
|
|
|
35
47
|
unless from_commit
|
|
36
|
-
$stderr.puts "Commit '#{from_sha}' not found
|
|
48
|
+
$stderr.puts "Commit '#{from_sha[0..7]}' not found"
|
|
37
49
|
exit 1
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
unless to_commit
|
|
41
|
-
$stderr.puts "Commit '#{to_sha}' not found
|
|
53
|
+
$stderr.puts "Commit '#{to_sha[0..7]}' not found"
|
|
42
54
|
exit 1
|
|
43
55
|
end
|
|
44
56
|
|
|
@@ -98,17 +110,38 @@ module Git
|
|
|
98
110
|
puts "Summary: +#{added.map(&:name).uniq.count} -#{removed.map(&:name).uniq.count} ~#{modified.map(&:name).uniq.count}"
|
|
99
111
|
end
|
|
100
112
|
|
|
113
|
+
def find_or_create_commit(repo, sha)
|
|
114
|
+
commit = Models::Commit.find_by(sha: sha) ||
|
|
115
|
+
Models::Commit.where("sha LIKE ?", "#{sha}%").first
|
|
116
|
+
return commit if commit
|
|
117
|
+
|
|
118
|
+
# Lazily insert commit if it exists in git but not in database
|
|
119
|
+
rugged_commit = repo.lookup(sha)
|
|
120
|
+
return nil unless rugged_commit
|
|
121
|
+
|
|
122
|
+
Models::Commit.create!(
|
|
123
|
+
sha: rugged_commit.oid,
|
|
124
|
+
message: rugged_commit.message,
|
|
125
|
+
author_name: rugged_commit.author[:name],
|
|
126
|
+
author_email: rugged_commit.author[:email],
|
|
127
|
+
committed_at: rugged_commit.time,
|
|
128
|
+
has_dependency_changes: false
|
|
129
|
+
)
|
|
130
|
+
rescue Rugged::OdbError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
101
134
|
def parse_options
|
|
102
135
|
options = {}
|
|
103
136
|
|
|
104
137
|
parser = OptionParser.new do |opts|
|
|
105
|
-
opts.banner = "Usage: git pkgs diff --from=
|
|
138
|
+
opts.banner = "Usage: git pkgs diff --from=REF [--to=REF] [options]"
|
|
106
139
|
|
|
107
|
-
opts.on("-f", "--from=
|
|
140
|
+
opts.on("-f", "--from=REF", "Start commit (required)") do |v|
|
|
108
141
|
options[:from] = v
|
|
109
142
|
end
|
|
110
143
|
|
|
111
|
-
opts.on("-t", "--to=
|
|
144
|
+
opts.on("-t", "--to=REF", "End commit (default: HEAD)") do |v|
|
|
112
145
|
options[:to] = v
|
|
113
146
|
end
|
|
114
147
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module Git
|
|
4
6
|
module Pkgs
|
|
5
7
|
module Commands
|
|
@@ -12,11 +14,6 @@ module Git
|
|
|
12
14
|
def run
|
|
13
15
|
package_name = @args.shift
|
|
14
16
|
|
|
15
|
-
unless package_name
|
|
16
|
-
$stderr.puts "Usage: git pkgs history <package>"
|
|
17
|
-
exit 1
|
|
18
|
-
end
|
|
19
|
-
|
|
20
17
|
repo = Repository.new
|
|
21
18
|
|
|
22
19
|
unless Database.exists?(repo.git_dir)
|
|
@@ -28,15 +25,38 @@ module Git
|
|
|
28
25
|
|
|
29
26
|
changes = Models::DependencyChange
|
|
30
27
|
.includes(:commit, :manifest)
|
|
31
|
-
.for_package(package_name)
|
|
32
28
|
.order("commits.committed_at ASC")
|
|
33
29
|
|
|
30
|
+
changes = changes.for_package(package_name) if package_name
|
|
31
|
+
|
|
34
32
|
if @options[:ecosystem]
|
|
35
33
|
changes = changes.for_platform(@options[:ecosystem])
|
|
36
34
|
end
|
|
37
35
|
|
|
36
|
+
if @options[:author]
|
|
37
|
+
author = @options[:author]
|
|
38
|
+
changes = changes.joins(:commit).where(
|
|
39
|
+
"commits.author_name LIKE ? OR commits.author_email LIKE ?",
|
|
40
|
+
"%#{author}%", "%#{author}%"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if @options[:since]
|
|
45
|
+
since_time = parse_time(@options[:since])
|
|
46
|
+
changes = changes.joins(:commit).where("commits.committed_at >= ?", since_time)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if @options[:until]
|
|
50
|
+
until_time = parse_time(@options[:until])
|
|
51
|
+
changes = changes.joins(:commit).where("commits.committed_at <= ?", until_time)
|
|
52
|
+
end
|
|
53
|
+
|
|
38
54
|
if changes.empty?
|
|
39
|
-
|
|
55
|
+
if package_name
|
|
56
|
+
puts "No history found for '#{package_name}'"
|
|
57
|
+
else
|
|
58
|
+
puts "No dependency changes found"
|
|
59
|
+
end
|
|
40
60
|
return
|
|
41
61
|
end
|
|
42
62
|
|
|
@@ -48,7 +68,11 @@ module Git
|
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
def output_text(changes, package_name)
|
|
51
|
-
|
|
71
|
+
if package_name
|
|
72
|
+
puts "History for #{package_name}:"
|
|
73
|
+
else
|
|
74
|
+
puts "Dependency history:"
|
|
75
|
+
end
|
|
52
76
|
puts
|
|
53
77
|
|
|
54
78
|
changes.each do |change|
|
|
@@ -67,7 +91,8 @@ module Git
|
|
|
67
91
|
version_info = change.requirement
|
|
68
92
|
end
|
|
69
93
|
|
|
70
|
-
|
|
94
|
+
name_prefix = package_name ? "" : "#{change.name} "
|
|
95
|
+
puts "#{date} #{action} #{name_prefix}#{version_info}"
|
|
71
96
|
puts " Commit: #{commit.short_sha} #{commit.message&.lines&.first&.strip}"
|
|
72
97
|
puts " Author: #{commit.author_name} <#{commit.author_email}>"
|
|
73
98
|
puts " Manifest: #{change.manifest.path}"
|
|
@@ -80,6 +105,7 @@ module Git
|
|
|
80
105
|
|
|
81
106
|
data = changes.map do |change|
|
|
82
107
|
{
|
|
108
|
+
name: change.name,
|
|
83
109
|
date: change.commit.committed_at.iso8601,
|
|
84
110
|
change_type: change.change_type,
|
|
85
111
|
requirement: change.requirement,
|
|
@@ -102,7 +128,7 @@ module Git
|
|
|
102
128
|
options = {}
|
|
103
129
|
|
|
104
130
|
parser = OptionParser.new do |opts|
|
|
105
|
-
opts.banner = "Usage: git pkgs history
|
|
131
|
+
opts.banner = "Usage: git pkgs history [package] [options]"
|
|
106
132
|
|
|
107
133
|
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
108
134
|
options[:ecosystem] = v
|
|
@@ -112,6 +138,18 @@ module Git
|
|
|
112
138
|
options[:format] = v
|
|
113
139
|
end
|
|
114
140
|
|
|
141
|
+
opts.on("--author=NAME", "Filter by author name or email") do |v|
|
|
142
|
+
options[:author] = v
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
opts.on("--since=DATE", "Show changes after date (YYYY-MM-DD)") do |v|
|
|
146
|
+
options[:since] = v
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
opts.on("--until=DATE", "Show changes before date (YYYY-MM-DD)") do |v|
|
|
150
|
+
options[:until] = v
|
|
151
|
+
end
|
|
152
|
+
|
|
115
153
|
opts.on("-h", "--help", "Show this help") do
|
|
116
154
|
puts opts
|
|
117
155
|
exit
|
|
@@ -121,6 +159,13 @@ module Git
|
|
|
121
159
|
parser.parse!(@args)
|
|
122
160
|
options
|
|
123
161
|
end
|
|
162
|
+
|
|
163
|
+
def parse_time(str)
|
|
164
|
+
Time.parse(str)
|
|
165
|
+
rescue ArgumentError
|
|
166
|
+
$stderr.puts "Invalid date format: #{str}"
|
|
167
|
+
exit 1
|
|
168
|
+
end
|
|
124
169
|
end
|
|
125
170
|
end
|
|
126
171
|
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Show
|
|
7
|
+
def initialize(args)
|
|
8
|
+
@args = args
|
|
9
|
+
@options = parse_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
ref = @args.shift || "HEAD"
|
|
14
|
+
|
|
15
|
+
repo = Repository.new
|
|
16
|
+
|
|
17
|
+
unless Database.exists?(repo.git_dir)
|
|
18
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Database.connect(repo.git_dir)
|
|
23
|
+
|
|
24
|
+
sha = repo.rev_parse(ref)
|
|
25
|
+
|
|
26
|
+
unless sha
|
|
27
|
+
$stderr.puts "Could not resolve '#{ref}'"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
commit = find_or_create_commit(repo, sha)
|
|
32
|
+
|
|
33
|
+
unless commit
|
|
34
|
+
$stderr.puts "Commit '#{sha[0..7]}' not found"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
changes = Models::DependencyChange
|
|
39
|
+
.includes(:commit, :manifest)
|
|
40
|
+
.where(commit_id: commit.id)
|
|
41
|
+
|
|
42
|
+
if @options[:ecosystem]
|
|
43
|
+
changes = changes.where(ecosystem: @options[:ecosystem])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if changes.empty?
|
|
47
|
+
puts "No dependency changes in #{commit.short_sha}"
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if @options[:format] == "json"
|
|
52
|
+
output_json(commit, changes)
|
|
53
|
+
else
|
|
54
|
+
output_text(commit, changes)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def output_text(commit, changes)
|
|
59
|
+
puts "Commit: #{commit.short_sha} #{commit.message&.lines&.first&.strip}"
|
|
60
|
+
puts "Author: #{commit.author_name} <#{commit.author_email}>"
|
|
61
|
+
puts "Date: #{commit.committed_at.strftime("%Y-%m-%d")}"
|
|
62
|
+
puts
|
|
63
|
+
|
|
64
|
+
added = changes.select { |c| c.change_type == "added" }
|
|
65
|
+
modified = changes.select { |c| c.change_type == "modified" }
|
|
66
|
+
removed = changes.select { |c| c.change_type == "removed" }
|
|
67
|
+
|
|
68
|
+
if added.any?
|
|
69
|
+
puts "Added:"
|
|
70
|
+
added.each do |change|
|
|
71
|
+
puts " #{change.name} #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})"
|
|
72
|
+
end
|
|
73
|
+
puts
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if modified.any?
|
|
77
|
+
puts "Modified:"
|
|
78
|
+
modified.each do |change|
|
|
79
|
+
puts " #{change.name} #{change.previous_requirement} -> #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})"
|
|
80
|
+
end
|
|
81
|
+
puts
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if removed.any?
|
|
85
|
+
puts "Removed:"
|
|
86
|
+
removed.each do |change|
|
|
87
|
+
puts " #{change.name} #{change.requirement} (#{change.ecosystem}, #{change.manifest.path})"
|
|
88
|
+
end
|
|
89
|
+
puts
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def output_json(commit, changes)
|
|
94
|
+
require "json"
|
|
95
|
+
|
|
96
|
+
data = {
|
|
97
|
+
commit: {
|
|
98
|
+
sha: commit.sha,
|
|
99
|
+
short_sha: commit.short_sha,
|
|
100
|
+
message: commit.message&.lines&.first&.strip,
|
|
101
|
+
author_name: commit.author_name,
|
|
102
|
+
author_email: commit.author_email,
|
|
103
|
+
date: commit.committed_at.iso8601
|
|
104
|
+
},
|
|
105
|
+
changes: changes.map do |change|
|
|
106
|
+
{
|
|
107
|
+
name: change.name,
|
|
108
|
+
change_type: change.change_type,
|
|
109
|
+
requirement: change.requirement,
|
|
110
|
+
previous_requirement: change.previous_requirement,
|
|
111
|
+
ecosystem: change.ecosystem,
|
|
112
|
+
manifest: change.manifest.path
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
puts JSON.pretty_generate(data)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def find_or_create_commit(repo, sha)
|
|
121
|
+
commit = Models::Commit.find_by(sha: sha) ||
|
|
122
|
+
Models::Commit.where("sha LIKE ?", "#{sha}%").first
|
|
123
|
+
return commit if commit
|
|
124
|
+
|
|
125
|
+
rugged_commit = repo.lookup(sha)
|
|
126
|
+
return nil unless rugged_commit
|
|
127
|
+
|
|
128
|
+
Models::Commit.create!(
|
|
129
|
+
sha: rugged_commit.oid,
|
|
130
|
+
message: rugged_commit.message,
|
|
131
|
+
author_name: rugged_commit.author[:name],
|
|
132
|
+
author_email: rugged_commit.author[:email],
|
|
133
|
+
committed_at: rugged_commit.time,
|
|
134
|
+
has_dependency_changes: false
|
|
135
|
+
)
|
|
136
|
+
rescue Rugged::OdbError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_options
|
|
141
|
+
options = {}
|
|
142
|
+
|
|
143
|
+
parser = OptionParser.new do |opts|
|
|
144
|
+
opts.banner = "Usage: git pkgs show [commit] [options]"
|
|
145
|
+
|
|
146
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
147
|
+
options[:ecosystem] = v
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
151
|
+
options[:format] = v
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
opts.on("-h", "--help", "Show this help") do
|
|
155
|
+
puts opts
|
|
156
|
+
exit
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
parser.parse!(@args)
|
|
161
|
+
options
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -22,19 +22,26 @@ module Git
|
|
|
22
22
|
branch_name = @options[:branch] || repo.default_branch
|
|
23
23
|
branch = Models::Branch.find_by(name: branch_name)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if @options[:format] == "json"
|
|
28
|
-
require "json"
|
|
29
|
-
puts JSON.pretty_generate(data)
|
|
25
|
+
if @options[:by_author]
|
|
26
|
+
output_by_author
|
|
30
27
|
else
|
|
31
|
-
|
|
28
|
+
data = collect_stats(branch, branch_name)
|
|
29
|
+
|
|
30
|
+
if @options[:format] == "json"
|
|
31
|
+
require "json"
|
|
32
|
+
puts JSON.pretty_generate(data)
|
|
33
|
+
else
|
|
34
|
+
output_text(data)
|
|
35
|
+
end
|
|
32
36
|
end
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def collect_stats(branch, branch_name)
|
|
40
|
+
ecosystem = @options[:ecosystem]
|
|
41
|
+
|
|
36
42
|
data = {
|
|
37
43
|
branch: branch_name,
|
|
44
|
+
ecosystem: ecosystem,
|
|
38
45
|
commits_analyzed: branch&.commits&.count || 0,
|
|
39
46
|
commits_with_changes: branch&.commits&.where(has_dependency_changes: true)&.count || 0,
|
|
40
47
|
current_dependencies: {},
|
|
@@ -46,6 +53,7 @@ module Git
|
|
|
46
53
|
if branch&.last_analyzed_sha
|
|
47
54
|
current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
|
|
48
55
|
snapshots = current_commit&.dependency_snapshots || []
|
|
56
|
+
snapshots = snapshots.where(ecosystem: ecosystem) if ecosystem
|
|
49
57
|
|
|
50
58
|
data[:current_dependencies] = {
|
|
51
59
|
total: snapshots.count,
|
|
@@ -54,22 +62,28 @@ module Git
|
|
|
54
62
|
}
|
|
55
63
|
end
|
|
56
64
|
|
|
65
|
+
changes = Models::DependencyChange.all
|
|
66
|
+
changes = changes.where(ecosystem: ecosystem) if ecosystem
|
|
67
|
+
|
|
57
68
|
data[:changes] = {
|
|
58
|
-
total:
|
|
59
|
-
by_type:
|
|
69
|
+
total: changes.count,
|
|
70
|
+
by_type: changes.group(:change_type).count
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
most_changed =
|
|
73
|
+
most_changed = changes
|
|
63
74
|
.group(:name, :ecosystem)
|
|
64
75
|
.order("count_all DESC")
|
|
65
76
|
.limit(10)
|
|
66
77
|
.count
|
|
67
78
|
|
|
68
|
-
data[:most_changed] = most_changed.map do |(name,
|
|
69
|
-
{ name: name, ecosystem:
|
|
79
|
+
data[:most_changed] = most_changed.map do |(name, eco), count|
|
|
80
|
+
{ name: name, ecosystem: eco, changes: count }
|
|
70
81
|
end
|
|
71
82
|
|
|
72
|
-
|
|
83
|
+
manifests = Models::Manifest.all
|
|
84
|
+
manifests = manifests.where(ecosystem: ecosystem) if ecosystem
|
|
85
|
+
|
|
86
|
+
data[:manifests] = manifests.map do |manifest|
|
|
73
87
|
{ path: manifest.path, ecosystem: manifest.ecosystem, changes: manifest.dependency_changes.count }
|
|
74
88
|
end
|
|
75
89
|
|
|
@@ -82,6 +96,7 @@ module Git
|
|
|
82
96
|
puts
|
|
83
97
|
|
|
84
98
|
puts "Branch: #{data[:branch]}"
|
|
99
|
+
puts "Ecosystem: #{data[:ecosystem]}" if data[:ecosystem]
|
|
85
100
|
puts "Commits analyzed: #{data[:commits_analyzed]}"
|
|
86
101
|
puts "Commits with changes: #{data[:commits_with_changes]}"
|
|
87
102
|
puts
|
|
@@ -128,6 +143,38 @@ module Git
|
|
|
128
143
|
end
|
|
129
144
|
end
|
|
130
145
|
|
|
146
|
+
def output_by_author
|
|
147
|
+
changes = Models::DependencyChange
|
|
148
|
+
.joins(:commit)
|
|
149
|
+
.where(change_type: "added")
|
|
150
|
+
|
|
151
|
+
changes = changes.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem]
|
|
152
|
+
|
|
153
|
+
counts = changes
|
|
154
|
+
.group("commits.author_name")
|
|
155
|
+
.order("count_all DESC")
|
|
156
|
+
.limit(@options[:limit] || 20)
|
|
157
|
+
.count
|
|
158
|
+
|
|
159
|
+
if counts.empty?
|
|
160
|
+
puts "No dependency additions found"
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if @options[:format] == "json"
|
|
165
|
+
require "json"
|
|
166
|
+
data = counts.map { |name, count| { author: name, added: count } }
|
|
167
|
+
puts JSON.pretty_generate(data)
|
|
168
|
+
else
|
|
169
|
+
puts "Dependencies Added by Author"
|
|
170
|
+
puts "=" * 40
|
|
171
|
+
puts
|
|
172
|
+
counts.each do |name, count|
|
|
173
|
+
puts " #{count.to_s.rjust(4)} #{name}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
131
178
|
def parse_options
|
|
132
179
|
options = {}
|
|
133
180
|
|
|
@@ -138,10 +185,22 @@ module Git
|
|
|
138
185
|
options[:branch] = v
|
|
139
186
|
end
|
|
140
187
|
|
|
188
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
189
|
+
options[:ecosystem] = v
|
|
190
|
+
end
|
|
191
|
+
|
|
141
192
|
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
142
193
|
options[:format] = v
|
|
143
194
|
end
|
|
144
195
|
|
|
196
|
+
opts.on("--by-author", "Show dependencies added by author") do
|
|
197
|
+
options[:by_author] = true
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
opts.on("-n", "--limit=N", Integer, "Limit results (default: 20)") do |v|
|
|
201
|
+
options[:limit] = v
|
|
202
|
+
end
|
|
203
|
+
|
|
145
204
|
opts.on("-h", "--help", "Show this help") do
|
|
146
205
|
puts opts
|
|
147
206
|
exit
|
data/lib/git/pkgs/repository.rb
CHANGED
data/lib/git/pkgs/version.rb
CHANGED
data/lib/git/pkgs.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-pkgs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- lib/git/pkgs/commands/list.rb
|
|
99
99
|
- lib/git/pkgs/commands/outdated.rb
|
|
100
100
|
- lib/git/pkgs/commands/search.rb
|
|
101
|
+
- lib/git/pkgs/commands/show.rb
|
|
101
102
|
- lib/git/pkgs/commands/stats.rb
|
|
102
103
|
- lib/git/pkgs/commands/tree.rb
|
|
103
104
|
- lib/git/pkgs/commands/update.rb
|