Projects.psm1

<#
.SYNOPSIS
Get one or more Gitlab projects

.DESCRIPTION
Lookup metadata about Gitlab projects by one or more identifiers

.PARAMETER ProjectId
Project id - can be an integer, or a full path

.PARAMETER GroupId
Group id - can be an integer, or a full path

.PARAMETER Recurse
Whether or not to recurse specified group (default: false)
Alias: -r

.PARAMETER Url
Get a project by URL

.PARAMETER IncludeArchived
Whether or not to return archived projects (default: false)

.PARAMETER MaxPages
Maximum pages to return (default: 10)

.PARAMETER SiteUrl
Which Gitlab instance to query. This is optional, if not provided, will
first attempt to use the remote associated with the local git context.
If there is no established context (or no matching configuration), the default
site is used.

.PARAMETER WhatIf
Preview Gitlab API requests

.EXAMPLE
Get-GitlabProject

Get a project from local git context

.EXAMPLE
Get-GitlabProject -ProjectId 'mygroup/myproject'
OR
PS > Get-GitlabProject 'mygroup/myproject'
OR
PS > Get-GitlabProject 42

Get a single project by id

.EXAMPLE
Get-GitlabProject -GroupId 'mygroup' [-Recurse]

Get multiple projects by containing group

.EXAMPLE
Get-GitlabGroup 'mygroup' | Get-GitlabProject

Enumerate projects within a group

.LINK
https://github.com/chris-peterson/pwsh-gitlab#projects

.LINK
https://docs.gitlab.com/ee/api/projects.html
#>

$global:GitlabGetProjectDefaultPages = 10
function Get-GitlabProject {

    [CmdletBinding(DefaultParameterSetName='ById')]
    param (
        [Parameter(Position=0, Mandatory=$false, ParameterSetName='ById')]
        [string]
        $ProjectId = '.',

        [Parameter(Position=0, Mandatory=$true, ParameterSetName='ByGroup', ValueFromPipelineByPropertyName=$true)]
        [string]
        $GroupId,

        [Parameter(Mandatory=$false, ParameterSetName='ByUser')]
        [string]
        $UserId,

        [Parameter(Mandatory=$false, ParameterSetName='ByUser')]
        [switch]
        $Mine,

        [Parameter(Position=0, Mandatory=$true, ParameterSetName='ByTopics')]
        [string []]
        $Topics,

        [Parameter(Mandatory=$false, ParameterSetName='ByGroup')]
        [Alias('r')]
        [switch]
        $Recurse,

        [Parameter(Position=0, Mandatory=$true, ParameterSetName='ByUrl')]
        [string]
        $Url,

        [Parameter(Mandatory=$false)]
        [string]
        $Select,

        [switch]
        [Parameter(Mandatory=$false, ParameterSetName='ByGroup')]
        $IncludeArchived = $false,

        [switch]
        [Parameter(Mandatory=$false)]
        $All,

        [Parameter(Mandatory=$false)]
        [int]
        $MaxPages = $global:GitlabGetProjectDefaultPages,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    if ($All) {
        if ($MaxPages -ne $global:GitlabGetProjectDefaultPages) {
            Write-Warning -Message "Ignoring -MaxPages in favor of -All"
        }
        $MaxPages = [int]::MaxValue
    }

    $Projects = @()
    switch ($PSCmdlet.ParameterSetName) {
        ById {
            if ($ProjectId -eq '.') {
                $ProjectId = $(Get-LocalGitContext).Project
                if (-not $ProjectId) {
                    throw "Could not infer project based on current directory ($(Get-Location))"
                }
            }
            $Projects = Invoke-GitlabApi GET "projects/$($ProjectId | ConvertTo-UrlEncoded)" -SiteUrl $SiteUrl -WhatIf:$WhatIf
        }
        ByGroup {
            $Group = Get-GitlabGroup $GroupId
            $Query = @{
                'include_subgroups' = $Recurse ? 'true' : 'false'
            }
            if (-not $IncludeArchived) {
                $Query['archived'] = 'false'
            }
            $Projects = Invoke-GitlabApi GET "groups/$($Group.Id)/projects" $Query -MaxPages $MaxPages -SiteUrl $SiteUrl -WhatIf:$WhatIf |
                Where-Object { $($_.path_with_namespace).StartsWith($Group.FullPath) } |
                Sort-Object -Property 'Name'
        }
        ByUser {
            # https://docs.gitlab.com/ee/api/projects.html#list-user-projects

            if ($Mine) {
                if ($UserId) {
                    Write-Warning "Ignoring '-UserId $UserId' parameter since -Mine was also provided"
                }
                $UserId = Get-GitlabUser -Me | Select-Object -ExpandProperty Username
            }

            $Projects = Invoke-GitlabApi GET "users/$UserId/projects"
        }
        ByTopics {
            $Projects = Invoke-GitlabApi GET "projects" -Query @{
                topic = $Topics -join ','
            } -MaxPages $MaxPages -SiteUrl $SiteUrl -WhatIf:$WhatIf
        }
        ByUrl {
            $Url -match "$($(Get-DefaultGitlabSite).Url)/(?<ProjectId>.*)" | Out-Null
            if ($Matches) {
                $ProjectId = $Matches.ProjectId
                $ProjectId = $Matches.ProjectId
                Get-GitlabProject -ProjectId $ProjectId
            } else {
                throw "Url didn't match expected format"
            }
        }
    }

    $Projects |
        New-WrapperObject 'Gitlab.Project' |
        Get-FilteredObject $Select
}

function ConvertTo-GitlabTriggerYaml {
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $InputObject,

        [Parameter(Mandatory=$false)]
        [ValidateSet($null, 'depend')]
        [string]
        $Strategy = $null,

        [Parameter(Mandatory=$false)]
        [string]
        $StageName = 'trigger'
    )
    Begin {
        $Yaml = @"
stages:
  - $StageName
"@

        $Projects = @()
        if ([string]::IsNullOrWhiteSpace($Strategy)) {
            $StrategyYaml = ''
        } else {
            $StrategyYaml = "`n strategy: $Strategy"
        }
    }
    Process {
        foreach ($Object in $InputObject) {
            if ($Projects.Contains($Object.ProjectId)) {
                continue
            }
            $Projects += $Object.ProjectId
            $Yaml += @"


$($Object.Name):
  stage: $StageName
  trigger:
    project: $($Object.PathWithNamespace)$StrategyYaml
"@

        }
    }
    End {
        $Yaml
    }
}

