Private/Functions/MergeRequestHelpers.ps1

# Merge request helper functions

function Add-GitlabMergeRequestApprovals {
    param(
        [Parameter(Position=0, Mandatory, ValueFromPipeline)]
        $MergeRequest
    )

    $Approval = Invoke-GitlabApi GET "projects/$($MergeRequest.SourceProjectId)/merge_requests/$($MergeRequest.MergeRequestId)/approvals"

    $MergeRequest | Add-Member -NotePropertyMembers @{
        ApprovalsRequired = $Approval.approvals_required
        ApprovalsLeft     = $Approval.approvals_left
        ApprovedBy        = $Approval.approved_by.user.name
    }
}

function Add-GitlabMergeRequestChangeSummary {
    param (
        [Parameter(Position=0, Mandatory, ValueFromPipeline)]
        $MergeRequest,

        [Parameter()]
        [switch]
        $IncludeDiffs
    )

    # First query: get commit info without diffs (diffs have a 10 commit limit per request)
    $Data = Invoke-GitlabGraphQL -Query @"
    {
        project(fullPath: "$($MergeRequest.ProjectPath)") {
            mergeRequest(iid: "$($MergeRequest.MergeRequestId)") {
                diffStatsSummary {
                    additions
                    deletions
                    files: fileCount
                }
                commitsWithoutMergeCommits {
                    nodes {
                      author {
                          username
                      }
                      authoredDate
                    }
                }
                notes {
                    nodes {
                      id
                      author {
                          username
                      }
                      body
                      updatedAt
                    }
                }
            }
        }
    }
"@


    $Mr = $Data.Project.mergeRequest
    $Notes = $Mr.notes.nodes | Sort-Object -Descending updatedAt

    $SpecialNotes = @{
        SelfAssigned         = @{ Regex="^assigned to @$($MergeRequest.Author.username)"; Notes=@() }
        Assigned             = @{ Regex='^assigned to @'; Notes=@() }
        DescriptionChanges   = @{ Regex='^changed the description'; Notes=@() }
        TitleChanges         = @{ Regex='^<div>changed title from'; Notes=@() }
        MarkedDraft          = @{ Regex='^marked this merge request as \*\*draft\*\*'; Notes=@() }
        MarkedReady          = @{ Regex='^marked this merge request as \*\*ready\*\*'; Notes=@() }
        ReviewRequested      = @{ Regex='^requested review from @'; Notes=@() }
        Approved             = @{ Regex='^approved this merge request'; Notes=@() }
        Commits              = @{ Regex='^added \d+ commit'; Notes=@() }
        MentionCommits       = @{ Regex="^mentioned in commit"; Notes=@() }
        MentionMergeRequests = @{ Regex="^mentioned in merge request"; Notes=@() }
        Edits                = @{ Regex="^changed this line in \[version"; Notes=@() }
        PostMergeCommits     = @{ Regex="^deleted the `.*` branch."; Notes=@() }
    }
    $SpecialNoteIds = @()
    foreach ($Key in $SpecialNotes.Keys) {
        $Special = $Notes | Where-Object body -Match $SpecialNotes[$Key].Regex
        if ($Special) {
            $SpecialNotes[$Key].Notes = $Special
            $SpecialNoteIds += $Special.id
        }
    }
    $RegularComments = $Notes | Where-Object {
        $SpecialNoteIds -notcontains $_.id
    }
    $SelfComments     = $RegularComments | Where-Object { $_.author.username -eq $MergeRequest.Author.username }
    $ReviewerComments = $RegularComments | Where-Object { $_.author.username -ne $MergeRequest.Author.username }

    $Summary = [PSCustomObject]@{
        Changes                 = $Mr.diffStatsSummary | New-WrapperObject
        Authors                 = $Mr.commitsWithoutMergeCommits.nodes.author.username | Select-Object -Unique | Sort-Object
        FirstCommittedAt        = @($Mr.commitsWithoutMergeCommits.nodes.authoredDate) + @($SpecialNotes.Commits.Notes.updatedAt) | Sort-Object | Select-Object -First 1
        ReviewRequestedAt       = $SpecialNotes.ReviewRequested.Notes | Select-Object -First 1 | Select-Object -ExpandProperty updatedAt
        AssignedAt              = $SpecialNotes.SelfAssigned.Notes | Select-Object -First 1 | Select-Object -ExpandProperty updatedAt
        MarkedReadyAt           = $SpecialNotes.MarkedReady.Notes | Select-Object -First 1 | Select-Object -ExpandProperty updatedAt
        ApprovedAt              = $SpecialNotes.Approved.Notes | Select-Object -First 1 | Select-Object -ExpandProperty updatedAt
        FirstNonAuthorCommentAt = $ReviewerComments | Select-Object -First 1 | Select-Object -ExpandProperty updatedAt
        TimeToMerge             = '(computed below)'
    }
    if ($DebugPreference -eq 'Continue') {
        $Summary | Add-Member -NotePropertyMembers @{
            Commits         = $Mr.commitsWithoutMergeCommits.nodes
            SpecialNotes     = $SpecialNotes
            ReviewerComments = $ReviewerComments
            SelfComments     = $SelfComments
        }
    }

    $MergedAt = $MergeRequest.MergedAt
    if ($Summary.ReviewRequestedAt) {
        $Summary.TimeToMerge = @{ Duration = $MergedAt - $Summary.ReviewRequestedAt; Measure='FromReviewRequested' }
    } elseif ($Summary.AssignedAt) {
        $Summary.TimeToMerge = @{ Duration = $MergedAt - $Summary.AssignedAt; Measure='FromAssigned' }
    } else {
        $Summary.TimeToMerge = @{ Duration = $MergedAt - $MergeRequest.CreatedAt; Measure='FromCreated'}
    }

    $MergeRequest | Add-Member -NotePropertyMembers @{
        ChangeSummary = $Summary
    }

    if ($IncludeDiffs) {
        # GitLab limits diffs to 10 commits per request, so use cursor-based pagination
        $BatchSize = 10
        $AllDiffs = @()
        $HasNextPage = $true
        $AfterCursor = $null

        while ($HasNextPage) {
            $AfterArg = if ($AfterCursor) { ", after: `"$AfterCursor`"" } else { '' }

            $DiffData = Invoke-GitlabGraphQL -Query @"
            {
                project(fullPath: "$($MergeRequest.ProjectPath)") {
                    mergeRequest(iid: "$($MergeRequest.MergeRequestId)") {
                        commitsWithoutMergeCommits(first: $BatchSize$AfterArg) {
                            pageInfo {
                                hasNextPage
                                endCursor
                            }
                            nodes {
                                diffs { newFile newPath oldPath diff }
                            }
                        }
                    }
                }
            }
"@

            $CommitsResult = $DiffData.Project.mergeRequest.commitsWithoutMergeCommits
            $HasNextPage = $CommitsResult.pageInfo.hasNextPage
            $AfterCursor = $CommitsResult.pageInfo.endCursor

            $BatchDiffs = @($CommitsResult.nodes | ForEach-Object {
                $_.diffs | ForEach-Object {
                    [PSCustomObject]@{
                        Path = if ($_.newFile -eq 'true') { $_.newPath } else { $_.oldPath }
                        Diff = $_.diff
                    }
                }
            })
            $AllDiffs += $BatchDiffs
        }

        Write-Verbose "!$($MergeRequest.MergeRequestId) has $($AllDiffs.Count) diffs"
        $MergeRequest | Add-Member -NotePropertyName Diffs -NotePropertyValue $AllDiffs
    }
}