Skip to content

Commit 90310fa

Browse files
committed
chore: improve git interactions
chore: wip
1 parent 418fe4d commit 90310fa

File tree

2 files changed

+218
-39
lines changed

2 files changed

+218
-39
lines changed

bin/cli.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -944,13 +944,47 @@ cli
944944
hasWorkflowPermissions,
945945
)
946946

947-
// Get all open PRs
948-
const prs = await gitProvider.getPullRequests('open')
949-
const buddyPRs = prs.filter(pr =>
950-
pr.head.startsWith('buddy-bot/')
951-
|| pr.author === 'github-actions[bot]'
952-
|| pr.author.includes('buddy'),
953-
)
947+
// Get buddy-bot PRs using GitHub CLI to avoid API rate limits
948+
let buddyPRs: any[] = []
949+
try {
950+
// Try GitHub CLI first (much faster and doesn't count against API limits)
951+
const prOutput = await gitProvider.runCommand('gh', [
952+
'pr',
953+
'list',
954+
'--state',
955+
'open',
956+
'--json',
957+
'number,title,body,headRefName,author,url,createdAt,updatedAt',
958+
])
959+
const allPRs = JSON.parse(prOutput)
960+
961+
buddyPRs = allPRs.filter((pr: any) =>
962+
pr.headRefName?.startsWith('buddy-bot/')
963+
|| pr.author?.login === 'github-actions[bot]'
964+
|| pr.author?.login?.includes('buddy'),
965+
).map((pr: any) => ({
966+
number: pr.number,
967+
title: pr.title,
968+
body: pr.body || '',
969+
head: pr.headRefName,
970+
author: pr.author?.login || 'unknown',
971+
url: pr.url,
972+
createdAt: new Date(pr.createdAt),
973+
updatedAt: new Date(pr.updatedAt),
974+
}))
975+
976+
console.log(`🔍 Found ${buddyPRs.length} buddy-bot PRs using GitHub CLI (no API calls)`)
977+
}
978+
catch (cliError) {
979+
console.warn('⚠️ GitHub CLI failed, falling back to API:', cliError)
980+
// Fallback to API method
981+
const prs = await gitProvider.getPullRequests('open')
982+
buddyPRs = prs.filter(pr =>
983+
pr.head.startsWith('buddy-bot/')
984+
|| pr.author === 'github-actions[bot]'
985+
|| pr.author.includes('buddy'),
986+
)
987+
}
954988

