Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions pr_agent/git_providers/gitlab_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,22 +954,47 @@ def generate_link_to_relevant_line_number(self, suggestion) -> str:
return ""
#Clone related
def _prepare_clone_url_with_token(self, repo_url_to_clone: str) -> str | None:
if "gitlab." not in repo_url_to_clone:
get_logger().error(f"Repo URL: {repo_url_to_clone} is not a valid gitlab URL.")
return None
(scheme, base_url) = repo_url_to_clone.split("gitlab.")
access_token = getattr(self.gl, 'oauth_token', None) or getattr(self.gl, 'private_token', None)
if not all([scheme, access_token, base_url]):
get_logger().error(f"Either no access token found, or repo URL: {repo_url_to_clone} "
f"is missing prefix: {scheme} and/or base URL: {base_url}.")
return None
if not access_token:
return self._log_missing_clone_token()

#Note that the ""official"" method found here:
# https://docs.gitlab.com/user/profile/personal_access_tokens/#clone-repository-using-personal-access-token
# requires a username, which may not be applicable.
# The following solution is taken from: https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication/35003812#35003812
# For example: For repo url: https://gitlab.codium-inc.com/qodo/autoscraper.git
# Then to clone one will issue: 'git clone https://oauth2:<access token>@gitlab.codium-inc.com/qodo/autoscraper.git'
# Note: GitLab instances are not always hosted under a gitlab.* domain.
# Build a clone URL that works with any host (e.g., gitlab.example.com).
if repo_url_to_clone.startswith(("http://", "https://")):
try:
parsed = urlparse(repo_url_to_clone)
if not parsed.scheme or not parsed.netloc:
raise ValueError("missing scheme or host")
netloc = parsed.netloc.split("@")[-1]
return f"{parsed.scheme}://oauth2:{access_token}@{netloc}{parsed.path}"
except Exception as e:
get_logger().error(
f"Repo URL: {repo_url_to_clone} could not be parsed for clone.",
artifact={"error": str(e)},
)
return None

clone_url = f"{scheme}oauth2:{access_token}@gitlab.{base_url}"
return clone_url
# Fallback for non-HTTP URLs (e.g., ssh or scp-style).
try:
if "@" in repo_url_to_clone and ":" in repo_url_to_clone and not repo_url_to_clone.startswith("ssh://"):
# Handle SCP-like URLs: git@gitlab.com:group/repo.git
repo_url_to_clone = "ssh://" + repo_url_to_clone.replace(":", "/", 1)

parsed = urlparse(repo_url_to_clone)
if not parsed.netloc:
raise ValueError("missing host")

netloc = parsed.netloc.split("@")[-1]
scheme = parsed.scheme if parsed.scheme else "https"
return f"{scheme}://oauth2:{access_token}@{netloc}{parsed.path}"
except Exception as e:
get_logger().error(
f"Repo URL: {repo_url_to_clone} could not be parsed for clone.",
artifact={"error": str(e)},
)
return None

@staticmethod
def _log_missing_clone_token() -> None:
get_logger().error("No access token found for GitLab clone.")
return None
41 changes: 41 additions & 0 deletions tests/unittest/test_gitlab_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def gitlab_provider(self, mock_gitlab_client, mock_project):
provider = GitLabProvider("https://gitlab.com/test/repo/-/merge_requests/1")
provider.gl = mock_gitlab_client
provider.id_project = "test/repo"
provider.gl.oauth_token = "fake_token"
return provider

def test_get_pr_file_content_success(self, gitlab_provider, mock_project):
Expand Down Expand Up @@ -192,3 +193,43 @@ def test_compare_submodule_cached(self, gitlab_provider):
assert first == second == [{"diff": "d"}]
m_pbp.assert_called_once_with("grp/repo")
proj.repository_compare.assert_called_once_with("old", "new")

def test_prepare_clone_url_with_token_gitlab_com(self, gitlab_provider):
gitlab_provider.gl.oauth_token = "token123"
repo_url = "https://gitlab.com/group/repo.git"

result = gitlab_provider._prepare_clone_url_with_token(repo_url)

assert result == "https://oauth2:token123@gitlab.com/group/repo.git"

def test_prepare_clone_url_with_token_custom_domain(self, gitlab_provider):
gitlab_provider.gl.oauth_token = "token123"
repo_url = "https://gitlab.example.com/group/repo.git"

result = gitlab_provider._prepare_clone_url_with_token(repo_url)

assert result == "https://oauth2:token123@gitlab.example.com/group/repo.git"

def test_prepare_clone_url_with_token_invalid_url(self, gitlab_provider):
gitlab_provider.gl.oauth_token = "token123"
repo_url = "gitlab.example.com/group/repo.git"

result = gitlab_provider._prepare_clone_url_with_token(repo_url)

assert result is None

def test_prepare_clone_url_with_token_scp_style(self, gitlab_provider):
gitlab_provider.gl.oauth_token = "token123"
repo_url = "git@gitlab.example.com:group/repo.git"

result = gitlab_provider._prepare_clone_url_with_token(repo_url)

assert result == "ssh://oauth2:token123@gitlab.example.com/group/repo.git"

def test_prepare_clone_url_with_token_ssh_url(self, gitlab_provider):
gitlab_provider.gl.oauth_token = "token123"
repo_url = "ssh://git@gitlab.example.com/group/repo.git"

result = gitlab_provider._prepare_clone_url_with_token(repo_url)

assert result == "ssh://oauth2:token123@gitlab.example.com/group/repo.git"