Projects.psm1

function Get-GitlabProject {

    [CmdletBinding(DefaultParameterSetName='ById')]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(Position=0, ParameterSetName='ById', ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

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

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

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

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

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

        [Parameter()]
        [string]
        $Select,

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

        [Parameter()]
        [uint]
        $MaxPages,

        [switch]
        [Parameter()]
        $All,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $ProjectId = $ProjectId.TrimEnd('/')

    $MaxPages = Resolve-GitlabMaxPages -MaxPages:$MaxPages -All:$All -Recurse:$Recurse
    $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)"
        }
        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 |
                Where-Object { $($_.path_with_namespace).StartsWith($Group.FullPath) } |
                Sort-Object -Property 'Name'
        }
        ByUser {
            if ($Mine) {
                if ($UserId) {
                    Write-Warning "Ignoring '-UserId $UserId' parameter since -Mine was also provided"
                }
                $UserId = Get-GitlabUser -Me | Select-Object -ExpandProperty Username
            }
            # https://docs.gitlab.com/ee/api/projects.html#list-user-projects
            $Projects = Invoke-GitlabApi GET "users/$UserId/projects"
        }
        ByTopics {
            $Projects = Invoke-GitlabApi GET "projects" -Query @{
                topic = $Topics -join ','
            } -MaxPages $MaxPages
        }
        ByUrl {
            $Match = $Url | Get-GitlabResourceFromUrl
            if ($Match) {
                Get-GitlabProject -ProjectId $Match.ProjectId
            } else {
                throw "Url didn't match expected format"
            }
        }
    }

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

function ConvertTo-GitlabTriggerYaml {
    [CmdletBinding()]
    [OutputType([string])]
    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
    }
}

# https://docs.gitlab.com/ee/api/projects.html#transfer-a-project-to-a-new-namespace
function Move-GitlabProject {
    [Alias("Transfer-GitlabProject")]
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

        [Parameter()]
        [string]
        $SiteUrl
    )

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

    if ($PSCmdlet.ShouldProcess("group $($Group.FullName)", "transfer '$($SourceProject.PathWithNamespace)'")) {
        Invoke-GitlabApi PUT "projects/$($SourceProject.Id)/transfer" @{
            namespace = $Group.Id
        } -WhatIf:$WhatIfPreference | New-GitlabObject 'Gitlab.Project'
    }
}

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

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

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

    if ($PSCmdlet.ShouldProcess("$ProjectId", "rename to '$NewName'")) {
        Update-GitlabProject -ProjectId $ProjectId -Name $NewName -Path $NewName
    }
}

# https://docs.gitlab.com/ee/api/projects.html#fork-project
function Copy-GitlabProject {
    [Alias("Fork-GitlabProject")]
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $ProjectId = '.',

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

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


        # https://docs.gitlab.com/ee/api/projects.html#delete-an-existing-forked-from-relationship
        [TrueOrFalse()][bool]
        [Parameter(Mandatory=$false)]
        $PreserveForkRelationship = $true,

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

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

    if ($PSCmdlet.ShouldProcess("$($Group.FullPath)", "$($PreserveForkRelationship ? "fork" : "copy") $($SourceProject.Path)")) {
        $NewProject = Invoke-GitlabApi POST "projects/$($SourceProject.Id)/fork" @{
            namespace_id           = $Group.Id
            name                   = $DestinationProjectName ?? $SourceProject.Name
            path                   = $DestinationProjectName ?? $SourceProject.Name
            mr_default_target_self = 'true'
        } -WhatIf:$WhatIfPreference

        if (-not $PreserveForkRelationship) {
            $NewProject | Remove-GitlabProjectForkRelationship
        }
    }
}

function Remove-GitlabProjectForkRelationship {
    [Alias("Remove-GitlabProjectFork")]
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    [OutputType([void])]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject -ProjectId $ProjectId
    if (-not $Project.ForkedFromProjectId) {
        throw "Project $($Project.PathWithNamespace) does not have a fork"
    }
    $ForkedFromProject = Get-GitlabProject -ProjectId $Project.ForkedFromProjectId

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "remove fork relationship to $($ForkedFromProject.PathWithNamespace)")) {
        # https://docs.gitlab.com/api/project_forks/#delete-a-fork-relationship-between-projects
        Invoke-GitlabApi DELETE "projects/$($ProjectId)/fork" | Out-Null
        Write-Host "Removed fork relationship from $($Project.PathWithNamespace) (was $($ForkedFromProject.PathWithNamespace))"
    }
}

