diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eb26f4..2b9895b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - `git pkgs init` now installs git hooks by default (use `--no-hooks` to skip) +- Fix N+1 queries in `blame`, `stale`, `stats`, and `log` commands ## [0.4.0] - 2026-01-04 diff --git a/lib/git/pkgs/analyzer.rb b/lib/git/pkgs/analyzer.rb index 5c66a87..f2f1fdd 100644 --- a/lib/git/pkgs/analyzer.rb +++ b/lib/git/pkgs/analyzer.rb @@ -251,7 +251,7 @@ def parse_manifest_before_commit(rugged_commit, manifest_path) end def parse_manifest_by_oid(blob_oid, manifest_path) - cache_key = "#{blob_oid}:#{manifest_path}" + cache_key = [blob_oid, manifest_path] if @blob_cache.key?(cache_key) @blob_cache[cache_key][:hits] += 1 diff --git a/lib/git/pkgs/commands/blame.rb b/lib/git/pkgs/commands/blame.rb index e932fc6..0580f95 100644 --- a/lib/git/pkgs/commands/blame.rb +++ b/lib/git/pkgs/commands/blame.rb @@ -35,16 +35,34 @@ def run return end + # Batch fetch all "added" changes for current dependencies + snapshot_keys = snapshots.map { |s| [s.name, s.manifest_id] }.to_set + manifest_ids = snapshots.map(&:manifest_id).uniq + names = snapshots.map(&:name).uniq + + all_added_changes = Models::DependencyChange + .includes(:commit) + .added + .where(manifest_id: manifest_ids, name: names) + .to_a + + # Group by (name, manifest_id) and find earliest by committed_at + added_by_key = {} + all_added_changes.each do |change| + key = [change.name, change.manifest_id] + next unless snapshot_keys.include?(key) + + existing = added_by_key[key] + if existing.nil? || change.commit.committed_at < existing.commit.committed_at + added_by_key[key] = change + end + end + # For each current dependency, find who added it blame_data = [] snapshots.each do |snapshot| - added_change = Models::DependencyChange - .includes(:commit) - .where(name: snapshot.name, manifest: snapshot.manifest) - .added - .order("commits.committed_at ASC") - .first + added_change = added_by_key[[snapshot.name, snapshot.manifest_id]] next unless added_change diff --git a/lib/git/pkgs/commands/list.rb b/lib/git/pkgs/commands/list.rb index c531da5..571cb5f 100644 --- a/lib/git/pkgs/commands/list.rb +++ b/lib/git/pkgs/commands/list.rb @@ -90,7 +90,7 @@ def compute_dependencies_at_commit(target_commit, repo) # Replay changes from snapshot to target if snapshot_commit && snapshot_commit.id != target_commit.id changes = Models::DependencyChange - .joins(:commit, :manifest) + .joins(:commit) .where(commits: { id: branch.commit_ids }) .where("commits.committed_at > ? AND commits.committed_at <= ?", snapshot_commit.committed_at, target_commit.committed_at) diff --git a/lib/git/pkgs/commands/log.rb b/lib/git/pkgs/commands/log.rb index 6b76d93..311a391 100644 --- a/lib/git/pkgs/commands/log.rb +++ b/lib/git/pkgs/commands/log.rb @@ -18,6 +18,7 @@ def run Database.connect(repo.git_dir) commits = Models::Commit + .includes(:dependency_changes) .where(has_dependency_changes: true) .order(committed_at: :desc) @@ -42,8 +43,8 @@ def run def output_text(commits) commits.each do |commit| - changes = commit.dependency_changes - changes = changes.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem] + changes = commit.dependency_changes.to_a + changes = changes.select { |c| c.ecosystem == @options[:ecosystem] } if @options[:ecosystem] next if changes.empty? puts "#{commit.short_sha} #{commit.message&.lines&.first&.strip}" @@ -75,8 +76,8 @@ def output_json(commits) require "json" data = commits.map do |commit| - changes = commit.dependency_changes - changes = changes.where(ecosystem: @options[:ecosystem]) if @options[:ecosystem] + changes = commit.dependency_changes.to_a + changes = changes.select { |c| c.ecosystem == @options[:ecosystem] } if @options[:ecosystem] { sha: commit.sha, diff --git a/lib/git/pkgs/commands/stale.rb b/lib/git/pkgs/commands/stale.rb index 14f6dda..a209e5f 100644 --- a/lib/git/pkgs/commands/stale.rb +++ b/lib/git/pkgs/commands/stale.rb @@ -34,19 +34,38 @@ def run return end + # Batch fetch all changes for current dependencies + snapshot_keys = snapshots.map { |s| [s.name, s.manifest_id] }.to_set + manifest_ids = snapshots.map(&:manifest_id).uniq + names = snapshots.map(&:name).uniq + + all_changes = Models::DependencyChange + .includes(:commit) + .where(manifest_id: manifest_ids, name: names) + .to_a + + # Group by (name, manifest_id) and find latest by committed_at + latest_by_key = {} + all_changes.each do |change| + key = [change.name, change.manifest_id] + next unless snapshot_keys.include?(key) + + existing = latest_by_key[key] + if existing.nil? || change.commit.committed_at > existing.commit.committed_at + latest_by_key[key] = change + end + end + # Find last update for each dependency outdated_data = [] + now = Time.now snapshots.each do |snapshot| - last_change = Models::DependencyChange - .includes(:commit) - .where(name: snapshot.name, manifest: snapshot.manifest) - .order("commits.committed_at DESC") - .first + last_change = latest_by_key[[snapshot.name, snapshot.manifest_id]] next unless last_change - days_since_update = ((Time.now - last_change.commit.committed_at) / 86400).to_i + days_since_update = ((now - last_change.commit.committed_at) / 86400).to_i outdated_data << { name: snapshot.name, diff --git a/lib/git/pkgs/commands/stats.rb b/lib/git/pkgs/commands/stats.rb index 3b60e84..74cb056 100644 --- a/lib/git/pkgs/commands/stats.rb +++ b/lib/git/pkgs/commands/stats.rb @@ -91,11 +91,16 @@ def collect_stats(branch, branch_name) manifests = Models::Manifest.all manifests = manifests.where(ecosystem: ecosystem) if ecosystem + manifest_ids = manifests.pluck(:id) + change_counts_query = Models::DependencyChange + .joins(:commit) + .where(manifest_id: manifest_ids) + change_counts_query = change_counts_query.where("commits.committed_at >= ?", since_time) if since_time + change_counts_query = change_counts_query.where("commits.committed_at <= ?", until_time) if until_time + change_counts = change_counts_query.group(:manifest_id).count + data[:manifests] = manifests.map do |manifest| - manifest_changes = manifest.dependency_changes.joins(:commit) - manifest_changes = manifest_changes.where("commits.committed_at >= ?", since_time) if since_time - manifest_changes = manifest_changes.where("commits.committed_at <= ?", until_time) if until_time - { path: manifest.path, ecosystem: manifest.ecosystem, changes: manifest_changes.count } + { path: manifest.path, ecosystem: manifest.ecosystem, changes: change_counts[manifest.id] || 0 } end data