Public/Test-SLLineEnding.ps1

function Test-SLLineEnding {
    <#
    .SYNOPSIS
        Audits line-ending rules and the actual line endings of files in git repos. Read-only.
 
    .DESCRIPTION
        For each repo at or under -Path, and each tracked text file (and, by default,
        each untracked file that isn't ignored), it compares:
 
          rules effective .gitattributes (as git computes it), the applicable
                        .editorconfig files (local and parent folders, up to
                        root = true) and explicit VS Code files.eol settings
                        (.vscode/settings.json, *.code-workspace, global or
                        per-language)
          working copy the endings actually in the file
          stored what git keeps in the index
 
        Issues:
          Mixed both LF and CRLF in one file
          WrongEnding the file uses another ending than its rule
          EditorConfigConflict end_of_line disagrees with .gitattributes eol
          VSCodeConflict files.eol disagrees with the effective rule
          Renormalize git stores CRLF for a text file
 
        The rule is .gitattributes eol when set, otherwise .editorconfig end_of_line.
        A file with neither only has to be consistent. Binary files (-text, binary, or
        detected by git) are skipped. CRLF is not assumed wrong: Windows scripts can
        require it.
 
        Limits: VS Code files.eol only applies to new files, and its user settings
        and files.associations are not read. Editors cannot control scripts or
        generators, which must write the declared endings explicitly. This command
        reports. Repair-SLLineEnding fixes. Set-SLGitAttributes manages
        .gitattributes and renormalizing.
 
    .PARAMETER Path
        A repo, or a folder of repos. Default: the current folder.
 
    .PARAMETER Depth
        How many folders below -Path to look for repos. Default: 2.
 
    .PARAMETER TrackedOnly
        Check only tracked files.
 
    .PARAMETER PassThru
        Return one result per repo instead of $true/$false.
 
    .PARAMETER Quiet
        Don't print the report.
 
    .PARAMETER Strict
        Throw if any repo has an issue. For CI.
 
    .OUTPUTS
        Without -PassThru: $true if every repo is clean, else $false (also when no repo is found).
 
    .EXAMPLE
        Test-SLLineEnding -Path . -Strict
 
    .EXAMPLE
        (Test-SLLineEnding C:\src -PassThru -Quiet).Issues | Group-Object Kind
    #>

    [CmdletBinding()]
    [OutputType([bool], [pscustomobject])]
    param(
        [Parameter(Position = 0)] [string] $Path,
        [ValidateRange(0, 10)] [int] $Depth = 2,
        [switch] $TrackedOnly,
        [switch] $PassThru,
        [switch] $Quiet,
        [switch] $Strict
    )
    [void] (Get-SLGitPath)
    if (-not $Path) { $Path = (Get-Location).ProviderPath }
    if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "Folder not found: $Path. Pass a repo, or a folder that contains repos." }
    $Path = (Resolve-Path -LiteralPath $Path).ProviderPath
    $repos = @(Find-SLGitRepo -Path $Path -Depth $Depth)
    if ($repos.Count -eq 0) { Write-Warning "No git repos found in $Path or up to $Depth folder(s) below it." }
    $checks = @(foreach ($repo in $repos) { Get-SLLineEndingAudit -RepoPath $repo -IncludeUntracked:(-not $TrackedOnly) })
    if (-not $Quiet) { Write-SLLineEndingReport -Checks $checks -Path $Path }
    $bad = @($checks | Where-Object { -not $_.Ok })
    if ($Strict -and ($bad.Count -gt 0 -or $checks.Count -eq 0)) {
        $what = 'no repo was checked'
        if ($checks.Count -gt 0) { $what = (@($bad | ForEach-Object { "$($_.Repo) ($(@($_.Issues).Count))" }) -join ', ') }
        throw "SecondLine line endings: $($bad.Count) of $($checks.Count) repos have issues: $what"
    }
    if ($PassThru) { return $checks }
    return ($checks.Count -gt 0 -and $bad.Count -eq 0)
}

function Write-SLLineEndingReport {
    param([object[]] $Checks, [string] $Path)
    Write-Host ''
    Write-Host " SecondLine - line endings ($Path)" -ForegroundColor White
    foreach ($c in $Checks) {
        Write-Host ''
        $color = 'Green'
        $summary = "OK $($c.Repo) $($c.FilesChecked) text file(s), $($c.Binary) binary skipped"
        if (-not $c.Ok) {
            $color = 'Red'
            $kinds = @($c.Issues | Group-Object Kind | ForEach-Object { "$($_.Count) $($_.Name)" }) -join ', '
            $summary = "ISSUES $($c.Repo) $kinds"
        }
        Write-Host " $summary" -ForegroundColor $color
        foreach ($group in @($c.Issues | Group-Object Kind)) {
            foreach ($i in @($group.Group | Select-Object -First 8)) {
                $tag = ''
                if (-not $i.Tracked) { $tag = ' (untracked)' }
                Write-Host (" {0,-21} {1}{2}: {3}" -f $i.Kind, $i.File, $tag, $i.Detail) -ForegroundColor DarkGray
            }
            if ($group.Count -gt 8) { Write-Host " ... and $($group.Count - 8) more $($group.Name)" -ForegroundColor DarkGray }
        }
        if (-not $c.Ok) { Write-Host " fix: Repair-SLLineEnding -Path '$($c.Path.Replace("'", "''"))' -WhatIf" -ForegroundColor DarkGray }
        foreach ($n in $c.Notes) { Write-Host " note: $n" -ForegroundColor DarkGray }
    }
    $ok = @($Checks | Where-Object Ok).Count
    $color = 'Green'
    if ($ok -ne $Checks.Count) { $color = 'Yellow' }
    Write-Host ''
    Write-Host " $ok of $($Checks.Count) repos OK" -ForegroundColor $color
    Write-Host ''
}