401 lines
14 KiB
PowerShell
401 lines
14 KiB
PowerShell
# Copyright (c) Microsoft Corporation.
|
|
# Licensed under the MIT license.
|
|
|
|
#Requires -Version 7
|
|
#Requires -Modules PSGitHub
|
|
|
|
[CmdletBinding()]
|
|
Param(
|
|
[string]$Version,
|
|
[string]$SourceBranch = "origin/main",
|
|
[switch]$GpgSign = $False
|
|
)
|
|
|
|
Function Prompt() {
|
|
"PSCR {0}> " -f $PWD.Path
|
|
}
|
|
|
|
Function Enter-ConflictResolutionShell() {
|
|
$Global:Abort = $False
|
|
$Global:Skip = $False
|
|
$Global:Reject = $False
|
|
Push-Location -StackName:"ServicingStack" -Path:$PWD > $Null
|
|
Write-Host @"
|
|
`e[31;1;7mCONFLICT RESOLUTION REQUIRED`e[m
|
|
|
|
"@
|
|
|
|
& git status --short
|
|
|
|
Write-Host (@"
|
|
|
|
`e[1mCommands`e[m
|
|
`e[1;96mdone `e[m Complete conflict resolution and commit
|
|
`e[1;96mskip `e[m Skip this commit
|
|
`e[1;96mabort `e[m Stop everything
|
|
`e[1;96mreject`e[m Skip and `e[31mremove this commit from servicing consideration`e[m
|
|
"@)
|
|
$Host.EnterNestedPrompt()
|
|
Pop-Location -StackName:"ServicingStack" > $Null
|
|
}
|
|
|
|
Function Done() {
|
|
$Host.ExitNestedPrompt()
|
|
}
|
|
|
|
Function Abort() {
|
|
$Global:Abort = $True
|
|
$Host.ExitNestedPrompt()
|
|
}
|
|
|
|
Function Skip() {
|
|
$Global:Skip = $True
|
|
$Host.ExitNestedPrompt()
|
|
}
|
|
|
|
Function Reject() {
|
|
$Global:Skip = $True
|
|
$Global:Reject = $True
|
|
$Host.ExitNestedPrompt()
|
|
}
|
|
|
|
Function Set-GraphQlProjectEntryStatus($Project, $Item, $Field, $Value) {
|
|
Invoke-GitHubGraphQlApi -Query 'mutation($project: ID!, $item: ID!, $field: ID!, $value: String!) {
|
|
updateProjectV2ItemFieldValue(
|
|
input: {
|
|
projectId: $project
|
|
itemId: $item
|
|
fieldId: $field
|
|
value: {
|
|
singleSelectOptionId: $value
|
|
}
|
|
}
|
|
) { projectV2Item { id } }
|
|
}' -Variables @{ project = $Project; item = $Item; field = $Field; value = $Value } | Out-Null
|
|
}
|
|
|
|
Function Get-GraphQlProjectNumberGivenName($Organization, $Name) {
|
|
$projectNumber = ""
|
|
$cursor = ""
|
|
While ([String]::IsNullOrEmpty($projectNumber)) {
|
|
$o = Invoke-GitHubGraphQlApi -Query '
|
|
query($organization: String!, $after: String) {
|
|
organization (login: $organization) {
|
|
projectsV2 (first: 20, after: $after) {
|
|
nodes { id title number }
|
|
pageInfo { hasNextPage endCursor }
|
|
}
|
|
}
|
|
}' -Variables @{ organization = $Organization; after = $cursor }
|
|
|
|
If($false -eq $o.organization.projectsV2.pageInfo.hasNextPage) {
|
|
Break
|
|
}
|
|
|
|
$pl = $o.organization.projectsV2.nodes | Where-Object title -Like $Name
|
|
If($null -ne $pl) {
|
|
$projectNumber = $pl.number
|
|
Break
|
|
}
|
|
|
|
$cursor = $o.organization.projectsV2.pageInfo.endCursor
|
|
}
|
|
$projectNumber
|
|
}
|
|
|
|
Function Get-GraphQlProjectWithNodes($Organization, $Number) {
|
|
# It's terrible, but it pulls *all* of the info we need all at once!
|
|
$cursor = ""
|
|
$hasMore = $true
|
|
$nodes = @()
|
|
While ($hasMore) {
|
|
$Project = Invoke-GitHubGraphQlApi -Query '
|
|
query($organization: String!, $number: Int!, $after: String) {
|
|
organization(login: $organization) {
|
|
projectV2(number: $number) {
|
|
id
|
|
number
|
|
title
|
|
fields(first:20){
|
|
nodes {
|
|
... on ProjectV2FieldCommon { id name }
|
|
... on ProjectV2SingleSelectField { options { id name } }
|
|
}
|
|
}
|
|
items(first: 100, after: $after) {
|
|
pageInfo { hasNextPage endCursor }
|
|
nodes {
|
|
id
|
|
type
|
|
title: fieldValueByName(name: "Title") {
|
|
... on ProjectV2ItemFieldTextValue { text }
|
|
}
|
|
status: fieldValueByName(name: "Status") {
|
|
... on ProjectV2ItemFieldSingleSelectValue { name }
|
|
}
|
|
content {
|
|
... on Issue {
|
|
number
|
|
closedByPullRequestsReferences(first: 8) {
|
|
... on PullRequestConnection {
|
|
nodes {
|
|
number
|
|
mergeCommit { oid }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
... on PullRequest {
|
|
number
|
|
mergeCommit { oid }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
' -Variables @{ organization = $Organization; number = $Number; after = $cursor }
|
|
|
|
$n += $Project.organization.projectV2.items.nodes
|
|
$cursor = $Project.organization.projectV2.items.pageInfo.endCursor
|
|
$hasMore = $Project.organization.projectV2.items.pageInfo.hasNextPage
|
|
}
|
|
$Project.organization.projectV2.items.nodes = $n
|
|
$Project
|
|
}
|
|
|
|
Enum ServicingStatus {
|
|
Unknown
|
|
ToConsider
|
|
ToCherryPick
|
|
CherryPicked
|
|
Validated
|
|
Shipped
|
|
Rejected
|
|
}
|
|
|
|
Enum ServicingItemType {
|
|
Unknown
|
|
DraftIssue
|
|
Issue
|
|
PullRequest
|
|
Redacted
|
|
}
|
|
|
|
Class ServicingCard {
|
|
[String]$Id
|
|
[Int]$Number
|
|
[String]$Title
|
|
[String]$Commit
|
|
[ServicingStatus]$Status
|
|
[ServicingItemType]$Type
|
|
static [ServicingItemType]TypeFromString($name) {
|
|
$v = Switch -Exact ($name) {
|
|
"DRAFT_ISSUE" { [ServicingItemType]::DraftIssue }
|
|
"ISSUE" { [ServicingItemType]::Issue }
|
|
"PULL_REQUEST" { [ServicingItemType]::PullRequest }
|
|
"REDACTED" { [ServicingItemType]::Redacted }
|
|
Default { [ServicingItemType]::Unknown }
|
|
}
|
|
Return $v
|
|
}
|
|
|
|
static [ServicingStatus]StatusFromString($name) {
|
|
$v = Switch -Exact ($name) {
|
|
"To Consider" { [ServicingStatus]::ToConsider }
|
|
"To Cherry Pick" { [ServicingStatus]::ToCherryPick }
|
|
"Cherry Picked" { [ServicingStatus]::CherryPicked }
|
|
"Validated" { [ServicingStatus]::Validated }
|
|
"Shipped" { [ServicingStatus]::Shipped }
|
|
"Rejected" { [ServicingStatus]::Rejected }
|
|
Default { [ServicingStatus]::Unknown }
|
|
}
|
|
Return $v
|
|
}
|
|
|
|
ServicingCard([object]$node) {
|
|
$this.Id = $node.id
|
|
$this.Title = $node.title.text
|
|
If (-Not [String]::IsNullOrEmpty($node.content.mergeCommit.oid)) {
|
|
$this.Commit = $node.content.mergeCommit.oid
|
|
} ElseIf (-Not [String]::IsNullOrEmpty($node.content.closedByPullRequestsReferences.nodes.mergeCommit.oid)) {
|
|
$this.Commit = $node.content.closedByPullRequestsReferences.nodes.mergeCommit.oid
|
|
}
|
|
If (-Not [String]::IsNullOrEmpty($node.content.number)) {
|
|
$this.Number = [Int]$node.content.number
|
|
}
|
|
$this.Status = [ServicingCard]::StatusFromString($node.status.name)
|
|
$this.Type = [ServicingCard]::TypeFromString($node.type)
|
|
}
|
|
|
|
[string]Format() {
|
|
$color = Switch -Exact ($this.Status) {
|
|
([ServicingStatus]::ToConsider) { "`e[38:5:166m" }
|
|
([ServicingStatus]::ToCherryPick) { "`e[38:5:28m" }
|
|
([ServicingStatus]::CherryPicked) { "`e[38:5:20m" }
|
|
([ServicingStatus]::Validated) { "`e[38:5:126m" }
|
|
([ServicingStatus]::Shipped) { "`e[38:5:92m" }
|
|
([ServicingStatus]::Rejected) { "`e[38:5:160m" }
|
|
Default { "`e[m" }
|
|
}
|
|
$symbol = Switch -Exact ($this.Type) {
|
|
([ServicingItemType]::DraftIssue) { "`u{25cb}" } # white circle
|
|
([ServicingItemType]::Issue) { "`u{2b24}" } # black circle
|
|
([ServicingItemType]::PullRequest) { "`u{25c0}" } # black left triangle
|
|
([ServicingItemType]::Redacted) { "`u{25ec}" } # triangle with dot
|
|
Default { "`u{2b24}" }
|
|
}
|
|
$localTitle = $this.Title
|
|
If ($this.Number -Gt 0) {
|
|
$localTitle = "[`e]8;;https://github.com/microsoft/terminal/issues/{1}`e\Link`e]8;;`e\] {0} (#{1})" -f ($this.Title, $this.Number)
|
|
}
|
|
Return "{0}{1}`e[m ({2}) {3}" -f ($color, $symbol, $this.Status, $localTitle)
|
|
}
|
|
}
|
|
|
|
|
|
If ([String]::IsNullOrEmpty($Version)) {
|
|
$BranchVersionRegex = [Regex]"^release-(\d+(\.\d+)+)$"
|
|
$Branch = & git rev-parse --abbrev-ref HEAD
|
|
$Version = $BranchVersionRegex.Match($Branch).Groups[1].Value
|
|
If ([String]::IsNullOrEmpty($Version)) {
|
|
Write-Error "No version specified, and we can't infer it from the name of your branch ($Branch)."
|
|
Exit 1
|
|
}
|
|
Write-Host "Inferred servicing version $Version"
|
|
}
|
|
|
|
$Script:TodoStatusName = "To Cherry Pick"
|
|
$Script:DoneStatusName = "Cherry Picked"
|
|
$Script:RejectStatusName = "Rejected"
|
|
|
|
# Propagate default values into all PSGitHub cmdlets
|
|
$GithubOrg = "microsoft"
|
|
$PSDefaultParameterValues['*GitHub*:Owner'] = $GithubOrg
|
|
$PSDefaultParameterValues['*GitHub*:RepositoryName'] = "terminal"
|
|
|
|
& git fetch --all 2>&1 | Out-Null
|
|
|
|
$Branch = & git rev-parse --abbrev-ref HEAD
|
|
$RemoteForCurrentBranch = & git config "branch.$Branch.remote"
|
|
$CommitsBehind = [int](& git rev-list --count "$RemoteForCurrentBranch/$Branch" "^$Branch")
|
|
|
|
If ($LASTEXITCODE -Ne 0) {
|
|
Write-Error "Failed to determine branch divergence"
|
|
Exit 1
|
|
}
|
|
|
|
If ($CommitsBehind -Gt 0) {
|
|
Write-Error "Local branch $Branch is out of date with $RemoteForCurrentBranch; consider git pull"
|
|
Exit 1
|
|
}
|
|
|
|
$projectNumber = Get-GraphQlProjectNumberGivenName -Organization $GithubOrg -Name "$Version Servicing Pipeline"
|
|
If([String]::IsNullOrEmpty($projectNumber)) {
|
|
Throw "Failed to find ProjectV2 for `"$Version Servicing Pipeline`""
|
|
}
|
|
|
|
$Project = Get-GraphQlProjectWithNodes -Organization $GithubOrg -Number $projectNumber
|
|
|
|
$StatusField = $Project.organization.projectV2.fields.nodes | Where-Object { $_.name -eq "Status" }
|
|
$StatusFieldId = $StatusField.id
|
|
$StatusRejectOptionId = $StatusField.options | Where-Object name -eq $script:RejectStatusName | Select-Object -Expand id
|
|
$StatusDoneOptionId = $StatusField.options | Where-Object name -eq $script:DoneStatusName | Select-Object -Expand id
|
|
|
|
# Create ServicingCards out of each node, but filter out the ones with no commits.
|
|
$cards = $Project.organization.projectV2.items.nodes | ForEach-Object { [ServicingCard]::new($_) }
|
|
|
|
$incompleteCards = $cards | Where-Object { [String]::IsNullOrEmpty($_.Commit) -And $_.Status -Eq ([ServicingStatus]::ToCherryPick) }
|
|
If ($incompleteCards.Length -Gt 0) {
|
|
Write-Host "Cards to cherry pick are not associated with commits:"
|
|
$incompleteCards | ForEach-Object {
|
|
Write-Host " - $($_.Format())"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
$considerCards = $cards | Where-Object Status -Eq ([ServicingStatus]::ToConsider)
|
|
If ($considerCards.Length -Gt 0) {
|
|
Write-Host "`e[7m CONSIDERATION QUEUE `e[27m"
|
|
$considerCards | ForEach-Object {
|
|
Write-Host " - $($_.Format())"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
$commitGroups = $cards | Where-Object { -Not [String]::IsNullOrEmpty($_.Commit) } | Group-Object Commit
|
|
$commits = New-Object System.Collections.ArrayList
|
|
$finalCardsForCommit = [System.Collections.Generic.Dictionary[String, ServicingCard[]]]::new()
|
|
|
|
$commitGroups | ForEach-Object {
|
|
$statuses = $_.Group | Select-Object -Unique Status
|
|
If ($statuses.Length -Gt 1) {
|
|
Write-Host "Commit $($_.Name) is present in more than one column:"
|
|
$_.Group | ForEach-Object {
|
|
Write-Host " - $($_.Format())"
|
|
}
|
|
Write-Host "`e[1mIt will be ignored.`e[m`n"
|
|
} Else {
|
|
If ($statuses[0].Status -eq ([ServicingStatus]::ToCherryPick)) {
|
|
$null = $commits.Add($_.Name)
|
|
$finalCardsForCommit[$_.Name] = $_.Group
|
|
}
|
|
}
|
|
}
|
|
|
|
If ($commits.Count -Eq 0) {
|
|
Write-Host "Nothing to do."
|
|
Exit
|
|
}
|
|
|
|
$sortedAllCommits = & git rev-list --no-walk=sorted $commits
|
|
[Array]::Reverse($sortedAllCommits)
|
|
|
|
$PickArgs = @()
|
|
If ($GpgSign) {
|
|
$PickArgs += , "-S"
|
|
}
|
|
|
|
$sortedAllCommits | ForEach-Object {
|
|
Write-Host "`e[96m`e[1;7mPICK`e[22;27m`e[m $(& git show -q --pretty=oneline $_)"
|
|
ForEach($card In $finalCardsForCommit[$_]) {
|
|
Write-Host $card.Format()
|
|
}
|
|
$null = & git cherry-pick -x $_ 2>&1
|
|
$Err = ""
|
|
While ($True) {
|
|
If ($LASTEXITCODE -ne 0) {
|
|
$Err
|
|
Enter-ConflictResolutionShell
|
|
|
|
If ($Global:Abort) {
|
|
& git cherry-pick --abort
|
|
Write-Host -ForegroundColor "Red" "YOU'RE ON YOUR OWN"
|
|
Exit
|
|
}
|
|
|
|
If ($Global:Reject) {
|
|
ForEach($card In $finalCardsForCommit[$_]) {
|
|
Set-GraphQlProjectEntryStatus -Project $Project.organization.projectV2.id -Item $card.Id -Field $StatusFieldId -Value $StatusRejectOptionId
|
|
}
|
|
# Fall through to Skip
|
|
}
|
|
|
|
If ($Global:Skip) {
|
|
& git cherry-pick --skip
|
|
Break
|
|
}
|
|
|
|
$Err = & git cherry-pick --continue --no-edit
|
|
} Else {
|
|
& git commit @PickArgs --amend --no-edit --trailer "Service-Card-Id:$($finalCardsForCommit[$_].Id)" --trailer "Service-Version:$Version" | Out-Null
|
|
Write-Host "`e[92;1;7m OK `e[m"
|
|
ForEach($card In $finalCardsForCommit[$_]) {
|
|
Set-GraphQlProjectEntryStatus -Project $Project.organization.projectV2.id -Item $card.Id -Field $StatusFieldId -Value $StatusDoneOptionId
|
|
}
|
|
Break
|
|
}
|
|
}
|
|
}
|