function Move-GitlabProject {
    [Alias("Transfer-GitlabProject")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$true)]
        [string]
        $DestinationGroup,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $SourceProject = Get-GitlabProject -ProjectId $ProjectId
    $Group = Get-GitlabGroup -GroupId $DestinationGroup

    Invoke-GitlabApi PUT "projects/$($SourceProject.Id)/transfer" @{
        namespace = $Group.Id
    } -SiteUrl $SiteUrl -WhatIf:$WhatIf | New-WrapperObject 'Gitlab.Project'
}

function Rename-GitlabProject {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)]
        [string]
        $ProjectId = '.',

        [Parameter(Position=0, Mandatory=$true)]
        [string]
        $NewName,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    Update-GitlabProject -ProjectId $ProjectId -Name $NewName -Path $NewName -SiteUrl $SiteUrl -WhatIf:$WhatIf
}

function Copy-GitlabProject {
    [Alias("Fork-GitlabProject")]
    [CmdletBinding()]
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string]
        $ProjectId,

        [Parameter(Position=1, Mandatory=$true)]
        [string]
        $DestinationGroup,

        [bool]
        [Parameter(Mandatory=$false)]
        $PreserveForkRelationship = $true,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $SourceProject = Get-GitlabProject -ProjectId $ProjectId
    $Group = Get-GitlabGroup -GroupId $DestinationGroup

    if ($WhatIf) {
        Write-Host "WhatIf: forking '$($SourceProject.Name)' (project id: $($SourceProject.Id)) to '$($Group.FullPath)' (group id: $($Group.Id))"
    } else {
        $NewProject = Invoke-GitlabApi POST "projects/$($SourceProject.Id)/fork" @{
            namespace_id = $Group.Id
        } -SiteUrl $SiteUrl -WhatIf:$WhatIf
    }

    if (-not $PreserveForkRelationship) {
        Invoke-GitlabApi DELETE "projects/$($NewProject.id)/fork" -SiteUrl $SiteUrl -WhatIf:$WhatIf
    }
}
function New-GitlabProject {
    [CmdletBinding()]
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string]
        $ProjectName,

        [Parameter(Position=1, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Alias('GroupId')]
        [string]
        $DestinationGroup,

        [Parameter(Mandatory=$false)]
        [ValidateSet('private', 'internal', 'public')]
        [string]
        $Visibility = 'internal',

        [switch]
        [Parameter(Mandatory=$false)]
        $CloneNow,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Group = Get-GitlabGroup -GroupId $DestinationGroup
    if(-not $Group) {
        throw "DestinationGroup '$DestinationGroup' not found"
    }

    $Project = Invoke-GitlabApi POST "projects" @{
        name = $ProjectName
        namespace_id = $Group.Id
        visibility = $Visibility
    } -SiteUrl $SiteUrl -WhatIf:$WhatIf |
        New-WrapperObject 'Gitlab.Project'
    
    if ($CloneNow) {
        git clone $Project.SshUrlToRepo
        Set-Location $ProjectName
    } else {
        $Project
    }
}