955989
if (buddyPRs.length === 0) {
956990
logger.info('📋 No buddy-bot PRs found')

src/git/github-provider.ts

Lines changed: 177 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ export class GitHubProvider implements GitProvider {
477477
/**
478478
* Run a command and return its output
479479
*/
480-
private async runCommand(command: string, args: string[]): Promise<string> {
480+
async runCommand(command: string, args: string[]): Promise<string> {
481481
return new Promise((resolve, reject) => {
482482
const child = spawn(command, args, {
483483
stdio: 'pipe',
@@ -707,20 +707,18 @@ export class GitHubProvider implements GitProvider {
707707
*/
708708
async deleteBranch(branchName: string): Promise<void> {
709709
try {
710-
// Try to delete using GitHub CLI first
710+
// Try to delete using GitHub CLI first (corrected command)
711711
try {
712-
await this.runCommand('gh', ['api', 'repos', this.owner, this.repo, 'git/refs/heads', branchName, '-X', 'DELETE'])
713-
console.log(`✅ Deleted branch ${branchName} via CLI`)
712+
await this.runCommand('gh', ['api', `repos/${this.owner}/${this.repo}/git/refs/heads/${branchName}`, '-X', 'DELETE'])
714713
return
715714
}
716715
catch (cliError) {
717-
// Fall back to API
716+
// Fall back to API if CLI fails
718717
console.log(`⚠️ CLI branch deletion failed, trying API: ${cliError}`)
719718
}
720719

721-
// Fall back to API
722-
await this.apiRequest(`DELETE /repos/${this.owner}/${this.repo}/git/refs/heads/${branchName}`)
723-
console.log(`✅ Deleted branch ${branchName} via API`)
720+
// Fall back to API with retry logic for rate limiting
721+
await this.apiRequestWithRetry(`DELETE /repos/${this.owner}/${this.repo}/git/refs/heads/${branchName}`)
724722
}
725723
catch (error) {
726724
// Don't throw error for branch deletion failures - they're not critical
@@ -729,9 +727,68 @@ export class GitHubProvider implements GitProvider {
729727
}
730728

731729
/**
732-
* Get all buddy-bot branches from the repository
730+
* Get all buddy-bot branches from the repository using local git commands
733731
*/
734732
async getBuddyBotBranches(): Promise<Array<{ name: string, sha: string, lastCommitDate: Date }>> {
733+
try {
734+
// Use local git to get all remote branches
735+
const remoteBranchesOutput = await this.runCommand('git', ['branch', '-r', '--format=%(refname:short) %(objectname) %(committerdate:iso8601)'])
736+
737+
const branches: Array<{ name: string, sha: string, lastCommitDate: Date }> = []
738+
739+
for (const line of remoteBranchesOutput.split('\n')) {
740+
const trimmed = line.trim()
741+
if (!trimmed)
742+
continue
743+
744+
const parts = trimmed.split(' ')
745+
if (parts.length < 3)
746+
continue
747+
748+
const fullBranchName = parts[0] // e.g., "origin/buddy-bot/update-deps"
749+
const sha = parts[1]
750+
const dateStr = parts.slice(2).join(' ') // Join back in case date has spaces
751+
752+
// Extract just the branch name without remote prefix
753+
const branchName = fullBranchName.replace(/^origin\//, '')
754+
755+
// Only include buddy-bot branches
756+
if (!branchName.startsWith('buddy-bot/'))
757+
continue
758+
759+
try {
760+
const lastCommitDate = new Date(dateStr)
761+
branches.push({
762+
name: branchName,
763+
sha,
764+
lastCommitDate,
765+
})
766+
}
767+
catch {
768+
console.warn(`⚠️ Failed to parse date for branch ${branchName}: ${dateStr}`)
769+
branches.push({
770+
name: branchName,
771+
sha,
772+
lastCommitDate: new Date(0), // Fallback to epoch
773+
})
774+
}
775+
}
776+
777+
console.log(`🔍 Found ${branches.length} buddy-bot branches using local git`)
778+
return branches
779+
}
780+
catch (error) {
781+
console.warn('⚠️ Failed to fetch buddy-bot branches via git, falling back to API:', error)
782+
783+
// Fallback to API method if git fails
784+
return this.getBuddyBotBranchesViaAPI()
785+
}
786+
}
787+
788+
/**
789+
* Fallback method to get buddy-bot branches via API (original implementation)
790+
*/
791+
private async getBuddyBotBranchesViaAPI(): Promise<Array<{ name: string, sha: string, lastCommitDate: Date }>> {
735792
try {
736793
// Fetch all branches with pagination
737794
let allBranches: any[] = []
@@ -796,13 +853,20 @@ export class GitHubProvider implements GitProvider {
796853
*/
797854
async getOrphanedBuddyBotBranches(): Promise<Array<{ name: string, sha: string, lastCommitDate: Date }>> {
798855
try {
799-
const [buddyBranches, openPRs] = await Promise.all([
800-
this.getBuddyBotBranches(),
801-
this.getPullRequests('open'),
802-
])
856+
const buddyBranches = await this.getBuddyBotBranches()
857+
858+
// Try to get PR branches using local git first
859+
let prBranches: Set<string>
860+
try {
861+
prBranches = await this.getOpenPRBranchesViaGit()
862+
}
863+
catch (error) {
864+
console.warn('⚠️ Failed to get PR branches via git, falling back to API:', error)
865+
const openPRs = await this.getPullRequests('open')
866+
prBranches = new Set(openPRs.map(pr => pr.head))
867+
}
803868

804869
// Filter out branches that have active PRs
805-
const prBranches = new Set(openPRs.map(pr => pr.head))
806870
const orphanedBranches = buddyBranches.filter(branch => !prBranches.has(branch.name))
807871

808872
return orphanedBranches
@@ -813,6 +877,56 @@ export class GitHubProvider implements GitProvider {
813877
}
814878
}
815879

880+
/**
881+
* Get branches that have open PRs using local git commands
882+
*/
883+
private async getOpenPRBranchesViaGit(): Promise<Set<string>> {
884+
try {
885+
// Use GitHub CLI to get open PRs if available
886+
try {
887+
const prOutput = await this.runCommand('gh', ['pr', 'list', '--state', 'open', '--json', 'headRefName'])
888+
const prs = JSON.parse(prOutput)
889+
const prBranches = new Set<string>()
890+
891+
for (const pr of prs) {
892+
if (pr.headRefName && pr.headRefName.startsWith('buddy-bot/')) {
893+
prBranches.add(pr.headRefName)
894+
}
895+
}
896+
897+
console.log(`🔍 Found ${prBranches.size} open PR branches using GitHub CLI`)
898+
return prBranches
899+
}
900+
catch {
901+
console.warn('⚠️ GitHub CLI not available or failed, trying alternative method')
902+
903+
// Alternative: check if branches exist in refs/pull/ (GitHub's PR refs)
904+
try {
905+
const pullRefsOutput = await this.runCommand('git', ['ls-remote', '--heads', 'origin'])
906+
const prBranches = new Set<string>()
907+
908+
for (const line of pullRefsOutput.split('\n')) {
909+
const match = line.match(/refs\/heads\/(buddy-bot\/\S+)/)
910+
if (match) {
911+
// This is a buddy-bot branch, but we can't easily tell if it has an open PR
912+
// without API calls, so we'll be conservative and assume it might
913+
prBranches.add(match[1])
914+
}
915+
}
916+
917+
console.log(`🔍 Found ${prBranches.size} potential PR branches using git ls-remote`)
918+
return prBranches
919+
}
920+
catch (gitError) {
921+
throw new Error(`Both GitHub CLI and git ls-remote failed: ${gitError}`)
922+
}
923+
}
924+
}
925+
catch (error) {
926+
throw new Error(`Failed to get open PR branches via git: ${error}`)
927+
}
928+
}
929+
816930
/**
817931
* Clean up stale buddy-bot branches
818932
*/
@@ -859,33 +973,36 @@ export class GitHubProvider implements GitProvider {
859973

860974
console.log(`🧹 Cleaning up ${staleBranches.length} stale branches...`)
861975

862-
// Delete branches in batches to avoid rate limiting
863-
const batchSize = 10
976+
// Delete branches in smaller batches with longer delays to avoid rate limiting
977+
const batchSize = 5 // Reduced from 10 to be more conservative
864978
for (let i = 0; i < staleBranches.length; i += batchSize) {
865979
const batch = staleBranches.slice(i, i + batchSize)
866980
const batchNumber = Math.floor(i / batchSize) + 1
867981
const totalBatches = Math.ceil(staleBranches.length / batchSize)
868982

869983
console.log(`🔄 Processing batch ${batchNumber}/${totalBatches} (${batch.length} branches)`)
870984

871-
await Promise.all(
872-
batch.map(async (branch) => {
873-
try {
874-
await this.deleteBranch(branch.name)
875-
deleted.push(branch.name)
876-
console.log(`✅ Deleted: ${branch.name}`)
877-
}
878-
catch (error) {
879-
failed.push(branch.name)
880-
console.warn(`❌ Failed to delete ${branch.name}:`, error)
881-
}
882-
}),
883-
)
985+
// Process branches sequentially within batch to avoid overwhelming the API
986+
for (const branch of batch) {
987+
try {
988+
await this.deleteBranch(branch.name)
989+
deleted.push(branch.name)
990+
console.log(`✅ Deleted: ${branch.name}`)
991+
}
992+
catch (error) {
993+
failed.push(branch.name)
994+
console.warn(`❌ Failed to delete ${branch.name}:`, error)
995+
}
996+
997+
// Small delay between individual deletions within batch
998+
await new Promise(resolve => setTimeout(resolve, 200))
999+
}
8841000

885-
// Small delay between batches to be nice to the API
1001+
// Longer delay between batches to be respectful of API limits
8861002
if (i + batchSize < staleBranches.length) {
887-
console.log('⏳ Waiting 1 second before next batch...')
888-
await new Promise(resolve => setTimeout(resolve, 1000))
1003+
const delay = 3000 // 3 seconds between batches
1004+
console.log(`⏳ Waiting ${delay / 1000} seconds before next batch...`)
1005+
await new Promise(resolve => setTimeout(resolve, delay))
8891006
}
8901007
}
8911008

@@ -936,6 +1053,34 @@ export class GitHubProvider implements GitProvider {
9361053
return response.text()
9371054
}
9381055

1056+
/**
1057+
* Make authenticated API request to GitHub with retry logic for rate limiting
1058+
*/
1059+
private async apiRequestWithRetry(endpoint: string, data?: any, maxRetries = 3): Promise<any> {
1060+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
1061+
try {
1062+
return await this.apiRequest(endpoint, data)
1063+
}
1064+
catch (error: any) {
1065+
const isRateLimit = error.message?.includes('403') && error.message?.includes('rate limit')
1066+
1067+
if (isRateLimit && attempt < maxRetries) {
1068+
// Extract retry-after from error or use exponential backoff
1069+
const baseDelay = 2 ** attempt * 1000 // 2s, 4s, 8s
1070+
const jitter = Math.random() * 1000 // Add up to 1s jitter
1071+
const delay = baseDelay + jitter
1072+
1073+
console.log(`⏳ Rate limited, waiting ${Math.round(delay / 1000)}s before retry ${attempt}/${maxRetries}...`)
1074+
await new Promise(resolve => setTimeout(resolve, delay))
1075+
continue
1076+
}
1077+
1078+
// If not rate limit or max retries reached, throw the error
1079+
throw error
1080+
}
1081+
}
1082+
}
1083+
9391084
async createIssue(options: IssueOptions): Promise<Issue> {
9401085
try {
9411086
const response = await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues`, {

0 commit comments

Comments
 (0)