Public/Get-ChangedFiles.ps1

function Get-ChangedFiles {
    <#
    .SYNOPSIS
    Get list of changed files using native git
    
    .DESCRIPTION
    Detects changed files matching specified extensions in the current branch compared to a base branch.
    Auto-detects PR context in Azure DevOps pipelines.
    
    .PARAMETER BaseBranch
    Base branch for comparison (default: origin/master). Only used outside a PR merge commit
    (three-dot, merge-base). In a PR build the base is the merge commit's first parent; see
    Resolve-DiffRange.
    
    .PARAMETER FileExtensions
    Array of file extensions to filter on.
    Each extension should include the leading dot.
    
    .EXAMPLE
    Get-ChangedFiles -FileExtensions @('.al', '.yml', '.ps1', '.json')
    Returns all changed .al and .yml files compared to origin/master
    
    .EXAMPLE
    Get-ChangedFiles -FileExtensions @('.al')
    Returns only changed .al files
    
    .EXAMPLE
    Get-ChangedFiles -BaseBranch "origin/development" -FileExtensions @('.al', '.yml', '.json')
    Compare against specific branch and filter on multiple extensions
    
    .OUTPUTS
    System.String[] - Array of file paths relative to repository root
    
    .NOTES
    Author: waldo
    Version: 2.1.0
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$BaseBranch = "origin/master",

        [Parameter(Mandatory = $true)]
        [string[]]$FileExtensions
    )
    
    begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand.Name)"
    }
    
    process {
        try {
            # Build regex pattern from extensions (e.g. "\.al$|\.yml$")
            $extPattern = ($FileExtensions | ForEach-Object { [regex]::Escape($_) + '$' }) -join '|'
            $extDisplay = $FileExtensions -join ', '

            # One shared resolution for the file list and the per-file diffs (Resolve-DiffRange).
            # PR build: the merge commit's first parent, never the target branch as it is now.
            $range = Resolve-DiffRange -BaseBranch $BaseBranch -Fetch
            if ($range.Skip) {
                Write-Host "##[warning]AI Code Review skipped: $($range.Reason). Not reviewing against another commit."
                return @()
            }

            Write-Host "Current HEAD: $(git rev-parse HEAD 2>&1)"
            Write-Host "Compare commit: $($range.Compare)"
            Write-Host "Base commit: $($range.Base) ($($range.Label))"
            Write-Host ""

            Write-Verbose "Getting changed files ($extDisplay): git diff $($range.Base) $($range.Compare)"
            $gitOutput = git diff --name-only --diff-filter=ACMR $range.Base $range.Compare 2>&1

            # Check if git command failed
            if ($LASTEXITCODE -ne 0) {
                Write-Error "Git diff failed with exit code $LASTEXITCODE"
                Write-Error "Git output: $gitOutput"
                throw "Git diff command failed"
            }

            $allChanged = @($gitOutput | Where-Object { $_ })
            Write-Host "Changed files: $($allChanged.Count) in total"

            $changedFiles = $allChanged | Where-Object { $_ -match $extPattern }
            
            if (-not $changedFiles) {
                Write-Host "No matching files changed (extensions: $extDisplay)"
                Write-Host ""
                Write-Host "Debug: All changed files:"
                if ($allChanged) {
                    $allChanged | ForEach-Object { Write-Host " $_" }
                } else {
                    Write-Host " (no files changed)"
                }
                Write-Host ""
                Write-Host "✅ No matching files changed - skipping review"
                return @()
            }
            
            $changedFiles = @($changedFiles)  # Ensure array
            Write-Host "✅ Found $($changedFiles.Count) changed file(s) (extensions: $extDisplay):"
            foreach ($file in $changedFiles) {
                Write-Host " - $file"
            }
            
            return $changedFiles
        }
        catch {
            Write-Error "Error in $($MyInvocation.MyCommand.Name): $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
    }
}