diff --git a/docs/admin/config.rst b/docs/admin/config.rst index 1e77c0e11c37..4bb293c8aa77 100644 --- a/docs/admin/config.rst +++ b/docs/admin/config.rst @@ -2115,6 +2115,10 @@ The configuration dictionary consists of credentials defined for each API host. The API host might be different from what you use in the web browser, for example GitHub API is accessed as ``api.github.com``. +The credentials can also be overridden in :ref:`component-push` or +:ref:`component-repo` (if push URL is not configured), these take precedence +over the ones specified in the configuration file. + The following configuration is available for each host: ``username`` diff --git a/docs/changes.rst b/docs/changes.rst index 25dce509c0ca..ce252bfbcb3e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -6,6 +6,7 @@ Not yet released. **New features** * :ref:`Searching` now supports filtering by object path. +* Merge requests credentials can now be passed in the repository URL, see :ref:`settings-credentials`. **Improvements** diff --git a/weblate/vcs/git.py b/weblate/vcs/git.py index ee7c665788ae..478a251cb751 100644 --- a/weblate/vcs/git.py +++ b/weblate/vcs/git.py @@ -802,12 +802,14 @@ def merge( def parse_repo_url( self, repo: str | None = None - ) -> tuple[str | None, str, str, str]: + ) -> tuple[str | None, str | None, str | None, str, str, str]: if repo is None: - repo = self.component.repo + repo = self.component.push or self.component.repo parsed = urllib.parse.urlparse(repo) host = parsed.hostname scheme: str | None = parsed.scheme + username: str | None = parsed.username + password: str | None = parsed.password if not host: # Assume SSH URL host, path = repo.split(":") @@ -829,7 +831,7 @@ def parse_repo_url( continue slug_parts.insert(-1, part) slug = "/".join(slug_parts) - return (scheme, host, owner, slug) + return (scheme, username, password, host, owner, slug) def format_url( self, scheme: str, hostname: str, owner: str, slug: str, **extra: str @@ -878,10 +880,14 @@ def get_credentials_configuration(cls): return getattr(settings, cls.get_credentials_name()) def get_credentials(self) -> dict[str, str]: - scheme, host, owner, slug = self.parse_repo_url() + scheme, username, password, host, owner, slug = self.parse_repo_url() hostname = self.format_api_host(host).lower() credentials = self.get_credentials_by_hostname(hostname) + if not username or not password: + username = credentials["username"] + password = credentials["token"] + # Scheme override if "scheme" in credentials: scheme = credentials["scheme"] @@ -894,8 +900,8 @@ def get_credentials(self) -> dict[str, str]: "owner": owner, "slug": slug, "hostname": hostname, - "username": credentials["username"], - "token": credentials["token"], + "username": username, + "token": password, "scheme": scheme, } @@ -1211,7 +1217,7 @@ def fork(self, credentials: dict) -> None: def parse_repo_url( self, repo: str | None = None - ) -> tuple[str | None, str, str, str]: + ) -> tuple[str | None, str | None, str | None, str, str, str]: if repo is None: repo = self.component.repo @@ -1220,7 +1226,7 @@ def parse_repo_url( if not re.match(scheme_regex, repo): repo = f"ssh://{repo}" # assume all links without schema are ssh links - (scheme, host, owner, slug) = super().parse_repo_url(repo) + (scheme, username, password, host, owner, slug) = super().parse_repo_url(repo) # ssh links are in a subdomain, the API link doesn't have that so remove it if host.startswith("ssh."): @@ -1237,7 +1243,7 @@ def parse_repo_url( owner = owner + "/" + parts[0] # we want owner to be org/project slug = parts[1] - return scheme, host, owner, slug + return (scheme, username, password, host, owner, slug) def get_headers(self, credentials: dict) -> dict[str, str]: headers = super().get_headers(credentials) @@ -1358,7 +1364,9 @@ def __get_forked_id(self, credentials: dict, remote: str) -> str: """ cmd = ["remote", "get-url", "--push", remote] fork_remotes = self.execute(cmd, needs_lock=False, merge_err=False).splitlines() - (_, hostname, owner, slug) = self.parse_repo_url(fork_remotes[0]) + (_scheme, _username, _password, hostname, owner, slug) = self.parse_repo_url( + fork_remotes[0] + ) url = self.format_url("https", hostname, owner, slug) # Get repo info @@ -1717,7 +1725,7 @@ class GitLabRepository(GitMergeRequestBase): ) def get_fork_path(self, repo: str) -> str: - _scheme, _host, owner, slug = self.parse_repo_url(repo) + _scheme, _username, _password, _host, owner, slug = self.parse_repo_url(repo) return urllib.parse.quote(f"{owner}/{slug}", safe="") def get_forked_url(self, credentials: dict) -> str: diff --git a/weblate/vcs/tests/test_vcs.py b/weblate/vcs/tests/test_vcs.py index f680eaaf8c12..4241a33bd84b 100644 --- a/weblate/vcs/tests/test_vcs.py +++ b/weblate/vcs/tests/test_vcs.py @@ -1364,11 +1364,19 @@ def test_get_fork_path(self) -> None: ), "group1%2Fsubgroup%2Fproject", ) + + def test_parse_repo_url(self) -> None: self.assertEqual( self.repo.parse_repo_url( "git@gitlab.domain.com:group1/subgroup/project.git" ), - (None, "gitlab.domain.com", "group1", "subgroup/project"), + (None, None, None, "gitlab.domain.com", "group1", "subgroup/project"), + ) + self.assertEqual( + self.repo.parse_repo_url( + "https://bot:glpat@gitlab.com/path/group/repo.git" + ), + ("https", "bot", "glpat", "gitlab.com", "path", "group/repo"), ) @override_settings(