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,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class History
|
|
7
|
+
def initialize(args)
|
|
8
|
+
@args = args
|
|
9
|
+
@options = parse_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
package_name = @args.shift
|
|
14
|
+
|
|
15
|
+
unless package_name
|
|
16
|
+
$stderr.puts "Usage: git pkgs history <package>"
|
|
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
|
+
changes = Models::DependencyChange
|
|
30
|
+
.includes(:commit, :manifest)
|
|
31
|
+
.for_package(package_name)
|
|
32
|
+
.order("commits.committed_at ASC")
|
|
33
|
+
|
|
34
|
+
if @options[:ecosystem]
|
|
35
|
+
changes = changes.for_platform(@options[:ecosystem])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if changes.empty?
|
|
39
|
+
puts "No history found for '#{package_name}'"
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if @options[:format] == "json"
|
|
44
|
+
output_json(changes)
|
|
45
|
+
else
|
|
46
|
+
output_text(changes, package_name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def output_text(changes, package_name)
|
|
51
|
+
puts "History for #{package_name}:"
|
|
52
|
+
puts
|
|
53
|
+
|
|
54
|
+
changes.each do |change|
|
|
55
|
+
commit = change.commit
|
|
56
|
+
date = commit.committed_at.strftime("%Y-%m-%d")
|
|
57
|
+
|
|
58
|
+
case change.change_type
|
|
59
|
+
when "added"
|
|
60
|
+
action = "Added"
|
|
61
|
+
version_info = change.requirement
|
|
62
|
+
when "modified"
|
|
63
|
+
action = "Updated"
|
|
64
|
+
version_info = "#{change.previous_requirement} -> #{change.requirement}"
|
|
65
|
+
when "removed"
|
|
66
|
+
action = "Removed"
|
|
67
|
+
version_info = change.requirement
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
puts "#{date} #{action} #{version_info}"
|
|
71
|
+
puts " Commit: #{commit.short_sha} #{commit.message&.lines&.first&.strip}"
|
|
72
|
+
puts " Author: #{commit.author_name} <#{commit.author_email}>"
|
|
73
|
+
puts " Manifest: #{change.manifest.path}"
|
|
74
|
+
puts
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def output_json(changes)
|
|
79
|
+
require "json"
|
|
80
|
+
|
|
81
|
+
data = changes.map do |change|
|
|
82
|
+
{
|
|
83
|
+
date: change.commit.committed_at.iso8601,
|
|
84
|
+
change_type: change.change_type,
|
|
85
|
+
requirement: change.requirement,
|
|
86
|
+
previous_requirement: change.previous_requirement,
|
|
87
|
+
manifest: change.manifest.path,
|
|
88
|
+
ecosystem: change.ecosystem,
|
|
89
|
+
commit: {
|
|
90
|
+
sha: change.commit.sha,
|
|
91
|
+
message: change.commit.message&.lines&.first&.strip,
|
|
92
|
+
author_name: change.commit.author_name,
|
|
93
|
+
author_email: change.commit.author_email
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
puts JSON.pretty_generate(data)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_options
|
|
102
|
+
options = {}
|
|
103
|
+
|
|
104
|
+
parser = OptionParser.new do |opts|
|
|
105
|
+
opts.banner = "Usage: git pkgs history <package> [options]"
|
|
106
|
+
|
|
107
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
108
|
+
options[:ecosystem] = v
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
112
|
+
options[:format] = v
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
opts.on("-h", "--help", "Show this help") do
|
|
116
|
+
puts opts
|
|
117
|
+
exit
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
parser.parse!(@args)
|
|
122
|
+
options
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Hooks
|
|
7
|
+
HOOK_SCRIPT = <<~SCRIPT
|
|
8
|
+
#!/bin/sh
|
|
9
|
+
# git-pkgs auto-update hook
|
|
10
|
+
git pkgs update 2>/dev/null || true
|
|
11
|
+
SCRIPT
|
|
12
|
+
|
|
13
|
+
HOOKS = %w[post-commit post-merge].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(args)
|
|
16
|
+
@args = args
|
|
17
|
+
@options = parse_options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
repo = Repository.new
|
|
22
|
+
|
|
23
|
+
if @options[:install]
|
|
24
|
+
install_hooks(repo)
|
|
25
|
+
elsif @options[:uninstall]
|
|
26
|
+
uninstall_hooks(repo)
|
|
27
|
+
else
|
|
28
|
+
show_status(repo)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def install_hooks(repo)
|
|
33
|
+
hooks_dir = File.join(repo.git_dir, "hooks")
|
|
34
|
+
|
|
35
|
+
HOOKS.each do |hook_name|
|
|
36
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
37
|
+
|
|
38
|
+
if File.exist?(hook_path)
|
|
39
|
+
content = File.read(hook_path)
|
|
40
|
+
if content.include?("git-pkgs")
|
|
41
|
+
puts "Hook #{hook_name} already contains git-pkgs"
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
File.open(hook_path, "a") do |f|
|
|
46
|
+
f.puts "\n# git-pkgs auto-update"
|
|
47
|
+
f.puts "git pkgs update 2>/dev/null || true"
|
|
48
|
+
end
|
|
49
|
+
puts "Appended git-pkgs to existing #{hook_name} hook"
|
|
50
|
+
else
|
|
51
|
+
File.write(hook_path, HOOK_SCRIPT)
|
|
52
|
+
File.chmod(0o755, hook_path)
|
|
53
|
+
puts "Created #{hook_name} hook"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
puts "Hooks installed successfully"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def uninstall_hooks(repo)
|
|
61
|
+
hooks_dir = File.join(repo.git_dir, "hooks")
|
|
62
|
+
|
|
63
|
+
HOOKS.each do |hook_name|
|
|
64
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
65
|
+
next unless File.exist?(hook_path)
|
|
66
|
+
|
|
67
|
+
content = File.read(hook_path)
|
|
68
|
+
|
|
69
|
+
if content.strip == HOOK_SCRIPT.strip
|
|
70
|
+
File.delete(hook_path)
|
|
71
|
+
puts "Removed #{hook_name} hook"
|
|
72
|
+
elsif content.include?("git-pkgs")
|
|
73
|
+
new_content = content.lines.reject { |line|
|
|
74
|
+
line.include?("git-pkgs") || line.include?("git pkgs")
|
|
75
|
+
}.join
|
|
76
|
+
new_content = new_content.gsub(/\n# git-pkgs auto-update\n/, "\n")
|
|
77
|
+
|
|
78
|
+
if new_content.strip.empty? || new_content.strip == "#!/bin/sh"
|
|
79
|
+
File.delete(hook_path)
|
|
80
|
+
puts "Removed #{hook_name} hook"
|
|
81
|
+
else
|
|
82
|
+
File.write(hook_path, new_content)
|
|
83
|
+
puts "Removed git-pkgs from #{hook_name} hook"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
puts "Hooks uninstalled successfully"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def show_status(repo)
|
|
92
|
+
hooks_dir = File.join(repo.git_dir, "hooks")
|
|
93
|
+
|
|
94
|
+
puts "Hook status:"
|
|
95
|
+
HOOKS.each do |hook_name|
|
|
96
|
+
hook_path = File.join(hooks_dir, hook_name)
|
|
97
|
+
if File.exist?(hook_path) && File.read(hook_path).include?("git-pkgs")
|
|
98
|
+
puts " #{hook_name}: installed"
|
|
99
|
+
else
|
|
100
|
+
puts " #{hook_name}: not installed"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_options
|
|
106
|
+
options = {}
|
|
107
|
+
|
|
108
|
+
parser = OptionParser.new do |opts|
|
|
109
|
+
opts.banner = "Usage: git pkgs hooks [options]"
|
|
110
|
+
|
|
111
|
+
opts.on("-i", "--install", "Install git hooks for auto-updating") do
|
|
112
|
+
options[:install] = true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
opts.on("-u", "--uninstall", "Remove git hooks") do
|
|
116
|
+
options[:uninstall] = true
|
|
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
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Info
|
|
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
|
+
db_path = Database.path(repo.git_dir)
|
|
21
|
+
Database.connect(repo.git_dir)
|
|
22
|
+
|
|
23
|
+
puts "Database Info"
|
|
24
|
+
puts "=" * 40
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
# File info
|
|
28
|
+
db_size = File.size(db_path)
|
|
29
|
+
puts "Location: #{db_path}"
|
|
30
|
+
puts "Size: #{format_size(db_size)}"
|
|
31
|
+
puts
|
|
32
|
+
|
|
33
|
+
# Row counts
|
|
34
|
+
puts "Row Counts"
|
|
35
|
+
puts "-" * 40
|
|
36
|
+
counts = {
|
|
37
|
+
"Branches" => Models::Branch.count,
|
|
38
|
+
"Commits" => Models::Commit.count,
|
|
39
|
+
"Branch-Commits" => Models::BranchCommit.count,
|
|
40
|
+
"Manifests" => Models::Manifest.count,
|
|
41
|
+
"Dependency Changes" => Models::DependencyChange.count,
|
|
42
|
+
"Dependency Snapshots" => Models::DependencySnapshot.count
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
counts.each do |name, count|
|
|
46
|
+
puts " #{name.ljust(22)} #{count.to_s.rjust(10)}"
|
|
47
|
+
end
|
|
48
|
+
puts " #{'-' * 34}"
|
|
49
|
+
puts " #{'Total'.ljust(22)} #{counts.values.sum.to_s.rjust(10)}"
|
|
50
|
+
puts
|
|
51
|
+
|
|
52
|
+
# Branch info
|
|
53
|
+
puts "Branches"
|
|
54
|
+
puts "-" * 40
|
|
55
|
+
Models::Branch.all.each do |branch|
|
|
56
|
+
commit_count = branch.commits.count
|
|
57
|
+
last_sha = branch.last_analyzed_sha&.slice(0, 7) || "none"
|
|
58
|
+
puts " #{branch.name}: #{commit_count} commits (last: #{last_sha})"
|
|
59
|
+
end
|
|
60
|
+
puts
|
|
61
|
+
|
|
62
|
+
# Snapshot coverage
|
|
63
|
+
puts "Snapshot Coverage"
|
|
64
|
+
puts "-" * 40
|
|
65
|
+
total_dep_commits = Models::Commit.where(has_dependency_changes: true).count
|
|
66
|
+
snapshot_commits = Models::Commit
|
|
67
|
+
.joins(:dependency_snapshots)
|
|
68
|
+
.distinct
|
|
69
|
+
.count
|
|
70
|
+
puts " Commits with dependency changes: #{total_dep_commits}"
|
|
71
|
+
puts " Commits with snapshots: #{snapshot_commits}"
|
|
72
|
+
if total_dep_commits > 0
|
|
73
|
+
ratio = (snapshot_commits.to_f / total_dep_commits * 100).round(1)
|
|
74
|
+
puts " Coverage: #{ratio}% (1 snapshot per ~#{(total_dep_commits.to_f / snapshot_commits).round(0)} changes)"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def format_size(bytes)
|
|
79
|
+
units = %w[B KB MB GB]
|
|
80
|
+
unit_index = 0
|
|
81
|
+
size = bytes.to_f
|
|
82
|
+
|
|
83
|
+
while size >= 1024 && unit_index < units.length - 1
|
|
84
|
+
size /= 1024
|
|
85
|
+
unit_index += 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
"#{size.round(1)} #{units[unit_index]}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_options
|
|
92
|
+
options = {}
|
|
93
|
+
|
|
94
|
+
parser = OptionParser.new do |opts|
|
|
95
|
+
opts.banner = "Usage: git pkgs info"
|
|
96
|
+
|
|
97
|
+
opts.on("-h", "--help", "Show this help") do
|
|
98
|
+
puts opts
|
|
99
|
+
exit
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
parser.parse!(@args)
|
|
104
|
+
options
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Init
|
|
7
|
+
BATCH_SIZE = 100
|
|
8
|
+
SNAPSHOT_INTERVAL = 20 # Store snapshot every N dependency-changing commits
|
|
9
|
+
|
|
10
|
+
def initialize(args)
|
|
11
|
+
@args = args
|
|
12
|
+
@options = parse_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
repo = Repository.new
|
|
17
|
+
|
|
18
|
+
if Database.exists?(repo.git_dir) && !@options[:force]
|
|
19
|
+
puts "Database already exists. Use --force to rebuild."
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Database.drop if @options[:force]
|
|
24
|
+
Database.connect(repo.git_dir)
|
|
25
|
+
Database.create_schema(with_indexes: false)
|
|
26
|
+
Database.optimize_for_bulk_writes
|
|
27
|
+
|
|
28
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
29
|
+
unless repo.branch_exists?(branch_name)
|
|
30
|
+
$stderr.puts "Branch '#{branch_name}' not found"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
branch = Models::Branch.find_or_create(branch_name)
|
|
35
|
+
analyzer = Analyzer.new(repo)
|
|
36
|
+
|
|
37
|
+
puts "Analyzing branch: #{branch_name}"
|
|
38
|
+
|
|
39
|
+
walker = repo.walk(branch_name, @options[:since])
|
|
40
|
+
commits = walker.to_a
|
|
41
|
+
total = commits.size
|
|
42
|
+
|
|
43
|
+
stats = bulk_process_commits(commits, branch, analyzer, total)
|
|
44
|
+
|
|
45
|
+
branch.update(last_analyzed_sha: repo.branch_target(branch_name))
|
|
46
|
+
|
|
47
|
+
print "\rCreating indexes..."
|
|
48
|
+
Database.create_bulk_indexes
|
|
49
|
+
Database.optimize_for_reads
|
|
50
|
+
|
|
51
|
+
cache_stats = analyzer.cache_stats
|
|
52
|
+
|
|
53
|
+
puts "\rDone!#{' ' * 20}"
|
|
54
|
+
puts "Analyzed #{total} commits"
|
|
55
|
+
puts "Found #{stats[:dependency_commits]} commits with dependency changes"
|
|
56
|
+
puts "Stored #{stats[:snapshots_stored]} snapshots (every #{SNAPSHOT_INTERVAL} changes)"
|
|
57
|
+
puts "Blob cache: #{cache_stats[:cached_blobs]} unique blobs, #{cache_stats[:blobs_with_hits]} had cache hits"
|
|
58
|
+
|
|
59
|
+
if @options[:hooks]
|
|
60
|
+
Commands::Hooks.new(["--install"]).run
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bulk_process_commits(commits, branch, analyzer, total)
|
|
65
|
+
now = Time.now
|
|
66
|
+
snapshot = {}
|
|
67
|
+
manifests_cache = {}
|
|
68
|
+
|
|
69
|
+
pending_commits = []
|
|
70
|
+
pending_branch_commits = []
|
|
71
|
+
pending_changes = []
|
|
72
|
+
pending_snapshots = []
|
|
73
|
+
|
|
74
|
+
dependency_commit_count = 0
|
|
75
|
+
snapshots_stored = 0
|
|
76
|
+
processed = 0
|
|
77
|
+
|
|
78
|
+
flush = lambda do
|
|
79
|
+
return if pending_commits.empty?
|
|
80
|
+
|
|
81
|
+
ActiveRecord::Base.transaction do
|
|
82
|
+
Models::Commit.insert_all(pending_commits) if pending_commits.any?
|
|
83
|
+
|
|
84
|
+
commit_ids = Models::Commit
|
|
85
|
+
.where(sha: pending_commits.map { |c| c[:sha] })
|
|
86
|
+
.pluck(:sha, :id).to_h
|
|
87
|
+
|
|
88
|
+
if pending_branch_commits.any?
|
|
89
|
+
branch_commit_records = pending_branch_commits.map do |bc|
|
|
90
|
+
{ branch_id: bc[:branch_id], commit_id: commit_ids[bc[:sha]], position: bc[:position] }
|
|
91
|
+
end
|
|
92
|
+
Models::BranchCommit.insert_all(branch_commit_records)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if pending_changes.any?
|
|
96
|
+
manifest_ids = Models::Manifest.pluck(:path, :id).to_h
|
|
97
|
+
change_records = pending_changes.map do |c|
|
|
98
|
+
{
|
|
99
|
+
commit_id: commit_ids[c[:sha]],
|
|
100
|
+
manifest_id: manifest_ids[c[:manifest_path]],
|
|
101
|
+
name: c[:name],
|
|
102
|
+
ecosystem: c[:ecosystem],
|
|
103
|
+
change_type: c[:change_type],
|
|
104
|
+
requirement: c[:requirement],
|
|
105
|
+
previous_requirement: c[:previous_requirement],
|
|
106
|
+
dependency_type: c[:dependency_type],
|
|
107
|
+
created_at: now,
|
|
108
|
+
updated_at: now
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
Models::DependencyChange.insert_all(change_records)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if pending_snapshots.any?
|
|
115
|
+
manifest_ids ||= Models::Manifest.pluck(:path, :id).to_h
|
|
116
|
+
snapshot_records = pending_snapshots.map do |s|
|
|
117
|
+
{
|
|
118
|
+
commit_id: commit_ids[s[:sha]],
|
|
119
|
+
manifest_id: manifest_ids[s[:manifest_path]],
|
|
120
|
+
name: s[:name],
|
|
121
|
+
ecosystem: s[:ecosystem],
|
|
122
|
+
requirement: s[:requirement],
|
|
123
|
+
dependency_type: s[:dependency_type],
|
|
124
|
+
created_at: now,
|
|
125
|
+
updated_at: now
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
Models::DependencySnapshot.insert_all(snapshot_records)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
pending_commits.clear
|
|
133
|
+
pending_branch_commits.clear
|
|
134
|
+
pending_changes.clear
|
|
135
|
+
pending_snapshots.clear
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
commits.each do |rugged_commit|
|
|
139
|
+
processed += 1
|
|
140
|
+
print "\rProcessing commit #{processed}/#{total}..." if processed % 50 == 0 || processed == total
|
|
141
|
+
|
|
142
|
+
next if rugged_commit.parents.length > 1 # skip merge commits
|
|
143
|
+
|
|
144
|
+
result = analyzer.analyze_commit(rugged_commit, snapshot)
|
|
145
|
+
has_changes = result && result[:changes].any?
|
|
146
|
+
|
|
147
|
+
pending_commits << {
|
|
148
|
+
sha: rugged_commit.oid,
|
|
149
|
+
message: rugged_commit.message,
|
|
150
|
+
author_name: rugged_commit.author[:name],
|
|
151
|
+
author_email: rugged_commit.author[:email],
|
|
152
|
+
committed_at: rugged_commit.time,
|
|
153
|
+
has_dependency_changes: has_changes,
|
|
154
|
+
created_at: now,
|
|
155
|
+
updated_at: now
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pending_branch_commits << {
|
|
159
|
+
branch_id: branch.id,
|
|
160
|
+
sha: rugged_commit.oid,
|
|
161
|
+
position: processed
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if has_changes
|
|
165
|
+
dependency_commit_count += 1
|
|
166
|
+
|
|
167
|
+
result[:changes].each do |change|
|
|
168
|
+
manifest_key = change[:manifest_path]
|
|
169
|
+
unless manifests_cache[manifest_key]
|
|
170
|
+
manifests_cache[manifest_key] = Models::Manifest.find_or_create(
|
|
171
|
+
path: change[:manifest_path],
|
|
172
|
+
ecosystem: change[:ecosystem],
|
|
173
|
+
kind: change[:kind]
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
pending_changes << {
|
|
178
|
+
sha: rugged_commit.oid,
|
|
179
|
+
manifest_path: manifest_key,
|
|
180
|
+
name: change[:name],
|
|
181
|
+
ecosystem: change[:ecosystem],
|
|
182
|
+
change_type: change[:change_type],
|
|
183
|
+
requirement: change[:requirement],
|
|
184
|
+
previous_requirement: change[:previous_requirement],
|
|
185
|
+
dependency_type: change[:dependency_type]
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
snapshot = result[:snapshot]
|
|
190
|
+
|
|
191
|
+
# Store snapshot at intervals
|
|
192
|
+
if dependency_commit_count % SNAPSHOT_INTERVAL == 0
|
|
193
|
+
snapshot.each do |(manifest_path, name), dep_info|
|
|
194
|
+
pending_snapshots << {
|
|
195
|
+
sha: rugged_commit.oid,
|
|
196
|
+
manifest_path: manifest_path,
|
|
197
|
+
name: name,
|
|
198
|
+
ecosystem: dep_info[:ecosystem],
|
|
199
|
+
requirement: dep_info[:requirement],
|
|
200
|
+
dependency_type: dep_info[:dependency_type]
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
snapshots_stored += snapshot.size
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
flush.call if pending_commits.size >= BATCH_SIZE
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Always store final snapshot for HEAD
|
|
211
|
+
if snapshot.any?
|
|
212
|
+
last_sha = commits.last&.oid
|
|
213
|
+
if last_sha && !pending_snapshots.any? { |s| s[:sha] == last_sha }
|
|
214
|
+
snapshot.each do |(manifest_path, name), dep_info|
|
|
215
|
+
pending_snapshots << {
|
|
216
|
+
sha: last_sha,
|
|
217
|
+
manifest_path: manifest_path,
|
|
218
|
+
name: name,
|
|
219
|
+
ecosystem: dep_info[:ecosystem],
|
|
220
|
+
requirement: dep_info[:requirement],
|
|
221
|
+
dependency_type: dep_info[:dependency_type]
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
snapshots_stored += snapshot.size
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
flush.call
|
|
229
|
+
|
|
230
|
+
{ dependency_commits: dependency_commit_count, snapshots_stored: snapshots_stored }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def parse_options
|
|
234
|
+
options = {}
|
|
235
|
+
|
|
236
|
+
parser = OptionParser.new do |opts|
|
|
237
|
+
opts.banner = "Usage: git pkgs init [options]"
|
|
238
|
+
|
|
239
|
+
opts.on("-b", "--branch=NAME", "Branch to analyze (default: default branch)") do |v|
|
|
240
|
+
options[:branch] = v
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
opts.on("-s", "--since=SHA", "Start from specific commit") do |v|
|
|
244
|
+
options[:since] = v
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
opts.on("-f", "--force", "Rebuild database from scratch") do
|
|
248
|
+
options[:force] = true
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
opts.on("--hooks", "Install git hooks for auto-updating") do
|
|
252
|
+
options[:hooks] = true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
opts.on("-h", "--help", "Show this help") do
|
|
256
|
+
puts opts
|
|
257
|
+
exit
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
parser.parse!(@args)
|
|
262
|
+
options
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|