Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ __pycache__/
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini

.~lock*
320 changes: 320 additions & 0 deletions Audit-RepoHealth.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Audits a Git repository for Windows-incompatible and system files.

.DESCRIPTION
Scans the repo for problematic files:
- Windows system metadata (desktop.ini, Thumbs.db, etc.)
- Reserved Windows filenames (CON, NUL, PRN, AUX, LPT1-9, COM1-9)
- Untracked system files that shouldn't be committed
- Symlink anomalies

.PARAMETER RepoPath
Path to the Git repository. Defaults to current working directory.

.PARAMETER FixMode
If specified, automatically removes problematic files and updates .gitignore.

.EXAMPLE
.\Audit-RepoHealth.ps1
.\Audit-RepoHealth.ps1 -RepoPath "C:\repos\my-project"
.\Audit-RepoHealth.ps1 -RepoPath "." -FixMode

.NOTES
Author: GitHub Copilot
Date: December 2025
#>

param(
[string]$RepoPath = (Get-Location).Path,
[switch]$FixMode = $false
)

# Define problematic files and patterns
$WindowsSystemFiles = @(
"desktop.ini"
"Thumbs.db"
"ehthumbs.db"
".DS_Store"
"._*"
".Trashes"
".Spotlight-V100"
"Zone.Identifier"
".fseventsd"
)

$ReservedNames = @(
"CON", "PRN", "AUX", "NUL"
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9"
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
)

function Test-IsGitRepo {
param([string]$Path)

try {
Push-Location $Path
$null = git rev-parse --git-dir 2>$null
$result = $LASTEXITCODE -eq 0
Pop-Location
return $result
}
catch {
return $false
}
}

function Get-GitTrackedFiles {
param([string]$Path)

try {
Push-Location $Path
$files = @(git ls-files 2>$null)
Pop-Location
return $files
}
catch {
return @()
}
}

function Get-UntrackedFiles {
param([string]$Path)

try {
Push-Location $Path
$files = @(git ls-files --others --exclude-standard 2>$null)
Pop-Location
return $files
}
catch {
return @()
}
}

function Find-ProblematicFiles {
param(
[string]$Path,
[string[]]$Patterns,
[string]$SearchType = "untracked" # "tracked", "untracked", "all"
)

$problematic = @()

if ($SearchType -in @("tracked", "all")) {
$tracked = Get-GitTrackedFiles -Path $Path
foreach ($pattern in $Patterns) {
$matching = $tracked | Where-Object { $_ -match $pattern }
$problematic += $matching
}
}

if ($SearchType -in @("untracked", "all")) {
$untracked = Get-UntrackedFiles -Path $Path
foreach ($pattern in $Patterns) {
$matching = $untracked | Where-Object { $_ -match $pattern }
$problematic += $matching
}
}

return $problematic | Select-Object -Unique
}

function Test-ReservedName {
param([string]$Filename)

$baseName = [System.IO.Path]::GetFileNameWithoutExtension($Filename)
return $baseName -in $ReservedNames
}

function Find-ReservedNames {
param([string]$Path)

$problematic = @()

try {
Push-Location $Path
$allFiles = Get-ChildItem -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { -not $_.PSIsContainer }

foreach ($file in $allFiles) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
if (Test-ReservedName -Filename $file.Name) {
$problematic += $file.FullName
}
}

Pop-Location
}
catch {
Write-Warning "Error scanning for reserved names: $_"
}

return $problematic
}

function Remove-ProblematicFile {
param(
[string]$FilePath,
[string]$RepoPath
)

try {
$fullPath = Join-Path $RepoPath $FilePath

if (Test-Path $fullPath) {
# Try to remove from git first if tracked
Push-Location $RepoPath
$tracked = @(git ls-files) | Where-Object { $_ -eq $FilePath }
if ($tracked) {
git rm --cached $FilePath 2>$null | Out-Null
Write-Host " ✓ Untracked from git: $FilePath"
}
Pop-Location

# Remove local file
Remove-Item $fullPath -Force -ErrorAction SilentlyContinue
Write-Host " ✓ Deleted: $FilePath"
return $true
}
}
catch {
Write-Warning " ✗ Failed to remove $FilePath : $_"
return $false
}

return $true
}

