libs/cmdlets/Get-ReleaseNotes.psm1

using module private/Remove-Emojis.psm1

<#
    .SYNOPSIS
    Get Release Notes from git log based on branch, head and commit message body, cmdlet supports a release notes based on commit message,
    commit message body, commit message notes, commit message feature additions, commit message feature updates, commit message breaking changes.
     
    Also supports a commit link prefix for each commit message category with the commit link prefix being set to github, gitlab, gitea, or custom.
 
    .DESCRIPTION
    Get Release Notes from git log based on branch, head and commit message body, cmdlet supports a release notes based on commit message,
    commit message body, commit message notes, commit message feature additions, commit message feature updates, commit message breaking changes.
 
    .PARAMETER Notes
    Set to true to include commit message that are contained in 'Notes:'.
 
    .PARAMETER FeaturesNotes
    Set to true to include commit message that are contained in 'Features Notes:'.
 
    .PARAMETER BreakingChanges
    Set to true to include commit message that are contained in 'Breaking Changes:'.
 
    .PARAMETER FeatureAdditions
    Set to true to include commit message that are contained in 'Feature Additions:'.
 
    .PARAMETER FeatureUpdates
    Set to true to include commit message that are contained in 'Feature Updates:'.
 
    .PARAMETER CommitLink
    Set to true to include commit links in the release notes.
 
    .PARAMETER CommitLinkPrefix
    Set to github, gitlab, gitea, or custom to include commit links in the release notes.
 
    .PARAMETER CustomPrefix
    Set to a custom prefix for the commit link.
 
    .PARAMETER NameSpace
    Set to true to include the namespace in the commit link.
 
    .PARAMETER AsObject
    Set to true to return the release notes as an object.
 
    .EXAMPLE
    Get-ReleaseNotes
 
    .EXAMPLE
    Get-ReleaseNotes -Notes -FeaturesNotes -BreakingChanges -FeatureAdditions
 
    .EXAMPLE
 
    Get-ReleaseNotes -Notes -FeaturesNotes -BreakingChanges -FeatureAdditions -CommitLink -CommitLinkPrefix github
 
    .EXAMPLE
 
    Get-ReleaseNotes -Notes -FeaturesNotes -BreakingChanges -FeatureAdditions -CommitLink -CommitLinkPrefix gitlab
 
    .EXAMPLE
 
    Get-ReleaseNotes -Notes -FeaturesNotes -BreakingChanges -FeatureAdditions -CommitLink -CommitLinkPrefix gitea
 
    .EXAMPLE
 
    Get-ReleaseNotes -Notes -FeaturesNotes -BreakingChanges -FeatureAdditions -CommitLink -CommitLinkPrefix custom -CustomPrefix example.com
 
    .EXAMPLE
 
    Get-ReleaseNotes -Notes -FeaturesNotes -BreakingChanges -FeatureAdditions -AsObject -Debug
#>

