Compare commits

...

22 Commits

Author SHA1 Message Date
Srikanth Patchava
4cfd0b81b3
test: add pytest tests for template validator
Comprehensive test suite covering glob validation, trailing
whitespace detection, duplicate and conflict detection,
coverage analysis, and report formatting.

Signed-off-by: Srikanth Patchava <spatchava@meta.com>
2026-05-16 09:30:28 -07:00
Srikanth Patchava
afdec22fa1
fix: add script to strip trailing whitespace from templates
Trailing whitespace in .gitignore patterns can cause unexpected
behavior. This script detects and removes trailing whitespace
from template files with dry-run support.

Signed-off-by: Srikanth Patchava <spatchava@meta.com>
2026-05-16 09:30:28 -07:00
Srikanth Patchava
46742213f6
feat: add .gitignore template validator script
Add a comprehensive template validation tool that checks for:
- Valid glob pattern syntax
- Duplicate pattern detection
- Conflicting include/exclude rules
- Coverage analysis with suggestions
- CLI interface with argparse
- Detailed reporting (errors, warnings, suggestions)
- Support for processing all templates in the repo

Signed-off-by: Srikanth Patchava <spatchava@meta.com>
2026-05-16 09:30:28 -07:00
Srikanth Patchava
7d476866ad
feat: add Zephyr RTOS .gitignore template
Add gitignore template for Zephyr RTOS projects covering:
- Build artifacts and twister output
- Generated config/devicetree files
- West workspace metadata
- Python virtual environments
2026-05-16 09:30:28 -07:00
Devin Dooley
ca6c873762
Merge pull request #4728 from TimHatchard/lasal-gitignore
Add SIGMATEK Automation's LASAL gitignore
2026-05-15 16:15:08 -07:00
Devin Dooley
7338c62e90
Merge pull request #4827 from arllemfarias/patch-1
Update MATLAB.gitignore
2026-05-15 16:07:29 -07:00
Devin Dooley
3407009677
Merge pull request #4860 from bac0id/bac0id/remove-empty-lines-cpp
Remove empty line in C++.gitignore
2026-05-15 16:06:04 -07:00
Devin Dooley
8224d3117f
Merge pull request #4719 from robbiemu/ai-agents
adds common AI agents and assistants configuration files and folder
2026-05-15 15:57:48 -07:00
BAC
e5414fa4fe
Remove empty line in .gitignore 2026-05-10 17:09:15 +08:00
robbiemu
cff16449a5 added codex opt-in 2026-05-08 13:20:32 -04:00
robbiemu
8f2f1edc2c Move AI agents template to Global and scope default ignores
Keep only documented local/high-churn agent artifacts active by default, preserving common shared instruction/config files as commented opt-in examples
2026-05-08 13:16:54 -04:00
Devin Dooley
1046d8fba6
Merge pull request #4846 from yufeiminds/main
Add .gitignore for MoonBit
2026-04-27 09:16:09 -07:00
Yufei Li
b632b12ac9 fix: Fix comment style issues 2026-04-27 15:21:45 +08:00
Yufei LI
47eaa84be9 fix: Remove bin 2026-04-20 01:14:53 +08:00
Yufei LI
4a96a23dfd fix: Remove deprecated folder and logs 2026-04-20 01:01:50 +08:00
Yufei LI
071d8034a4 Add .gitignore for MoonBit 2026-04-20 00:31:16 +08:00
Arllem Farias
411ca2450b
Update MATLAB.gitignore
Ignore auto-generated backup files
2026-03-28 11:51:24 -04:00
Tim Hatchard
6502030068
Update Lasal.gitignore with additional entries
Added maeexp and opcua entries
2026-03-27 16:00:32 +00:00
Tim Hatchard
d36057a9f8
Added lob and lba exceptions for SigCLib
SigCLib requires the lob and lba files to load a project.
2026-03-20 08:53:26 +00:00
Robert Collins
844333a10b
added .claude/ (by popular demand) and .gemini/ 2025-10-31 10:43:32 -05:00
Theophylakto
b7562cc78e
Add LASAL gitignore 2025-09-08 10:06:47 +01:00
Robert Collins
b5bf7e40ff
adds common AI agents and assistants configuration files and folder 2025-08-25 10:36:18 -04:00
9 changed files with 896 additions and 1 deletions

View File

