Skip to content

Instantly share code, notes, and snippets.

@thedavecarroll
Last active April 1, 2021 04:40
Show Gist options
  • Save thedavecarroll/25705fc239392d246521b28c00b093ff to your computer and use it in GitHub Desktop.
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
#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