66import sys
77import argparse
88import requests
9+ import re
910
1011
1112def organise_prs (prs ):
@@ -29,23 +30,21 @@ def organise_prs(prs):
2930 merged_at .append (pr ['merged_at' ])
3031
3132 # sort the merged PRs by their merged timestamp
32- prs_merged = [pr for _ , pr in sorted (zip (merged_at , prs ))]
33+ prs_merged = [pr for _ , pr in sorted (zip (merged_at , prs_merged ))]
3334
3435 return prs_merged , prs_other
3536
3637
37- def get_prs (owner , repo , prod_label , pr_state , include_unmerged , per_page = 50 , start_page = 1 , pages = 1 ):
38+ def get_prs (owner , repo , request_labels , pr_state , per_page = 50 , start_page = 1 , pages = 1 ):
3839 """
3940 Get PRs according to some selection
4041 """
4142 # GitHub API endpoint for listing closed pull requests with a specific label
42- merged_token = '&is:merged=true' if not include_unmerged else ''
4343 prs_return = []
4444
4545 has_error = False
4646 for page in range (start_page , pages + 1 ):
47- url = f'https://api.github.com/repos/{ owner } /{ repo } /pulls?state={ pr_state } { merged_token } &page={ page } &per_page={ per_page } '
48- print (f'Fetch PRs accrodring to { url } ' )
47+ url = f'https://api.github.com/repos/{ owner } /{ repo } /pulls?state={ pr_state } &page={ page } &per_page={ per_page } '
4948
5049 # Send GET request to GitHub API
5150 response = requests .get (url )
@@ -57,13 +56,13 @@ def get_prs(owner, repo, prod_label, pr_state, include_unmerged, per_page=50, st
5756 # PRs to return because we filter on a specific label
5857 for pr in prs :
5958 labels = pr ['labels' ]
60- accept = False
59+ take_pr = False
6160 for label in labels :
62- if label ['name' ] == prod_label :
61+ if label ['name' ] in request_labels :
6362 # only with the correct the label will be accepted
64- accept = True
63+ take_pr = True
6564 break
66- if not accept :
65+ if not take_pr :
6766 continue
6867 # we will end up here if accepted, so append
6968 prs_return .append (pr )
@@ -80,62 +79,125 @@ def get_prs(owner, repo, prod_label, pr_state, include_unmerged, per_page=50, st
8079 return organise_prs (prs_return )
8180
8281
83- def make_report (prs_merged , prs_other , outfile ):
82+ def get_labels (owner , repo , regex = None ):
83+ """
84+ Get the labels that match given regex
85+ """
86+ # the REST API url
87+ url = f'https://api.github.com/repos/{ owner } /{ repo } /labels'
88+ # Send GET request to GitHub API
89+ response = requests .get (url )
90+
91+ if response .status_code != 200 :
92+ print (f'ERROR: Problem to retrieve labels for owner { owner } and repository { repo } ' )
93+ return None
94+
95+ return [label ['name' ] for label in response .json () if not repo or re .match (regex , label ['name' ])]
96+
97+
98+ def separate_labels_request_accept (labels , accept_suffix = None ):
99+ """
100+ Disentangle labels and <label>-accepted
101+ """
102+ if not accept_suffix :
103+ # all labels are considered request labels
104+ return labels .copy (), []
105+
106+ labels_request = []
107+ labels_accept = []
108+
109+ for label in labels :
110+ if label .endswith (accept_suffix ):
111+ labels_accept .append (label )
112+ continue
113+ labels_request .append (label )
114+
115+ return labels_request , labels_accept
116+
117+
118+ def make_report (prs_merged , prs_other , repo , labels_request , label_accept_suffix , outfile ):
84119 """
85120 Make a report
86121
87- simply dump into text file
122+ The report consists of one table per label which will be written to a text file.
88123 """
124+ # common header for each single table
125+ common_header = '| Requestor | Package | PR | PR title | Merged at | Data or MC |\n | --- | --- | --- | --- | --- | --- |\n '
126+ rows_per_label = {label : [] for label in labels_request }
89127
90128 with open (outfile , 'w' ) as f :
91- f .write ('# FROM OLDEST TO RECENT\n ' )
92- # our common header
93- f .write ('| Date of next tag | Requestor | Package | PR | Data or MC | Comment | JIRA (if it exists) | Accepted | In production | Validated by requestor |\n ' )
94- f .write ('| ---------------- | ------------ | ------- | --------------------------------------------------------:|:--------------------------------------------- | ------------------- | ---------------- | ------------- |-------------| ------------------|\n ' )
129+
130+ f .write (f'Merged PRs: { len (prs_merged )} \n Other closed PRs: { len (prs_other )} \n Labels: { ", " .join (labels_request )} \n \n ' )
131+ f .write ('# List PRs from oldest to recent (merged)\n ' )
95132
96133 # first put the merged PRs
97134 for pr in prs_merged :
98135 mc_data = []
136+ # collect the labels for which table this PR should be taken into account
137+ labels_take = []
99138
100139 for label in pr ['labels' ]:
101- if label ['name' ] in ('MC' , 'DATA' ):
140+ label_name = label ['name' ]
141+ if label_name .lower () in ('mc' , 'data' ):
102142 # get assigned MC or DATA label if this PR has it
103143 mc_data .append (label ['name' ])
144+ if label_name in labels_request and (not label_accept_suffix or f'{ label_name } -{ label_accept_suffix } ' not in pr ['labels' ]):
145+ # check if that label is one that flags a request. If at the same time there is also the corresponding accepted label, don't take this PR into account for the report.
146+ labels_take .append (label_name )
147+
148+ if not labels_take :
149+ # no labels of interest
150+ continue
104151
105152 # if no specific MC or DATA label, assume valid for both
106153 mc_data = ',' .join (mc_data ) if mc_data else 'MC,DATA'
107- # add the full line to the output file
108- f .write (f'| { args .date } | { pr ["user" ]["login" ]} | { args .repo } | [PR]({ pr ["html_url" ]} ) | { mc_data } | { pr ["title" ]} | | | | |\n ' )
154+ for label in labels_take :
155+ rows_per_label [label ].append (f'| { pr ["user" ]["login" ]} | { repo } | [PR]({ pr ["html_url" ]} ) | { pr ["title" ]} | { pr ["merged_at" ]} | { mc_data } |\n ' )
156+
157+ for label , rows in rows_per_label .items ():
158+ if not rows :
159+ # nothing to add here
160+ continue
161+ f .write (f'\n ==> START label { label } <==\n ' )
162+ f .write (common_header )
163+ for row in rows :
164+ f .write (row )
165+ f .write (f'==> END label { label } <==\n ' )
109166
110167 # add all the other commits
111- f .write ('OTHER PRs\n ' )
168+ f .write ('\n # Other PRs (not merged) \n ' )
112169 for pr in prs_other :
113- f .write (f'| { args .date } | { pr ["user" ]["login" ]} | { args .repo } | [PR]({ pr ["html_url" ]} ) | | { pr ["title" ]} | | | | |\n ' )
170+ f .write (f'| { pr ["user" ]["login" ]} | { repo } | [PR]({ pr ["html_url" ]} ) | { pr ["title" ]} | not merged | { ", " .join (labels_take )} | { mc_data } |\n ' )
171+
172+ print (f"==> Report written to { outfile } " )
114173
115174
116175if __name__ == '__main__' :
117176 # Parse command-line arguments
118177 parser = argparse .ArgumentParser (description = 'Retrieve closed pull requests with a specific label from a GitHub repository' )
119178 parser .add_argument ('--owner' , help = 'GitHub repository owner' , default = 'AliceO2Group' )
120179 parser .add_argument ('--repo' , required = True , help = 'GitHub repository name, e.g. O2DPG or AliceO2' )
121- parser .add_argument ('--prod-label' , dest = 'prod_label' , required = True , help = 'Production label to filter PRs' )
122180 parser .add_argument ('--pr-state' , dest = 'pr_state' , default = 'closed' , help = 'The state of the PR' )
123- parser .add_argument ('--include-unmerged' , dest = 'include_unmerged' , action = 'store_true' , help = 'To fetch also unmerged PRs' )
124181 parser .add_argument ('--output' , default = 'o2dpg_pr_report.txt' )
125- parser .add_argument ('--date' , help = 'The date tag to be put' , required = True )
126182 parser .add_argument ('--per-page' , dest = 'per_page' , default = 50 , help = 'How many results per page' )
127183 parser .add_argument ('--start-page' , dest = 'start_page' , type = int , default = 1 , help = 'Start on this page' )
128184 parser .add_argument ('--pages' , type = int , default = 1 , help = 'Number of pages' )
129-
130-
185+ parser .add_argument ('--label-regex' , dest = 'label_regex' , help = 'Provide a regular expression to decide which labels to fetch.' , default = '^async-\w+' )
186+ parser .add_argument ('--label-accepted-suffix' , dest = 'label_accepted_suffix' , help = 'Provide a regular expression to decide which labels to fetch.' , default = 'accept' )
187+ parser .add_argument ('--include-accepted' , action = 'store_true' , help = 'By default, only PRs are fetched where at least one label has no "<label>-accepted" label' )
131188 args = parser .parse_args ()
132189
133- # Retrieve closed pull requests with the specified label
134- prs_merged , prs_other = get_prs (args .owner , args .repo , args .prod_label , args .pr_state , args .include_unmerged , args .per_page , args .start_page , args .pages )
190+ # get all labels of interest
191+ labels = get_labels (args .owner , args .repo , args .label_regex )
192+ # split into request and accept labels, here we currently only need the request labels
193+ labels_request , _ = separate_labels_request_accept (labels , args .label_accepted_suffix )
194+
195+ # Retrieve closed pull requests with the specified label, split into merged and other (closed) PRs
196+ prs_merged , prs_other = get_prs (args .owner , args .repo , labels_request , args .pr_state , args .per_page , args .start_page , args .pages )
135197 if prs_merged is None :
136198 print ('ERROR: There was a problem fetching the info.' )
137199 sys .exit (1 )
138200
139- make_report (prs_merged , prs_other , args .output )
201+ make_report (prs_merged , prs_other , args .repo , labels_request , args . label_accepted_suffix , args . output )
140202
141203 sys .exit (0 )
0 commit comments