functions/content/Get-AdsRepositoryFile.ps1
function Get-AdsRepositoryFile { <# .SYNOPSIS Search an Azure DevOps repository for files. .DESCRIPTION Search an Azure DevOps repository for files. This does NOT use the search API but instead enumerates the entire content of each repository in each project of an organization. This will be fairly slow, but should work in all project/organization configurations. Scoped filters: Most include and exclude filters support scoping filters. The filter notation works like this: <Project>/<Repository>/<Branch>/<File> Example using "-ExcludeRepository": - "main" - "*/main" - "dev-*/sub-*" The first two inputs are equivalent, as not providing any segments automatically has the missing segments interpreted as "*". The third exclude filter will exclude all repositories starting with "sub-" so long as the project starts with "dev-". Example using "-Name": - "test.ps1": scan files named test.ps1 in any project, repository or branch. - "*/*/*/test.ps1": scan files named test.ps1 in any project, repository or branch. - "marigold/*/*/*.ps1"; Scan all PowerShell (ps1) files in any branch or repository of the marigold project Each filter parameter should have exactly ONE element or N elements in its filter, where N is: - Repository / ExcludeRepository: 2 - Branch / ExcludeBranch: 3 - Name / ExcludeName: 4 .PARAMETER Organization The organizations to search. .PARAMETER Project The projects to inspect. Defaults to '*' .PARAMETER ExcludeProject Projects to explicitly NOT inspect. .PARAMETER Repository The repositories to inspect. Defaults to '*' Supports scoped filters (See description). .PARAMETER ExcludeRepository Repositories to explicitly NOT inspect. Supports scoped filters (See description). .PARAMETER Branch The branches to look at. Defaults to '*' Supports scoped filters (See description). .PARAMETER ExcludeBranch Branches to explicitly NOT inspect. Supports scoped filters (See description). .PARAMETER Name A name filter to apply to all files. For example, set this to '*.ps1' to search all PowerShell script files. Defaults to '*' Supports scoped filters (See description). .PARAMETER ExcludeName File names to NOT inspect. Supports scoped filters (See description). .PARAMETER IncludeContent Whether the actual file content should be included in the output .PARAMETER ApiVersion The API version to use for this request. Defaults to '6.0' .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE PS C:\> Get-AdsRepositoryFile -Organization myOrg Returns all files in all branches of all repositories of all projects of myOrg .EXAMPLE PS C:\> Get-AdsRepositoryFile -Organization myOrg -Name *.ps1,*.psm1 Returns all PowerShell script & module files in all branches of all repositories of all projects of myOrg .EXAMPLE PS C:\> Get-AdsRepositoryFile -Organization Contoso -Name *.ps1,*.psm1 -Project ContosoTools -Branch master Returns all PowerShell script & module files in the master branch of all repositories in the ContosoTools project under the Contoso organization. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Organization, [string[]] $Project = '*', [string[]] $ExcludeProject, [string[]] $Repository = '*', [string[]] $ExcludeRepository, [string[]] $Branch = '*', [string[]] $ExcludeBranch, [string[]] $Name = '*', [string[]] $ExcludeName, [switch] $IncludeContent, [string] $ApiVersion = '6.0', [switch] $EnableException ) begin { #region Functions function ConvertTo-Filter { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet('Repository', 'Branch', 'File')] [string] $Type, [Parameter(ValueFromPipeline = $true)] [string] $InputObject ) process { if (-not $InputObject) { return } switch ($Type) { #region Repository 'Repository' { if ($InputObject -notmatch '/') { [PSCustomObject]@{ Project = '*' Repository = $InputObject } } else { [PSCustomObject]@{ Project = $InputObject -replace '/.+$' Repository = $InputObject -replace '^.+?/' } } } #endregion Repository #region Branch 'Branch' { if ($InputObject -notmatch '/.+/') { [PSCustomObject]@{ Project = '*' Repository = '*' Branch = $InputObject } } else { [PSCustomObject]@{ Project = $InputObject -replace '/.+$' Repository = $InputObject -replace '^.+?/' -replace '/.+$' Branch = $InputObject -replace '^.+/' } } } #endregion Branch #region File 'File' { if ($InputObject -notmatch '/.+/.+/') { [PSCustomObject]@{ Project = '*' Repository = '*' Branch = '*' File = $InputObject } } else { [PSCustomObject]@{ Project = $InputObject -replace '/.+$' Repository = $InputObject -replace '^.+?/' -replace '/.+$' Branch = $InputObject -replace '^.+/' -replace '^.+/' -replace '/.+$' File = $InputObject -replace '^.+/' } } } #endregion File } } } function Select-Filter { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $Filters, [Parameter(Mandatory = $true, ParameterSetName = 'Project')] $Project, [Parameter(Mandatory = $true, ParameterSetName = 'Repository')] $Repository, [Parameter(Mandatory = $true, ParameterSetName = 'Branch')] $Branch ) $newFilter = $Filters.Clone() $keys = $newFilter.Keys | Write-Output if ($Poject) { foreach ($key in $keys) { $newFilter.$key = $newFilter.$key | Where-Object { $Project -like $_.Project } } } if ($Repository) { foreach ($key in $keys) { $newFilter.$key = $newFilter.$key | Where-Object { $Repository -like $_.Repository } } } if ($Branch) { foreach ($key in $keys) { $newFilter.$key = $newFilter.$key | Where-Object { $Branch -like $_.Branch } } } $newFilter } #endregion Functions $apiParam = @{ ApiVersion = $ApiVersion } $allFilters = @{ RepoFilters = $Repository | ConvertTo-Filter -Type Repository RepoExcludeFilters = $ExcludeRepository | ConvertTo-Filter -Type Repository BranchFilters = $Branch | ConvertTo-Filter -Type Branch BranchExcludeFilters = $ExcludeBranch | ConvertTo-Filter -Type Branch FileFilters = $Name | ConvertTo-Filter -Type File FileExcludeFilters = $ExcludeName | ConvertTo-Filter -Type File } } process { foreach ($orgName in $Organization) { Write-PSFMessage -Message 'Processing Organization: {0}' -StringValues $orgName try { $projects = Get-AdsProject @apiParam -Organization $orgName -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to retrieve projects from Organization $orgName" -ErrorRecord $_ -EnableException $EnableException -Continue } foreach ($projectItem in $projects) { if (Test-Overlap -Value $projectItem.Name -Filter $Project -Not) { continue } if (Test-Overlap -Value $projectItem.Name -Filter $ExcludeProject) { continue } $projectFilter = Select-Filter -Filters $allFilters -Project $projectItem.Name Write-PSFMessage -Message ' Processing Project: {0}' -StringValues $projectItem.Name $projectParam = $apiParam.Clone() + @{ Organization = $orgName Project = $projectItem.id } try { $repositories = Get-AdsGitRepository @projectParam -ErrorAction Stop } catch { # Project doesn't have any repos if ($_.Exception.Response.StatusCode -eq 'NotFound') { continue } Stop-PSFFunction -Message "Failed to retrieve repositories from project $($projectItem.id) from Organization $orgName" -ErrorRecord $_ -EnableException $EnableException -Continue -Target $projectItem } foreach ($repositoryItem in $repositories) { if (Test-Overlap -Value $repositoryItem.Name -Filter $projectFilter.RepoFilters.Repository -Not) { continue } if (Test-Overlap -Value $repositoryItem.Name -Filter $projectFilter.RepoExcludeFilters.Repository) { continue } $repositoryFilter = Select-Filter -Filters $projectFilter -Repository $repositoryItem.Name Write-PSFMessage -Level SomewhatVerbose -Message ' Scanning repository: {0}' -StringValues $repositoryItem.Name try { $branches = Get-AdsGitRepositoryBranchStatistics @projectParam -RepositoryId $repositoryItem.id -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to retrieve branches from repository $($repositoryItem.id) of project $($projectItem.id) from Organization $orgName" -ErrorRecord $_ -EnableException $EnableException -Continue -Target $repositoryItem } foreach ($branchItem in $branches) { if (Test-Overlap -Value $branchItem.Name -Filter $repositoryFilter.BranchFilters.Branch -Not) { continue } if (Test-Overlap -Value $branchItem.Name -Filter $repositoryFilter.BranchExcludeFilters.Branch) { continue } $branchFilter = Select-Filter -Filters $repositoryFilter -Branch $branchItem.Name $files = Get-AdsGitRepositoryItem @projectParam -RepositoryId $repositoryItem.id -RecursionLevel Full -IncludeContentMetadata $true -VersionType branch -Version $branchItem.name foreach ($fileItem in $files) { if ($fileItem.isFolder) { continue } $fileName = Split-Path -Path $fileItem.path -Leaf $folderPath = (Split-Path -Path $fileItem.path) -replace '\\', '/' if ($folderPath -notlike "*/") { $folderPath += "/" } if (Test-Overlap -Value $fileName -Filter $branchFilter.FileFilters.File -Not) { continue } if (Test-Overlap -Value $fileName -Filter $branchFilter.FileExcludeFilters.File) { continue } $result = [PSCustomObject]@{ PSTypeName = 'AzureDevOps.Git.File' Name = '{0}/{1}/{2}{3} [{4}]' -f $orgName, $projectItem.Name, $repositoryItem.name, $fileItem.path, $branchItem.Name FileName = $fileName FilePath = $fileItem.path FolderPath = $folderPath Content = "" Organization = $orgName ProjectName = $projectItem.name ProjectID = $projectItem.id RepositoryName = $repositoryItem.name RepositoryID = $repositoryItem.id Branch = $branchItem.name url = $fileItem.url FileID = $fileItem.ObjectID CommitID = $fileItem.commitId } if ($IncludeContent) { $result.Content = Get-AdsGitRepositoryItem @projectParam -RepositoryId $repositoryItem.id -Version $branchItem.name -ScopePath $fileItem.path } $result } } } } } } } |