diff --git a/CHANGELOG.md b/CHANGELOG.md index bd37dc9..d064d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Viscalyx.Common + - Added unit tests to run in Windows PowerShell. +- Public commands: + - `Update-GitBranch` + - `Rename-GitLocalBranch` + +### Fixed + +- `ConvertTo-DifferenceString` + - Make it render ANSI sequences in Windows PowerShell + - Optimize using List\ instead of using `+=` for adding to arrays. + ## [0.2.0] - 2024-08-25 ### Added @@ -12,12 +26,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Public commands: - `ConvertTo-AnsiSequence` - `ConvertTo-DifferenceString` + - `Get-GitTag` - `Get-NumericalSequence` - `Get-PSReadLineHistory` + - `New-GitTag` - `New-SamplerGitHubReleaseTag` - `Out-Difference` - `Pop-VMLatestSnapShot` + - `Push-GitTag` - `Remove-History` - `Remove-PSHistory` - `Remove-PSReadLineHistory` + - `Request-GitTag` - `Split-StringAtIndices` + - `Switch-GitLocalBranch` + - `Test-GitLocalChanges` + - `Test-GitRemote` + - `Test-GitRemoteBranch` + - `Update-GitLocalBranch` + - `Update-RemoteTrackingBranch` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f1da216..7c4ad56 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -156,7 +156,7 @@ stages: inputs: testResultsFormat: 'NUnit' testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' - testRunTitle: 'Windows Server Core (PowerShell Core)' + testRunTitle: 'Windows Server (PowerShell Core)' - task: PublishPipelineArtifact@1 displayName: 'Publish Test Artifact' condition: succeededOrFailed() @@ -165,6 +165,40 @@ stages: artifactName: 'CodeCoverageWinPS7_$(System.JobAttempt)' parallel: true + - job: test_windows_powershell + displayName: 'Unit Windows (Windows PowerShell)' + timeoutInMinutes: 0 + pool: + vmImage: 'windows-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: test + displayName: 'Run Tests' + inputs: + filePath: './build.ps1' + arguments: '-tasks test' + pwsh: false + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Windows Server (Windows PowerShell)' + - task: PublishPipelineArtifact@1 + displayName: 'Publish Test Artifact' + condition: succeededOrFailed() + inputs: + targetPath: '$(buildFolderName)/$(testResultFolderName)/' + artifactName: 'CodeCoverageWinPS_$(System.JobAttempt)' + parallel: true + - job: test_macos displayName: 'Unit macOS' timeoutInMinutes: 0 @@ -234,6 +268,12 @@ stages: buildType: 'current' artifactName: 'CodeCoverageWinPS7_$(System.JobAttempt)' targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' + - task: DownloadPipelineArtifact@2 + displayName: 'Download Test Artifact Windows (WinPS)' + inputs: + buildType: 'current' + artifactName: 'CodeCoverageWinPS_$(System.JobAttempt)' + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' - task: PowerShell@2 name: merge displayName: 'Merge Code Coverage files' diff --git a/build.yaml b/build.yaml index 5197d51..0391581 100644 --- a/build.yaml +++ b/build.yaml @@ -101,7 +101,7 @@ Pester: StackTraceVerbosity: Full CIFormat: Auto CodeCoverage: - CoveragePercentTarget: 85 + CoveragePercentTarget: 60 OutputEncoding: ascii UseBreakpoints: false TestResult: diff --git a/source/Public/Assert-GitLocalChange.ps1 b/source/Public/Assert-GitLocalChange.ps1 new file mode 100644 index 0000000..eee48a6 --- /dev/null +++ b/source/Public/Assert-GitLocalChange.ps1 @@ -0,0 +1,37 @@ +<# + .SYNOPSIS + Asserts that there are no unstaged or staged changes in the local Git branch. + + .DESCRIPTION + The Assert-GitLocalChange command checks whether there are any unstaged + or staged changes in the local Git branch. If there are any staged or + unstaged changes, it throws a terminating error. + + .EXAMPLE + Assert-GitLocalChange + + This example demonstrates how to use the Assert-GitLocalChange command + to ensure that there are no local changes in the Git repository. +#> +function Assert-GitLocalChange +{ + [CmdletBinding()] + param () + + # Change the error action preference to always stop the script if an error occurs. + $ErrorActionPreference = 'Stop' + + $hasChanges = Test-GitLocalChanges + + if ($hasChanges) + { + $errorMessageParameters = @{ + Message = $script:localizedData.Assert_GitLocalChanges_FailedUnstagedChanges + Category = 'InvalidResult' + ErrorId = 'AGLC0001' # cspell: disable-line + TargetObject = 'Staged or unstaged changes' # cSpell: ignore unstaged + } + + Write-Error @errorMessageParameters + } +} diff --git a/source/Public/Assert-GitRemote.ps1 b/source/Public/Assert-GitRemote.ps1 new file mode 100644 index 0000000..0d6a4d3 --- /dev/null +++ b/source/Public/Assert-GitRemote.ps1 @@ -0,0 +1,54 @@ +<# + .SYNOPSIS + Checks if the specified Git remote exists locally and throws an error if it doesn't. + + .DESCRIPTION + The `Assert-GitRemote` command checks if the remote specified in the `$Name` + parameter exists locally. If the remote doesn't exist, it throws an error. + + .PARAMETER RemoteName + Specifies the name of the Git remote to check. + + .EXAMPLE + PS> Assert-GitRemote -Name "origin" + + This example checks if the Git remote named "origin" exists locally. + + .INPUTS + None. + + .OUTPUTS + None. +#> +function Assert-GitRemote +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, Position = 0)] + [System.String] + $Name + ) + + # Change the error action preference to always stop the script if an error occurs. + $ErrorActionPreference = 'Stop' + + <# + Check if the remote specified in $UpstreamRemoteName exists locally and + throw an error if it doesn't. + #> + $remoteExists = Test-GitRemote -Name $Name + + if (-not $remoteExists) + { + + $errorMessageParameters = @{ + Message = $script:localizedData.New_SamplerGitHubReleaseTag_RemoteMissing -f $Name + Category = 'ObjectNotFound' + ErrorId = 'AGR0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + } +} diff --git a/source/Public/ConvertTo-AnsiSequence.ps1 b/source/Public/ConvertTo-AnsiSequence.ps1 index 634a52e..6cd7a3e 100644 --- a/source/Public/ConvertTo-AnsiSequence.ps1 +++ b/source/Public/ConvertTo-AnsiSequence.ps1 @@ -44,7 +44,8 @@ function ConvertTo-AnsiSequence { if ($Value -match "^(?:`e)?\[?([0-9;]+)m?$") { - $Value = "`e[" + $Matches[1] + 'm' + # Cannot use `e in Windows PowerShell, so use [char]0x1b instead. + $Value = "$([System.Char] 0x1b)[" + $Matches[1] + 'm' } } diff --git a/source/Public/ConvertTo-DifferenceString.ps1 b/source/Public/ConvertTo-DifferenceString.ps1 index 000088e..25d3a02 100644 --- a/source/Public/ConvertTo-DifferenceString.ps1 +++ b/source/Public/ConvertTo-DifferenceString.ps1 @@ -168,11 +168,11 @@ function ConvertTo-DifferenceString # Determine the maximum length of the two byte arrays $maxLength = [Math]::Max($referenceBytes.Length, $differenceBytes.Length) - # Initialize arrays to hold hex values and characters - $refHexArray = @() - $refCharArray = @() - $diffHexArray = @() - $diffCharArray = @() + # Initialize lists to hold hex values and characters + $refHexList = [System.Collections.Generic.List[string]]::new() + $refCharList = [System.Collections.Generic.List[string]]::new() + $diffHexList = [System.Collections.Generic.List[string]]::new() + $diffCharList = [System.Collections.Generic.List[string]]::new() # Escape $HighlightStart and $HighlightEnd for regex matching $escapedHighlightStart = [regex]::Escape($HighlightStart) @@ -230,8 +230,11 @@ function ConvertTo-DifferenceString $diffChar = "$($HighlightStart)$diffChar$($HighlightEnd)" } - # Replace control characters with their Unicode representations in the output - $refChar = $refChar` + <# + Replace control characters with their Unicode representations. + Cannot use `e in Windows PowerShell, so use [char]0x1b instead. + #> + $refChar = $refChar ` -replace "`0", '␀' ` -replace "`a", '␇' ` -replace "`b", '␈' ` @@ -239,7 +242,7 @@ function ConvertTo-DifferenceString -replace "`f", '␌' ` -replace "`r", '␍' ` -replace "`n", '␊' ` - -replace "(?!$($escapedHighlightStart))(?!$($escapedHighlightEnd))`e", '␛' + -replace "(?!$($escapedHighlightStart))(?!$($escapedHighlightEnd))$([System.Char] 0x1b)", '␛' $diffChar = $diffChar ` -replace "`0", '␀' ` @@ -249,39 +252,39 @@ function ConvertTo-DifferenceString -replace "`f", '␌' ` -replace "`r", '␍' ` -replace "`n", '␊' ` - -replace "(?!$($escapedHighlightStart))(?!$($escapedHighlightEnd))`e", '␛' + -replace "(?!$($escapedHighlightStart))(?!$($escapedHighlightEnd))$([System.Char] 0x1b)", '␛' - # Add to arrays - $refHexArray += $refHex - $refCharArray += $refChar - $diffHexArray += $diffHex - $diffCharArray += $diffChar + # Add to lists + $refHexList.Add($refHex) + $refCharList.Add($refChar) + $diffHexList.Add($diffHex) + $diffCharList.Add($diffChar) # Output the results in groups of 16 if (($i + 1) % 16 -eq 0 -or $i -eq $maxLength - 1) { - # Pad arrays to ensure they have 16 elements - while ($refHexArray.Count -lt 16) + # Pad lists to ensure they have 16 elements + while ($refHexList.Count -lt 16) { - $refHexArray += ' ' + $refHexList.Add(' ') } - while ($refCharArray.Count -lt 16) + while ($refCharList.Count -lt 16) { - $refCharArray += ' ' + $refCharList.Add(' ') } - while ($diffHexArray.Count -lt 16) + while ($diffHexList.Count -lt 16) { - $diffHexArray += ' ' + $diffHexList.Add(' ') } - while ($diffCharArray.Count -lt 16) + while ($diffCharList.Count -lt 16) { - $diffCharArray += ' ' + $diffCharList.Add(' ') } - $refHexLine = ($refHexArray -join ' ') - $refCharLine = ($refCharArray -join '') - $diffHexLine = ($diffHexArray -join ' ') - $diffCharLine = ($diffCharArray -join '') + $refHexLine = ($refHexList -join ' ') + $refCharLine = ($refCharList -join '') + $diffHexLine = ($diffHexList -join ' ') + $diffCharLine = ($diffCharList -join '') # Determine if the line was highlighted $indicator = if ($refHexLine -match $escapedHighlightStart -or $diffHexLine -match $escapedHighlightStart) @@ -296,11 +299,11 @@ function ConvertTo-DifferenceString # Output the results in the specified format '{0} {1} {2} {3} {4}' -f $refHexLine, $refCharLine, $indicator, $diffHexLine, $diffCharLine - # Clear arrays for the next group of 16 - $refHexArray = @() - $refCharArray = @() - $diffHexArray = @() - $diffCharArray = @() + # Clear lists for the next group of 16 + $refHexList.Clear() + $refCharList.Clear() + $diffHexList.Clear() + $diffCharList.Clear() } } } diff --git a/source/Public/Get-GitBranchCommit.ps1 b/source/Public/Get-GitBranchCommit.ps1 new file mode 100644 index 0000000..5c50cad --- /dev/null +++ b/source/Public/Get-GitBranchCommit.ps1 @@ -0,0 +1,159 @@ +<# + .SYNOPSIS + Retrieves the commit ID(s) for a specified Git branch. + + .DESCRIPTION + The Get-GitBranchCommit command retrieves the commit ID(s) for a specified + Git branch. It provides options to retrieve the latest commit ID, a specific + number of latest commit IDs, or the first X number of commit IDs. + + .PARAMETER BranchName + Specifies the name of the Git branch. If not provided, the current branch + will be used. + + .PARAMETER Latest + Retrieves only the latest commit ID. + + .PARAMETER Last + Retrieves the specified number of latest commit IDs. The order will be from + the newest to the oldest commit. + + .PARAMETER First + Retrieves the first X number of commit IDs. The order will be from the + oldest to the newest commit. + + .OUTPUTS + System.String + + The commit ID(s) for the specified Git branch. + + .EXAMPLE + Get-GitBranchCommit -BranchName 'feature/branch' + + Retrieves all commit IDs for the 'feature/branch' Git branch. + + .EXAMPLE + Get-GitBranchCommit -Latest + + Retrieves only the latest commit ID for the current Git branch. + + .EXAMPLE + Get-GitBranchCommit -Last 5 + + Retrieves the 5 latest commit IDs for the current Git branch. + + .EXAMPLE + Get-GitBranchCommit -First 3 + + Retrieves the first 3 commit IDs for the current Git branch. +#> +function Get-GitBranchCommit +{ + [CmdletBinding(DefaultParameterSetName = 'NoParameter')] + [OutputType([System.String])] + param + ( + [Parameter(ParameterSetName = 'NoParameter')] + [Parameter(ParameterSetName = 'Latest')] + [Parameter(ParameterSetName = 'Last')] + [Parameter(ParameterSetName = 'First')] + [System.String] + $BranchName, + + [Parameter(ParameterSetName = 'Latest')] + [System.Management.Automation.SwitchParameter] + $Latest, + + [Parameter(ParameterSetName = 'Last')] + [System.UInt32] + $Last, + + [Parameter(ParameterSetName = 'First')] + [System.UInt32] + $First + ) + + $commitId = $null + $exitCode = 0 + $argument = @() + + if ($PSBoundParameters.ContainsKey('BranchName')) + { + if ($BranchName -eq '.') + { + $BranchName = Get-GitLocalBranchName -Current + } + + $argument += @( + $BranchName + ) + } + + if ($Latest.IsPresent) + { + # Return only the latest commit ID. + $commitId = git rev-parse HEAD @argument + + $exitCode = $LASTEXITCODE # cSpell: ignore LASTEXITCODE + } + elseif ($Last) + { + # Return the latest X number of commits. + $commitId = git log -n $Last --pretty=format:"%H" @argument + + $exitCode = $LASTEXITCODE + } + elseif ($First) + { + if (-not $PSBoundParameters.ContainsKey('BranchName')) + { + $BranchName = Get-GitLocalBranchName -Current + } + + # Count the total number of commits in the branch. + $totalCommits = git rev-list --count $BranchName + + $exitCode = $LASTEXITCODE + + if ($exitCode -eq 0) + { + # Calculate the number of commits to skip. + $skipCommits = $totalCommits - $First + + # Return the first X number of commits. + $commitId = git log --skip $skipCommits --reverse -n $First --pretty=format:"%H" $BranchName + + $exitCode = $LASTEXITCODE + } + } + else + { + # Return all commit IDs. + $commitId = git log --pretty=format:"%H" @argument + + $exitCode = $LASTEXITCODE + } + + if ($exitCode -ne 0) + { + if($PSBoundParameters.ContainsKey('BranchName')) + { + $errorMessage = $script:localizedData.Get_GitBranchCommit_FailedFromBranch -f $BranchName + } + else + { + $errorMessage = $script:localizedData.Get_GitBranchCommit_FailedFromCurrent + } + + $errorMessageParameters = @{ + Message = $errorMessage + Category = 'ObjectNotFound' + ErrorId = 'GGBC0001' # cspell: disable-line + TargetObject = $BranchName # This will be null if no branch name is provided. + } + + Write-Error @errorMessageParameters + } + + return $commitId +} diff --git a/source/Public/Get-GitLocalBranchName.ps1 b/source/Public/Get-GitLocalBranchName.ps1 new file mode 100644 index 0000000..6a9fb67 --- /dev/null +++ b/source/Public/Get-GitLocalBranchName.ps1 @@ -0,0 +1,87 @@ +<# + .SYNOPSIS + Retrieves the name of the local Git branch. + + .DESCRIPTION + The Get-GitLocalBranchName command is used to retrieve the name of the + local Git branch. It can either return the name of the current branch or + search for branches based on a specified name or wildcard pattern. + + .PARAMETER Name + Specifies the name or wildcard pattern of the branch to search for. + If not provided, all branch names will be returned. + + .PARAMETER Current + Indicates whether to retrieve the name of the current branch. If this switch parameter is present, the function will return the name of the current branch. + + .OUTPUTS + System.String + + The name of the local Git branch. + + .EXAMPLE + PS> Get-GitLocalBranchName -Name 'main' + + Returns the branch that match exactly to the name 'main'. + + .EXAMPLE + PS> Get-GitLocalBranchName -Name 'f/*' + + Returns the names of all branches that match the wildcard pattern "f/*". + + .EXAMPLE + PS> Get-GitLocalBranchName -Current + + Returns the name of the current Git branch. + + .NOTES + This function requires Git to be installed and accessible from the command line. +#> +function Get-GitLocalBranchName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Current + ) + + $branchName = $null + + if ($Current.IsPresent) + { + $branchName = git rev-parse --abbrev-ref HEAD + } + else + { + if ($Name) + { + # Can do wildcard search for branch name, e.g. f/* to find feature branches. + $branchName = git branch --format='%(refname:short)' --list $Name + } + else + { + $branchName = git branch --format='%(refname:short)' --list + } + } + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + $errorMessageParameters = @{ + Message = $script:localizedData.Get_GitLocalBranchName_Failed + Category = 'ObjectNotFound' + ErrorId = 'GGLBN0001' # cspell: disable-line + TargetObject = $null + } + + Write-Error @errorMessageParameters + } + + return $branchName +} diff --git a/source/Public/Get-GitRemote.ps1 b/source/Public/Get-GitRemote.ps1 new file mode 100644 index 0000000..f85acdf --- /dev/null +++ b/source/Public/Get-GitRemote.ps1 @@ -0,0 +1,105 @@ +<# + .SYNOPSIS + Retrieves the names or URL(s) of the specified Git remote or all remotes. + + .DESCRIPTION + The Get-GitRemote commands retrieves the names or URL(s) of the specified + Git remote or all remotes. It can be used to get the name of a remote or + the URL(s) for fetching or pushing. + + .PARAMETER Name + Specifies the name of the Git remote. + + .PARAMETER FetchUrl + Indicates that the URL(s) for fetching should be retrieved. + + .PARAMETER PushUrl + Indicates that the URL(s) for pushing should be retrieved. + + .INPUTS + None. You cannot pipe input to this function. + + .OUTPUTS + System.String[] + + The function returns an array of strings representing the names or URL of + the specified Git remote or all remotes. + + .EXAMPLE + PS C:\> Get-GitRemote + + Returns the names of all Git remotes. + + .EXAMPLE + PS C:\> Get-GitRemote -Name origin + + Returns the name of the Git remote named 'origin' if it exist. + + .EXAMPLE + PS C:\> Get-GitRemote -Name origin -FetchUrl + + Returns the URL for fetching from the Git remote named 'origin'. + + .EXAMPLE + PS C:\> Get-GitRemote -Name origin -PushUrl + + Returns the URL for pushing to the Git remote named 'origin'. +#> +function Get-GitRemote +{ + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([System.String])] + param + ( + [Parameter(Position = 0, ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, ParameterSetName = 'FetchUrl')] + [Parameter(Mandatory = $true, ParameterSetName = 'PushUrl')] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'FetchUrl')] + [Switch] + $FetchUrl, + + [Parameter(Mandatory = $true, ParameterSetName = 'PushUrl')] + [Switch] + $PushUrl + ) + + $arguments = @() + + if ($PSCmdlet.ParameterSetName -in 'FetchUrl', 'PushUrl') + { + $arguments += 'get-url' + + if ($PSBoundParameters.ContainsKey('PushUrl')) + { + $arguments += '--push' + } + + $arguments += $Name + } + + $result = git remote @arguments + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + $errorMessageParameters = @{ + Message = $script:localizedData.Get_GitRemote_Failed + Category = 'ObjectNotFound' + ErrorId = 'GGR0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + } + + if ($PSCmdlet.ParameterSetName -eq 'Default') + { + # Filter out the remote if it exist. + $result = @($result) -eq $Name + } + + return $result +} diff --git a/source/Public/Get-GitRemoteBranch.ps1 b/source/Public/Get-GitRemoteBranch.ps1 new file mode 100644 index 0000000..63004e5 --- /dev/null +++ b/source/Public/Get-GitRemoteBranch.ps1 @@ -0,0 +1,106 @@ +function Get-GitRemoteBranch +{ + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([System.String])] + param + ( + [Parameter(Position = 0, ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name')] + [ValidateNotNullOrEmpty()] + [System.String] + $RemoteName, + + [Parameter(Position = 1, ParameterSetName = 'Name')] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $RemoveRefsHeads + ) + + $arguments = @() + + # Make sure the remote URL is not printed to stderr. + $arguments += '--quiet' + + if ($PSBoundParameters.ContainsKey('RemoteName')) + { + $arguments += $RemoteName + } + + if ($PSBoundParameters.ContainsKey('Name')) + { + if ($Name -match 'refs/heads/') + { + $Name = $Name -replace 'refs/heads/' + } + + if ($Name -notmatch '\*\*$' -and $Name -match '^([^*].*\*)$') + { + <# + if Name contains '*' but do not end with '**' or is already + prefixed with'*', then prefix with '*'. + #> + $Name = '*{0}' -f $matches[1] + } + + # Name can also have wildcard like 'feature/*' + $arguments += $Name + } + + $result = git ls-remote --branches @arguments + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + $targetObject = $null + + if ($PSBoundParameters.ContainsKey('Name')) + { + $errorMessage = $script:localizedData.Get_GitRemoteBranch_ByName_Failed -f $Name, $RemoteName + $targetObject = 'Name' + } + elseif ($PSBoundParameters.ContainsKey('RemoteName')) + { + $errorMessage = $script:localizedData.Get_GitRemoteBranch_FromRemote_Failed -f $RemoteName + $targetObject = 'RemoteName' + } + else + { + $errorMessage = $script:localizedData.Get_GitRemoteBranch_Failed + } + + $errorMessageParameters = @{ + Message = $errorMessage + Category = 'ObjectNotFound' + ErrorId = 'GGRB0001' # cspell: disable-line + TargetObject = $targetObject # Null if neither RemoteName or Name is provided. + } + + Write-Error @errorMessageParameters + } + + $headsArray = $null + + if ($result) + { + $oidArray = @() + $headsArray = @() + + $result | ForEach-Object -Process { + $oid, $heads = $_ -split "`t" + + $oidArray += $oid + $headsArray += $heads + } + + # Remove refs/heads/ from the branch names. + if ($RemoveRefsHeads.IsPresent) + { + $headsArray = $headsArray -replace 'refs/heads/' + } + } + + return $headsArray +} diff --git a/source/Public/Get-GitTag.ps1 b/source/Public/Get-GitTag.ps1 new file mode 100644 index 0000000..e598b0d --- /dev/null +++ b/source/Public/Get-GitTag.ps1 @@ -0,0 +1,153 @@ +<# + .SYNOPSIS + Retrieves Git tags based on specified parameters. + + .DESCRIPTION + The Get-GitTag function retrieves Git tags based on the specified parameters. + It can retrieve the latest tag, a specific tag by name, or a list of tags. + The function supports sorting and filtering options. + + .PARAMETER Name + Specifies the name of the tag to retrieve. This parameter is used in the + 'First' parameter set. + + .PARAMETER Latest + Retrieves the latest Git tag. This parameter is used in the 'Latest' parameter + set. + + .PARAMETER First + Specifies the number of tags to retrieve. This parameter is used in the 'First' + parameter set. + + .PARAMETER AsVersions + Specifies whether to retrieve tags as version numbers. This parameter is + used in the 'First' parameter set. + + .PARAMETER Descending + Specifies whether to sort the tags in descending order. This parameter is + used in the 'First' parameter set. Default is ascending order. + + .OUTPUTS + System.String + The retrieved Git tag(s). + + .EXAMPLE + Get-GitTag -Name 'v1.0' + + Retrieves the Git tag with the name 'v1.0'. + + .EXAMPLE + Get-GitTag -Latest + + Retrieves the latest Git tag. + + .EXAMPLE + Get-GitTag -Name 'v13*' -AsVersions -Descending + + Retrieves all Git tags as versions that start with 'v13', and sort them + in descending order. + + .EXAMPLE + Get-GitTag -First 5 -AsVersions -Descending + + Retrieves the first 5 Git tags as version numbers after the tags are sorted + in descending order. + + .NOTES + This function requires Git to be installed and accessible from the command line. +#> +function Get-GitTag +{ + [CmdletBinding()] + param ( + [Parameter(Position = 0, ParameterSetName = 'First')] + [System.String] + $Name, + + [Parameter(ParameterSetName = 'Latest')] + [System.Management.Automation.SwitchParameter] + $Latest, + + [Parameter(ParameterSetName = 'First')] + [System.UInt32] + $First, + + [Parameter(ParameterSetName = 'First')] + [System.Management.Automation.SwitchParameter] + $AsVersions, + + [Parameter(ParameterSetName = 'First')] + [System.Management.Automation.SwitchParameter] + $Descending + ) + + if ($Latest.IsPresent) + { + $First = 1 + $Descending = $true + $AsVersions = $true + # git describe --tags --abbrev=0 + + # $exitCode = $LASTEXITCODE # cSpell: ignore LASTEXITCODE + } + + $arguments = @( + '--list' + ) + + if ($AsVersions.IsPresent) + { + if ($Descending.IsPresent) + { + $arguments += '--sort=-v:refname' + } + else + { + $arguments += '--sort=v:refname' + } + } + else + { + if ($Descending.IsPresent) + { + $arguments += '--sort=-refname' + } + else + { + $arguments += '--sort=refname' + } + } + + # Get all tags or filter tags using git directly + if ($Name) + { + $tag = git tag @arguments $Name + + $exitCode = $LASTEXITCODE + } + else + { + $tag = git tag @arguments + + $exitCode = $LASTEXITCODE + } + + if ($First) + { + $tag = $tag | Select-Object -First $First + } + + if ($exitCode -ne 0) + { + $errorMessageParameters = @{ + Message = $script:localizedData.Get_GitTag_FailedToGetTag + Category = 'InvalidOperation' + ErrorId = 'GGT0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + } + + return $tag +} diff --git a/source/Public/New-GitTag.ps1 b/source/Public/New-GitTag.ps1 new file mode 100644 index 0000000..2730200 --- /dev/null +++ b/source/Public/New-GitTag.ps1 @@ -0,0 +1,40 @@ +function New-GitTag +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $verboseDescriptionMessage = $script:localizedData.New_GitTag_ShouldProcessVerboseDescription -f $Name + $verboseWarningMessage = $script:localizedData.New_GitTag_ShouldProcessVerboseWarning -f $Name + $captionMessage = $script:localizedData.New_GitTag_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + git tag $ReleaseTag + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + $errorMessageParameters = @{ + Message = $script:localizedData.New_GitTag_FailedToCreateTag -f $Name + Category = 'InvalidOperation' + ErrorId = 'NGT0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + } + } +} diff --git a/source/Public/New-SamplerGitHubReleaseTag.ps1 b/source/Public/New-SamplerGitHubReleaseTag.ps1 index 03c9396..2ee2611 100644 --- a/source/Public/New-SamplerGitHubReleaseTag.ps1 +++ b/source/Public/New-SamplerGitHubReleaseTag.ps1 @@ -28,7 +28,7 @@ Specifies the release tag to create. Must be in the format 'vX.X.X'. If not specified, the latest preview tag will be used. - .PARAMETER SwitchBackToPreviousBranch + .PARAMETER ReturnToCurrentBranch Specifies that the command should switches back to the previous branch after creating the release tag. @@ -47,7 +47,7 @@ to the 'origin' remote. .EXAMPLE - New-SamplerGitHubReleaseTag -SwitchBackToPreviousBranch + New-SamplerGitHubReleaseTag -ReturnToCurrentBranch Creates a new release tag and switches back to the previous branch. @@ -75,15 +75,15 @@ function New-SamplerGitHubReleaseTag [Parameter()] [System.Management.Automation.SwitchParameter] - $SwitchBackToPreviousBranch, + $ReturnToCurrentBranch, [Parameter()] [System.Management.Automation.SwitchParameter] - $Force, + $PushTag, [Parameter()] [System.Management.Automation.SwitchParameter] - $PushTag + $Force ) if ($Force.IsPresent -and -not $Confirm) @@ -91,207 +91,107 @@ function New-SamplerGitHubReleaseTag $ConfirmPreference = 'None' } - # Check if the remote specified in $UpstreamRemoteName exists locally and throw an error if it doesn't. - $remoteExists = git remote | Where-Object -FilterScript { $_ -eq $UpstreamRemoteName } - - if (-not $remoteExists) + # Only check assertions if not in WhatIf mode. + if (-not $WhatIfPreference) { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.New_SamplerGitHubReleaseTag_RemoteMissing -f $UpstreamRemoteName), - 'NSGRT0001', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $DatabaseName - ) - ) + Assert-GitRemote -Name $UpstreamRemoteName -Verbose:$VerbosePreference -ErrorAction 'Stop' } - $verboseDescriptionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FetchUpstream_ShouldProcessVerboseDescription -f $DefaultBranchName, $UpstreamRemoteName - $verboseWarningMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FetchUpstream_ShouldProcessVerboseWarning -f $DefaultBranchName, $UpstreamRemoteName - $captionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FetchUpstream_ShouldProcessCaption + $currentLocalBranchName = Get-GitLocalBranchName -Current -Verbose:$VerbosePreference -ErrorAction 'Stop' - if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + if ($DefaultBranchName -ne $currentLocalBranchName) { - # Fetch $DefaultBranchName from upstream and throw an error if it doesn't exist. - git fetch $UpstreamRemoteName $DefaultBranchName + # This command will also assert that there are no local changes if not in WhatIf mode. + Switch-GitLocalBranch -Name $DefaultBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' - if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE - { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.New_SamplerGitHubReleaseTag_FailedFetchBranchFromRemote -f $DefaultBranchName, $UpstreamRemoteName), - 'NSGRT0002', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DatabaseName - ) - ) - } + $switchedBranch = $true } - if ($SwitchBackToPreviousBranch.IsPresent) + try { - $currentLocalBranchName = git rev-parse --abbrev-ref HEAD + Update-GitLocalBranch -Rebase -SkipSwitchingBranch -RemoteName $UpstreamRemoteName -BranchName $DefaultBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' - if ($LASTEXITCODE -ne 0) - { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - $script:localizedData.New_SamplerGitHubReleaseTag_FailedGetLocalBranchName, - 'NSGRT0003', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DatabaseName - ) - ) - } + $headCommitId = Get-GitBranchCommit -Latest -Verbose:$VerbosePreference -ErrorAction 'Stop' } - - $continueProcessing = $true - $errorMessage = $null - - $verboseDescriptionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_Rebase_ShouldProcessVerboseDescription -f $DefaultBranchName, $UpstreamRemoteName - $verboseWarningMessage = $script:localizedData.New_SamplerGitHubReleaseTag_Rebase_ShouldProcessVerboseWarning -f $DefaultBranchName - $captionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_Rebase_ShouldProcessCaption - - if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + catch { - git checkout $DefaultBranchName - - if ($LASTEXITCODE -ne 0) + # If something failed, revert back to the previous branch if requested. + if ($switchedBranch) { - $continueProcessing = $false - $errorMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FailedCheckoutLocalBranch -f $DefaultBranchName - $errorCode = 'NSGRT0004' # cspell: disable-line + # This command will also assert that there are no local changes if not in WhatIf mode. + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' } - $switchedToDefaultBranch = $true + Write-Error -ErrorRecord $_ -ErrorAction 'Stop' + } - if ($continueProcessing) + try + { + # Fetch all tags from the upstream remote. + Request-GitTag -RemoteName $UpstreamRemoteName -Force:$Force -Verbose:$VerbosePreference -ErrorAction 'Stop' + } + catch + { + if ($ReturnToCurrentBranch.IsPresent -and $switchedBranch) { - git rebase $UpstreamRemoteName/$DefaultBranchName - - if ($LASTEXITCODE -ne 0) - { - $continueProcessing = $false - $errorMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FailedRebaseLocalDefaultBranch -f $DefaultBranchName, $UpstreamRemoteName - $errorCode = 'NSGRT0005' # cspell: disable-line - } - - if ($continueProcessing) - { - $headCommitId = git rev-parse HEAD - - if ($LASTEXITCODE -ne 0) - { - $continueProcessing = $false - $errorMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FailedGetLastCommitId -f $DefaultBranchName - $errorCode = 'NSGRT0006' # cspell: disable-line - } - } + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' } - if (-not $continueProcessing) - { - # If something failed, revert back to the previous branch if requested. - if ($SwitchBackToPreviousBranch.IsPresent -and $switchedToDefaultBranch) - { - git checkout $currentLocalBranchName - } - - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - $errorMessage, - $errorCode, # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DatabaseName - ) - ) - } + Write-Error -ErrorRecord $_ -ErrorAction 'Stop' } - $verboseDescriptionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessVerboseDescription -f $UpstreamRemoteName - $verboseWarningMessage = $script:localizedData.New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessVerboseWarning -f $UpstreamRemoteName - $captionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessCaption - - if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + <# + We cannot reliably determine during WhatIf if the current latest tag is + the actual latest preview tag. So this section is skipped during WhatIf. + #> + if (-not $PSBoundParameters.ContainsKey('ReleaseTag') -and -not $WhatIfPreference) { - git fetch $UpstreamRemoteName --tags - - if ($LASTEXITCODE -ne 0) + try { - if ($SwitchBackToPreviousBranch.IsPresent -and $switchedToDefaultBranch) - { - git checkout $currentLocalBranchName - } - - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.New_SamplerGitHubReleaseTag_FailedFetchTagsFromUpstreamRemote -f $UpstreamRemoteName), - 'NSGRT0007', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DatabaseName - ) - ) - } - } + $latestTag = Get-GitTag -Latest -Verbose:$VerbosePreference -ErrorAction 'Stop' - if (-not $ReleaseTag) - { - $tagExist = git tag | Select-Object -First 1 + if (-not $latestTag) + { + $errorMessageParameters = @{ + Message = $script:localizedData.New_SamplerGitHubReleaseTag_MissingTagsInLocalRepository + Category = 'InvalidOperation' + ErrorId = 'NSGRT0008' # cspell: disable-line + TargetObject = 'tag' + } - if ($LASTEXITCODE -ne 0 -or -not $tagExist) - { - $continueProcessing = $false - $errorMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FailedGetTagsOrMissingTagsInLocalRepository - $errorCode = 'NSGRT0008' # cspell: disable-line - } + Write-Error @errorMessageParameters -ErrorAction 'Stop' + } - if ($continueProcessing) - { - $latestPreviewTag = git describe --tags --abbrev=0 + $isCorrectlyFormattedPreviewTag = $latestTag -match '^(v\d+\.\d+\.\d+)-.*' - if ($LASTEXITCODE -ne 0) + if ($isCorrectlyFormattedPreviewTag) { - $continueProcessing = $false - $errorMessage = $script:localizedData.New_SamplerGitHubReleaseTag_FailedDescribeTags - $errorCode = 'NSGRT0009' # cspell: disable-line + $ReleaseTag = $matches[1] } - - if ($continueProcessing) + else { - $isCorrectlyFormattedPreviewTag = $latestPreviewTag -match '^(v\d+\.\d+\.\d+)-.*' - - if ($isCorrectlyFormattedPreviewTag) - { - $ReleaseTag = $matches[1] - } - else - { - $continueProcessing = $false - $errorMessage = $script:localizedData.New_SamplerGitHubReleaseTag_LatestTagIsNotPreview -f $latestPreviewTag - $errorCode = 'NSGRT0010' # cspell: disable-line + $errorMessageParameters = @{ + Message = $script:localizedData.New_SamplerGitHubReleaseTag_LatestTagIsNotPreview -f $latestPreviewTag + Category = 'InvalidOperation' + ErrorId = 'NSGRT0010' # cspell: disable-line + TargetObject = $latestTag } + + Write-Error @errorMessageParameters -ErrorAction 'Stop' } } - - if (-not $continueProcessing) + catch { - if ($SwitchBackToPreviousBranch.IsPresent -and $switchedToDefaultBranch) + if ($switchedBranch) { - git checkout $currentLocalBranchName + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' } - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - $errorMessage, - $errorCode, # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DatabaseName - ) - ) + Write-Error -ErrorRecord $_ -ErrorAction 'Stop' } } - if ($WhatIfPreference) + if ($WhatIfPreference -and -not $ReleaseTag) { $messageShouldProcess = $script:localizedData.New_SamplerGitHubReleaseTag_NewTagWhatIf_ShouldProcessVerboseDescription } @@ -304,44 +204,48 @@ function New-SamplerGitHubReleaseTag $verboseWarningMessage = $script:localizedData.New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseWarning -f $ReleaseTag $captionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_NewTag_ShouldProcessCaption - if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + # If ReleaseTag is specified and in WhatIf mode then we can skip ShouldProcess and let each individual command handle it. + if (($WhatIfPreference -and $PSBoundParameters.ContainsKey('ReleaseTag')) -or $PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) { - git tag $ReleaseTag - - if ($PushTag -and ($Force -or $PSCmdlet.ShouldContinue(('Do you want to push the tags to the upstream ''{0}''?' -f $UpstreamRemoteName), 'Confirm'))) + try { - git push origin --tags + <# + Already asked if the user wants to create the tag, so we use Force + to avoid asking again when creating the tag. + #> + New-GitTag -Name $ReleaseTag -Force -Verbose:$VerbosePreference -ErrorAction 'Stop' + + if ($PushTag.IsPresent) + { + Push-GitTag -RemoteName $UpstreamRemoteName -Name $ReleaseTag -Force:$Force -Verbose:$VerbosePreference -ErrorAction 'Stop' - Write-Information -MessageData ("`e[32mTag `e[1;37;44m{0}`e[0m`e[32m was created and pushed to upstream '{1}'`e[0m" -f $ReleaseTag, $UpstreamRemoteName) -InformationAction Continue + if (-not $WhatIfPreference) + { + Write-Information -MessageData ("`e[32mTag `e[1;37;44m{0}`e[0m`e[32m was created and pushed to upstream '{1}'`e[0m" -f $ReleaseTag, $UpstreamRemoteName) -InformationAction Continue + } + } + else + { + if (-not $WhatIfPreference) + { + # cSpell: disable-next-line + Write-Information -MessageData ("`e[32mTag `e[1;37;44m{0}`e[0m`e[32m was created. To push the tag to upstream, run `e[1;37;44mgit push {1} refs/tags/{0}`e[0m`e[32m.`e[0m" -f $ReleaseTag, $UpstreamRemoteName) -InformationAction Continue + } + } } - else + catch { - # cSpell: disable-next-line - Write-Information -MessageData ("`e[32mTag `e[1;37;44m{0}`e[0m`e[32m was created. To push the tag to upstream, run `e[1;37;44mgit push {1} --tags`e[0m`e[32m.`e[0m" -f $ReleaseTag, $UpstreamRemoteName) -InformationAction Continue + if ($switchedBranch) + { + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' + } + + Write-Error -ErrorRecord $_ -ErrorAction 'Stop' } } - if ($SwitchBackToPreviousBranch.IsPresent) + if ($switchedBranch) { - $verboseDescriptionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessVerboseDescription -f $currentLocalBranchName - $verboseWarningMessage = $script:localizedData.New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessVerboseWarning -f $currentLocalBranchName - $captionMessage = $script:localizedData.New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessCaption - - if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) - { - git checkout $currentLocalBranchName - - if ($LASTEXITCODE -ne 0) - { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.New_SamplerGitHubReleaseTag_FailedCheckoutPreviousBranch -f $currentLocalBranchName), - 'NSGRT0011', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DatabaseName - ) - ) - } - } + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' } } diff --git a/source/Public/Push-GitTag.ps1 b/source/Public/Push-GitTag.ps1 new file mode 100644 index 0000000..fc97c1a --- /dev/null +++ b/source/Public/Push-GitTag.ps1 @@ -0,0 +1,121 @@ +<# + .SYNOPSIS + Pushes a Git tag to a remote repository. + + .DESCRIPTION + The Push-GitTag function is used to push a Git tag to a remote repository. + It supports pushing a specific tag or pushing all tags. + + .PARAMETER RemoteName + Specifies the name of the remote repository. The default value is 'origin'. + + .PARAMETER Name + Specifies the name of the tag to push. This parameter is optional if, if + left out all tags are pushed. + + .EXAMPLE + Push-GitTag + + Pushes all tags to the default remote ('origin') repository. + + .EXAMPLE + Push-GitTag -Name 'v1.0.0' + + Pushes the 'v1.0.0' tag to the default ('origin') remote repository. + + .EXAMPLE + Push-GitTag -RemoteName 'my' -Name 'v1.0.0' + + Pushes the 'v1.0.0' tag to the 'my' remote repository. + + .EXAMPLE + Push-GitTag -RemoteName 'upstream' + + Pushes all tags to the 'upstream' remote repository. + + .INPUTS + None. + + .OUTPUTS + None. + + .NOTES + This function requires Git to be installed and accessible from the command + line. +#> +function Push-GitTag +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Position = 0)] + [ValidateNotNullOrEmpty()] + [System.String] + $RemoteName = 'origin', + + [Parameter(Position = 1)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $arguments = @($RemoteName) + + # If Name is not provided, push all tags. + if ($PSBoundParameters.ContainsKey('Name')) + { + $arguments += "refs/tags/$Name" + } + else + { + $arguments += '--tags' + } + + if ($PSBoundParameters.ContainsKey('Name')) + { + $verboseDescriptionMessage = $script:localizedData.Push_GitTag_PushTag_ShouldProcessVerboseDescription -f $Name, $RemoteName + $verboseWarningMessage = $script:localizedData.Push_GitTag_PushTag_ShouldProcessVerboseWarning -f $Name, $RemoteName + $captionMessage = $script:localizedData.Push_GitTag_PushTag_ShouldProcessCaption + } + else + { + $verboseDescriptionMessage = $script:localizedData.Push_GitTag_PushAllTags_ShouldProcessVerboseDescription -f $RemoteName + $verboseWarningMessage = $script:localizedData.Push_GitTag_PushAllTags_ShouldProcessVerboseWarning -f $RemoteName + $captionMessage = $script:localizedData.Push_GitTag_PushAllTags_ShouldProcessCaption + } + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + git push @arguments + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + if ($PSBoundParameters.ContainsKey('Name')) + { + $errorMessage = $script:localizedData.Push_GitTag_FailedPushTag -f $Name, $RemoteName + } + else + { + $errorMessage = $script:localizedData.Push_GitTag_FailedPushAllTags -f $RemoteName + } + + $errorMessageParameters = @{ + Message = $errorMessage + Category = 'InvalidOperation' + ErrorId = 'PGT0001' # cspell: disable-line + TargetObject = $Name # Null if Name is not provided. + } + + Write-Error @errorMessageParameters + } + } +} diff --git a/source/Public/Rename-GitLocalBranch.ps1 b/source/Public/Rename-GitLocalBranch.ps1 new file mode 100644 index 0000000..32a6061 --- /dev/null +++ b/source/Public/Rename-GitLocalBranch.ps1 @@ -0,0 +1,160 @@ +<# + .SYNOPSIS + Renames a local Git branch and optionally updates remote tracking and default + branch settings. + + .DESCRIPTION + This function renames a local Git branch. It can also update the upstream + tracking and set the new branch as the default for the remote repository. + + .PARAMETER Name + The current name of the branch to be renamed. + + .PARAMETER NewName + The new name for the branch. + + .PARAMETER RemoteName + The name of the remote repository. Defaults to 'origin'. + + .PARAMETER SetDefault + If specified, sets the newly renamed branch as the default branch for the + remote repository. + + .PARAMETER TrackUpstream + If specified, sets up the newly renamed branch to track the upstream branch. + + .EXAMPLE + Rename-GitLocalBranch -Name "feature/old-name" -NewName "feature/new-name" + + This example renames a local branch from "feature/old-name" to "feature/new-name". + It does not affect any remote settings. + + .EXAMPLE + Rename-GitLocalBranch -Name "develop" -NewName "main" -TrackUpstream -SetDefault + + This example renames the local "develop" branch to "main", sets up upstream tracking + for the new branch, and sets it as the default branch for the remote repository. + This is useful when standardizing branch names across projects. + + .EXAMPLE + Rename-GitLocalBranch -Name "bugfix/issue-123" -NewName "hotfix/critical-fix" -RemoteName "upstream" -TrackUpstream + + This example renames a branch from "bugfix/issue-123" to "hotfix/critical-fix", + sets up tracking with a remote named "upstream", but does not change the default branch. + + .NOTES + This function requires Git to be installed and accessible in the system PATH. +#> +function Rename-GitLocalBranch +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $NewName, + + [Parameter()] + [System.String] + $RemoteName = 'origin', + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SetDefault, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TrackUpstream + ) + + # Rename the local branch + git branch -m $Name $NewName + + if ($LASTEXITCODE -eq 0) # cSpell: ignore LASTEXITCODE + { + Write-Verbose -Message ($script:localizedData.Rename_GitLocalBranch_RenamedBranch -f $Name, $NewName) + } + else + { + $errorMessageParameters = @{ + Message = $script:localizedData.Rename_GitLocalBranch_FailedToRename -f $Name, $NewName + Category = 'InvalidOperation' + ErrorId = 'RGLB0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + + # Exit early if the branch rename failed, and user did not ask for terminating error. + return + } + + + # Only fetch if either switch parameter is passed + if ($SetDefault.IsPresent -or $TrackUpstream.IsPresent) + { + # Fetch the remote to ensure we have the latest information + git fetch $RemoteName + + if ($LASTEXITCODE -ne 0) + { + $errorMessageParameters = @{ + Message = $script:localizedData.Rename_GitLocalBranch_FailedFetch -f $RemoteName + Category = 'InvalidOperation' + ErrorId = 'RGLB0002' # cspell: disable-line + TargetObject = $RemoteName + } + + Write-Error @errorMessageParameters + + # Exit early if the branch rename failed, and user did not ask for terminating error. + return + } + } + + if ($TrackUpstream.IsPresent) + { + # Set up the new branch to track the upstream branch + git branch -u "$RemoteName/$NewName" $NewName + + if ($LASTEXITCODE -ne 0) + { + $errorMessageParameters = @{ + Message = $script:localizedData.Rename_GitLocalBranch_FailedSetUpstreamTracking -f $NewName, $RemoteName + Category = 'InvalidOperation' + ErrorId = 'RGLB0003' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + + # Exit early if the branch rename failed, and user did not ask for terminating error. + return + } + } + + if ($SetDefault.IsPresent) + { + # Set the new branch as the default for the remote + git remote set-head $RemoteName --auto + + if ($LASTEXITCODE -ne 0) + { + $errorMessageParameters = @{ + Message = $script:localizedData.Rename_GitLocalBranch_FailedSetDefaultBranchForRemote -f $NewName, $RemoteName + Category = 'InvalidOperation' + ErrorId = 'RGLB0003' # cspell: disable-line + TargetObject = $RemoteName + } + + Write-Error @errorMessageParameters + + # Exit early if the branch rename failed, and user did not ask for terminating error. + return + } + } +} diff --git a/source/Public/Request-GitTag.ps1 b/source/Public/Request-GitTag.ps1 new file mode 100644 index 0000000..f5b9efd --- /dev/null +++ b/source/Public/Request-GitTag.ps1 @@ -0,0 +1,74 @@ +function Request-GitTag +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $RemoteName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + if ($PSBoundParameters.ContainsKey('Name')) + { + $verboseDescriptionMessage = $script:localizedData.Request_GitTag_FetchTag_ShouldProcessVerboseDescription -f $Name, $RemoteName + $verboseWarningMessage = $script:localizedData.Request_GitTag_FetchTag_ShouldProcessVerboseWarning -f $Name, $RemoteName + $captionMessage = $script:localizedData.Request_GitTag_FetchTag_ShouldProcessCaption + } + else + { + $verboseDescriptionMessage = $script:localizedData.Request_GitTag_FetchAllTags_ShouldProcessVerboseDescription -f $RemoteName + $verboseWarningMessage = $script:localizedData.Request_GitTag_FetchAllTags_ShouldProcessVerboseWarning -f $RemoteName + $captionMessage = $script:localizedData.Request_GitTag_FetchAllTags_ShouldProcessCaption + } + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + $arguments = @($RemoteName) + + if ($PSBoundParameters.ContainsKey('Name')) + { + $arguments += "refs/tags/$Name:refs/tags/$Name" + } + else + { + $arguments += '--tags' + } + + git fetch @arguments + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + if ($PSBoundParameters.ContainsKey('Name')) + { + $errorMessage = $script:localizedData.Request_GitTag_FailedFetchTag -f $Name, $RemoteName + } + else + { + $errorMessage = $script:localizedData.Request_GitTag_FailedFetchAllTags -f $RemoteName + } + + $errorMessageParameters = @{ + Message = $errorMessage + Category = 'InvalidOperation' + ErrorId = 'RGT0001' # cspell: disable-line + TargetObject = $Name # Null if Name is not provided. + } + + Write-Error @errorMessageParameters + } + } +} diff --git a/source/Public/Switch-GitLocalBranch.ps1 b/source/Public/Switch-GitLocalBranch.ps1 new file mode 100644 index 0000000..3bd1baf --- /dev/null +++ b/source/Public/Switch-GitLocalBranch.ps1 @@ -0,0 +1,63 @@ +0<# + .SYNOPSIS + Switches to the specified local Git branch. + + .DESCRIPTION + The Switch-GitLocalBranch command is used to switch to the specified local + Git branch. It checks if the branch exists and performs the checkout operation. + If the checkout fails, it throws an error. + + .PARAMETER Name + The name of the branch to switch to. + + .EXAMPLE + Switch-GitLocalBranch -Name "feature/branch" + + This example switches to the "feature/branch" local Git branch. +#> +function Switch-GitLocalBranch +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param + ( + [Parameter(Mandatory = $true, Position = 0)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + # Only check assertions if not in WhatIf mode. + if ($WhatIfPreference -eq $false) + { + Assert-GitLocalChange + } + + $verboseDescriptionMessage = $script:localizedData.Switch_GitLocalBranch_ShouldProcessVerboseDescription -f $Name + $verboseWarningMessage = $script:localizedData.Switch_GitLocalBranch_ShouldProcessVerboseWarning -f $Name + $captionMessage = $script:localizedData.Switch_GitLocalBranch_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + git checkout $Name + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + $errorMessageParameters = @{ + Message = $script:localizedData.Switch_GitLocalBranch_FailedCheckoutLocalBranch -f $Name + Category = 'InvalidOperation' + ErrorId = 'SGLB0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @errorMessageParameters + } + } +} diff --git a/source/Public/Test-GitLocalChanges.ps1 b/source/Public/Test-GitLocalChanges.ps1 new file mode 100644 index 0000000..3ebe058 --- /dev/null +++ b/source/Public/Test-GitLocalChanges.ps1 @@ -0,0 +1,37 @@ +<# + .SYNOPSIS + Checks for unstaged or staged changes in the current local Git branch. + + .DESCRIPTION + The Test-GitLocalChanges command checks whether there are any unstaged or + staged changes in the current local Git branch. + + .OUTPUTS + System.Boolean + + Returns $true if there are unstaged or staged changes, otherwise returns $false. + + .EXAMPLE + PS> Test-GitLocalChanges + + This example demonstrates how to use the Test-GitLocalChanges function to + check for unstaged or staged changes in the current local Git branch. +#> +function Test-GitLocalChanges +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param () + + # Check for unstaged or staged changes + $status = git status --porcelain # cSpell: ignore unstaged + + $result = $false + + if ($status) + { + $result = $true + } + + return $result +} diff --git a/source/Public/Test-GitRemote.ps1 b/source/Public/Test-GitRemote.ps1 new file mode 100644 index 0000000..e3fe040 --- /dev/null +++ b/source/Public/Test-GitRemote.ps1 @@ -0,0 +1,41 @@ +<# + .SYNOPSIS + Tests if a Git remote exists locally. + + .DESCRIPTION + The Test-GitRemote command checks if the specified Git remote exists locally + and returns a boolean value indicating its existence. + + .PARAMETER Name + Specifies the name of the Git remote to be tested. + + .EXAMPLE + Test-GitRemote -Name "origin" + + Returns $true if the "origin" remote exists locally, otherwise returns $false. + + .NOTES + This function requires Git to be installed and accessible from the command line. +#> +function Test-GitRemote +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true, Position = 0)] + [System.String] + $Name + ) + + $remoteExists = Get-GitRemote -Name $Name + + $result = $false + + if ($remoteExists) + { + $result = $true + } + + return $result +} diff --git a/source/Public/Test-GitRemoteBranch.ps1 b/source/Public/Test-GitRemoteBranch.ps1 new file mode 100644 index 0000000..10c77d0 --- /dev/null +++ b/source/Public/Test-GitRemoteBranch.ps1 @@ -0,0 +1,70 @@ +<# + .SYNOPSIS + Tests if a remote branch exists in a Git repository. + + .DESCRIPTION + The Test-GitRemoteBranch command checks if a specified branch exists in a + remote Git repository. + + .PARAMETER Name + Specifies the name of the branch to check. + + .PARAMETER RemoteName + Specifies the name of the remote repository. + + .EXAMPLE + Test-GitRemoteBranch -BranchName "feature/branch" -Name "origin" + + This example tests if the branch "feature/branch" exists in the remote repository + named "origin". + + .INPUTS + None. You cannot pipe input to this function. + + .OUTPUTS + System.Boolean + + This function returns a Boolean value indicating whether the branch exists in + the remote repository. +#> +function Test-GitRemoteBranch +{ + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([System.Boolean])] + param + ( + [Parameter(Position = 0, ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name')] + [ValidateNotNullOrEmpty()] + [System.String] + $RemoteName, + + [Parameter(Position = 1, ParameterSetName = 'Name')] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + $getGitRemoteBranchParameters = @{} + + if ($PSBoundParameters.ContainsKey('RemoteName')) + { + $getGitRemoteBranchParameters['RemoteName'] = $RemoteName + } + + if ($PSBoundParameters.ContainsKey('Name')) + { + $getGitRemoteBranchParameters['Name'] = $Name + } + + $branch = Get-GitRemoteBranch @getGitRemoteBranchParameters -RemoveRefsHeads + + $result = $false + + if ($branch) + { + $result = $true + } + + return $result +} diff --git a/source/Public/Update-GitLocalBranch.ps1 b/source/Public/Update-GitLocalBranch.ps1 new file mode 100644 index 0000000..59006bd --- /dev/null +++ b/source/Public/Update-GitLocalBranch.ps1 @@ -0,0 +1,242 @@ +<# + .SYNOPSIS + Updates the specified Git branch by pulling or rebasing from the upstream + branch. + + .DESCRIPTION + This function checks out the specified local branch and either pulls the + latest changes or rebases it with the upstream branch. + + .PARAMETER BranchName + Specifies the local branch name. Default is 'main'. + + .PARAMETER UpstreamBranchName + Specifies the upstream branch name. If not specified the value in BranchName + will be used. + + .PARAMETER RemoteName + Specifies the remote name. Default is 'origin'. + + .PARAMETER Rebase + Specifies that the local branch should be rebased with the upstream branch. + + .PARAMETER ReturnToCurrentBranch + If specified, switches back to the original branch after performing the + pull or rebase. + + .PARAMETER SkipSwitchingBranch + If specified, the function will not switch to the specified branch. + + .PARAMETER OnlyUpdateRemoteTrackingBranch + If specified, only the remote tracking branch will be updated. + + .PARAMETER UseExistingTrackingBranch + If specified, only the existing tracking branch will be used to update the + local branch. + + .PARAMETER Force + If specified, the command will not prompt for confirmation. + + .EXAMPLE + Update-GitLocalBranch + + Checks out the 'main' branch and pulls the latest changes. + + .EXAMPLE + Update-GitLocalBranch -BranchName 'feature-branch' + + Checks out the 'feature-branch' and pulls the latest changes. + + .EXAMPLE + Update-GitLocalBranch -BranchName 'feature-branch' -UpstreamBranchName 'develop' -Rebase + + Checks out the 'feature-branch' and rebases it with the 'develop' branch. + + .EXAMPLE + Update-GitLocalBranch -BranchName 'feature-branch' -RemoteName 'upstream' + + Checks out the 'feature-branch' and pulls the latest changes from the + 'upstream' remote. + + .EXAMPLE + Update-GitLocalBranch -BranchName . + + Pulls the latest changes into the current branch. + + .EXAMPLE + Update-GitLocalBranch -ReturnToCurrentBranch + + Checks out the 'main' branch, pulls the latest changes, and switches back + to the original branch. +#> +function Update-GitLocalBranch +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is implemented correctly.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Default')] + param + ( + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.String] + $BranchName = 'main', + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.String] + $UpstreamBranchName, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.String] + $RemoteName = 'origin', + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.Management.Automation.SwitchParameter] + $Rebase, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.Management.Automation.SwitchParameter] + $ReturnToCurrentBranch, + + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [System.Management.Automation.SwitchParameter] + $SkipSwitchingBranch, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [System.Management.Automation.SwitchParameter] + $OnlyUpdateRemoteTrackingBranch, + + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.Management.Automation.SwitchParameter] + $UseExistingTrackingBranch, + + [Parameter(ParameterSetName = 'Default')] + [Parameter(ParameterSetName = 'Default_SkipSwitchingBranch')] + [Parameter(ParameterSetName = 'Default_UseExistingTrackingBranch')] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + # Only check assertions if not in WhatIf mode. + if ($WhatIfPreference -eq $false) + { + Assert-GitRemote -Name $RemoteName + } + + $currentLocalBranchName = Get-GitLocalBranchName -Current + + if ($BranchName -eq '.') + { + $BranchName = $currentLocalBranchName + } + + if (-not $UpstreamBranchName) + { + $UpstreamBranchName = $BranchName + } + + if (-not $SkipSwitchingBranch.IsPresent -and $BranchName -ne $currentLocalBranchName) + { + # This command will also assert that there are no local changes if not in WhatIf mode. + Switch-GitLocalBranch -Name $BranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' + } + + if ($Rebase.IsPresent) + { + $verboseDescriptionMessage = $script:localizedData.Update_GitLocalBranch_Rebase_ShouldProcessVerboseDescription -f $BranchName, $RemoteName, $UpstreamBranchName + $verboseWarningMessage = $script:localizedData.Update_GitLocalBranch_Rebase_ShouldProcessVerboseWarning -f $BranchName + $captionMessage = $script:localizedData.Update_GitLocalBranch_Rebase_ShouldProcessCaption + } + else + { + $verboseDescriptionMessage = $script:localizedData.Update_GitLocalBranch_Pull_ShouldProcessVerboseDescription -f $BranchName, $RemoteName, $UpstreamBranchName + $verboseWarningMessage = $script:localizedData.Update_GitLocalBranch_Pull_ShouldProcessVerboseWarning -f $BranchName + $captionMessage = $script:localizedData.Update_GitLocalBranch_Pull_ShouldProcessCaption + } + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + # Fetch the upstream branch + if (-not $UseExistingTrackingBranch.IsPresent) + { + # TODO: If this fails it should switch to the previous branch + Update-RemoteTrackingBranch -RemoteName $RemoteName -BranchName $UpstreamBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' + } + + if (-not $OnlyUpdateRemoteTrackingBranch.IsPresent) + { + if ($Rebase.IsPresent) + { + $argument = "$RemoteName/$UpstreamBranchName" + + # TODO: Should call new command `Start-GitRebase` + # Rebase the local branch + git rebase $argument + + $exitCode = $LASTEXITCODE # cSpell: ignore LASTEXITCODE + } + else + { + $argument = @($RemoteName, $UpstreamBranchName) + + # Run git pull with the specified remote and upstream branch + git pull @argument + + $exitCode = $LASTEXITCODE + } + + if ($ReturnToCurrentBranch.IsPresent -and $BranchName -ne $currentLocalBranchName) + { + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' + } + + if ($exitCode -ne 0) + { + $mergeConflicts = git ls-files --unmerged + + # TODO: Handle when error is rebase conflict resolution - THIS SHOULD BE MOVED TO START-GITREBASE + if ($mergeConflicts) + { + Write-Information -MessageData 'Merge conflict detected, when conflicts are resolved run `Resume-GitRebase` or to abort rebase run `Stop-GitRebase`.' -InformationAction 'Continue' + + if ($ReturnToCurrentBranch.IsPresent -and $BranchName -ne $currentLocalBranchName) + { + $ReturnToCurrentBranch = $false + + Write-Information -MessageData ('After rebase is finished run `Switch-GitLocalBranch -Name {0}` to return to the original branch.' -f $currentLocalBranchName) -InformationAction 'Continue' + } + + return + } + + $errorMessageParameters = @{ + Message = $script:localizedData.Update_GitLocalBranch_FailedRebase -f $RemoteName, $UpstreamBranchName + + Category = 'InvalidOperation' + ErrorId = 'UGLB0001' # cspell: disable-line + TargetObject = $argument -join ' ' + } + + Write-Error @errorMessageParameters + } + } + } + + # Switch back to the original branch if specified + if ($ReturnToCurrentBranch.IsPresent -and $BranchName -ne $currentLocalBranchName) + { + Switch-GitLocalBranch -Name $currentLocalBranchName -Verbose:$VerbosePreference -ErrorAction 'Stop' + } +} diff --git a/source/Public/Update-RemoteTrackingBranch.ps1 b/source/Public/Update-RemoteTrackingBranch.ps1 new file mode 100644 index 0000000..81d3c05 --- /dev/null +++ b/source/Public/Update-RemoteTrackingBranch.ps1 @@ -0,0 +1,76 @@ +<# + .SYNOPSIS + Updates the remote tracking branch in the local git repository. + + .DESCRIPTION + The Update-RemoteTrackingBranch command fetches updates from the specified + remote and branch in Git. It is used to keep the local tracking branch up + to date with the remote branch. + + .PARAMETER RemoteName + Specifies the name of the remote. + + .PARAMETER BranchName + Specifies the name of the branch to update. If not provided, all branches + will be updated. + + .EXAMPLE + Update-RemoteTrackingBranch -RemoteName 'origin' -BranchName 'main' + + Fetches updates from the 'origin' remote repository for the 'main' branch. + + .EXAMPLE + Update-RemoteTrackingBranch -RemoteName 'upstream' + + Fetches updates from the 'upstream' remote repository for all branches. + + .NOTES + This function requires Git to be installed and accessible from the command line. +#> +function Update-RemoteTrackingBranch +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param + ( + [Parameter(Mandatory = $true, Position = 0)] + [System.String] + $RemoteName, + + [Parameter(Position = 1)] + [System.String] + $BranchName + ) + + $arguments = @( + $RemoteName + ) + + if ($PSBoundParameters.ContainsKey('BranchName')) + { + $arguments += @( + $BranchName + ) + } + + $verboseDescriptionMessage = $script:localizedData.Update_RemoteTrackingBranch_FetchUpstream_ShouldProcessVerboseDescription -f $BranchName, $RemoteName + $verboseWarningMessage = $script:localizedData.Update_RemoteTrackingBranch_FetchUpstream_ShouldProcessVerboseWarning -f $BranchName, $RemoteName + $captionMessage = $script:localizedData.Update_RemoteTrackingBranch_FetchUpstream_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + # Fetch the updates from the specified remote and branch + git fetch @arguments + + if ($LASTEXITCODE -ne 0) # cSpell: ignore LASTEXITCODE + { + $errorMessageParameters = @{ + Message = $script:localizedData.Update_RemoteTrackingBranch_FailedFetchBranchFromRemote -f ($arguments -join ' ') + Category = 'InvalidOperation' + ErrorId = 'URTB0001' # cspell: disable-line + TargetObject = ($BranchName + ' ' + $RemoteName) + } + + Write-Error @errorMessageParameters + } + } +} diff --git a/source/en-US/Viscalyx.Common.strings.psd1 b/source/en-US/Viscalyx.Common.strings.psd1 index cd93f9e..aba5581 100644 --- a/source/en-US/Viscalyx.Common.strings.psd1 +++ b/source/en-US/Viscalyx.Common.strings.psd1 @@ -5,49 +5,107 @@ classes (that are not a DSC resource). #> +# cSpell: ignore unstaged ConvertFrom-StringData @' + ## Assert-GitLocalChange + Assert_GitLocalChanges_FailedUnstagedChanges = There are unstaged or staged changes. Please commit or stash your changes before proceeding. + + ## Get-GitLocalBranchName + Get_GitLocalBranchName_Failed = Failed to get the name of the local branch. Make sure git repository is accessible. + + ## Get-GitBranchCommit + Get_GitBranchCommit_FailedFromBranch = Failed to retrieve commits. Make sure the branch '{0}' exists and is accessible. + Get_GitBranchCommit_FailedFromCurrent = Failed to retrieve commits from current branch. + + ## Get-GitRemote + Get_GitRemote_Failed = Failed to get the remote '{0}'. Make sure the remote exists and is accessible. + + ## Get-GitRemoteBranch + Get_GitRemoteBranch_Failed = Failed to get the remote branches'. Make sure the remote branch exists and is accessible. + Get_GitRemoteBranch_FromRemote_Failed = Failed to get the remote branches from remote '{0}'. Make sure the remote branch exists and is accessible. + Get_GitRemoteBranch_ByName_Failed = Failed to get the remote branch '{0}' using the remote '{1}'. Make sure the remote branch exists and is accessible. + + ## Get-GitTag + Get_GitTag_FailedToGetTag = Failed to get the tag '{0}'. Make sure the tags exist and is accessible. + ## Remove-History Convert_PesterSyntax_ShouldProcessVerboseDescription = Converting the script file '{0}'. Convert_PesterSyntax_ShouldProcessVerboseWarning = Are you sure you want to convert the script file '{0}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. Convert_PesterSyntax_ShouldProcessCaption = Convert script file + ## Rename-GitLocalBranch + Rename_GitLocalBranch_FailedToRename = Failed to rename branch '{0}' to '{1}'. Make sure the local repository is accessible. + Rename_GitLocalBranch_FailedFetch = Failed to fetch from remote '{0}'. Make sure the remote exists and is accessible. + Rename_GitLocalBranch_FailedSetUpstreamTracking = Failed to set upstream tracking for branch '{0}' against remote '{1}'. Make sure the local repository is accessible. + Rename_GitLocalBranch_FailedSetDefaultBranchForRemote = Failed to set '{0}' as the default branch for remote '{1}'. Make sure the local repository is accessible. + Rename_GitLocalBranch_RenamedBranch = Successfully renamed branch '{0}' to '{1}'. + + ## New-GitTag + New_GitTag_ShouldProcessVerboseDescription = Creating tag '{0}'. + New_GitTag_ShouldProcessVerboseWarning = Are you sure you want to create tag '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_GitTag_ShouldProcessCaption = Create tag + New_GitTag_FailedToCreateTag = Failed to create tag '{0}'. Make sure the local repository is accessible. + ## New-SamplerGitHubReleaseTag New_SamplerGitHubReleaseTag_RemoteMissing = The remote '{0}' does not exist in the local git repository. Please add the remote before proceeding. - New_SamplerGitHubReleaseTag_FailedFetchBranchFromRemote = Failed to fetch branch '{0}' from the remote '{1}'. Make sure the branch exists in the remote git repository and the remote is accessible. - New_SamplerGitHubReleaseTag_FailedGetLocalBranchName = Failed to get the name of the local branch. Make sure the local branch exists and is accessible. - New_SamplerGitHubReleaseTag_FailedCheckoutLocalBranch = Failed to checkout the local branch '{0}'. Make sure the branch exists and is accessible. New_SamplerGitHubReleaseTag_FailedRebaseLocalDefaultBranch = Failed to rebase the local default branch '{0}' using '{1}/{0}'. Make sure the branch exists and is accessible. - New_SamplerGitHubReleaseTag_FailedGetLastCommitId = Failed to get the last commit id of the local branch '{0}'. Make sure the branch exists and is accessible. - New_SamplerGitHubReleaseTag_FailedFetchTagsFromUpstreamRemote = Failed to fetch tags from the upstream remote '{0}'. Make sure the remote exists and is accessible. - New_SamplerGitHubReleaseTag_FailedGetTagsOrMissingTagsInLocalRepository = Failed to get tags from the local repository or the tags are missing. Make sure that at least one preview tag exist in the local repository, or specify a release tag. - New_SamplerGitHubReleaseTag_FailedDescribeTags = Failed to describe the tags. Make sure the tags exist in the local repository. + New_SamplerGitHubReleaseTag_MissingTagsInLocalRepository = Tags are missing. Make sure that at least one preview tag exist in the local repository, or specify a release tag. New_SamplerGitHubReleaseTag_LatestTagIsNotPreview = The latest tag '{0}' is not a preview tag or not a correctly formatted preview tag. Make sure the latest tag is a preview tag, or specify a release tag. New_SamplerGitHubReleaseTag_FailedCheckoutPreviousBranch = Failed to checkout the previous branch '{0}'. + New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseDescription = Creating tag '{0}' for (latest) commit '{2}' in the local branch '{1}'. + New_SamplerGitHubReleaseTag_NewTagWhatIf_ShouldProcessVerboseDescription = Creating tag for latest commit in the local branch '{1}'. Note: Actual tag name and commit id cannot be determine during -WhatIf. + New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseWarning = Are you sure you want to create tag '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_SamplerGitHubReleaseTag_NewTag_ShouldProcessCaption = Create tag - New_SamplerGitHubReleaseTag_FetchUpstream_ShouldProcessVerboseDescription = Fetching branch '{0}' from the upstream remote '{1}'. - New_SamplerGitHubReleaseTag_FetchUpstream_ShouldProcessVerboseWarning = Are you sure you want to fetch branch '{0}' from the upstream remote '{1}'? + ## Push-GitTag + Push_GitTag_PushTag_ShouldProcessVerboseDescription = Pushing tag '{0}' to remote '{1}'. + Push_GitTag_PushTag_ShouldProcessVerboseWarning = Are you sure you want to push tag '{0}' to remote '{1}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Push_GitTag_PushTag_ShouldProcessCaption = Push tag + Push_GitTag_PushAllTags_ShouldProcessVerboseDescription = Pushing all tags to remote '{0}'. + Push_GitTag_PushAllTags_ShouldProcessVerboseWarning = Are you sure you want to push all tags to remote '{0}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. - New_SamplerGitHubReleaseTag_FetchUpstream_ShouldProcessCaption = Fetch upstream branch + Push_GitTag_PushAllTags_ShouldProcessCaption = Push all tags + Push_GitTag_FailedPushTag = Failed to push tag '{0}' to remote '{1}'. + Push_GitTag_FailedPushAllTags = Failed to push all tags to remote '{0}'. - New_SamplerGitHubReleaseTag_Rebase_ShouldProcessVerboseDescription = Switching to and rebasing the local default branch '{0}' using the upstream branch '{1}/{0}'. - New_SamplerGitHubReleaseTag_Rebase_ShouldProcessVerboseWarning = Are you sure you want switch to and rebase the local branch '{0}'? + ## Request-GitTag + Request_GitTag_FetchTag_ShouldProcessVerboseDescription = Fetching tag '{0}' from remote '{1}'. + Request_GitTag_FetchTag_ShouldProcessVerboseWarning = Are you sure you want to fetch tag '{0}' from remote '{1}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. - New_SamplerGitHubReleaseTag_Rebase_ShouldProcessCaption = Rebase the local default branch + Request_GitTag_FetchTag_ShouldProcessCaption = Fetch tag + Request_GitTag_FetchAllTags_ShouldProcessVerboseDescription = Fetching all tags from remote '{0}'. + Request_GitTag_FetchAllTags_ShouldProcessVerboseWarning = Are you sure you want to fetch all tags from remote '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Request_GitTag_FetchAllTags_ShouldProcessCaption = Fetch all tags + Request_GitTag_FailedFetchTag = Failed to fetch tag '{0}' from remote '{1}'. Make sure the tag exists and remote is accessible. + Request_GitTag_FailedFetchAllTags = Failed to fetch all tags from remote '{0}'. - New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessVerboseDescription = Fetching the tags from (upstream) remote '{0}'. - New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessVerboseWarning = Are you sure you want to fetch tags from remote '{0}'? + ## Switch-GitLocalBranch + Switch_GitLocalBranch_FailedCheckoutLocalBranch = Failed to checkout the local branch '{0}'. Make sure the branch exists and is accessible. + Switch_GitLocalBranch_ShouldProcessVerboseDescription = Switching to the local branch '{0}'. + Switch_GitLocalBranch_ShouldProcessVerboseWarning = Are you sure you want to switch to the local branch '{0}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. - New_SamplerGitHubReleaseTag_UpstreamTags_ShouldProcessCaption = Fetch tags from remote + Switch_GitLocalBranch_ShouldProcessCaption = Switch local branch - New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseDescription = Creating tag '{0}' for commit '{2}' in the local branch '{1}'. - New_SamplerGitHubReleaseTag_NewTagWhatIf_ShouldProcessVerboseDescription = Creating tag for commit in the local branch '{1}'. Note: Actual tag name and commit id cannot be determine during -WhatIf. - New_SamplerGitHubReleaseTag_NewTag_ShouldProcessVerboseWarning = Are you sure you want to create tag '{0}'? + ## Update-GitLocalBranch + Update_GitLocalBranch_Rebase_ShouldProcessVerboseDescription = Rebasing the local branch '{0}' using tracking branch '{1}/{2}'. + Update_GitLocalBranch_Rebase_ShouldProcessVerboseWarning = Are you sure you want to rebase the local branch '{0}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. - New_SamplerGitHubReleaseTag_NewTag_ShouldProcessCaption = Create tag + Update_GitLocalBranch_Rebase_ShouldProcessCaption = Rebase local branch + Update_GitLocalBranch_Pull_ShouldProcessVerboseDescription = Updating the local branch '{0}' by pulling from tracking branch '{1}/{2}'. + Update_GitLocalBranch_Pull_ShouldProcessVerboseWarning = Are you sure you want to pull into the local branch '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Update_GitLocalBranch_Pull_ShouldProcessCaption = Pull into local branch + Update_GitLocalBranch_FailedRebase = Failed to rebase the local branch '{0}' from remote '{1}'. Make sure the branch exists and is accessible. - New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessVerboseDescription = Switching back to previous local branch '{0}'. - New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessVerboseWarning = Are you sure you want to switch back to previous local branch '{0}'? + ## Update-RemoteTrackingBranch + Update_RemoteTrackingBranch_FailedFetchBranchFromRemote = Failed to fetch from '{0}'. Make sure the branch exists in the remote git repository and the remote is accessible. + Update_RemoteTrackingBranch_FetchUpstream_ShouldProcessVerboseDescription = Fetching branch '{0}' from the upstream remote '{1}'. + Update_RemoteTrackingBranch_FetchUpstream_ShouldProcessVerboseWarning = Are you sure you want to fetch branch '{0}' from the upstream remote '{1}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. - New_SamplerGitHubReleaseTag_SwitchBack_ShouldProcessCaption = Switch to previous branch + Update_RemoteTrackingBranch_FetchUpstream_ShouldProcessCaption = Fetch upstream branch '@ diff --git a/tests/Unit/Public/ConvertTo-DifferenceString.tests.ps1 b/tests/Unit/Public/ConvertTo-DifferenceString.tests.ps1 index 75f357c..55bc858 100644 --- a/tests/Unit/Public/ConvertTo-DifferenceString.tests.ps1 +++ b/tests/Unit/Public/ConvertTo-DifferenceString.tests.ps1 @@ -43,13 +43,13 @@ AfterAll { } Describe '-join (ConvertTo-DifferenceString' { - It 'should use custom labels' { + It 'Should use custom labels' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -ReferenceLabel 'Ref:' -DifferenceLabel 'Diff:') $result | Should -Match 'Ref:' $result | Should -Match 'Diff:' } - It 'should handle different encoding types' { + It 'Should handle different encoding types' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -EncodingType 'ASCII') $result | Should -Match '31m65' $result | Should -Match '31m61' @@ -57,24 +57,24 @@ Describe '-join (ConvertTo-DifferenceString' { $result | Should -Match '31ma' } - It 'should exclude column header when NoColumnHeader is specified' { + It 'Should exclude column header when NoColumnHeader is specified' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -NoColumnHeader) $result | Should -Not -Match 'Bytes' $result | Should -Not -Match 'Ascii' } - It 'should exclude labels when NoLabels is specified' { + It 'Should exclude labels when NoLabels is specified' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -NoLabels) $result | Should -Not -Match 'Expected:' $result | Should -Not -Match 'But was:' } - It 'should return equal indicators for identical strings' { + It 'Should return equal indicators for identical strings' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hello') $result | Should -Not -Match '!=' } - It 'should highlight differences for different strings' { + It 'Should highlight differences for different strings' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo') $result | Should -Match '31m65' $result | Should -Match '31m61' @@ -82,19 +82,19 @@ Describe '-join (ConvertTo-DifferenceString' { $result | Should -Match '31ma' } - It 'should use custom indicators' { + It 'Should use custom indicators' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -EqualIndicator 'EQ' -NotEqualIndicator 'NE') $result | Should -Match 'NE' $result | Should -Not -Match '==' $result | Should -Not -Match '!=' } - It 'should handle empty strings' { + It 'Should handle empty strings' { $result = -join (ConvertTo-DifferenceString -ReferenceString '' -DifferenceString '') $result | Should -Not -Match '!=' } - It 'should handle different length strings' { + It 'Should handle different length strings' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'HelloWorld') $result | Should -Match '31mW' $result | Should -Match '31mo' @@ -103,19 +103,19 @@ Describe '-join (ConvertTo-DifferenceString' { $result | Should -Match '31md' } - It 'should handle special characters' { + It 'Should handle special characters' { $result = -join (ConvertTo-DifferenceString -ReferenceString "Hello`n" -DifferenceString "Hello`r") $result | Should -Match '31m0A' $result | Should -Match '31m0D' } - It 'should use custom highlighting' { + It 'Should use custom highlighting' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -HighlightStart "`e[32m" -HighlightEnd "`e[0m") $result | Should -Match '32m65' $result | Should -Match '32m61' } - It 'should exclude labels and column header' { + It 'Should exclude labels and column header' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -NoLabels -NoColumnHeader) $result | Should -Not -Match 'Expected:' $result | Should -Not -Match 'But was:' @@ -123,31 +123,43 @@ Describe '-join (ConvertTo-DifferenceString' { $result | Should -Not -Match 'Ascii' } - It 'should handle different encodings' { + It 'Should handle different encodings' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString 'Hallo' -EncodingType 'ASCII') $result | Should -Match '31m65' $result | Should -Match '31m61' } - It 'should handle null reference string' { + It 'Should handle null reference string' { $result = -join (ConvertTo-DifferenceString -ReferenceString $null -DifferenceString 'Hallo') $result | Should -Match '31m48' } - It 'should handle null difference string' { + It 'Should handle null difference string' { $result = -join (ConvertTo-DifferenceString -ReferenceString 'Hello' -DifferenceString $null) $result | Should -Match '31m48' } - It 'should handle escaped characters' { + It 'Should handle escaped characters' { $result = -join (ConvertTo-DifferenceString -ReferenceString "Hello`tWorld" -DifferenceString "Hello`nWorld") $result | Should -Match '31m09' # Tab character $result | Should -Match '31m0A' # Newline character } - It 'should handle multiple escaped characters' { + It 'Should handle multiple escaped characters' { $result = -join (ConvertTo-DifferenceString -ReferenceString "Hello`r`nWorld" -DifferenceString "Hello`n`rWorld") $result | Should -Match "`e\[31m0D`e\[0m `e\[31m0A`e\[0m" # Carriage return + Newline $result | Should -Match "`e\[31m0A`e\[0m `e\[31m0D`e\[0m" # Newline + Carriage return } + + It 'Should handle longer strings' { + $result = ConvertTo-DifferenceString -ReferenceString 'This is a string' -DifferenceString 'This is a string that is longer' + $result | Should-BeBlockString -Expected @( + "Expected: But was:" + "---------------------------------------------------------------- ----------------------------------------------------------------" + "Bytes Ascii Bytes Ascii" + "----- ----- ----- -----" + "54 68 69 73 20 69 73 20 61 20 73 74 72 69 6E 67 This is a string == 54 68 69 73 20 69 73 20 61 20 73 74 72 69 6E 67 This is a string" + "                                              != 20 74 68 61 74 20 69 73 20 6C 6F 6E 67 65 72  that is longer " + ) + } } diff --git a/tests/Unit/Public/New-SamplerGitHubReleaseTag.tests.ps1 b/tests/Unit/Public/New-SamplerGitHubReleaseTag.tests.ps1 index 8092ec1..4ec0653 100644 --- a/tests/Unit/Public/New-SamplerGitHubReleaseTag.tests.ps1 +++ b/tests/Unit/Public/New-SamplerGitHubReleaseTag.tests.ps1 @@ -109,6 +109,9 @@ Describe 'New-SamplerGitHubReleaseTag' { AfterAll { $script:MockLastExitCode = 0 + + # Reset the LASTEXITCODE to 0 after all the tests + Set-Variable -Name 'LASTEXITCODE' -Value $script:MockLastExitCode -Scope Global } It 'Should throw if branch does not exist' { @@ -133,7 +136,7 @@ Describe 'New-SamplerGitHubReleaseTag' { } It 'Should switch back to previous branch if specified' { - { New-SamplerGitHubReleaseTag -ReleaseTag 'v1.0.0' -SwitchBackToPreviousBranch -Force } | Should -Not -Throw + { New-SamplerGitHubReleaseTag -ReleaseTag 'v1.0.0' -ReturnToCurrentBranch -Force } | Should -Not -Throw } It 'Should push tag to upstream if specified' { diff --git a/tests/Unit/Public/Remove-PSReadLineHistory.tests.ps1 b/tests/Unit/Public/Remove-PSReadLineHistory.tests.ps1 index 4f2fbfa..3e31381 100644 --- a/tests/Unit/Public/Remove-PSReadLineHistory.tests.ps1 +++ b/tests/Unit/Public/Remove-PSReadLineHistory.tests.ps1 @@ -74,13 +74,12 @@ Describe 'Remove-PSReadLineHistory' { # Assert Should -Invoke -CommandName Set-Content -Exactly -Times 1 -Scope It -ParameterFilter { - # TODO: Implement a comparison function (Compare-String) to compare arrays that also calls Out-Diff when the arrays are not equal. # Compare the arrays. $compareResult = Compare-Object -ReferenceObject $Value -DifferenceObject $expectedContent if ($compareResult) { - Out-Diff -ActualString $Value -ExpectedString $expectedContent + Out-Difference -Difference $Value -Reference $expectedContent } # Compare-Object returns 0 when equal. @@ -111,7 +110,7 @@ Describe 'Remove-PSReadLineHistory' { if ($compareResult) { - Out-Diff -ActualString $Value -ExpectedString $expectedContent + Out-Difference -Difference $Value -Reference $expectedContent } # Compare-Object returns 0 when equal. diff --git a/tests/Unit/Public/Rename-GitLocalBranch.tests.ps1 b/tests/Unit/Public/Rename-GitLocalBranch.tests.ps1 new file mode 100644 index 0000000..7c4222a --- /dev/null +++ b/tests/Unit/Public/Rename-GitLocalBranch.tests.ps1 @@ -0,0 +1,374 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Viscalyx.Common' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +# cSpell: ignore LASTEXITCODE +Describe 'Rename-GitLocalBranch' { + BeforeAll { + InModuleScope -ScriptBlock { + # Stub for git command + function script:git + { + } + } + } + + Context 'When renaming a local branch' { + BeforeAll { + Mock -CommandName git -MockWith { + if ($args[0] -eq 'branch' -and $args[1] -eq '-m') + { + $global:LASTEXITCODE = 0 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should rename the branch successfully' { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' + + Should -Invoke -CommandName git -ParameterFilter { + $args[0] -eq 'branch' -and $args[1] -eq '-m' -and $args[2] -eq 'old-branch' -and $args[3] -eq 'new-branch' + } + } + + Context 'When the operation fails' { + BeforeAll { + Mock -CommandName git -MockWith { + if ($args[0] -eq 'branch' -and $args[1] -eq '-m') + { + $global:LASTEXITCODE = 1 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Rename_GitLocalBranch_FailedToRename + } + + $mockErrorMessage = $mockErrorMessage -f 'old-branch', 'new-branch' + } + + It 'Should have a localized error message' { + $mockErrorMessage | Should-BeTruthy -Because 'The error message should have been localized, and shall not be empty' + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should handle non-terminating error correctly' { + Mock -CommandName Write-Error + + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' + + Should -Invoke -CommandName Write-Error -ParameterFilter { + $Message -eq $mockErrorMessage + } + } + + It 'Should handle terminating error correctly' { + { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -ErrorAction 'Stop' + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + Context 'When updating upstream tracking' { + BeforeAll { + Mock -CommandName git -MockWith { + if (($args[0] -eq 'branch' -and $args[1] -eq '-m') -or $args[0] -eq 'fetch' -or ($args[0] -eq 'branch' -and $args[1] -eq '-u')) + { + $global:LASTEXITCODE = 0 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should update upstream tracking when TrackUpstream is specified' { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -TrackUpstream + + Should -Invoke -CommandName git -ParameterFilter { $args[0] -eq 'fetch' } + Should -Invoke -CommandName git -ParameterFilter { $args[0] -eq 'branch' -and $args[1] -eq '-u' } + } + + Context 'When the operation fails' { + Context 'When fetching fails' { + BeforeAll { + Mock -CommandName git -MockWith { + if ($args[0] -eq 'branch' -and $args[1] -eq '-m') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'fetch') + { + $global:LASTEXITCODE = 1 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Rename_GitLocalBranch_FailedFetch + } + + $mockErrorMessage = $mockErrorMessage -f 'origin' + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should have a localized error message' { + $mockErrorMessage | Should-BeTruthy -Because 'The error message should have been localized, and shall not be empty' + } + + It 'Should handle non-terminating error correctly' { + Mock -CommandName Write-Error + + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -TrackUpstream + + Should -Invoke -CommandName Write-Error -ParameterFilter { + $Message -eq $mockErrorMessage + } + } + + It 'Should handle terminating error correctly' { + { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -TrackUpstream -ErrorAction 'Stop' + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + Context 'When setting upstream tracking fails' { + BeforeAll { + Mock -CommandName git -MockWith { + if ($args[0] -eq 'branch' -and $args[1] -eq '-m') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'fetch') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'branch' -and $args[1] -eq '-u') + { + $global:LASTEXITCODE = 1 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Rename_GitLocalBranch_FailedSetUpstreamTracking + } + + $mockErrorMessage = $mockErrorMessage -f 'new-branch', 'origin' + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should have a localized error message' { + $mockErrorMessage | Should-BeTruthy -Because 'The error message should have been localized, and shall not be empty' + } + + It 'Should handle non-terminating error correctly when setting upstream tracking fails' { + Mock -CommandName Write-Error + + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -TrackUpstream + + Should -Invoke -CommandName Write-Error -ParameterFilter { + $Message -eq $mockErrorMessage + } + } + + It 'Should handle terminating error correctly when setting upstream tracking fails' { + { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -TrackUpstream -ErrorAction 'Stop' + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + } + + Context 'When setting the default branch' { + BeforeAll { + Mock -CommandName git -MockWith { + if ($args[0] -eq 'branch' -and $args[1] -eq '-m') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'fetch') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'remote' -and $args[1] -eq 'set-head') + { + $global:LASTEXITCODE = 0 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + } + + It 'Should set the new branch as default for the remote' { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -SetDefault + Should -Invoke -CommandName git -ParameterFilter { + $args -contains 'remote' -and $args -contains 'set-head' -and $args -contains '--auto' + } + } + + Context 'When setting default branch fails' { + BeforeAll { + Mock -CommandName git -MockWith { + if ($args[0] -eq 'branch' -and $args[1] -eq '-m') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'fetch') + { + $global:LASTEXITCODE = 0 + } + elseif ($args[0] -eq 'remote' -and $args[1] -eq 'set-head') + { + $global:LASTEXITCODE = 1 + } + else + { + throw "Mock git unexpected args: $($args -join ' ')" + } + } + + $mockErrorMessage = InModuleScope -ScriptBlock { + $script:localizedData.Rename_GitLocalBranch_FailedSetDefaultBranchForRemote + } + + $mockErrorMessage = $mockErrorMessage -f 'new-branch', 'origin' + } + + It 'Should have a localized error message' { + $mockErrorMessage | Should-BeTruthy -Because 'The error message should have been localized, and shall not be empty' + } + + It 'Should handle non-terminating error correctly when setting default branch fails' { + Mock -CommandName Write-Error + + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -SetDefault + + Should -Invoke -CommandName Write-Error -ParameterFilter { + $Message -eq $mockErrorMessage + } + } + + It 'Should handle terminating error correctly when setting default branch fails' { + { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -SetDefault -ErrorAction 'Stop' + } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + } + + Context 'When using the default remote name' { + BeforeAll { + Mock -CommandName git -MockWith { + $global:LASTEXITCODE = 0 + } + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should update upstream tracking when TrackUpstream is specified' { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -TrackUpstream + + Should -Invoke -CommandName git -ParameterFilter { $args[0] -eq 'fetch' -and $args[1] -eq 'origin' } + Should -Invoke -CommandName git -ParameterFilter { $args[0] -eq 'branch' -and $args[1] -eq '-u' -and $args[2] -eq 'origin/new-branch' -and $args[3] -eq 'new-branch' } + } + } + + Context 'When specifying a custom remote name' { + BeforeAll { + Mock -CommandName git -MockWith { + $global:LASTEXITCODE = 0 + } + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should update upstream tracking when TrackUpstream is specified' { + Rename-GitLocalBranch -Name 'old-branch' -NewName 'new-branch' -RemoteName 'upstream' -TrackUpstream + + Should -Invoke -CommandName git -ParameterFilter { $args[0] -eq 'fetch' -and $args[1] -eq 'upstream' } + Should -Invoke -CommandName git -ParameterFilter { $args[0] -eq 'branch' -and $args[1] -eq '-u' -and $args[2] -eq 'upstream/new-branch' -and $args[3] -eq 'new-branch' } + } + } +}