Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoGitRepository/New-AzDoGitRepository.ps1

<#
.SYNOPSIS
Creates a new Azure DevOps Git repository within a specified project.
 
.DESCRIPTION
The New-AzDoGitRepository function creates a new Git repository in an Azure DevOps project.
 
When 'SourceRepository' is supplied, the repository is seeded at creation time as either a fork of
an existing Azure DevOps repository (using the native 'parentRepository' support on the create
call) or an import from an external Git URL (via the asynchronous Import Requests API, polled to
completion) - see 'SourceType'. This only ever happens here, at creation time: it is never repeated
by Set-AzDoGitRepository, and a failed import is surfaced as an error rather than left as a
silently-empty repository.
 
.PARAMETER ProjectName
The name of the Azure DevOps project where the new repository will be created.
 
.PARAMETER RepositoryName
The name of the new Git repository to be created.
 
.PARAMETER SourceRepository
(Optional) Seeds the repository at creation time. Its meaning depends on 'SourceType': a Git URL
(or SSH remote) for an import, or 'Project/Repo' (or just 'Repo' for a repository in this project)
for a fork.
 
.PARAMETER SourceType
(Optional) 'Import' or 'Fork'. When omitted and 'SourceRepository' is set, it is inferred: a URL or
an SSH remote (e.g. 'git@host:owner/repo.git') defaults to 'Import', anything else to 'Fork'.
 
.PARAMETER ImportServiceConnectionName
(Optional) The name of a generic Git service connection (in this project) holding the credentials
needed to import from a private 'SourceRepository' URL. Only used when importing.
 
.PARAMETER IsDisabled
(Optional) When $true, disables the repository once it (and any import) has finished being created.
 
.PARAMETER LookupResult
(Optional) A hashtable to store lookup results.
 
.PARAMETER Ensure
(Optional) Specifies whether to ensure the repository exists or does not exist.
 
.PARAMETER Force
(Optional) Forces the creation of the repository even if it already exists.
 
.EXAMPLE
PS> New-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo"
 
Creates a new, empty Git repository named "MyRepo" in the "MyProject" Azure DevOps project.
 
.EXAMPLE
PS> New-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" -SourceRepository "https://github.com/MyUser/MyRepo.git"
 
Creates "MyRepo" and imports the contents of the public GitHub repository at that URL.
 
.EXAMPLE
PS> New-AzDoGitRepository -ProjectName "MyProject" -RepositoryName "MyRepo" -SourceRepository "TemplateProject/TemplateRepo" -SourceType Fork
 
Creates "MyRepo" as a fork of "TemplateRepo" in the "TemplateProject" project.
 
.NOTES
This function requires the Azure DevOps organization name to be set in the global variable (Get-AzDoOrganizationName).
#>


