Skip to content

Commit b59f692

Browse files
authored
Add migration endpoint for single repo by name (#5)
1 parent 03a2756 commit b59f692

File tree

4 files changed

+165
-5
lines changed

4 files changed

+165
-5
lines changed

gitea_github_sync/cli.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import click
44
from rich import print
55

6-
from . import gitea, github, repository
6+
from . import config, gitea, github, repository
77

88

99
@click.group()
@@ -45,3 +45,19 @@ def list_all_gitea_repositories(stats: bool) -> None:
4545
gt = gitea.get_gitea()
4646
repos = gt.get_repos()
4747
print_repositories(repos, stats)
48+
49+
50+
@cli.command()
51+
@click.argument("full_repo_name")
52+
def migrate_repo(full_repo_name: str) -> None:
53+
conf = config.load_config()
54+
gt = gitea.get_gitea()
55+
gh = github.get_github()
56+
github_repos = github.list_all_repositories(gh)
57+
try:
58+
repo = next((repo for repo in github_repos if repo.full_repo_name == full_repo_name))
59+
except StopIteration:
60+
print(f"[b red]Repository {full_repo_name} does not exist on Github[/]")
61+
raise click.Abort()
62+
63+
gt.migrate_repo(repo=repo, github_token=conf.github_token)

gitea_github_sync/gitea.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
from .repository import Repository, Visibility
1111

1212

13+
@dataclass(frozen=True)
14+
class GiteaMigrationError(ValueError):
15+
full_repo_name: str
16+
17+
def __str__(self) -> str:
18+
return f"Could not migrate {self.full_repo_name}"
19+
20+
1321
@dataclass(frozen=True)
1422
class Gitea:
1523
api_url: str
@@ -42,6 +50,25 @@ def get_repos(self) -> List[Repository]:
4250
for repo in repos
4351
]
4452

53+
def migrate_repo(self, repo: Repository, github_token: str) -> None:
54+
request_data = {
55+
"auth_token": github_token,
56+
"clone_addr": f"https://github.com/{repo.full_repo_name}",
57+
"repo_name": repo.get_repo_name(),
58+
"service": "github",
59+
"mirror": True,
60+
"private": repo.visibility == Visibility.PRIVATE,
61+
}
62+
res = requests.post(
63+
f"{self.api_url}/repos/migrate",
64+
headers=self._get_authorization_header(),
65+
json=request_data,
66+
)
67+
try:
68+
res.raise_for_status()
69+
except requests.HTTPError as e:
70+
raise GiteaMigrationError(repo.full_repo_name) from e
71+
4572

4673
def get_gitea(conf: Optional[config.Config] = None) -> Gitea:
4774
if conf is None:

tests/test_cli.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import textwrap
22
from io import StringIO
33
from typing import List
4-
from unittest.mock import MagicMock, patch
4+
from unittest.mock import MagicMock, PropertyMock, patch
55

