Skip to content

Commit bf16491

Browse files
committed
hide tokens in Git Backends automatically, push to repository
1 parent 56f2759 commit bf16491

File tree

2 files changed

+98
-23
lines changed

2 files changed

+98
-23
lines changed

conda_forge_tick/git_utils.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -548,21 +548,38 @@ def get_remote_url(
548548
owner: str,
549549
repo_name: str,
550550
connection_mode: GitConnectionMode = GitConnectionMode.HTTPS,
551+
token: str | None = None,
551552
) -> str:
552553
"""
553554
Get the URL of a remote repository.
554555
:param owner: The owner of the repository.
555556
:param repo_name: The name of the repository.
556557
:param connection_mode: The connection mode to use.
558+
:param token: A token to use for authentication. If falsy, no token is used. Use get_authenticated_remote_url
559+
instead if you want to use the token of the current user.
557560
:raises ValueError: If the connection mode is not supported.
558561
"""
559562
# Currently we don't need any abstraction for other platforms than GitHub, so we don't build such abstractions.
560563
match connection_mode:
561564
case GitConnectionMode.HTTPS:
562-
return f"https://github.com/{owner}/{repo_name}.git"
565+
return f"https://{f'{token}@' if token else ''}github.com/{owner}/{repo_name}.git"
563566
case _:
564567
raise ValueError(f"Unsupported connection mode: {connection_mode}")
565568

