Skip to content

Commit e083c53

Browse files
aspalaclaude
andcommitted
feat: add --ignore-paths option with glob wildcard support
Adds ignore-paths input to the GitHub Action (YAML list format) and --ignore-paths CLI flag (comma-separated) to all commands. Patterns support * (single dir), ** (recursive), and ? (single char) wildcards. Also adds devenv/direnv setup for local development and tests for the ignore-paths filtering logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c1f5097 commit e083c53

10 files changed

Lines changed: 264 additions & 10 deletions

File tree

.envrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
3+
export DIRENV_WARN_TIMEOUT=20s
4+
5+
eval "$(devenv direnvrc)"
6+
7+
use devenv

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@
33
/doc/
44
erl_crash.dump
55
*.ez
6+
7+
# devenv
8+
.devenv
9+
.devenv.flake.nix
10+
.devenv.local.nix
11+
devenv.lock

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ inputs:
4242
description: "Path to .codeqa.yml config file"
4343
required: false
4444
default: ""
45+
ignore-paths:
46+
description: "YAML list of path patterns to ignore (supports wildcards, e.g. 'test/*')"
47+
required: false
48+
default: ""
4549
extra-args:
4650
description: "Additional CLI flags passed through to codeqa"
4751
required: false
@@ -78,6 +82,7 @@ runs:
7882
INPUT_TOP: ${{ inputs.top }}
7983
INPUT_FORMAT: ${{ inputs.format }}
8084
INPUT_CONFIG: ${{ inputs.config }}
85+
INPUT_IGNORE_PATHS: ${{ inputs.ignore-paths }}
8186
INPUT_EXTRA_ARGS: ${{ inputs.extra-args }}
8287
INPUT_BASE_REF: ${{ inputs.base-ref }}
8388
INPUT_VERSION: ${{ inputs.version }}

devenv.nix

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{pkgs, ...}: {
2+
packages = [
3+
pkgs.beam.packages.erlang_27.elixir_1_19
4+
];
5+
6+
languages.elixir = {
7+
enable = true;
8+
package = pkgs.beam.packages.erlang_27.elixir_1_19;
9+
};
10+
11+
enterShell = ''
12+
elixir --version
13+
mix deps.get
14+
'';
15+
}

devenv.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
inputs:
2+
nixpkgs:
3+
url: github:cachix/devenv-nixpkgs/rolling

lib/codeqa/cli.ex

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ defmodule CodeQA.CLI do
1717

1818
defp handle_analyze(args) do
1919
{opts, [path], _} = OptionParser.parse(args,
20-
strict: [output: :string, progress: :boolean, workers: :integer, cache: :boolean, cache_dir: :string, timeout: :integer, show_ncd: :boolean, ncd_top: :integer, ncd_paths: :string, show_files: :boolean, show_file_paths: :string, combinations: :boolean, telemetry: :boolean, experimental_stopwords: :boolean, stopwords_threshold: :float],
20+
strict: [output: :string, progress: :boolean, workers: :integer, cache: :boolean, cache_dir: :string, timeout: :integer, show_ncd: :boolean, ncd_top: :integer, ncd_paths: :string, show_files: :boolean, show_file_paths: :string, combinations: :boolean, telemetry: :boolean, experimental_stopwords: :boolean, stopwords_threshold: :float, ignore_paths: :string],
2121
aliases: [o: :output, w: :workers, t: :timeout]
2222
)
2323

@@ -28,7 +28,8 @@ defmodule CodeQA.CLI do
2828
exit({:shutdown, 1})
2929
end
3030

