Last active
April 1, 2021 04:40
-
-
Save thedavecarroll/25705fc239392d246521b28c00b093ff to your computer and use it in GitHub Desktop.
PSChangeLogTools script module which uses local git log, GitHub release, and GitHub issues to generate changelog updates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -Module PowerShellforGitHub | |
#Requires -Version 5.1 | |
# GitLog Class | |
# class definition created by ConvertTo-ClassDefinition at 3/31/2021 9:47:01 PM for object type PSCustomObject | |
class PSGitLog { | |
[String]$CommitId | |
[String]$ShortCommitId | |
[DateTime]$AuthorDate | |
[String]$AuthorName | |
[String]$AuthorEmail | |
[DateTime]$CommitterDate | |
[String]$CommitterName | |
[String]$ComitterEmail | |
[String]$CommitterSignature | |
[String]$CommitMessage | |
[String]$SafeCommitMessage | |
PSGitLog () { } | |
PSGitLog ([PSCustomObject]$InputObject) { | |
$this.CommitId = $InputObject.CommitId | |
$this.ShortCommitId = $InputObject.ShortCommitId | |
$this.AuthorDate = [datetime]::Parse($InputObject.AuthorDate,(Get-Culture)) | |
$this.AuthorName = $InputObject.AuthorName | |
$this.AuthorEmail = $InputObject.AuthorEmail | |
$this.CommitterDate = [datetime]::Parse($InputObject.CommitterDate,(Get-Culture)) | |
$this.CommitterName = $InputObject.CommitterName | |
$this.ComitterEmail = $InputObject.ComitterEmail | |
$this.CommitMessage = $InputObject.CommitMessage | |
$this.SafeCommitMessage = $InputObject.SafeCommitMessage | |
$this.CommitterSignature = switch ($InputObject.CommitterSignature) { | |
'G' { 'Valid'} | |
'B' { 'BadSignature'} | |
'U' { 'GoodSignatureUnknownValidity'} | |
'X' { 'GoodSignatureExpired'} | |
'Y' { 'GoodSignatureExpiredKey'} | |
'R' { 'GoodSignatureRevokedKey'} | |
'E' { 'MissingKey'} | |
'N' { 'NoSignature'} | |
} | |
} | |
} | |
# converts local git log into PSGitLog object | |
function Get-GitLog { | |
[CmdLetBinding(DefaultParameterSetName='Default')] | |
param ( | |
[Parameter(ParameterSetName='Default',ValueFromPipeline)] | |
[Parameter(ParameterSetName='SourceTarget',ValueFromPipeline)] | |
[ValidateScript({Resolve-Path -Path $_ | Test-Path})] | |
[string]$GitFolder='.', | |
[Parameter(ParameterSetName='SourceTarget',Mandatory)] | |
[string]$StartCommitId, | |
[Parameter(ParameterSetName='SourceTarget')] | |
[string]$EndCommitId='HEAD' | |
) | |
Push-Location | |
try { | |
$GitPath = (Resolve-Path -Path $GitFolder).Path | |
$GitCommand = Get-Command -Name git -ErrorAction Stop | |
if ((Get-Location).Path -ne $GitPath) { | |
Set-Location -Path $GitFolder | |
} | |
Write-Verbose -Message "Folder - $GitPath" | |
} | |
catch { | |
$PSCmdlet.ThrowTerminatingError($_) | |
} | |
if ($StartCommitId) { | |
$GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%G?`t%s`t%f" {1}...{2} 2>&1' -f $GitCommand.Source,$StartCommitId,$EndCommitId | |
} else { | |
$GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%G?`t%s`t%f" 2>&1' -f $GitCommand.Source | |
} | |
Write-Verbose -Message "Command - $GitLogCommand" | |
$GitLog = Invoke-Expression -Command "& $GitLogCommand" -ErrorAction SilentlyContinue | |
if ((Get-Location).Path -ne $GitPath) { | |
Pop-Location | |
} | |
try { | |
if ($GitLog[0] -notmatch 'fatal:') { | |
$GitLog | ConvertFrom-Csv -Delimiter "`t" -Header $([PSGitLog]::new().psobject.Properties.Name) | ForEach-Object { [PSGitLog]::new($_) } | |
} else { | |
if ($GitLog[0] -like "fatal: ambiguous argument '*...*'*") { | |
Write-Warning -Message 'Unknown revision. Please check the values for StartCommitId or EndCommitId; omit the parameters to retrieve the entire log.' | |
} else { | |
Write-Error -Category InvalidArgument -Message ($GitLog -join [System.Environment]::NewLine) -ErrorAction Stop | |
} | |
} | |
} | |
catch { | |
$PSCmdlet.ThrowTerminatingError($_) | |
} | |
Pop-Location | |
} | |
# uses Get-GitHubRelease and Get-GitHubIssue from PowerShellforGitHub to get the latest issues and releases | |
# ChangeLogEntryType is set to suggested type of changes from https://keepachangelog.com | |
function Get-ChangeLogUpdate { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory)] | |
[ValidateScript({Test-Path -Path $_})] | |
[string]$ProjectPath, | |
[Parameter(Mandatory)] | |
[ValidateSet('Bugfix','Security','Feature','Maintenance')] | |
[string[]]$ReleaseType, | |
[Parameter(Mandatory)] | |
[ValidateSet('No Required','Recommended','Strongly Recommended')] | |
[string]$UpdateRequired, | |
[Parameter(Mandatory)] | |
[string]$ProjectOwner, | |
[Parameter(Mandatory)] | |
[version]$TargetRelease, | |
[uri]$ReleaseLink, | |
[string]$TargetReleaseDate='Unreleased' | |
) | |
try { | |
enum ChangeLogEntryType { | |
Security; Deprecated; Removed; Fixed; Changed; Added; Maintenance | |
} | |
$ProjectPath = Resolve-Path -Path $ProjectPath | |
$Project = Split-Path -Path $ProjectPath -Leaf | |
$LastReleaseCommit = Get-GitHubRelease -OwnerName $ProjectOwner -RepositoryName $Project | Sort-Object -Property created_at -Descending | Select-Object -First 1 | |
if ($LastReleaseCommit) { | |
$GitLog = Get-GitLog -GitFolder $ProjectPath -StartCommitId $LastReleaseCommit.target_commitish | |
} else { | |
$GitLog = Get-GitLog -GitFolder $ProjectPath | |
} | |
} | |
catch { | |
$PSCmdlet.ThrowTerminatingError($_) | |
} | |
$NewChangeLogEntry = [System.Text.StringBuilder]::new() | |
[void]$NewChangeLogEntry.AppendLine() | |
if ($ReleaseLink) { | |
$TargetReleaseText = '## [{0}] - {1}' -f $TargetRelease.ToString(),$TargetReleaseDate | |
} else { | |
$TargetReleaseText = '## {0} - {1}' -f $TargetRelease.ToString(),$TargetReleaseDate | |
} | |
[void]$NewChangeLogEntry.AppendLine($TargetReleaseText) | |
[void]$NewChangeLogEntry.AppendLine() | |
switch ($ReleaseType.Count) { | |
1 { $ReleaseTags = $ReleaseType[0] } | |
2 { $ReleaseTags = '{0} and {1}' -f $ReleaseType[0],$ReleaseType[1] } | |
3 { $ReleaseTags = '{0}, {1}, and {2}' -f $ReleaseType[0],$ReleaseType[1],$ReleaseType[2] } | |
4 { $ReleaseTags = '{0}, {1}, {2}, and {3}' -f $ReleaseType[0],$ReleaseType[1],$ReleaseType[2],$ReleaseType[3] } | |
} | |
$ReleaseTypeText = '{0}; Update {1}' -f $ReleaseTags,$UpdateRequired | |
[void]$NewChangeLogEntry.AppendLine($ReleaseTypeText) | |
[void]$NewChangeLogEntry.AppendLine() | |
$ChangeLogCommits = foreach ($Commit in $GitLog) { | |
$IssueNumber = $GitHubIssue = $null | |
$Action,$Message = $Commit.CommitMessage -Split ' ' | |
if ([ChangeLogEntryType].GetEnumNames() -match "^$Action") { | |
$EntryType = [ChangeLogEntryType]$Action | |
} else { | |
$EntryType = 'Maintenance' | |
} | |
if ($Message -match '#') { | |
$Issue = $Message -match '#' | |
if ($Issue -is [boolean]) { | |
$IssueNumber = $Message.Replace('#','') | |
} else { | |
$IssueNumber = $Issue.Replace('#','') | |
} | |
$GitHubIssue = Get-GitHubIssue -OwnerName $ProjectOwner -RepositoryName $Project -Issue $IssueNumber | | |
Select-Object -Property number,html_url,title | | |
Sort-Object -Property number | |
} | |
[PSCustomObject]@{ | |
ShortCommitId = $Commit.ShortCommitId | |
CommitDate = $Commit.CommitDate | |
EntryType = $EntryType | |
CommitMessage = $Commit.CommitMessage | |
GitHubIssue = $GitHubIssue | |
} | |
} | |
$ChangeLogCommits | Out-String | Write-Verbose | |
foreach ($EntryType in [ChangeLogEntryType].GetEnumNames()) { | |
$SectionCommits = $ChangeLogCommits.Where({$_.EntryType -match $EntryType -and $_.GitHubIssue}) | Sort-Object -Property GitHubIssue.created_at,CommitterDate,CommitMessage | |
if ($SectionCommits) { | |
$SectionHeader = '### {0}' -f $EntryType | |
[void]$NewChangeLogEntry.AppendLine($SectionHeader) | |
[void]$NewChangeLogEntry.AppendLine() | |
foreach ($Entry in $SectionCommits) { | |
if ($Entry.GitHubIssue) { | |
$EntryText = '- [Issue #{0}]({1}) - {2}' -f $Entry.GitHubIssue.number,$Entry.GitHubIssue.html_url,$Entry.GitHubIssue.title | |
} | |
$EntryText | Write-Verbose | |
if (-Not $NewChangeLogEntry.ToString().Contains($EntryText)) { | |
[void]$NewChangeLogEntry.AppendLine($EntryText) | |
} | |
} | |
[void]$NewChangeLogEntry.AppendLine() | |
} | |
} | |
if ($ReleaseLink) { | |
$ReleaseLinkText = '[{0}]: {1}' -f $TargetRelease.ToString(),$ReleaseLink.AbsoluteUri | |
[void]$NewChangeLogEntry.AppendLine($ReleaseLinkText) | |
} | |
$NewChangeLogEntry.ToString() | |
} | |
# updates the actual changelog. | |
# note that if you have another changelog in your docs, you will need | |
# to copy the one in the root of your project over (or use the DocsPath parameter | |
# that I just added). | |
function Set-ChangeLog { | |
[CmdletBinding()] | |
param( | |
[ValidateScript({Test-Path $_})] | |
[string]$ChangeLogPath, | |
[Parameter(Mandatory,ValueFromPipeline)] | |
[string]$ChangeLogUpdate, | |
[ValidateScript({Test-Path $_})] | |
[string]$DocsPath | |
) | |
$ChangeLog = [System.Text.StringBuilder]::new() | |
$Lines = Get-Content -Path $ChangeLogPath | |
$Count = 0 | |
foreach ($Line in $Lines) { | |
if ($Line -match '^## \[\d\.|^## \d\.') { | |
if ($null -eq $LastReleaseBegin) { | |
$LastReleaseBegin = $Count | |
} elseif ($null -eq $LastReleaseEnd) { | |
$LastReleaseEnd = $Count - 1 | |
break | |
} | |
} | |
$Count++ | |
} | |
if ($Lines[$LastReleaseBegin] -ne $ChangeLogUpdate.Split([System.Environment]::NewLine)[2]) { | |
# use original heading | |
[void]$ChangeLog.Append($Lines[0..($LastReleaseBegin-1)] -join [System.Environment]::NewLine) | |
# add updated entry | |
[void]$ChangeLog.Append($ChangeLogUpdate) | |
# use original remainder | |
[void]$ChangeLog.Append($Lines[($LastReleaseBegin)..($Lines.Count)] -join [System.Environment]::NewLine) | |
Set-Content -Path $ChangeLogPath -Value $ChangeLog.ToString() -Force | |
if ($DocsPath) { | |
'Replacing {0} with contents from {1}' -f $DocsPath,$ChangeLogPath | Write-Verbose | |
Copy-Item -Path $ChangeLogPath -Destination $DocsPath -Force | Out-Null | |
} | |
} else { | |
' No changes made to {0}' -f $ChangeLogPath | Write-Warning | |
} | |
} | |
# gets the release notes, aka the last release in the changelog | |
# this would be used to updated the module's PrivateData.PSData.ReleaseNotes | |
# assumes only the last release info will be included | |
# also assumes the module has an online changelog | |
function Get-ReleaseNotes { | |
[CmdLetBinding()] | |
param( | |
[Parameter(Mandatory)] | |
[ValidateScript({Test-Path -Path $_})] | |
[string]$ChangeLogPath, | |
[Parameter(Mandatory)] | |
[uri]$ChangeLogUri | |
) | |
$FullChangeLogLocation = "For full CHANGELOG, see $ChangeLogUri" -f $ChangeLogUri.AbsoluteUri | |
$ChangeLog = [System.Text.StringBuilder]::new() | |
$Lines = Get-Content -Path $ChangeLogPath | |
$Count = 0 | |
foreach ($Line in $Lines) { | |
if ($Line -match '^## \[\d\.|^## \d\.') { | |
if ($null -eq $LastReleaseBegin) { | |
$LastReleaseBegin = $Count | |
} elseif ($null -eq $LastReleaseEnd) { | |
$LastReleaseEnd = $Count - 1 | |
break | |
} | |
} | |
$Count++ | |
} | |
[void]$ChangeLog.Append($Lines[$LastReleaseBegin..$LastReleaseEnd] -join [System.Environment]::NewLine) | |
[void]$ChangeLog.AppendLine() | |
[void]$ChangeLog.AppendLine($FullChangeLogLocation) | |
$ChangeLog.ToString() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment