@@ -162,6 +162,96 @@ def detect_repo_type(url: str) -> str | None:
162162 return None
163163
164164
165+ def convert_gitlab_url_to_api (url : str ) -> str :
166+ """Convert GitLab web UI URL to API URL for authentication.
167+
168+ GitLab web UI URLs don't accept API tokens via headers.
169+ We need to use the API endpoint for private repo access.
170+
171+ Converts:
172+ - From: https://gitlab.com/namespace/project/-/raw/branch/path/to/file
173+ - To: https://gitlab.com/api/v4/projects/namespace%2Fproject/repository/files/path%2Fto%2Ffile/raw?ref=branch
174+
175+ Args:
176+ url: GitLab web UI raw URL
177+
178+ Returns:
179+ GitLab API URL that accepts PRIVATE-TOKEN header
180+ """
181+ # Check if it's already an API URL
182+ if '/api/v4/projects/' in url :
183+ return url
184+
185+ # Check if it's a GitLab web UI raw URL
186+ if '/-/raw/' not in url :
187+ return url # Not a GitLab raw URL, return as-is
188+
189+ # Parse the URL to extract components
190+ # Format: https://gitlab.com/namespace/project/-/raw/branch/path/to/file?query
191+ try :
192+ # Split off query parameters first
193+ base_url , _ , query = url .partition ('?' )
194+
195+ # Extract the domain and path
196+ if base_url .startswith ('https://' ):
197+ domain_end = base_url .index ('/' , 8 ) # Find end of domain after https://
198+ domain = base_url [:domain_end ]
199+ path = base_url [domain_end + 1 :] # Skip the /
200+ elif base_url .startswith ('http://' ):
201+ domain_end = base_url .index ('/' , 7 ) # Find end of domain after http://
202+ domain = base_url [:domain_end ]
203+ path = base_url [domain_end + 1 :] # Skip the /
204+ else :
205+ return url # Unknown format
206+
207+ # Split the path by /-/raw/
208+ parts = path .split ('/-/raw/' )
209+ if len (parts ) != 2 :
210+ return url # Unexpected format
211+
212+ project_path = parts [0 ] # e.g., "ai/claude-code-configs"
213+ remainder = parts [1 ] # e.g., "main/environments/examples/file.yaml"
214+
215+ # Split remainder into branch and file path
216+ # The branch is the first part before /
217+ branch_end = remainder .find ('/' )
218+ if branch_end == - 1 :
219+ # No file path, just branch
220+ branch = remainder
221+ file_path = ''
222+ else :
223+ branch = remainder [:branch_end ]
224+ file_path = remainder [branch_end + 1 :]
225+
226+ # URL-encode the project path for API (namespace/project -> namespace%2Fproject)
227+ encoded_project = urllib .parse .quote (project_path , safe = '' )
228+
229+ # URL-encode the file path for API
230+ encoded_file = urllib .parse .quote (file_path , safe = '' )
231+
232+ # Extract ref parameter from query if present (it overrides branch)
233+ ref = branch
234+ if query :
235+ # Parse query parameters
236+ params = urllib .parse .parse_qs (query )
237+ # Check for ref or ref_type parameters
238+ if 'ref' in params :
239+ ref = params ['ref' ][0 ]
240+ elif 'ref_type' in params and branch :
241+ # ref_type is just metadata, use the branch from path
242+ ref = branch
243+
244+ # Build the API URL
245+ api_url = f'{ domain } /api/v4/projects/{ encoded_project } /repository/files/{ encoded_file } /raw?ref={ ref } '
246+
247+ info ('Converted GitLab URL to API format for authentication' )
248+ return api_url
249+
250+ except (ValueError , IndexError ) as e :
251+ warning (f'Could not convert GitLab URL to API format: { e } ' )
252+ return url # Return original if conversion fails
253+
254+
165255def get_auth_headers (url : str , auth_param : str | None = None ) -> dict [str , str ]:
166256 """Get authentication headers using multiple fallback methods.
167257
@@ -524,6 +614,13 @@ def fetch_url_with_auth(url: str, auth_headers: dict[str, str] | None = None, au
524614 HTTPError: If the HTTP request fails after authentication attempts
525615 URLError: If there's a URL/network error (including SSL issues)
526616 """
617+ # Convert GitLab web URLs to API URLs for authentication
618+ original_url = url
619+ if detect_repo_type (url ) == 'gitlab' and '/-/raw/' in url :
620+ url = convert_gitlab_url_to_api (url )
621+ if url != original_url :
622+ info (f'Using API URL: { url } ' )
623+
527624 # First try without auth (for public repos)
528625 try :
529626 request = Request (url )
0 commit comments