git-pkgs 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE +661 -0
- data/README.md +279 -0
- data/Rakefile +8 -0
- data/benchmark_bulk.rb +167 -0
- data/benchmark_db.rb +138 -0
- data/benchmark_detailed.rb +151 -0
- data/benchmark_full.rb +131 -0
- data/docs/schema.md +129 -0
- data/exe/git-pkgs +6 -0
- data/lib/git/pkgs/analyzer.rb +270 -0
- data/lib/git/pkgs/cli.rb +73 -0
- data/lib/git/pkgs/commands/blame.rb +142 -0
- data/lib/git/pkgs/commands/branch.rb +337 -0
- data/lib/git/pkgs/commands/diff.rb +131 -0
- data/lib/git/pkgs/commands/history.rb +127 -0
- data/lib/git/pkgs/commands/hooks.rb +131 -0
- data/lib/git/pkgs/commands/info.rb +109 -0
- data/lib/git/pkgs/commands/init.rb +267 -0
- data/lib/git/pkgs/commands/list.rb +159 -0
- data/lib/git/pkgs/commands/outdated.rb +122 -0
- data/lib/git/pkgs/commands/search.rb +152 -0
- data/lib/git/pkgs/commands/stats.rb +157 -0
- data/lib/git/pkgs/commands/tree.rb +124 -0
- data/lib/git/pkgs/commands/update.rb +147 -0
- data/lib/git/pkgs/commands/why.rb +82 -0
- data/lib/git/pkgs/database.rb +143 -0
- data/lib/git/pkgs/models/branch.rb +18 -0
- data/lib/git/pkgs/models/branch_commit.rb +14 -0
- data/lib/git/pkgs/models/commit.rb +29 -0
- data/lib/git/pkgs/models/dependency_change.rb +21 -0
- data/lib/git/pkgs/models/dependency_snapshot.rb +27 -0
- data/lib/git/pkgs/models/manifest.rb +21 -0
- data/lib/git/pkgs/repository.rb +125 -0
- data/lib/git/pkgs/version.rb +7 -0
- data/lib/git/pkgs.rb +37 -0
- metadata +138 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class List
|
|
7
|
+
def initialize(args)
|
|
8
|
+
@args = args
|
|
9
|
+
@options = parse_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
repo = Repository.new
|
|
14
|
+
|
|
15
|
+
unless Database.exists?(repo.git_dir)
|
|
16
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Database.connect(repo.git_dir)
|
|
21
|
+
|
|
22
|
+
commit_sha = @options[:commit] || repo.head_sha
|
|
23
|
+
target_commit = Models::Commit.find_by(sha: commit_sha)
|
|
24
|
+
|
|
25
|
+
unless target_commit
|
|
26
|
+
$stderr.puts "Commit #{commit_sha[0, 7]} not found in database"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
deps = compute_dependencies_at_commit(target_commit, repo)
|
|
31
|
+
|
|
32
|
+
if deps.empty?
|
|
33
|
+
puts "No dependencies found"
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Apply filters
|
|
38
|
+
if @options[:ecosystem]
|
|
39
|
+
deps = deps.select { |d| d[:ecosystem] == @options[:ecosystem] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if @options[:type]
|
|
43
|
+
deps = deps.select { |d| d[:dependency_type] == @options[:type] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if @options[:format] == "json"
|
|
47
|
+
require "json"
|
|
48
|
+
puts JSON.pretty_generate(deps)
|
|
49
|
+
else
|
|
50
|
+
grouped = deps.group_by { |d| [d[:manifest_path], d[:ecosystem]] }
|
|
51
|
+
|
|
52
|
+
grouped.each do |(path, platform), manifest_deps|
|
|
53
|
+
puts "#{path} (#{platform}):"
|
|
54
|
+
manifest_deps.sort_by { |d| d[:name] }.each do |dep|
|
|
55
|
+
type_suffix = dep[:dependency_type] ? " [#{dep[:dependency_type]}]" : ""
|
|
56
|
+
puts " #{dep[:name]} #{dep[:requirement]}#{type_suffix}"
|
|
57
|
+
end
|
|
58
|
+
puts
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def compute_dependencies_at_commit(target_commit, repo)
|
|
64
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
65
|
+
branch = Models::Branch.find_by(name: branch_name)
|
|
66
|
+
return [] unless branch
|
|
67
|
+
|
|
68
|
+
# Find the nearest snapshot commit before or at target
|
|
69
|
+
snapshot_commit = branch.commits
|
|
70
|
+
.joins(:dependency_snapshots)
|
|
71
|
+
.where("commits.committed_at <= ?", target_commit.committed_at)
|
|
72
|
+
.order(committed_at: :desc)
|
|
73
|
+
.distinct
|
|
74
|
+
.first
|
|
75
|
+
|
|
76
|
+
# Build initial state from snapshot
|
|
77
|
+
deps = {}
|
|
78
|
+
if snapshot_commit
|
|
79
|
+
snapshot_commit.dependency_snapshots.includes(:manifest).each do |s|
|
|
80
|
+
key = [s.manifest.path, s.name]
|
|
81
|
+
deps[key] = {
|
|
82
|
+
manifest_path: s.manifest.path,
|
|
83
|
+
name: s.name,
|
|
84
|
+
ecosystem: s.ecosystem,
|
|
85
|
+
requirement: s.requirement,
|
|
86
|
+
dependency_type: s.dependency_type
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Replay changes from snapshot to target
|
|
92
|
+
if snapshot_commit && snapshot_commit.id != target_commit.id
|
|
93
|
+
changes = Models::DependencyChange
|
|
94
|
+
.joins(:commit, :manifest)
|
|
95
|
+
.where(commits: { id: branch.commit_ids })
|
|
96
|
+
.where("commits.committed_at > ? AND commits.committed_at <= ?",
|
|
97
|
+
snapshot_commit.committed_at, target_commit.committed_at)
|
|
98
|
+
.order("commits.committed_at ASC")
|
|
99
|
+
.includes(:manifest)
|
|
100
|
+
|
|
101
|
+
changes.each do |change|
|
|
102
|
+
key = [change.manifest.path, change.name]
|
|
103
|
+
case change.change_type
|
|
104
|
+
when "added", "modified"
|
|
105
|
+
deps[key] = {
|
|
106
|
+
manifest_path: change.manifest.path,
|
|
107
|
+
name: change.name,
|
|
108
|
+
ecosystem: change.ecosystem,
|
|
109
|
+
requirement: change.requirement,
|
|
110
|
+
dependency_type: change.dependency_type
|
|
111
|
+
}
|
|
112
|
+
when "removed"
|
|
113
|
+
deps.delete(key)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
deps.values
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_options
|
|
122
|
+
options = {}
|
|
123
|
+
|
|
124
|
+
parser = OptionParser.new do |opts|
|
|
125
|
+
opts.banner = "Usage: git pkgs list [options]"
|
|
126
|
+
|
|
127
|
+
opts.on("-c", "--commit=SHA", "Show dependencies at specific commit") do |v|
|
|
128
|
+
options[:commit] = v
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem (npm, rubygems, etc.)") do |v|
|
|
132
|
+
options[:ecosystem] = v
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
opts.on("-t", "--type=TYPE", "Filter by dependency type") do |v|
|
|
136
|
+
options[:type] = v
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
opts.on("-b", "--branch=NAME", "Branch context for finding snapshots") do |v|
|
|
140
|
+
options[:branch] = v
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
144
|
+
options[:format] = v
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
opts.on("-h", "--help", "Show this help") do
|
|
148
|
+
puts opts
|
|
149
|
+
exit
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
parser.parse!(@args)
|
|
154
|
+
options
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Outdated
|
|
7
|
+
def initialize(args)
|
|
8
|
+
@args = args
|
|
9
|
+
@options = parse_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
repo = Repository.new
|
|
14
|
+
|
|
15
|
+
unless Database.exists?(repo.git_dir)
|
|
16
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Database.connect(repo.git_dir)
|
|
21
|
+
|
|
22
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
23
|
+
branch = Models::Branch.find_by(name: branch_name)
|
|
24
|
+
|
|
25
|
+
unless branch&.last_analyzed_sha
|
|
26
|
+
$stderr.puts "No analysis found for branch '#{branch_name}'"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
|
|
31
|
+
snapshots = current_commit&.dependency_snapshots&.includes(:manifest) || []
|
|
32
|
+
|
|
33
|
+
if @options[:ecosystem]
|
|
34
|
+
snapshots = snapshots.where(ecosystem: @options[:ecosystem])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if snapshots.empty?
|
|
38
|
+
puts "No dependencies found"
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Find last update for each dependency
|
|
43
|
+
outdated_data = []
|
|
44
|
+
|
|
45
|
+
snapshots.each do |snapshot|
|
|
46
|
+
last_change = Models::DependencyChange
|
|
47
|
+
.includes(:commit)
|
|
48
|
+
.where(name: snapshot.name, manifest: snapshot.manifest)
|
|
49
|
+
.order("commits.committed_at DESC")
|
|
50
|
+
.first
|
|
51
|
+
|
|
52
|
+
next unless last_change
|
|
53
|
+
|
|
54
|
+
days_since_update = ((Time.now - last_change.commit.committed_at) / 86400).to_i
|
|
55
|
+
|
|
56
|
+
outdated_data << {
|
|
57
|
+
name: snapshot.name,
|
|
58
|
+
ecosystem: snapshot.ecosystem,
|
|
59
|
+
requirement: snapshot.requirement,
|
|
60
|
+
manifest: snapshot.manifest.path,
|
|
61
|
+
last_updated: last_change.commit.committed_at,
|
|
62
|
+
days_ago: days_since_update,
|
|
63
|
+
change_type: last_change.change_type
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Sort by days since last update (oldest first)
|
|
68
|
+
outdated_data.sort_by! { |d| -d[:days_ago] }
|
|
69
|
+
|
|
70
|
+
if @options[:days]
|
|
71
|
+
outdated_data = outdated_data.select { |d| d[:days_ago] >= @options[:days] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if outdated_data.empty?
|
|
75
|
+
puts "All dependencies have been updated recently"
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts "Dependencies by last update:"
|
|
80
|
+
puts
|
|
81
|
+
|
|
82
|
+
max_name_len = outdated_data.map { |d| d[:name].length }.max
|
|
83
|
+
max_version_len = outdated_data.map { |d| d[:requirement].to_s.length }.max
|
|
84
|
+
|
|
85
|
+
outdated_data.each do |dep|
|
|
86
|
+
date = dep[:last_updated].strftime("%Y-%m-%d")
|
|
87
|
+
days = "#{dep[:days_ago]} days ago"
|
|
88
|
+
puts "#{dep[:name].ljust(max_name_len)} #{dep[:requirement].to_s.ljust(max_version_len)} #{date} (#{days})"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_options
|
|
93
|
+
options = {}
|
|
94
|
+
|
|
95
|
+
parser = OptionParser.new do |opts|
|
|
96
|
+
opts.banner = "Usage: git pkgs outdated [options]"
|
|
97
|
+
|
|
98
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
99
|
+
options[:ecosystem] = v
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
opts.on("-b", "--branch=NAME", "Branch to analyze") do |v|
|
|
103
|
+
options[:branch] = v
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
opts.on("-d", "--days=N", Integer, "Only show deps not updated in N days") do |v|
|
|
107
|
+
options[:days] = v
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
opts.on("-h", "--help", "Show this help") do
|
|
111
|
+
puts opts
|
|
112
|
+
exit
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
parser.parse!(@args)
|
|
117
|
+
options
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Search
|
|
7
|
+
def initialize(args)
|
|
8
|
+
@args = args
|
|
9
|
+
@options = parse_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
pattern = @args.first
|
|
14
|
+
|
|
15
|
+
unless pattern
|
|
16
|
+
$stderr.puts "Usage: git pkgs search <pattern>"
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
repo = Repository.new
|
|
21
|
+
|
|
22
|
+
unless Database.exists?(repo.git_dir)
|
|
23
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Database.connect(repo.git_dir)
|
|
28
|
+
|
|
29
|
+
# Search for dependencies matching the pattern
|
|
30
|
+
query = Models::DependencyChange
|
|
31
|
+
.joins(:manifest)
|
|
32
|
+
.where("dependency_changes.name LIKE ?", "%#{pattern}%")
|
|
33
|
+
|
|
34
|
+
if @options[:ecosystem]
|
|
35
|
+
query = query.where(ecosystem: @options[:ecosystem])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if @options[:direct]
|
|
39
|
+
query = query.where(manifests: { kind: "manifest" })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get unique dependency names
|
|
43
|
+
matches = query.distinct.pluck(:name, :ecosystem)
|
|
44
|
+
|
|
45
|
+
if matches.empty?
|
|
46
|
+
puts "No dependencies found matching '#{pattern}'"
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if @options[:format] == "json"
|
|
51
|
+
output_json(matches, pattern)
|
|
52
|
+
else
|
|
53
|
+
output_text(matches, pattern)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def output_text(matches, pattern)
|
|
58
|
+
puts "Dependencies matching '#{pattern}':"
|
|
59
|
+
puts
|
|
60
|
+
|
|
61
|
+
matches.group_by { |_, platform| platform }.each do |platform, deps|
|
|
62
|
+
puts "#{platform}:"
|
|
63
|
+
deps.each do |name, _|
|
|
64
|
+
summary = dependency_summary(name, platform)
|
|
65
|
+
puts " #{name}"
|
|
66
|
+
puts " #{summary}"
|
|
67
|
+
end
|
|
68
|
+
puts
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def output_json(matches, pattern)
|
|
73
|
+
require "json"
|
|
74
|
+
|
|
75
|
+
results = matches.map do |name, platform|
|
|
76
|
+
changes = Models::DependencyChange
|
|
77
|
+
.where(name: name, ecosystem: platform)
|
|
78
|
+
.includes(:commit)
|
|
79
|
+
.order("commits.committed_at ASC")
|
|
80
|
+
|
|
81
|
+
first = changes.first
|
|
82
|
+
last = changes.last
|
|
83
|
+
current = changes.where(change_type: %w[added modified]).last
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
name: name,
|
|
87
|
+
ecosystem: platform,
|
|
88
|
+
first_seen: first&.commit&.committed_at&.iso8601,
|
|
89
|
+
last_changed: last&.commit&.committed_at&.iso8601,
|
|
90
|
+
current_version: current&.requirement,
|
|
91
|
+
removed: changes.last&.change_type == "removed",
|
|
92
|
+
total_changes: changes.count
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts JSON.pretty_generate(results)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def dependency_summary(name, platform)
|
|
100
|
+
changes = Models::DependencyChange
|
|
101
|
+
.where(name: name, ecosystem: platform)
|
|
102
|
+
.includes(:commit)
|
|
103
|
+
.order("commits.committed_at ASC")
|
|
104
|
+
|
|
105
|
+
first = changes.first
|
|
106
|
+
last = changes.last
|
|
107
|
+
|
|
108
|
+
parts = []
|
|
109
|
+
parts << "added #{first.commit.committed_at.strftime('%Y-%m-%d')}"
|
|
110
|
+
|
|
111
|
+
if last.change_type == "removed"
|
|
112
|
+
parts << "removed #{last.commit.committed_at.strftime('%Y-%m-%d')}"
|
|
113
|
+
else
|
|
114
|
+
current = changes.where(change_type: %w[added modified]).last
|
|
115
|
+
parts << "current: #{current&.requirement || 'unknown'}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
parts << "#{changes.count} changes"
|
|
119
|
+
parts.join(", ")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_options
|
|
123
|
+
options = {}
|
|
124
|
+
|
|
125
|
+
parser = OptionParser.new do |opts|
|
|
126
|
+
opts.banner = "Usage: git pkgs search <pattern> [options]"
|
|
127
|
+
|
|
128
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
129
|
+
options[:ecosystem] = v
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
opts.on("-d", "--direct", "Only show direct dependencies (not from lockfiles)") do
|
|
133
|
+
options[:direct] = true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
137
|
+
options[:format] = v
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
opts.on("-h", "--help", "Show this help") do
|
|
141
|
+
puts opts
|
|
142
|
+
exit
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
parser.parse!(@args)
|
|
147
|
+
options
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Stats
|
|
7
|
+
def initialize(args)
|
|
8
|
+
@args = args
|
|
9
|
+
@options = parse_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
repo = Repository.new
|
|
14
|
+
|
|
15
|
+
unless Database.exists?(repo.git_dir)
|
|
16
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Database.connect(repo.git_dir)
|
|
21
|
+
|
|
22
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
23
|
+
branch = Models::Branch.find_by(name: branch_name)
|
|
24
|
+
|
|
25
|
+
data = collect_stats(branch, branch_name)
|
|
26
|
+
|
|
27
|
+
if @options[:format] == "json"
|
|
28
|
+
require "json"
|
|
29
|
+
puts JSON.pretty_generate(data)
|
|
30
|
+
else
|
|
31
|
+
output_text(data)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def collect_stats(branch, branch_name)
|
|
36
|
+
data = {
|
|
37
|
+
branch: branch_name,
|
|
38
|
+
commits_analyzed: branch&.commits&.count || 0,
|
|
39
|
+
commits_with_changes: branch&.commits&.where(has_dependency_changes: true)&.count || 0,
|
|
40
|
+
current_dependencies: {},
|
|
41
|
+
changes: {},
|
|
42
|
+
most_changed: [],
|
|
43
|
+
manifests: []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if branch&.last_analyzed_sha
|
|
47
|
+
current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
|
|
48
|
+
snapshots = current_commit&.dependency_snapshots || []
|
|
49
|
+
|
|
50
|
+
data[:current_dependencies] = {
|
|
51
|
+
total: snapshots.count,
|
|
52
|
+
by_platform: snapshots.group(:ecosystem).count,
|
|
53
|
+
by_type: snapshots.group(:dependency_type).count
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
data[:changes] = {
|
|
58
|
+
total: Models::DependencyChange.count,
|
|
59
|
+
by_type: Models::DependencyChange.group(:change_type).count
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
most_changed = Models::DependencyChange
|
|
63
|
+
.group(:name, :ecosystem)
|
|
64
|
+
.order("count_all DESC")
|
|
65
|
+
.limit(10)
|
|
66
|
+
.count
|
|
67
|
+
|
|
68
|
+
data[:most_changed] = most_changed.map do |(name, ecosystem), count|
|
|
69
|
+
{ name: name, ecosystem: ecosystem, changes: count }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
data[:manifests] = Models::Manifest.all.map do |manifest|
|
|
73
|
+
{ path: manifest.path, ecosystem: manifest.ecosystem, changes: manifest.dependency_changes.count }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
data
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def output_text(data)
|
|
80
|
+
puts "Dependency Statistics"
|
|
81
|
+
puts "=" * 40
|
|
82
|
+
puts
|
|
83
|
+
|
|
84
|
+
puts "Branch: #{data[:branch]}"
|
|
85
|
+
puts "Commits analyzed: #{data[:commits_analyzed]}"
|
|
86
|
+
puts "Commits with changes: #{data[:commits_with_changes]}"
|
|
87
|
+
puts
|
|
88
|
+
|
|
89
|
+
if data[:current_dependencies][:total]
|
|
90
|
+
puts "Current Dependencies"
|
|
91
|
+
puts "-" * 20
|
|
92
|
+
puts "Total: #{data[:current_dependencies][:total]}"
|
|
93
|
+
|
|
94
|
+
data[:current_dependencies][:by_platform].sort_by { |_, c| -c }.each do |ecosystem, count|
|
|
95
|
+
puts " #{ecosystem}: #{count}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
by_type = data[:current_dependencies][:by_type]
|
|
99
|
+
if by_type.keys.compact.any?
|
|
100
|
+
puts
|
|
101
|
+
puts "By type:"
|
|
102
|
+
by_type.sort_by { |_, c| -c }.each do |type, count|
|
|
103
|
+
puts " #{type || 'unknown'}: #{count}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
puts
|
|
109
|
+
puts "Dependency Changes"
|
|
110
|
+
puts "-" * 20
|
|
111
|
+
puts "Total changes: #{data[:changes][:total]}"
|
|
112
|
+
data[:changes][:by_type].each do |type, count|
|
|
113
|
+
puts " #{type}: #{count}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
puts
|
|
117
|
+
puts "Most Changed Dependencies"
|
|
118
|
+
puts "-" * 25
|
|
119
|
+
data[:most_changed].each do |dep|
|
|
120
|
+
puts " #{dep[:name]} (#{dep[:ecosystem]}): #{dep[:changes]} changes"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
puts
|
|
124
|
+
puts "Manifest Files"
|
|
125
|
+
puts "-" * 14
|
|
126
|
+
data[:manifests].each do |m|
|
|
127
|
+
puts " #{m[:path]} (#{m[:ecosystem]}): #{m[:changes]} changes"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_options
|
|
132
|
+
options = {}
|
|
133
|
+
|
|
134
|
+
parser = OptionParser.new do |opts|
|
|
135
|
+
opts.banner = "Usage: git pkgs stats [options]"
|
|
136
|
+
|
|
137
|
+
opts.on("-b", "--branch=NAME", "Branch to analyze") do |v|
|
|
138
|
+
options[:branch] = v
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
142
|
+
options[:format] = v
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
opts.on("-h", "--help", "Show this help") do
|
|
146
|
+
puts opts
|
|
147
|
+
exit
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
parser.parse!(@args)
|
|
152
|
+
options
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|