31-
files = CodeQA.Collector.collect_files(path)
31+
ignore_patterns = parse_ignore_paths(opts[:ignore_paths])
32+
files = CodeQA.Collector.collect_files(path, ignore_patterns: ignore_patterns)
3233
if map_size(files) == 0 do
3334
IO.puts(:stderr, "Warning: no source files found in '#{path}'")
3435
exit({:shutdown, 1})
@@ -88,7 +89,8 @@ defmodule CodeQA.CLI do
8889
ncd_top: :integer, ncd_paths: :string,
8990
combinations: :boolean, telemetry: :boolean,
9091
experimental_stopwords: :boolean, stopwords_threshold: :float,
91-
show_files: :boolean, show_file_paths: :string
92+
show_files: :boolean, show_file_paths: :string,
93+
ignore_paths: :string
9294
],
9395
aliases: [w: :workers, t: :timeout]
9496
)
@@ -106,6 +108,8 @@ defmodule CodeQA.CLI do
106108
exit({:shutdown, 1})
107109
end
108110

111+
ignore_patterns = parse_ignore_paths(opts[:ignore_paths])
112+
opts = Keyword.put(opts, :ignore_patterns, ignore_patterns)
109113
{base_result, head_result, changes} = run_comparison(path, base_ref, head_ref, changes_only, opts)
110114

111115
comparison =
@@ -127,7 +131,8 @@ defmodule CodeQA.CLI do
127131
timeout: :integer, show_ncd: :boolean,
128132
ncd_top: :integer, ncd_paths: :string,
129133
combinations: :boolean,
130-
show_files: :boolean, show_file_paths: :string
134+
show_files: :boolean, show_file_paths: :string,
135+
ignore_paths: :string
131136
],
132137
aliases: [n: :commits, o: :output_dir, w: :workers, t: :timeout]
133138
)
@@ -154,6 +159,7 @@ defmodule CodeQA.CLI do
154159
IO.puts(:stderr, "Found #{length(commits)} commits to analyze.")
155160

156161
analyze_opts = build_analyze_opts(opts)
162+
ignore_patterns = parse_ignore_paths(opts[:ignore_paths])
157163

158164
commits
159165
|> Enum.with_index(1)
@@ -164,6 +170,7 @@ defmodule CodeQA.CLI do
164170
current_opts = if opts[:progress], do: [{:on_progress, fn c, t, p, _tt -> progress_callback(c, t, p, start_time_progress) end} | analyze_opts], else: analyze_opts
165171

166172
files = CodeQA.Git.collect_files_at_ref(path, commit)
173+
files = CodeQA.Collector.reject_ignored_map(files, ignore_patterns)
167174

168175
if map_size(files) == 0 do
169176
IO.puts(:stderr, "Warning: no source files found at commit #{commit}")
@@ -410,7 +417,9 @@ defmodule CodeQA.CLI do
410417
end
411418

412419
defp run_comparison(path, base_ref, head_ref, changes_only, opts) do
420+
ignore_patterns = opts[:ignore_patterns] || []
413421
changes = CodeQA.Git.changed_files(path, base_ref, head_ref)
422+
changes = CodeQA.Collector.reject_ignored(changes, ignore_patterns, & &1.path)
414423

415424
file_paths = if changes_only do
416425
IO.puts(:stderr, "Comparing #{length(changes)} changed files...")
@@ -422,6 +431,8 @@ defmodule CodeQA.CLI do
422431

423432
base_files = CodeQA.Git.collect_files_at_ref(path, base_ref, file_paths)
424433
head_files = CodeQA.Git.collect_files_at_ref(path, head_ref, file_paths)
434+
base_files = CodeQA.Collector.reject_ignored_map(base_files, ignore_patterns)
435+
head_files = CodeQA.Collector.reject_ignored_map(head_files, ignore_patterns)
425436

426437
if map_size(base_files) == 0 and map_size(head_files) == 0 do
427438
IO.puts(:stderr, "Warning: no source files found at either ref")
@@ -538,7 +549,7 @@ defmodule CodeQA.CLI do
538549

539550
defp handle_stopwords(args) do
540551
{opts, [path], _} = OptionParser.parse(args,
541-
strict: [workers: :integer, stopwords_threshold: :float, progress: :boolean],
552+
strict: [workers: :integer, stopwords_threshold: :float, progress: :boolean, ignore_paths: :string],
542553
aliases: [w: :workers]
543554
)
544555

