Skip to content
Merged
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
29 changes: 27 additions & 2 deletions src/dvsim/utils/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""Git utility functions."""

from pathlib import Path
from urllib.parse import urlsplit, urlunsplit

from git import Repo

Expand All @@ -13,6 +14,26 @@
__all__ = ("repo_root",)


def strip_url_credentials(url: str) -> str:
"""Remove any username/password from a URL, keeping the rest of it intact.

A CI checkout leaves its access token in the remote URL as ``https://oauth2:<token>@host/org/repo``.
That URL is recorded in the run metadata and published in the reports, which get archived, so the
token would outlive the run that leaked it.

An ssh URL keeps its user: ``git@`` names an ssh account rather than an identity to authenticate
as, so dropping it gives a URL that no longer addresses the remote. A password is stripped
whatever the scheme carries it.
"""
parts = urlsplit(url)
userinfo, _, host = parts.netloc.rpartition("@")
if not userinfo:
return url
if ":" not in userinfo and parts.scheme not in ("http", "https"):
return url
return urlunsplit((parts.scheme, host, parts.path, parts.query, parts.fragment))


def repo_root(path: Path) -> Path | None:
"""Given a sub dir in a git repo provide the root path.

Expand Down Expand Up @@ -56,7 +77,11 @@ def git_is_dirty(path: Path | None = None) -> bool:


def git_origin_url(path: Path | None = None) -> str | None:
"""Get the git remote origin url, or None if no ``origin`` remote is configured."""
"""Get the git remote origin url, or None if no ``origin`` remote is configured.

Any credentials the remote carries are stripped, so that the url is safe to record in run
metadata and reports.
"""
root = repo_root(path=path or Path.cwd())

if root is None:
Expand All @@ -68,7 +93,7 @@ def git_origin_url(path: Path | None = None) -> str | None:
if "origin" not in [remote.name for remote in r.remotes]:
return None

return r.remote("origin").url
return strip_url_credentials(r.remote("origin").url)


def git_https_url_with_commit(path: Path | None = None) -> str | None:
Expand Down
64 changes: 64 additions & 0 deletions tests/utils/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,66 @@ def test_git_origin_url(tmp_path: Path) -> None:
equal_to(url),
)

@staticmethod
@pytest.mark.parametrize(
("url", "expected"),
[
(
"https://github.com/lowRISC/test.git",
"https://github.com/lowRISC/test.git",
),
("git@github.com:lowRISC/test.git", "git@github.com:lowRISC/test.git"),
# An ssh user is part of the address rather than a credential, so it stays. Dropping
# it would leave a url that no longer reaches the remote.
(
"ssh://git@github.com/lowRISC/test.git",
"ssh://git@github.com/lowRISC/test.git",
),
(
"git+ssh://git@github.com/lowRISC/test.git",
"git+ssh://git@github.com/lowRISC/test.git",
),
(
"https://oauth2:ghs_secrettoken@github.com/lowRISC/test.git",
"https://github.com/lowRISC/test.git",
),
(
"https://someuser@github.com/lowRISC/test.git",
"https://github.com/lowRISC/test.git",
),
# A password is a credential whatever the scheme carries it.
(
"ssh://user:secretpw@github.com/lowRISC/test.git",
"ssh://github.com/lowRISC/test.git",
),
],
ids=[
"plain_https",
"ssh_scp_form",
"ssh_url_form",
"git_ssh_scheme",
"token",
"user_only",
"ssh_with_password",
],
)
def test_git_origin_url_strips_credentials(tmp_path: Path, url: str, expected: str) -> None:
"""A token in the remote url never reaches the caller, whatever the url's shape.

The url is recorded in the run metadata and published in the reports, so a credential
left in it outlives the run. Both ssh forms carry an '@' without being credentialed, and
have to survive untouched or the recorded url stops addressing the remote.
"""
r = Repo.init(path=tmp_path)

file = tmp_path / "a"
file.write_text("file to commit")
r.index.add([file])
r.index.commit("initial commit")
r.create_remote("origin", url)

assert_that(git_origin_url(tmp_path), equal_to(expected))

@staticmethod
@pytest.mark.parametrize(
("url", "expected"),
Expand All @@ -141,6 +201,10 @@ def test_git_origin_url(tmp_path: Path) -> None:
"https://github.com/lowRISC/test.git",
"https://github.com/lowRISC/test/tree/{commit}",
),
(
"https://oauth2:ghs_secrettoken@github.com/lowRISC/test.git",
"https://github.com/lowRISC/test/tree/{commit}",
),
],
)
def test_git_https_url_with_commit(tmp_path: Path, url: str, expected: str) -> None:
Expand Down
Loading