99import io
1010import json
1111import math
12+ import os
1213import re
1314import shutil
1415import tempfile
1516from pathlib import Path
1617from typing import Callable , Dict , Iterable , Iterator , List , Optional , Sequence , Tuple
17- from urllib .parse import urlparse
18+ from urllib .parse import urlparse , urlunparse
1819from urllib .request import Request , urlopen
1920from zipfile import ZIP_DEFLATED , ZipFile
2021import subprocess
@@ -89,6 +90,47 @@ class AndroidSkinSource:
8990)
9091
9192
93+ def _github_token () -> Optional [str ]:
94+ token = os .environ .get ("GITHUB_TOKEN" ) or os .environ .get ("GH_TOKEN" )
95+ if token :
96+ return token .strip ()
97+ return None
98+
99+
100+ def _github_headers (url : Optional [str ] = None ) -> Dict [str , str ]:
101+ headers : Dict [str , str ] = {"User-Agent" : "codenameone-skin-generator/1.0" }
102+ token = _github_token ()
103+ if not token :
104+ return headers
105+ if url is None :
106+ headers ["Authorization" ] = f"Bearer { token } "
107+ return headers
108+ host = urlparse (url ).netloc .lower ()
109+ if "github.com" in host or "githubusercontent.com" in host or host .startswith ("api.github" ):
110+ headers ["Authorization" ] = f"Bearer { token } "
111+ return headers
112+
113+
114+ def _authenticated_git_url (url : str ) -> str :
115+ token = _github_token ()
116+ if not token :
117+ return url
118+ parsed = urlparse (url )
119+ host = parsed .netloc .lower ()
120+ if "github.com" not in host :
121+ return url
122+ safe_netloc = f"{ token } :x-oauth-basic@{ parsed .netloc } "
123+ return urlunparse (parsed ._replace (netloc = safe_netloc ))
124+
125+
126+ def _sanitize_url (url : str ) -> str :
127+ token = _github_token ()
128+ if not token :
129+ return url
130+ sanitized = url .replace (token , "***" )
131+ return sanitized .replace (f"{ token } :x-oauth-basic" , "***:x-oauth-basic" )
132+
133+
92134@dataclasses .dataclass (frozen = True )
93135class SkinGeneration :
94136 """Description of a generated skin archive."""
@@ -601,12 +643,11 @@ def _github_repo_from_url(url: str) -> Optional[Tuple[str, str]]:
601643def _branch_candidates (base_url : str ) -> List [str ]:
602644 owner_repo = _github_repo_from_url (base_url )
603645 candidates : List [str ] = []
604- headers = {"User-Agent" : "codenameone-skin-generator/1.0" }
605646 if owner_repo :
606647 owner , repo = owner_repo
607648 api_url = f"https://api.github.com/repos/{ owner } /{ repo } "
608649 try :
609- request = Request (api_url , headers = headers )
650+ request = Request (api_url , headers = _github_headers ( api_url ) )
610651 with urlopen (request ) as response : # type: ignore[arg-type]
611652 payload = json .load (response )
612653 default_branch = payload .get ("default_branch" )
@@ -628,7 +669,6 @@ def _download_android_skin_repo(source: AndroidSkinSource) -> Tuple[Path, Path]:
628669
629670 tmp_root = Path (tempfile .mkdtemp (prefix = "android-skins-" ))
630671 archive_path = tmp_root / "repo.zip"
631- headers = {"User-Agent" : "codenameone-skin-generator/1.0" }
632672 errors : List [str ] = []
633673
634674 base_urls : Tuple [str , ...] = (source .url , * source .alternate_urls )
@@ -646,7 +686,7 @@ def _download_android_skin_repo(source: AndroidSkinSource) -> Tuple[Path, Path]:
646686
647687 for candidate in archive_candidates :
648688 try :
649- request = Request (candidate , headers = headers )
689+ request = Request (candidate , headers = _github_headers ( candidate ) )
650690 with urlopen (request ) as response , archive_path .open ("wb" ) as fh : # type: ignore[arg-type]
651691 shutil .copyfileobj (response , fh )
652692 with ZipFile (archive_path ) as zf :
@@ -677,14 +717,17 @@ def _download_android_skin_repo(source: AndroidSkinSource) -> Tuple[Path, Path]:
677717 repo_root = tmp_root
678718 return repo_root , tmp_root
679719 except Exception as exc : # pylint: disable=broad-except
680- errors .append (f"{ candidate } : { exc } " )
720+ errors .append (f"{ _sanitize_url ( candidate ) } : { exc } " )
681721
682722 # Zip downloads failed, try shallow git clones as fallbacks
683723 for attempt_index , base_url in enumerate (base_urls ):
684724 branch_candidates = _branch_candidates (base_url )
685725 for branch in branch_candidates :
686726 clone_dir = tmp_root / f"repo-{ attempt_index } -{ branch } "
687727 try :
728+ clone_url = _authenticated_git_url (base_url )
729+ env = os .environ .copy ()
730+ env .setdefault ("GIT_TERMINAL_PROMPT" , "0" )
688731 subprocess .run (
689732 [
690733 "git" ,
@@ -695,18 +738,19 @@ def _download_android_skin_repo(source: AndroidSkinSource) -> Tuple[Path, Path]:
695738 "--filter=blob:none" ,
696739 "--branch" ,
697740 branch ,
698- base_url ,
741+ clone_url ,
699742 str (clone_dir ),
700743 ],
701744 check = True ,
702745 capture_output = True ,
703746 text = True ,
747+ env = env ,
704748 )
705749 except subprocess .CalledProcessError as exc : # pragma: no cover - network dependent
706750 stderr = exc .stderr .strip ()
707751 stdout = exc .stdout .strip ()
708752 details = stderr or stdout or str (exc )
709- errors .append (f"git clone ({ base_url } @{ branch } ): { details } " )
753+ errors .append (f"git clone ({ _sanitize_url ( base_url ) } @{ branch } ): { details } " )
710754 continue
711755 except FileNotFoundError as exc : # pragma: no cover - git missing
712756 errors .append (f"git clone unavailable: { exc } " )
0 commit comments