# https://docs.gitlab.com/ee/api/projects.html#edit-project
function Update-GitlabProject {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$false)]
        [ValidateSet('private', 'internal', 'public')]
        [string]
        $Visibility,

        [Parameter(Mandatory=$false)]
        [string]
        $Name,

        [Parameter(Mandatory=$false)]
        [string]
        $Path,

        [Parameter(Mandatory=$false)]
        [string]
        $DefaultBranch,

        [Parameter(Mandatory=$false)]
        [string []]
        $Topics,

        [Parameter(Mandatory=$false)]
        [ValidateSet('fetch', 'clone')]
        [string]
        $BuildGitStrategy,

        [Parameter(Mandatory=$false)]
        [uint]
        $CiDefaultGitDepth,

        [Parameter(Mandatory=$false)]
        [bool]
        $CiForwardDeployment,

        [Parameter(Mandatory=$false)]
        [ValidateSet('disabled', 'private', 'enabled')]
        [string]
        $RepositoryAccessLevel,

        [Parameter(Mandatory=$false)]
        [ValidateSet('disabled', 'private', 'enabled')]
        [string]
        $BuildsAccessLevel,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Project = Get-GitlabProject $ProjectId

    $Query = @{}

    if($PSBoundParameters.ContainsKey("CiForwardDeployment")){
        $Query.ci_forward_deployment_enabled = $CiForwardDeployment.ToString().ToLower()
    }
    if ($BuildGitStrategy) {
        $Query.build_git_strategy = $BuildGitStrategy
    }
    if ($CiDefaultGitDepth) {
        $Query.ci_default_git_depth = $CiDefaultGitDepth
    }
    if ($Visibility) {
        $Query.visibility = $Visibility
    }
    if ($Name) {
        $Query.name = $Name
    }
    if ($Path) {
        $Query.path = $Path
    }
    if ($DefaultBranch) {
        $Query.default_branch = $DefaultBranch
    }
    if ($Topics) {
        $Query.topics = $Topics -join ','
    }
    if ($RepositoryAccessLevel) {
        $Query.repository_access_level = $RepositoryAccessLevel
    }
    if ($BuildsAccessLevel) {
        $Query.builds_access_level = $BuildsAccessLevel
    }

    Invoke-GitlabApi PUT "projects/$($Project.Id)" $Query -SiteUrl $SiteUrl -WhatIf:$WhatIf |
        New-WrapperObject 'Gitlab.Project'
}

# https://docs.gitlab.com/ee/api/projects.html#archive-a-project
function Invoke-GitlabProjectArchival {
    [Alias('Archive-GitlabProject')]
    [CmdletBinding(DefaultParameterSetName='ByProjectId')]
    param (
        [Parameter(ValueFromPipeline=$true, ParameterSetName='ByProject')]
        $Project,

        [Parameter(Mandatory=$false, ParameterSetName='ByProjectId')]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    Process {

        if ($PSCmdlet.ParameterSetName -eq 'ByProjectId') {
            $Project = $(Get-GitlabProject -ProjectId $ProjectId)
        }

        Invoke-GitlabApi POST "projects/$($Project.Id)/archive" -SiteUrl $SiteUrl -WhatIf:$WhatIf |
            New-WrapperObject 'Gitlab.Project'
    }
}

# https://docs.gitlab.com/ee/api/projects.html#unarchive-a-project
function Invoke-GitlabProjectUnarchival {
    [Alias('Unarchive-GitlabProject')]
    [CmdletBinding(DefaultParameterSetName='ByProjectId')]
    param (
        [Parameter(ValueFromPipeline=$true, ParameterSetName='ByProject')]
        $Project,

        [Parameter(Position=0)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    Process {
        if ($PSCmdlet.ParameterSetName -eq 'ByProjectId') {
            $Project = $(Get-GitlabProject -ProjectId $ProjectId)
        }
    
        Invoke-GitlabApi POST "projects/$($Project.Id)/unarchive" -SiteUrl $SiteUrl -WhatIf:$WhatIf |
            New-WrapperObject 'Gitlab.Project'
    }
}

# https://docs.gitlab.com/ee/api/project_level_variables.html#list-project-variables
function Get-GitlabProjectVariable {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)]
        [string]
        $ProjectId = '.',

        [Parameter(Position=0, Mandatory=$false)]
        [string]
        $Key,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Project = Get-GitlabProject $ProjectId

    if ($Key) {
        Invoke-GitlabApi GET "projects/$($Project.Id)/variables/$Key" -SiteUrl $SiteUrl -WhatIf:$WhatIf | New-WrapperObject 'Gitlab.Variable'
    }
    else {
        Invoke-GitlabApi GET "projects/$($Project.Id)/variables" -SiteUrl $SiteUrl -WhatIf:$WhatIf | New-WrapperObject 'Gitlab.Variable'
    }
}

# https://docs.gitlab.com/ee/api/project_level_variables.html#update-variable
function Set-GitlabProjectVariable {

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
        [string]
        $Key,

        [Parameter(Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)]
        [string]
        $Value,

        [switch]
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)]
        $Protect,

        [switch]
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)]
        $Mask,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl
    )

    $Query = @{
        value = $Value
    }
    if ($Protect) {
        $Query.protected = 'true'
    }
    if ($Mask) {
        $Query.masked = 'true'
    }

    $ProjectId = $(Get-GitlabProject $ProjectId).Id

    try {
        Get-GitlabProjectVariable -ProjectId $ProjectId -Key $Key | Out-Null
        $IsExistingVariable = $True
    }
    catch {
        $IsExistingVariable = $False
    }

    if ($PSCmdlet.ShouldProcess($ProjectId, "set $($IsExistingVariable ? 'existing' : 'new') project variable to '$Value'")) {
        if ($IsExistingVariable) {
            Invoke-GitlabApi PUT "projects/$($ProjectId)/variables/$Key" -Query $Query -SiteUrl $SiteUrl -WhatIf:$WhatIfPreference | New-WrapperObject 'Gitlab.Variable'
        }
        else {
            $Query.key = $Key
            Invoke-GitlabApi POST "projects/$($ProjectId)/variables" -Query $Query -SiteUrl $SiteUrl -WhatIf:$WhatIfPreference | New-WrapperObject 'Gitlab.Variable'
        }
    }
}