function Get-ReleaseNotes {
    [CmdletBinding()]
    [Alias("grn")]
    param (
        [Parameter(Mandatory = $false)]
        [switch]$Notes,
        [Parameter(Mandatory = $false)]
        [switch]$FeatureNotes,
        [Parameter(Mandatory = $false)]
        [switch]$BreakingChanges,
        [Parameter(Mandatory = $false)]
        [switch]$FeatureAdditions,
        [Parameter(Mandatory = $false)]
        [switch]$FeatureUpdates,
        [Parameter(Mandatory = $false)]
        [switch]$CommitLink,
        [Parameter(Mandatory = $false)]
        [validateset("github", "gitlab", "gitea", "custom")]
        [string]$CommitLinkPrefix,
        [Parameter(Mandatory = $false)]
        [string]$CustomPrefix,
        [Parameter(Mandatory = $false)]
        [string]$NameSpace,
        [Parameter(Mandatory = $false)]
        [switch]$AsObject,
        [Parameter(Mandatory = $false)]
        [switch]$OnlyMessageHead,
        [Parameter(Mandatory = $false)]
        [switch]$AheadOnly,
        [Parameter(Mandatory = $false)]
        [string]$MainBranch
    )
    begin {

        [hashtable]$categories = @{}
        $categories['Notes:'] = @{ entry = @(); strippedLine = ''; commitid = '' }
        $categories['Feature Additions:'] = @{ entry = @(); strippedLine = ''; commitid = '' }
        $categories['Feature Notes:'] = @{ entry = @(); strippedLine = ''; commitid = '' }
        $categories['Feature Updates:'] = @{ entry = @(); strippedLine = ''; commitid = '' }
        $categories['Breaking Changes:'] = @{ entry = @(); strippedLine = ''; commitid = '' }

        # Function to generate commit link: Internal function
        function generate_commit_link([string]$commitid) {
            if ($CommitLinkPrefix -eq "github") { $CommitLinkPrefix = "github.com" }
            elseif ($CommitLinkPrefix -eq "gitlab") { $CommitLinkPrefix = "gitlab.com" }
            elseif ($CommitLinkPrefix -eq "custom") { $CommitLinkPrefix = $CustomPrefix }
            else { $CommitLinkPrefix = "github.com" }
            $repoName = (Get-ItemProperty -Path ".\").Name
            $commitid_small = $commitid.Substring(0, 15)
            $commitLinkUrl = "https://$($CommitLinkPrefix)/$($namespace)/$($RepoName)/-/commit/$($current_commit)"
            $link = "[$commitid_small]($commitLinkUrl)"
            return $link
        }

        if(!$MainBranch){
            $MainBranch = "main"
        }

    }
    process {
        # Get the current branch name
        $currentBranch = git rev-parse --abbrev-ref HEAD

        # Fetch the git log for the current branch, including the full commit body
        # if OnlyMessage is true, fetch only the commit messages not the full commit body and
        # return as an array and exit
        if ($OnlyMessageHead) {
            $commitArray = @()
            $gitlab = $null
            if($AheadOnly){
                $gitLog = git log origin/$MainBranch.. --pretty=format:"%s"
            }else{
                $gitLog = git log $currentBranch --pretty=format:"%s"
            }
            foreach ($line in $gitLog) {
                $line = $line.Trim()
                # control mardown new dot points with correct spacing
                $commitArray += " - $line"
            }
            return $commitArray
        }
        else {
            if ($AheadOnly) {
                $gitLog = git log origin/$MainBranch.. --pretty=format:"%H%n%B" 
            }
            else {
                $gitLog = git log $currentBranch --pretty=format:"%H%n%B" 
            }

            # if branch is not main or master, get git log from brain main/master
            if ($currentBranch -ne "main" -or $currentBranch -ne "master") {
                $gitLogMM = git log origin/main --pretty=format:"%H"
                $lastCommitFromMain = ($gitLogMM -split "`n")
            }
            # Debugging: Print the fetched git log to verify its content
            Write-debug "Git Log Contents:`n$gitLog`n"

            # Split the log into lines and process each line
            [string]$currentCategory = $null
            [string]$currentEntry = $null
            [string]$current_commit = $null
            [string]$commitLinkUrl = $null
            $gitLog -split "`n"| ForEach-Object {
                
                $line = $_.Trim()

                # check if commit is in main/master and ignore
                if ($lastCommitFromMain -Contains $line) { return }

                # Ignore empty lines
                if ([string]::IsNullOrWhiteSpace($line)) { return }

                # Check if the line contains a commit hash and set it as the current commit
                if ($line -match "\b[0-9a-f]{40}\b") {
                    $current_commit = $line
                    Write-Debug "----[ setting commit tag to: $current_commit"
                    return
                }

                # Check if the line matches one of the predefined categories
                $strippedLine = Remove-Emojis -inputString $line
                
                # Sort categories by length in descending order to match most specific first
                $sortedCategories = $categories.Keys | Sort-Object -Property Length -Descending
                
                $matchedCategory = $sortedCategories | Where-Object { $strippedLine -eq $_ } | Select-Object -First 1
                
                if ($matchedCategory) {
                    Write-Debug "valid category detected on line: $strippedLine"
                    $currentCategory = $matchedCategory
                    $categories[$currentCategory]['strippedLine'] = $strippedLine
                    $categories[$currentCategory]['commitid'] = $current_commit
                    $currentEntry = $null
                    return
                }

                # If the line belongs to the current category and starts with " - ", start a new entry
                if ($currentCategory -and $line -match "^- .*") {
                    Write-debug "adding new entry to $currentCategory`: $line"
                    $currentEntry = $line.Trim()
                    if ($CommitLink) { $commitLinkUrl = generate_commit_link($current_commit) }
                    $categories[$currentCategory]['entry'] += "$currentEntry $commitLinkUrl" 
                    return
                }

                # If the line does not start with " - " but follows an existing entry, append it
                # commit fusion usies psparagraph to indent the text in commit messages
                if (
                    $currentEntry -and $currentCategory['strippedLine'] -and 
                    $line -notmatch "^- " -and 
                    $line -notmatch "^.*Merge*.*branch*.*into.*" -and 
                    $line -notmatch "^.*Merge*.*tag*.*into.*" -and
                    $line -notmatch "^.*Merge*.*commit*.*into.*" -and
                    $line -notmatch "^.*Merge*.*request.*" -and
                    $line -notmatch "^.*# " -and
                    $line -notmatch "[\uD83C-\uDBFF\uDC00-\uDFFF\u2600-\u26FF\u2700-\u27BF\u2B50\u231A-\u23F3\u2B06\u2194\u25AA\u25FE\u25B6\u23F8\u23F9\u23F3\u26A0\u26C8\u2694\u2696\u2702\u2728\u2734\u2744\u2747\u2753\u2755\u2795\u2796\u2797\u2B06\u21A9\u2B05]+") {
                    # Append the additional line to the last entry
                    $categories[$currentCategory]['entry'][-1] += " " + $line.TrimStart()
                    Write-debug "possible Second paragraph found and appending to $($currentCategory['strippedLine'])`: $line"
                    return
                }

            }

            # Debugging: Print the hashtable content after processing
            #Write-Output "Processed Release Notes Hashtable:"
            # $categories.GetEnumerator() | ForEach-Object {
            # Write-Output "$($_.Key):"
            # $_.Value | ForEach-Object { Write-Output " $_" }
            # }

            # Generate the markdown string
            [string]$markdown = ""
            foreach ($category in $categories.GetEnumerator()) {
                Write-Debug "Processing category: $($category.Key)"
                if ($Notes -eq $true -and $category.Key -eq "Notes:" -and $category.value['entry'].Count -gt 0) {
                    $markdown += "## $($category.Key)`n`n"
                    foreach ($entry in $category.value['entry']) {
                        $markdown += " $entry`n"
                    }
                }
                if ($FeatureNotes -eq $true -and $category.Key -eq "Feature Notes:" -and $category.value['entry'].Count -gt 0) {
                    $markdown += "## $($category.Key)`n`n"
                    foreach ($entry in $category.value['entry']) {
                        $markdown += " $entry`n"
                    }
                }
                if ($FeatureAdditions -eq $true -and $category.Key -eq "Feature Additions:" -and $category.value['entry'].Count -gt 0) {
                    $markdown += "## $($category.Key)`n`n"
                    foreach ($entry in $category.value['entry']) {
                        $markdown += " $entry`n"
                    }
                }
                if ($FeatureUpdates -eq $true -and $category.Key -eq "Feature Updates:" -and $category.value['entry'].Count -gt 0) {
                    $markdown += "## $($category.Key)`n`n"
                    foreach ($entry in $category.value['entry']) {
                        $markdown += " $entry`n"
                    }
                }
                if ($BreakingChanges -eq $true -and $category.Key -eq "Breaking Changes:" -and $category.value['entry'].Count -gt 0) {
                    $markdown += "## $($category.Key)`n`n"
                    foreach ($entry in $category.value['entry']) {
                        $markdown += " $entry`n"
                    }
                }
                $markdown += "`n"
            }
        }
        # return object or markdown not use if OnlyMessageHead is true
        if ($AsObject) {
            return $categories
        }else {
            return $markdown
        }
    }
}

$cmdletconfig = @{
    function = 'Get-ReleaseNotes'
    alias    = 'grn' 
}

Export-ModuleMember @cmdletconfig