public/Test-GitFileModified.ps1

function Test-GitFileModified {
    <#
    .SYNOPSIS
        Tests if a file has been modified in git.
 
    .DESCRIPTION
        Checks if a file has uncommitted changes, staged changes, or unpushed commits.
        Returns $true if the file is modified, $false if clean.
        Uses begin block to cache git repository context, making it efficient for
        pipeline operations while still checking fresh per-file git status.
 
    .PARAMETER Path
        The file path to check. Can be absolute or relative.
 
    .PARAMETER CommitDepth
        When on main/master/trunk branch, specifies how many recent commits to check
        for file modifications. Defaults to 10.
        On feature branches, checks ALL commits in the branch (ignores this parameter).
 
    .EXAMPLE
        Test-GitFileModified -Path "C:\repo\file.ps1"
        Returns $true if file.ps1 has any uncommitted, staged, or unpushed changes.
 
    .EXAMPLE
        Get-ChildItem *.ps1 | Where-Object { -not (Test-GitFileModified -Path $_.FullName) }
        Gets all .ps1 files that have NOT been modified.
 
    .OUTPUTS
        [bool]
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [string]$Path,

        [Parameter()]
        [ValidateRange(0, 100)]
        [int]$CommitDepth = 10
    )

    begin {
        # Cache git repository context once for all pipeline items
        $script:gitContextCache = $null

        try {
            # Check if we're in a git repository
            $null = git rev-parse --is-inside-work-tree 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-PSFMessage -Level Verbose -Message "Not in a git repository"
                $script:gitContextCache = @{ InRepo = $false }
                return
            }

            # Get repo root
            $repoRoot = git rev-parse --show-toplevel 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-PSFMessage -Level Warning -Message "Could not determine repo root"
                $script:gitContextCache = @{ InRepo = $false }
                return
            }
            $repoRoot = $repoRoot -replace '/', '\'

            # Get current branch and upstream info
            $currentBranch = git rev-parse --abbrev-ref HEAD 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-PSFMessage -Level Warning -Message "Could not determine current branch"
                $script:gitContextCache = @{ InRepo = $false }
                return
            }

            $upstreamBranch = git symbolic-ref refs/remotes/origin/HEAD 2>&1 | ForEach-Object { $_ -replace 'refs/remotes/', '' }
            $hasUpstream = $LASTEXITCODE -eq 0

            if (-not $hasUpstream) {
                Write-PSFMessage -Level Verbose -Message "No upstream branch found, using local commit history"
            }

            $upstreamBranchName = if ($hasUpstream) { $upstreamBranch -replace '^origin/', '' } else { $null }
            $isOnMainBranch = $currentBranch -in @('main', 'master', 'trunk', $upstreamBranchName)

            # Cache context
            $script:gitContextCache = @{
                InRepo = $true
                RepoRoot = $repoRoot
                CurrentBranch = $currentBranch
                UpstreamBranch = $upstreamBranch
                HasUpstream = $hasUpstream
                IsOnMainBranch = $isOnMainBranch
            }

            Write-PSFMessage -Level Verbose -Message "Git context cached: Branch=$currentBranch, IsMain=$isOnMainBranch, HasUpstream=$hasUpstream"
        } catch {
            Write-PSFMessage -Level Warning -Message "Error initializing git context: $_"
            $script:gitContextCache = @{ InRepo = $false }
        }
    }

    process {
        try {
            # Quick exit if not in repo
            if (-not $script:gitContextCache.InRepo) {
                return $false
            }

            # Normalize paths using cached repo root
            $resolvedPath = Resolve-Path -Path $Path -ErrorAction SilentlyContinue
            if (-not $resolvedPath) {
                Write-PSFMessage -Level Warning -Message "Could not resolve path: $Path"
                return $false
            }

            $normalizedPath = $resolvedPath.Path -replace '\\', '/'
            $relativePath = $normalizedPath -replace [regex]::Escape($script:gitContextCache.RepoRoot), '' -replace '^[/\\]', '' -replace '\\', '/'

            Write-PSFMessage -Level Verbose -Message "Checking if file is modified: $relativePath"

            # Check uncommitted working tree changes
            $workingTreeCheck = git diff --name-only -- $relativePath 2>&1
            if ($LASTEXITCODE -eq 0 -and $workingTreeCheck) {
                Write-PSFMessage -Level Verbose -Message "File has uncommitted changes: $relativePath"
                return $true
            }

            # Check staged changes
            $stagedCheck = git diff --name-only --cached -- $relativePath 2>&1
            if ($LASTEXITCODE -eq 0 -and $stagedCheck) {
                Write-PSFMessage -Level Verbose -Message "File has staged changes: $relativePath"
                return $true
            }

            # Check commit history based on branch type
            if ($script:gitContextCache.IsOnMainBranch) {
                # On main/master/trunk: check only recent commits (use CommitDepth)
                if ($CommitDepth -gt 0) {
                    $recentCommitCheck = git log -n $CommitDepth --name-only --pretty=format: -- $relativePath 2>&1 | Where-Object { $_.Trim() }
                    if ($LASTEXITCODE -eq 0 -and $recentCommitCheck) {
                        Write-PSFMessage -Level Verbose -Message "File modified in last $CommitDepth commits: $relativePath"
                        return $true
                    }
                }
            } else {
                # On feature branch: check ALL commits in branch (ignore CommitDepth)
                if ($script:gitContextCache.HasUpstream) {
                    $committedCheck = git diff --name-only "$($script:gitContextCache.UpstreamBranch)..HEAD" -- $relativePath 2>&1
                    if ($LASTEXITCODE -eq 0 -and $committedCheck) {
                        Write-PSFMessage -Level Verbose -Message "File has unpushed commits in feature branch: $relativePath"
                        return $true
                    }
                } else {
                    # No upstream, check local commit history
                    if ($CommitDepth -gt 0) {
                        $recentCommitCheck = git log -n $CommitDepth --name-only --pretty=format: -- $relativePath 2>&1 | Where-Object { $_.Trim() }
                        if ($LASTEXITCODE -eq 0 -and $recentCommitCheck) {
                            Write-PSFMessage -Level Verbose -Message "File modified in last $CommitDepth commits: $relativePath"
                            return $true
                        }
                    }
                }
            }

            Write-PSFMessage -Level Verbose -Message "File is clean: $relativePath"
            return $false

        } catch {
            Write-PSFMessage -Level Warning -Message "Error checking git status for $Path : $_"
            return $false
        }
    }
}