function New-GitlabProject {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(Position=0, Mandatory)]
        [Alias('Name')]
        [string]
        $ProjectName,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName='Group')]
        [Alias('Group')]
        [string]
        $DestinationGroup,

        [Parameter(ParameterSetName = 'Personal')]
        [switch]
        $Personal,

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

        [Parameter()]
        [uint]
        $BuildTimeout = 0,

        [switch]
        [Parameter()]
        $CloneNow,

        [Parameter()]
        [string]
        $SiteUrl
    )

    if ($DestinationGroup) {
        $Group = Get-GitlabGroup -GroupId $DestinationGroup
        if(-not $Group) {
            throw "DestinationGroup '$DestinationGroup' not found"
        }
        $NamespaceId = $Group.Id
    }
    if ($Personal) {
        $NamespaceId = $null # defaults to current user
    }

    $Request = @{
        name = $ProjectName
        namespace_id = $NamespaceId
        visibility = $Visibility
    }
    if ($BuildTimeout -gt 0) {
        $Request.build_timeout = $BuildTimeout
    }

    if ($PSCmdlet.ShouldProcess($NamespaceId, "create new project '$ProjectName' $($Request | ConvertTo-Json)" )) {
        # https://docs.gitlab.com/ee/api/projects.html#create-project
        $Project = Invoke-GitlabApi POST "projects" -Body $Request | New-GitlabObject '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(SupportsShouldProcess)]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $DefaultBranch,

        [Parameter()]
        [string []]
        $Topics,

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

        [Parameter()]
        [uint]
        $CiDefaultGitDepth,

        [Parameter()]
        [TrueOrFalse()][bool]
        $CiForwardDeployment,

        [Parameter()]
        [uint]
        $BuildTimeout = 0,

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

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

        [Parameter()]
        [TrueOrFalse()][bool]
        $OnlyAllowMergeIfAllDiscussionsAreResolved,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject $ProjectId

    $Request = @{}

    if ($BuildsAccessLevel) {
        $Request.builds_access_level = $BuildsAccessLevel
    }
    if ($BuildGitStrategy) {
        $Request.build_git_strategy = $BuildGitStrategy
    }
    if ($BuildTimeout -gt 0) {
        $Request.build_timeout = $BuildTimeout
    }
    if ($CiDefaultGitDepth) {
        $Request.ci_default_git_depth = $CiDefaultGitDepth
    }
    if ($PSBoundParameters.ContainsKey('CiForwardDeployment')){
        $Request.ci_forward_deployment_enabled = $CiForwardDeployment
    }
    if ($DefaultBranch) {
        $Request.default_branch = $DefaultBranch
    }
    if ($Name) {
        $Request.name = $Name
    }
    if ($PSBoundParameters.ContainsKey('OnlyAllowMergeIfAllDiscussionsAreResolved')) {
        $Request.only_allow_merge_if_all_discussions_are_resolved = $OnlyAllowMergeIfAllDiscussionsAreResolved
    }
    if ($Path) {
        $Request.path = $Path
    }
    if ($RepositoryAccessLevel) {
        $Request.repository_access_level = $RepositoryAccessLevel
    }
    if($PSBoundParameters.ContainsKey('Topics')) {
        if ($Topics) {
            $Request.topics = $Topics -join ','
        } else {
            $Request.topics = ''
        }
    }
    if ($Visibility) {
        $Request.visibility = $Visibility
    }

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "update project ($($Request | ConvertTo-Json))")) {
        Invoke-GitlabApi PUT "projects/$($Project.Id)" -Body $Request |
            New-GitlabObject 'Gitlab.Project'
    }
}

