5 Commits
Author SHA1 Message Date
Srikanth Patchava fceedfce23 Merge 4cfd0b81b3 into 57286c3887 2026-07-24 01:20:57 -04:00
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
59 changed files with 881 additions and 179 deletions

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
### AL ###
#Template for AL projects for Dynamics 365 Business Central
#launch.json folder
.vscode/
#Cache folder
.alcache/
#Symbols folder
.alpackages/
#Snapshots folder
.snapshots/
#Testing Output folder
.output/
#Extension App-file
*.app
#Rapid Application Development File
rad.json
#Translation Base-file
*.g.xlf
#License-files
*.bclicense
*.flf
#Test results file
TestResults.xml
-1
View File
@@ -1,7 +1,6 @@
# Gradle files
.gradle/
build/
.kotlin/
# Local configuration file (sdk path, etc)
local.properties
+3
View File
@@ -77,3 +77,6 @@ __recovery/
# Castalia statistics file (since XE7 Castalia is distributed with Delphi)
*.stat
# Boss dependency manager vendor folder https://github.com/HashLoad/boss
modules/
+3 -7
View File
@@ -7,6 +7,8 @@
.buildlog/
.history
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
@@ -44,12 +46,6 @@ linked_*.ds
unlinked.ds
unlinked_spec.ds
# FVM Version Cache
.fvm/
# Widget Preview related
.widget_preview/
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
@@ -120,4 +116,4 @@ app.*.symbols
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!/dev/ci/**/Gemfile.lock
+1
View File
@@ -1 +1,2 @@
secring.*
+1 -1
View File
@@ -10,4 +10,4 @@ deploy/
javadoc/
# lock file, a part of Oracle Credential Store Framework
cwallet.sso.lck
cwallet.sso.lck
+2 -2
View File
@@ -1,2 +1,2 @@
# Notepad++ backups #
*.bak
# Notepad++ backups #
*.bak
+1 -1
View File
@@ -1,7 +1,7 @@
# STM32CubeIDE specific files
# Project-specific settings. Ignore it if developers have different preferences.
# However, if you want all team members to use the same code formatting and settings,
# However, if you want all team members to use the same code formatting and settings,
# consider including the .settings folder in the repository.
# /.settings/
-4
View File
@@ -1,6 +1,2 @@
# Syncthing caches
.stversions
# Temporary files created during synchronization
.syncthing.*.tmp
~syncthing~*.tmp
+1
View File
@@ -14,3 +14,4 @@ cscope.files
cscope.out
cscope.in.out
cscope.po.out
+1
View File
@@ -15,3 +15,4 @@ drcRunDir/*
# Abstract generation tool
abstract.log*
abstract.record*
-5
View File
@@ -7,10 +7,5 @@ mise.local.toml
.mise/*.local.toml
mise/*.local.toml
# https://mise.jdx.dev/dev-tools/mise-lock.html#local-lockfiles
# https://mise.jdx.dev/dev-tools/mise-lock.html#environment-specific-lockfiles
mise.local.lock
mise.*.local.lock
# https://mise.jdx.dev/configuration.html#tool-versions
#.tool-versions
-1
View File
@@ -14,7 +14,6 @@
# Code coverage profiles and other test artifacts
*.out
coverage.*
!coverage.go
*.coverprofile
profile.cov
-5
View File
@@ -18,8 +18,3 @@ export_credentials.cfg
.mono/
data_*/
mono_crash.*.json
# Export outputs
*.exe
# Uncomment if your project does not version PCK files as runtime dependencies.
# *.pck
+1 -1
View File
@@ -44,4 +44,4 @@ settings/
Backup*
# IAR .dep files
*.dep
*.dep
-4
View File
@@ -4,7 +4,6 @@
# Files generated by invoking Julia with --track-allocation
*.jl.mem
*.jl.*.mem
# System-specific files and directories generated by the BinaryProvider and BinDeps packages
# They contain absolute paths specific to the host computer, and so should not be committed
@@ -27,6 +26,3 @@ Manifest*.toml
# File generated by the Preferences package to store local preferences
LocalPreferences.toml
JuliaLocalPreferences.toml
# Conda environment and metadata created in the project directory by the CondaPkg package
.CondaPkg/
+1
View File
@@ -18,6 +18,7 @@ Libs/
.svn/
.gradle
# Log file
*.log
+1 -1
View File
@@ -24,4 +24,4 @@ hs_err_pid*
replay_pid*
# Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects
.kotlin/
.kotlin/
-2
View File
@@ -14,6 +14,4 @@
# Metadata
*.aliases
*.lvlps
*.lvprojstate
.cache/
*.UserState
+47 -47
View File
@@ -1,47 +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/**
## 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/**
+1
View File
@@ -38,3 +38,4 @@ luac.out
*.i*86
*.x86_64
*.hex
+1
View File
@@ -42,3 +42,4 @@
!/var/package
/var/package/*
!/var/package/*.xml
+1 -7
View File
@@ -3,12 +3,7 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.pnp.js
# testing
/coverage
@@ -28,7 +23,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
+1 -7
View File
@@ -122,7 +122,7 @@ dist
# TernJS port file
.tern-port
# Stores Visual Studio Code versions used for testing Visual Studio Code extensions
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# pnpm
@@ -141,9 +141,3 @@ dist
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
# Astro generated types
.astro/
# Turbo Repo
.turbo
+1 -1
View File
@@ -1,3 +1,4 @@
!Build/
.last_cover_stats
/META.yml
/META.json
@@ -18,7 +19,6 @@ nytprof.out
# Module::Build
_build/
Build
!Build/
Build.bat
# Module::Install
+2
View File
@@ -71,6 +71,7 @@
*.hot-update.js
*.hot-update.json
/admin-dev/autoupgrade/*
!/admin-dev/autoupgrade/index.php
!/admin-dev/autoupgrade/backup/index.php
@@ -169,3 +170,4 @@ themes/default-bootstrap/modules/*/mails/*
!var/SymfonyRequirements.php
/vendor/
/web/bundles/
+6 -9
View File
@@ -95,7 +95,7 @@ ipython_config.py
# install all needed dependencies.
# Pipfile.lock
# uv
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
@@ -159,9 +159,6 @@ ENV/
env.bak/
venv.bak/
# Conda environments
conda-meta/
# Spyder project settings
.spyderproject
.spyproject
@@ -169,7 +166,7 @@ conda-meta/
# Rope project settings
.ropeproject
# mkdocs/Zensical documentation
# mkdocs documentation
/site
# mypy
@@ -200,10 +197,10 @@ cython_debug/
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
# can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer, you
# could uncomment the following to ignore the entire vscode folder
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Temporary file for partial code execution
tempCodeRunnerFile.py
-1
View File
@@ -38,7 +38,6 @@ vignettes/*.pdf
# R Environment Variables
.Renviron
.env
# pkgdown site
docs/
+5 -5
View File
@@ -12,7 +12,7 @@ the following resources are a great place to start:
- The [gitignore(5)][man] manual page.
[man]: https://git-scm.com/docs/gitignore
[help]: https://docs.github.com/en/get-started/git-basics/ignoring-files
[help]: https://help.github.com/articles/ignoring-files
[chapter]: https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring
[progit]: https://git-scm.com/book
@@ -95,7 +95,7 @@ For example, this template might live at `community/DotNet/InforCRM.gitignore`:
```gitignore
# gitignore template for InforCRM (formerly SalesLogix)
# website: https://www.infor.com/products/customer-experience-suite/crm
# website: https://www.infor.com/product-summary/cx/infor-crm/
#
# Recommended: VisualStudio.gitignore
@@ -123,9 +123,9 @@ Here’s how we suggest you go about proposing a change to this project:
Using the web-based interface to make changes is fine too, and will help you
by automatically forking the project and prompting to send a pull request too.
[fork]: https://docs.github.com/en/get-started/quickstart/fork-a-repo
[branch]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository
[pr]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests
[fork]: https://help.github.com/articles/fork-a-repo/
[branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository
[pr]: https://help.github.com/articles/using-pull-requests/
## License
+1
View File
@@ -4,3 +4,4 @@
# precompiled files
.precomp
lib/.precomp
+1 -1
View File
@@ -1,4 +1,4 @@
# for projects that use SCons for building: https://www.scons.org/
# for projects that use SCons for building: http://http://www.scons.org/
.sconsign.dblite
# When configure fails, SCons outputs these
+1
View File
@@ -28,3 +28,4 @@ obj/
# Backup files
*.bak
*.log
+1 -1
View File
@@ -1,4 +1,4 @@
# This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
# This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
# If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
# For useful gitignore templates see: https://github.com/github/gitignore
+1 -1
View File
@@ -12,4 +12,4 @@ states/
# Optional
.env
.env.local
.env.local
+1 -1
View File
@@ -41,4 +41,4 @@ terraform.rc
# Optional: ignore plan files saved before destroying Terraform configuration
# Uncomment the line below if you want to ignore planout files.
# planout
# planout
+1
View File
@@ -45,6 +45,7 @@ LineIDs.dbg.bak
**/_Deployment/
**/_Repository/
# To include a specific library directory (i.e. third party/custom libs),
# use pattern `!/**/_Libraries/<directory name>/` i.e. `!/**/_Libraries/www.tcunit.org/`
#
+17 -48
View File
@@ -15,10 +15,13 @@
/[Uu]ser[Ss]ettings/
*.log
# Memory Profiler generated files
# MemoryCaptures can get excessive in size
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
*.blend1
*.blend1.meta
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emory[Cc]aptures/
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
@@ -65,16 +68,11 @@ ExportedObj/
sysinfo.txt
# Mono auto generated files
.dirstamp
mono_crash.*
tmpinst-dir.stamp
# Builds
/*_BackUpThisFolder_ButDontShipItWithYourGame/
/*_BurstDebugInformation_DoNotShip/
*.apk
*.aab
*.obb
*.unitypackage
*.unitypackage.meta
*.app
@@ -82,56 +80,27 @@ tmpinst-dir.stamp
# Crashlytics generated file
crashlytics-build.properties
# Blender generated backup files
/[Aa]ssets/**/*.blend1
/[Aa]ssets/**/*.blend1.meta
# TouchDesigner generated cache files
/[Aa]ssets/**/TDImportCache/
/[Aa]ssets/**/TDImportCache.meta
# Unity Crash Recovery generated files
/[Aa]ssets/_Recovery/
/[Aa]ssets/_Recovery.meta
# TestRunner generated files
InitTestScene*.unity*
# Addressables default ignores, before user customizations
/ServerData
/[Aa]ssets/StreamingAssets/aa*
/[Aa]ssets/AddressableAssetsData/link.xml*
/[Aa]ssets/Addressables_Temp*
# By default, Addressables content builds will generate addressables_content_state.bin
# files in platform-specific subfolders, for example:
# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin
/[Aa]ssets/AddressableAssetsData/*/addressables_content_state.bin
/[Aa]ssets/AddressableAssetsData/*/addressables_content_state.bin.meta
/[Aa]ssets/AddressableAssetsData/link.xml
/[Aa]ssets/AddressableAssetsData/link.xml.meta
/[Aa]ssets/Addressables_Temp/
/[Aa]ssets/Addressables_Temp.meta
/[Aa]ssets/StreamingAssets/aa/
/[Aa]ssets/StreamingAssets/aa.meta
/ServerData/
# EDM4U generated files
/[Aa]ssets/GeneratedLocalRepo/
/[Aa]ssets/GeneratedLocalRepo.meta
# Android Gradle generated files
/[Aa]ssets/Plugins/Android/**/*.gradle.backup*
/[Aa]ssets/Plugins/Android/**/local.properties
# Test Framework generated files
/[Aa]ssets/Resources/PerformanceTestRunInfo.json
/[Aa]ssets/Resources/PerformanceTestRunInfo.json.meta
/[Aa]ssets/Resources/PerformanceTestRunSettings.json
/[Aa]ssets/Resources/PerformanceTestRunSettings.json.meta
/[Aa]ssets/InitTestScene*.unity
/[Aa]ssets/InitTestScene*.unity.meta
/[Aa]ssets/AddressableAssetsData/*/*.bin*
# Visual Scripting auto-generated files
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers/
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
# UI Toolkit auto generated files
/UIElementsSchema/
# Auto-generated scenes by play mode tests
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
# Auto-generated cache in Assets folder
/[Aa]ssets/[Ss]ceneDependencyCache*
+1
View File
@@ -1,3 +1,4 @@
# Office temporary files
~$*
+1
View File
@@ -1,3 +1,4 @@
# .v4p backup files
*~.xml
+4 -3
View File
@@ -270,6 +270,10 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
@@ -423,6 +427,3 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp
# Official VS Code C# Dev Kit Extension exclusion
#*.lscache
+2 -1
View File
@@ -1,5 +1,6 @@
# Xojo (formerly REALbasic and Real Studio)
Builds - */
Builds*
*.debug
*.debug.app
Debug*.exe
-1
View File
@@ -1,4 +1,3 @@
.zig-cache/
zig-out/
zig-pkg/
*.o
+1 -1
View File
@@ -41,4 +41,4 @@ CASS.ini
# git add .
# git commit -m "fixed untracked files"
# author: Kacper Ksieski
# author: Kacper Ksieski
-3
View File
@@ -49,7 +49,4 @@
!**/umbraco/models
#include default location for packages
!**/umbraco/Data/
**/umbraco/Data/*
!**/umbraco/Data/packages
!**/umbraco/Data/packages/*
+1
View File
@@ -35,3 +35,4 @@ msbuild.wrn
# Visual Studio 2015
.vs/
+27
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
+1 -1
View File
@@ -2,4 +2,4 @@
*.FC.lock
*.bak
*.FCBak
*.tmp
*.tmp
+1
View File
@@ -30,3 +30,4 @@
/server/default/deploy/*.xml.dodeploy
/server/default/deploy/*.war.failed
/server/default/deploy/*.war.dodeploy
-3
View File
@@ -1,3 +0,0 @@
.flatpak-builder
builddir
repo
+1
View File
@@ -42,3 +42,4 @@
!/var/package
/var/package/*
!/var/package/*.xml
+1 -1
View File
@@ -3,7 +3,7 @@
/pub/sitemap
/pub/sitemap.xml
/app/config_sandbox
# Keep shared app/etc/config.php in version control; ignore environment-specific settings.
/app/etc/config.php
/app/etc/env.php
/app/code/Magento/TestModule*
/lib/internal/flex/uploader/.actionScriptProperties
+1 -1
View File
@@ -5,4 +5,4 @@
/Application/Runtime/
# Common configure file
/Application/Common/Conf/config.php
/Application/Common/Conf/config.php
+8
View File
@@ -10,12 +10,14 @@ Icon
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
@@ -29,6 +31,7 @@ $RECYCLE.BIN/
*.msm
*.msp
############################
# Packages
############################
@@ -57,6 +60,7 @@ $RECYCLE.BIN/
*.out
*.pid
############################
# Logs and databases
############################
@@ -66,6 +70,7 @@ $RECYCLE.BIN/
*.sql
*.sqlite
############################
# Misc.
############################
@@ -75,6 +80,7 @@ $RECYCLE.BIN/
nbproject
.vscode/
############################
# Node.js
############################
@@ -92,6 +98,7 @@ package-lock.json
!docs/package-lock.json
*.heapsnapshot
############################
# Tests
############################
@@ -101,6 +108,7 @@ coverage
cypress/screenshots
cypress/videos
############################
# Documentation
############################
+1
View File
@@ -49,3 +49,4 @@ SKC_2
*.zip
*.rar
*.exe
@@ -24,3 +24,4 @@
**/*.X/mcc-manifest-*.yml
#-----------------------------------------------------------------------------
+1
View File
@@ -5,6 +5,7 @@ build/
meta.xml
icon.png
# Ignore editor or IDE-specific files
.vscode/
.idea/
+78
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()
+189
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"])
+433
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()