66
import pytest
77
from click.testing import CliRunner
@@ -71,6 +71,62 @@ def test_list_all_gitea_repositories(
7171
mock_print_repositories.assert_called_once_with(repositories_fixture, expected_stat)
7272

7373

74+
@patch("gitea_github_sync.cli.config.load_config", autospec=True)
75+
@patch("gitea_github_sync.cli.github.list_all_repositories", autospec=True)
76+
@patch("gitea_github_sync.cli.github.get_github", autospec=True)
77+
@patch("gitea_github_sync.cli.gitea.get_gitea", autospec=True)
78+
def test_migrate_repo(
79+
mock_get_gitea: MagicMock,
80+
mock_get_github: MagicMock,
81+
mock_list_all_repositories: MagicMock,
82+
mock_load_config: MagicMock,
83+
repositories_fixture: List[Repository],
84+
) -> None:
85+
expected_repo = Repository("Muscaw/gitea-github-sync", Visibility.PRIVATE)
86+
expected_github_token = "some-github-token"
87+
88+
type(mock_load_config.return_value).github_token = PropertyMock(
89+
return_value=expected_github_token
90+
)
91+
mock_list_all_repositories.return_value = repositories_fixture + [expected_repo]
92+
93+
runner = CliRunner()
94+
command = ["migrate-repo", "Muscaw/gitea-github-sync"]
95+
result = runner.invoke(cli, command)
96+
97+
assert result.exit_code == 0
98+
mock_list_all_repositories.assert_called_once_with(mock_get_github.return_value)
99+
mock_get_gitea.return_value.migrate_repo.assert_called_once_with(
100+
repo=expected_repo, github_token=expected_github_token
101+
)
102+
103+
104+
@patch("gitea_github_sync.cli.config.load_config", autospec=True)
105+
@patch("gitea_github_sync.cli.github.list_all_repositories", autospec=True)
106+
@patch("gitea_github_sync.cli.github.get_github", autospec=True)
107+
@patch("gitea_github_sync.cli.gitea.get_gitea", autospec=True)
108+
def test_migrate_repo_no_match(
109+
mock_get_gitea: MagicMock,
110+
mock_get_github: MagicMock,
111+
mock_list_all_repositories: MagicMock,
112+
mock_load_config: MagicMock,
113+
repositories_fixture: List[Repository],
114+
) -> None:
115+
mock_list_all_repositories.return_value = repositories_fixture
116+
repo_name = "Muscaw/gitea-github-sync"
117+
118+
runner = CliRunner()
119+
command = ["migrate-repo", repo_name]
120+
result = runner.invoke(cli, command)
121+
122+
assert result.exit_code != 0
123+
assert "Aborted!" in result.stdout
124+
assert f"Repository {repo_name} does not exist on Github" in result.stdout
125+
mock_list_all_repositories.assert_called_once_with(mock_get_github.return_value)
126+
mock_get_gitea.return_value.migrate_repo.assert_not_called()
127+
mock_load_config.assert_called_once()
128+
129+
74130
@patch("sys.stdout", new_callable=StringIO)
75131
def test_print_repositories_without_stats(
76132
stdout: StringIO,

tests/test_gitea.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from responses import matchers
66

77
from gitea_github_sync.config import Config
8-
from gitea_github_sync.gitea import Gitea, get_gitea
8+
from gitea_github_sync.gitea import Gitea, GiteaMigrationError, get_gitea
99
from gitea_github_sync.repository import Repository, Visibility
1010

1111
GITEA_BASE_API_URL = "https://gitea.yourinstance.com/api/v1"
@@ -25,7 +25,7 @@ def gitea_fixture(conf_fixture: Config) -> Gitea:
2525

2626

2727
@responses.activate
28-
def test_get_repos(gitea_fixture: Gitea) -> None:
28+
def test_gitea_get_repos(gitea_fixture: Gitea) -> None:
2929

3030
json = [
3131
{"full_name": "some-team/a-repo", "private": True},
@@ -47,7 +47,7 @@ def test_get_repos(gitea_fixture: Gitea) -> None:
4747

4848

4949
@responses.activate
50-
def test_get_repos_multiple_pages(gitea_fixture: Gitea) -> None:
50+
def test_gitea_get_repos_multiple_pages(gitea_fixture: Gitea) -> None:
5151

5252
json_1 = [
5353
{"full_name": "some-team/a-repo", "private": True},
@@ -110,6 +110,62 @@ def test_get_repos_multiple_pages(gitea_fixture: Gitea) -> None:
110110
assert expected_repos == result
111111

112112

113+
@responses.activate
114+
@pytest.mark.parametrize("is_private", [True, False])
115+
def test_gitea_migrate_repo(gitea_fixture: Gitea, is_private: bool) -> None:
116+
gh_token = "some-github-token"
117+
expected_data = {
118+
"auth_token": gh_token,
119+
"clone_addr": "https://github.com/Muscaw/gitea-github-sync",
120+
"repo_name": "gitea-github-sync",
121+
"service": "github",
122+
"mirror": True,
123+
"private": is_private,
124+
}
125+
repo = Repository(
126+
full_repo_name="Muscaw/gitea-github-sync",
127+
visibility=Visibility.PRIVATE if is_private else Visibility.PUBLIC,
128+
)
129+
responses.post(
130+
f"{GITEA_BASE_API_URL}/repos/migrate",
131+
match=[
132+
matchers.header_matcher({"Authorization": f"token {GITEA_TOKEN}"}),
133+
matchers.json_params_matcher(expected_data),
134+
],
135+
)
136+
137+
gitea_fixture.migrate_repo(repo, gh_token)
138+
139+
140+
@responses.activate
141+
@pytest.mark.parametrize("is_private", [True, False])
142+
def test_gitea_migrate_repo_failure_to_migrate(gitea_fixture: Gitea, is_private: bool) -> None:
143+
gh_token = "some-github-token"
144+
expected_data = {
145+
"auth_token": gh_token,
146+
"clone_addr": "https://github.com/Muscaw/gitea-github-sync",
147+
"repo_name": "gitea-github-sync",
148+
"service": "github",
149+
"mirror": True,
150+
"private": is_private,
151+
}
152+
repo = Repository(
153+
full_repo_name="Muscaw/gitea-github-sync",
154+
visibility=Visibility.PRIVATE if is_private else Visibility.PUBLIC,
155+
)
156+
responses.post(
157+
f"{GITEA_BASE_API_URL}/repos/migrate",
158+
match=[
159+
matchers.header_matcher({"Authorization": f"token {GITEA_TOKEN}"}),
160+
matchers.json_params_matcher(expected_data),
161+
],
162+
status=409,
163+
)
164+
165+
with pytest.raises(GiteaMigrationError):
166+
gitea_fixture.migrate_repo(repo, gh_token)
167+
168+
113169
def test_gitea(gitea_fixture: Gitea, conf_fixture: Config) -> None:
114170
gt = get_gitea(conf_fixture)
115171

@@ -125,3 +181,8 @@ def test_gitea_default_value(
125181

126182
assert gt == gitea_fixture
127183
mock_load_config.assert_called_once()
184+
185+
186+
def test_gitea_migration_error() -> None:
187+
error = GiteaMigrationError("Muscaw/gitea-github-sync")
188+
assert str(error) == "Could not migrate Muscaw/gitea-github-sync"

0 commit comments

Comments
 (0)