7
7
import os
8
8
import re
9
9
import sys
10
+ from contextlib import contextmanager , suppress
10
11
from pathlib import Path
11
- from typing import TYPE_CHECKING , Final , Iterable
12
- from urllib .parse import urlparse , urlunparse
12
+ from typing import TYPE_CHECKING , Final , Generator , Iterable
13
+ from urllib .parse import urlparse
13
14
14
15
import git
15
16
@@ -217,13 +218,6 @@ async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str |
217
218
218
219
# Use GitPython to get remote references
219
220
try :
220
- git_cmd = git .Git ()
221
-
222
- # Prepare authentication if needed
223
- if token and is_github_host (url ):
224
- auth_url = _add_token_to_url (url , token )
225
- url = auth_url
226
-
227
221
fetch_tags = ref_type == "tags"
228
222
to_fetch = "tags" if fetch_tags else "heads"
229
223
@@ -233,8 +227,9 @@ async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str |
233
227
cmd_args .append ("--refs" ) # Filter out peeled tag objects
234
228
cmd_args .append (url )
235
229
236
- # Run the command using git_cmd.ls_remote() method
237
- output = git_cmd .ls_remote (* cmd_args )
230
+ # Run the command with proper authentication
231
+ with git_auth_context (url , token ) as git_cmd :
232
+ output = git_cmd .ls_remote (* cmd_args )
238
233
239
234
# Parse output
240
235
return [
@@ -318,6 +313,60 @@ def create_git_auth_header(token: str, url: str = "https://github.com") -> str:
318
313
return f"http.https://{ hostname } /.extraheader=Authorization: Basic { basic } "
319
314
320
315
316
+ @contextmanager
317
+ def git_auth_context (url : str , token : str | None = None ) -> Generator [git .Git ]:
318
+ """Context manager for GitPython authentication.
319
+
320
+ Creates a Git command object configured with authentication for GitHub repositories.
321
+ Uses git's credential system instead of URL manipulation.
322
+
323
+ Parameters
324
+ ----------
325
+ url : str
326
+ The repository URL to check if authentication is needed.
327
+ token : str | None
328
+ GitHub personal access token (PAT) for accessing private repositories.
329
+
330
+ Yields
331
+ ------
332
+ Generator[git.Git]
333
+ Git command object configured with authentication.
334
+
335
+ """
336
+ git_cmd = git .Git ()
337
+
338
+ if token and is_github_host (url ):
339
+ # Configure git to use the token for this hostname
340
+ # This is equivalent to: git config credential.https://hostname.username x-oauth-basic
341
+ # and: git config credential.https://hostname.helper store
342
+ auth_header = create_git_auth_header (token , url )
343
+ key , value = auth_header .split ("=" , 1 )
344
+
345
+ # Set the auth configuration for this git command
346
+ original_config = {}
347
+ try :
348
+ # Store original config if it exists
349
+ with suppress (git .GitCommandError ):
350
+ original_config [key ] = git_cmd .config ("--get" , key )
351
+
352
+ # Set the authentication
353
+ git_cmd .config (key , value )
354
+
355
+ yield git_cmd
356
+
357
+ finally :
358
+ # Restore original config
359
+ try :
360
+ if key in original_config :
361
+ git_cmd .config (key , original_config [key ])
362
+ else :
363
+ git_cmd .config ("--unset" , key )
364
+ except git .GitCommandError :
365
+ pass # Config cleanup failed, not critical
366
+ else :
367
+ yield git_cmd
368
+
369
+
321
370
def validate_github_token (token : str ) -> None :
322
371
"""Validate the format of a GitHub Personal Access Token.
323
372
@@ -419,15 +468,9 @@ async def _resolve_ref_to_sha(url: str, pattern: str, token: str | None = None)
419
468
420
469
"""
421
470
try :
422
- git_cmd = git .Git ()
423
-
424
- # Prepare authentication if needed
425
- auth_url = url
426
- if token and is_github_host (url ):
427
- auth_url = _add_token_to_url (url , token )
428
-
429
- # Execute ls-remote command
430
- output = git_cmd .ls_remote (auth_url , pattern )
471
+ # Execute ls-remote command with proper authentication
472
+ with git_auth_context (url , token ) as git_cmd :
473
+ output = git_cmd .ls_remote (url , pattern )
431
474
lines = output .splitlines ()
432
475
433
476
sha = _pick_commit_sha (lines )
@@ -475,37 +518,3 @@ def _pick_commit_sha(lines: Iterable[str]) -> str | None:
475
518
first_non_peeled = sha
476
519
477
520
return first_non_peeled # branch or lightweight tag (or None)
478
-
479
-
480
- def _add_token_to_url (url : str , token : str ) -> str :
481
- """Add authentication token to GitHub URL.
482
-
483
- Parameters
484
- ----------
485
- url : str
486
- The original GitHub URL.
487
- token : str
488
- The GitHub token to add.
489
-
490
- Returns
491
- -------
492
- str
493
- The URL with embedded authentication.
494
-
495
- """
496
- parsed = urlparse (url )
497
- # Add token as username in URL (GitHub supports this)
498
- netloc = f"x-oauth-basic:{ token } @{ parsed .hostname } "
499
- if parsed .port :
500
- netloc += f":{ parsed .port } "
501
-
502
- return urlunparse (
503
- (
504
- parsed .scheme ,
505
- netloc ,
506
- parsed .path ,
507
- parsed .params ,
508
- parsed .query ,
509
- parsed .fragment ,
510
- ),
511
- )
0 commit comments