Function New-AzDoGitRepository
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Alias('Name')]
        [System.String]$ProjectName,

        [Parameter(Mandatory = $true)]
        [Alias('Repository')]
        [System.String]$RepositoryName,

        [Parameter()]
        [Alias('Source')]
        [System.String]$SourceRepository,

        [Parameter()]
        [Alias('SourceKind')]
        [System.String]$SourceType,

        [Parameter()]
        [Alias('ServiceConnection')]
        [System.String]$ImportServiceConnectionName,

        [Parameter()]
        [System.Boolean]$IsDisabled = $false,

        [Parameter()]
        [HashTable]$LookupResult,

        [Parameter()]
        [Ensure]$Ensure,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Force
    )

    Write-Verbose "[New-AzDoGitRepository] Creating new repository '$($RepositoryName)' in project '$($ProjectName)'"

    $OrganizationName = Get-AzDoOrganizationName
    $ApiUri  = 'https://dev.azure.com/{0}/' -f $OrganizationName
    $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects'

    # If not in cache, fall back to a live API lookup
    if ($null -eq $project)
    {
        Write-Verbose "[New-AzDoGitRepository] Project '$ProjectName' not in cache — falling back to live API lookup."
        $project = Invoke-AzDevOpsApiRestMethod -Uri "https://dev.azure.com/$OrganizationName/_apis/projects/${ProjectName}?api-version=7.1-preview.4" -Method Get
        if ($project) { Add-CacheItem -Key $ProjectName -Value $project -Type 'LiveProjects' }
    }

    if ($null -eq $project)
    {
        Write-Error "[New-AzDoGitRepository] Project '$ProjectName' not found. Skipping change."
        return
    }

    # Resolve how (if at all) the repository should be seeded. This only ever happens here, at
    # creation time - 'SourceRepository'/'SourceType'/'ImportServiceConnectionName' are listed in
    # the class's 'GetDscResourcePropertyNamesWithNoSetSupport()' and are never passed to Set().
    $effectiveSourceType = $null
    $parentRepository    = $null

    if (![System.String]::IsNullOrWhiteSpace($SourceRepository))
    {
        if (![System.String]::IsNullOrWhiteSpace($SourceType))
        {
            $effectiveSourceType = $SourceType
        }
        else
        {
            # A URL (any scheme) or an SSH remote (e.g. 'git@host:owner/repo.git') is an import;
            # anything else - a bare repository name, or 'Project/Repo' - is a fork of an existing
            # Azure DevOps repository.
            $looksLikeUrl = ($SourceRepository -match '^[a-zA-Z][a-zA-Z0-9+.-]*://') -or ($SourceRepository -match '^[^@\s/]+@[^:\s]+:')
            $effectiveSourceType = if ($looksLikeUrl) { 'Import' } else { 'Fork' }
        }

        Write-Verbose "[New-AzDoGitRepository] SourceRepository '$SourceRepository' resolved to SourceType '$effectiveSourceType'."
    }

    if ($effectiveSourceType -eq 'Fork')
    {
        # 'SourceRepository' is 'Project/Repo', or just 'Repo' for a repository in this project.
        if ($SourceRepository -match '^(?<project>[^/]+)/(?<repo>.+)$')
        {
            $sourceProjectName = $Matches.project
            $sourceRepoName    = $Matches.repo
        }
        else
        {
            $sourceProjectName = $ProjectName
            $sourceRepoName    = $SourceRepository
        }

        $sourceProject = if ($sourceProjectName -eq $ProjectName) { $project } else { Resolve-AzDoProject -ProjectName $sourceProjectName }

        if ($null -eq $sourceProject)
        {
            Write-Error "[New-AzDoGitRepository] Fork source project '$sourceProjectName' not found. Skipping change."
            return
        }

        $sourceRepo = Get-CacheItem -Key "$sourceProjectName\$sourceRepoName" -Type 'LiveRepositories'
        if ($null -eq $sourceRepo)
        {
            $allSourceRepos = List-DevOpsGitRepository -OrganizationName $OrganizationName -ProjectName $sourceProjectName
            $sourceRepo     = $allSourceRepos | Where-Object { $_.name -eq $sourceRepoName } | Select-Object -First 1
        }

        if ($null -eq $sourceRepo)
        {
            Write-Error "[New-AzDoGitRepository] Fork source repository '$sourceRepoName' not found in project '$sourceProjectName'. Skipping change."
            return
        }

        $parentRepository = @{
            id      = $sourceRepo.id
            project = @{ id = $sourceProject.id }
        }
    }

    # Define parameters for creating the repository
    $params = @{
        ApiUri         = $ApiUri
        Project        = $project
        RepositoryName = $RepositoryName
    }

    if ($parentRepository)
    {
        $params.ParentRepository = $parentRepository
    }

    # Create the (possibly forked) repository
    $value = New-GitRepository @params

    if ($null -eq $value)
    {
        Write-Error "[New-AzDoGitRepository] New-GitRepository returned null for repository '$RepositoryName' in project '$ProjectName'. Check authentication token and organization settings."
        return
    }

    if ($effectiveSourceType -eq 'Import')
    {
        $serviceEndpointId = $null

        if (![System.String]::IsNullOrWhiteSpace($ImportServiceConnectionName))
        {
            $serviceConnection = Get-CacheItem -Key "$ProjectName\$ImportServiceConnectionName" -Type 'LiveServiceConnections'
            if ($null -eq $serviceConnection)
            {
                $allServiceConnections = List-DevOpsServiceConnections -ApiUri $ApiUri -ProjectName $ProjectName
                $serviceConnection     = $allServiceConnections | Where-Object { $_.name -eq $ImportServiceConnectionName } | Select-Object -First 1
            }

            if ($null -eq $serviceConnection)
            {
                Write-Error "[New-AzDoGitRepository] Import service connection '$ImportServiceConnectionName' not found in project '$ProjectName'. Repository '$RepositoryName' was created empty."
                return
            }

            $serviceEndpointId = $serviceConnection.id
        }

        Write-Verbose "[New-AzDoGitRepository] Importing '$SourceRepository' into repository '$RepositoryName'."

        $importParams = @{
            ApiUri     = $ApiUri
            Project    = $project
            Repository = $value
            SourceUrl  = $SourceRepository
        }
        if ($serviceEndpointId) { $importParams.ServiceEndpointId = $serviceEndpointId }

        $importRequest = New-GitImportRequest @importParams

        if ($null -eq $importRequest)
        {
            Write-Error "[New-AzDoGitRepository] New-GitImportRequest returned null for repository '$RepositoryName'. Repository was created empty - import was not started."
            return
        }

        $null = Wait-DevOpsGitImportRequest -ApiUri $ApiUri -Project $project -Repository $value -ImportRequestId $importRequest.importRequestId
    }

    if ($IsDisabled)
    {
        Write-Verbose "[New-AzDoGitRepository] Disabling repository '$RepositoryName' as requested."
        $disabledRepo = Set-GitRepository -ApiUri $ApiUri -Project $project -Repository $value -IsDisabled $true
        if ($disabledRepo) { $value = $disabledRepo }
    }

    # Add the repository to the LiveRepositories cache and write to verbose log
    Add-CacheItem -Key "$ProjectName\$RepositoryName" -Value $value -Type 'LiveRepositories'
    Export-CacheObject -CacheType 'LiveRepositories' -Content $AzDoLiveRepositories
    Refresh-CacheObject -CacheType 'LiveRepositories'
    Write-Verbose "[New-AzDoGitRepository] Added new group to LiveGroups cache with key: '$($value.Name)'"

}