function Invoke-GitlabProjectArchival {
    [Alias('Archive-GitlabProject')]
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject $ProjectId

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "archive")) {
        # https://docs.gitlab.com/ee/api/projects.html#archive-a-project
        Invoke-GitlabApi POST "projects/$($Project.Id)/archive" |
            New-GitlabObject 'Gitlab.Project'
    }
}

function Invoke-GitlabProjectUnarchival {
    [Alias('Unarchive-GitlabProject')]
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject $ProjectId

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "unarchive")) {
        # https://docs.gitlab.com/ee/api/projects.html#unarchive-a-project
        Invoke-GitlabApi POST "projects/$($Project.Id)/unarchive" |
            New-GitlabObject 'Gitlab.Project'
    }
}

function Get-GitlabProjectVariable {
    [CmdletBinding()]
    [OutputType('Gitlab.Variable')]
    param (
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [string]
        $ProjectId = '.',

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

        [Parameter()]
        [uint]
        $MaxPages,

        [switch]
        [Parameter()]
        $All,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $MaxPages = Resolve-GitlabMaxPages -MaxPages:$MaxPages -All:$All

    $ProjectId = Resolve-GitlabProjectId $ProjectId

    if ($Key) {
        # https://docs.gitlab.com/ee/api/project_level_variables.html#get-a-single-variable
        Invoke-GitlabApi GET "projects/$ProjectId/variables/$Key" | New-GitlabObject 'Gitlab.Variable'
    }
    else {
        # https://docs.gitlab.com/ee/api/project_level_variables.html#list-project-variables
        Invoke-GitlabApi GET "projects/$ProjectId/variables" -MaxPages $MaxPages | New-GitlabObject 'Gitlab.Variable'
    }
}

function Set-GitlabProjectVariable {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
    [OutputType('Gitlab.Variable')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

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

        [TrueOrFalse()][bool]
        [Parameter(ValueFromPipelineByPropertyName)]
        $Protect,

        [TrueOrFalse()][bool]
        [Parameter(ValueFromPipelineByPropertyName)]
        $Mask,

        [TrueOrFalse()][bool]
        [Parameter(ValueFromPipelineByPropertyName)]
        $ExpandVariables = $true,

        [switch]
        [Parameter()]
        $NoExpand,

        [Parameter()]
        [string]
        $SiteUrl
    )

    if ($NoExpand) {
        $ExpandVariables = $false
    }

    $Request = @{
        value = $Value
        raw   = -not $ExpandVariables
    }
    if ($PSBoundParameters.ContainsKey('Protect')) {
        $Request.protected = $Protect
    }
    if ($PSBoundParameters.ContainsKey('Mask')) {
        $Request.masked = $Mask
    }

    $Project = Get-GitlabProject $ProjectId

    try {
        Get-GitlabProjectVariable -ProjectId $($Project.Id) -Key $Key | Out-Null
        $IsExistingVariable = $true
    }
    catch {
        $IsExistingVariable = $false
    }

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "set $($IsExistingVariable ? 'existing' : 'new') project variable ($Key) $(($Request | ConvertTo-Json))")) {
        if ($IsExistingVariable) {
            # https://docs.gitlab.com/ee/api/project_level_variables.html#update-variable
            Invoke-GitlabApi PUT "projects/$($Project.Id)/variables/$Key" -Body $Request | New-GitlabObject 'Gitlab.Variable'
        }
        else {
            $Request.key = $Key
            # https://docs.gitlab.com/ee/api/project_level_variables.html#create-a-variable
            Invoke-GitlabApi POST "projects/$($Project.Id)/variables" -Body $Request | New-GitlabObject 'Gitlab.Variable'
        }
    }
}

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

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

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

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

    $Project = Get-GitlabProject $ProjectId

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "delete variable '$Key'")) {
        Invoke-GitlabApi DELETE "projects/$($Project.Id)/variables/$Key" | Out-Null
    }
}