569+
@abstractmethod
570+
def push_to_repository(
571+
self, owner: str, repo_name: str, git_dir: Path, branch: str
572+
):
573+
"""
574+
Push changes to a repository.
575+
:param owner: The owner of the repository.
576+
:param repo_name: The name of the repository.
577+
:param git_dir: The directory of the git repository.
578+
:param branch: The branch to push to.
579+
:raises GitPlatformError: If the push fails.
580+
"""
581+
pass
582+
566583
@abstractmethod
567584
def fork(self, owner: str, repo_name: str):
568585
"""
@@ -729,7 +746,7 @@ def __init__(
729746
self,
730747
github3_client: github3.GitHub,
731748
pygithub_client: github.Github,
732-
token_to_hide: str | None = None,
749+
token: str,
733750
):
734751
"""
735752
Create a new GitHubBackend.
@@ -739,12 +756,14 @@ def __init__(
739756
740757
:param github3_client: The github3 client to use for interacting with the GitHub API.
741758
:param pygithub_client: The PyGithub client to use for interacting with the GitHub API.
742-
:param token_to_hide: A token to hide in the CLI output. If None, no tokens are hidden.
759+
:param token: The token that will be hidden in CLI outputs and used for writing to git repositories. Note that
760+
you need to authenticate github3 and PyGithub yourself. Use the `from_token` class method to create an instance
761+
that has all necessary clients set up.
743762
"""
744763
cli = GitCli()
745-
if token_to_hide:
746-
cli.add_hidden_token(token_to_hide)
764+
cli.add_hidden_token(token)
747765
super().__init__(cli)
766+
self.__token = token
748767
self.github3_client = github3_client
749768
self._github3_session = _Github3SessionWrapper(self.github3_client.session)
750769
self.github3_client.session = self._github3_session
@@ -756,13 +775,22 @@ def from_token(cls, token: str):
756775
return cls(
757776
github3.login(token=token),
758777
github.Github(auth=github.Auth.Token(token), per_page=cls._GITHUB_PER_PAGE),
759-
token_to_hide=token,
778+
token=token,
760779
)
761780

762781
def does_repository_exist(self, owner: str, repo_name: str) -> bool:
763782
repo = self.github3_client.repository(owner, repo_name)
764783
return repo is not None
765784

785+
def push_to_repository(
786+
self, owner: str, repo_name: str, git_dir: Path, branch: str
787+
):
788+
# we need an authenticated URL with write access
789+
remote_url = self.get_remote_url(
790+
owner, repo_name, GitConnectionMode.HTTPS, self.__token
791+
)
792+
self.cli.push_to_url(git_dir, remote_url, branch)
793+
766794
@lock_git_operation()
767795
def fork(self, owner: str, repo_name: str):
768796
if self.does_repository_exist(self.user, repo_name):
@@ -923,6 +951,13 @@ def does_repository_exist(self, owner: str, repo_name: str) -> bool:
923951
self.get_remote_url(owner, repo_name, GitConnectionMode.HTTPS)
924952
)
925953

954+
def push_to_repository(
955+
self, owner: str, repo_name: str, git_dir: Path, branch: str
956+
):
957+
logger.debug(
958+
f"Dry Run: Pushing changes from {git_dir} to {owner}/{repo_name} on branch {branch}."
959+
)
960+
926961
@lock_git_operation()
927962
def fork(self, owner: str, repo_name: str):
928963
if repo_name in self._repos:

tests/test_git_utils.py

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,16 @@ def test_git_platform_backend_get_remote_url_https():
10531053
assert url == f"https://github.com/{owner}/{repo}.git"
10541054

10551055

1056+
def test_git_platform_backend_get_remote_url_token():
1057+
owner = "OWNER"
1058+
repo = "REPO"
1059+
token = "TOKEN"
1060+
1061+
url = GitPlatformBackend.get_remote_url(owner, repo, GitConnectionMode.HTTPS, token)
1062+
1063+
assert url == f"https://{token}@github.com/{owner}/{repo}.git"
1064+
1065+
10561066
def test_github_backend_from_token():
10571067
token = "TOKEN"
10581068

@@ -1088,7 +1098,7 @@ def test_github_backend_token_to_hide(caplog, capfd, from_token: bool):
10881098
def test_github_backend_does_repository_exist(does_exist: bool):
10891099
github3_client = MagicMock()
10901100

1091-
backend = GitHubBackend(github3_client, MagicMock())
1101+
backend = GitHubBackend(github3_client, MagicMock(), "")
10921102

10931103
github3_client.repository.return_value = MagicMock() if does_exist else None
10941104

@@ -1110,7 +1120,7 @@ def test_github_backend_fork_not_exists_repo_found(
11101120
repository = MagicMock()
11111121
github3_client.repository.return_value = repository
11121122

1113-
backend = GitHubBackend(github3_client, MagicMock())
1123+
backend = GitHubBackend(github3_client, MagicMock(), "")
11141124
user_mock.return_value = "USER"
11151125
backend.fork("UPSTREAM-OWNER", "REPO")
11161126

@@ -1120,6 +1130,21 @@ def test_github_backend_fork_not_exists_repo_found(
11201130
sleep_mock.assert_called_once_with(5)
11211131

11221132

1133+
@mock.patch("conda_forge_tick.git_utils.GitCli.push_to_url")
1134+
def test_github_backend_push_to_repository(push_to_url_mock: MagicMock):
1135+
backend = GitHubBackend.from_token("THIS_IS_THE_TOKEN")
1136+
1137+
git_dir = Path("GIT_DIR")
1138+
1139+
backend.push_to_repository("OWNER", "REPO", git_dir, "BRANCH_NAME")
1140+
1141+
push_to_url_mock.assert_called_once_with(
1142+
git_dir,
1143+
"https://[email protected]/OWNER/REPO.git",
1144+
"BRANCH_NAME",
1145+
)
1146+
1147+
11231148
@pytest.mark.parametrize("branch_already_synced", [True, False])
11241149
@mock.patch("time.sleep", return_value=None)
11251150
@mock.patch(
@@ -1158,7 +1183,7 @@ def get_repo(full_name: str):
11581183
upstream_repo.default_branch = "UPSTREAM_BRANCH_NAME"
11591184
fork_repo.default_branch = "FORK_BRANCH_NAME"
11601185

1161-
backend = GitHubBackend(MagicMock(), pygithub_client)
1186+
backend = GitHubBackend(MagicMock(), pygithub_client, "")
11621187
backend.fork("UPSTREAM-OWNER", "REPO")
11631188

11641189
if not branch_already_synced:
@@ -1181,7 +1206,7 @@ def test_github_backend_remote_does_not_exist(
11811206
github3_client = MagicMock()
11821207
github3_client.repository.return_value = None
11831208

1184-
backend = GitHubBackend(github3_client, MagicMock())
1209+
backend = GitHubBackend(github3_client, MagicMock(), "")
11851210

11861211
user_mock.return_value = "USER"
11871212

@@ -1198,7 +1223,7 @@ def test_github_backend_user():
11981223
user.login = "USER"
11991224
pygithub_client.get_user.return_value = user
12001225

1201-
backend = GitHubBackend(MagicMock(), pygithub_client)
1226+
backend = GitHubBackend(MagicMock(), pygithub_client, "")
12021227

12031228
for _ in range(4):
12041229
# cached property
@@ -1213,7 +1238,7 @@ def test_github_backend_get_api_requests_left_github_exception(caplog):
12131238
"API Error"
12141239
)
12151240

1216-
backend = GitHubBackend(github3_client, MagicMock())
1241+
backend = GitHubBackend(github3_client, MagicMock(), "")
12171242

12181243
assert backend.get_api_requests_left() is None
12191244
assert "API error while fetching" in caplog.text
@@ -1225,7 +1250,7 @@ def test_github_backend_get_api_requests_left_unexpected_response_schema(caplog)
12251250
github3_client = MagicMock()
12261251
github3_client.rate_limit.return_value = {"some": "gibberish data"}
12271252

1228-
backend = GitHubBackend(github3_client, MagicMock())
1253+
backend = GitHubBackend(github3_client, MagicMock(), "")
12291254

12301255
assert backend.get_api_requests_left() is None
12311256
assert "API Error while parsing"
@@ -1237,7 +1262,7 @@ def test_github_backend_get_api_requests_left_nonzero():
12371262
github3_client = MagicMock()
12381263
github3_client.rate_limit.return_value = {"resources": {"core": {"remaining": 5}}}
12391264

1240-
backend = GitHubBackend(github3_client, MagicMock())
1265+
backend = GitHubBackend(github3_client, MagicMock(), "")
12411266

12421267
assert backend.get_api_requests_left() == 5
12431268

@@ -1249,7 +1274,7 @@ def test_github_backend_get_api_requests_left_zero_invalid_reset_time(caplog):
12491274

12501275
github3_client.rate_limit.return_value = {"resources": {"core": {"remaining": 0}}}
12511276

1252-
backend = GitHubBackend(github3_client, MagicMock())
1277+
backend = GitHubBackend(github3_client, MagicMock(), "")
12531278

12541279
assert backend.get_api_requests_left() == 0
12551280

@@ -1269,7 +1294,7 @@ def test_github_backend_get_api_requests_left_zero_valid_reset_time(caplog):
12691294
"resources": {"core": {"remaining": 0, "reset": reset_timestamp}}
12701295
}
12711296

1272-
backend = GitHubBackend(github3_client, MagicMock())
1297+
backend = GitHubBackend(github3_client, MagicMock(), "")
12731298

12741299
assert backend.get_api_requests_left() == 0
12751300

@@ -1308,7 +1333,7 @@ def request_side_effect(method, _url, **_kwargs):
13081333
pygithub_mock = MagicMock()
13091334
pygithub_mock.get_user.return_value.login = "CURRENT_USER"
13101335

1311-
backend = GitHubBackend(github3.login(token="TOKEN"), pygithub_mock)
1336+
backend = GitHubBackend(github3.login(token="TOKEN"), pygithub_mock, "")
13121337

13131338
pr_data = backend.create_pull_request(
13141339
"conda-forge",
@@ -1394,7 +1419,7 @@ def request_side_effect(method, url, **_kwargs):
13941419

13951420
request_mock.side_effect = request_side_effect
13961421

1397-
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock())
1422+
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock(), "")
13981423

13991424
backend.comment_on_pull_request(
14001425
"conda-forge",
@@ -1426,7 +1451,7 @@ def request_side_effect(method, url, **_kwargs):
14261451

14271452
request_mock.side_effect = request_side_effect
14281453

1429-
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock())
1454+
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock(), "")
14301455

14311456
with pytest.raises(RepositoryNotFoundError):
14321457
backend.comment_on_pull_request(
@@ -1463,7 +1488,7 @@ def request_side_effect(method, url, **_kwargs):
14631488
assert False, f"Unexpected endpoint: {method} {url}"
14641489

14651490
request_mock.side_effect = request_side_effect
1466-
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock())
1491+
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock(), "")
14671492

14681493
with pytest.raises(
14691494
GitPlatformError,
@@ -1516,7 +1541,7 @@ def request_side_effect(method, url, **_kwargs):
15161541

15171542
request_mock.side_effect = request_side_effect
15181543

1519-
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock())
1544+
backend = GitHubBackend(github3.login(token="TOKEN"), MagicMock(), "")
15201545

15211546
with pytest.raises(GitPlatformError, match="Could not comment on pull request"):
15221547
backend.comment_on_pull_request(
@@ -1528,7 +1553,7 @@ def request_side_effect(method, url, **_kwargs):
15281553

15291554

15301555
@pytest.mark.parametrize(
1531-
"backend", [GitHubBackend(MagicMock(), MagicMock()), DryRunBackend()]
1556+
"backend", [GitHubBackend(MagicMock(), MagicMock(), ""), DryRunBackend()]
15321557
)
15331558
@mock.patch(
15341559
"conda_forge_tick.git_utils.GitHubBackend.user", new_callable=mock.PropertyMock
@@ -1547,7 +1572,7 @@ def test_git_platform_backend_clone_fork_and_branch(
15471572

15481573
user_mock.return_value = "USER"
15491574

1550-
backend = GitHubBackend(MagicMock(), MagicMock())
1575+
backend = GitHubBackend(MagicMock(), MagicMock(), "")
15511576
backend.clone_fork_and_branch(
15521577
upstream_owner, repo_name, target_dir, new_branch, base_branch
15531578
)
@@ -1584,6 +1609,21 @@ def test_dry_run_backend_does_repository_exist_other_repo():
15841609
)
15851610

15861611

1612+
def test_dry_run_backend_push_to_repository(caplog):
1613+
caplog.set_level(logging.DEBUG)
1614+
1615+
backend = DryRunBackend()
1616+
1617+
git_dir = Path("GIT_DIR")
1618+
1619+
backend.push_to_repository("OWNER", "REPO", git_dir, "BRANCH_NAME")
1620+
1621+
assert (
1622+
"Dry Run: Pushing changes from GIT_DIR to OWNER/REPO on branch BRANCH_NAME"
1623+
in caplog.text
1624+
)
1625+
1626+
15871627
def test_dry_run_backend_fork(caplog):
15881628
caplog.set_level(logging.DEBUG)
15891629

0 commit comments

Comments
 (0)