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
data/lib/git/pkgs/cli.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Pkgs
|
|
7
|
+
class CLI
|
|
8
|
+
COMMANDS = %w[init update hooks info list tree history search why blame outdated stats diff branch].freeze
|
|
9
|
+
|
|
10
|
+
def self.run(args)
|
|
11
|
+
new(args).run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(args)
|
|
15
|
+
@args = args.dup
|
|
16
|
+
@options = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
command = @args.shift
|
|
21
|
+
|
|
22
|
+
case command
|
|
23
|
+
when nil, "-h", "--help", "help"
|
|
24
|
+
print_help
|
|
25
|
+
when "-v", "--version", "version"
|
|
26
|
+
puts "git-pkgs #{Git::Pkgs::VERSION}"
|
|
27
|
+
when *COMMANDS
|
|
28
|
+
run_command(command)
|
|
29
|
+
else
|
|
30
|
+
$stderr.puts "Unknown command: #{command}"
|
|
31
|
+
$stderr.puts "Run 'git pkgs help' for usage"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run_command(command)
|
|
37
|
+
command_class = Commands.const_get(command.capitalize.gsub(/_([a-z])/) { $1.upcase })
|
|
38
|
+
command_class.new(@args).run
|
|
39
|
+
rescue NameError
|
|
40
|
+
$stderr.puts "Command '#{command}' not yet implemented"
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def print_help
|
|
45
|
+
puts <<~HELP
|
|
46
|
+
Usage: git pkgs <command> [options]
|
|
47
|
+
|
|
48
|
+
Commands:
|
|
49
|
+
init Initialize the package database for this repository
|
|
50
|
+
update Update the database with new commits
|
|
51
|
+
hooks Manage git hooks for auto-updating
|
|
52
|
+
info Show database size and row counts
|
|
53
|
+
branch Manage tracked branches
|
|
54
|
+
list List dependencies at a commit
|
|
55
|
+
tree Show dependency tree grouped by type
|
|
56
|
+
history Show the history of a package
|
|
57
|
+
search Find a dependency across all history
|
|
58
|
+
why Explain why a dependency exists
|
|
59
|
+
blame Show who added each dependency
|
|
60
|
+
outdated Show dependencies that haven't been updated
|
|
61
|
+
stats Show dependency statistics
|
|
62
|
+
diff Show dependency changes between commits
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
-h, --help Show this help message
|
|
66
|
+
-v, --version Show version
|
|
67
|
+
|
|
68
|
+
Run 'git pkgs <command> --help' for command-specific options.
|
|
69
|
+
HELP
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Blame
|
|
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
|
+
# Get current dependencies at the last analyzed commit
|
|
23
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
24
|
+
branch = Models::Branch.find_by(name: branch_name)
|
|
25
|
+
|
|
26
|
+
unless branch&.last_analyzed_sha
|
|
27
|
+
$stderr.puts "No analysis found for branch '#{branch_name}'"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
|
|
32
|
+
snapshots = current_commit&.dependency_snapshots&.includes(:manifest) || []
|
|
33
|
+
|
|
34
|
+
if @options[:ecosystem]
|
|
35
|
+
snapshots = snapshots.where(ecosystem: @options[:ecosystem])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if snapshots.empty?
|
|
39
|
+
puts "No dependencies found"
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# For each current dependency, find who added it
|
|
44
|
+
blame_data = []
|
|
45
|
+
|
|
46
|
+
snapshots.each do |snapshot|
|
|
47
|
+
added_change = Models::DependencyChange
|
|
48
|
+
.includes(:commit)
|
|
49
|
+
.where(name: snapshot.name, manifest: snapshot.manifest)
|
|
50
|
+
.added
|
|
51
|
+
.order("commits.committed_at ASC")
|
|
52
|
+
.first
|
|
53
|
+
|
|
54
|
+
next unless added_change
|
|
55
|
+
|
|
56
|
+
commit = added_change.commit
|
|
57
|
+
author = best_author(commit)
|
|
58
|
+
|
|
59
|
+
blame_data << {
|
|
60
|
+
name: snapshot.name,
|
|
61
|
+
ecosystem: snapshot.ecosystem,
|
|
62
|
+
requirement: snapshot.requirement,
|
|
63
|
+
manifest: snapshot.manifest.path,
|
|
64
|
+
author: author,
|
|
65
|
+
date: commit.committed_at,
|
|
66
|
+
sha: commit.short_sha
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if @options[:format] == "json"
|
|
71
|
+
require "json"
|
|
72
|
+
json_data = blame_data.map do |d|
|
|
73
|
+
d.merge(date: d[:date].iso8601)
|
|
74
|
+
end
|
|
75
|
+
puts JSON.pretty_generate(json_data)
|
|
76
|
+
else
|
|
77
|
+
grouped = blame_data.group_by { |d| [d[:manifest], d[:ecosystem]] }
|
|
78
|
+
|
|
79
|
+
grouped.each do |(manifest, ecosystem), deps|
|
|
80
|
+
puts "#{manifest} (#{ecosystem}):"
|
|
81
|
+
|
|
82
|
+
max_name_len = deps.map { |d| d[:name].length }.max
|
|
83
|
+
max_author_len = deps.map { |d| d[:author].length }.max
|
|
84
|
+
|
|
85
|
+
deps.sort_by { |d| d[:name] }.each do |dep|
|
|
86
|
+
date = dep[:date].strftime("%Y-%m-%d")
|
|
87
|
+
puts " #{dep[:name].ljust(max_name_len)} #{dep[:author].ljust(max_author_len)} #{date} #{dep[:sha]}"
|
|
88
|
+
end
|
|
89
|
+
puts
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def best_author(commit)
|
|
95
|
+
authors = [commit.author_name] + parse_coauthors(commit.message)
|
|
96
|
+
|
|
97
|
+
# Prefer human authors over bots
|
|
98
|
+
human = authors.find { |a| !bot_author?(a) }
|
|
99
|
+
human || authors.first
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_coauthors(message)
|
|
103
|
+
return [] unless message
|
|
104
|
+
|
|
105
|
+
message.scan(/^Co-authored-by:\s*(.+?)\s*<[^>]+>/i).flatten
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def bot_author?(name)
|
|
109
|
+
name =~ /\[bot\]$|^dependabot|^renovate|^github-actions/i
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_options
|
|
113
|
+
options = {}
|
|
114
|
+
|
|
115
|
+
parser = OptionParser.new do |opts|
|
|
116
|
+
opts.banner = "Usage: git pkgs blame [options]"
|
|
117
|
+
|
|
118
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
119
|
+
options[:ecosystem] = v
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts.on("-b", "--branch=NAME", "Branch to analyze") do |v|
|
|
123
|
+
options[:branch] = v
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
127
|
+
options[:format] = v
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
opts.on("-h", "--help", "Show this help") do
|
|
131
|
+
puts opts
|
|
132
|
+
exit
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
parser.parse!(@args)
|
|
137
|
+
options
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Branch
|
|
7
|
+
BATCH_SIZE = 100
|
|
8
|
+
SNAPSHOT_INTERVAL = 20
|
|
9
|
+
|
|
10
|
+
def initialize(args)
|
|
11
|
+
@args = args
|
|
12
|
+
@options = parse_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
subcommand = @args.shift
|
|
17
|
+
|
|
18
|
+
case subcommand
|
|
19
|
+
when "add"
|
|
20
|
+
add_branch
|
|
21
|
+
when "list"
|
|
22
|
+
list_branches
|
|
23
|
+
when "remove", "rm"
|
|
24
|
+
remove_branch
|
|
25
|
+
when nil, "-h", "--help"
|
|
26
|
+
print_help
|
|
27
|
+
else
|
|
28
|
+
$stderr.puts "Unknown subcommand: #{subcommand}"
|
|
29
|
+
$stderr.puts "Run 'git pkgs branch --help' for usage"
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_branch
|
|
35
|
+
branch_name = @args.shift
|
|
36
|
+
unless branch_name
|
|
37
|
+
$stderr.puts "Usage: git pkgs branch add <name>"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
repo = Repository.new
|
|
42
|
+
|
|
43
|
+
unless Database.exists?(repo.git_dir)
|
|
44
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Database.connect(repo.git_dir)
|
|
49
|
+
|
|
50
|
+
unless repo.branch_exists?(branch_name)
|
|
51
|
+
$stderr.puts "Branch '#{branch_name}' not found"
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
existing = Models::Branch.find_by(name: branch_name)
|
|
56
|
+
if existing
|
|
57
|
+
puts "Branch '#{branch_name}' already tracked (#{existing.commits.count} commits)"
|
|
58
|
+
puts "Use 'git pkgs update' to refresh"
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Database.optimize_for_bulk_writes
|
|
63
|
+
|
|
64
|
+
branch = Models::Branch.create!(name: branch_name)
|
|
65
|
+
analyzer = Analyzer.new(repo)
|
|
66
|
+
|
|
67
|
+
puts "Analyzing branch: #{branch_name}"
|
|
68
|
+
|
|
69
|
+
walker = repo.walk(branch_name)
|
|
70
|
+
commits = walker.to_a
|
|
71
|
+
total = commits.size
|
|
72
|
+
|
|
73
|
+
stats = bulk_process_commits(commits, branch, analyzer, total, repo)
|
|
74
|
+
|
|
75
|
+
branch.update(last_analyzed_sha: repo.branch_target(branch_name))
|
|
76
|
+
|
|
77
|
+
Database.optimize_for_reads
|
|
78
|
+
|
|
79
|
+
puts "\rDone!#{' ' * 20}"
|
|
80
|
+
puts "Analyzed #{total} commits"
|
|
81
|
+
puts "Found #{stats[:dependency_commits]} commits with dependency changes"
|
|
82
|
+
puts "Stored #{stats[:snapshots_stored]} snapshots"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def list_branches
|
|
86
|
+
repo = Repository.new
|
|
87
|
+
|
|
88
|
+
unless Database.exists?(repo.git_dir)
|
|
89
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
90
|
+
exit 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Database.connect(repo.git_dir)
|
|
94
|
+
|
|
95
|
+
branches = Models::Branch.all
|
|
96
|
+
|
|
97
|
+
if branches.empty?
|
|
98
|
+
puts "No branches tracked"
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
puts "Tracked branches:"
|
|
103
|
+
branches.each do |branch|
|
|
104
|
+
commit_count = branch.commits.count
|
|
105
|
+
dep_commits = branch.commits.where(has_dependency_changes: true).count
|
|
106
|
+
last_sha = branch.last_analyzed_sha&.slice(0, 7) || "none"
|
|
107
|
+
puts " #{branch.name}: #{commit_count} commits (#{dep_commits} with deps), last: #{last_sha}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def remove_branch
|
|
112
|
+
branch_name = @args.shift
|
|
113
|
+
unless branch_name
|
|
114
|
+
$stderr.puts "Usage: git pkgs branch remove <name>"
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
repo = Repository.new
|
|
119
|
+
|
|
120
|
+
unless Database.exists?(repo.git_dir)
|
|
121
|
+
$stderr.puts "Database not initialized. Run 'git pkgs init' first."
|
|
122
|
+
exit 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Database.connect(repo.git_dir)
|
|
126
|
+
|
|
127
|
+
branch = Models::Branch.find_by(name: branch_name)
|
|
128
|
+
unless branch
|
|
129
|
+
$stderr.puts "Branch '#{branch_name}' not tracked"
|
|
130
|
+
exit 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Only delete branch_commits, keep shared commits
|
|
134
|
+
count = branch.branch_commits.count
|
|
135
|
+
branch.branch_commits.delete_all
|
|
136
|
+
branch.destroy
|
|
137
|
+
|
|
138
|
+
puts "Removed branch '#{branch_name}' (#{count} branch-commit links)"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def bulk_process_commits(commits, branch, analyzer, total, repo)
|
|
142
|
+
now = Time.now
|
|
143
|
+
snapshot = {}
|
|
144
|
+
manifests_cache = {}
|
|
145
|
+
|
|
146
|
+
pending_commits = []
|
|
147
|
+
pending_branch_commits = []
|
|
148
|
+
pending_changes = []
|
|
149
|
+
pending_snapshots = []
|
|
150
|
+
|
|
151
|
+
dependency_commit_count = 0
|
|
152
|
+
snapshots_stored = 0
|
|
153
|
+
processed = 0
|
|
154
|
+
|
|
155
|
+
flush = lambda do
|
|
156
|
+
return if pending_commits.empty?
|
|
157
|
+
|
|
158
|
+
ActiveRecord::Base.transaction do
|
|
159
|
+
# Use upsert for commits since they may already exist from other branches
|
|
160
|
+
if pending_commits.any?
|
|
161
|
+
Models::Commit.upsert_all(
|
|
162
|
+
pending_commits,
|
|
163
|
+
unique_by: :sha
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
commit_ids = Models::Commit
|
|
168
|
+
.where(sha: pending_commits.map { |c| c[:sha] })
|
|
169
|
+
.pluck(:sha, :id).to_h
|
|
170
|
+
|
|
171
|
+
if pending_branch_commits.any?
|
|
172
|
+
branch_commit_records = pending_branch_commits.map do |bc|
|
|
173
|
+
{ branch_id: bc[:branch_id], commit_id: commit_ids[bc[:sha]], position: bc[:position] }
|
|
174
|
+
end
|
|
175
|
+
Models::BranchCommit.insert_all(branch_commit_records)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if pending_changes.any?
|
|
179
|
+
manifest_ids = Models::Manifest.pluck(:path, :id).to_h
|
|
180
|
+
change_records = pending_changes.map do |c|
|
|
181
|
+
{
|
|
182
|
+
commit_id: commit_ids[c[:sha]],
|
|
183
|
+
manifest_id: manifest_ids[c[:manifest_path]],
|
|
184
|
+
name: c[:name],
|
|
185
|
+
ecosystem: c[:ecosystem],
|
|
186
|
+
change_type: c[:change_type],
|
|
187
|
+
requirement: c[:requirement],
|
|
188
|
+
previous_requirement: c[:previous_requirement],
|
|
189
|
+
dependency_type: c[:dependency_type],
|
|
190
|
+
created_at: now,
|
|
191
|
+
updated_at: now
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
Models::DependencyChange.insert_all(change_records)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if pending_snapshots.any?
|
|
198
|
+
manifest_ids ||= Models::Manifest.pluck(:path, :id).to_h
|
|
199
|
+
snapshot_records = pending_snapshots.map do |s|
|
|
200
|
+
{
|
|
201
|
+
commit_id: commit_ids[s[:sha]],
|
|
202
|
+
manifest_id: manifest_ids[s[:manifest_path]],
|
|
203
|
+
name: s[:name],
|
|
204
|
+
ecosystem: s[:ecosystem],
|
|
205
|
+
requirement: s[:requirement],
|
|
206
|
+
dependency_type: s[:dependency_type],
|
|
207
|
+
created_at: now,
|
|
208
|
+
updated_at: now
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
Models::DependencySnapshot.insert_all(snapshot_records)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
pending_commits.clear
|
|
216
|
+
pending_branch_commits.clear
|
|
217
|
+
pending_changes.clear
|
|
218
|
+
pending_snapshots.clear
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
commits.each do |rugged_commit|
|
|
222
|
+
processed += 1
|
|
223
|
+
print "\rProcessing commit #{processed}/#{total}..." if processed % 50 == 0 || processed == total
|
|
224
|
+
|
|
225
|
+
next if rugged_commit.parents.length > 1
|
|
226
|
+
|
|
227
|
+
result = analyzer.analyze_commit(rugged_commit, snapshot)
|
|
228
|
+
has_changes = result && result[:changes].any?
|
|
229
|
+
|
|
230
|
+
pending_commits << {
|
|
231
|
+
sha: rugged_commit.oid,
|
|
232
|
+
message: rugged_commit.message,
|
|
233
|
+
author_name: rugged_commit.author[:name],
|
|
234
|
+
author_email: rugged_commit.author[:email],
|
|
235
|
+
committed_at: rugged_commit.time,
|
|
236
|
+
has_dependency_changes: has_changes,
|
|
237
|
+
created_at: now,
|
|
238
|
+
updated_at: now
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pending_branch_commits << {
|
|
242
|
+
branch_id: branch.id,
|
|
243
|
+
sha: rugged_commit.oid,
|
|
244
|
+
position: processed
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if has_changes
|
|
248
|
+
dependency_commit_count += 1
|
|
249
|
+
|
|
250
|
+
result[:changes].each do |change|
|
|
251
|
+
manifest_key = change[:manifest_path]
|
|
252
|
+
unless manifests_cache[manifest_key]
|
|
253
|
+
manifests_cache[manifest_key] = Models::Manifest.find_or_create(
|
|
254
|
+
path: change[:manifest_path],
|
|
255
|
+
ecosystem: change[:ecosystem],
|
|
256
|
+
kind: change[:kind]
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
pending_changes << {
|
|
261
|
+
sha: rugged_commit.oid,
|
|
262
|
+
manifest_path: manifest_key,
|
|
263
|
+
name: change[:name],
|
|
264
|
+
ecosystem: change[:ecosystem],
|
|
265
|
+
change_type: change[:change_type],
|
|
266
|
+
requirement: change[:requirement],
|
|
267
|
+
previous_requirement: change[:previous_requirement],
|
|
268
|
+
dependency_type: change[:dependency_type]
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
snapshot = result[:snapshot]
|
|
273
|
+
|
|
274
|
+
if dependency_commit_count % SNAPSHOT_INTERVAL == 0
|
|
275
|
+
snapshot.each do |(manifest_path, name), dep_info|
|
|
276
|
+
pending_snapshots << {
|
|
277
|
+
sha: rugged_commit.oid,
|
|
278
|
+
manifest_path: manifest_path,
|
|
279
|
+
name: name,
|
|
280
|
+
ecosystem: dep_info[:ecosystem],
|
|
281
|
+
requirement: dep_info[:requirement],
|
|
282
|
+
dependency_type: dep_info[:dependency_type]
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
snapshots_stored += snapshot.size
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
flush.call if pending_commits.size >= BATCH_SIZE
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
if snapshot.any?
|
|
293
|
+
last_sha = commits.last&.oid
|
|
294
|
+
if last_sha && !pending_snapshots.any? { |s| s[:sha] == last_sha }
|
|
295
|
+
snapshot.each do |(manifest_path, name), dep_info|
|
|
296
|
+
pending_snapshots << {
|
|
297
|
+
sha: last_sha,
|
|
298
|
+
manifest_path: manifest_path,
|
|
299
|
+
name: name,
|
|
300
|
+
ecosystem: dep_info[:ecosystem],
|
|
301
|
+
requirement: dep_info[:requirement],
|
|
302
|
+
dependency_type: dep_info[:dependency_type]
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
snapshots_stored += snapshot.size
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
flush.call
|
|
310
|
+
|
|
311
|
+
{ dependency_commits: dependency_commit_count, snapshots_stored: snapshots_stored }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def print_help
|
|
315
|
+
puts <<~HELP
|
|
316
|
+
Usage: git pkgs branch <subcommand> [options]
|
|
317
|
+
|
|
318
|
+
Subcommands:
|
|
319
|
+
add <name> Analyze and track a branch
|
|
320
|
+
list List tracked branches
|
|
321
|
+
remove <name> Stop tracking a branch
|
|
322
|
+
|
|
323
|
+
Examples:
|
|
324
|
+
git pkgs branch add feature-x
|
|
325
|
+
git pkgs branch list
|
|
326
|
+
git pkgs branch remove feature-x
|
|
327
|
+
HELP
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def parse_options
|
|
331
|
+
options = {}
|
|
332
|
+
options
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Diff
|
|
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
|
+
from_sha = @options[:from]
|
|
23
|
+
to_sha = @options[:to] || repo.head_sha
|
|
24
|
+
|
|
25
|
+
unless from_sha
|
|
26
|
+
$stderr.puts "Usage: git pkgs diff --from=SHA [--to=SHA]"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
from_commit = Models::Commit.find_by(sha: from_sha) ||
|
|
31
|
+
Models::Commit.where("sha LIKE ?", "#{from_sha}%").first
|
|
32
|
+
to_commit = Models::Commit.find_by(sha: to_sha) ||
|
|
33
|
+
Models::Commit.where("sha LIKE ?", "#{to_sha}%").first
|
|
34
|
+
|
|
35
|
+
unless from_commit
|
|
36
|
+
$stderr.puts "Commit '#{from_sha}' not found in database"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
unless to_commit
|
|
41
|
+
$stderr.puts "Commit '#{to_sha}' not found in database"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get all changes between the two commits
|
|
46
|
+
changes = Models::DependencyChange
|
|
47
|
+
.includes(:commit, :manifest)
|
|
48
|
+
.joins(:commit)
|
|
49
|
+
.where("commits.committed_at > ? AND commits.committed_at <= ?",
|
|
50
|
+
from_commit.committed_at, to_commit.committed_at)
|
|
51
|
+
.order("commits.committed_at ASC")
|
|
52
|
+
|
|
53
|
+
if @options[:ecosystem]
|
|
54
|
+
changes = changes.where(ecosystem: @options[:ecosystem])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if changes.empty?
|
|
58
|
+
puts "No dependency changes between #{from_commit.short_sha} and #{to_commit.short_sha}"
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
puts "Dependency changes from #{from_commit.short_sha} to #{to_commit.short_sha}:"
|
|
63
|
+
puts
|
|
64
|
+
|
|
65
|
+
added = changes.select { |c| c.change_type == "added" }
|
|
66
|
+
modified = changes.select { |c| c.change_type == "modified" }
|
|
67
|
+
removed = changes.select { |c| c.change_type == "removed" }
|
|
68
|
+
|
|
69
|
+
if added.any?
|
|
70
|
+
puts "Added:"
|
|
71
|
+
added.group_by(&:name).each do |name, pkg_changes|
|
|
72
|
+
latest = pkg_changes.last
|
|
73
|
+
puts " + #{name} #{latest.requirement} (#{latest.manifest.path})"
|
|
74
|
+
end
|
|
75
|
+
puts
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if modified.any?
|
|
79
|
+
puts "Modified:"
|
|
80
|
+
modified.group_by(&:name).each do |name, pkg_changes|
|
|
81
|
+
first = pkg_changes.first
|
|
82
|
+
latest = pkg_changes.last
|
|
83
|
+
puts " ~ #{name} #{first.previous_requirement} -> #{latest.requirement}"
|
|
84
|
+
end
|
|
85
|
+
puts
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if removed.any?
|
|
89
|
+
puts "Removed:"
|
|
90
|
+
removed.group_by(&:name).each do |name, pkg_changes|
|
|
91
|
+
latest = pkg_changes.last
|
|
92
|
+
puts " - #{name} (was #{latest.requirement})"
|
|
93
|
+
end
|
|
94
|
+
puts
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Summary
|
|
98
|
+
puts "Summary: +#{added.map(&:name).uniq.count} -#{removed.map(&:name).uniq.count} ~#{modified.map(&:name).uniq.count}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_options
|
|
102
|
+
options = {}
|
|
103
|
+
|
|
104
|
+
parser = OptionParser.new do |opts|
|
|
105
|
+
opts.banner = "Usage: git pkgs diff --from=SHA [--to=SHA] [options]"
|
|
106
|
+
|
|
107
|
+
opts.on("-f", "--from=SHA", "Start commit (required)") do |v|
|
|
108
|
+
options[:from] = v
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
opts.on("-t", "--to=SHA", "End commit (default: HEAD)") do |v|
|
|
112
|
+
options[:to] = v
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
116
|
+
options[:ecosystem] = v
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
opts.on("-h", "--help", "Show this help") do
|
|
120
|
+
puts opts
|
|
121
|
+
exit
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
parser.parse!(@args)
|
|
126
|
+
options
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|