From 45f7f06a190e17b4a092aff2885050a1fb2acb89 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 4 Jan 2026 13:02:10 +0000 Subject: [PATCH] Add configuration options for ecosystems and ignored files/directories --- CHANGELOG.md | 2 + README.md | 21 +++- lib/git/pkgs.rb | 1 + lib/git/pkgs/analyzer.rb | 4 +- lib/git/pkgs/commands/diff_driver.rb | 2 + lib/git/pkgs/commands/info.rb | 66 ++++++++++- lib/git/pkgs/config.rb | 73 ++++++++++++ test/git/pkgs/test_config.rb | 165 +++++++++++++++++++++++++++ 8 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 lib/git/pkgs/config.rb create mode 100644 test/git/pkgs/test_config.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9895b..c63133d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - `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 +- Configuration via git config: `pkgs.ecosystems`, `pkgs.ignoredDirs`, `pkgs.ignoredFiles` +- `git pkgs info --ecosystems` to show available ecosystems and their status ## [0.4.0] - 2026-01-04 diff --git a/README.md b/README.md index a66a360..f2f9a58 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,21 @@ git-pkgs respects [standard git configuration](https://git-scm.com/docs/git-conf **Pager** follows git's precedence: `GIT_PAGER` env, `core.pager` config, `PAGER` env, then `less -FRSX`. Use `--no-pager` flag or `git config core.pager cat` to disable. +**Ecosystem filtering** lets you limit which package ecosystems are tracked: + +```bash +git config --add pkgs.ecosystems rubygems +git config --add pkgs.ecosystems npm +git pkgs info --ecosystems # show enabled/disabled ecosystems +``` + +**Ignored paths** let you skip directories or files from analysis: + +```bash +git config --add pkgs.ignoredDirs third_party +git config --add pkgs.ignoredFiles test/fixtures/package.json +``` + **Environment variables:** - `GIT_DIR` - git directory location (standard git variable) @@ -399,7 +414,11 @@ Optimizations: git-pkgs uses [ecosystems-bibliothecary](https://github.com/ecosyste-ms/bibliothecary) for parsing, supporting: -Actions, Anaconda, BentoML, Bower, Cargo, CocoaPods, Cog, CPAN, CRAN, CycloneDX, Docker, Dub, DVC, Elm, Go, Haxelib, Homebrew, Julia, Maven, Meteor, MLflow, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, SPDX, Vcpkg +Actions, BentoML, Bower, Cargo, CocoaPods, Cog, Conda, CPAN, CRAN, Docker, Dub, DVC, Elm, Go, Haxelib, Homebrew, Julia, Maven, Meteor, MLflow, npm, NuGet, Ollama, Packagist, Pub, PyPI, RubyGems, Shards, Vcpkg + +Some ecosystems require remote parsing services and are disabled by default: Carthage, Clojars, Hackage, Hex, SwiftPM. Enable with `git config --add pkgs.ecosystems `. + +SBOM formats (CycloneDX, SPDX) are not supported as they duplicate information from the actual lockfiles. ## Contributing diff --git a/lib/git/pkgs.rb b/lib/git/pkgs.rb index 459fc3a..104875b 100644 --- a/lib/git/pkgs.rb +++ b/lib/git/pkgs.rb @@ -3,6 +3,7 @@ require_relative "pkgs/version" require_relative "pkgs/output" require_relative "pkgs/color" +require_relative "pkgs/config" require_relative "pkgs/cli" require_relative "pkgs/database" require_relative "pkgs/repository" diff --git a/lib/git/pkgs/analyzer.rb b/lib/git/pkgs/analyzer.rb index f2f1fdd..f3b7b1f 100644 --- a/lib/git/pkgs/analyzer.rb +++ b/lib/git/pkgs/analyzer.rb @@ -24,8 +24,6 @@ class Analyzer Podfile Podfile.lock *.podspec *.podspec.json packages.config packages.lock.json Project.json Project.lock.json *.nuspec paket.lock *.csproj project.assets.json - cyclonedx.xml cyclonedx.json *.cdx.xml *.cdx.json - *.spdx *.spdx.json bower.json bentofile.yaml META.json META.yml environment.yml environment.yaml @@ -56,6 +54,7 @@ class Analyzer def initialize(repository) @repository = repository @blob_cache = {} + Config.configure_bibliothecary end # Quick check if any paths might be manifests (fast regex check) @@ -262,6 +261,7 @@ def parse_manifest_by_oid(blob_oid, manifest_path) return nil unless content result = Bibliothecary.analyse_file(manifest_path, content).first + result = nil if result && Config.filter_ecosystem?(result[:platform]) @blob_cache[cache_key] = { result: result, hits: 0 } result end diff --git a/lib/git/pkgs/commands/diff_driver.rb b/lib/git/pkgs/commands/diff_driver.rb index bab189d..ae5499e 100644 --- a/lib/git/pkgs/commands/diff_driver.rb +++ b/lib/git/pkgs/commands/diff_driver.rb @@ -42,6 +42,7 @@ class DiffDriver def initialize(args) @args = args @options = parse_options + Config.configure_bibliothecary end def run @@ -132,6 +133,7 @@ def parse_deps(path, content) result = Bibliothecary.analyse_file(path, content).first return {} unless result + return {} if Config.filter_ecosystem?(result[:platform]) result[:dependencies].map { |d| [d[:name], d] }.to_h rescue StandardError diff --git a/lib/git/pkgs/commands/info.rb b/lib/git/pkgs/commands/info.rb index 96e462a..9f56b9e 100644 --- a/lib/git/pkgs/commands/info.rb +++ b/lib/git/pkgs/commands/info.rb @@ -12,6 +12,11 @@ def initialize(args) end def run + if @options[:ecosystems] + output_ecosystems + return + end + repo = Repository.new require_database(repo) @@ -77,6 +82,61 @@ def run end end + def output_ecosystems + require "bibliothecary" + + all_ecosystems = Bibliothecary::Parsers.constants.map do |c| + parser = Bibliothecary::Parsers.const_get(c) + parser.platform_name if parser.respond_to?(:platform_name) + end.compact.sort + + configured = Config.ecosystems + filtering = configured.any? + + puts "Available Ecosystems" + puts "=" * 40 + puts + + enabled_ecos = [] + disabled_ecos = [] + + all_ecosystems.each do |eco| + if Config.filter_ecosystem?(eco) + remote = Config.remote_ecosystem?(eco) + disabled_ecos << { name: eco, remote: remote } + else + enabled_ecos << eco + end + end + + puts "Enabled:" + if enabled_ecos.any? + enabled_ecos.each { |eco| puts " #{Color.green(eco)}" } + else + puts " (none)" + end + + puts + puts "Disabled:" + if disabled_ecos.any? + disabled_ecos.each do |eco| + suffix = eco[:remote] ? " (remote)" : "" + puts " #{eco[:name]}#{suffix}" + end + else + puts " (none)" + end + + puts + if filtering + puts "Filtering: only #{configured.join(', ')}" + else + puts "All local ecosystems enabled" + end + puts "Remote ecosystems require explicit opt-in" + puts "Configure with: git config --add pkgs.ecosystems " + end + def format_size(bytes) units = %w[B KB MB GB] unit_index = 0 @@ -94,7 +154,11 @@ def parse_options options = {} parser = OptionParser.new do |opts| - opts.banner = "Usage: git pkgs info" + opts.banner = "Usage: git pkgs info [options]" + + opts.on("--ecosystems", "Show available ecosystems and filter status") do + options[:ecosystems] = true + end opts.on("-h", "--help", "Show this help") do puts opts diff --git a/lib/git/pkgs/config.rb b/lib/git/pkgs/config.rb new file mode 100644 index 0000000..785f9e7 --- /dev/null +++ b/lib/git/pkgs/config.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "bibliothecary" + +module Git + module Pkgs + module Config + # Ecosystems that require remote parsing services - disabled by default + REMOTE_ECOSYSTEMS = %w[carthage clojars hackage hex swiftpm].freeze + + # File patterns ignored by default (SBOM formats not supported) + DEFAULT_IGNORED_FILES = %w[ + cyclonedx.xml + cyclonedx.json + *.cdx.xml + *.cdx.json + *.spdx + *.spdx.json + ].freeze + + def self.ignored_dirs + @ignored_dirs ||= read_config_list("pkgs.ignoredDirs") + end + + def self.ignored_files + @ignored_files ||= read_config_list("pkgs.ignoredFiles") + end + + def self.ecosystems + @ecosystems ||= read_config_list("pkgs.ecosystems") + end + + def self.configure_bibliothecary + dirs = ignored_dirs + files = DEFAULT_IGNORED_FILES + ignored_files + + Bibliothecary.configure do |config| + config.ignored_dirs += dirs unless dirs.empty? + config.ignored_files += files + end + end + + def self.filter_ecosystem?(platform) + platform_lower = platform.to_s.downcase + + # Remote ecosystems are disabled unless explicitly enabled + if REMOTE_ECOSYSTEMS.include?(platform_lower) + return !ecosystems.map(&:downcase).include?(platform_lower) + end + + # If no filter configured, allow all non-remote ecosystems + return false if ecosystems.empty? + + # Otherwise, only allow explicitly listed ecosystems + !ecosystems.map(&:downcase).include?(platform_lower) + end + + def self.remote_ecosystem?(platform) + REMOTE_ECOSYSTEMS.include?(platform.to_s.downcase) + end + + def self.reset! + @ignored_dirs = nil + @ignored_files = nil + @ecosystems = nil + end + + def self.read_config_list(key) + `git config --get-all #{key} 2>/dev/null`.split("\n").map(&:strip).reject(&:empty?) + end + end + end +end diff --git a/test/git/pkgs/test_config.rb b/test/git/pkgs/test_config.rb new file mode 100644 index 0000000..5197dcb --- /dev/null +++ b/test/git/pkgs/test_config.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +require "test_helper" + +class Git::Pkgs::TestConfig < Minitest::Test + include TestHelpers + + def setup + create_test_repo + Git::Pkgs::Config.reset! + end + + def teardown + cleanup_test_repo + Git::Pkgs::Config.reset! + end + + def test_ignored_dirs_returns_empty_array_when_not_configured + Dir.chdir(@test_dir) do + assert_equal [], Git::Pkgs::Config.ignored_dirs + end + end + + def test_ignored_dirs_returns_configured_values + Dir.chdir(@test_dir) do + system("git config --add pkgs.ignoredDirs third_party", out: File::NULL) + system("git config --add pkgs.ignoredDirs external", out: File::NULL) + Git::Pkgs::Config.reset! + + assert_equal ["third_party", "external"], Git::Pkgs::Config.ignored_dirs + end + end + + def test_ignored_files_returns_empty_array_when_not_configured + Dir.chdir(@test_dir) do + assert_equal [], Git::Pkgs::Config.ignored_files + end + end + + def test_ignored_files_returns_configured_values + Dir.chdir(@test_dir) do + system("git config --add pkgs.ignoredFiles test/fixtures/package.json", out: File::NULL) + Git::Pkgs::Config.reset! + + assert_equal ["test/fixtures/package.json"], Git::Pkgs::Config.ignored_files + end + end + + def test_ecosystems_returns_empty_array_when_not_configured + Dir.chdir(@test_dir) do + assert_equal [], Git::Pkgs::Config.ecosystems + end + end + + def test_ecosystems_returns_configured_values + Dir.chdir(@test_dir) do + system("git config --add pkgs.ecosystems rubygems", out: File::NULL) + system("git config --add pkgs.ecosystems npm", out: File::NULL) + Git::Pkgs::Config.reset! + + assert_equal ["rubygems", "npm"], Git::Pkgs::Config.ecosystems + end + end + + def test_filter_ecosystem_returns_false_for_local_when_no_ecosystems_configured + Dir.chdir(@test_dir) do + refute Git::Pkgs::Config.filter_ecosystem?("rubygems") + refute Git::Pkgs::Config.filter_ecosystem?("npm") + end + end + + def test_filter_ecosystem_returns_true_for_remote_when_no_ecosystems_configured + Dir.chdir(@test_dir) do + assert Git::Pkgs::Config.filter_ecosystem?("carthage") + assert Git::Pkgs::Config.filter_ecosystem?("clojars") + assert Git::Pkgs::Config.filter_ecosystem?("hackage") + assert Git::Pkgs::Config.filter_ecosystem?("hex") + assert Git::Pkgs::Config.filter_ecosystem?("swiftpm") + end + end + + def test_filter_ecosystem_allows_remote_when_explicitly_enabled + Dir.chdir(@test_dir) do + system("git config --add pkgs.ecosystems carthage", out: File::NULL) + Git::Pkgs::Config.reset! + + refute Git::Pkgs::Config.filter_ecosystem?("carthage") + end + end + + def test_filter_ecosystem_returns_false_for_included_ecosystem + Dir.chdir(@test_dir) do + system("git config --add pkgs.ecosystems rubygems", out: File::NULL) + system("git config --add pkgs.ecosystems npm", out: File::NULL) + Git::Pkgs::Config.reset! + + refute Git::Pkgs::Config.filter_ecosystem?("rubygems") + refute Git::Pkgs::Config.filter_ecosystem?("npm") + end + end + + def test_filter_ecosystem_returns_true_for_excluded_ecosystem + Dir.chdir(@test_dir) do + system("git config --add pkgs.ecosystems rubygems", out: File::NULL) + Git::Pkgs::Config.reset! + + assert Git::Pkgs::Config.filter_ecosystem?("npm") + assert Git::Pkgs::Config.filter_ecosystem?("pypi") + end + end + + def test_filter_ecosystem_is_case_insensitive + Dir.chdir(@test_dir) do + system("git config --add pkgs.ecosystems RubyGems", out: File::NULL) + Git::Pkgs::Config.reset! + + refute Git::Pkgs::Config.filter_ecosystem?("rubygems") + refute Git::Pkgs::Config.filter_ecosystem?("RUBYGEMS") + end + end + + def test_remote_ecosystem_returns_true_for_remote_ecosystems + assert Git::Pkgs::Config.remote_ecosystem?("carthage") + assert Git::Pkgs::Config.remote_ecosystem?("clojars") + assert Git::Pkgs::Config.remote_ecosystem?("hackage") + assert Git::Pkgs::Config.remote_ecosystem?("hex") + assert Git::Pkgs::Config.remote_ecosystem?("swiftpm") + end + + def test_remote_ecosystem_returns_false_for_local_ecosystems + refute Git::Pkgs::Config.remote_ecosystem?("rubygems") + refute Git::Pkgs::Config.remote_ecosystem?("npm") + refute Git::Pkgs::Config.remote_ecosystem?("pypi") + end + + def test_configure_bibliothecary_adds_ignored_dirs + Dir.chdir(@test_dir) do + system("git config --add pkgs.ignoredDirs my_vendor", out: File::NULL) + Git::Pkgs::Config.reset! + + original_dirs = Bibliothecary.configuration.ignored_dirs.dup + Git::Pkgs::Config.configure_bibliothecary + + assert_includes Bibliothecary.configuration.ignored_dirs, "my_vendor" + + # Clean up + Bibliothecary.configuration.ignored_dirs = original_dirs + end + end + + def test_configure_bibliothecary_adds_ignored_files + Dir.chdir(@test_dir) do + system("git config --add pkgs.ignoredFiles fixtures/Gemfile", out: File::NULL) + Git::Pkgs::Config.reset! + + original_files = Bibliothecary.configuration.ignored_files.dup + Git::Pkgs::Config.configure_bibliothecary + + assert_includes Bibliothecary.configuration.ignored_files, "fixtures/Gemfile" + + # Clean up + Bibliothecary.configuration.ignored_files = original_files + end + end +end