@ -23,7 +23,6 @@
*.dll
*.so.*
# Fortran module files
*.mod
*.smod

68
Global/Agents.gitignore Normal file
View File

@ -0,0 +1,68 @@
# AI agents and assistants
#
# Some common agent instruction and project configuration files are listed
# below as commented-out examples. They are often intentionally committed and
# shared with a team, so only uncomment them if they are local-only in your
# project.
# GEMINI.md
# WARP.md
# CRUSH.md
# QWEN.md
# OpenAI Codex
# AGENTS.md
# .codex/
# Aider
.aider.input.history
.aider.chat.history.md
.aider.llm.history
.aider.tags.cache.v*
# .aiderignore
# Claude Code
.claude/*.local.json
.claude/**/*.log
CLAUDE.local.md
# .claude/
# Gemini CLI
gemini-debug.log
.gemini-clipboard/
# .gemini/
# Cursor AI
# .cursorrules
# .cursor/
# .cursor.json
# .cursor-settings.yaml
# Continue
# .continue/
# .continuerc.json
# Cline
# .cline/
# .clinerules
# cline.json
# Other agent/editor project config
# .warp/
# .crush/
# .codeium/
# .deepseek/
# .amazon-codewhisperer/
# .tabnineignore
# .tabnine/
# GitHub Copilot
# .github/copilot-instructions.md
# Windsurf Editor
# .windsurfrules
# .windsurf/
# Replit AI Development
# .replit
# replit.nix

View File

@ -4,6 +4,7 @@
*.autosave
*.slx.r*
*.mdl.r*
*.bak
# Derived content-obscured files
*.p

47
Lasal.gitignore Normal file
View File

@ -0,0 +1,47 @@
## LASAL ###
# website: https://www.sigmatek-automation.com/en/products/engineering-tool-lasal/
# lasal screen/visu runtime folders generated during build
**/Runtime
**/BAK
**/Language
**/TempPreview
# lasal class runtime folders generated during build
ProjectInternal
Internal
**/Network/*/*.st
**/Drive/*.st
# bootdisk files
**/Bootdisk
# various filetypes generated at runtime or used for internal backups
*.ldi
*.bak
*.lob
*.lba
*.lcb
*.lock
*.plc
*.7z
*.zip
*.lcc
*.lds
# tag files generated by the compiler
*.mme
**/MaeExp.txt
**/MaeExp.xml
# SigCLib requires lba and lob files
!cObjLib.lob
!cObjLib.lba
# opc related classes added automatically from lasal instance
!**/Class/OPC_UA/**
!**/Class/OPC_UA_Client/**
!**/Class/OPC_UA_Server/**
!**/Class/OPC_UA_ModuleBase/**
!**/Class/OPC_UA_AddressSpace/**
!**/Class/OPC_UA_OpenSSL_v1_1_1/**

53
MoonBit.gitignore Normal file
View File

@ -0,0 +1,53 @@
# Build System Artifacts
# MoonBit default build output directory.
_build/
# Local MoonBit package cache directory.
.mooncakes/
# Coverage and Test Artifacts
# Coverage reports generated by MoonBit tooling.
moonbit-coverage-*.txt
# Coverage output directory used by coverage commands.
_coverage/
# Bisect runtime coverage data file.
bisect.coverage
# JavaScript Target Artifacts
# Uncomment if generated JavaScript should be ignored.
# *.js
# Source map files generated for JavaScript output.
*.js.map
# Node.js dependencies used in JS target workflows.
node_modules/
# WebAssembly Target Artifacts
# Compiled WebAssembly binaries.
*.wasm
# WebAssembly text format output.
*.wat
# MoonBit Pilot / Agent Artifacts
# MoonBit agent runtime state and temporary files.
.moonagent/
# LLVM Backend Artifacts
# LLVM textual intermediate representation (IR) files.
*.ll
# LLVM bitcode files.
*.bc
# Native object files from LLVM/native compilation.
*.o
# Generated assembly source files.
*.s
# Default executable output name on Unix-like systems.
*.out
# Native executable output on Windows.
*.exe
# macOS debug symbol bundles.
*.dSYM/

View File

@ -0,0 +1,27 @@
# Zephyr RTOS build artifacts
/build/
/twister-out*/
# Generated configuration files
*.config
*.config.old
/zephyr/.config
# Device tree generated files
*.dts.pre.tmp
*.dts_compiled
# Kconfig generated
autoconf.h
# West workspace
/.west/
# Python virtual environment
/.venv/
/venv/
# IDE
.vscode/launch.json
.vscode/tasks.json
*.jlink

