Utilities.psm1
# Inspired by https://gist.github.com/awakecoding/acc626741704e8885da8892b0ac6ce64 function ConvertTo-PascalCase { param( [Parameter(Position=0, ValueFromPipeline=$true)] [string] $Value ) # https://devblogs.microsoft.com/oldnewthing/20190909-00/?p=102844 return [regex]::replace($Value.ToLower(), '(^|_)(.)', { $args[0].Groups[2].Value.ToUpper()}) } function ConvertTo-SnakeCase { param( [Parameter(Position=0, ValueFromPipeline=$true)] $InputObject ) Process { foreach ($Value in $InputObject) { if ($Value -is [string]) { return [regex]::replace($Value, '(?<=.)(?=[A-Z])', '_').ToLower() } if ($Value -is [hashtable]) { $Value.Keys.Clone() | ForEach-Object { $OriginalValue = $Value[$_] $Value.Remove($_) $Value[$($_ | ConvertTo-SnakeCase)] = $OriginalValue } $Value } } } } function ConvertTo-UrlEncoded { param ( [Parameter(Position=0, ValueFromPipeline=$true)] [string] $Value ) [System.Net.WebUtility]::UrlEncode($Value) } function Invoke-GitlabApi { param( [Parameter(Position=0, Mandatory)] [Alias('Method')] [string] $HttpMethod, [Parameter(Position=1, Mandatory)] [string] $Path, [Parameter(Position=2)] [hashtable] $Query = @{}, [Parameter()] [hashtable] $Body = @{}, [Parameter()] [uint] $MaxPages = 1, [Parameter()] [string] $Api = 'v4', [Parameter()] [string] $SiteUrl, [Parameter()] [string] $AccessToken, [Parameter()] [string] $ProxyUrl, [Parameter()] [switch] $WhatIf ) if ($MaxPages -gt [int]::MaxValue) { $MaxPages = [int]::MaxValue } if ($SiteUrl) { Write-Debug "Attempting to resolve site using $SiteUrl" $Site = Get-GitlabConfiguration | Select-Object -ExpandProperty Sites | Where-Object Url -eq $SiteUrl } if (-not $Site) { Write-Debug "Attempting to resolve site using local git context" $Site = Get-GitlabConfiguration | Select-Object -ExpandProperty Sites | Where-Object Url -eq $(Get-LocalGitContext).Site } if (-not $Site -or $Site -is [array]) { $Site = Get-DefaultGitlabSite Write-Debug "Using default site ($($Site.Url))" } $GitlabUrl = $Site.Url $Headers = @{ Accept = 'application/json' } if ($global:GitlabUserImpersonationSession) { Write-Verbose "Impersonating API call as '$($global:GitlabUserImpersonationSession.Username)'..." $AccessToken = $global:GitlabUserImpersonationSession.Token } elseif (-not $AccessToken) { $AccessToken = $Site.AccessToken } if ($AccessToken) { $Headers.Authorization = "Bearer $AccessToken" } else { throw "GitlabCli: environment not configured`nSee https://github.com/chris-peterson/pwsh-gitlab#getting-started for details" } if (-not $GitlabUrl.StartsWith('http')) { $GitlabUrl = "https://$GitlabUrl" } $GitlabUrl = $GitlabUrl.TrimEnd('/') $SerializedQuery = '' $Delimiter = '?' if($Query.Count -gt 0) { foreach($Name in $Query.Keys) { $Value = $Query[$Name] if ($Value) { $SerializedQuery += $Delimiter $SerializedQuery += "$Name=" $SerializedQuery += [System.Net.WebUtility]::UrlEncode($Value) $Delimiter = '&' } } } $RestMethodParams = @{ Method = $HttpMethod Uri = "$GitlabUrl/api/$Api/$Path$SerializedQuery" Header = $Headers } $Proxy = $ProxyUrl ?? $Site.ProxyUrl if (-not [string]::IsNullOrWhiteSpace($Proxy)) { Write-Verbose "Using proxy $Proxy..." } if($MaxPages -gt 1) { $RestMethodParams.FollowRelLink = $true $RestMethodParams.MaximumFollowRelLink = $MaxPages } if ($Body.Count -gt 0) { $RestMethodParams.ContentType = 'application/json' $RestMethodParams.Body = $Body | ConvertTo-Json } $HostOutput = "$($RestMethodParams | ConvertTo-Json)" if($WhatIf) { Write-Host "WhatIf: $HostOutput" } else { Write-Verbose "Request: $HostOutput" $Result = Invoke-RestMethod @RestMethodParams Write-Verbose "Response: $($Result | ConvertTo-Json -Depth 10)" if($MaxPages -gt 1) { # Unwrap pagination container $Result | ForEach-Object { Write-Output $_ } } else { Write-Output $Result } } } function Add-AliasedProperty { param ( [PSCustomObject] [Parameter(Mandatory=$true, Position = 0)] $On, [string] [Parameter(Mandatory=$true)] $From, [string] [Parameter(Mandatory=$true)] $To ) if ($null -ne $On.$To -and -NOT (Get-Member -Name $On.$To -InputObject $On)) { $On | Add-Member -MemberType NoteProperty -Name $From -Value $On.$To } } function New-WrapperObject { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] $InputObject, [Parameter(Position=0, Mandatory=$false)] [string] $DisplayType ) Begin{} Process { foreach ($item in $InputObject) { $Wrapper = New-Object PSObject $item.PSObject.Properties | Sort-Object Name | ForEach-Object { $Wrapper | Add-Member -MemberType NoteProperty -Name $($_.Name | ConvertTo-PascalCase) -Value $_.Value } # aliases for common property names Add-AliasedProperty -On $Wrapper -From 'Url' -To 'WebUrl' Add-AliasedProperty -On $Wrapper -From 'Url' -To 'TargetUrl' if ($DisplayType) { $Wrapper.PSTypeNames.Insert(0, $DisplayType) $IdentityPropertyName = $global:GitlabIdentityPropertyNameExemptions[$DisplayType] if ($IdentityPropertyName -eq $null) { $IdentityPropertyName = 'Iid' # default for anything that isn't explicitly mapped } if ($IdentityPropertyName -ne '') { if ($Wrapper.$IdentityPropertyName) { $TypeShortName = $DisplayType.Split('.') | Select-Object -Last 1 Add-AliasedProperty -On $Wrapper -From "$($TypeShortName)Id" -To $IdentityPropertyName } else { Write-Warning "$DisplayType does not have an identity field" } } } Write-Output $Wrapper } } End{} } function Open-InBrowser { [CmdletBinding()] [Alias('go')] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] $InputObject ) Process { if (-not $InputObject) { # do nothing } elseif ($InputObject -is [string]) { Start-Process $InputObject } elseif ($InputObject.Url -and $InputObject.Url -is [string]) { Start-Process $InputObject.Url } elseif ($InputObject.WebUrl -and $InputObject.WebUrl -is [string]) { Start-Process $InputObject.WebUrl } } } function ValidateGitlabDateFormat { param( [Parameter(Mandatory=$true,Position=0)] [string] $DateString ) if($DateString -match "\d\d\d\d-\d\d-\d\d") { $true } else { throw "$DateString is invalid. The date format expected is YYYY-MM-DD" } } function Get-FilteredObject { param ( [Parameter(ValueFromPipeline=$true, Mandatory=$true)] $InputObject, [Parameter(Position=0, Mandatory=$false)] [string] $Select = '*' ) Begin {} Process { foreach ($Object in $InputObject) { if (($Select -eq '*') -or (-not $Select)) { $Object } elseif ($Select.Contains(',')) { $Object | Select-Object $($Select -split ',') } else { $Object | Select-Object -ExpandProperty $Select } } } End {} } function Get-GitlabVersion { param( [Parameter(Mandatory=$false)] [string] $Select = 'Version', [Parameter(Mandatory=$false)] [string] $SiteUrl, [switch] [Parameter(Mandatory=$false)] $WhatIf ) Invoke-GitlabApi GET 'version' -SiteUrl $SiteUrl -WhatIf:$WhatIf | New-WrapperObject | Get-FilteredObject $Select } function Get-GitlabResourceFromUrl { param( [Parameter(Mandatory, ValueFromPipeline)] [string] $Url ) $Match = $null Get-GitlabConfiguration | Select-Object -Expand sites | Select-Object -Expand Url | ForEach-Object { if ($Url -match "$_/(?<ProjectId>.*)/-/(?<ResourceType>[a-zA-Z_]+)/(?<ResourceId>\d+)") { $Match = [PSCustomObject]@{ ProjectId = $Matches.ProjectId ResourceType = $Matches.ResourceType ResourceId = $Matches.ResourceId } } } if (-not $Match) { throw "Could not extract a GitLab resource from '$Url'" } $Match } $global:GitlabDefaultMaxPages = 10 # Helper function for consistency of paging parameters # Add these parameters to your cmdlet <# [Parameter()] [uint] $MaxPages, [switch] [Parameter()] $All, #> # then call <# $MaxPages = Get-GitlabMaxPages -MaxPages:$MaxPages -All:$All #> function Get-GitlabMaxPages { param ( [Parameter()] [uint] $MaxPages, [switch] [Parameter()] $All ) if ($MaxPages -eq 0) { $MaxPages = $global:GitlabDefaultMaxPages } if ($All) { if ($MaxPages -ne $global:GitlabDefaultMaxPages) { Write-Warning -Message "Ignoring -MaxPages in favor of -All" } $MaxPages = [uint]::MaxValue } Write-Debug "MaxPages: $MaxPages" $MaxPages } |