77import os
88import re
99import sys
10+ from contextlib import contextmanager , suppress
1011from 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
1314
1415import git
1516
@@ -217,13 +218,6 @@ async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str |
217218
218219 # Use GitPython to get remote references
219220 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-
227221 fetch_tags = ref_type == "tags"
228222 to_fetch = "tags" if fetch_tags else "heads"
229223
@@ -233,8 +227,9 @@ async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str |
233227 cmd_args .append ("--refs" ) # Filter out peeled tag objects
234228 cmd_args .append (url )
235229
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 )
238233
239234 # Parse output
240235 return [
@@ -318,6 +313,60 @@ def create_git_auth_header(token: str, url: str = "https://github.com") -> str:
318313 return f"http.https://{ hostname } /.extraheader=Authorization: Basic { basic } "
319314
320315
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+
321370def validate_github_token (token : str ) -> None :
322371 """Validate the format of a GitHub Personal Access Token.
323372
@@ -419,15 +468,9 @@ async def _resolve_ref_to_sha(url: str, pattern: str, token: str | None = None)
419468
420469 """
421470 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 )
431474 lines = output .splitlines ()
432475
433476 sha = _pick_commit_sha (lines )
@@ -475,37 +518,3 @@ def _pick_commit_sha(lines: Iterable[str]) -> str | None:
475518 first_non_peeled = sha
476519
477520 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