From 92c4aa8b3a0142752e68a50af73ac276db0c1ff6 Mon Sep 17 00:00:00 2001 From: Howard Wolosky Date: Mon, 20 Jul 2020 15:07:58 -0700 Subject: [PATCH] Add support for gists (#172) This completes the required work to support the full set of API's around gists. It adds the following functions: * `Get-GitHubGist` * `Remove-GitHubGist` * `Copy-GitHubGist` (aka. `Fork-GitHubGist`) * `Add-GitHubGistStar` * `Remove-GitHubGistStar` * `Set-GitHubGistStar` (just a wrapper around `Add/Remove-GitHubGistStar` * `Test-GitHubGistStar` * `New-GitHubGist` * `Set-GitHubGist` * `Rename-GitHubGistFile` (exposed helper function) * `Remove-GitHubGistFile` (exposed helper function) * `Set-GitHubGistFile` (exposed helper function, also known as `Add-GitHubGistFile`) * `Get-GitHubGistComment` * `Set-GitHubGistComment` * `New-GitHubGistComment` * `Remove-GitHubGistComment` This also adds formatters for all newly introduced types: `GitHub.Gist`, `GitHub.GistCommit`, `GitHub.GistDetail`, and `GitHub.GistFork`. Positional Binding has been set as `false`, and `Position` attributes added to the functions' mandatory parameters. References: [GitHub Gist](https://developer.github.com/v3/gists/) [GitHub Gist Comments](https://developer.github.com/v3/gists/comments/) Fixes #32 --- Formatters/GitHubGistComments.Format.ps1xml | 54 + Formatters/GitHubGists.Format.ps1xml | 241 +++ GitHubCore.ps1 | 9 + GitHubGistComments.ps1 | 495 +++++ GitHubGists.ps1 | 1884 +++++++++++++++++++ PowerShellForGitHub.psd1 | 28 + Tests/GitHubGistComments.tests.ps1 | 335 ++++ Tests/GitHubGists.tests.ps1 | 1193 ++++++++++++ USAGE.md | 175 ++ 9 files changed, 4414 insertions(+) create mode 100644 Formatters/GitHubGistComments.Format.ps1xml create mode 100644 Formatters/GitHubGists.Format.ps1xml create mode 100644 GitHubGistComments.ps1 create mode 100644 GitHubGists.ps1 create mode 100644 Tests/GitHubGistComments.tests.ps1 create mode 100644 Tests/GitHubGists.tests.ps1 diff --git a/Formatters/GitHubGistComments.Format.ps1xml b/Formatters/GitHubGistComments.Format.ps1xml new file mode 100644 index 00000000..b2035fca --- /dev/null +++ b/Formatters/GitHubGistComments.Format.ps1xml @@ -0,0 +1,54 @@ + + + + + + GitHub.GistComment + + GitHub.GistComment + + + + + + + + + + + + + + + + + + + + + + + + id + + + + $_.user.login + + + + body + + + created_at + + + updated_at + + + + + + + + diff --git a/Formatters/GitHubGists.Format.ps1xml b/Formatters/GitHubGists.Format.ps1xml new file mode 100644 index 00000000..3e02fa85 --- /dev/null +++ b/Formatters/GitHubGists.Format.ps1xml @@ -0,0 +1,241 @@ + + + + + + GitHub.Gist + + GitHub.Gist + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + + $_.owner.login + + + + description + + + public + + + + ($_.files | + Get-Member -MemberType NoteProperty | + Select-Object -ExpandProperty Name) -join ', ' + + + + created_at + + + updated_at + + + + + + + + + GitHub.GistCommit + + GitHub.GistCommit + + + + + + + + + + + + + + + + + + + + + GistId + + + version + + + + ($_.user.login) + + + + committed_at + + + + + + + + + GitHub.GistDetail + + GitHub.GistDetail + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + + ($_.owner.login) + + + + description + + + public + + + + ($_.files | + Get-Member -MemberType NoteProperty | + Select-Object -ExpandProperty Name) -join ', ' + + + + + ($_.forks.Count) + + + + created_at + + + updated_at + + + + + + + + + GitHub.GistFork + + GitHub.GistFork + + + + + + + + + + + + + + + + + + + + + id + + + + # There appears to be a bug in the GitHub API. + # Documentation says that the property should alway be "user", + # but it switches between "owner" and "user" depending on if it's a property + # of a gist, or the direct result from a gist forks query. + # https://github.community/t/gist-api-v3-documentation-incorrect-for-forks/122545 + if ($null -eq $_.user) + { + $_.owner.login + } + else + { + $_.user.login + } + + + + created_at + + + updated_at + + + + + + + + diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 index 02c887fd..4ea6fabd 100644 --- a/GitHubCore.ps1 +++ b/GitHubCore.ps1 @@ -329,6 +329,15 @@ function Invoke-GHRestMethod $finalResult = $finalResult | ConvertFrom-Json } } + catch [InvalidOperationException] + { + # In some cases, the returned data might have two different keys of the same characters + # but different casing (this can happen with gists with two files named 'a.txt' and 'A.txt'). + # PowerShell 6 introduced the -AsHashtable switch to work around this issue, but this + # module wants to be compatible down to PowerShell 4, so we're unable to use that feature. + Write-Log -Message 'The returned object likely contains keys that differ only in casing. Unable to convert to an object. Returning the raw JSON as a fallback.' -Level Warning + $finalResult = $finalResult + } catch [ArgumentException] { # The content must not be JSON (which is a legitimate situation). diff --git a/GitHubGistComments.ps1 b/GitHubGistComments.ps1 new file mode 100644 index 00000000..b8d43b31 --- /dev/null +++ b/GitHubGistComments.ps1 @@ -0,0 +1,495 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +@{ + GitHubGistCommentTypeName = 'GitHub.GistComment' + }.GetEnumerator() | ForEach-Object { + Set-Variable -Scope Script -Option ReadOnly -Name $_.Key -Value $_.Value + } + +filter Get-GitHubGistComment +{ +<# + .SYNOPSIS + Retrieves comments for a specific gist from GitHub. + + .DESCRIPTION + Retrieves comments for a specific gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to retrieve the comments for. + + .PARAMETER Comment + The ID of the specific comment on the gist that you wish to retrieve. + + .PARAMETER MediaType + The format in which the API will return the body of the comment. + + Raw - Return the raw markdown body. Response will include body. This is the default if you do not pass any specific media type. + 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. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.GistComment + + .EXAMPLE + Get-GitHubGistComment -Gist 6cad326836d38bd3a7ae + + Gets all comments on octocat's "hello_world.rb" gist. + + .EXAMPLE + Get-GitHubGistComment -Gist 6cad326836d38bd3a7ae -Comment 1507813 + + Gets comment 1507813 from octocat's "hello_world.rb" gist. +#> + [CmdletBinding(PositionalBinding = $false)] + [OutputType({$script:GitHubGistCommentTypeName})] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [string] $Gist, + + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('GistCommentId')] + [ValidateNotNullOrEmpty()] + [int64] $Comment, + + [ValidateSet('Raw', 'Text', 'Html', 'Full')] + [string] $MediaType = 'Full', + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + + $uriFragment = [String]::Empty + $description = [String]::Empty + + if ($PSBoundParameters.ContainsKey('Comment')) + { + $telemetryProperties['SpecifiedComment'] = $true + + $uriFragment = "gists/$Gist/comments/$Comment" + $description = "Getting comment $Comment for gist $Gist" + } + else + { + $uriFragment = "gists/$Gist/comments" + $description = "Getting comments for gist $Gist" + } + + $params = @{ + 'UriFragment' = $uriFragment + 'Description' = $description + 'AccessToken' = $AccessToken + 'AcceptHeader' = (Get-MediaAcceptHeader -MediaType $MediaType -AsJson) + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethodMultipleResult @params | Add-GitHubGistCommentAdditionalProperties) +} + +filter Remove-GitHubGistComment +{ +<# + .SYNOPSIS + Removes/deletes a comment from a gist on GitHub. + + .DESCRIPTION + Removes/deletes a comment from a gist on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to remove the comment from. + + .PARAMETER Comment + The ID of the comment to remove from the gist. + + .PARAMETER Force + If this switch is specified, you will not be prompted for confirmation of command execution. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .EXAMPLE + Remove-GitHubGist -Gist 6cad326836d38bd3a7ae -Comment 12324567 + + Removes the specified comment from octocat's "hello_world.rb" gist + (assuming you have permission). + + .EXAMPLE + Remove-GitHubGist -Gist 6cad326836d38bd3a7ae -Comment 12324567 -Confirm:$false + + Removes the specified comment from octocat's "hello_world.rb" gist + (assuming you have permission). + Will not prompt for confirmation, as -Confirm:$false was specified. + + .EXAMPLE + Remove-GitHubGist -Gist 6cad326836d38bd3a7ae -Comment 12324567 -Force + + Removes the specified comment from octocat's "hello_world.rb" gist + (assuming you have permission). + Will not prompt for confirmation, as -Force was specified. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false, + ConfirmImpact="High")] + [Alias('Delete-GitHubGist')] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 2)] + [Alias('GistCommentId')] + [ValidateNotNullOrEmpty()] + [int64] $Comment, + + [switch] $Force, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + if ($Force -and (-not $Confirm)) + { + $ConfirmPreference = 'None' + } + + if (-not $PSCmdlet.ShouldProcess($Comment, "Delete comment from gist $Gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/comments/$Comment" + 'Method' = 'Delete' + 'Description' = "Removing comment $Comment from gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +filter New-GitHubGistComment +{ +<# + .SYNOPSIS + Creates a new comment on the specified gist from GitHub. + + .DESCRIPTION + Creates a new comment on the specified gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to add the comment to. + + .PARAMETER Body + The body of the comment that you wish to leave on the gist. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.GistComment + + .EXAMPLE + New-GitHubGistComment -Gist 6cad326836d38bd3a7ae -Body 'Hello World' + + Adds a new comment of "Hello World" to octocat's "hello_world.rb" gist. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [OutputType({$script:GitHubGistCommentTypeName})] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter( + Mandatory, + Position = 2)] + [ValidateNotNullOrEmpty()] + [string] $Body, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $hashBody = @{ + 'body' = $Body + } + + if (-not $PSCmdlet.ShouldProcess($Gist, "Create new comment for gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/comments" + 'Body' = (ConvertTo-Json -InputObject $hashBody) + 'Method' = 'Post' + 'Description' = "Creating new comment on gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | Add-GitHubGistCommentAdditionalProperties) +} + +filter Set-GitHubGistComment +{ + <# + .SYNOPSIS + Edits a comment on the specified gist from GitHub. + + .DESCRIPTION + Edits a comment on the specified gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the gist that the comment is on. + + .PARAMETER Comment + The ID of the comment that you wish to edit. + + .PARAMETER Body + The new text of the comment that you wish to leave on the gist. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.GistComment + + .EXAMPLE + New-GitHubGistComment -Gist 6cad326836d38bd3a7ae -Comment 1232456 -Body 'Hello World' + + Updates the body of the comment with ID 1232456 octocat's "hello_world.rb" gist to be + "Hello World". +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [OutputType({$script:GitHubGistCommentTypeName})] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 2)] + [Alias('GistCommentId')] + [ValidateNotNullOrEmpty()] + [int64] $Comment, + + [Parameter( + Mandatory, + Position = 3)] + [ValidateNotNullOrEmpty()] + [string] $Body, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $hashBody = @{ + 'body' = $Body + } + + if (-not $PSCmdlet.ShouldProcess($Comment, "Update gist comment on gist $Gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/comments/$Comment" + 'Body' = (ConvertTo-Json -InputObject $hashBody) + 'Method' = 'Patch' + 'Description' = "Creating new comment on gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | Add-GitHubGistCommentAdditionalProperties) +} + +filter Add-GitHubGistCommentAdditionalProperties +{ +<# + .SYNOPSIS + Adds type name and additional properties to ease pipelining to GitHub Gist Comment objects. + + .PARAMETER InputObject + The GitHub object to add additional properties to. + + .PARAMETER TypeName + The type that should be assigned to the object. + + .PARAMETER GistId + The ID of the gist that the comment is for. + + .INPUTS + [PSCustomObject] + + .OUTPUTS + GitHub.GistComment +#> + [CmdletBinding()] + [OutputType({$script:GitHubGisCommentTypeName})] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Internal helper that is definitely adding more than one property.")] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [AllowNull()] + [AllowEmptyCollection()] + [PSCustomObject[]] $InputObject, + + [ValidateNotNullOrEmpty()] + [string] $TypeName = $script:GitHubGistCommentTypeName + ) + + foreach ($item in $InputObject) + { + $item.PSObject.TypeNames.Insert(0, $TypeName) + + if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) + { + $hostName = $(Get-GitHubConfiguration -Name 'ApiHostName') + if ($item.url -match "^https?://(?:www\.|api\.|)$hostName/gists/([^/]+)/comments/(.+)$") + { + $gistId = $Matches[1] + $commentId = $Matches[2] + + if ($commentId -ne $item.id) + { + $message = "The gist comment url no longer follows the expected pattern. Please contact the PowerShellForGitHubTeam: $item.url" + Write-Log -Message $message -Level Warning + } + } + + Add-Member -InputObject $item -Name 'GistCommentId' -Value $item.id -MemberType NoteProperty -Force + Add-Member -InputObject $item -Name 'GistId' -Value $gistId -MemberType NoteProperty -Force + + if ($null -ne $item.user) + { + $null = Add-GitHubUserAdditionalProperties -InputObject $item.user + } + } + + Write-Output $item + } +} diff --git a/GitHubGists.ps1 b/GitHubGists.ps1 new file mode 100644 index 00000000..d357bc49 --- /dev/null +++ b/GitHubGists.ps1 @@ -0,0 +1,1884 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +@{ + GitHubGistTypeName = 'GitHub.Gist' + GitHubGistCommitTypeName = 'GitHub.GistCommit' + GitHubGistForkTypeName = 'GitHub.GistFork' + GitHubGistSummaryTypeName = 'GitHub.GistSummary' + }.GetEnumerator() | ForEach-Object { + Set-Variable -Scope Script -Option ReadOnly -Name $_.Key -Value $_.Value + } + +filter Get-GitHubGist +{ +<# + .SYNOPSIS + Retrieves gist information from GitHub. + + .DESCRIPTION + Retrieves gist information from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to retrieve. + + .PARAMETER Sha + The specific revision of the gist that you wish to retrieve. + + .PARAMETER Forks + Gets the forks of the specified gist. + + .PARAMETER Commits + Gets the commits of the specified gist. + + .PARAMETER UserName + Gets public gists for the specified user. + + .PARAMETER Path + Download the files that are part of the specified gist to this path. + + .PARAMETER Force + If downloading files, this will overwrite any files with the same name in the provided path. + + .PARAMETER Current + Gets the authenticated user's gists. + + .PARAMETER Starred + Gets the authenticated user's starred gists. + + .PARAMETER Public + Gets public gists sorted by most recently updated to least recently updated. + The results will be limited to the first 3000. + + .PARAMETER Since + Only gists updated at or after this time are returned. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.Gist + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .EXAMPLE + Get-GitHubGist -Starred + + Gets all starred gists for the current authenticated user. + + .EXAMPLE + Get-GitHubGist -Public -Since ((Get-Date).AddDays(-2)) + + Gets all public gists that have been updated within the past two days. + + .EXAMPLE + Get-GitHubGist -Gist 6cad326836d38bd3a7ae + + Gets octocat's "hello_world.rb" gist. +#> + [CmdletBinding( + DefaultParameterSetName='Current', + PositionalBinding = $false)] + [OutputType({$script:GitHubGistTypeName})] + [OutputType({$script:GitHubGistCommitTypeName})] + [OutputType({$script:GitHubGistForkTypeName})] + [OutputType({$script:GitHubGistSummaryTypeName})] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + ParameterSetName='Id', + Position = 1)] + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + ParameterSetName='Download', + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter(ParameterSetName='Id')] + [Parameter(ParameterSetName='Download')] + [ValidateNotNullOrEmpty()] + [string] $Sha, + + [Parameter(ParameterSetName='Id')] + [switch] $Forks, + + [Parameter(ParameterSetName='Id')] + [switch] $Commits, + + [Parameter( + Mandatory, + ParameterSetName='User')] + [ValidateNotNullOrEmpty()] + [string] $UserName, + + [Parameter( + Mandatory, + ParameterSetName='Download', + Position = 2)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [Parameter(ParameterSetName='Download')] + [switch] $Force, + + [Parameter(ParameterSetName='Current')] + [switch] $Current, + + [Parameter(ParameterSetName='Current')] + [switch] $Starred, + + [Parameter(ParameterSetName='Public')] + [switch] $Public, + + [Parameter(ParameterSetName='User')] + [Parameter(ParameterSetName='Current')] + [Parameter(ParameterSetName='Public')] + [DateTime] $Since, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + + $uriFragment = [String]::Empty + $description = [String]::Empty + $outputType = $script:GitHubGistSummaryTypeName + + if ($PSCmdlet.ParameterSetName -in ('Id', 'Download')) + { + $telemetryProperties['ById'] = $true + + if ($PSBoundParameters.ContainsKey('Sha')) + { + if ($Forks -or $Commits) + { + $message = 'Cannot check for forks or commits of a specific SHA. Do not specify SHA if you want to list out forks or commits.' + Write-Log -Message $message -Level Error + throw $message + } + + $telemetryProperties['SpecifiedSha'] = $true + + $uriFragment = "gists/$Gist/$Sha" + $description = "Getting gist $Gist with specified Sha" + $outputType = $script:GitHubGistTypeName + } + elseif ($Forks) + { + $uriFragment = "gists/$Gist/forks" + $description = "Getting forks of gist $Gist" + $outputType = $script:GitHubGistForkTypeName + } + elseif ($Commits) + { + $uriFragment = "gists/$Gist/commits" + $description = "Getting commits of gist $Gist" + $outputType = $script:GitHubGistCommitTypeName + } + else + { + $uriFragment = "gists/$Gist" + $description = "Getting gist $Gist" + $outputType = $script:GitHubGistTypeName + } + } + elseif ($PSCmdlet.ParameterSetName -eq 'User') + { + $telemetryProperties['ByUserName'] = $true + + $uriFragment = "users/$UserName/gists" + $description = "Getting public gists for $UserName" + $outputType = $script:GitHubGistSummaryTypeName + } + elseif ($PSCmdlet.ParameterSetName -eq 'Current') + { + $telemetryProperties['CurrentUser'] = $true + $outputType = $script:GitHubGistSummaryTypeName + + if ((Test-GitHubAuthenticationConfigured) -or (-not [String]::IsNullOrEmpty($AccessToken))) + { + if ($Starred) + { + $uriFragment = 'gists/starred' + $description = 'Getting starred gists for current authenticated user' + } + else + { + $uriFragment = 'gists' + $description = 'Getting gists for current authenticated user' + } + } + else + { + if ($Starred) + { + $message = 'Starred can only be specified for authenticated users. Either call Set-GitHubAuthentication first, or provide a value for the AccessToken parameter.' + Write-Log -Message $message -Level Error + throw $message + } + + $message = 'Specified -Current, but not currently authenticated. Either call Set-GitHubAuthentication first, or provide a value for the AccessToken parameter.' + Write-Log -Message $message -Level Error + throw $message + } + } + elseif ($PSCmdlet.ParameterSetName -eq 'Public') + { + $telemetryProperties['Public'] = $true + $outputType = $script:GitHubGistSummaryTypeName + + $uriFragment = "gists/public" + $description = 'Getting public gists' + } + + $getParams = @() + $sinceFormattedTime = [String]::Empty + if ($null -ne $Since) + { + $sinceFormattedTime = $Since.ToUniversalTime().ToString('o') + $getParams += "since=$sinceFormattedTime" + } + + $params = @{ + 'UriFragment' = $uriFragment + '?' + ($getParams -join '&') + 'Description' = $description + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + $result = (Invoke-GHRestMethodMultipleResult @params | + Add-GitHubGistAdditionalProperties -TypeName $outputType) + + if ($PSCmdlet.ParameterSetName -eq 'Download') + { + Save-GitHubGist -GistObject $result -Path $Path -Force:$Force + } + else + { + if ($result.truncated -eq $true) + { + $message = @( + 'Response has been truncated. The API will only return the first 3000 gist results', + 'the first 300 files within the gist, and the first 1 Mb of an individual', + 'file. If the file has been truncated, you can call', + '(Invoke-WebRequest -UseBasicParsing -Method Get -Uri ).Content)', + 'where is the value of raw_url for the file in question. Be aware that', + 'for files larger than 10 Mb, you''ll need to clone the gist via the URL provided', + 'by git_pull_url.') + + Write-Log -Message ($message -join ' ') -Level Warning + } + + return $result + } +} + +function Save-GitHubGist +{ +<# + .SYNOPSIS + Downloads the contents of a gist to the specified file path. + + .DESCRIPTION + Downloads the contents of a gist to the specified file path. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER GistObject + The Gist PSCustomObject + + .PARAMETER Path + Download the files that are part of the specified gist to this path. + + .PARAMETER Force + If downloading files, this will overwrite any files with the same name in the provided path. + + .NOTES + Internal-only helper +#> + [CmdletBinding(PositionalBinding = $false)] + param( + [Parameter(Mandatory)] + [PSCustomObject] $GistObject, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [switch] $Force + ) + + # First, check to see if the response is missing files. + if ($GistObject.truncated) + { + $message = @( + 'Gist response has been truncated. The API will only return information on', + 'the first 300 files within a gist. To download this entire gist,', + 'you''ll need to clone it via the URL provided by git_pull_url', + "[$($GistObject.git_pull_url)].") + + Write-Log -Message ($message -join ' ') -Level Error + throw $message + } + + # Then check to see if there are files we won't be able to download + $files = $GistObject.files | Get-Member -Type NoteProperty | Select-Object -ExpandProperty Name + foreach ($fileName in $files) + { + if ($GistObject.files.$fileName.truncated -and + ($GistObject.files.$fileName.size -gt 10mb)) + { + $message = @( + "At least one file ($fileName) in this gist is larger than 10mb.", + 'In order to download this gist, you''ll need to clone it via the URL', + "provided by git_pull_url [$($GistObject.git_pull_url)].") + + Write-Log -Message ($message -join ' ') -Level Error + throw $message + } + } + + # Finally, we're ready to directly save the non-truncated files, + # and download the ones that are between 1 - 10mb. + $originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol + [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12 + try + { + $headers = @{} + $AccessToken = Get-AccessToken -AccessToken $AccessToken + if (-not [String]::IsNullOrEmpty($AccessToken)) + { + $headers['Authorization'] = "token $AccessToken" + } + + $Path = Resolve-UnverifiedPath -Path $Path + $null = New-Item -Path $Path -ItemType Directory -Force + foreach ($fileName in $files) + { + $filePath = Join-Path -Path $Path -ChildPath $fileName + if ((Test-Path -Path $filePath -PathType Leaf) -and (-not $Force)) + { + $message = "File already exists at path [$filePath]. Choose a different path or specify -Force" + Write-Log -Message $message -Level Error + throw $message + } + + if ($GistObject.files.$fileName.truncated) + { + # Disable Progress Bar in function scope during Invoke-WebRequest + $ProgressPreference = 'SilentlyContinue' + + $webRequestParams = @{ + UseBasicParsing = $true + Method = 'Get' + Headers = $headers + Uri = $GistObject.files.$fileName.raw_url + OutFile = $filePath + } + + Invoke-WebRequest @webRequestParams + } + else + { + $stream = New-Object -TypeName System.IO.StreamWriter -ArgumentList ($filePath) + try + { + $stream.Write($GistObject.files.$fileName.content) + } + finally + { + $stream.Close() + } + } + } + } + finally + { + [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol + } +} + +filter Remove-GitHubGist +{ +<# + .SYNOPSIS + Removes/deletes a gist from GitHub. + + .DESCRIPTION + Removes/deletes a gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to retrieve. + + .PARAMETER Force + If this switch is specified, you will not be prompted for confirmation of command execution. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .EXAMPLE + Remove-GitHubGist -Gist 6cad326836d38bd3a7ae + + Removes octocat's "hello_world.rb" gist (assuming you have permission). + + .EXAMPLE + Remove-GitHubGist -Gist 6cad326836d38bd3a7ae -Confirm:$false + + Removes octocat's "hello_world.rb" gist (assuming you have permission). + Will not prompt for confirmation, as -Confirm:$false was specified. + + .EXAMPLE + Remove-GitHubGist -Gist 6cad326836d38bd3a7ae -Force + + Removes octocat's "hello_world.rb" gist (assuming you have permission). + Will not prompt for confirmation, as -Force was specified. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false, + ConfirmImpact = 'High')] + [Alias('Delete-GitHubGist')] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [switch] $Force, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + if ($Force -and (-not $Confirm)) + { + $ConfirmPreference = 'None' + } + + if (-not $PSCmdlet.ShouldProcess($Gist, "Delete gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist" + 'Method' = 'Delete' + 'Description' = "Removing gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +filter Copy-GitHubGist +{ +<# + .SYNOPSIS + Forks a gist from GitHub. + + .DESCRIPTION + Forks a gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to fork. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.GistSummary + + .EXAMPLE + Copy-GitHubGist -Gist 6cad326836d38bd3a7ae + + Forks octocat's "hello_world.rb" gist. + + .EXAMPLE + Fork-GitHubGist -Gist 6cad326836d38bd3a7ae + + Forks octocat's "hello_world.rb" gist. This is using the alias for the command. + The result is the same whether you use Copy-GitHubGist or Fork-GitHubGist. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [OutputType({$script:GitHubGistSummaryTypeName})] + [Alias('Fork-GitHubGist')] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + if (-not $PSCmdlet.ShouldProcess($Gist, "Forking gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/forks" + 'Method' = 'Post' + 'Description' = "Forking gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | + Add-GitHubGistAdditionalProperties -TypeName $script:GitHubGistSummaryTypeName) +} + +filter Set-GitHubGistStar +{ +<# + .SYNOPSIS + Changes the starred state of a gist on GitHub for the current authenticated user. + + .DESCRIPTION + Changes the starred state of a gist on GitHub for the current authenticated user. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific Gist that you wish to change the starred state for. + + .PARAMETER Star + Include this switch to star the gist. Exclude the switch (or use -Star:$false) to + remove the star. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .EXAMPLE + Set-GitHubGistStar -Gist 6cad326836d38bd3a7ae -Star + + Stars octocat's "hello_world.rb" gist. + + .EXAMPLE + Set-GitHubGistStar -Gist 6cad326836d38bd3a7ae + + Unstars octocat's "hello_world.rb" gist. + + .EXAMPLE + Get-GitHubGist -Gist 6cad326836d38bd3a7ae | Set-GitHubGistStar -Star:$false + + Unstars octocat's "hello_world.rb" gist. + +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [switch] $Star, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + Set-TelemetryEvent -EventName $MyInvocation.MyCommand.Name + + $PSBoundParameters.Remove('Star') + if ($Star) + { + return Add-GitHubGistStar @PSBoundParameters + } + else + { + return Remove-GitHubGistStar @PSBoundParameters + } +} + +filter Add-GitHubGistStar +{ +<# + .SYNOPSIS + Star a gist from GitHub. + + .DESCRIPTION + Star a gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific Gist that you wish to star. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .EXAMPLE + Add-GitHubGistStar -Gist 6cad326836d38bd3a7ae + + Stars octocat's "hello_world.rb" gist. + + .EXAMPLE + Star-GitHubGist -Gist 6cad326836d38bd3a7ae + + Stars octocat's "hello_world.rb" gist. This is using the alias for the command. + The result is the same whether you use Add-GitHubGistStar or Star-GitHubGist. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [Alias('Star-GitHubGist')] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + if (-not $PSCmdlet.ShouldProcess($Gist, "Starring gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/star" + 'Method' = 'Put' + 'Description' = "Starring gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +filter Remove-GitHubGistStar +{ +<# + .SYNOPSIS + Unstar a gist from GitHub. + + .DESCRIPTION + Unstar a gist from GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to unstar. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .EXAMPLE + Remove-GitHubGistStar -Gist 6cad326836d38bd3a7ae + + Unstars octocat's "hello_world.rb" gist. + + .EXAMPLE + Unstar-GitHubGist -Gist 6cad326836d38bd3a7ae + + Unstars octocat's "hello_world.rb" gist. This is using the alias for the command. + The result is the same whether you use Remove-GitHubGistStar or Unstar-GitHubGist. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [Alias('Unstar-GitHubGist')] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + if (-not $PSCmdlet.ShouldProcess($Gist, "Unstarring gist")) + { + return + } + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/star" + 'Method' = 'Delete' + 'Description' = "Unstarring gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params +} + +filter Test-GitHubGistStar +{ +<# + .SYNOPSIS + Checks if a gist from GitHub is starred. + + .DESCRIPTION + Checks if a gist from GitHub is starred. + Will return $false if it isn't starred, as well as if it couldn't be checked + (due to permissions or non-existence). + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID of the specific gist that you wish to check. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + Boolean indicating if the gist was both found and determined to be starred. + + .EXAMPLE + Test-GitHubGistStar -Gist 6cad326836d38bd3a7ae + + Returns $true if the gist is starred, or $false if isn't starred or couldn't be checked + (due to permissions or non-existence). +#> + [CmdletBinding(PositionalBinding = $false)] + [OutputType([bool])] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + $params = @{ + 'UriFragment' = "gists/$Gist/star" + 'Method' = 'Get' + 'Description' = "Checking if gist $Gist is starred" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'ExtendedResult' = $true + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + try + { + $response = Invoke-GHRestMethod @params + return $response.StatusCode -eq 204 + } + catch + { + return $false + } +} + +filter New-GitHubGist +{ +<# + .SYNOPSIS + Creates a new gist on GitHub. + + .DESCRIPTION + Creates a new gist on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER File + An array of filepaths that should be part of this gist. + Use this when you have multiple files that should be part of a gist, or when you simply + want to reference an existing file on disk. + + .PARAMETER FileName + The name of the file that Content should be stored in within the newly created gist. + + .PARAMETER Content + The content of a single file that should be part of the gist. + + .PARAMETER Description + A descriptive name for this gist. + + .PARAMETER Public + When specified, the gist will be public and available for anyone to see. + + .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. + + .INPUTS + String - Filename(s) of file(s) that should be the content of the gist. + + .OUTPUTS + GitHub.GitDetail + + .EXAMPLE + New-GitHubGist -FileName 'sample.txt' -Content 'Body of my file.' -Description 'This is my gist!' -Public + + Creates a new public gist with a single file named 'sample.txt' that has the body of "Body of my file." + + .EXAMPLE + New-GitHubGist -File 'c:\files\foo.txt' -Description 'This is my gist!' + + Creates a new private gist with a single file named 'foo.txt'. Will populate it with the + content of the file at c:\files\foo.txt. + + .EXAMPLE + New-GitHubGist -File ('c:\files\foo.txt', 'c:\other\bar.txt', 'c:\octocat.ps1') -Description 'This is my gist!' + + Creates a new private gist with a three files named 'foo.txt', 'bar.txt' and 'octocat.ps1'. + Each will be populated with the content from the file on disk at the specified location. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParameterSetName='FileRef', + PositionalBinding = $false)] + [OutputType({$script:GitHubGistTypeName})] + param( + [Parameter( + Mandatory, + ValueFromPipeline, + ParameterSetName='FileRef', + Position = 1)] + [ValidateNotNullOrEmpty()] + [string[]] $File, + + [Parameter( + Mandatory, + ParameterSetName='Content', + Position = 1)] + [ValidateNotNullOrEmpty()] + [string] $FileName, + + [Parameter( + Mandatory, + ParameterSetName='Content', + Position = 2)] + [ValidateNotNullOrEmpty()] + [string] $Content, + + [string] $Description, + + [switch] $Public, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + begin + { + $files = @{} + } + + process + { + foreach ($path in $File) + { + $path = Resolve-UnverifiedPath -Path $path + if (-not (Test-Path -Path $path -PathType Leaf)) + { + $message = "Specified file [$path] could not be found or was inaccessible." + Write-Log -Message $message -Level Error + throw $message + } + + $content = [System.IO.File]::ReadAllText($path) + $fileName = (Get-Item -Path $path).Name + + if ($files.ContainsKey($fileName)) + { + $message = "You have specified more than one file with the same name [$fileName]. gists don't have a concept of directory structures, so please ensure each file has a unique name." + Write-Log -Message $message -Level Error + throw $message + } + + $files[$fileName] = @{ 'content' = $Content } + } + } + + end + { + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + + if ($PSCmdlet.ParameterSetName -eq 'Content') + { + $files[$FileName] = @{ 'content' = $Content } + } + + if (($files.Keys.StartsWith('gistfile') | Where-Object { $_ -eq $true }).Count -gt 0) + { + $message = "Don't name your files starting with 'gistfile'. This is the format of the automatic naming scheme that Gist uses internally." + Write-Log -Message $message -Level Error + throw $message + } + + $hashBody = @{ + 'description' = $Description + 'public' = $Public.ToBool() + 'files' = $files + } + + if (-not $PSCmdlet.ShouldProcess('Create new gist')) + { + return + } + + $params = @{ + 'UriFragment' = "gists" + 'Body' = (ConvertTo-Json -InputObject $hashBody) + 'Method' = 'Post' + 'Description' = "Creating a new gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | + Add-GitHubGistAdditionalProperties -TypeName $script:GitHubGistTypeName) + } +} + +filter Set-GitHubGist +{ +<# + .SYNOPSIS + Updates a gist on GitHub. + + .DESCRIPTION + Updates a gist on GitHub. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID for the gist to update. + + .PARAMETER Update + A hashtable of files to update in the gist. + The key should be the name of the file in the gist as it exists right now. + The value should be another hashtable with the following optional key/value pairs: + fileName - Specify a new name here if you want to rename the file. + filePath - Specify a path to a file on disk if you wish to update the contents of the + file in the gist with the contents of the specified file. + Should not be specified if you use 'content' (below) + content - Directly specify the raw content that the file in the gist should be updated with. + Should not be used if you use 'filePath' (above). + + .PARAMETER Delete + A list of filenames that should be removed from this gist. + + .PARAMETER Description + New description for this gist. + + .PARAMETER Force + If this switch is specified, you will not be prompted for confirmation of command execution. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.GistDetail + + .EXAMPLE + Set-GitHubGist -Gist 6cad326836d38bd3a7ae -Description 'This is my newer description' + + Updates the description for the specified gist. + + .EXAMPLE + Set-GitHubGist -Gist 6cad326836d38bd3a7ae -Delete 'hello_world.rb' -Force + + Deletes the 'hello_world.rb' file from the specified gist without prompting for confirmation. + + .EXAMPLE + Set-GitHubGist -Gist 6cad326836d38bd3a7ae -Delete 'hello_world.rb' -Description 'This is my newer description' + + Deletes the 'hello_world.rb' file from the specified gist and updates the description. + + .EXAMPLE + Set-GitHubGist -Gist 6cad326836d38bd3a7ae -Update @{'hello_world.rb' = @{ 'fileName' = 'hello_universe.rb' }} + + Renames the 'hello_world.rb' file in the specified gist to be 'hello_universe.rb'. + + .EXAMPLE + Set-GitHubGist -Gist 6cad326836d38bd3a7ae -Update @{'hello_world.rb' = @{ 'fileName' = 'hello_universe.rb' }} + + Renames the 'hello_world.rb' file in the specified gist to be 'hello_universe.rb'. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParameterSetName='Content', + PositionalBinding = $false)] + [OutputType({$script:GitHubGistTypeName})] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [hashtable] $Update, + + [string[]] $Delete, + + [string] $Description, + + [switch] $Force, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + + $telemetryProperties = @{} + + $files = @{} + + $shouldProcessMessage = 'Update gist' + + # Mark the files that should be deleted. + if ($Delete.Count -gt 0) + { + $ConfirmPreference = 'Low' + $shouldProcessMessage = 'Update gist (and remove files)' + + foreach ($toDelete in $Delete) + { + $files[$toDelete] = $null + } + } + + # Then figure out which ones need content updates and/or file renames + if ($null -ne $Update) + { + foreach ($toUpdate in $Update.GetEnumerator()) + { + $currentFileName = $toUpdate.Key + + $providedContent = $toUpdate.Value.Content + $providedFileName = $toUpdate.Value.FileName + $providedFilePath = $toUpdate.Value.FilePath + + if (-not [String]::IsNullOrWhiteSpace($providedContent)) + { + $files[$currentFileName] = @{ 'content' = $providedContent } + } + + if (-not [String]::IsNullOrWhiteSpace($providedFilePath)) + { + if (-not [String]::IsNullOrWhiteSpace($providedContent)) + { + $message = "When updating a file [$currentFileName], you cannot provide both a path to a file [$providedFilePath] and the raw content." + Write-Log -Message $message -Level Error + throw $message + } + + $providedFilePath = Resolve-Path -Path $providedFilePath + if (-not (Test-Path -Path $providedFilePath -PathType Leaf)) + { + $message = "Specified file [$providedFilePath] could not be found or was inaccessible." + Write-Log -Message $message -Level Error + throw $message + } + + $newContent = [System.IO.File]::ReadAllText($providedFilePath) + $files[$currentFileName] = @{ 'content' = $newContent } + } + + # The user has chosen to rename the file. + if (-not [String]::IsNullOrWhiteSpace($providedFileName)) + { + $files[$currentFileName] = @{ 'filename' = $providedFileName } + } + } + } + + $hashBody = @{} + if (-not [String]::IsNullOrWhiteSpace($Description)) { $hashBody['description'] = $Description } + if ($files.Keys.count -gt 0) { $hashBody['files'] = $files } + + if ($Force -and (-not $Confirm)) + { + $ConfirmPreference = 'None' + } + + if (-not $PSCmdlet.ShouldProcess($Gist, $shouldProcessMessage)) + { + return + } + + $ConfirmPreference = 'None' + $params = @{ + 'UriFragment' = "gists/$Gist" + 'Body' = (ConvertTo-Json -InputObject $hashBody) + 'Method' = 'Patch' + 'Description' = "Updating gist $Gist" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + try + { + return (Invoke-GHRestMethod @params | + Add-GitHubGistAdditionalProperties -TypeName $script:GitHubGistTypeName) + } + catch + { + if ($_.Exception.Message -like '*(422)*') + { + $message = 'This error can happen if you try to delete a file that doesn''t exist. Be aware that casing matters. ''A.txt'' is not the same as ''a.txt''.' + Write-Log -Message $message -Level Warning + } + + throw + } +} + +function Set-GitHubGistFile +{ +<# + .SYNOPSIS + Updates content of file(s) in an existing gist on GitHub, + or adds them if they aren't already part of the gist. + + .DESCRIPTION + Updates content of file(s) in an existing gist on GitHub, + or adds them if they aren't already part of the gist. + + This is a helper function built on top of Set-GitHubGist. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID for the gist to update. + + .PARAMETER File + An array of filepaths that should be part of this gist. + Use this when you have multiple files that should be part of a gist, or when you simply + want to reference an existing file on disk. + + .PARAMETER FileName + The name of the file that Content should be stored in within the newly created gist. + + .PARAMETER Content + The content of a single file that should be part of the gist. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.Gist + + .EXAMPLE + Set-GitHubGistFile -Gist 1234567 -Content 'Body of my file.' -FileName 'sample.txt' + + Adds a file named 'sample.txt' that has the body of "Body of my file." to the existing + specified gist, or updates the contents of 'sample.txt' in the gist if is already there. + + .EXAMPLE + Set-GitHubGistFile -Gist 1234567 -File 'c:\files\foo.txt' + + Adds the file 'foo.txt' to the existing specified gist, or updates its content if it + is already there. + + .EXAMPLE + Set-GitHubGistFile -Gist 1234567 -File ('c:\files\foo.txt', 'c:\other\bar.txt', 'c:\octocat.ps1') + + Adds all three files to the existing specified gist, or updates the contents of the files + in the gist if they are already there. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParameterSetName='Content', + PositionalBinding = $false)] + [OutputType({$script:GitHubGistTypeName})] + [Alias('Add-GitHubGistFile')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="This is a helper method for Set-GitHubGist which will handle ShouldProcess.")] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter( + Mandatory, + ValueFromPipeline, + ParameterSetName='FileRef', + Position = 2)] + [ValidateNotNullOrEmpty()] + [string[]] $File, + + [Parameter( + Mandatory, + ParameterSetName='Content', + Position = 2)] + [ValidateNotNullOrEmpty()] + [string] $FileName, + + [Parameter( + Mandatory, + ParameterSetName='Content', + Position = 3)] + [ValidateNotNullOrEmpty()] + [string] $Content, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + begin + { + $files = @{} + } + + process + { + foreach ($path in $File) + { + $path = Resolve-UnverifiedPath -Path $path + if (-not (Test-Path -Path $path -PathType Leaf)) + { + $message = "Specified file [$path] could not be found or was inaccessible." + Write-Log -Message $message -Level Error + throw $message + } + + $fileName = (Get-Item -Path $path).Name + $files[$fileName] = @{ 'filePath' = $path } + } + } + + end + { + Write-InvocationLog -Invocation $MyInvocation + Set-TelemetryEvent -EventName $MyInvocation.MyCommand.Name + + if ($PSCmdlet.ParameterSetName -eq 'Content') + { + $files[$FileName] = @{ 'content' = $Content } + } + + $params = @{ + 'Gist' = $Gist + 'Update' = $files + 'AccessToken' = $AccessToken + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Set-GitHubGist @params) + } +} + +function Remove-GitHubGistFile +{ +<# + .SYNOPSIS + Removes one or more files from an existing gist on GitHub. + + .DESCRIPTION + Removes one or more files from an existing gist on GitHub. + + This is a helper function built on top of Set-GitHubGist. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID for the gist to update. + + .PARAMETER FileName + An array of filenames (no paths, just names) to remove from the gist. + + .PARAMETER Force + If this switch is specified, you will not be prompted for confirmation of command execution. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.Gist + + .EXAMPLE + Remove-GitHubGistFile -Gist 1234567 -FileName ('foo.txt') + + Removes the file 'foo.txt' from the specified gist. + + .EXAMPLE + Remove-GitHubGistFile -Gist 1234567 -FileName ('foo.txt') -Force + + Removes the file 'foo.txt' from the specified gist without prompting for confirmation. + + .EXAMPLE + @('foo.txt', 'bar.txt') | Remove-GitHubGistFile -Gist 1234567 + + Removes the files 'foo.txt' and 'bar.txt' from the specified gist. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [OutputType({$script:GitHubGistTypeName})] + [Alias('Delete-GitHubGistFile')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="This is a helper method for Set-GitHubGist which will handle ShouldProcess.")] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter( + Mandatory, + ValueFromPipeline, + Position = 2)] + [ValidateNotNullOrEmpty()] + [string[]] $FileName, + + [switch] $Force, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + begin + { + $files = @() + } + + process + { + foreach ($name in $FileName) + { + $files += $name + } + } + + end + { + Write-InvocationLog -Invocation $MyInvocation + Set-TelemetryEvent -EventName $MyInvocation.MyCommand.Name + + $params = @{ + 'Gist' = $Gist + 'Delete' = $files + 'Force' = $Force + 'Confirm' = ($Confirm -eq $true) + 'AccessToken' = $AccessToken + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Set-GitHubGist @params) + } +} + +filter Rename-GitHubGistFile +{ +<# + .SYNOPSIS + Renames a file in a gist on GitHub. + + .DESCRIPTION + Renames a file in a gist on GitHub. + + This is a helper function built on top of Set-GitHubGist. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER Gist + The ID for the gist to update. + + .PARAMETER FileName + The current file in the gist to be renamed. + + .PARAMETER NewName + The new name of the file for the gist. + + .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. + + .INPUTS + GitHub.Gist + GitHub.GistComment + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary + + .OUTPUTS + GitHub.Gist + + .EXAMPLE + Rename-GitHubGistFile -Gist 1234567 -FileName 'foo.txt' -NewName 'bar.txt' + + Renames the file 'foo.txt' to 'bar.txt' in the specified gist. +#> + [CmdletBinding( + SupportsShouldProcess, + PositionalBinding = $false)] + [OutputType({$script:GitHubGistTypeName})] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="This is a helper method for Set-GitHubGist which will handle ShouldProcess.")] + param( + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1)] + [Alias('GistId')] + [ValidateNotNullOrEmpty()] + [string] $Gist, + + [Parameter( + Mandatory, + Position = 2)] + [ValidateNotNullOrEmpty()] + [string] $FileName, + + [Parameter( + Mandatory, + Position = 3)] + [ValidateNotNullOrEmpty()] + [string] $NewName, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog -Invocation $MyInvocation + Set-TelemetryEvent -EventName $MyInvocation.MyCommand.Name + + $params = @{ + 'Gist' = $Gist + 'Update' = @{$FileName = @{ 'fileName' = $NewName }} + 'AccessToken' = $AccessToken + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -BoundParameters $PSBoundParameters -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Set-GitHubGist @params) +} + +filter Add-GitHubGistAdditionalProperties +{ +<# + .SYNOPSIS + Adds type name and additional properties to ease pipelining to GitHub Gist objects. + + .PARAMETER InputObject + The GitHub object to add additional properties to. + + .PARAMETER TypeName + The type that should be assigned to the object. + + .INPUTS + [PSCustomObject] + + .OUTPUTS + GitHub.Gist + GitHub.GistCommit + GitHub.GistFork + GitHub.GistSummary +#> + [CmdletBinding()] + [OutputType({$script:GitHubGistTypeName})] + [OutputType({$script:GitHubGistFormTypeName})] + [OutputType({$script:GitHubGistSummaryTypeName})] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Internal helper that is definitely adding more than one property.")] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [AllowNull()] + [AllowEmptyCollection()] + [PSCustomObject[]] $InputObject, + + [ValidateNotNullOrEmpty()] + [string] $TypeName = $script:GitHubGistSummaryTypeName + ) + + if ($TypeName -eq $script:GitHubGistCommitTypeName) + { + return Add-GitHubGistCommitAdditionalProperties -InputObject $InputObject + } + elseif ($TypeName -eq $script:GitHubGistForkTypeName) + { + return Add-GitHubGistForkAdditionalProperties -InputObject $InputObject + } + + foreach ($item in $InputObject) + { + $item.PSObject.TypeNames.Insert(0, $TypeName) + + if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) + { + Add-Member -InputObject $item -Name 'GistId' -Value $item.id -MemberType NoteProperty -Force + + @('user', 'owner') | + ForEach-Object { + if ($null -ne $item.$_) + { + $null = Add-GitHubUserAdditionalProperties -InputObject $item.$_ + } + } + + if ($null -ne $item.forks) + { + $item.forks = Add-GitHubGistForkAdditionalProperties -InputObject $item.forks + } + + if ($null -ne $item.history) + { + $item.history = Add-GitHubGistCommitAdditionalProperties -InputObject $item.history + } + } + + Write-Output $item + } +} + +filter Add-GitHubGistCommitAdditionalProperties +{ +<# + .SYNOPSIS + Adds type name and additional properties to ease pipelining to GitHub GistCommit objects. + + .PARAMETER InputObject + The GitHub object to add additional properties to. + + .PARAMETER TypeName + The type that should be assigned to the object. + + .INPUTS + [PSCustomObject] + + .OUTPUTS + GitHub.GistCommit +#> + [CmdletBinding()] + [OutputType({$script:GitHubGistCommitTypeName})] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Internal helper that is definitely adding more than one property.")] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [AllowNull()] + [AllowEmptyCollection()] + [PSCustomObject[]] $InputObject, + + [ValidateNotNullOrEmpty()] + [string] $TypeName = $script:GitHubGistCommitTypeName + ) + + foreach ($item in $InputObject) + { + $item.PSObject.TypeNames.Insert(0, $TypeName) + + if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) + { + $hostName = $(Get-GitHubConfiguration -Name 'ApiHostName') + if ($item.url -match "^https?://(?:www\.|api\.|)$hostName/gists/([^/]+)/(.+)$") + { + $id = $Matches[1] + $sha = $Matches[2] + + if ($sha -ne $item.version) + { + $message = "The gist commit url no longer follows the expected pattern. Please contact the PowerShellForGitHubTeam: $item.uri" + Write-Log -Message $message -Level Warning + } + } + + Add-Member -InputObject $item -Name 'GistId' -Value $id -MemberType NoteProperty -Force + Add-Member -InputObject $item -Name 'Sha' -Value $item.version -MemberType NoteProperty -Force + + $null = Add-GitHubUserAdditionalProperties -InputObject $item.user + } + + Write-Output $item + } +} + +filter Add-GitHubGistForkAdditionalProperties +{ +<# + .SYNOPSIS + Adds type name and additional properties to ease pipelining to GitHub Gist Fork objects. + + .PARAMETER InputObject + The GitHub object to add additional properties to. + + .PARAMETER TypeName + The type that should be assigned to the object. + + .INPUTS + [PSCustomObject] + + .OUTPUTS + GitHub.GistFork +#> + [CmdletBinding()] + [OutputType({$script:GitHubGistForkTypeName})] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Internal helper that is definitely adding more than one property.")] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [AllowNull()] + [AllowEmptyCollection()] + [PSCustomObject[]] $InputObject, + + [ValidateNotNullOrEmpty()] + [string] $TypeName = $script:GitHubGistForkTypeName + ) + + foreach ($item in $InputObject) + { + $item.PSObject.TypeNames.Insert(0, $TypeName) + + if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) + { + Add-Member -InputObject $item -Name 'GistId' -Value $item.id -MemberType NoteProperty -Force + + # See here for why we need to work with both 'user' _and_ 'owner': + # https://github.community/t/gist-api-v3-documentation-incorrect-for-forks/122545 + @('user', 'owner') | + ForEach-Object { + if ($null -ne $item.$_) + { + $null = Add-GitHubUserAdditionalProperties -InputObject $item.$_ + } + } + } + + Write-Output $item + } +} \ No newline at end of file diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index 3063585b..5cbc5e48 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -15,6 +15,8 @@ # Format files (.ps1xml) to be loaded when importing this module FormatsToProcess = @( + 'Formatters/GitHubGistComments.Format.ps1xml', + 'Formatters/GitHubGists.Format.ps1xml', 'Formatters/GitHubReleases.Format.ps1xml' 'Formatters/GitHubRepositories.Format.ps1xml' ) @@ -32,6 +34,8 @@ 'GitHubCore.ps1', 'GitHubContents.ps1', 'GitHubEvents.ps1', + 'GitHubGistComments.ps1', + 'GitHubGists.ps1', 'GitHubIssueComments.ps1', 'GitHubIssues.ps1', 'GitHubLabels.ps1', @@ -59,9 +63,11 @@ FunctionsToExport = @( 'Add-GitHubAssignee', 'Add-GitHubIssueLabel', + 'Add-GitHubGistStar', 'Backup-GitHubConfiguration', 'Clear-GitHubAuthentication', 'ConvertFrom-GitHubMarkdown', + 'Copy-GitHubGist', 'Disable-GitHubRepositorySecurityFix', 'Disable-GitHubRepositoryVulnerabilityAlert', 'Enable-GitHubRepositorySecurityFix', @@ -73,6 +79,8 @@ 'Get-GitHubContent', 'Get-GitHubEmoji', 'Get-GitHubEvent', + 'Get-GitHubGist', + 'Get-GitHubGistComment', 'Get-GitHubGitIgnore', 'Get-GitHubIssue', 'Get-GitHubIssueComment', @@ -115,6 +123,8 @@ 'Move-GitHubProjectCard', 'Move-GitHubProjectColumn', 'Move-GitHubRepositoryOwnership', + 'New-GitHubGist', + 'New-GitHubGistComment', 'New-GitHubIssue', 'New-GitHubIssueComment', 'New-GitHubLabel', @@ -130,6 +140,11 @@ 'New-GitHubRepositoryBranch', 'New-GitHubRepositoryFork', 'Remove-GitHubAssignee', + 'Remove-GitHubComment', + 'Remove-GitHubGist', + 'Remove-GitHubGistComment', + 'Remove-GitHubGistFile', + 'Remove-GitHubGistStar', 'Remove-GitHubIssueComment', 'Remove-GitHubIssueLabel', 'Remove-GitHubLabel', @@ -142,12 +157,17 @@ 'Remove-GitHubReleaseAsset', 'Remove-GitHubRepository', 'Remove-GitHubRepositoryBranch' + 'Rename-GitHubGistFile', 'Rename-GitHubRepository', 'Reset-GitHubConfiguration', 'Restore-GitHubConfiguration', 'Set-GitHubAuthentication', 'Set-GitHubConfiguration', 'Set-GitHubContent', + 'Set-GitHubGist', + 'Set-GitHubGistComment', + 'Set-GitHubGistFile', + 'Set-GitHubGistStar', 'Set-GitHubIssue', 'Set-GitHubIssueComment', 'Set-GitHubIssueLabel', @@ -165,15 +185,20 @@ 'Split-GitHubUri', 'Test-GitHubAssignee', 'Test-GitHubAuthenticationConfigured', + 'Test-GitHubGistStar', 'Test-GitHubOrganizationMember', 'Test-GitHubRepositoryVulnerabilityAlert', 'Unlock-GitHubIssue' ) AliasesToExport = @( + 'Add-GitHubGistFile', 'Delete-GitHubAsset', 'Delete-GitHubBranch', 'Delete-GitHubComment', + 'Delete-GitHubGist', + 'Delete-GitHubGistComment', + 'Delete-GitHubGistFile', 'Delete-GitHubIssueComment', 'Delete-GitHubLabel', 'Delete-GitHubMilestone', @@ -185,6 +210,7 @@ 'Delete-GitHubReleaseAsset', 'Delete-GitHubRepository', 'Delete-GitHubRepositoryBranch', + 'Fork-GitHubGist', 'Get-GitHubAsset', 'Get-GitHubBranch', 'Get-GitHubComment', @@ -197,7 +223,9 @@ 'Remove-GitHubComment', 'Set-GitHubAsset', 'Set-GitHubComment', + 'Star-GitHubGist', 'Transfer-GitHubRepositoryOwnership' + 'Unstar-GitHubGist' 'Update-GitHubIssue', 'Update-GitHubLabel', 'Update-GitHubCurrentUser', diff --git a/Tests/GitHubGistComments.tests.ps1 b/Tests/GitHubGistComments.tests.ps1 new file mode 100644 index 00000000..ab14b510 --- /dev/null +++ b/Tests/GitHubGistComments.tests.ps1 @@ -0,0 +1,335 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.Synopsis + Tests for GitHubGistCommentss.ps1 module +#> + +[CmdletBinding()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification='Suppress false positives in Pester code blocks')] +param() + +# 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 +{ + Describe 'Get-GitHubGistComment' { + BeforeAll { + $body = 'Comment body' + } + + Context 'By parameters' { + BeforeAll { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + $body = 'Comment body' + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + $comments = @(Get-GitHubGistComment -Gist $gist.id -MediaType 'Raw') + It 'Should have no comments so far' { + $comments.Count | Should -Be 0 + } + + $firstComment = New-GitHubGistComment -Gist $gist.id -Body $body + $comments = @(Get-GitHubGistComment -Gist $gist.id -MediaType 'Text') + It 'Should have one comments so far' { + $comments.Count | Should -Be 1 + $comments[0].id | Should -Be $firstComment.id + $comments[0].body | Should -BeNullOrEmpty + $comments[0].body_html | Should -BeNullOrEmpty + $comments[0].body_text | Should -Not -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $comments[0].PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comments[0].GistCommentId | Should -Be $comments[0].id + $comments[0].GistId | Should -Be $gist.id + $comments[0].user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $null = New-GitHubGistComment -Gist $gist.id -Body $body + $comments = @(Get-GitHubGistComment -Gist $gist.id -MediaType 'Html') + It 'Should have one comments so far' { + $comments.Count | Should -Be 2 + foreach ($comment in $comments) + { + $comment.body | Should -BeNullOrEmpty + $comment.body_html | Should -Not -BeNullOrEmpty + $comment.body_text | Should -BeNullOrEmpty + } + } + + It 'Should have the expected type and additional properties' { + foreach ($comment in $comments) + { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + $comment = Get-GitHubGistComment -Gist $gist.id -Comment $firstComment.id -MediaType 'Html' + It 'Should retrieve the specific comment' { + $comment.id | Should -Be $firstComment.id + $comment.body | Should -BeNullOrEmpty + $comment.body_html | Should -Not -BeNullOrEmpty + $comment.body_text | Should -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + Context 'Gist on the pipeline' { + BeforeAll { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + $body = 'Comment body' + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + $comments = @(Get-GitHubGistComment -Gist $gist.id -MediaType 'Text') + It 'Should have no comments so far' { + $comments.Count | Should -Be 0 + } + + $firstComment = $gist | New-GitHubGistComment -Body $body + $comments = @($gist | Get-GitHubGistComment -MediaType 'Raw') + It 'Should have one comments so far' { + $comments.Count | Should -Be 1 + $comments[0].id | Should -Be $firstComment.id + $comments[0].body | Should -Not -BeNullOrEmpty + $comments[0].body_html | Should -BeNullOrEmpty + $comments[0].body_text | Should -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $comments[0].PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comments[0].GistCommentId | Should -Be $comments[0].id + $comments[0].GistId | Should -Be $gist.id + $comments[0].user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $null = $gist | New-GitHubGistComment -Body $body + $comments = @($gist | Get-GitHubGistComment -MediaType 'Full') + It 'Should have one comments so far' { + $comments.Count | Should -Be 2 + foreach ($comment in $comments) + { + $comment.body | Should -Not -BeNullOrEmpty + $comment.body_html | Should -Not -BeNullOrEmpty + $comment.body_text | Should -Not -BeNullOrEmpty + } + } + + It 'Should have the expected type and additional properties' { + foreach ($comment in $comments) + { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + $comment = Get-GitHubGistComment -Gist $gist.id -Comment $firstComment.id -MediaType 'Html' + It 'Should retrieve the specific comment' { + $comment.id | Should -Be $firstComment.id + $comment.body | Should -BeNullOrEmpty + $comment.body_html | Should -Not -BeNullOrEmpty + $comment.body_text | Should -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $comment = $firstComment | Get-GitHubGistComment -MediaType 'Html' + It 'Should retrieve the specific comment with the comment on the pipeline' { + $comment.id | Should -Be $firstComment.id + $comment.body | Should -BeNullOrEmpty + $comment.body_html | Should -Not -BeNullOrEmpty + $comment.body_text | Should -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + } + + Describe 'New-GitHubGistComment' { + BeforeAll { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + $body = 'Comment body' + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + Context 'By parameters' { + $comment = New-GitHubGistComment -Gist $gist.id -Body $body + It 'Should have the expected result' { + $comment.body | Should -Be $body + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + Context 'Gist on the pipeline' { + $comment = $gist | New-GitHubGistComment -Body $body + It 'Should have the expected result' { + $comment.body | Should -Be $body + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + } + + Describe 'New-GitHubGistComment' { + BeforeAll { + $gist = New-GitHubGist -Filename 'sample.txt' -Content 'Sample text' + $body = 'Comment body' + $updatedBody = 'Updated comment body' + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + Context 'By parameters' { + $comment = New-GitHubGistComment -Gist $gist.id -Body $body + It 'Should have the expected result' { + $comment.body | Should -Be $body + } + + $comment = Set-GitHubGistComment -Gist $gist.id -Comment $comment.id -Body $updatedBody + It 'Should have the expected result' { + $comment.body | Should -Be $updatedBody + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + Context 'Gist on the pipeline' { + $comment = $gist | New-GitHubGistComment -Body $body + It 'Should have the expected result' { + $comment.body | Should -Be $body + } + + $comment = $gist | Set-GitHubGistComment -Comment $comment.id -Body $updatedBody + It 'Should have the expected result' { + $comment.body | Should -Be $updatedBody + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + Context 'Gist Comment on the pipeline' { + $comment = $gist | New-GitHubGistComment -Body $body + It 'Should have the expected result' { + $comment.body | Should -Be $body + } + + $comment = $comment | Set-GitHubGistComment -Body $updatedBody + It 'Should have the expected result' { + $comment.body | Should -Be $updatedBody + } + + It 'Should have the expected type and additional properties' { + $comment.PSObject.TypeNames[0] | Should -Be 'GitHub.GistComment' + $comment.GistCommentId | Should -Be $comment.id + $comment.GistId | Should -Be $gist.id + $comment.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + } + + Describe 'Remove-GitHubGistComment' { + BeforeAll { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + $body = 'Comment body' + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + Context 'By parameters' { + $comment = New-GitHubGistComment -Gist $gist.id -Body $body + + Remove-GitHubGistComment -Gist $gist.id -Comment $comment.id -Force + It 'Should be gone' { + { Get-GitHubGistComment -Gist $gist.id -Comment $comment.id } | Should -Throw + } + } + + Context 'Gist on the pipeline' { + $comment = $gist | New-GitHubGistComment -Body $body + + $gist | Remove-GitHubGistComment -Comment $comment.id -Force + It 'Should be gone' { + { $gist | Get-GitHubGistComment -Comment $comment.id } | Should -Throw + } + } + + Context 'Gist Comment on the pipeline' { + $comment = $gist | New-GitHubGistComment -Body $body + + $comment | Remove-GitHubGistComment -Force + It 'Should be gone' { + { $comment | Get-GitHubGistComment } | Should -Throw + } + } + } +} +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 + } +} diff --git a/Tests/GitHubGists.tests.ps1 b/Tests/GitHubGists.tests.ps1 new file mode 100644 index 00000000..67e06ccf --- /dev/null +++ b/Tests/GitHubGists.tests.ps1 @@ -0,0 +1,1193 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.Synopsis + Tests for GitHubGists.ps1 module +#> + +[CmdletBinding()] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification='Suppress false positives in Pester code blocks')] +param() + +# 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') + +filter New-LargeFile +{ +<# + .SYNOPSIS + Creates a large dummy file with random conntent + + .DESCRIPTION + Creates a large dummy file with random conntent + Credits for the random content creation logic goes to Robert Robelo + + .PARAMETER Path + The full path to the file to create. + + .PARAMETER SizeMB + The size of the random file to be genrated. Default is one MB + + .PARAMETER Type + The type of file should be created. + + .PARAMETER Force + Will allow this to overwrite the target file if it already exists. + + .EXAMPLE + New-LargeFile -Path C:\Temp\LF\bigfile.txt -SizeMB 10 +#> + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(ValueFromPipeline)] + [String] $Path, + + [ValidateRange(1, 5120)] + [UInt16] $SizeMB = 1, + + [ValidateSet('Text', 'Binary')] + [string] $Type = 'Text', + + [switch] $Force + ) + + $tempFile = New-TemporaryFile + + if ($Type -eq 'Text') + { + $streamWriter = New-Object -TypeName IO.StreamWriter -ArgumentList ($tempFile) + try + { + # get a 64 element Char[]; I added the - and \n to have 64 chars + [char[]]$chars = 'azertyuiopqsdfghjklmwxcvbnAZERTYUIOPQSDFGHJKLMWXCVBN0123456789-\n' + 1..$SizeMB | ForEach-Object { + # get 1MB of chars from 4 256KB strings + 1..4 | ForEach-Object { + $randomizedChars = $chars | Get-Random -Count $chars.Count + + # repeat random string 4096 times to get a 256KB string + $output = (-join $randomizedChars) * 4kb + + # write 256KB string to file + $streamWriter.Write($output) + + # release resources + Clear-Variable -Name @('randomizedChars', 'output') + } + } + } + catch + { + Remove-File -Path $tempFile -ErrorAction SilentlyContinue + } + finally + { + $streamWriter.Close() + $streamWriter.Dispose() + + # Force the immediate garbage collection of allocated resources + [GC]::Collect() + } + } + else + { + $content = New-Object -TypeName Byte[] -ArgumentList ($SizeMB * 1mb) + (New-Object -TypeName Random).NextBytes($content) + [IO.File]::WriteAllBytes($tempFile, $content) + } + + try + { + if ($PSBoundParameters.ContainsKey('Path')) + { + return (Move-Item -Path $tempFile -Destination $Path -Force:$Force) + } + else + { + return (Get-Item -Path $tempFile) + } + } + catch + { + Remove-File -Path $tempFile -ErrorAction SilentlyContinue + } +} + +try +{ + Describe 'Get-GitHubGist' { + Context 'Specific Gist' { + $id = '0831f3fbd83ac4d46451' # octocat/git-author-rewrite.sh + $gist = Get-GitHubGist -Gist $id + It 'Should be the expected gist' { + $gist.id | Should -Be $id + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + $gist.history[0].PSObject.TypeNames[0] | Should -Be 'GitHub.GistCommit' + $gist.forks[0].PSObject.TypeNames[0] | Should -Be 'GitHub.GistFork' + } + + $gist = $gist | Get-GitHubGist + It 'Should be the expected gist with the gist on the pipeline' { + $gist.id | Should -Be $id + } + } + + Context 'Commits and specific Gist with Sha' { + $id = '0831f3fbd83ac4d46451' # octocat/git-author-rewrite.sh + + $gist = Get-GitHubGist -Gist $id + $commits = Get-GitHubGist -Gist $gist.id -Commits + + It 'Should have multiple commits' { + $commits.Count | Should -BeGreaterThan 1 + } + + It 'Should have the expected type and additional properties' { + foreach ($commit in $commits) + { + $commit.PSObject.TypeNames[0] | Should -Be 'GitHub.GistCommit' + $commit.GistId | Should -Be $gist.id + $commit.Sha | Should -Be $commit.version + $commit.user.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + $oldestSha = $commits | Sort-Object -Property 'committed_at' | Select-Object -First 1 + + $firstGistCommit = Get-GitHubGist -Gist $gist.id -Sha $oldestSha.version + It 'Should be the expected commit' { + $firstGistCommit.created_at | Should -Be $oldestSha.committed_at + } + + It 'Should have the expected type and additional properties' { + $firstGistCommit.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $firstGistCommit.GistId | Should -Be $firstGistCommit.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + $gist.history[0].PSObject.TypeNames[0] | Should -Be 'GitHub.GistCommit' + $gist.forks[0].PSObject.TypeNames[0] | Should -Be 'GitHub.GistFork' + } + + It 'Should fail if we specify Sha _and_ Commits' { + { Get-GitHubGist -Gist $gist.id -Commits -Sha $oldestSha.version } | Should -Throw + } + + It 'Should fail if we specify Sha _and_ Forks' { + { Get-GitHubGist -Gist $gist.id -Forks -Sha $oldestSha.version } | Should -Throw + } + + $firstGistCommit = $gist | Get-GitHubGist -Sha $oldestSha.version + It 'Should be the expected gist commit with the gist on the pipeline' { + $firstGistCommit.created_at | Should -Be $oldestSha.committed_at + } + + $firstGistCommit = $firstGistCommit | Get-GitHubGist + It 'Should be the expected gist commit with the gist commit on the pipeline' { + $firstGistCommit.created_at | Should -Be $oldestSha.committed_at + } + } + + Context 'Forks' { + $id = '0831f3fbd83ac4d46451' # octocat/git-author-rewrite.sh + + $gist = Get-GitHubGist -Gist $id + $forks = Get-GitHubGist -Gist $gist.id -Forks + + It 'Should have multiple forks' { + $forks.Count | Should -BeGreaterThan 1 + } + + It 'Should have the expected type and additional properties' { + foreach ($fork in $forks) + { + $fork.PSObject.TypeNames[0] | Should -Be 'GitHub.GistFork' + $fork.GistId | Should -Be $fork.id + $fork.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + $forks = $gist | Get-GitHubGist -Forks + + It 'Should have multiple forks when gist is on the pipeline' { + $forks.Count | Should -BeGreaterThan 1 + } + } + + Context 'All gists for a specific user' { + $username = 'octocat' + $gists = Get-GitHubGist -UserName $username + + It 'Should have multiple gists' { + $gists.Count | Should -BeGreaterThan 1 + } + + It 'Should have the expected type and additional properties' { + foreach ($gist in $gists) + { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.GistSummary' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + $since = (Get-Date -Date '01/01/2016') + $sinceGists = Get-GitHubGist -UserName $username -Since $since + It 'Should have fewer results with using the since parameter' { + $sinceGists.Count | Should -BeGreaterThan 0 + $sinceGists.Count | Should -BeLessThan $gists.Count + } + } + + Context 'All gists for the current authenticated user' { + $gist = New-GitHubGist -Filename 'sample.txt' -Content 'Sample text' + $gists = @(Get-GitHubGist) + It 'Should at least one gist including the one just created' { + $gists.Count | Should -BeGreaterOrEqual 1 + $gists.id | Should -Contain $gist.id + } + + It 'Should have the expected type and additional properties' { + foreach ($gist in $gists) + { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.GistSummary' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + } + + It 'Should be removed' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + + Context 'All gists for the current authenticated user, but not authenticated' { + # This would just be testing that an exception is thrown. + # There's no easy way to cover unauthenticated sessions in the UT's, + # so we'll just accept the lower code coverage here. + } + + Context 'All starred gists for the current authenticated user' { + $id = '0831f3fbd83ac4d46451' # octocat/git-author-rewrite.sh + Add-GitHubGistStar -Gist $id + + $gists = @(Get-GitHubGist -Starred) + It 'Should include the one we just starred' { + $gists.Count | Should -BeGreaterOrEqual 1 + $gists.id | Should -Contain $id + } + + Remove-GitHubGistStar -Gist $id + } + + Context 'All starred gists for the current authenticated user, but not authenticated' { + # This would just be testing that an exception is thrown. + # There's no easy way to cover unauthenticated sessions in the UT's, + # so we'll just accept the lower code coverage here. + } + + Context 'All public gists' { + # This would require 100 queries, taking over 2 minutes. + # Given the limited additional value that we'd get from this additional test relative + # to the time it would take to execute, we'll just accept the lower code coverage here. + } + } + + Describe 'Get-GitHubGist/Download' { + BeforeAll { + # To get access to New-TemporaryDirectory + $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent + . (Join-Path -Path $moduleRootPath -ChildPath 'Helpers.ps1') + $tempPath = New-TemporaryDirectory + } + + AfterAll { + if (Get-Variable -Name tempPath -ErrorAction SilentlyContinue) + { + Remove-Item -Path $tempPath -Recurse -ErrorAction SilentlyContinue -Force + } + } + + Context 'Download gist content' { + BeforeAll { + $tempFile = New-TemporaryFile + $fileA = "$($tempFile.FullName).ps1" + Move-Item -Path $tempFile -Destination $fileA + $fileAName = (Get-Item -Path $fileA).Name + $fileAContent = 'fileA content' + Out-File -FilePath $fileA -InputObject $fileAContent -Encoding utf8 + + $tempFile = New-TemporaryFile + $fileB = "$($tempFile.FullName).txt" + Move-Item -Path $tempFile -Destination $fileB + $fileBName = (Get-Item -Path $fileB).Name + $fileBContent = 'fileB content' + Out-File -FilePath $fileB -InputObject $fileBContent -Encoding utf8 + + $tempFile = New-LargeFile -SizeMB 1 + $twoMegFile = "$($tempFile.FullName).bin" + Move-Item -Path $tempFile -Destination $twoMegFile + $twoMegFileName = (Get-Item -Path $twoMegFile).Name + + $gist = @($fileA, $fileB, $twoMegFile) | New-GitHubGist + } + + AfterAll { + $gist | Remove-GitHubGist -Force + @($fileA, $fileB, $twoMegFile) | + Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + It 'Should have no files at the download path' { + @(Get-ChildItem -Path $tempPath).Count | Should -Be 0 + } + + Get-GitHubGist -Gist $gist.id -Path $tempPath + It 'Should download all of the files' { + @(Get-ChildItem -Path $tempPath).Count | Should -Be 3 + [System.IO.File]::ReadAllText($fileA).Trim() | + Should -Be ([System.IO.File]::ReadAllText((Join-Path -Path $tempPath -ChildPath $fileAName)).Trim()) + [System.IO.File]::ReadAllText($fileB).Trim() | + Should -Be ([System.IO.File]::ReadAllText((Join-Path -Path $tempPath -ChildPath $fileBName)).Trim()) + (Get-FileHash -Path $twoMegFile).Hash | + Should -Be (Get-FileHash -Path (Join-Path -Path $tempPath -ChildPath $twoMegFileName)).Hash + } + + $gist | Get-GitHubGist -Path $tempPath -Force + It 'Should download all of the files with the gist on the pipeline and -Force' { + @(Get-ChildItem -Path $tempPath).Count | Should -Be 3 + [System.IO.File]::ReadAllText($fileA).Trim() | + Should -Be ([System.IO.File]::ReadAllText((Join-Path -Path $tempPath -ChildPath $fileAName)).Trim()) + [System.IO.File]::ReadAllText($fileB).Trim() | + Should -Be ([System.IO.File]::ReadAllText((Join-Path -Path $tempPath -ChildPath $fileBName)).Trim()) + (Get-FileHash -Path $twoMegFile).Hash | + Should -Be (Get-FileHash -Path (Join-Path -Path $tempPath -ChildPath $twoMegFileName)).Hash + } + } + + Context 'More than 300 files' { + BeforeAll { + $files = @() + 1..301 | ForEach-Object { + $tempFile = New-TemporaryFile + $file = "$($tempFile.FullName)-$_.ps1" + Move-Item -Path $tempFile -Destination $file + $fileContent = "file-$_ content" + Out-File -FilePath $file -InputObject $fileContent -Encoding utf8 + $files += $file + } + } + + AfterAll { + $files | Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + # May want to consider skipping this test. + # It works just fine, but takes 26 second to execute. + # (May not be worth it for the moderate improvement to code coverage.) + It 'Should throw an exception because there are too many files' { + $gist = $files | New-GitHubGist + { $gist | Get-GitHubGist -Path $tempPath -Force } | Should -Throw + $gist | Remove-GitHubGist -Force + } + + } + + Context 'Download gist content' { + BeforeAll { + $tempFile = New-LargeFile -SizeMB 10 + $tenMegFile = "$($tempFile.FullName).bin" + Move-Item -Path $tempFile -Destination $tenMegFile + $tenMegFileName = (Get-Item -Path $tenMegFile).Name + } + + AfterAll { + @($tenMegFile) | + Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + # May want to consider skipping this test. + # It works just fine, but takes 26 second to execute. + # (May not be worth it for the moderate improvement to code coverage.) + It 'Should throw an exception because the file is too large to download' { + $gist = $tenMegFile | New-GitHubGist + { $gist | Get-GitHubGist -Path $tempPath -Force } | Should -Throw + $gist | Remove-GitHubGist -Force + } + } + } + + Describe 'Remove-GitHubGist' { + Context 'With parameters' { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + It 'Should be there' { + { Get-GitHubGist -Gist $gist.id } | Should -Not -Throw + } + + It 'Should remove the gist successfully' { + { Remove-GitHubGist -Gist $gist.id -Force } | Should -Not -Throw + } + + It 'Should be removed' { + { Get-GitHubGist -Gist $gist.id } | Should -Throw + } + } + + Context 'With the gist on the pipeline' { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + It 'Should be there' { + { $gist | Get-GitHubGist } | Should -Not -Throw + } + + It 'Should remove the gist successfully' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + + It 'Should be removed' { + { $gist | Get-GitHubGist } | Should -Throw + } + } + } + + Describe 'Copy-GitHubGist' { + BeforeAll { + $originalGist = Get-GitHubGist -Gist '1169852' # octocat/test.cs + } + + Context 'By parameters' { + $gist = Copy-GitHubGist -Gist $originalGist.id + It 'Should have been forked' { + $gist.files.Count | Should -Be $originalGist.files.Count + foreach ($file in $gist.files) + { + $originalFile = $originalGist.files | + Where-Object { $_.filename -eq $file.filename } + $file.filename | Should -Be $originalFile.filename + $file.size | Should -Be $originalFile.size + } + } + + It 'Should have the expected additional type and properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.GistSummary' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should be removed' { + { Remove-GitHubGist -Gist $gist.id -Force } | Should -Not -Throw + } + } + + Context 'Gist on the pipeline' { + $gist = $originalGist | Copy-GitHubGist + It 'Should have been forked' { + $gist.files.Count | Should -Be $originalGist.files.Count + foreach ($file in $gist.files) + { + $originalFile = $originalGist.files | + Where-Object { $_.filename -eq $file.filename } + $file.filename | Should -Be $originalFile.filename + $file.size | Should -Be $originalFile.size + } + } + + It 'Should have the expected additional type and properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.GistSummary' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should be removed' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + } + + Describe 'Add/Remove/Set/Test-GitHubGistStar' { + BeforeAll { + $gist = New-GitHubGist -FileName 'sample.txt' -Content 'Sample text' + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + Context 'With parameters' { + $starred = Test-GitHubGistStar -Gist $gist.id + It 'Should not be starred yet' { + $starred | Should -BeFalse + } + + Add-GitHubGistStar -Gist $gist.id + $starred = Test-GitHubGistStar -Gist $gist.id + It 'Should now be starred' { + $starred | Should -BeTrue + } + + Remove-GitHubGistStar -Gist $gist.id + $starred = Test-GitHubGistStar -Gist $gist.id + It 'Should no longer be starred' { + $starred | Should -BeFalse + } + + Set-GitHubGistStar -Gist $gist.id -Star + $starred = Test-GitHubGistStar -Gist $gist.id + It 'Should now be starred' { + $starred | Should -BeTrue + } + + Set-GitHubGistStar -Gist $gist.id + $starred = Test-GitHubGistStar -Gist $gist.id + It 'Should no longer be starred' { + $starred | Should -BeFalse + } + } + + Context 'With the gist on the pipeline' { + $starred = $gist | Test-GitHubGistStar + It 'Should not be starred yet' { + $starred | Should -BeFalse + } + + $gist | Add-GitHubGistStar + $starred = $gist | Test-GitHubGistStar + It 'Should now be starred' { + $starred | Should -BeTrue + } + + $gist | Remove-GitHubGistStar + $starred = $gist | Test-GitHubGistStar + It 'Should no longer be starred' { + $starred | Should -BeFalse + } + + $gist | Set-GitHubGistStar -Star + $starred = $gist | Test-GitHubGistStar + It 'Should now be starred' { + $starred | Should -BeTrue + } + + $gist | Set-GitHubGistStar + $starred = $gist | Test-GitHubGistStar + It 'Should no longer be starred' { + $starred | Should -BeFalse + } + } + } + + Describe 'New-GitHubGist' { + Context 'By content' { + BeforeAll { + $content = 'This is my content' + $filename = 'sample.txt' + $description = 'my description' + } + + $gist = New-GitHubGist -FileName $filename -Content $content -Public + It 'Should have the expected result' { + $gist.public | Should -BeTrue + $gist.description | Should -BeNullOrEmpty + $gist.files.$filename.content | Should -Be $content + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should be removed' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + + $gist = New-GitHubGist -FileName $filename -Content $content -Description $description -Public:$false + It 'Should have the expected result' { + $gist.public | Should -BeFalse + $gist.description | Should -Be $description + $gist.files.$filename.content | Should -Be $content + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should error if file starts with "gistfile"' { + { New-GitHubGist -FileName 'gistfile1' -Content $content } | Should -Throw + } + + It 'Should be removed' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + + Context 'By files' { + BeforeAll { + $tempFile = New-TemporaryFile + $fileA = "$($tempFile.FullName).ps1" + Move-Item -Path $tempFile -Destination $fileA + $fileAName = (Get-Item -Path $fileA).Name + $fileAContent = 'fileA content' + Out-File -FilePath $fileA -InputObject $fileAContent -Encoding utf8 + + $tempFile = New-TemporaryFile + $fileB = "$($tempFile.FullName).txt" + Move-Item -Path $tempFile -Destination $fileB + $fileBName = (Get-Item -Path $fileB).Name + $fileBContent = 'fileB content' + Out-File -FilePath $fileB -InputObject $fileBContent -Encoding utf8 + + $description = 'my description' + } + + AfterAll { + @($fileA, $fileB) | Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + $gist = New-GitHubGist -File @($fileA, $fileB) -Public + It 'Should have the expected result' { + $gist.public | Should -BeTrue + $gist.description | Should -BeNullOrEmpty + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$fileAName.content.Trim() | Should -Be $fileAContent + $gist.files.$fileBName.content.Trim() | Should -Be $fileBContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should be removed' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + + $gist = New-GitHubGist -File @($fileA, $fileB) -Description $description -Public:$false + It 'Should have the expected result' { + $gist.public | Should -BeFalse + $gist.description | Should -Be $description + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$fileAName.content.Trim() | Should -Be $fileAContent + $gist.files.$fileBName.content.Trim() | Should -Be $fileBContent + } + + It 'Should be removed' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + + $gist = @($fileA, $fileB) | New-GitHubGist -Description $description -Public:$false + It 'Should have the expected result with the files on the pipeline' { + $gist.public | Should -BeFalse + $gist.description | Should -Be $description + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$fileAName.content.Trim() | Should -Be $fileAContent + $gist.files.$fileBName.content.Trim() | Should -Be $fileBContent + } + } + } + + Describe 'Set-GitHubGist' { + BeforeAll { + $fileAName = 'foo.txt' + $fileAContent = 'foo content' + $fileAUpdatedContent = 'foo updated content' + $fileANewName = 'gamma.txt' + + $fileBName = 'bar.txt' + $fileBContent = 'bar content' + $fileBUpdatedContent = 'bar updated content' + + $fileCName = 'alpha.txt' + $fileCContent = 'alpha content' + $fileCUpdatedContent = 'alpha updated content' + $fileCNewName = 'gamma.txt' + + $tempFile = New-TemporaryFile + $fileD = "$($tempFile.FullName).txt" + Move-Item -Path $tempFile -Destination $fileD + $fileDName = (Get-Item -Path $fileD).Name + $fileDContent = 'fileD content' + Out-File -FilePath $fileD -InputObject $fileDContent -Encoding utf8 + $fileDUpdatedContent = 'fileD updated content' + + $description = 'my description' + $updatedDescription = 'updated description' + } + + AfterAll { + @($fileD) | Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + Context 'With parameters' { + BeforeAll { + $gist = New-GitHubGist -FileName $fileAName -Content $fileAContent -Description $description + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $fileBName -Content $fileBContent + It 'Should be in the expected, original state' { + $gist.description | Should -Be $description + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$fileAName.content | Should -Be $fileAContent + $gist.files.$fileBName.content | Should -Be $fileBContent + } + + $setParams = @{ + Gist = $gist.id + Description = $updatedDescription + Delete = @($fileBName) + Update = @{ + $fileAName = @{ + fileName = $fileANewName + content = $fileAUpdatedContent + } + $fileCName = @{ content = $fileCContent } + $fileDName = @{ filePath = $fileD } + } + } + + $gist = Set-GitHubGist @setParams -Force + It 'Should have been properly updated' { + $gist.description | Should -Be $updatedDescription + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 3 + $gist.files.$fileAName | Should -BeNullOrEmpty + $gist.files.$fileANewName.content | Should -Be $fileAContent + $gist.files.$fileBName | Should -BeNullOrEmpty + $gist.files.$fileCName.content | Should -Be $fileCContent + $gist.files.$fileDName.content.Trim() | Should -Be $fileDContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $setParams = @{ + Gist = $gist.id + Update = @{ + $fileDName = @{ + content = 'updated content' + filePath = $fileD + } + } + } + + It 'Should throw if updating a file with both a filePath and content' { + { $gist = Set-GitHubGist @setParams } | Should -Throw + } + } + + Context 'With the gist on the pipeline' { + BeforeAll { + $gist = New-GitHubGist -FileName $fileAName -Content $fileAContent -Description $description + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $fileBName -Content $fileBContent + It 'Should be in the expected, original state' { + $gist.description | Should -Be $description + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$fileAName.content | Should -Be $fileAContent + $gist.files.$fileBName.content | Should -Be $fileBContent + } + + $setParams = @{ + Description = $updatedDescription + Delete = @($fileBName) + Update = @{ + $fileAName = @{ + fileName = $fileANewName + content = $fileAUpdatedContent + } + $fileCName = @{ content = $fileCContent } + $fileDName = @{ filePath = $fileD } + } + } + + $gist = $gist | Set-GitHubGist @setParams -Confirm:$false + It 'Should have been properly updated' { + $gist.description | Should -Be $updatedDescription + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 3 + $gist.files.$fileAName | Should -BeNullOrEmpty + $gist.files.$fileANewName.content | Should -Be $fileAContent + $gist.files.$fileBName | Should -BeNullOrEmpty + $gist.files.$fileCName.content | Should -Be $fileCContent + $gist.files.$fileDName.content.Trim() | Should -Be $fileDContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $setParams = @{ + Update = @{ + $fileDName = @{ + content = 'updated content' + filePath = $fileD + } + } + } + + It 'Should throw if updating a file with both a filePath and content' { + { $gist = $gist | Set-GitHubGist @setParams } | Should -Throw + } + } + } + + Describe 'Set-GitHubGistFile' { + BeforeAll { + $origFileName = 'foo.txt' + $origContent = 'original content' + $updatedOrigContent = 'updated content' + + $newFileName = 'bar.txt' + $newContent = 'new content' + + $gist = New-GitHubGist -FileName $origFileName -Content $origContent + } + + AfterAll { + $gist | Remove-GitHubGist -Force + } + + Context 'By content with parameters' { + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $origFileName -Content $updatedOrigContent + It 'Should have the expected result' { + $gist.files.$origFileName.content | Should -Be $updatedOrigContent + $gist.files.$newFileName | Should -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $newFileName -Content $newContent + It 'Should have the expected result' { + $gist.files.$origFileName.content | Should -Be $updatedOrigContent + $gist.files.$newFileName.content | Should -Be $newContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $origFileName -Content $origContent + It 'Should remove the new file' { + { $gist | Remove-GitHubGistFile -FileName $newFileName -Force } | Should -Not -Throw + } + } + + Context 'By content with the gist on the pipeline' { + $gist = $gist | Set-GitHubGistFile -FileName $origFileName -Content $updatedOrigContent + It 'Should have the expected result' { + $gist.files.$origFileName.content | Should -Be $updatedOrigContent + $gist.files.$newFileName | Should -BeNullOrEmpty + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = $gist | Set-GitHubGistFile -FileName $newFileName -Content $newContent + It 'Should have the expected result' { + $gist.files.$origFileName.content | Should -Be $updatedOrigContent + $gist.files.$newFileName.content | Should -Be $newContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $origFileName -Content $origContent + It 'Should remove the new file' { + { $gist | Remove-GitHubGistFile -FileName $newFileName -Force } | Should -Not -Throw + } + } + + Context 'By files with parameters' { + BeforeAll { + $tempFile = New-TemporaryFile + $fileA = "$($tempFile.FullName).txt" + Move-Item -Path $tempFile -Destination $fileA + $fileAName = (Get-Item -Path $fileA).Name + $fileAContent = 'fileA content' + Out-File -FilePath $fileA -InputObject $fileAContent -Encoding utf8 + $fileAUpdatedContent = 'fileA content updated' + } + + AfterAll { + @($fileA) | Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + $gist = Set-GitHubGistFile -Gist $gist.id -File $fileA + It 'Should have the expected result' { + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$origFileName.content | Should -Be $origContent + $gist.files.$fileAName.content.Trim() | Should -Be $fileAContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + Out-File -FilePath $fileA -InputObject $fileAUpdatedContent -Encoding utf8 + $gist = Set-GitHubGistFile -Gist $gist.id -File $fileA + It 'Should have the expected result' { + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$origFileName.content | Should -Be $origContent + $gist.files.$fileAName.content.Trim() | Should -Be $fileAUpdatedContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $origFileName -Content $origContent + It 'Should remove the new file' { + { $gist | Remove-GitHubGistFile -FileName $fileAName -Force } | Should -Not -Throw + } + } + + Context 'By files with the gist on the pipeline' { + BeforeAll { + $tempFile = New-TemporaryFile + $fileA = "$($tempFile.FullName).txt" + Move-Item -Path $tempFile -Destination $fileA + $fileAName = (Get-Item -Path $fileA).Name + $fileAContent = 'fileA content' + Out-File -FilePath $fileA -InputObject $fileAContent -Encoding utf8 + $fileAUpdatedContent = 'fileA content updated' + } + + AfterAll { + @($fileA) | Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + $gist = $gist | Set-GitHubGistFile -File $fileA + It 'Should have the expected result' { + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$origFileName.content | Should -Be $origContent + $gist.files.$fileAName.content.Trim() | Should -Be $fileAContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + Out-File -FilePath $fileA -InputObject $fileAUpdatedContent -Encoding utf8 + $gist = $gist | Set-GitHubGistFile -File $fileA + It 'Should have the expected result' { + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$origFileName.content | Should -Be $origContent + $gist.files.$fileAName.content.Trim() | Should -Be $fileAUpdatedContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $origFileName -Content $origContent + It 'Should remove the new file' { + { $gist | Remove-GitHubGistFile -FileName $fileAName -Force } | Should -Not -Throw + } + } + + Context 'By files with the file on the pipeline' { + BeforeAll { + $tempFile = New-TemporaryFile + $fileA = "$($tempFile.FullName).txt" + Move-Item -Path $tempFile -Destination $fileA + $fileAName = (Get-Item -Path $fileA).Name + $fileAContent = 'fileA content' + Out-File -FilePath $fileA -InputObject $fileAContent -Encoding utf8 + $fileAUpdatedContent = 'fileA content updated' + } + + AfterAll { + @($fileA) | Remove-Item -Force -ErrorAction SilentlyContinue | Out-Null + } + + $gist = $fileA | Set-GitHubGistFile -Gist $gist.id + It 'Should have the expected result' { + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$origFileName.content | Should -Be $origContent + $gist.files.$fileAName.content.Trim() | Should -Be $fileAContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + Out-File -FilePath $fileA -InputObject $fileAUpdatedContent -Encoding utf8 + $gist = $fileA | Set-GitHubGistFile -Gist $gist.id + It 'Should have the expected result' { + ($gist.files | Get-Member -Type NoteProperty).Count | Should -Be 2 + $gist.files.$origFileName.content | Should -Be $origContent + $gist.files.$fileAName.content.Trim() | Should -Be $fileAUpdatedContent + } + + It 'Should have the expected type and additional properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + $gist = Set-GitHubGistFile -Gist $gist.id -FileName $origFileName -Content $origContent + It 'Should remove the new file' { + { $gist | Remove-GitHubGistFile -FileName $fileAName -Force } | Should -Not -Throw + } + } + } + + Describe 'Rename-GitHubGistFile' { + BeforeAll { + $originalName = 'foo.txt' + $newName = 'bar.txt' + $content = 'sample content' + } + + Context 'With parameters' { + $gist = New-GitHubGist -FileName $originalName -Content $content + It 'Should have the expected file' { + $gist.files.$originalName.content | Should -Be $content + $gist.files.$newName | Should -BeNullOrEmpty + } + + $gist = Rename-GitHubGistFile -Gist $gist.id -FileName $originalName -NewName $newName + It 'Should have been renamed' { + $gist.files.$originalName | Should -BeNullOrEmpty + $gist.files.$newName.content | Should -Be $content + } + + It 'Should have the expected additional type and properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should successfully remove the gist' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + + Context 'With the gist on the pipeline' { + $gist = New-GitHubGist -FileName $originalName -Content $content + It 'Should have the expected file' { + $gist.files.$originalName.content | Should -Be $content + $gist.files.$newName | Should -BeNullOrEmpty + } + + $gist = $gist | Rename-GitHubGistFile -FileName $originalName -NewName $newName + It 'Should have been renamed' { + $gist.files.$originalName | Should -BeNullOrEmpty + $gist.files.$newName.content | Should -Be $content + } + + It 'Should have the expected additional type and properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should successfully remove the gist' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + } + + Describe 'Remove-GitHubGistFile' { + BeforeAll { + $fileName = 'sample.txt' + $content = 'sample' + } + + Context 'With parameters' { + $gist = New-GitHubGist -FileName $fileName -Content $content + It 'Should have the expected file' { + $gist.files.$fileName | Should -Not -BeNullOrEmpty + } + + $gist = Remove-GitHubGistFile -Gist $gist.id -FileName $fileName -Force + It 'Should have been removed' { + $gist.files.$fileName | Should -BeNullOrEmpty + } + + It 'Should have the expected additional type and properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should successfully remove the gist' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + + Context 'With the gist on the pipeline' { + $gist = New-GitHubGist -FileName $fileName -Content $content + It 'Should have the expected file' { + $gist.files.$fileName | Should -Not -BeNullOrEmpty + } + + $gist = $gist | Remove-GitHubGistFile -FileName $fileName -Confirm:$false + It 'Should have been removed' { + $gist.files.$fileName | Should -BeNullOrEmpty + } + + It 'Should have the expected additional type and properties' { + $gist.PSObject.TypeNames[0] | Should -Be 'GitHub.Gist' + $gist.GistId | Should -Be $gist.id + $gist.owner.PSObject.TypeNames[0] | Should -Be 'GitHub.User' + } + + It 'Should successfully remove the gist' { + { $gist | Remove-GitHubGist -Force } | Should -Not -Throw + } + } + } +} +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 + } +} diff --git a/USAGE.md b/USAGE.md index 8e93a1c5..456c0186 100644 --- a/USAGE.md +++ b/USAGE.md @@ -97,6 +97,18 @@ * [Create a release asset](#create-a-release-asset) * [Update a release asset](#update-a-release-asset) * [Remove a release asset](#remove-a-release-asset) + * [Gists](#gists) + * [Getting gists](#getting-gists) + * [Download a gist](#download-a-gist) + * [Fork a gist](#fork-a-gist) + * [Creating a gist](#creating-a-gist) + * [Removing a gist](#removing-a-gist) + * [Updating a gist](#updating-a-gist) + * [Starring a gist](#starring-a-gist) + * [Getting gist comments](#getting-gist-comments) + * [Adding a gist comment](#adding-a-gist-comment) + * [Changing a gist comment](#changing-a-gist-comment) + * [Removing a gist comment](#removing-a-gist-comment) * [Advanced](#advanced) * [Migrating blog comments to GitHub issues](#migrating-blog-comments-to-github-issues) @@ -869,6 +881,169 @@ or with pipelining... ```powershell $asset | Remove-GitHubReleaseAsset -force + +---------- + +### Gists + +#### Getting gists +```powershell +# There are many options here: + +# 1. Getting all gists for the current authenticated user: +Get-GitHubGist + +# 1b. Getting all gists for the current authenticated user that were updated in the past 6 days. +Get-GitHubGist -Since ((Get-Date).AddDays(-6)) + +# 2. Get all starred gists for the current authenticated user +Get-GitHubGist -Starred + +# 3. Get all public gists for a specific user +Get-GitHubGist -UserName 'octocat' + +# 4. Get all public gists (well, the first 3000): +Get-GitHubGist -Public + +# 5. Get a specific gist +Get-GitHubGist -Gist '6cad326836d38bd3a7ae' + +# 5a. List all commits for a specific gist +Get-GitHubGist -Gist '6cad326836d38bd3a7ae' -Commits + +# 5b. Get a gist at a specific commit (Sha) +Get-GitHubGist -Gist '6cad326836d38bd3a7ae' -Sha 'de5b9b59d1f28206e8d646c7c8025e9809d0ed73' + +# 5c. Get all of the forks for a gist +Get-GitHubGist -Gist '6cad326836d38bd3a7ae' -Forks +``` + +#### Download a gist +```powershell +Get-GitHubGist -Gist '6cad326836d38bd3a7ae' -Path 'c:\users\octocat\downloads\gist\' +``` + +#### Fork a gist +```powershell +Fork-GitHubGist -Gist '6cad326836d38bd3a7ae' +``` + +#### Creating a gist +```powershell +# You can create a gist by specifying a single file's content in-line... +New-GitHubGist -FileName 'foo.txt' -Content 'foo content' + +# or by providing one or more files that should be part of the gist +New-GitHubGist -File @('c:\files\foo.txt', 'c:\files\bar.txt') +@('c:\files\foo.txt', 'c:\files\bar.txt') | New-GitHubGist +``` + +#### Removing a gist +```powershell +Remove-GitHubGist -Gist '6cad326836d38bd3a7ae' +``` + +#### Updating a gist +```powershell +$gist = New-GitHubGist -FileName 'foo.txt' -Content 'content' + +# The main method to use is Set-GitHubGist, however it is quite complicated. +$params = @{ + Description = 'new description' # modifies the description of the gist + Update = @{ + 'foo.txt' = @{ + fileName = 'alpha.txt' # Will rename foo.txt -> alpha.txt + content = 'updated content' # and will also update its content + } + 'bar.txt' = @{ + filePath = 'c:\files\bar.txt' # Will upload the content of bar.txt to the gist. + } + } + Delete = @('bar.txt') + Force = $true # avoid confirmation prompting due to the deletion +} + +Set-GitHubGist -Gist $gist.id @params + +# Therefore, you can use simpler helper methods to accomplish atomic tasks +Set-GistHubGistFile -Gist $gist.id -FileName 'foo.txt' -Content 'updated content' + +# This will update the text in the existing file 'foo.txt' and add the file 'bar.txt' +$gist | Set-GitHubGistFile -File ('c:\files\foo.txt', 'c:\files\bar.txt') + +Rename-GistHubGistFile -Gist $gist.id -FileName 'foo.txt' -NewName 'bar.txt' + +$gist | Remove-GitHubGistFile -FileName 'bar.txt' -Force + +``` + +#### Starring a gist +```powershell +$gistId = '6cad326836d38bd3a7ae' + +# All of these options will star the same gist +Star-GitHubGist -Gist $gistId +Add-GitHubGistStar -Gist $gistId +Set-GitHubGistStar -Gist $gistId -Star +Get-GitHubGist -Gist $gistId | Star-GitHubGist + +# All of these options will unstar the same gist +Unstar-GitHubGist -Gist $gistId +Remove-GitHubGistStar -Gist $gistId +Set-GitHubGistStar -Gist $gistId +Set-GitHubGistStar -Gist $gistId -Star:$false +Get-GitHubGist -Gist $gistId | Unstar-GitHubGist + +# All of these options will tell you if you have starred a gist +Test-GitHubGistStar -Gist $gistId +Get-GitHubGist -Gist $gistId | Test-GitHubGistStar +``` + +#### Getting gist comments +```powershell +$gistId = '6cad326836d38bd3a7ae' +$commentId = 1507813 + +# You can get all comments for a gist with any of these options: +Get-GitHubGistComment -Gist $gistId +Get-GitHubGist -Gist $gistId | Get-GitHubGistComment + +# You can retrieve an individual comment like this: +Get-GitHubGistComment -Gist $gistId -Comment $commentId +``` + +#### Adding a gist comment +```powershell +$gistId = '6cad326836d38bd3a7ae' + +New-GitHubGistComment -Gist $gistId -Body 'Hello World' + +# or with the pipeline +Get-GitHubGist -Gist $gistId | New-GitHubGistComment -Body 'Hello World' +``` + +#### Changing a gist comment +```powershell +$gistId = '6cad326836d38bd3a7ae' +$commentId = 1507813 + +Set-GitHubGistComment -Gist $gistId -Comment $commentId -Body 'Updated comment' + +# or with the pipeline +Get-GitHubGist -Gist $gistId -Comment $commentId | Set-GitHubGistComment -Body 'Updated comment' +``` + +#### Removing a gist comment +```powershell +$gistId = '6cad326836d38bd3a7ae' +$commentId = 1507813 + +# If you don't specify -Force, it will prompt for confirmation before it will delete the comment + +Remove-GitHubGistComment -Gist $gistId -Comment $commentId -Force + +# or with the pipeline +Get-GitHubGist -Gist $gistId -Comment $commentId | Remove-GitHubGistComment -Force ``` ----------