@@ -547,7 +558,8 @@ defmodule CodeQA.CLI do
547558
exit({:shutdown, 1})
548559
end
549560

550-
files = CodeQA.Collector.collect_files(path)
561+
ignore_patterns = parse_ignore_paths(opts[:ignore_paths])
562+
files = CodeQA.Collector.collect_files(path, ignore_patterns: ignore_patterns)
551563
if map_size(files) == 0 do
552564
IO.puts(:stderr, "Warning: no source files found in '#{path}'")
553565
exit({:shutdown, 1})
@@ -589,7 +601,8 @@ defmodule CodeQA.CLI do
589601
timeout: :integer, show_ncd: :boolean,
590602
ncd_top: :integer, ncd_paths: :string,
591603
combinations: :boolean, telemetry: :boolean,
592-
experimental_stopwords: :boolean, stopwords_threshold: :float
604+
experimental_stopwords: :boolean, stopwords_threshold: :float,
605+
ignore_paths: :string
593606
],
594607
aliases: [o: :output, w: :workers, t: :timeout]
595608
)
@@ -601,7 +614,8 @@ defmodule CodeQA.CLI do
601614
exit({:shutdown, 1})
602615
end
603616

604-
files = CodeQA.Collector.collect_files(path)
617+
ignore_patterns = parse_ignore_paths(opts[:ignore_paths])
618+
files = CodeQA.Collector.collect_files(path, ignore_patterns: ignore_patterns)
605619
if map_size(files) == 0 do
606620
IO.puts(:stderr, "Warning: no source files found in '#{path}'")
607621
exit({:shutdown, 1})
@@ -680,6 +694,13 @@ defmodule CodeQA.CLI do
680694
# credo:disable-for-next-line Credo.Check.Warning.OperationOnSameValues
681695
defp nan?(x), do: x != x
682696

697+
defp parse_ignore_paths(nil), do: []
698+
defp parse_ignore_paths(paths_string) do
699+
paths_string
700+
|> String.split(",", trim: true)
701+
|> Enum.map(&String.trim/1)
702+
end
703+
683704
defp print_usage do
684705
IO.puts("""
685706
Usage: codeqa <command> [options]
@@ -704,6 +725,7 @@ defmodule CodeQA.CLI do
704725
--ncd-paths PATHS Comma-separated list of paths to compute NCD for
705726
--show-files Include individual file metrics in the output
706727
--show-file-paths P Comma-separated list of paths to include in the output
728+
--ignore-paths PATHS Comma-separated list of path patterns to ignore (supports wildcards, e.g. "test/*,docs/*")
707729
708730
Options for compare:
709731
--base-ref REF Base git ref to compare from (required)
@@ -722,6 +744,7 @@ defmodule CodeQA.CLI do
722744
--ncd-paths PATHS Comma-separated list of paths to compute NCD for
723745
--show-files Include individual file metrics in the output
724746
--show-file-paths P Comma-separated list of paths to include in the output
747+
--ignore-paths PATHS Comma-separated list of path patterns to ignore (supports wildcards, e.g. "test/*,docs/*")
725748
726749
Options for history:
727750
-n, --commits N Number of recent commits to analyze
@@ -737,6 +760,7 @@ defmodule CodeQA.CLI do
737760
--ncd-paths PATHS Comma-separated list of paths to compute NCD for
738761
--show-files Include individual file metrics in the output
739762
--show-file-paths P Comma-separated list of paths to include in the output
763+
--ignore-paths PATHS Comma-separated list of path patterns to ignore (supports wildcards, e.g. "test/*,docs/*")
740764
741765
Options for correlate:
742766
-t, --top N Number of top correlations to show (default: 20)
@@ -757,6 +781,7 @@ defmodule CodeQA.CLI do
757781
--cache Enable caching file metrics
758782
--cache-dir DIR Directory to store cache (default: .codeqa_cache)
759783
-t, --timeout MS Timeout for similarity analysis (default: 5000)
784+
--ignore-paths PATHS Comma-separated list of path patterns to ignore (supports wildcards, e.g. "test/*,docs/*")
760785
""")
761786
end
762787
end

