-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathDelete-RemoteBranches.ps1
More file actions
221 lines (202 loc) · 8.48 KB
/
Delete-RemoteBranches.ps1
File metadata and controls
221 lines (202 loc) · 8.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
[CmdletBinding(SupportsShouldProcess)]
param(
# Please use the RepoOwner/RepoName format: e.g. Azure/azure-sdk-for-java
$RepoId,
# CentralRepoId the original PR to generate sync PR. E.g Azure/azure-sdk-tools for eng/common
$CentralRepoId,
# We start from the sync PRs, use the branch name to get the PR number of central repo. E.g. sync-eng/common-(<branchName>)-(<PrNumber>). Have group name on PR number.
# For sync-eng/common work, we use regex as "^sync-eng/common.*-(?<PrNumber>\d+).*$".
# For sync-.github work, we use regex as "^sync-.github.*-(?<PrNumber>\d+).*$".
$BranchRegex,
# When set, directly delete this exact branch name without querying all branches.
# This is a fast path that avoids the expensive GraphQL branch listing and rate limit
# checks, useful when the caller already knows the exact branch name to delete (e.g.
# the eng/common sync pipeline). If the branch does not exist, a warning is
# logged and the script exits successfully.
[string]$TargetBranch,
# Date format: e.g. Tuesday, April 12, 2022 1:36:02 PM. Allow to use other date format.
[AllowNull()]
[DateTime]$LastCommitOlderThan,
[Switch]$DeleteBranchesEvenIfThereIsOpenPR = $false,
$AuthToken
)
Set-StrictMode -version 3
. (Join-Path $PSScriptRoot common.ps1)
function Get-AllBranchesAndPullRequestInfo($owner, $repo) {
$query = @'
query($owner: String!, $repo: String!, $refPrefix: String!$endCursor: String) {
repository(owner: $owner, name: $repo) {
refs(first: 100, refPrefix: $refPrefix, after: $endCursor) {
nodes {
name
target {
commitUrl
... on Commit {
committedDate
}
}
associatedPullRequests(first: 100) {
nodes {
url
closed
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
'@
$all_branches = gh api graphql --paginate -F owner=$owner -F repo=$repo -F refPrefix='refs/heads/' -f query=$query `
--jq '.data.repository.refs.nodes[] | { name, commitUrl: .target.commitUrl, committedDate: .target.committedDate, pullRequests: .associatedPullRequests.nodes }' | ConvertFrom-Json
if ($LASTEXITCODE) {
LogError "Failed to retrieve branches for '$owner' and '$repo' running query '$query'"
exit $LASTEXITCODE
}
return $all_branches
}
LogDebug "Operating on Repo '$RepoId'"
# Setup GH_TOKEN for the gh cli commands
if ($AuthToken) {
$env:GH_TOKEN = $AuthToken
}
$owner, $repo = $RepoId -split "/"
# Fast path: when the caller already knows the exact branch name, skip the expensive
# GraphQL query that lists all branches and the associated rate limit checks. This
# reduces the number of GitHub API calls from ~5+ down to 1 per repo, which helps
# avoid hitting the secondary rate limit when deleting branches across many repos.
if ($TargetBranch) {
LogDebug "TargetBranch specified, using direct deletion for branch '$TargetBranch' in '$RepoId'"
if ($PSCmdlet.ShouldProcess("'$TargetBranch' in '$RepoId'", "Deleting branch (target)")) {
$response = gh api "repos/${RepoId}/git/refs/heads/${TargetBranch}" -X DELETE 2>&1
if ($LASTEXITCODE) {
# GitHub returns 422 when the ref doesn't exist. Treat that as a non-fatal warning
# since the goal (branch gone) is already achieved.
if ($response -match "Reference does not exist" -or $response -match "HTTP 422") {
LogWarning "Branch '$TargetBranch' does not exist in '$RepoId'. It may have already been deleted."
}
else {
LogError "Deletion of branch '$TargetBranch' in '$RepoId' failed: $response"
exit 1
}
}
else {
LogDebug "Successfully deleted branch '$TargetBranch' in '$RepoId'."
}
}
# Nothing else to do, exit early.
exit 0
}
# These will always be output at the end of the script. Their only purpose is for information gathering
# Total number returned from query
$totalBranchesFromQuery = 0
# reasons why a branch was skipped
$skippedBranchNotMatchRegex = 0
$skippedForCommitDate = 0
$skippedForOpenPRs = 0
$skippedForPRNotInBranch = 0
$skippedForPRNotInRepo = 0
# gh call counters
$ghPRViewCalls = 0
$ghBranchDeleteCalls = 0
try {
# Output the core rate limit at the start of processing. There's no real need
# to output this at the end because the GH call counts are being output
$coreRateLimit = Get-RateLimit core
Write-RateLimit $coreRateLimit
# Output the GraphQL rate limit before and after the call
$graphqlRateLimit = Get-RateLimit graphql
Write-RateLimit $graphqlRateLimit "Before GraphQL Call"
$branches = Get-AllBranchesAndPullRequestInfo $owner $repo
$graphqlRateLimit = Get-RateLimit graphql
Write-RateLimit $graphqlRateLimit "After GraphQL Call"
if ($branches) {
$totalBranchesFromQuery = $branches.Count
}
foreach ($branch in $branches)
{
$branchName = $branch.Name
if ($branchName -notmatch $BranchRegex) {
$skippedBranchNotMatchRegex++
continue
}
$openPullRequests = @($branch.pullRequests | Where-Object { !$_.Closed })
# If we have a central PR that created this branch still open don't delete the branch
if ($CentralRepoId)
{
$pullRequestNumber = $matches["PrNumber"]
# If central PR number is not found, then skip
if (!$pullRequestNumber) {
LogError "No PR number found in the branch name. Please check the branch name '$branchName'. Skipping..."
$skippedForPRNotInBranch++
continue
}
$ghPRViewCalls++
$centralPR = gh pr view --json 'url,closed' --repo $CentralRepoId $pullRequestNumber | ConvertFrom-Json
if ($LASTEXITCODE) {
LogError "PR '$pullRequestNumber' not found in repo '$CentralRepoId'. Skipping..."
$skippedForPRNotInRepo++
continue
} else {
LogDebug "Found central PR $($centralPR.url) and Closed=$($centralPR.closed)"
if (!$centralPR.Closed) {
$skippedForOpenPRs++
# Skipping if there is an open central PR open for the branch.
LogDebug "Central PR is still open so skipping the deletion of branch '$branchName'. Skipping..."
continue
}
}
}
else {
# Not CentralRepoId - not associated with a central repo PR
if ($openPullRequests.Count -gt 0 -and !$DeleteBranchesEvenIfThereIsOpenPR) {
$skippedForOpenPRs++
LogDebug "Found open PRs associate with branch '$branchName'. Skipping..."
continue
}
}
# If there is date filter, then check if branch last commit is older than the date.
if ($LastCommitOlderThan)
{
$commitDate = $branch.committedDate
if ($commitDate -gt $LastCommitOlderThan) {
$skippedForCommitDate++
LogDebug "The branch $branch last commit date '$commitDate' is newer than the date '$LastCommitOlderThan'. Skipping..."
continue
}
}
foreach ($openPullRequest in $openPullRequests) {
LogDebug "Note: Open pull Request '$($openPullRequest.url)' will be closed after branch deletion, given the central PR is closed."
}
$commitUrl = $branch.commitUrl
if ($PSCmdlet.ShouldProcess("'$branchName' in '$RepoId'", "Deleting branch on cleanup script")) {
$ghBranchDeleteCalls++
gh api "repos/${RepoId}/git/refs/heads/${branchName}" -X DELETE
if ($LASTEXITCODE) {
LogError "Deletion of branch '$branchName` failed, see command output above"
exit $LASTEXITCODE
}
LogDebug "The branch '$branchName' at commit '$commitUrl' in '$RepoId' has been deleted."
}
}
}
finally {
Write-Host "Number of branches returned from graphql query: $totalBranchesFromQuery"
# The $BranchRegex seems to be always set
if ($BranchRegex) {
Write-Host "Number of branches that didn't match the BranchRegex: $skippedBranchNotMatchRegex"
}
Write-Host "Number of branches skipped for newer last commit date: $skippedForCommitDate"
Write-Host "Number of branches skipped for open PRs: $skippedForOpenPRs"
Write-Host "Number of gh api calls to delete branches: $ghBranchDeleteCalls"
# The following are only applicable when $CentralRepoId is passed in
if ($CentralRepoId) {
Write-Host "The following are applicable because CentralRepoId was passed in:"
Write-Host " Number of gh pr view calls: $ghPRViewCalls"
Write-Host " Number of branches skipped due to PR not in the repository: $skippedForPRNotInRepo "
Write-Host " Number of branches skipped due to PR not in the branch name: $skippedForPRNotInBranch"
}
}