@@ -773,12 +773,17 @@ export class GithubService
773
773
* @param params - The parameters for fetching pull requests, including organization and team data, repository filters, and pull request filters.
774
774
* @param params.organizationAndTeamData - The organization and team data containing organizationId and teamId.
775
775
* @param params.repository - Optional repository filter to fetch pull requests from a specific repository.
776
- * @param params.filters - Optional filters for pull requests, including startDate, endDate, state, author, and branch .
776
+ * @param params.filters - Optional filters for pull requests, including startDate, endDate, state, author, branch, number, id, title, repository, and url .
777
777
* @param params.filters.startDate - The start date for filtering pull requests.
778
778
* @param params.filters.endDate - The end date for filtering pull requests.
779
779
* @param params.filters.state - The state of the pull requests to filter (e.g., 'open', 'closed', 'all').
780
780
* @param params.filters.author - The author of the pull requests to filter.
781
781
* @param params.filters.branch - The branch from which to fetch pull requests.
782
+ * @param params.filters.number - The pull request number to retrieve.
783
+ * @param params.filters.id - The pull request id to filter by.
784
+ * @param params.filters.title - The pull request title to filter by (contains match).
785
+ * @param params.filters.repository - The repository name to filter by (contains match).
786
+ * @param params.filters.url - The pull request URL to filter by (contains match).
782
787
* @returns A promise that resolves to an array of PullRequest objects.
783
788
*/
784
789
async getPullRequests ( params : {
@@ -793,6 +798,11 @@ export class GithubService
793
798
state ?: PullRequestState ;
794
799
author ?: string ;
795
800
branch ?: string ;
801
+ number ?: number ;
802
+ id ?: number ;
803
+ title ?: string ;
804
+ repository ?: string ;
805
+ url ?: string ;
796
806
} ;
797
807
} ) : Promise < PullRequest [ ] > {
798
808
const { organizationAndTeamData, repository, filters = { } } = params ;
@@ -851,11 +861,51 @@ export class GithubService
851
861
}
852
862
853
863
reposToProcess = [ foundRepo ] ;
864
+ } else if ( filters . repository ) {
865
+ reposToProcess = allRepositories . filter ( ( r ) =>
866
+ r . name . toLowerCase ( ) . includes ( filters . repository ! . toLowerCase ( ) )
867
+ ) ;
868
+
869
+ if ( reposToProcess . length === 0 ) {
870
+ this . logger . warn ( {
871
+ message : `No repositories found matching filter: ${ filters . repository } ` ,
872
+ context : GithubService . name ,
873
+ metadata : params ,
874
+ } ) ;
875
+
876
+ return [ ] ;
877
+ }
854
878
}
855
879
856
880
const octokit = await this . instanceOctokit ( organizationAndTeamData ) ;
857
881
const owner = await this . getCorrectOwner ( githubAuthDetail , octokit ) ;
858
882
883
+ // If URL filter is provided, try to extract PR info from URL for optimization
884
+ if ( filters . url ) {
885
+ const urlInfo = this . parseGithubUrl ( filters . url ) ;
886
+ if ( urlInfo ?. owner && urlInfo ?. repo && urlInfo ?. prNumber ) {
887
+ // Direct fetch if URL contains complete PR info
888
+ const specificRepo = reposToProcess . find ( r =>
889
+ r . name === urlInfo . repo ||
890
+ r . name === `${ urlInfo . owner } /${ urlInfo . repo } `
891
+ ) ;
892
+
893
+ if ( specificRepo ) {
894
+ const directResult = await this . getPullRequestsByRepo ( {
895
+ octokit,
896
+ owner,
897
+ repo : specificRepo . name ,
898
+ filters : { ...filters , number : urlInfo . prNumber } ,
899
+ } ) ;
900
+
901
+ const rawPullRequests = directResult . flat ( ) ;
902
+ return rawPullRequests . map ( ( rawPr ) =>
903
+ this . transformPullRequest ( rawPr , organizationAndTeamData ) ,
904
+ ) ;
905
+ }
906
+ }
907
+ }
908
+
859
909
const promises = reposToProcess . map ( ( r ) =>
860
910
this . getPullRequestsByRepo ( {
861
911
octokit,
@@ -898,14 +948,70 @@ export class GithubService
898
948
state ?: PullRequestState ;
899
949
author ?: string ;
900
950
branch ?: string ;
951
+ number ?: number ;
952
+ id ?: number ;
953
+ title ?: string ;
954
+ url ?: string ;
901
955
} ;
902
956
} ) : Promise <
903
957
| RestEndpointMethodTypes [ 'pulls' ] [ 'list' ] [ 'response' ] [ 'data' ]
904
958
| RestEndpointMethodTypes [ 'pulls' ] [ 'get' ] [ 'response' ] [ 'data' ] [ ]
905
959
> {
906
960
const { octokit, owner, repo, filters = { } } = params ;
907
- const { startDate, endDate, state, author, branch } = filters ;
961
+ const { startDate, endDate, state, author, branch, number, id, title, url } = filters ;
962
+
963
+ // If PR number is provided, fetch it directly for this repo
964
+ if ( number ) {
965
+ try {
966
+ const { data : pr } = await octokit . rest . pulls . get ( {
967
+ owner,
968
+ repo,
969
+ pull_number : number ,
970
+ } ) ;
971
+
972
+ let isValid = true ;
973
+
974
+ if ( author ) {
975
+ isValid =
976
+ isValid &&
977
+ pr . user ?. login . toLowerCase ( ) === author . toLowerCase ( ) ;
978
+ }
979
+
980
+ if ( typeof id === 'number' ) {
981
+ isValid = isValid && pr . id === id ;
982
+ }
983
+
984
+ if ( title ) {
985
+ isValid =
986
+ isValid &&
987
+ pr . title . toLowerCase ( ) . includes ( title . toLowerCase ( ) ) ;
988
+ }
989
+
990
+ if ( url ) {
991
+ isValid =
992
+ isValid &&
993
+ pr . html_url . toLowerCase ( ) . includes ( url . toLowerCase ( ) ) ;
994
+ }
908
995
996
+ return isValid ? [ pr ] : [ ] ;
997
+ } catch ( error ) {
998
+ const status = ( error as { status ?: number } ) ?. status ;
999
+ if ( status === 404 ) return [ ] ;
1000
+ return [ ] ;
1001
+ }
1002
+ }
1003
+
1004
+ // Use GitHub Search API for text-based filters (more efficient)
1005
+ if ( title || url ) {
1006
+ return this . searchPullRequestsByTitle ( {
1007
+ octokit,
1008
+ owner,
1009
+ repo,
1010
+ filters,
1011
+ } ) ;
1012
+ }
1013
+
1014
+ // Use native API filters when possible
909
1015
const pullRequests = await octokit . paginate ( octokit . rest . pulls . list , {
910
1016
owner,
911
1017
repo,
@@ -929,10 +1035,184 @@ export class GithubService
929
1035
pr . user ?. login . toLowerCase ( ) === author . toLowerCase ( ) ;
930
1036
}
931
1037
1038
+ if ( typeof id === 'number' ) {
1039
+ isValid = isValid && pr . id === id ;
1040
+ }
1041
+
1042
+ if ( url ) {
1043
+ isValid =
1044
+ isValid &&
1045
+ pr . html_url . toLowerCase ( ) . includes ( url . toLowerCase ( ) ) ;
1046
+ }
1047
+
932
1048
return isValid ;
933
1049
} ) ;
934
1050
}
935
1051
1052
+ private async searchPullRequestsByTitle ( params : {
1053
+ octokit : Octokit ;
1054
+ owner : string ;
1055
+ repo : string ;
1056
+ filters : {
1057
+ startDate ?: Date ;
1058
+ endDate ?: Date ;
1059
+ state ?: PullRequestState ;
1060
+ author ?: string ;
1061
+ branch ?: string ;
1062
+ title ?: string ;
1063
+ id ?: number ;
1064
+ url ?: string ;
1065
+ } ;
1066
+ } ) : Promise < RestEndpointMethodTypes [ 'pulls' ] [ 'list' ] [ 'response' ] [ 'data' ] > {
1067
+ const { octokit, owner, repo, filters } = params ;
1068
+ const { startDate, endDate, state, author, branch, title, id, url } = filters ;
1069
+
1070
+ let query = `is:pr repo:${ owner } /${ repo } ` ;
1071
+
1072
+ if ( title ) {
1073
+ query += ` ${ title } in:title` ;
1074
+ }
1075
+
1076
+ if ( state && state !== PullRequestState . ALL ) {
1077
+ const githubState = this . _prStateMapReverse . get ( state ) ;
1078
+ if ( githubState && githubState !== 'all' ) {
1079
+ query += ` is:${ githubState } ` ;
1080
+ }
1081
+ }
1082
+
1083
+ if ( author ) {
1084
+ query += ` author:${ author } ` ;
1085
+ }
1086
+
1087
+ if ( branch ) {
1088
+ query += ` base:${ branch } ` ;
1089
+ }
1090
+
1091
+ if ( startDate ) {
1092
+ query += ` created:>=${ startDate . toISOString ( ) . split ( 'T' ) [ 0 ] } ` ;
1093
+ }
1094
+
1095
+ if ( endDate ) {
1096
+ query += ` created:<=${ endDate . toISOString ( ) . split ( 'T' ) [ 0 ] } ` ;
1097
+ }
1098
+
1099
+ try {
1100
+ const searchResults = await octokit . paginate ( octokit . rest . search . issuesAndPullRequests , {
1101
+ q : query ,
1102
+ sort : 'created' ,
1103
+ order : 'desc' ,
1104
+ per_page : 100 ,
1105
+ } ) ;
1106
+
1107
+ const pullRequests = searchResults . filter ( ( item ) => item . pull_request ) ;
1108
+
1109
+ const filteredBySearch = pullRequests . filter ( ( pr ) => {
1110
+ let isValid = true ;
1111
+
1112
+ if ( typeof id === 'number' ) {
1113
+ isValid = isValid && pr . id === id ;
1114
+ }
1115
+
1116
+ return isValid ;
1117
+ } ) ;
1118
+
1119
+ const prNumbers = filteredBySearch . map ( pr => pr . number ) ;
1120
+
1121
+ const detailedPRs = await Promise . all (
1122
+ prNumbers . map ( async ( prNumber ) => {
1123
+ try {
1124
+ const { data } = await octokit . rest . pulls . get ( {
1125
+ owner,
1126
+ repo,
1127
+ pull_number : prNumber ,
1128
+ } ) ;
1129
+ return data ;
1130
+ } catch ( error ) {
1131
+ return null ;
1132
+ }
1133
+ } )
1134
+ ) ;
1135
+
1136
+ return detailedPRs . filter ( ( pr ) => pr !== null ) as unknown as RestEndpointMethodTypes [ 'pulls' ] [ 'list' ] [ 'response' ] [ 'data' ] ;
1137
+ } catch ( error ) {
1138
+ this . logger . warn ( {
1139
+ message : 'GitHub Search API failed, falling back to list API' ,
1140
+ context : GithubService . name ,
1141
+ error,
1142
+ metadata : { query, repo : `${ owner } /${ repo } ` } ,
1143
+ } ) ;
1144
+
1145
+ const pullRequests = await octokit . paginate ( octokit . rest . pulls . list , {
1146
+ owner,
1147
+ repo,
1148
+ state : state
1149
+ ? this . _prStateMapReverse . get ( state )
1150
+ : this . _prStateMapReverse . get ( PullRequestState . ALL ) ,
1151
+ base : branch ,
1152
+ sort : 'created' ,
1153
+ direction : 'desc' ,
1154
+ since : startDate ?. toISOString ( ) ,
1155
+ until : endDate ?. toISOString ( ) ,
1156
+ per_page : 100 ,
1157
+ } ) ;
1158
+
1159
+ return pullRequests . filter ( ( pr ) => {
1160
+ let isValid = true ;
1161
+
1162
+ if ( author ) {
1163
+ isValid =
1164
+ isValid &&
1165
+ pr . user ?. login . toLowerCase ( ) === author . toLowerCase ( ) ;
1166
+ }
1167
+
1168
+ if ( typeof id === 'number' ) {
1169
+ isValid = isValid && pr . id === id ;
1170
+ }
1171
+
1172
+ if ( title ) {
1173
+ isValid =
1174
+ isValid &&
1175
+ pr . title . toLowerCase ( ) . includes ( title . toLowerCase ( ) ) ;
1176
+ }
1177
+
1178
+ if ( url ) {
1179
+ isValid =
1180
+ isValid &&
1181
+ pr . html_url . toLowerCase ( ) . includes ( url . toLowerCase ( ) ) ;
1182
+ }
1183
+
1184
+ return isValid ;
1185
+ } ) ;
1186
+ }
1187
+ }
1188
+
1189
+ private parseGithubUrl ( url : string ) : { owner : string ; repo : string ; prNumber : number } | null {
1190
+ try {
1191
+ // Parse GitHub PR URLs like:
1192
+ // https://github.com/owner/repo/pull/123
1193
+ // https://github.com/owner/repo/pulls/123
1194
+ const urlObj = new URL ( url ) ;
1195
+ const pathParts = urlObj . pathname . split ( '/' ) . filter ( part => part ) ;
1196
+
1197
+ if ( pathParts . length >= 4 &&
1198
+ urlObj . hostname === 'github.com' &&
1199
+ ( pathParts [ 2 ] === 'pull' || pathParts [ 2 ] === 'pulls' ) ) {
1200
+
1201
+ const owner = pathParts [ 0 ] ;
1202
+ const repo = pathParts [ 1 ] ;
1203
+ const prNumber = parseInt ( pathParts [ 3 ] , 10 ) ;
1204
+
1205
+ if ( ! isNaN ( prNumber ) ) {
1206
+ return { owner, repo, prNumber } ;
1207
+ }
1208
+ }
1209
+ } catch ( error ) {
1210
+ // Invalid URL, ignore
1211
+ }
1212
+
1213
+ return null ;
1214
+ }
1215
+
936
1216
async getPullRequestAuthors ( params : {
937
1217
organizationAndTeamData : OrganizationAndTeamData ;
938
1218
} ) : Promise < PullRequestAuthor [ ] > {
0 commit comments