function Update-Gitignore {
param(
[string]$RepoPath,
[string[]]$Entries
)

$gitignorePath = Join-Path $RepoPath ".gitignore"
$existingContent = @()

if (Test-Path $gitignorePath) {
$existingContent = @(Get-Content $gitignorePath)
}

$toAdd = @()
foreach ($entry in $Entries) {
if ($existingContent -notcontains $entry) {
$toAdd += $entry
}
}

if ($toAdd.Count -gt 0) {
Add-Content -Path $gitignorePath -Value "" -ErrorAction SilentlyContinue
Add-Content -Path $gitignorePath -Value "# System files (auto-added by Audit-RepoHealth.ps1)" -ErrorAction SilentlyContinue
Add-Content -Path $gitignorePath -Value $toAdd -ErrorAction SilentlyContinue
Write-Host "✓ Updated .gitignore with $($ toAdd.Count) entries"
return $true
}

return $false
}

# ===== MAIN SCRIPT =====

Write-Host "`n🔍 Git Repository Health Audit`n" -ForegroundColor Cyan
Write-Host "Repository: $RepoPath`n"

# Verify it's a git repo
if (-not (Test-IsGitRepo -Path $RepoPath)) {
Write-Host "❌ Error: Not a valid Git repository!" -ForegroundColor Red
exit 1
}

# Build regex patterns
$patterns = $WindowsSystemFiles | ForEach-Object {
[regex]::Escape($_) -replace '\\\*', '.*'
}

# ===== SCAN FOR TRACKED SYSTEM FILES =====
Write-Host "📋 Scanning for tracked system files..." -ForegroundColor Yellow
$trackedProblematic = Find-ProblematicFiles -Path $RepoPath -Patterns $patterns -SearchType "tracked"

if ($trackedProblematic.Count -gt 0) {
Write-Host "`n⚠️ Found tracked system files:" -ForegroundColor Red
$trackedProblematic | ForEach-Object { Write-Host " - $_" }

if ($FixMode) {
Write-Host "`n🔧 Removing tracked system files..." -ForegroundColor Yellow
foreach ($file in $trackedProblematic) {
Remove-ProblematicFile -FilePath $file -RepoPath $RepoPath
}
}
}
else {
Write-Host "✓ No tracked system files found" -ForegroundColor Green
}

# ===== SCAN FOR UNTRACKED SYSTEM FILES =====
Write-Host "`n📋 Scanning for untracked system files..." -ForegroundColor Yellow
$untrackedProblematic = Find-ProblematicFiles -Path $RepoPath -Patterns $patterns -SearchType "untracked"

if ($untrackedProblematic.Count -gt 0) {
Write-Host "`n⚠️ Found untracked system files:" -ForegroundColor Yellow
$untrackedProblematic | ForEach-Object { Write-Host " - $_" }

if ($FixMode) {
Write-Host "`n🔧 Removing untracked system files..." -ForegroundColor Yellow
foreach ($file in $untrackedProblematic) {
$fullPath = Join-Path $RepoPath $file
if (Test-Path $fullPath) {
Remove-Item $fullPath -Force -ErrorAction SilentlyContinue
Write-Host " ✓ Deleted: $file"
}
}
}
}
else {
Write-Host "✓ No untracked system files found" -ForegroundColor Green
}

# ===== SCAN FOR RESERVED WINDOWS NAMES =====
Write-Host "`n📋 Scanning for reserved Windows filenames..." -ForegroundColor Yellow
$reserved = Find-ReservedNames -Path $RepoPath

if ($reserved.Count -gt 0) {
Write-Host "`n⚠️ Found files with reserved Windows names:" -ForegroundColor Red
$reserved | ForEach-Object {
$relPath = $_.Replace($RepoPath, "").TrimStart("\")
Write-Host " - $relPath"
}

if ($FixMode) {
Write-Host "`n🔧 These files should be renamed manually (Windows reserved names):`n" -ForegroundColor Yellow
$reserved | ForEach-Object {
$relPath = $_.Replace($RepoPath, "").TrimStart("\")
Write-Host " TODO: Rename $relPath"
}
}
}
else {
Write-Host "✓ No reserved Windows filenames found" -ForegroundColor Green
}

# ===== UPDATE .GITIGNORE =====
if ($FixMode -and ($trackedProblematic.Count -gt 0 -or $untrackedProblematic.Count -gt 0)) {
Write-Host "`n📝 Updating .gitignore..." -ForegroundColor Yellow
Update-Gitignore -RepoPath $RepoPath -Entries $WindowsSystemFiles
}

# ===== FINAL REPORT =====
$totalIssues = $trackedProblematic.Count + $untrackedProblematic.Count + $reserved.Count

Write-Host "`n" + ("=" * 60)
if ($totalIssues -eq 0) {
Write-Host "✅ Repository is clean! No issues detected." -ForegroundColor Green
}
else {
Write-Host "⚠️ Found $totalIssues issue(s). Use -FixMode to remediate." -ForegroundColor Yellow
}
Write-Host "=" * 60 + "`n"

exit $totalIssues
Loading