diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..301416b6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +[*] + +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e9600a4a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "[powershell]": { + "files.trimTrailingWhitespace": true + }, + "powershell.codeFormatting.openBraceOnSameLine": false, + "powershell.codeFormatting.alignPropertyValuePairs": false +} \ No newline at end of file diff --git a/GitHubComments.ps1 b/GitHubComments.ps1 index cb77ef70..d25a1a9a 100644 --- a/GitHubComments.ps1 +++ b/GitHubComments.ps1 @@ -181,7 +181,7 @@ function Get-GitHubComment 'UriFragment' = $uriFragment 'Description' = $description 'AccessToken' = $AccessToken - 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $squirrelAcceptHeader) + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $squirrelAcceptHeader) 'TelemetryEventName' = $MyInvocation.MyCommand.Name 'TelemetryProperties' = $telemetryProperties 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus) @@ -292,7 +292,7 @@ function New-GitHubComment 'Method' = 'Post' 'Description' = "Creating comment under issue $Issue for $RepositoryName" 'AccessToken' = $AccessToken - 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $squirrelAcceptHeader) + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $squirrelAcceptHeader) 'TelemetryEventName' = $MyInvocation.MyCommand.Name 'TelemetryProperties' = $telemetryProperties 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus) @@ -403,7 +403,7 @@ function Set-GitHubComment 'Method' = 'Patch' 'Description' = "Update comment $CommentID for $RepositoryName" 'AccessToken' = $AccessToken - 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $squirrelAcceptHeader) + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $squirrelAcceptHeader) 'TelemetryEventName' = $MyInvocation.MyCommand.Name 'TelemetryProperties' = $telemetryProperties 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus) diff --git a/GitHubContents.ps1 b/GitHubContents.ps1 new file mode 100644 index 00000000..6bf56224 --- /dev/null +++ b/GitHubContents.ps1 @@ -0,0 +1,145 @@ +function Get-GitHubContent +{ + <# + .SYNOPSIS + Retrieve the contents of a file or directory in a repository on GitHub. + + .DESCRIPTION + Retrieve content from files on GitHub. + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER Path + The file path for which to retrieve contents + + .PARAMETER MediaType + The format in which the API will return the body of the issue. + Object - Return a json object representation a file or folder. This is the default if you do not pass any specific media type. + Raw - Return the raw contents of a file. + Html - For markup files such as Markdown or AsciiDoc, you can retrieve the rendered HTML using the Html media type. + + .PARAMETER ResultAsString + If this switch is specified and the MediaType is either Raw or Html then the resulting bytes will be decoded the result will be + returned as a string instead of bytes. If the MediaType is Object, then an additional property on the object is returned 'contentAsString' + which will be the decoded base64 result as a string. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .EXAMPLE + Get-GitHubContent -OwnerName microsoft -RepositoryName PowerShellForGitHub -Path README.md -MediaType Html + + Get the Html output for the README.md file + + .EXAMPLE + Get-GitHubContent -OwnerName microsoft -RepositoryName PowerShellForGitHub -Path LICENSE + + Get the Binary file output for the LICENSE file + + .EXAMPLE + Get-GitHubContent -OwnerName microsoft -RepositoryName PowerShellForGitHub -Path Tests + + List the files within the "Tests" path of the repository +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParametersetName = 'Elements')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] + param( + [Parameter(Mandatory, ParameterSetName = 'Elements')] + [string] $OwnerName, + + [Parameter(Mandatory, ParameterSetName = 'Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ParameterSetName='Uri')] + [string] $Uri, + + [string] $Path, + + [ValidateSet('Raw', 'Html', 'Object')] + [string] $MediaType = 'Object', + + [switch] $ResultAsString, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements -DisableValidation + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $description = [String]::Empty + + $uriFragment = "/repos/$OwnerName/$RepositoryName/contents" + + if ($PSBoundParameters.ContainsKey('Path')) + { + $Path = $Path.TrimStart("\", "/") + $uriFragment += "/$Path" + $description = "Getting content for $Path in $RepositoryName" + } + else + { + $description = "Getting all content for in $RepositoryName" + } + + $params = @{ + 'UriFragment' = $uriFragment + 'Description' = $description + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType) + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + $result = Invoke-GHRestMethodMultipleResult @params + + if ($ResultAsString) + { + if ($MediaType -eq 'Raw' -or $MediaType -eq 'Html') + { + # Decode bytes to string + $result = [System.Text.Encoding]::UTF8.GetString($result) + } + elseif ($MediaType -eq 'Object') + { + # Convert from base64 + $decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($result.content)) + Add-Member -InputObject $result -NotePropertyName "contentAsString" -NotePropertyValue $decoded + } + } + + return $result +} diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 index ea61d95d..dc0beff1 100644 --- a/GitHubCore.ps1 +++ b/GitHubCore.ps1 @@ -127,7 +127,7 @@ function Invoke-GHRestMethod # be coming from the Location header in a previous response. Either way, we don't want there # to be a leading "/" or trailing '/' if ($UriFragment.StartsWith('/')) { $UriFragment = $UriFragment.Substring(1) } - if ($UriFragment.EndsWIth('/')) { $UriFragment = $UriFragment.Substring(0, $UriFragment.Length - 1) } + if ($UriFragment.EndsWith('/')) { $UriFragment = $UriFragment.Substring(0, $UriFragment.Length - 1) } if ([String]::IsNullOrEmpty($Description)) { @@ -645,7 +645,8 @@ function Invoke-GHRestMethodMultipleResult try { - do { + do + { $params = @{ 'UriFragment' = $nextLink 'Method' = 'Get' @@ -949,6 +950,10 @@ function Get-MediaAcceptHeader Text - Return a text only representation of the markdown body. Response will include body_text. Html - Return HTML rendered from the body's markdown. Response will include body_html. Full - Return raw, text and HTML representations. Response will include body, body_text, and body_html. + Object - Return a json object representation a file or folder. + + .PARAMETER AsJson + If this switch is specified as +json value is appended to the MediaType header. .PARAMETER AcceptHeader The accept header that should be included with the MediaType accept header. @@ -960,16 +965,24 @@ function Get-MediaAcceptHeader #> [CmdletBinding()] param( - [ValidateSet('Raw', 'Text', 'Html', 'Full')] + [ValidateSet('Raw', 'Text', 'Html', 'Full', 'Object')] [string] $MediaType = 'Raw', - [Parameter(Mandatory)] + [switch] $AsJson, + [string] $AcceptHeader ) - $acceptHeaders = @( - $AcceptHeader, - "application/vnd.github.$mediaTypeVersion.$($MediaType.ToLower())+json") + $resultHeaders = "application/vnd.github.$mediaTypeVersion.$($MediaType.ToLower())" + if ($AsJson) + { + $resultHeaders = $resultHeaders + "+json" + } + + if (-not [String]::IsNullOrEmpty($AcceptHeader)) + { + $resultHeaders = "$AcceptHeader,$resultHeaders" + } - return ($acceptHeaders -join ',') + return $resultHeaders } diff --git a/GitHubIssues.ps1 b/GitHubIssues.ps1 index d9105f30..60553f43 100644 --- a/GitHubIssues.ps1 +++ b/GitHubIssues.ps1 @@ -315,7 +315,7 @@ function Get-GitHubIssue $params = @{ 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') 'Description' = $description - 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $symmetraAcceptHeader) + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $symmetraAcceptHeader) 'AccessToken' = $AccessToken 'TelemetryEventName' = $MyInvocation.MyCommand.Name 'TelemetryProperties' = $telemetryProperties @@ -543,7 +543,7 @@ function New-GitHubIssue 'Body' = (ConvertTo-Json -InputObject $hashBody) 'Method' = 'Post' 'Description' = "Creating new Issue ""$Title"" on $RepositoryName" - 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $symmetraAcceptHeader) + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $symmetraAcceptHeader) 'AccessToken' = $AccessToken 'TelemetryEventName' = $MyInvocation.MyCommand.Name 'TelemetryProperties' = $telemetryProperties @@ -694,7 +694,7 @@ function Update-GitHubIssue 'Body' = (ConvertTo-Json -InputObject $hashBody) 'Method' = 'Patch' 'Description' = "Updating Issue #$Issue on $RepositoryName" - 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AcceptHeader $symmetraAcceptHeader) + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson -AcceptHeader $symmetraAcceptHeader) 'AccessToken' = $AccessToken 'TelemetryEventName' = $MyInvocation.MyCommand.Name 'TelemetryProperties' = $telemetryProperties diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index 31c82786..7d40ceb1 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -25,6 +25,7 @@ 'GitHubBranches.ps1', 'GitHubCore.ps1', 'GitHubComments.ps1', + 'GitHubContents.ps1', 'GitHubEvents.ps1', 'GitHubIssues.ps1', 'GitHubLabels.ps1', @@ -55,6 +56,7 @@ 'Get-GitHubCodeOfConduct', 'Get-GitHubComment', 'Get-GitHubConfiguration', + 'Get-GitHubContent', 'Get-GitHubEmoji', 'Get-GitHubEvent', 'Get-GitHubGitIgnore', diff --git a/Tests/GitHubContents.tests.ps1 b/Tests/GitHubContents.tests.ps1 new file mode 100644 index 00000000..554ce61b --- /dev/null +++ b/Tests/GitHubContents.tests.ps1 @@ -0,0 +1,176 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.Synopsis + Tests for GitHubContents.ps1 module +#> + +# This is common test code setup logic for all Pester test files +$moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +. (Join-Path -Path $moduleRootPath -ChildPath 'Tests\Common.ps1') + +try +{ + # Define Script-scoped, readonly, hidden variables. + @{ + repoGuid = [Guid]::NewGuid().Guid + readmeFileName = "README.md" + }.GetEnumerator() | ForEach-Object { + Set-Variable -Force -Scope Script -Option ReadOnly -Visibility Private -Name $_.Key -Value $_.Value + } + + # Need two separate blocks to set constants because we need to reference a constant from the first block in this block. + @{ + htmlOutput = "
" + rawOutput = "# $repoGuid" + }.GetEnumerator() | ForEach-Object { + Set-Variable -Force -Scope Script -Option ReadOnly -Visibility Private -Name $_.Key -Value $_.Value + } + + Describe 'Getting file and folder content' { + # AutoInit will create a readme with the GUID of the repo name + $repo = New-GitHubRepository -RepositoryName ($repoGuid) -AutoInit + + Context 'For getting folder contents' { + + $folderOutput = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name + + It "Should have the expected name" { + $folderOutput.name | Should be "" + } + It "Should have the expected path" { + $folderOutput.path | Should be "" + } + It "Should have the expected type" { + $folderOutput.type | Should be "dir" + } + It "Should have the expected entries" { + $folderOutput.entries.length | Should be 1 + } + It "Should have the expected entry data" { + $folderOutput.entries[0].name | Should be $readmeFileName + $folderOutput.entries[0].path | Should be $readmeFileName + } + } + + Context 'For getting folder contents via URL' { + + $folderOutput = Get-GitHubContent -Uri "https://github.com/$($script:ownerName)/$($repo.name)" + + It "Should have the expected name" { + $folderOutput.name | Should be "" + } + It "Should have the expected path" { + $folderOutput.path | Should be "" + } + It "Should have the expected type" { + $folderOutput.type | Should be "dir" + } + It "Should have the expected entries" { + $folderOutput.entries.length | Should be 1 + } + It "Should have the expected entry data" { + $folderOutput.entries[0].name | Should be $readmeFileName + $folderOutput.entries[0].path | Should be $readmeFileName + } + } + + Context 'For getting raw (byte) file contents' { + + $readmeFileBytes = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Raw + $readmeFileString = [System.Text.Encoding]::UTF8.GetString($readmeFileBytes) + + It "Should have the expected content" { + $readmeFileString | Should be $rawOutput + } + } + + Context 'For getting raw (string) file contents' { + + $readmeFileString = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Raw -ResultAsString + + It "Should have the expected content" { + $readmeFileString | Should be $rawOutput + } + } + + Context 'For getting html (byte) file contents' { + + $readmeFileBytes = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Html + $readmeFileString = [System.Text.Encoding]::UTF8.GetString($readmeFileBytes) + + It "Should have the expected content" { + # Replace newlines with empty for comparison + $readmeFileString.Replace("`n", "").Replace("`r", "") | Should be $htmlOutput + } + } + + Context 'For getting html (string) file contents' { + + $readmeFileString = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Html -ResultAsString + + It "Should have the expected content" { + # Replace newlines with empty for comparison + $readmeFileString.Replace("`n", "").Replace("`r", "") | Should be $htmlOutput + } + } + + Context 'For getting object (default) file result' { + + $readmeFileObject = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName + + It "Should have the expected name" { + $readmeFileObject.name | Should be $readmeFileName + } + It "Should have the expected path" { + $readmeFileObject.path | Should be $readmeFileName + } + It "Should have the expected type" { + $readmeFileObject.type | Should be "file" + } + It "Should have the expected encoding" { + $readmeFileObject.encoding | Should be "base64" + } + + It "Should have the expected content" { + # Convert from base64 + $readmeFileString = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($readmeFileObject.content)) + $readmeFileString | Should be $rawOutput + } + } + + Context 'For getting object file result as string' { + + $readmeFileObject = Get-GitHubContent -OwnerName $script:ownerName -RepositoryName $repo.name -Path $readmeFileName -MediaType Object -ResultAsString + + It "Should have the expected name" { + $readmeFileObject.name | Should be $readmeFileName + } + It "Should have the expected path" { + $readmeFileObject.path | Should be $readmeFileName + } + It "Should have the expected type" { + $readmeFileObject.type | Should be "file" + } + It "Should have the expected encoding" { + $readmeFileObject.encoding | Should be "base64" + } + + It "Should have the expected content" { + $readmeFileObject.contentAsString | Should be $rawOutput + } + } + + Remove-GitHubRepository -Uri $repo.svn_url + } +} +finally +{ + if (Test-Path -Path $script:originalConfigFile -PathType Leaf) + { + # Restore the user's configuration to its pre-test state + Restore-GitHubConfiguration -Path $script:originalConfigFile + $script:originalConfigFile = $null + } +}