function Rename-GitlabProjectDefaultBranch {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string]
        $NewDefaultBranch,

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

    $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 ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "change default branch from $OldDefaultBranch to $NewDefaultBranch")) {
        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 | Out-Null
        try {
            UnProtect-GitlabBranch -Name $OldDefaultBranch | Out-Null
        }
        catch {
            Write-Debug "UnProtect-GitlabBranch failed for '$OldDefaultBranch': $_"
        }
        Protect-GitlabBranch -Name $NewDefaultBranch | 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()]
    [OutputType('Gitlab.Event')]
    param (
        [Parameter(Position=0, Mandatory=$false)]
        [string]
        $ProjectId = '.',

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

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

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

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

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

    $ProjectId = Resolve-GitlabProjectId $ProjectId

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

    Invoke-GitlabApi GET "projects/$ProjectId/events" `
        -Query $Query -MaxPages $MaxPages -SiteUrl |
        New-GitlabObject 'Gitlab.Event'
}

function New-GitlabGroupToProjectShare {
    [CmdletBinding(SupportsShouldProcess)]
    [Alias('Share-GitlabProjectWithGroup')]
    [OutputType([void])]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $GroupId,

        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Access')]
        [string]
        [ValidateScript({Test-GitlabSettableAccessLevel $_})]
        $GroupAccess,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $AccessLiteral = Get-GitlabMemberAccessLevel $GroupAccess
    $Project       = Get-GitlabProject $ProjectId
    $Group         = Get-GitlabGroup $GroupId

    # https://docs.gitlab.com/api/projects/#share-a-project-with-a-group
    $Request = @{
        Method = 'POST'
        Path = "projects/$($Project.Id)/share"
        Body = @{
            group_id     = $Group.Id
            group_access = $AccessLiteral
        }
    }

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "share project with group ($($Request | ConvertTo-Json))")) {
        if (Invoke-GitlabApi @Request | Out-Null) {
            Write-Host "Successfully shared $($Project.PathWithNamespace) with $($Group.FullPath)"
        }
    }
}

function Remove-GitlabGroupToProjectShare {

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    [OutputType([void])]
    param (
        [Parameter(Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

        [Parameter()]
        [string]
        $SiteUrl
    )

    $ProjectId = Resolve-GitlabProjectId $ProjectId
    $GroupShare = Get-GitlabGroup $GroupId
    if ($PSCmdlet.ShouldProcess("project $ProjectId", "remove sharing with group '$($GroupShare.Name)'")) {
        # https://docs.gitlab.com/api/projects/#delete-a-shared-project-link-in-a-grou
        if (Invoke-GitlabApi DELETE "projects/$ProjectId/share/$($GroupShare.Id)" | Out-Null) {
            Write-Host "Removed sharing with $($GroupShare.Name) from project $ProjectId"
        }
    }
}

function Remove-GitlabProject {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    [OutputType([void])]
    param (
        [Parameter(Position=0, Mandatory, ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject -ProjectId $ProjectId

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "remove project")) {
        # https://docs.gitlab.com/ee/api/projects.html#delete-project
        Invoke-GitlabApi DELETE "projects/$($ProjectId)" | Out-Null
        Write-Host "Removed $($Project.PathWithNamespace)"
    }
}

function Add-GitlabProjectTopic {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

    $Project        = Get-GitlabProject $ProjectId
    $ExistingTopics = @($Project.Topics)
    $NewTopics      = $ExistingTopics + $Topic | Select-Object -Unique

    if ($NewTopics.Count -eq $ExistingTopics.Count) {
        Write-Verbose "No new topics to add to $($Project.PathWithNamespace)"
        return
    }

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "add topic ($($Topic -join ', '))")) {
        Update-GitlabProject -ProjectId $Project.Id -Topics $NewTopics
    }
}

function Remove-GitlabProjectTopic {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    [OutputType('Gitlab.Project')]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

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

    $Project        = Get-GitlabProject $ProjectId
    $ExistingTopics = @($Project.Topics)
    $NewTopics      = $ExistingTopics | Where-Object { $Topic -notcontains $_ } | Select-Object -Unique

    if ($NewTopics.Count -eq $ExistingTopics.Count) {
        Write-Verbose "No topics to remove from $($Project.PathWithNamespace)"
        return
    }

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "remove topic ($($Topic -join ', '))")) {
        Update-GitlabProject -ProjectId $Project.Id -Topics $NewTopics
    }
}