diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index f595ace7..eaf3c653 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -154,6 +154,351 @@ 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) + } + + 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 $TargetBranchName 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 +537,28 @@ 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 + if ($null -ne $item.name) + { + $branchName = $item.name + } + else + { + $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 3504bf7c..0f11ca55 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -115,6 +115,7 @@ 'New-GitHubPullRequest', 'New-GitHubRepository', 'New-GitHubRepositoryFromTemplate', + 'New-GitHubRepositoryBranch', 'New-GitHubRepositoryFork', 'Remove-GitHubAssignee', 'Remove-GitHubIssueComment', @@ -125,6 +126,7 @@ 'Remove-GitHubProjectCard', 'Remove-GitHubProjectColumn', 'Remove-GitHubRepository', + 'Remove-GitHubRepositoryBranch' 'Rename-GitHubRepository', 'Reset-GitHubConfiguration', 'Restore-GitHubConfiguration', @@ -152,6 +154,7 @@ ) AliasesToExport = @( + 'Delete-GitHubBranch', 'Delete-GitHubComment', 'Delete-GitHubIssueComment', 'Delete-GitHubLabel', @@ -160,9 +163,12 @@ 'Delete-GitHubProjectCard', 'Delete-GitHubProjectColumn' 'Delete-GitHubRepository', + 'Delete-GitHubRepositoryBranch', 'Get-GitHubBranch', 'Get-GitHubComment', + '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..686e8ed5 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,163 @@ 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' { + Context 'When creating a new GitHub repository branch' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + $originBranchName = 'master' + $newBranchName = 'develop' + $newGitHubRepositoryParms = @{ + RepositoryName = $repoName + AutoInit = $true + } + + $repo = New-GitHubRepository @newGitHubRepositoryParms + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + OriginBranchName = $originBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + } + + It 'Should support pipeline input for the uri parameter' { + { $repo | New-GitHubRepositoryBranch -BranchName $newBranchName -WhatIf } | + Should -Not -Throw + } + + It 'Should support pipeline input for the BranchName parameter' { + { $newBranchName | New-GitHubRepositoryBranch -Uri $repo.html_url -WhatIf } | + Should -Not -Throw + } + + 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 = $newBranchName + OriginBranchName = $missingOriginBranchName + } + + { 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' + OriginBranchName = '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' + $newBranchName = 'develop' + $newGitHubRepositoryParms = @{ + RepositoryName = $repoName + AutoInit = $true + } + + $repo = New-GitHubRepository @newGitHubRepositoryParms + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:ownerName + RepositoryName = $repoName + BranchName = $newBranchName + OriginBranchName = $originBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + } + + It 'Should support pipeline input for the BranchName and Uri parameters' { + { $branch | Remove-GitHubRepositoryBranch -WhatIf } | Should -Not -Throw + } + + 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 + } + + 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 2b275c61..7b652e33 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) @@ -429,6 +432,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 @@ -456,7 +474,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