View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Strip trailing whitespace from .gitignore template files.
This script fixes trailing whitespace issues in gitignore templates,
which can cause unexpected behavior in pattern matching.
"""
import argparse
import os
import sys
def strip_file(filepath: str, dry_run: bool = False) -> int:
"""Strip trailing whitespace from a file. Returns number of lines fixed."""
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
fixed_count = 0
new_lines = []
for line in lines:
stripped = line.rstrip() + "\n"
if stripped != line:
fixed_count += 1
new_lines.append(stripped)
# Ensure file ends with a single newline
if new_lines and new_lines[-1] == "\n":
pass # already ends properly
elif not new_lines:
new_lines = ["\n"]
if fixed_count > 0 and not dry_run:
with open(filepath, "w", encoding="utf-8", newline="") as f:
f.writelines(new_lines)
return fixed_count
def main():
parser = argparse.ArgumentParser(
description="Strip trailing whitespace from .gitignore templates."
)
parser.add_argument("files", nargs="*", help="Files to process.")
parser.add_argument("--repo-root", default=".", help="Repo root directory.")
parser.add_argument(
"--dry-run", action="store_true", help="Report but do not modify files."
)
args = parser.parse_args()
if args.files:
targets = args.files
else:
targets = []
for entry in os.listdir(args.repo_root):
full = os.path.join(args.repo_root, entry)
if os.path.isfile(full) and entry.endswith(".gitignore"):
targets.append(full)
total_fixed = 0
for filepath in targets:
count = strip_file(filepath, dry_run=args.dry_run)
if count > 0:
action = "would fix" if args.dry_run else "fixed"
print(f" {action} {count} line(s) in {filepath}")
total_fixed += count
if total_fixed == 0:
print("No trailing whitespace found.")
else:
action = "Would fix" if args.dry_run else "Fixed"
print(f"\n{action} {total_fixed} line(s) total.")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Tests for the gitignore template validator."""
import os
import tempfile
import pytest
from validate_templates import (
Issue,
Severity,
ValidationReport,
analyze_coverage,
check_trailing_whitespace,
discover_templates,
find_conflicts,
find_duplicates,
format_report,
is_valid_glob_pattern,
validate_template,
)
class TestIsValidGlobPattern:
def test_simple_extension(self):
assert is_valid_glob_pattern("*.py") is True
def test_directory_pattern(self):
assert is_valid_glob_pattern("build/") is True
def test_negation(self):
assert is_valid_glob_pattern("!important.txt") is True
def test_double_star(self):
assert is_valid_glob_pattern("**/logs") is True
def test_bracket_pattern(self):
assert is_valid_glob_pattern("*.[oa]") is True
def test_unmatched_bracket(self):
assert is_valid_glob_pattern("*.[o") is False
def test_empty_after_strip(self):
assert is_valid_glob_pattern("!") is False
def test_rooted_pattern(self):
assert is_valid_glob_pattern("/build") is True
class TestCheckTrailingWhitespace:
def test_no_trailing(self):
assert check_trailing_whitespace("*.py", 1) is None
def test_with_trailing_space(self):
issue = check_trailing_whitespace("*.py ", 5)
assert issue is not None
assert issue.severity == Severity.WARNING
assert issue.line_number == 5
def test_blank_line_ignored(self):
assert check_trailing_whitespace(" ", 1) is None
class TestFindDuplicates:
def test_no_duplicates(self):
patterns = [(1, "*.py"), (2, "*.js"), (3, "build/")]
assert find_duplicates(patterns) == []
def test_exact_duplicate(self):
patterns = [(1, "*.py"), (2, "*.js"), (3, "*.py")]
issues = find_duplicates(patterns)
assert len(issues) == 1
assert issues[0].line_number == 3
def test_trailing_slash_normalization(self):
patterns = [(1, "build"), (2, "build/")]
issues = find_duplicates(patterns)
assert len(issues) == 1
class TestFindConflicts:
def test_no_conflicts(self):
patterns = [(1, "*.log"), (2, "*.tmp")]
assert find_conflicts(patterns) == []
def test_include_then_negate(self):
patterns = [(1, "*.log"), (2, "!*.log")]
issues = find_conflicts(patterns)
assert len(issues) == 1
assert issues[0].severity == Severity.WARNING
def test_negate_without_include(self):
patterns = [(1, "!important.txt")]
issues = find_conflicts(patterns)
assert len(issues) == 0
class TestAnalyzeCoverage:
def test_many_wildcards(self):
patterns = [(i, f"*.ext{i}") for i in range(12)]
issues = analyze_coverage(patterns)
msgs = [i.message for i in issues]
assert any("wildcard" in m for m in msgs)
def test_no_directory_patterns(self):
patterns = [(i, f"*.ext{i}") for i in range(6)]
issues = analyze_coverage(patterns)
msgs = [i.message for i in issues]
assert any("directory" in m.lower() for m in msgs)
def test_duplicate_extension_suggestion(self):
patterns = [(1, "*.py"), (2, "*.py")]
issues = analyze_coverage(patterns)
msgs = [i.message for i in issues]
assert any(".py" in m for m in msgs)
class TestValidateTemplate:
def test_valid_template(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".gitignore", delete=False
) as f:
f.write("# Build output\nbuild/\n*.o\n*.a\n")
f.flush()
report = validate_template(f.name)
os.unlink(f.name)
assert report.error_count == 0
assert report.pattern_count == 3
assert report.comment_count == 1
def test_invalid_pattern(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".gitignore", delete=False
) as f:
f.write("*.[invalid\n")
f.flush()
report = validate_template(f.name)
os.unlink(f.name)
assert report.error_count > 0
def test_file_not_found(self):
report = validate_template("/nonexistent/file.gitignore")
assert report.error_count > 0
class TestFormatReport:
def test_clean_report(self):
report = ValidationReport(
template_file="test.gitignore",
total_lines=5,
pattern_count=3,
comment_count=1,
blank_count=1,
)
output = format_report(report)
assert "No issues found" in output
def test_report_with_issues(self):
report = ValidationReport(
template_file="test.gitignore",
total_lines=5,
pattern_count=3,
)
report.issues.append(
Issue(
severity=Severity.ERROR,
line_number=2,
line_content="*.[bad",
message="Invalid glob pattern",
)
)
output = format_report(report, verbose=True)
assert "ERROR" in output
assert "*.[bad" in output
class TestDiscoverTemplates:
def test_discover_in_directory(self):
with tempfile.TemporaryDirectory() as tmpdir:
# Create fake gitignore files
for name in ["Python.gitignore", "Java.gitignore", "README.md"]:
with open(os.path.join(tmpdir, name), "w") as f:
f.write("# test\n")
templates = discover_templates(tmpdir)
assert len(templates) == 2
assert all(t.endswith(".gitignore") for t in templates)
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@ -0,0 +1,433 @@
#!/usr/bin/env python3
"""
Gitignore Template Validator
Validates .gitignore template files for syntax correctness, duplicate patterns,
conflicting rules, and provides coverage analysis with detailed reporting.
"""
import argparse
import fnmatch
import os
import re
import sys
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import List, Optional
class Severity(Enum):
ERROR = "ERROR"
WARNING = "WARNING"
SUGGESTION = "SUGGESTION"
@dataclass
class Issue:
severity: Severity
line_number: int
line_content: str
message: str
template_file: str = ""
@dataclass
class ValidationReport:
template_file: str
total_lines: int = 0
pattern_count: int = 0
comment_count: int = 0
blank_count: int = 0
issues: List[Issue] = field(default_factory=list)
@property
def error_count(self) -> int:
return sum(1 for i in self.issues if i.severity == Severity.ERROR)
@property
def warning_count(self) -> int:
return sum(1 for i in self.issues if i.severity == Severity.WARNING)
@property
def suggestion_count(self) -> int:
return sum(1 for i in self.issues if i.severity == Severity.SUGGESTION)
@property
def has_errors(self) -> bool:
return self.error_count > 0
def is_valid_glob_pattern(pattern: str) -> bool:
"""Check if a pattern is a valid glob/gitignore pattern."""
clean = pattern.lstrip("!")
if clean.startswith("/"):
clean = clean[1:]
if clean.endswith("/"):
clean = clean[:-1]
if not clean:
return False
try:
# Check for unmatched brackets
bracket_depth = 0
i = 0
while i < len(clean):
ch = clean[i]
if ch == "\\" and i + 1 < len(clean):
i += 2
continue
if ch == "[":
bracket_depth += 1
elif ch == "]":
if bracket_depth > 0:
bracket_depth -= 1
else:
return False
i += 1
if bracket_depth != 0:
return False
# Test compile the pattern via fnmatch
fnmatch.translate(clean)
return True
except Exception:
return False
def check_trailing_whitespace(line: str, line_number: int) -> Optional[Issue]:
"""Detect trailing whitespace on a line."""
if line != line.rstrip() and line.strip():
return Issue(
severity=Severity.WARNING,
line_number=line_number,
line_content=line,
message="Line has trailing whitespace",
)
return None
def find_duplicates(patterns: List[tuple]) -> List[Issue]:
"""Find duplicate patterns in the list."""
issues = []
seen = {}
for line_num, pattern in patterns:
normalized = pattern.strip().rstrip("/")
if normalized in seen:
issues.append(
Issue(
severity=Severity.WARNING,
line_number=line_num,
line_content=pattern,
message=f"Duplicate pattern (first seen at line {seen[normalized]})",
)
)
else:
seen[normalized] = line_num
return issues
def find_conflicts(patterns: List[tuple]) -> List[Issue]:
"""Find conflicting include/exclude rules for the same pattern."""
issues = []
include_patterns = {}
exclude_patterns = {}
for line_num, pattern in patterns:
stripped = pattern.strip()
if stripped.startswith("!"):
base = stripped[1:].strip().rstrip("/")
exclude_patterns[base] = line_num
if base in include_patterns:
issues.append(
Issue(
severity=Severity.WARNING,
line_number=line_num,
line_content=stripped,
message=(
f"Negation pattern conflicts with include at line "
f"{include_patterns[base]}. Order matters in gitignore."
),
)
)
else:
base = stripped.rstrip("/")
include_patterns[base] = line_num
return issues
def analyze_coverage(patterns: List[tuple]) -> List[Issue]:
"""Analyze pattern coverage and suggest improvements."""
issues = []
wildcard_count = 0
extension_patterns = []
directory_patterns = []
for line_num, pattern in patterns:
stripped = pattern.strip().lstrip("!")
if stripped.startswith("*"):
wildcard_count += 1
if re.match(r"^\*\.[a-zA-Z0-9]+$", stripped):
extension_patterns.append((line_num, stripped))
if stripped.endswith("/"):
directory_patterns.append((line_num, stripped))
if wildcard_count > 10:
issues.append(
Issue(
severity=Severity.SUGGESTION,
line_number=0,
line_content="",
message=(
f"Template has {wildcard_count} wildcard patterns. "
"Consider grouping related patterns with comments."
),
)
)
ext_groups = {}
for line_num, pat in extension_patterns:
ext = pat.split(".")[-1].lower()
ext_groups.setdefault(ext, []).append(line_num)
for ext, lines in ext_groups.items():
if len(lines) > 1:
issues.append(
Issue(
severity=Severity.SUGGESTION,
line_number=lines[1],
line_content="",
message=(
f"Multiple patterns target .{ext} files "
f"(lines {', '.join(map(str, lines))}). Consider consolidating."
),
)
)
if not directory_patterns and len(patterns) > 5:
issues.append(
Issue(
severity=Severity.SUGGESTION,
line_number=0,
line_content="",
message=(
"No directory-specific patterns found. "
"Consider adding directory ignores (e.g., build/)."
),
)
)
return issues
def validate_template(filepath: str) -> ValidationReport:
"""Validate a single gitignore template file."""
report = ValidationReport(template_file=filepath)
try:
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
except OSError as e:
report.issues.append(
Issue(
severity=Severity.ERROR,
line_number=0,
line_content="",
message=f"Cannot read file: {e}",
template_file=filepath,
)
)
return report
report.total_lines = len(lines)
patterns = []
for i, raw_line in enumerate(lines, start=1):
line = raw_line.rstrip("\n\r")
# Check trailing whitespace
ws_issue = check_trailing_whitespace(line, i)
if ws_issue:
ws_issue.template_file = filepath
report.issues.append(ws_issue)
stripped = line.strip()
# Blank line
if not stripped:
report.blank_count += 1
continue
# Comment
if stripped.startswith("#"):
report.comment_count += 1
continue
# Pattern line
report.pattern_count += 1
patterns.append((i, stripped))
# Validate glob syntax
if not is_valid_glob_pattern(stripped):
report.issues.append(
Issue(
severity=Severity.ERROR,
line_number=i,
line_content=stripped,
message="Invalid glob pattern syntax",
template_file=filepath,
)
)
# Check for spaces in patterns (usually a mistake)
if " " in stripped and not stripped.startswith("\\"):
report.issues.append(
Issue(
severity=Severity.WARNING,
line_number=i,
line_content=stripped,
message="Pattern contains spaces — this may be unintentional",
template_file=filepath,
)
)
# Run duplicate detection
dup_issues = find_duplicates(patterns)
for issue in dup_issues:
issue.template_file = filepath
report.issues.extend(dup_issues)
# Run conflict detection
conflict_issues = find_conflicts(patterns)
for issue in conflict_issues:
issue.template_file = filepath
report.issues.extend(conflict_issues)
# Run coverage analysis
coverage_issues = analyze_coverage(patterns)
for issue in coverage_issues:
issue.template_file = filepath
report.issues.extend(coverage_issues)
return report
def format_report(report: ValidationReport, verbose: bool = False) -> str:
"""Format a validation report as human-readable text."""
lines = []
lines.append(f"\n{'='*60}")
lines.append(f"File: {report.template_file}")
lines.append(f"{'='*60}")
lines.append(
f" Lines: {report.total_lines} | Patterns: {report.pattern_count} "
f"| Comments: {report.comment_count} | Blank: {report.blank_count}"
)
lines.append(
f" Errors: {report.error_count} | Warnings: {report.warning_count} "
f"| Suggestions: {report.suggestion_count}"
)
if report.issues:
lines.append("")
for issue in sorted(report.issues, key=lambda x: (x.severity.value, x.line_number)):
prefix = f" [{issue.severity.value}]"
loc = f" Line {issue.line_number}:" if issue.line_number > 0 else ""
lines.append(f"{prefix}{loc} {issue.message}")
if verbose and issue.line_content:
lines.append(f" > {issue.line_content}")
else:
lines.append(" No issues found. ✓")
return "\n".join(lines)
def discover_templates(repo_root: str) -> List[str]:
"""Discover all .gitignore template files in the repository."""
templates = []
for entry in os.listdir(repo_root):
full_path = os.path.join(repo_root, entry)
if os.path.isfile(full_path) and entry.endswith(".gitignore"):
templates.append(full_path)
# Also check Global/ and community/ subdirectories
for subdir in ["Global", "community"]:
subdir_path = os.path.join(repo_root, subdir)
if os.path.isdir(subdir_path):
for entry in os.listdir(subdir_path):
full_path = os.path.join(subdir_path, entry)
if os.path.isfile(full_path) and entry.endswith(".gitignore"):
templates.append(full_path)
return sorted(templates)
def main():
parser = argparse.ArgumentParser(
description="Validate .gitignore template files for common issues."
)
parser.add_argument(
"files",
nargs="*",
help="Template files to validate. If none given, validates all in repo.",
)
parser.add_argument(
"--repo-root",
default=".",
help="Root directory of the gitignore repo (default: current directory).",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Show pattern content alongside issues.",
)
parser.add_argument(
"--errors-only",
action="store_true",
help="Only report errors, not warnings or suggestions.",
)
parser.add_argument(
"--summary",
action="store_true",
help="Print only a summary, not per-file details.",
)
args = parser.parse_args()
if args.files:
templates = args.files
else:
templates = discover_templates(args.repo_root)
if not templates:
print("No .gitignore templates found.")
sys.exit(0)
reports = []
for tpl in templates:
report = validate_template(tpl)
reports.append(report)
total_errors = 0
total_warnings = 0
total_suggestions = 0
for report in reports:
if args.errors_only:
report.issues = [i for i in report.issues if i.severity == Severity.ERROR]
if not args.summary:
print(format_report(report, verbose=args.verbose))
total_errors += report.error_count
total_warnings += report.warning_count
total_suggestions += report.suggestion_count
print(f"\n{'='*60}")
print(f"SUMMARY: {len(reports)} templates validated")
print(f" Total errors: {total_errors}")
print(f" Total warnings: {total_warnings}")
print(f" Total suggestions: {total_suggestions}")
print(f"{'='*60}")
if total_errors > 0:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()