28
28
from requests .packages .urllib3 .util .retry import Retry
29
29
from absl import logging
30
30
31
- # Constants from firebase_github.py
31
+ # Constants for GitHub API interaction
32
32
RETRIES = 3
33
33
BACKOFF = 5
34
- RETRY_STATUS = (403 , 500 , 502 , 504 )
35
- TIMEOUT = 5
36
- TIMEOUT_LONG = 20 # Not used in the functions we are copying, but good to have if expanding.
37
-
38
- OWNER = '' # Will be determined dynamically or from args
39
- REPO = '' # Will be determined dynamically or from args
40
- BASE_URL = 'https://api.github.com'
41
- GITHUB_API_URL = '' # Will be set by set_repo_url_standalone
34
+ RETRY_STATUS = (403 , 500 , 502 , 504 ) # HTTP status codes to retry on
35
+ TIMEOUT = 5 # Default timeout for requests in seconds
36
+ TIMEOUT_LONG = 20 # Longer timeout, currently not used by functions in this script
37
+
38
+ # Global variables for the target repository.
39
+ # These are populated by set_repo_url_standalone() after determining
40
+ # the owner and repo from arguments or git configuration.
41
+ OWNER = ''
42
+ REPO = ''
43
+ BASE_URL = 'https://api.github.com' # Base URL for GitHub API
44
+ GITHUB_API_URL = '' # Dynamically constructed API URL for the specific repository
42
45
43
46
logging .set_verbosity (logging .INFO )
44
47
@@ -72,40 +75,37 @@ def get_pull_request_review_comments(token, pull_number, since=None):
72
75
headers = {'Accept' : 'application/vnd.github.v3+json' , 'Authorization' : f'token { token } ' }
73
76
74
77
page = 1
75
- per_page = 100
78
+ per_page = 100 # GitHub API default and max is 100 for many paginated endpoints
76
79
results = []
77
80
78
- # Base parameters for the API request
79
81
base_params = {'per_page' : per_page }
80
82
if since :
81
- base_params ['since' ] = since
83
+ base_params ['since' ] = since # Filter comments by timestamp
82
84
83
- while True : # Loop indefinitely until explicitly broken
85
+ while True :
84
86
current_page_params = base_params .copy ()
85
87
current_page_params ['page' ] = page
86
88
87
89
try :
88
90
with requests_retry_session ().get (url , headers = headers , params = current_page_params ,
89
91
stream = True , timeout = TIMEOUT ) as response :
90
92
response .raise_for_status ()
91
- # Log which page and if 'since' was used for clarity
92
93
logging .info ("get_pull_request_review_comments: %s params %s response: %s" , url , current_page_params , response )
93
94
94
95
current_page_results = response .json ()
95
- if not current_page_results : # No more results on this page
96
- break # Exit loop, no more comments to fetch
96
+ if not current_page_results : # No more data on this page
97
+ break
97
98
98
99
results .extend (current_page_results )
99
100
100
- # If fewer results than per_page were returned, it's the last page
101
- if len (current_page_results ) < per_page :
102
- break # Exit loop, this was the last page
101
+ if len (current_page_results ) < per_page : # Last page
102
+ break
103
103
104
- page += 1 # Increment page for the next iteration
104
+ page += 1
105
105
106
106
except requests .exceptions .RequestException as e :
107
107
logging .error (f"Error fetching review comments (page { page } , params: { current_page_params } ) for PR { pull_number } : { e } " )
108
- break # Stop trying if there's an error
108
+ break
109
109
return results
110
110
111
111
@@ -124,22 +124,19 @@ def list_pull_requests(token, state, head, base):
124
124
if base : params .update ({'base' : base })
125
125
page = page + 1
126
126
keep_going = False
127
- # Ensure GITHUB_API_URL is set before this function is called if OWNER/REPO are not passed explicitly
128
- # For standalone script, OWNER and REPO are global and GITHUB_API_URL is set by set_repo_url_standalone
129
127
try :
130
128
with requests_retry_session ().get (url , headers = headers , params = params ,
131
129
stream = True , timeout = TIMEOUT ) as response :
132
130
logging .info ("list_pull_requests: %s params: %s response: %s" , url , params , response )
133
- response .raise_for_status () # Raise an exception for bad status codes
131
+ response .raise_for_status ()
134
132
current_page_results = response .json ()
135
- if not current_page_results : # No more results on this page
133
+ if not current_page_results :
136
134
break
137
135
results .extend (current_page_results )
138
- # If exactly per_page results were retrieved, read the next page.
139
136
keep_going = (len (current_page_results ) == per_page )
140
137
except requests .exceptions .RequestException as e :
141
138
logging .error (f"Error listing pull requests (page { params .get ('page' , 'N/A' )} , params: { params } ) for { OWNER } /{ REPO } : { e } " )
142
- break # Stop trying if there's an error
139
+ break
143
140
return results
144
141
145
142
@@ -158,21 +155,20 @@ def get_latest_pr_for_branch(token, owner, repo, branch_name):
158
155
sys .stderr .write ("Owner and repo must be set to find PR for branch.\n " )
159
156
return None
160
157
161
- head_branch_spec = f"{ owner } :{ branch_name } " # GitHub API requires owner in head spec for forks
162
- prs = list_pull_requests (token = token , state = "open" , head = head_branch_spec , base = None ) # base can be None
158
+ head_branch_spec = f"{ owner } :{ branch_name } " # Format required by GitHub API for head branch
159
+ prs = list_pull_requests (token = token , state = "open" , head = head_branch_spec , base = None )
163
160
164
161
if not prs :
165
162
return None
166
163
167
- # Sort PRs by creation date, most recent first
168
- # PRs are dictionaries, 'created_at' is an ISO 8601 string
164
+ # Sort PRs by creation date (most recent first) to find the latest.
169
165
try :
170
166
prs .sort (key = lambda pr : pr .get ("created_at" , "" ), reverse = True )
171
- except Exception as e :
167
+ except Exception as e : # Broad exception for safety, though sort issues are rare with valid data.
172
168
sys .stderr .write (f"Could not sort PRs by creation date: { e } \n " )
173
- return None # Or handle more gracefully
169
+ return None
174
170
175
- if prs : # Check if list is not empty after sort
171
+ if prs :
176
172
return prs [0 ].get ("number" )
177
173
return None
178
174
@@ -187,21 +183,19 @@ def main():
187
183
try :
188
184
git_url_bytes = subprocess .check_output (["git" , "remote" , "get-url" , "origin" ], stderr = subprocess .PIPE )
189
185
git_url = git_url_bytes .decode ().strip ()
190
- # Regex for https://github.com/owner/repo.git or [email protected] :owner/repo.git
191
186
match = re .search (r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+)(?:\.git)?" , git_url )
192
187
if match :
193
188
determined_owner = match .group (1 )
194
189
determined_repo = match .group (2 )
195
190
sys .stderr .write (f"Determined repository: { determined_owner } /{ determined_repo } from git remote.\n " )
196
191
except (subprocess .CalledProcessError , FileNotFoundError , UnicodeDecodeError ) as e :
197
192
sys .stderr .write (f"Could not automatically determine repository from git remote: { e } \n " )
198
- except Exception as e : # Catch any other unexpected error during git processing
193
+ except Exception as e : # Catch any other unexpected error during git processing, though less likely.
199
194
sys .stderr .write (f"An unexpected error occurred while determining repository: { e } \n " )
200
195
201
- # Helper function to parse owner/repo from URL
202
196
def parse_repo_url (url_string ):
203
- # Regex for https://github.com/owner/repo.git or [email protected] :owner/repo.git
204
- # Also handles URLs without .git suffix
197
+ """Parses owner and repository name from various GitHub URL formats."""
198
+ # Handles https://github.com/owner/repo.git, [email protected] :owner/repo.git, and URLs without .git suffix.
205
199
url_match = re .search (r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+?)(?:\.git)?/?$" , url_string )
206
200
if url_match :
207
201
return url_match .group (1 ), url_match .group (2 )
@@ -212,11 +206,10 @@ def parse_repo_url(url_string):
212
206
"Repository can be specified via --url, or --owner AND --repo, or auto-detected from git remote 'origin'." ,
213
207
formatter_class = argparse .RawTextHelpFormatter
214
208
)
215
- # Arguments for repository specification
216
209
parser .add_argument (
217
210
"--pull_number" ,
218
211
type = int ,
219
- default = None , # Now optional
212
+ default = None ,
220
213
help = "Pull request number. If not provided, script attempts to find the latest open PR for the current git branch."
221
214
)
222
215
parser .add_argument (
@@ -228,13 +221,13 @@ def parse_repo_url(url_string):
228
221
parser .add_argument (
229
222
"--owner" ,
230
223
type = str ,
231
- default = determined_owner , # Default to auto-detected
224
+ default = determined_owner ,
232
225
help = f"Repository owner. Used if --url is not provided. { 'Default: ' + determined_owner if determined_owner else 'Required if --url is not used and not determinable from git.' } "
233
226
)
234
227
parser .add_argument (
235
228
"--repo" ,
236
229
type = str ,
237
- default = determined_repo , # Default to auto-detected
230
+ default = determined_repo ,
238
231
help = f"Repository name. Used if --url is not provided. { 'Default: ' + determined_repo if determined_repo else 'Required if --url is not used and not determinable from git.' } "
239
232
)
240
233
parser .add_argument (
@@ -278,14 +271,8 @@ def parse_repo_url(url_string):
278
271
final_owner = None
279
272
final_repo = None
280
273
281
- # Logic for determining owner and repo:
282
- # 1. If --url is provided, use it. Error if --owner or --repo are also explicitly set.
283
- # 2. Else if --owner and --repo are provided, use them.
284
- # 3. Else (neither --url nor --owner/--repo pair explicitly set), use auto-detected values (which are defaults for args.owner/args.repo).
285
-
274
+ # Determine repository owner and name
286
275
if args .url :
287
- # URL is provided, it takes precedence.
288
- # Error if --owner or --repo were also explicitly set by the user (i.e., different from their defaults)
289
276
owner_explicitly_set = args .owner is not None and args .owner != determined_owner
290
277
repo_explicitly_set = args .repo is not None and args .repo != determined_repo
291
278
if owner_explicitly_set or repo_explicitly_set :
@@ -301,44 +288,38 @@ def parse_repo_url(url_string):
301
288
sys .
stderr .
write (
f"Error: Invalid URL format: { args .url } . Expected https://github.com/owner/repo or [email protected] :owner/repo.git{ error_suffix } \n " )
302
289
sys .exit (1 )
303
290
else :
304
- # URL is not provided, try to use owner/repo.
305
- # These could be from explicit user input or from auto-detection (via defaults).
306
291
is_owner_from_user = args .owner is not None and args .owner != determined_owner
307
292
is_repo_from_user = args .repo is not None and args .repo != determined_repo
308
- is_owner_from_default = args .owner is not None and args .owner == determined_owner and determined_owner is not None
309
- is_repo_from_default = args .repo is not None and args .repo == determined_repo and determined_repo is not None
310
293
311
- if (is_owner_from_user or is_repo_from_user ): # User explicitly set at least one
312
- if args .owner and args .repo : # Both were set (either explicitly or one explicit, one default that existed)
294
+ if (is_owner_from_user or is_repo_from_user ): # User explicitly set at least one of owner/repo
295
+ if args .owner and args .repo :
313
296
final_owner = args .owner
314
297
final_repo = args .repo
315
298
sys .stderr .write (f"Using repository from --owner/--repo args: { final_owner } /{ final_repo } \n " )
316
- else : # User set one but not the other, and the other wasn't available from a default
299
+ else : # User set one but not the other ( and the other wasn't available from a default)
317
300
sys .stderr .write (f"Error: Both --owner and --repo must be specified if one is provided explicitly (and --url is not used).{ error_suffix } \n " )
318
301
sys .exit (1 )
319
- elif args .owner and args .repo : # Both are from successful auto-detection (already printed "Determined repository...")
302
+ elif args .owner and args .repo : # Both args have values, from successful auto-detection
320
303
final_owner = args .owner
321
304
final_repo = args .repo
322
- elif args .owner or args .repo : # Only one was available (e.g. auto-detect only found one, or bad default state )
305
+ elif args .owner or args .repo : # Only one has a value (e.g. auto-detect only found one)
323
306
sys .stderr .write (f"Error: Both --owner and --repo are required if not using --url, and auto-detection was incomplete.{ error_suffix } \n " )
324
307
sys .exit (1 )
325
- # If none of the above, final_owner/repo remain None, will be caught by the check below .
308
+ # If final_owner/repo are still None here, it means auto-detection failed and user provided nothing .
326
309
327
310
if not final_owner or not final_repo :
328
311
sys .stderr .write (f"Error: Could not determine repository. Please specify --url, OR both --owner and --repo, OR ensure git remote 'origin' is configured correctly.{ error_suffix } \n " )
329
312
sys .exit (1 )
330
313
331
-
332
- if not final_owner or not final_repo :
333
- sys .stderr .write (f"Error: Could not determine repository. Please specify --url, OR both --owner and --repo, OR ensure git remote 'origin' is configured correctly.{ error_suffix } \n " )
334
- sys .exit (1 )
335
-
336
- if not set_repo_url_standalone (final_owner , final_repo ): # Sets global OWNER and REPO
314
+ # Set global repository variables for API calls
315
+ if not set_repo_url_standalone (final_owner , final_repo ):
316
+ # This path should ideally not be reached if previous checks are robust.
337
317
sys .stderr .write (f"Error: Could not set repository to { final_owner } /{ final_repo } . Ensure owner/repo are correct.{ error_suffix } \n " )
338
318
sys .exit (1 )
339
319
320
+ # Determine Pull Request number
340
321
pull_request_number = args .pull_number
341
- current_branch_for_pr_check = None # Used to improve final error message
322
+ current_branch_for_pr_check = None
342
323
if not pull_request_number :
343
324
sys .stderr .write ("Pull number not specified, attempting to find PR for current branch...\n " )
344
325
current_branch_for_pr_check = get_current_branch_name ()
@@ -348,18 +329,18 @@ def parse_repo_url(url_string):
348
329
if pull_request_number :
349
330
sys .stderr .write (f"Found PR #{ pull_request_number } for branch { current_branch_for_pr_check } .\n " )
350
331
else :
351
- sys .stderr .write (f"No open PR found for branch { current_branch_for_pr_check } in { OWNER } /{ REPO } .\n " ) # This is informational, error below
332
+ # Informational, actual error handled by `if not pull_request_number:` below
333
+ sys .stderr .write (f"No open PR found for branch { current_branch_for_pr_check } in { OWNER } /{ REPO } .\n " )
352
334
else :
353
335
sys .stderr .write (f"Error: Could not determine current git branch. Cannot find PR automatically.{ error_suffix } \n " )
354
336
sys .exit (1 )
355
337
356
338
if not pull_request_number :
357
339
error_message = "Error: Pull request number is required."
358
- if not args .pull_number and current_branch_for_pr_check : # Auto-detect branch ok, but no PR found
340
+ if not args .pull_number and current_branch_for_pr_check :
359
341
error_message = f"Error: Pull request number not specified and no open PR found for branch { current_branch_for_pr_check } ."
360
- elif not args .pull_number and not current_branch_for_pr_check : # Should have been caught above
342
+ elif not args .pull_number and not current_branch_for_pr_check : # Should have been caught and exited above.
361
343
error_message = "Error: Pull request number not specified and could not determine current git branch."
362
-
363
344
sys .stderr .write (f"{ error_message } { error_suffix } \n " )
364
345
sys .exit (1 )
365
346
@@ -408,10 +389,10 @@ def parse_repo_url(url_string):
408
389
if status_text == STATUS_OLD and args .exclude_old :
409
390
continue
410
391
411
- # Track latest 'updated_at' for '--since' suggestion; 'created_at' is for display.
412
392
updated_at_str = comment .get ("updated_at" )
413
- if updated_at_str : # Check if updated_at_str is not None and not empty
393
+ if updated_at_str :
414
394
try :
395
+ # Compatibility for Python < 3.11 which doesn't handle 'Z' suffix in fromisoformat
415
396
if sys .version_info < (3 , 11 ):
416
397
dt_str_updated = updated_at_str .replace ("Z" , "+00:00" )
417
398
else :
@@ -422,12 +403,11 @@ def parse_repo_url(url_string):
422
403
except ValueError :
423
404
sys .stderr .write (f"Warning: Could not parse updated_at timestamp: { updated_at_str } \n " )
424
405
425
- # Get other comment details
426
406
user = comment .get ("user" , {}).get ("login" , "Unknown user" )
427
407
path = comment .get ("path" , "N/A" )
428
408
body = comment .get ("body" , "" ).strip ()
429
409
430
- if not body :
410
+ if not body : # Skip comments with no actual text content
431
411
continue
432
412
433
413
processed_comments_count += 1
@@ -446,24 +426,19 @@ def parse_repo_url(url_string):
446
426
print (f"* **URL**: <{ html_url } >\n " )
447
427
448
428
print ("\n ### Context:" )
449
- print ("```" ) # Start of Markdown code block
429
+ print ("```" )
450
430
if diff_hunk and diff_hunk .strip ():
451
- if args .context_lines == 0 : # User wants the full hunk
431
+ if args .context_lines == 0 : # 0 means full hunk
452
432
print (diff_hunk )
453
- else : # User wants N lines of context (args.context_lines > 0)
433
+ else : # Display header (if any) and last N lines
454
434
hunk_lines = diff_hunk .split ('\n ' )
455
- if hunk_lines and hunk_lines [0 ].startswith ("@@ " ):
435
+ if hunk_lines and hunk_lines [0 ].startswith ("@@ " ): # Diff hunk header
456
436
print (hunk_lines [0 ])
457
- hunk_lines = hunk_lines [1 :] # Modify list in place for remaining operations
458
-
459
- # Proceed with the (potentially modified) hunk_lines
460
- # If hunk_lines is empty here (e.g. original hunk was only a header that was removed),
461
- # hunk_lines[-args.context_lines:] will be [], and "\n".join([]) is "",
462
- # so print("") will effectively print a newline. This is acceptable.
437
+ hunk_lines = hunk_lines [1 :]
463
438
print ("\n " .join (hunk_lines [- args .context_lines :]))
464
- else : # diff_hunk was None or empty
439
+ else :
465
440
print ("(No diff hunk available for this comment)" )
466
- print ("```" ) # End of Markdown code block
441
+ print ("```" )
467
442
468
443
print ("\n ### Comment:" )
469
444
print (body )
@@ -473,15 +448,15 @@ def parse_repo_url(url_string):
473
448
474
449
if latest_activity_timestamp_obj :
475
450
try :
476
- # Ensure it's UTC before adding timedelta, then format
451
+ # Suggest next command with '--since' pointing to just after the latest comment processed.
477
452
next_since_dt = latest_activity_timestamp_obj .astimezone (timezone .utc ) + timedelta (seconds = 2 )
478
453
next_since_str = next_since_dt .strftime ('%Y-%m-%dT%H:%M:%SZ' )
479
454
480
- new_cmd_args = [sys .executable , sys .argv [0 ]] # Start with interpreter and script path
481
- i = 1 # Start checking from actual arguments in sys.argv
455
+ new_cmd_args = [sys .executable , sys .argv [0 ]]
456
+ i = 1
482
457
while i < len (sys .argv ):
483
- if sys .argv [i ] == "--since" :
484
- i += 2 # Skip --since and its value
458
+ if sys .argv [i ] == "--since" : # Skip existing --since argument and its value
459
+ i += 2
485
460
continue
486
461
new_cmd_args .append (sys .argv [i ])
487
462
i += 1
0 commit comments