git-pkgs 0.3.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 +27 -0
- data/LICENSE +189 -189
- data/README.md +89 -39
- data/lib/git/pkgs/analyzer.rb +36 -13
- data/lib/git/pkgs/cli.rb +86 -29
- data/lib/git/pkgs/color.rb +1 -0
- data/lib/git/pkgs/commands/blame.rb +26 -8
- 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 +171 -0
- 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 +70 -2
- data/lib/git/pkgs/commands/init.rb +40 -24
- 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/commands/where.rb +166 -0
- 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 +36 -0
- metadata +6 -8
- data/CODE_OF_CONDUCT.md +0 -10
- data/Rakefile +0 -8
- 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
|
@@ -6,8 +6,16 @@ module Git
|
|
|
6
6
|
class Init
|
|
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
|
|
@@ -17,8 +25,11 @@ module Git
|
|
|
17
25
|
def run
|
|
18
26
|
repo = Repository.new
|
|
19
27
|
|
|
28
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
29
|
+
error "Branch '#{branch_name}' not found. Check 'git branch -a' for available branches." unless repo.branch_exists?(branch_name)
|
|
30
|
+
|
|
20
31
|
if Database.exists?(repo.git_dir) && !@options[:force]
|
|
21
|
-
|
|
32
|
+
info "Database already exists. Use --force to rebuild."
|
|
22
33
|
return
|
|
23
34
|
end
|
|
24
35
|
|
|
@@ -27,35 +38,36 @@ module Git
|
|
|
27
38
|
Database.create_schema(with_indexes: false)
|
|
28
39
|
Database.optimize_for_bulk_writes
|
|
29
40
|
|
|
30
|
-
branch_name = @options[:branch] || repo.default_branch
|
|
31
|
-
error "Branch '#{branch_name}' not found" unless repo.branch_exists?(branch_name)
|
|
32
|
-
|
|
33
41
|
branch = Models::Branch.find_or_create(branch_name)
|
|
34
42
|
analyzer = Analyzer.new(repo)
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
info "Analyzing branch: #{branch_name}"
|
|
37
45
|
|
|
46
|
+
print "Loading commits..." unless Git::Pkgs.quiet
|
|
38
47
|
walker = repo.walk(branch_name, @options[:since])
|
|
39
48
|
commits = walker.to_a
|
|
40
49
|
total = commits.size
|
|
50
|
+
print "\rPrefetching diffs..." unless Git::Pkgs.quiet
|
|
51
|
+
repo.prefetch_blob_paths(commits)
|
|
52
|
+
print "\r#{' ' * 20}\r" unless Git::Pkgs.quiet
|
|
41
53
|
|
|
42
54
|
stats = bulk_process_commits(commits, branch, analyzer, total)
|
|
43
55
|
|
|
44
56
|
branch.update(last_analyzed_sha: repo.branch_target(branch_name))
|
|
45
57
|
|
|
46
|
-
print "\rCreating indexes
|
|
58
|
+
print "\rCreating indexes...#{' ' * 20}" unless Git::Pkgs.quiet
|
|
47
59
|
Database.create_bulk_indexes
|
|
48
60
|
Database.optimize_for_reads
|
|
49
61
|
|
|
50
62
|
cache_stats = analyzer.cache_stats
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
info "\rDone!#{' ' * 20}"
|
|
65
|
+
info "Analyzed #{total} commits"
|
|
66
|
+
info "Found #{stats[:dependency_commits]} commits with dependency changes"
|
|
67
|
+
info "Stored #{stats[:snapshots_stored]} snapshots (every #{snapshot_interval} changes)"
|
|
68
|
+
info "Blob cache: #{cache_stats[:cached_blobs]} unique blobs, #{cache_stats[:blobs_with_hits]} had cache hits"
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
unless @options[:no_hooks]
|
|
59
71
|
Commands::Hooks.new(["--install"]).run
|
|
60
72
|
end
|
|
61
73
|
end
|
|
@@ -73,6 +85,7 @@ module Git
|
|
|
73
85
|
dependency_commit_count = 0
|
|
74
86
|
snapshots_stored = 0
|
|
75
87
|
processed = 0
|
|
88
|
+
last_processed_sha = nil
|
|
76
89
|
|
|
77
90
|
flush = lambda do
|
|
78
91
|
return if pending_commits.empty?
|
|
@@ -134,9 +147,11 @@ module Git
|
|
|
134
147
|
pending_snapshots.clear
|
|
135
148
|
end
|
|
136
149
|
|
|
150
|
+
progress_interval = [total / 100, 10].max
|
|
151
|
+
|
|
137
152
|
commits.each do |rugged_commit|
|
|
138
153
|
processed += 1
|
|
139
|
-
print "\rProcessing commit #{processed}/#{total}..." if processed %
|
|
154
|
+
print "\rProcessing commit #{processed}/#{total}..." if !Git::Pkgs.quiet && (processed % progress_interval == 0 || processed == total)
|
|
140
155
|
|
|
141
156
|
next if rugged_commit.parents.length > 1 # skip merge commits
|
|
142
157
|
|
|
@@ -160,6 +175,8 @@ module Git
|
|
|
160
175
|
position: processed
|
|
161
176
|
}
|
|
162
177
|
|
|
178
|
+
last_processed_sha = rugged_commit.oid
|
|
179
|
+
|
|
163
180
|
if has_changes
|
|
164
181
|
dependency_commit_count += 1
|
|
165
182
|
|
|
@@ -188,7 +205,7 @@ module Git
|
|
|
188
205
|
snapshot = result[:snapshot]
|
|
189
206
|
|
|
190
207
|
# Store snapshot at intervals
|
|
191
|
-
if dependency_commit_count %
|
|
208
|
+
if dependency_commit_count % snapshot_interval == 0
|
|
192
209
|
snapshot.each do |(manifest_path, name), dep_info|
|
|
193
210
|
pending_snapshots << {
|
|
194
211
|
sha: rugged_commit.oid,
|
|
@@ -203,16 +220,15 @@ module Git
|
|
|
203
220
|
end
|
|
204
221
|
end
|
|
205
222
|
|
|
206
|
-
flush.call if pending_commits.size >=
|
|
223
|
+
flush.call if pending_commits.size >= batch_size
|
|
207
224
|
end
|
|
208
225
|
|
|
209
|
-
# Always store final snapshot for
|
|
210
|
-
if snapshot.any?
|
|
211
|
-
|
|
212
|
-
if last_sha && !pending_snapshots.any? { |s| s[:sha] == last_sha }
|
|
226
|
+
# Always store final snapshot for the last processed commit
|
|
227
|
+
if snapshot.any? && last_processed_sha
|
|
228
|
+
unless pending_snapshots.any? { |s| s[:sha] == last_processed_sha }
|
|
213
229
|
snapshot.each do |(manifest_path, name), dep_info|
|
|
214
230
|
pending_snapshots << {
|
|
215
|
-
sha:
|
|
231
|
+
sha: last_processed_sha,
|
|
216
232
|
manifest_path: manifest_path,
|
|
217
233
|
name: name,
|
|
218
234
|
ecosystem: dep_info[:ecosystem],
|
|
@@ -247,8 +263,8 @@ module Git
|
|
|
247
263
|
options[:force] = true
|
|
248
264
|
end
|
|
249
265
|
|
|
250
|
-
opts.on("--hooks", "
|
|
251
|
-
options[:
|
|
266
|
+
opts.on("--no-hooks", "Skip installing git hooks") do
|
|
267
|
+
options[:no_hooks] = true
|
|
252
268
|
end
|
|
253
269
|
|
|
254
270
|
opts.on("-h", "--help", "Show this help") do
|
|
@@ -20,7 +20,7 @@ module Git
|
|
|
20
20
|
commit_sha = @options[:commit] || repo.head_sha
|
|
21
21
|
target_commit = Models::Commit.find_by(sha: commit_sha)
|
|
22
22
|
|
|
23
|
-
error "Commit #{commit_sha[0, 7]} not
|
|
23
|
+
error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
|
|
24
24
|
|
|
25
25
|
deps = compute_dependencies_at_commit(target_commit, repo)
|
|
26
26
|
|
|
@@ -90,7 +90,7 @@ module Git
|
|
|
90
90
|
# Replay changes from snapshot to target
|
|
91
91
|
if snapshot_commit && snapshot_commit.id != target_commit.id
|
|
92
92
|
changes = Models::DependencyChange
|
|
93
|
-
.joins(:commit
|
|
93
|
+
.joins(:commit)
|
|
94
94
|
.where(commits: { id: branch.commit_ids })
|
|
95
95
|
.where("commits.committed_at > ? AND commits.committed_at <= ?",
|
|
96
96
|
snapshot_commit.committed_at, target_commit.committed_at)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
3
|
module Git
|
|
6
4
|
module Pkgs
|
|
7
5
|
module Commands
|
|
@@ -20,6 +18,7 @@ module Git
|
|
|
20
18
|
Database.connect(repo.git_dir)
|
|
21
19
|
|
|
22
20
|
commits = Models::Commit
|
|
21
|
+
.includes(:dependency_changes)
|
|
23
22
|
.where(has_dependency_changes: true)
|
|
24
23
|
.order(committed_at: :desc)
|
|
25
24
|
|
|
@@ -44,8 +43,8 @@ module Git
|
|
|
44
43
|
|
|
45
44
|
def output_text(commits)
|
|
46
45
|
commits.each do |commit|
|
|
47
|
-
changes = commit.dependency_changes
|
|
48
|
-
changes = changes.
|
|
46
|
+
changes = commit.dependency_changes.to_a
|
|
47
|
+
changes = changes.select { |c| c.ecosystem == @options[:ecosystem] } if @options[:ecosystem]
|
|
49
48
|
next if changes.empty?
|
|
50
49
|
|
|
51
50
|
puts "#{commit.short_sha} #{commit.message&.lines&.first&.strip}"
|
|
@@ -77,8 +76,8 @@ module Git
|
|
|
77
76
|
require "json"
|
|
78
77
|
|
|
79
78
|
data = commits.map do |commit|
|
|
80
|
-
changes = commit.dependency_changes
|
|
81
|
-
changes = changes.
|
|
79
|
+
changes = commit.dependency_changes.to_a
|
|
80
|
+
changes = changes.select { |c| c.ecosystem == @options[:ecosystem] } if @options[:ecosystem]
|
|
82
81
|
|
|
83
82
|
{
|
|
84
83
|
sha: commit.sha,
|
|
@@ -102,12 +101,6 @@ module Git
|
|
|
102
101
|
puts JSON.pretty_generate(data)
|
|
103
102
|
end
|
|
104
103
|
|
|
105
|
-
def parse_time(str)
|
|
106
|
-
Time.parse(str)
|
|
107
|
-
rescue ArgumentError
|
|
108
|
-
error "Invalid date format: #{str}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
104
|
def parse_options
|
|
112
105
|
options = {}
|
|
113
106
|
|
|
@@ -20,10 +20,10 @@ module Git
|
|
|
20
20
|
Database.connect(repo.git_dir)
|
|
21
21
|
|
|
22
22
|
sha = repo.rev_parse(ref)
|
|
23
|
-
error "Could not resolve '#{ref}'" unless sha
|
|
23
|
+
error "Could not resolve '#{ref}'. Check that the ref exists with 'git rev-parse #{ref}'." unless sha
|
|
24
24
|
|
|
25
|
-
commit =
|
|
26
|
-
error "Commit '#{sha[0..7]}' not
|
|
25
|
+
commit = Models::Commit.find_or_create_from_repo(repo, sha)
|
|
26
|
+
error "Commit '#{sha[0..7]}' not in database. Run 'git pkgs update' to index new commits." unless commit
|
|
27
27
|
|
|
28
28
|
changes = Models::DependencyChange
|
|
29
29
|
.includes(:commit, :manifest)
|
|
@@ -107,26 +107,6 @@ module Git
|
|
|
107
107
|
puts JSON.pretty_generate(data)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def find_or_create_commit(repo, sha)
|
|
111
|
-
commit = Models::Commit.find_by(sha: sha) ||
|
|
112
|
-
Models::Commit.where("sha LIKE ?", "#{sha}%").first
|
|
113
|
-
return commit if commit
|
|
114
|
-
|
|
115
|
-
rugged_commit = repo.lookup(sha)
|
|
116
|
-
return nil unless rugged_commit
|
|
117
|
-
|
|
118
|
-
Models::Commit.create!(
|
|
119
|
-
sha: rugged_commit.oid,
|
|
120
|
-
message: rugged_commit.message,
|
|
121
|
-
author_name: rugged_commit.author[:name],
|
|
122
|
-
author_email: rugged_commit.author[:email],
|
|
123
|
-
committed_at: rugged_commit.time,
|
|
124
|
-
has_dependency_changes: false
|
|
125
|
-
)
|
|
126
|
-
rescue Rugged::OdbError
|
|
127
|
-
nil
|
|
128
|
-
end
|
|
129
|
-
|
|
130
110
|
def parse_options
|
|
131
111
|
options = {}
|
|
132
112
|
|
|
@@ -20,7 +20,7 @@ module Git
|
|
|
20
20
|
branch_name = @options[:branch] || repo.default_branch
|
|
21
21
|
branch = Models::Branch.find_by(name: branch_name)
|
|
22
22
|
|
|
23
|
-
error "No analysis found for branch '#{branch_name}'" unless branch&.last_analyzed_sha
|
|
23
|
+
error "No analysis found for branch '#{branch_name}'. Run 'git pkgs init' first." unless branch&.last_analyzed_sha
|
|
24
24
|
|
|
25
25
|
current_commit = Models::Commit.find_by(sha: branch.last_analyzed_sha)
|
|
26
26
|
snapshots = current_commit&.dependency_snapshots&.includes(:manifest) || []
|
|
@@ -34,19 +34,38 @@ module Git
|
|
|
34
34
|
return
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Batch fetch all changes for current dependencies
|
|
38
|
+
snapshot_keys = snapshots.map { |s| [s.name, s.manifest_id] }.to_set
|
|
39
|
+
manifest_ids = snapshots.map(&:manifest_id).uniq
|
|
40
|
+
names = snapshots.map(&:name).uniq
|
|
41
|
+
|
|
42
|
+
all_changes = Models::DependencyChange
|
|
43
|
+
.includes(:commit)
|
|
44
|
+
.where(manifest_id: manifest_ids, name: names)
|
|
45
|
+
.to_a
|
|
46
|
+
|
|
47
|
+
# Group by (name, manifest_id) and find latest by committed_at
|
|
48
|
+
latest_by_key = {}
|
|
49
|
+
all_changes.each do |change|
|
|
50
|
+
key = [change.name, change.manifest_id]
|
|
51
|
+
next unless snapshot_keys.include?(key)
|
|
52
|
+
|
|
53
|
+
existing = latest_by_key[key]
|
|
54
|
+
if existing.nil? || change.commit.committed_at > existing.commit.committed_at
|
|
55
|
+
latest_by_key[key] = change
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
37
59
|
# Find last update for each dependency
|
|
38
60
|
outdated_data = []
|
|
61
|
+
now = Time.now
|
|
39
62
|
|
|
40
63
|
snapshots.each do |snapshot|
|
|
41
|
-
last_change =
|
|
42
|
-
.includes(:commit)
|
|
43
|
-
.where(name: snapshot.name, manifest: snapshot.manifest)
|
|
44
|
-
.order("commits.committed_at DESC")
|
|
45
|
-
.first
|
|
64
|
+
last_change = latest_by_key[[snapshot.name, snapshot.manifest_id]]
|
|
46
65
|
|
|
47
66
|
next unless last_change
|
|
48
67
|
|
|
49
|
-
days_since_update = ((
|
|
68
|
+
days_since_update = ((now - last_change.commit.committed_at) / 86400).to_i
|
|
50
69
|
|
|
51
70
|
outdated_data << {
|
|
52
71
|
name: snapshot.name,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
3
|
module Git
|
|
6
4
|
module Pkgs
|
|
7
5
|
module Commands
|
|
@@ -93,11 +91,16 @@ module Git
|
|
|
93
91
|
manifests = Models::Manifest.all
|
|
94
92
|
manifests = manifests.where(ecosystem: ecosystem) if ecosystem
|
|
95
93
|
|
|
94
|
+
manifest_ids = manifests.pluck(:id)
|
|
95
|
+
change_counts_query = Models::DependencyChange
|
|
96
|
+
.joins(:commit)
|
|
97
|
+
.where(manifest_id: manifest_ids)
|
|
98
|
+
change_counts_query = change_counts_query.where("commits.committed_at >= ?", since_time) if since_time
|
|
99
|
+
change_counts_query = change_counts_query.where("commits.committed_at <= ?", until_time) if until_time
|
|
100
|
+
change_counts = change_counts_query.group(:manifest_id).count
|
|
101
|
+
|
|
96
102
|
data[:manifests] = manifests.map do |manifest|
|
|
97
|
-
|
|
98
|
-
manifest_changes = manifest_changes.where("commits.committed_at >= ?", since_time) if since_time
|
|
99
|
-
manifest_changes = manifest_changes.where("commits.committed_at <= ?", until_time) if until_time
|
|
100
|
-
{ path: manifest.path, ecosystem: manifest.ecosystem, changes: manifest_changes.count }
|
|
103
|
+
{ path: manifest.path, ecosystem: manifest.ecosystem, changes: change_counts[manifest.id] || 0 }
|
|
101
104
|
end
|
|
102
105
|
|
|
103
106
|
data
|
|
@@ -199,12 +202,6 @@ module Git
|
|
|
199
202
|
end
|
|
200
203
|
end
|
|
201
204
|
|
|
202
|
-
def parse_time(str)
|
|
203
|
-
Time.parse(str)
|
|
204
|
-
rescue ArgumentError
|
|
205
|
-
error "Invalid date format: #{str}"
|
|
206
|
-
end
|
|
207
|
-
|
|
208
205
|
def parse_options
|
|
209
206
|
options = {}
|
|
210
207
|
|
|
@@ -21,7 +21,7 @@ module Git
|
|
|
21
21
|
commit_sha = @options[:commit] || repo.head_sha
|
|
22
22
|
commit = find_commit_with_snapshot(commit_sha, repo)
|
|
23
23
|
|
|
24
|
-
error "No dependency data
|
|
24
|
+
error "No dependency data for commit #{commit_sha[0, 7]}. Run 'git pkgs update' to index new commits." unless commit
|
|
25
25
|
|
|
26
26
|
# Get current snapshots
|
|
27
27
|
snapshots = commit.dependency_snapshots.includes(:manifest)
|
|
@@ -26,7 +26,7 @@ module Git
|
|
|
26
26
|
current_sha = repo.branch_target(branch_name)
|
|
27
27
|
|
|
28
28
|
if since_sha == current_sha
|
|
29
|
-
|
|
29
|
+
info "Already up to date."
|
|
30
30
|
return
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -50,17 +50,19 @@ module Git
|
|
|
50
50
|
walker = repo.walk(branch_name, since_sha)
|
|
51
51
|
commits = walker.to_a
|
|
52
52
|
total = commits.size
|
|
53
|
+
repo.prefetch_blob_paths(commits)
|
|
54
|
+
|
|
53
55
|
processed = 0
|
|
54
56
|
dependency_commits = 0
|
|
55
57
|
last_position = Models::BranchCommit.where(branch: branch).maximum(:position) || 0
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
info "Updating branch: #{branch_name}"
|
|
60
|
+
info "Found #{total} new commits"
|
|
59
61
|
|
|
60
62
|
ActiveRecord::Base.transaction do
|
|
61
63
|
commits.each do |rugged_commit|
|
|
62
64
|
processed += 1
|
|
63
|
-
print "\rProcessing commit #{processed}/#{total}..."
|
|
65
|
+
print "\rProcessing commit #{processed}/#{total}..." unless Git::Pkgs.quiet
|
|
64
66
|
|
|
65
67
|
result = analyzer.analyze_commit(rugged_commit, snapshot)
|
|
66
68
|
|
|
@@ -114,9 +116,9 @@ module Git
|
|
|
114
116
|
branch.update(last_analyzed_sha: current_sha)
|
|
115
117
|
end
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
info "\nDone!"
|
|
120
|
+
info "Processed #{total} new commits"
|
|
121
|
+
info "Found #{dependency_commits} commits with dependency changes"
|
|
120
122
|
end
|
|
121
123
|
|
|
122
124
|
def parse_options
|
|
@@ -21,13 +21,13 @@ module Git
|
|
|
21
21
|
current = Database::SCHEMA_VERSION
|
|
22
22
|
|
|
23
23
|
if stored >= current
|
|
24
|
-
|
|
24
|
+
info "Database is up to date (version #{current})"
|
|
25
25
|
return
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
info "Upgrading database from version #{stored} to #{current}..."
|
|
29
|
+
info "This requires re-indexing the repository."
|
|
30
|
+
info ""
|
|
31
31
|
|
|
32
32
|
# Run init --force
|
|
33
33
|
Init.new(["--force"]).run
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Where
|
|
7
|
+
include Output
|
|
8
|
+
|
|
9
|
+
def initialize(args)
|
|
10
|
+
@args = args
|
|
11
|
+
@options = parse_options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
name = @args.first
|
|
16
|
+
|
|
17
|
+
error "Usage: git pkgs where <package-name>" unless name
|
|
18
|
+
|
|
19
|
+
repo = Repository.new
|
|
20
|
+
require_database(repo)
|
|
21
|
+
|
|
22
|
+
Database.connect(repo.git_dir)
|
|
23
|
+
|
|
24
|
+
workdir = File.dirname(repo.git_dir)
|
|
25
|
+
branch = Models::Branch.find_by(name: @options[:branch] || repo.default_branch)
|
|
26
|
+
|
|
27
|
+
unless branch
|
|
28
|
+
error "Branch not found. Run 'git pkgs init' first."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
snapshots = Models::DependencySnapshot.current_for_branch(branch)
|
|
32
|
+
snapshots = snapshots.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem]
|
|
33
|
+
|
|
34
|
+
manifest_paths = snapshots.for_package(name).joins(:manifest).pluck("manifests.path").uniq
|
|
35
|
+
|
|
36
|
+
if manifest_paths.empty?
|
|
37
|
+
empty_result "Package '#{name}' not found in current dependencies"
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
results = manifest_paths.flat_map do |path|
|
|
42
|
+
find_in_manifest(name, File.join(workdir, path), path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if results.empty?
|
|
46
|
+
empty_result "Package '#{name}' tracked but not found in current files"
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if @options[:format] == "json"
|
|
51
|
+
output_json(results)
|
|
52
|
+
else
|
|
53
|
+
paginate { output_text(results, name) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find_in_manifest(name, full_path, display_path)
|
|
58
|
+
return [] unless File.exist?(full_path)
|
|
59
|
+
|
|
60
|
+
lines = File.readlines(full_path)
|
|
61
|
+
matches = []
|
|
62
|
+
|
|
63
|
+
lines.each_with_index do |line, idx|
|
|
64
|
+
next unless line.include?(name)
|
|
65
|
+
|
|
66
|
+
match = { path: display_path, line: idx + 1, content: line.rstrip }
|
|
67
|
+
|
|
68
|
+
if context_lines > 0
|
|
69
|
+
match[:before] = context_before(lines, idx)
|
|
70
|
+
match[:after] = context_after(lines, idx)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
matches << match
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
matches
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def context_lines
|
|
80
|
+
@options[:context] || 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def context_before(lines, idx)
|
|
84
|
+
start_idx = [0, idx - context_lines].max
|
|
85
|
+
(start_idx...idx).map { |i| { line: i + 1, content: lines[i].rstrip } }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def context_after(lines, idx)
|
|
89
|
+
end_idx = [lines.length - 1, idx + context_lines].min
|
|
90
|
+
((idx + 1)..end_idx).map { |i| { line: i + 1, content: lines[i].rstrip } }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def output_text(results, name)
|
|
94
|
+
results.each_with_index do |result, i|
|
|
95
|
+
puts "--" if i > 0 && context_lines > 0
|
|
96
|
+
|
|
97
|
+
result[:before]&.each do |ctx|
|
|
98
|
+
puts format_context_line(result[:path], ctx[:line], ctx[:content])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
puts format_match_line(result[:path], result[:line], result[:content], name)
|
|
102
|
+
|
|
103
|
+
result[:after]&.each do |ctx|
|
|
104
|
+
puts format_context_line(result[:path], ctx[:line], ctx[:content])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def format_match_line(path, line_num, content, name)
|
|
110
|
+
path_str = Color.magenta(path)
|
|
111
|
+
line_str = Color.green(line_num.to_s)
|
|
112
|
+
highlighted = content.gsub(name, Color.red(name))
|
|
113
|
+
"#{path_str}:#{line_str}:#{highlighted}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_context_line(path, line_num, content)
|
|
117
|
+
path_str = Color.magenta(path)
|
|
118
|
+
line_str = Color.green(line_num.to_s)
|
|
119
|
+
content_str = Color.dim(content)
|
|
120
|
+
"#{path_str}-#{line_str}-#{content_str}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def output_json(results)
|
|
124
|
+
require "json"
|
|
125
|
+
puts JSON.pretty_generate(results)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_options
|
|
129
|
+
options = {}
|
|
130
|
+
|
|
131
|
+
parser = OptionParser.new do |opts|
|
|
132
|
+
opts.banner = "Usage: git pkgs where <package-name> [options]"
|
|
133
|
+
|
|
134
|
+
opts.on("-b", "--branch=NAME", "Branch to search (default: current)") do |v|
|
|
135
|
+
options[:branch] = v
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
139
|
+
options[:ecosystem] = v
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
opts.on("-C", "--context=NUM", Integer, "Show NUM lines of context") do |v|
|
|
143
|
+
options[:context] = v
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
147
|
+
options[:format] = v
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
opts.on("--no-pager", "Do not pipe output into a pager") do
|
|
151
|
+
options[:no_pager] = true
|
|
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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bibliothecary"
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Pkgs
|
|
7
|
+
module Config
|
|
8
|
+
# Ecosystems that require remote parsing services - disabled by default
|
|
9
|
+
REMOTE_ECOSYSTEMS = %w[carthage clojars hackage hex swiftpm].freeze
|
|
10
|
+
|
|
11
|
+
# File patterns ignored by default (SBOM formats not supported)
|
|
12
|
+
DEFAULT_IGNORED_FILES = %w[
|
|
13
|
+
cyclonedx.xml
|
|
14
|
+
cyclonedx.json
|
|
15
|
+
*.cdx.xml
|
|
16
|
+
*.cdx.json
|
|
17
|
+
*.spdx
|
|
18
|
+
*.spdx.json
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def self.ignored_dirs
|
|
22
|
+
@ignored_dirs ||= read_config_list("pkgs.ignoredDirs")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.ignored_files
|
|
26
|
+
@ignored_files ||= read_config_list("pkgs.ignoredFiles")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.ecosystems
|
|
30
|
+
@ecosystems ||= read_config_list("pkgs.ecosystems")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.configure_bibliothecary
|
|
34
|
+
dirs = ignored_dirs
|
|
35
|
+
files = DEFAULT_IGNORED_FILES + ignored_files
|
|
36
|
+
|
|
37
|
+
Bibliothecary.configure do |config|
|
|
38
|
+
config.ignored_dirs += dirs unless dirs.empty?
|
|
39
|
+
config.ignored_files += files
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.filter_ecosystem?(platform)
|
|
44
|
+
platform_lower = platform.to_s.downcase
|
|
45
|
+
|
|
46
|
+
# Remote ecosystems are disabled unless explicitly enabled
|
|
47
|
+
if REMOTE_ECOSYSTEMS.include?(platform_lower)
|
|
48
|
+
return !ecosystems.map(&:downcase).include?(platform_lower)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# If no filter configured, allow all non-remote ecosystems
|
|
52
|
+
return false if ecosystems.empty?
|
|
53
|
+
|
|
54
|
+
# Otherwise, only allow explicitly listed ecosystems
|
|
55
|
+
!ecosystems.map(&:downcase).include?(platform_lower)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.remote_ecosystem?(platform)
|
|
59
|
+
REMOTE_ECOSYSTEMS.include?(platform.to_s.downcase)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.reset!
|
|
63
|
+
@ignored_dirs = nil
|
|
64
|
+
@ignored_files = nil
|
|
65
|
+
@ecosystems = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.read_config_list(key)
|
|
69
|
+
`git config --get-all #{key} 2>/dev/null`.split("\n").map(&:strip).reject(&:empty?)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|