# https://docs.gitlab.com/ee/api/project_level_variables.html#list-project-variables
function Remove-GitlabProjectVariable {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$true, Position=0)]
        [string]
        $Key,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Project = Get-GitlabProject $ProjectId

    Invoke-GitlabApi DELETE "projects/$($Project.Id)/variables/$Key" -SiteUrl $SiteUrl -WhatIf:$WhatIf | Out-Null
}


function Rename-GitlabProjectDefaultBranch {
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string]
        $NewDefaultBranch,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Project = Get-GitlabProject -ProjectId '.'
    if (-not $Project) {
        throw "This cmdlet requires being run from within a gitlab project"
    }

    if ($Project.DefaultBranch -ieq $NewDefaultBranch) {
        throw "Default branch for $($Project.Name) is already $($Project.DefaultBranch)"
    }
    $OldDefaultBranch = $Project.DefaultBranch

    if ($WhatIf) {
        Write-Host "WhatIf: would change default branch for $($Project.PathWithNamespace) from $OldDefaultBranch to $NewDefaultBranch"
        return
    }

    git checkout $OldDefaultBranch | Out-Null
    git pull -p | Out-Null
    git branch -m $OldDefaultBranch $NewDefaultBranch | Out-Null
    git push -u origin $NewDefaultBranch -o ci.skip | Out-Null
    Update-GitlabProject -DefaultBranch $NewDefaultBranch -SiteUrl $SiteUrl -WhatIf:$WhatIf | Out-Null
    try {
        UnProtect-GitlabBranch -Name $OldDefaultBranch -SiteUrl $SiteUrl -WhatIf:$WhatIf | Out-Null
    }
    catch {}
    Protect-GitlabBranch -Name $NewDefaultBranch -SiteUrl $SiteUrl -WhatIf:$WhatIf | Out-Null
    git push --delete origin $OldDefaultBranch | Out-Null
    git remote set-head origin -a | Out-Null

    Get-GitlabProject -ProjectId $Project.Id
}

function Get-GitlabProjectEvent {

    [CmdletBinding()]
    param (
        [Parameter(Position=0, Mandatory=$false)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$False)]
        [ValidateScript({ValidateGitlabDateFormat $_})]
        [string]
        $Before,

        [Parameter(Mandatory=$False)]
        [ValidateScript({ValidateGitlabDateFormat $_})]
        [string]
        $After,

        [Parameter(Mandatory=$False)]
        [ValidateSet("asc","desc")]
        [string]
        $Sort,

        [Parameter(Mandatory=$false)]
        [int]
        $MaxPages = 1,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Project = Get-GitLabProject $ProjectId

    $Query = @{}
    if($Before) {
        $Query.before = $Before
    }
    if($After) {
        $Query.after = $After
    }
    if($Sort) {
        $Query.sort = $Sort
    }

    Invoke-GitlabApi GET "projects/$($Project.Id)/events" `
        -Query $Query -MaxPages $MaxPages -SiteUrl $SiteUrl -WhatIf:$WhatIf | 
        New-WrapperObject 'Gitlab.Event'
}