Files
orca/src/shared/github/pull-requests/merge/pull-request-stack-merge.ts
T

105 lines
2.9 KiB
TypeScript

import type { GitHubPRStack, GitHubPRStackEntry } from '../pull-request-types'
export type GitHubPRStackMergeScope = {
count: number
complete: boolean
entries: GitHubPRStackEntry[]
}
export type GitHubPRStackMergeBlocker = {
reason:
| 'draft'
| 'closed'
| 'conflicts'
| 'changes_requested'
| 'review_required'
| 'behind'
| 'blocked'
prNumber: number
}
export function isGitHubPRStackMergeQueueRequired(
reviewMergeQueueRequired: boolean | null | undefined,
stackMergeQueueRequired: boolean | null | undefined
): boolean {
return reviewMergeQueueRequired === true || stackMergeQueueRequired === true
}
export function getGitHubPRStackMergeScope(stack: GitHubPRStack): GitHubPRStackMergeScope {
const entries = [...(stack.entries ?? [])]
.filter((entry) => entry.position <= stack.position)
.sort((a, b) => a.position - b.position)
const count = stack.position
const complete =
entries.length === count && entries.every((entry, index) => entry.position === index + 1)
return { count, complete, entries }
}
function stackEntryBlockerReason(
entry: GitHubPRStackEntry
): GitHubPRStackMergeBlocker['reason'] | null {
if (entry.state === 'draft') {
return 'draft'
}
if (entry.state === 'closed') {
return 'closed'
}
if (entry.mergeable === 'CONFLICTING' || entry.mergeStateStatus === 'DIRTY') {
return 'conflicts'
}
if (entry.reviewDecision === 'CHANGES_REQUESTED') {
return 'changes_requested'
}
if (entry.reviewDecision === 'REVIEW_REQUIRED') {
return 'review_required'
}
if (entry.mergeStateStatus === 'BEHIND') {
return 'behind'
}
if (entry.mergeStateStatus === 'BLOCKED') {
return 'blocked'
}
return null
}
export function getGitHubPRStackMergeBlocker(
scope: GitHubPRStackMergeScope
): GitHubPRStackMergeBlocker | null {
for (const entry of scope.entries) {
const reason = stackEntryBlockerReason(entry)
if (reason) {
return { reason, prNumber: entry.number }
}
}
return null
}
export type GitHubPRStackMergeDecision = {
scope: GitHubPRStackMergeScope
blocker: GitHubPRStackMergeBlocker | null
usesMergeQueue: boolean
directMergeAvailable: boolean
}
// A stack merge lands every PR below this one, so the stack's readiness replaces this PR's merge state.
export function resolveGitHubPRStackMergeDecision(args: {
stack: GitHubPRStack
reviewMergeQueueRequired: boolean | null | undefined
stackMergeQueueRequired: boolean | null | undefined
directMergeAvailable: boolean
}): GitHubPRStackMergeDecision {
const scope = getGitHubPRStackMergeScope(args.stack)
const blocker = getGitHubPRStackMergeBlocker(scope)
const usesMergeQueue = isGitHubPRStackMergeQueueRequired(
args.reviewMergeQueueRequired,
args.stackMergeQueueRequired
)
return {
scope,
blocker,
usesMergeQueue,
directMergeAvailable:
!blocker && (scope.complete || args.directMergeAvailable || usesMergeQueue)
}
}