diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index f595ace7..2e52cf40 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -154,6 +154,352 @@ filter Get-GitHubRepositoryBranch return (Invoke-GHRestMethodMultipleResult @params | Add-GitHubBranchAdditionalProperties) } +filter New-GitHubRepositoryBranch +{ + <# + .SYNOPSIS + Creates a new branch for a given GitHub repository. + + .DESCRIPTION + Creates a new branch for a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER BranchName + The name of the origin branch to create the new branch from. + + .PARAMETER TargetBranchName + Name of the branch to be created. + + .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.Branch + GitHub.Content + GitHub.Event + GitHub.Issue + GitHub.IssueComment + GitHub.Label + GitHub.Milestone + GitHub.PullRequest + GitHub.Project + GitHub.ProjectCard + GitHub.ProjectColumn + GitHub.Release + GitHub.Repository + + .OUTPUTS + GitHub.Branch + + .EXAMPLE + New-GitHubRepositoryBranch -OwnerName microsoft -RepositoryName PowerShellForGitHub -TargetBranchName new-branch + + Creates a new branch in the specified repository from the master branch. + + .EXAMPLE + New-GitHubRepositoryBranch -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName develop -TargetBranchName new-branch + + Creates a new branch in the specified repository from the 'develop' origin branch. + + .EXAMPLE + $repo = Get-GithubRepository -Uri https://github.com/You/YourRepo + $repo | New-GitHubRepositoryBranch -TargetBranchName new-branch + + You can also pipe in a repo that was returned from a previous command. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParameterSetName = 'Elements', + PositionalBinding = $false + )] + [OutputType({$script:GitHubBranchTypeName})] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', + Justification = 'Methods called within here make use of PSShouldProcess, and the switch is + passed on to them inherently.')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'One or more parameters (like NoStatus) are only referenced by helper + methods which get access to it from the stack via Get-Variable -Scope 1.')] + [Alias('New-GitHubBranch')] + param( + [Parameter(ParameterSetName = 'Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName = 'Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1, + ParameterSetName = 'Uri')] + [Alias('RepositoryUrl')] + [string] $Uri, + + [string] $BranchName = 'master', + + [Parameter( + Mandatory, + ValueFromPipeline, + Position = 2)] + [string] $TargetBranchName, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $originBranch = $null + + try + { + $getGitHubRepositoryBranchParms = @{ + OwnerName = $OwnerName + RepositoryName = $RepositoryName + BranchName = $BranchName + Whatif = $false + Confirm = $false + } + if ($PSBoundParameters.ContainsKey('AccessToken')) + { + $getGitHubRepositoryBranchParms['AccessToken'] = $AccessToken + } + if ($PSBoundParameters.ContainsKey('NoStatus')) + { + $getGitHubRepositoryBranchParms['NoStatus'] = $NoStatus + } + + Write-Log -Level Verbose "Getting $BranchName branch for sha reference" + $originBranch = Get-GitHubRepositoryBranch @getGitHubRepositoryBranchParms + } + catch + { + # Temporary code to handle current differences in exception object between PS5 and PS7 + $throwObject = $_ + + if ($PSVersionTable.PSedition -eq 'Core') + { + if ($_.Exception -is [Microsoft.PowerShell.Commands.HttpResponseException] -and + ($_.ErrorDetails.Message | ConvertFrom-Json).message -eq 'Branch not found') + { + $throwObject = "Origin branch $BranchName not found" + } + } + else + { + if ($_.Exception.Message -like '*Not Found*') + { + $throwObject = "Origin branch $BranchName not found" + } + } + + Write-Log -Message $throwObject -Level Error + throw $throwObject + } + + $uriFragment = "repos/$OwnerName/$RepositoryName/git/refs" + + $hashBody = @{ + ref = "refs/heads/$TargetBranchName" + sha = $originBranch.commit.sha + } + + $params = @{ + 'UriFragment' = $uriFragment + 'Body' = (ConvertTo-Json -InputObject $hashBody) + 'Method' = 'Post' + 'Description' = "Creating branch $TargetBranchName for $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | Add-GitHubBranchAdditionalProperties) +} + +filter Remove-GitHubRepositoryBranch +{ + <# + .SYNOPSIS + Removes a branch from a given GitHub repository. + + .DESCRIPTION + Removes a branch from a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER BranchName + Name of the branch to be removed. + + .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.Branch + GitHub.Content + GitHub.Event + GitHub.Issue + GitHub.IssueComment + GitHub.Label + GitHub.Milestone + GitHub.PullRequest + GitHub.Project + GitHub.ProjectCard + GitHub.ProjectColumn + GitHub.Release + GitHub.Repository + + .OUTPUTS + None + + .EXAMPLE + Remove-GitHubRepositoryBranch -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName develop + + Removes the 'develop' branch from the specified repository. + + .EXAMPLE + Remove-GitHubRepositoryBranch -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName develop -Force + + Removes the 'develop' branch from the specified repository without prompting for confirmation. + + .EXAMPLE + $branch = Get-GitHubRepositoryBranch -Uri https://github.com/You/YourRepo -BranchName BranchToDelete + $branch | Remove-GitHubRepositoryBranch -Force + + You can also pipe in a repo that was returned from a previous command. +#> + [CmdletBinding( + SupportsShouldProcess, + DefaultParameterSetName = 'Elements', + PositionalBinding = $false, + ConfirmImpact = 'High')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", + Justification = "Methods called within here make use of PSShouldProcess, and the switch is + passed on to them inherently.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", + Justification = "One or more parameters (like NoStatus) are only referenced by helper + methods which get access to it from the stack via Get-Variable -Scope 1.")] + [Alias('Remove-GitHubBranch')] + [Alias('Delete-GitHubRepositoryBranch')] + [Alias('Delete-GitHubBranch')] + param( + [Parameter(ParameterSetName = 'Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName = 'Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1, + ParameterSetName = 'Uri')] + [Alias('RepositoryUrl')] + [string] $Uri, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 2)] + [string] $BranchName, + + [switch] $Force, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $uriFragment = "repos/$OwnerName/$RepositoryName/git/refs/heads/$BranchName" + + if ($Force -and (-not $Confirm)) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ShouldProcess($BranchName, "Remove Repository Branch")) + { + Write-InvocationLog + + $params = @{ + 'UriFragment' = $uriFragment + 'Method' = 'Delete' + 'Description' = "Deleting branch $BranchName from $RepositoryName" + 'AccessToken' = $AccessToken + 'TelemetryEventName' = $MyInvocation.MyCommand.Name + 'TelemetryProperties' = $telemetryProperties + 'NoStatus' = (Resolve-ParameterWithDefaultConfigurationValue ` + -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + Invoke-GHRestMethod @params | Out-Null + } +} + filter Add-GitHubBranchAdditionalProperties { <# @@ -192,11 +538,25 @@ filter Add-GitHubBranchAdditionalProperties if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) { - $elements = Split-GitHubUri -Uri $item.commit.url + if ($null -ne $item.url) + { + $elements = Split-GitHubUri -Uri $item.url + } + else + { + $elements = Split-GitHubUri -Uri $item.commit.url + } $repositoryUrl = Join-GitHubUri @elements + Add-Member -InputObject $item -Name 'RepositoryUrl' -Value $repositoryUrl -MemberType NoteProperty -Force - Add-Member -InputObject $item -Name 'BranchName' -Value $item.name -MemberType NoteProperty -Force + $branchName = $item.name + if ($null -eq $branchName) + { + $branchName = $item.ref -replace ('refs/heads/', '') + } + + Add-Member -InputObject $item -Name 'BranchName' -Value $branchName -MemberType NoteProperty -Force } Write-Output $item diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index cb7fcf06..a15992a6 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -121,6 +121,7 @@ 'New-GitHubPullRequest', 'New-GitHubRepository', 'New-GitHubRepositoryFromTemplate', + 'New-GitHubRepositoryBranch', 'New-GitHubRepositoryFork', 'Remove-GitHubAssignee', 'Remove-GitHubIssueComment', @@ -131,6 +132,7 @@ 'Remove-GitHubProjectCard', 'Remove-GitHubProjectColumn', 'Remove-GitHubRepository', + 'Remove-GitHubRepositoryBranch' 'Rename-GitHubRepository', 'Reset-GitHubConfiguration', 'Restore-GitHubConfiguration', @@ -157,6 +159,7 @@ ) AliasesToExport = @( + 'Delete-GitHubBranch', 'Delete-GitHubComment', 'Delete-GitHubIssueComment', 'Delete-GitHubLabel', @@ -165,10 +168,13 @@ 'Delete-GitHubProjectCard', 'Delete-GitHubProjectColumn' 'Delete-GitHubRepository', + 'Delete-GitHubRepositoryBranch', 'Get-GitHubBranch', 'Get-GitHubComment', 'New-GitHubAssignee', + 'New-GitHubBranch', 'New-GitHubComment', + 'Remove-GitHubBranch' 'Remove-GitHubComment', 'Set-GitHubComment', 'Transfer-GitHubRepositoryOwnership' diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index f0df1ca9..2d52a19b 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -39,7 +39,7 @@ try $branches.name | Should -Contain $branchName } - It 'Should have the exected type and addititional properties' { + It 'Should have the expected type and addititional properties' { $branches[0].PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' $branches[0].RepositoryUrl | Should -Be $repo.RepositoryUrl $branches[0].BranchName | Should -Be $branches[0].name @@ -57,7 +57,7 @@ try $branches.name | Should -Contain $branchName } - It 'Should have the exected type and addititional properties' { + It 'Should have the expected type and addititional properties' { $branches[0].PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' $branches[0].RepositoryUrl | Should -Be $repo.RepositoryUrl $branches[0].BranchName | Should -Be $branches[0].name @@ -71,7 +71,7 @@ try $branch.name | Should -Be $branchName } - It 'Should have the exected type and addititional properties' { + It 'Should have the expected type and addititional properties' { $branch.PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' $branch.RepositoryUrl | Should -Be $repo.RepositoryUrl $branch.BranchName | Should -Be $branch.name @@ -85,7 +85,7 @@ try $branch.name | Should -Be $branchName } - It 'Should have the exected type and addititional properties' { + It 'Should have the expected type and addititional properties' { $branch.PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' $branch.RepositoryUrl | Should -Be $repo.RepositoryUrl $branch.BranchName | Should -Be $branch.name @@ -100,13 +100,236 @@ try $branchAgain.name | Should -Be $branchName } - It 'Should have the exected type and addititional properties' { + It 'Should have the expected type and addititional properties' { $branchAgain.PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' $branchAgain.RepositoryUrl | Should -Be $repo.RepositoryUrl $branchAgain.BranchName | Should -Be $branchAgain.name } } } + + Describe 'GitHubBranches\New-GitHubRepositoryBranch' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + $originBranchName = 'master' + $newGitHubRepositoryParms = @{ + RepositoryName = $repoName + AutoInit = $true + } + + $repo = New-GitHubRepository @newGitHubRepositoryParms + } + + Context 'When creating a new GitHub repository branch' { + Context 'When using non-pipelined parameters' { + BeforeAll { + $newBranchName = 'develop1' + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $originBranchName + TargetBranchName = $newBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + } + + It 'Should have the expected type and addititional properties' { + $branch.PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' + $branch.RepositoryUrl | Should -Be $repo.RepositoryUrl + $branch.BranchName | Should -Be $newBranchName + } + + It 'Should have created the branch' { + $getGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + } + + { Get-GitHubRepositoryBranch @getGitHubRepositoryBranchParms } | + Should -Not -Throw + } + } + + Context 'When using pipelined parameters' { + Context 'When providing pipeline input for the "Uri" parameter' { + BeforeAll { + $newBranchName = 'develop2' + $branch = $repo | New-GitHubRepositoryBranch -TargetBranchName $newBranchName + } + + It 'Should have the expected type and addititional properties' { + $branch.PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' + $branch.RepositoryUrl | Should -Be $repo.RepositoryUrl + $branch.BranchName | Should -Be $newBranchName + } + + It 'Should have created the branch' { + $getGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + } + + { Get-GitHubRepositoryBranch @getGitHubRepositoryBranchParms } | + Should -Not -Throw + } + } + + Context 'When providing pipeline input for the "TargetBranchName" parameter' { + BeforeAll { + $newBranchName = 'develop3' + $branch = $newBranchName | New-GitHubRepositoryBranch -Uri $repo.html_url + } + + It 'Should have the expected type and addititional properties' { + $branch.PSObject.TypeNames[0] | Should -Be 'GitHub.Branch' + $branch.RepositoryUrl | Should -Be $repo.RepositoryUrl + $branch.BranchName | Should -Be $newBranchName + } + + It 'Should have created the branch' { + $getGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + } + + { Get-GitHubRepositoryBranch @getGitHubRepositoryBranchParms } | + Should -Not -Throw + } + } + } + + Context 'When the origin branch cannot be found' { + BeforeAll -Scriptblock { + $missingOriginBranchName = 'Missing-Branch' + } + + It 'Should throw the correct exception' { + $errorMessage = "Origin branch $missingOriginBranchName not found" + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $missingOriginBranchName + TargetBranchName = $newBranchName + } + + { New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms } | + Should -Throw $errorMessage + } + } + + Context 'When Get-GitHubRepositoryBranch throws an undefined HttpResponseException' { + It 'Should throw the correct exception' { + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = 'test' + BranchName = 'test' + TargetBranchName = 'test' + } + + { New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms } | + Should -Throw 'Not Found' + } + } + } + + AfterAll -ScriptBlock { + if (Get-Variable -Name repo -ErrorAction SilentlyContinue) + { + Remove-GitHubRepository -Uri $repo.svn_url -Confirm:$false + } + } + } + + Describe 'GitHubBranches\Remove-GitHubRepositoryBranch' { + BeforeAll -Scriptblock { + $repoName = [Guid]::NewGuid().Guid + $originBranchName = 'master' + $newGitHubRepositoryParms = @{ + RepositoryName = $repoName + AutoInit = $true + } + + $repo = New-GitHubRepository @newGitHubRepositoryParms + } + + Context 'When using non-pipelined parameters' { + BeforeAll { + $newBranchName = 'develop1' + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $originBranchName + TargetBranchName = $newBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + } + + It 'Should not throw an exception' { + $removeGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + Confirm = $false + } + + { Remove-GitHubRepositoryBranch @removeGitHubRepositoryBranchParms } | + Should -Not -Throw + } + + It 'Should have removed the branch' { + $getGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + } + + { Get-GitHubRepositoryBranch @getGitHubRepositoryBranchParms } | + Should -Throw + } + } + + Context 'When using pipelined parameters' { + BeforeAll { + $newBranchName = 'develop2' + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $originBranchName + TargetBranchName = $newBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + } + + It 'Should not throw an exception' { + { $branch | Remove-GitHubRepositoryBranch -Force } | Should -Not -Throw + } + + It 'Should have removed the branch' { + $getGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + } + + { Get-GitHubRepositoryBranch @getGitHubRepositoryBranchParms } | + Should -Throw + } + } + + AfterAll -ScriptBlock { + if (Get-Variable -Name repo -ErrorAction SilentlyContinue) + { + Remove-GitHubRepository -Uri $repo.svn_url -Confirm:$false + } + } + } } finally { diff --git a/USAGE.md b/USAGE.md index 441081e7..bdd04488 100644 --- a/USAGE.md +++ b/USAGE.md @@ -39,6 +39,9 @@ * [Disable repository vulnerability alerts](#disable-repository-vulnerability-alerts) * [Enable repository automatic security fixes](#enable-repository-automatic-security-fixes) * [Disable repository automatic security fixes](#disable-repository-automatic-security-fixes) + * [Branches](#branches) + * [Adding a new Branch to a Repository](#adding-a-new-branch-to-a-repository) + * [Removing a Branch from a Repository](#removing-a-branch-from-a-repository) * [Forks](#forks) * [Get all the forks for a repository](#get-all-the-forks-for-a-repository) * [Create a new fork](#create-a-new-fork) @@ -432,6 +435,21 @@ Get-GitHubUser ``` > Warning: This will take a while. It's getting _every_ GitHub user. +---------- +### Repositories + +#### Adding a new Branch to a Repository + +```powershell +New-GitHubRepositoryBranch -OwnerName microsoft -RepositoryName PowerShellForGitHub -Name develop +``` + +#### Removing a Branch from a Repository + +```powershell +Remove-GitHubRepositoryBranch -OwnerName microsoft -RepositoryName PowerShellForGitHub -Name develop +``` + ---------- ### Repositories @@ -459,7 +477,8 @@ New-GitHubRepository -RepositoryName TestRepo -OrganizationName MyOrg -TeamId $m ```powershell New-GitHubRepositoryFromTemplate -OwnerName MyOrg -RepositoryName MyNewRepo-TemplateOwnerName MyOrg -TemplateRepositoryName MyTemplateRepo -======= +``` + #### Get repository vulnerability alert status ```powershell