@@ -477,7 +477,7 @@ export class GitHubProvider implements GitProvider {
477
477
/**
478
478
* Run a command and return its output
479
479
*/
480
- private async runCommand ( command : string , args : string [ ] ) : Promise < string > {
480
+ async runCommand ( command : string , args : string [ ] ) : Promise < string > {
481
481
return new Promise ( ( resolve , reject ) => {
482
482
const child = spawn ( command , args , {
483
483
stdio : 'pipe' ,
@@ -707,20 +707,18 @@ export class GitHubProvider implements GitProvider {
707
707
*/
708
708
async deleteBranch ( branchName : string ) : Promise < void > {
709
709
try {
710
- // Try to delete using GitHub CLI first
710
+ // Try to delete using GitHub CLI first (corrected command)
711
711
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' ] )
714
713
return
715
714
}
716
715
catch ( cliError ) {
717
- // Fall back to API
716
+ // Fall back to API if CLI fails
718
717
console . log ( `⚠️ CLI branch deletion failed, trying API: ${ cliError } ` )
719
718
}
720
719
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 } ` )
724
722
}
725
723
catch ( error ) {
726
724
// Don't throw error for branch deletion failures - they're not critical
@@ -729,9 +727,68 @@ export class GitHubProvider implements GitProvider {
729
727
}
730
728
731
729
/**
732
- * Get all buddy-bot branches from the repository
730
+ * Get all buddy-bot branches from the repository using local git commands
733
731
*/
734
732
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 ( / ^ o r i g i n \/ / , '' )
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 } > > {
735
792
try {
736
793
// Fetch all branches with pagination
737
794
let allBranches : any [ ] = [ ]
@@ -796,13 +853,20 @@ export class GitHubProvider implements GitProvider {
796
853
*/
797
854
async getOrphanedBuddyBotBranches ( ) : Promise < Array < { name : string , sha : string , lastCommitDate : Date } > > {
798
855
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
+ }
803
868
804
869
// Filter out branches that have active PRs
805
- const prBranches = new Set ( openPRs . map ( pr => pr . head ) )
806
870
const orphanedBranches = buddyBranches . filter ( branch => ! prBranches . has ( branch . name ) )
807
871
808
872
return orphanedBranches
@@ -813,6 +877,56 @@ export class GitHubProvider implements GitProvider {
813
877
}
814
878
}
815
879
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 ( / r e f s \/ h e a d s \/ ( b u d d y - b o t \/ \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
+
816
930
/**
817
931
* Clean up stale buddy-bot branches
818
932
*/
@@ -859,33 +973,36 @@ export class GitHubProvider implements GitProvider {
859
973
860
974
console . log ( `🧹 Cleaning up ${ staleBranches . length } stale branches...` )
861
975
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
864
978
for ( let i = 0 ; i < staleBranches . length ; i += batchSize ) {
865
979
const batch = staleBranches . slice ( i , i + batchSize )
866
980
const batchNumber = Math . floor ( i / batchSize ) + 1
867
981
const totalBatches = Math . ceil ( staleBranches . length / batchSize )
868
982
869
983
console . log ( `🔄 Processing batch ${ batchNumber } /${ totalBatches } (${ batch . length } branches)` )
870
984
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
+ }
884
1000
885
- // Small delay between batches to be nice to the API
1001
+ // Longer delay between batches to be respectful of API limits
886
1002
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 ) )
889
1006
}
890
1007
}
891
1008
@@ -936,6 +1053,34 @@ export class GitHubProvider implements GitProvider {
936
1053
return response . text ( )
937
1054
}
938
1055
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
+
939
1084
async createIssue ( options : IssueOptions ) : Promise < Issue > {
940
1085
try {
941
1086
const response = await this . apiRequest ( `POST /repos/${ this . owner } /${ this . repo } /issues` , {
0 commit comments