lib/codeqa/collector.ex

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ defmodule CodeQA.Collector do
1313
.next coverage
1414
])
1515

16-
@spec collect_files(String.t()) :: %{String.t() => String.t()}
17-
def collect_files(root) do
16+
@spec collect_files(String.t(), keyword()) :: %{String.t() => String.t()}
17+
def collect_files(root, opts \\ []) do
1818
root_path = Path.expand(root)
19+
ignore_patterns = Keyword.get(opts, :ignore_patterns, [])
1920

2021
unless File.dir?(root_path) do
2122
raise File.Error, reason: :enoent, path: root, action: "find directory"
@@ -27,10 +28,49 @@ defmodule CodeQA.Collector do
2728
rel = Path.relative_to(path, root_path)
2829
{rel, File.read!(path)}
2930
end)
31+
|> reject_ignored_map(ignore_patterns)
3032
end
3133

3234
def source_extensions, do: @source_extensions
3335

36+
@doc false
37+
def ignored?(path, patterns) do
38+
Enum.any?(patterns, fn pattern ->
39+
match_pattern?(path, pattern)
40+
end)
41+
end
42+
43+
@doc false
44+
def reject_ignored_map(files_map, []), do: files_map
45+
def reject_ignored_map(files_map, patterns) do
46+
Map.reject(files_map, fn {path, _} -> ignored?(path, patterns) end)
47+
end
48+
49+
@doc false
50+
def reject_ignored(list, [], _key_fn), do: list
51+
def reject_ignored(list, patterns, key_fn) do
52+
Enum.reject(list, fn item -> ignored?(key_fn.(item), patterns) end)
53+
end
54+
55+
defp match_pattern?(path, pattern) do
56+
# Convert glob pattern to regex:
57+
# - ** matches any number of directories
58+
# - * matches anything except /
59+
# - ? matches a single character except /
60+
regex_str =
61+
pattern
62+
|> String.replace(".", "\\.")
63+
|> String.replace("**", "\0GLOBSTAR\0")
64+
|> String.replace("*", "[^/]*")
65+
|> String.replace("?", "[^/]")
66+
|> String.replace("\0GLOBSTAR\0", ".*")
67+
68+
case Regex.compile("^#{regex_str}$") do
69+
{:ok, regex} -> Regex.match?(regex, path)
70+
_ -> false
71+
end
72+
end
73+
3474
defp walk_directory(dir) do
3575
dir
3676
|> File.ls!()

scripts/run.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@ case "$INPUT_COMMAND" in
7070
;;
7171
esac
7272

73+
# Parse ignore-paths YAML list into --ignore-paths flag
74+
if [[ -n "$INPUT_IGNORE_PATHS" ]]; then
75+
IGNORE_CSV=""
76+
while IFS= read -r line; do
77+
# Strip YAML list prefix "- " and surrounding whitespace
78+
pattern=$(echo "$line" | sed 's/^[[:space:]]*-[[:space:]]*//' | sed 's/[[:space:]]*$//')
79+
if [[ -n "$pattern" ]]; then
80+
if [[ -n "$IGNORE_CSV" ]]; then
81+
IGNORE_CSV="${IGNORE_CSV},${pattern}"
82+
else
83+
IGNORE_CSV="$pattern"
84+
fi
85+
fi
86+
done <<< "$INPUT_IGNORE_PATHS"
87+
if [[ -n "$IGNORE_CSV" ]]; then
88+
ARGS+=("--ignore-paths" "$IGNORE_CSV")
89+
fi
90+
fi
91+
7392
# Append extra args (word-split intentionally)
7493
if [[ -n "$INPUT_EXTRA_ARGS" ]]; then
7594
# shellcheck disable=SC2206

0 commit comments

Comments
 (0)