feat: git retry
This commit is contained in:
		
							parent
							
								
									c2333d88b1
								
							
						
					
					
						commit
						13e7278cdd
					
				|  | @ -4,20 +4,32 @@ from datetime import datetime | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import List | from typing import List | ||||||
| 
 | 
 | ||||||
| from typer import Typer, echo | from typer import Argument, Typer, echo | ||||||
| 
 | 
 | ||||||
| from joint_teapot.teapot import Teapot | from joint_teapot.teapot import Teapot | ||||||
| from joint_teapot.utils.logger import logger | from joint_teapot.utils.logger import logger | ||||||
| 
 | 
 | ||||||
| app = Typer(add_completion=False) | app = Typer(add_completion=False) | ||||||
| teapot = Teapot() | 
 | ||||||
|  | 
 | ||||||
|  | class Tea: | ||||||
|  |     _teapot = None | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def pot(self) -> Teapot: | ||||||
|  |         if not self._teapot: | ||||||
|  |             self._teapot = Teapot() | ||||||
|  |         return self._teapot | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | tea = Tea()  # lazy loader | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command( | @app.command( | ||||||
|     "invite-to-teams", help="invite all canvas students to gitea teams by team name" |     "invite-to-teams", help="invite all canvas students to gitea teams by team name" | ||||||
| ) | ) | ||||||
| def add_all_canvas_students_to_teams(team_names: List[str]) -> None: | def add_all_canvas_students_to_teams(team_names: List[str]) -> None: | ||||||
|     teapot.add_all_canvas_students_to_teams(team_names) |     tea.pot.add_all_canvas_students_to_teams(team_names) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command( | @app.command( | ||||||
|  | @ -25,32 +37,32 @@ def add_all_canvas_students_to_teams(team_names: List[str]) -> None: | ||||||
|     help="create personal repos on gitea for all canvas students", |     help="create personal repos on gitea for all canvas students", | ||||||
| ) | ) | ||||||
| def create_personal_repos_for_all_canvas_students() -> None: | def create_personal_repos_for_all_canvas_students() -> None: | ||||||
|     teapot.create_personal_repos_for_all_canvas_students() |     tea.pot.create_personal_repos_for_all_canvas_students() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("create-teams", help="create teams on gitea by canvas groups") | @app.command("create-teams", help="create teams on gitea by canvas groups") | ||||||
| def create_teams_and_repos_by_canvas_groups(group_prefix: str) -> None: | def create_teams_and_repos_by_canvas_groups(group_prefix: str) -> None: | ||||||
|     teapot.create_teams_and_repos_by_canvas_groups(group_prefix) |     tea.pot.create_teams_and_repos_by_canvas_groups(group_prefix) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("get-public-keys", help="list all public keys on gitea") | @app.command("get-public-keys", help="list all public keys on gitea") | ||||||
| def get_public_key_of_all_canvas_students() -> None: | def get_public_key_of_all_canvas_students() -> None: | ||||||
|     echo("\n".join(teapot.get_public_key_of_all_canvas_students())) |     echo("\n".join(tea.pot.get_public_key_of_all_canvas_students())) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("clone-all-repos", help="clone all gitea repos to local") | @app.command("clone-all-repos", help="clone all gitea repos to local") | ||||||
| def clone_all_repos() -> None: | def clone_all_repos() -> None: | ||||||
|     teapot.clone_all_repos() |     tea.pot.clone_all_repos() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("create-issues", help="create issues on gitea") | @app.command("create-issues", help="create issues on gitea") | ||||||
| def create_issue_for_repos(repo_names: List[str], title: str, body: str) -> None: | def create_issue_for_repos(repo_names: List[str], title: str, body: str) -> None: | ||||||
|     teapot.create_issue_for_repos(repo_names, title, body) |     tea.pot.create_issue_for_repos(repo_names, title, body) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("check-issues", help="check the existence of issue by title on gitea") | @app.command("check-issues", help="check the existence of issue by title on gitea") | ||||||
| def check_exist_issue_by_title(repo_names: List[str], title: str) -> None: | def check_exist_issue_by_title(repo_names: List[str], title: str) -> None: | ||||||
|     echo("\n".join(teapot.check_exist_issue_by_title(repo_names, title))) |     echo("\n".join(tea.pot.check_exist_issue_by_title(repo_names, title))) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command( | @app.command( | ||||||
|  | @ -58,31 +70,37 @@ def check_exist_issue_by_title(repo_names: List[str], title: str) -> None: | ||||||
|     help="checkout git repo to git tag fetched from gitea by release name, with due date", |     help="checkout git repo to git tag fetched from gitea by release name, with due date", | ||||||
| ) | ) | ||||||
| def checkout_to_repos_by_release_name( | def checkout_to_repos_by_release_name( | ||||||
|     repo_names: List[str], release_name: str, due: datetime = datetime(3000, 1, 1) |     repo_names: List[str], release_name: str, due: datetime = Argument("3000-01-01") | ||||||
| ) -> None: | ) -> None: | ||||||
|     teapot.checkout_to_repos_by_release_name(repo_names, release_name, due) |     failed_repos = tea.pot.checkout_to_repos_by_release_name( | ||||||
|  |         repo_names, release_name, due | ||||||
|  |     ) | ||||||
|  |     echo(f"failed repos: {failed_repos}") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command( | @app.command( | ||||||
|     "close-all-issues", help="close all issues and pull requests in gitea organization" |     "close-all-issues", help="close all issues and pull requests in gitea organization" | ||||||
| ) | ) | ||||||
| def close_all_issues() -> None: | def close_all_issues() -> None: | ||||||
|     teapot.gitea.close_all_issues() |     tea.pot.gitea.close_all_issues() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("archieve-all-repos", help="archieve all repos in gitea organization") | @app.command("archieve-all-repos", help="archieve all repos in gitea organization") | ||||||
| def archieve_all_repos() -> None: | def archieve_all_repos() -> None: | ||||||
|     teapot.gitea.archieve_all_repos() |     tea.pot.gitea.archieve_all_repos() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("get-no-collaborator-repos", help="list all repos with no collaborators") | @app.command("get-no-collaborator-repos", help="list all repos with no collaborators") | ||||||
| def get_no_collaborator_repos() -> None: | def get_no_collaborator_repos() -> None: | ||||||
|     teapot.gitea.get_no_collaborator_repos() |     tea.pot.gitea.get_no_collaborator_repos() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command("get-no-commit-repos", help="list all repos with no commit") | @app.command("get-repos-status", help="list status of all repos with conditions") | ||||||
| def get_no_commit_repos() -> None: | def get_repos_status( | ||||||
|     teapot.gitea.get_no_commit_repos() |     commit_lt: int = Argument(100000, help="commit count less than"), | ||||||
|  |     issue_lt: int = Argument(100000, help="issue count less than"), | ||||||
|  | ) -> None: | ||||||
|  |     tea.pot.get_repos_status(commit_lt, issue_lt) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command( | @app.command( | ||||||
|  | @ -90,7 +108,7 @@ def get_no_commit_repos() -> None: | ||||||
|     help='prepare assignment dir from extracted canvas "Download Submissions" zip', |     help='prepare assignment dir from extracted canvas "Download Submissions" zip', | ||||||
| ) | ) | ||||||
| def prepare_assignment_dir(dir: Path) -> None: | def prepare_assignment_dir(dir: Path) -> None: | ||||||
|     teapot.canvas.prepare_assignment_dir(str(dir)) |     tea.pot.canvas.prepare_assignment_dir(str(dir)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.command( | @app.command( | ||||||
|  | @ -99,7 +117,7 @@ def prepare_assignment_dir(dir: Path) -> None: | ||||||
|     + "read the first line as score, the rest as comments", |     + "read the first line as score, the rest as comments", | ||||||
| ) | ) | ||||||
| def upload_assignment_scores(dir: Path, assignment_name: str) -> None: | def upload_assignment_scores(dir: Path, assignment_name: str) -> None: | ||||||
|     teapot.canvas.upload_assignment_scores(str(dir), assignment_name) |     tea.pot.canvas.upload_assignment_scores(str(dir), assignment_name) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|  |  | ||||||
|  | @ -1,14 +1,19 @@ | ||||||
| import functools | import functools | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from typing import Any, Callable, List, Optional | from typing import Any, Callable, List, Optional, TypeVar | ||||||
| 
 | 
 | ||||||
| from joint_teapot.config import settings | from joint_teapot.config import settings | ||||||
| from joint_teapot.utils.logger import logger | from joint_teapot.utils.logger import logger | ||||||
| from joint_teapot.utils.main import first | from joint_teapot.utils.main import first | ||||||
| from joint_teapot.workers import Canvas, Git, Gitea | from joint_teapot.workers import Canvas, Git, Gitea | ||||||
| 
 | 
 | ||||||
|  | _T = TypeVar("_T") | ||||||
| 
 | 
 | ||||||
| def for_all_methods(decorator: Callable[..., Any]) -> Callable[..., Any]: | 
 | ||||||
|  | def for_all_methods( | ||||||
|  |     decorator: Callable[[Callable[[_T], _T]], Any] | ||||||
|  | ) -> Callable[[_T], _T]: | ||||||
|  |     @functools.wraps(decorator) | ||||||
|     def decorate(cls: Any) -> Any: |     def decorate(cls: Any) -> Any: | ||||||
|         for attr in cls.__dict__:  # there's propably a better way to do this |         for attr in cls.__dict__:  # there's propably a better way to do this | ||||||
|             if callable(getattr(cls, attr)): |             if callable(getattr(cls, attr)): | ||||||
|  | @ -55,7 +60,9 @@ class Teapot: | ||||||
| 
 | 
 | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|         logger.info( |         logger.info( | ||||||
|             f"Settings loaded. Canvas Course ID: {settings.canvas_course_id}, Gitea Organization name: {settings.gitea_org_name}" |             "Settings loaded. " | ||||||
|  |             f"Canvas Course ID: {settings.canvas_course_id}, " | ||||||
|  |             f"Gitea Organization name: {settings.gitea_org_name}" | ||||||
|         ) |         ) | ||||||
|         logger.debug("Teapot initialized.") |         logger.debug("Teapot initialized.") | ||||||
| 
 | 
 | ||||||
|  | @ -84,11 +91,10 @@ class Teapot: | ||||||
|     def get_public_key_of_all_canvas_students(self) -> List[str]: |     def get_public_key_of_all_canvas_students(self) -> List[str]: | ||||||
|         return self.gitea.get_public_key_of_canvas_students(self.canvas.students) |         return self.gitea.get_public_key_of_canvas_students(self.canvas.students) | ||||||
| 
 | 
 | ||||||
|     def clone_all_repos(self) -> List[str]: |     def clone_all_repos(self) -> None: | ||||||
|         return [ |         for i, repo_name in enumerate(self.gitea.get_all_repo_names()): | ||||||
|  |             logger.info(f"{i}, {self.gitea.org_name}/{repo_name} cloning...") | ||||||
|             self.git.repo_clean_and_checkout(repo_name, "master") |             self.git.repo_clean_and_checkout(repo_name, "master") | ||||||
|             for repo_name in self.gitea.get_all_repo_names() |  | ||||||
|         ] |  | ||||||
| 
 | 
 | ||||||
|     def create_issue_for_repos( |     def create_issue_for_repos( | ||||||
|         self, repo_names: List[str], title: str, body: str |         self, repo_names: List[str], title: str, body: str | ||||||
|  | @ -112,19 +118,26 @@ class Teapot: | ||||||
|         due: datetime = datetime(3000, 1, 1), |         due: datetime = datetime(3000, 1, 1), | ||||||
|     ) -> List[str]: |     ) -> List[str]: | ||||||
|         failed_repos = [] |         failed_repos = [] | ||||||
|         repos_releases = self.gitea.get_repos_releases(repo_names) |         for repo_name in repo_names: | ||||||
|         for repo_name, repo_releases in zip(repo_names, repos_releases): |             repo_releases = self.gitea.get_repo_releases(repo_name) | ||||||
|             release = first(repo_releases, lambda item: item["name"] == release_name) |             release = first(repo_releases, lambda item: item.name == release_name) | ||||||
|             if ( |             if release is None or release.created_at.replace(tzinfo=None) >= due: | ||||||
|                 release is None |  | ||||||
|                 or datetime.strptime(release["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ") |  | ||||||
|                 >= due |  | ||||||
|             ): |  | ||||||
|                 failed_repos.append(repo_name) |                 failed_repos.append(repo_name) | ||||||
|                 continue |                 continue | ||||||
|             self.git.repo_clean_and_checkout(repo_name, f"tags/{release['tag_name']}") |             self.git.repo_clean_and_checkout(repo_name, f"tags/{release.tag_name}") | ||||||
|  |             logger.info( | ||||||
|  |                 f"{self.gitea.org_name}/{repo_name} checkout to tags/{release.tag_name} succeed" | ||||||
|  |             ) | ||||||
|         return failed_repos |         return failed_repos | ||||||
| 
 | 
 | ||||||
|  |     def get_repos_status(self, commit_lt: int, issue_lt: int) -> None: | ||||||
|  |         for repo_name, commit_count, issue_count in self.gitea.get_repos_status(): | ||||||
|  |             if commit_count < commit_lt or issue_count < issue_lt: | ||||||
|  |                 logger.info( | ||||||
|  |                     f"{self.gitea.org_name}/{repo_name} has " | ||||||
|  |                     f"{commit_count} commit(s), {issue_count} issue(s)" | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     teapot = Teapot() |     teapot = Teapot() | ||||||
|  |  | ||||||
|  | @ -1,5 +1,9 @@ | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
|  | from time import sleep | ||||||
|  | from typing import Optional | ||||||
|  | 
 | ||||||
|  | from git.exc import GitCommandError | ||||||
| 
 | 
 | ||||||
| from joint_teapot.utils.logger import logger | from joint_teapot.utils.logger import logger | ||||||
| 
 | 
 | ||||||
|  | @ -24,25 +28,70 @@ class Git: | ||||||
|         self.repos_dir = repos_dir |         self.repos_dir = repos_dir | ||||||
|         logger.debug("Git initialized") |         logger.debug("Git initialized") | ||||||
| 
 | 
 | ||||||
|     def clone_repo(self, repo_name: str, branch: str = "master") -> Repo: |     def clone_repo( | ||||||
|  |         self, repo_name: str, branch: str = "master", auto_retry: bool = True | ||||||
|  |     ) -> Optional[Repo]: | ||||||
|  |         repo = None | ||||||
|         repo_dir = os.path.join(self.repos_dir, repo_name) |         repo_dir = os.path.join(self.repos_dir, repo_name) | ||||||
|         return Repo.clone_from( |         retry_interval = 2 | ||||||
|             f"ssh://git@focs.ji.sjtu.edu.cn:2222/{self.org_name}/{repo_name}.git", |         while retry_interval and auto_retry: | ||||||
|             repo_dir, |             try: | ||||||
|             branch=branch, |                 repo = Repo.clone_from( | ||||||
|         ) |                     f"ssh://git@focs.ji.sjtu.edu.cn:2222/{self.org_name}/{repo_name}.git", | ||||||
|  |                     repo_dir, | ||||||
|  |                     branch=branch, | ||||||
|  |                 ) | ||||||
|  |             except GitCommandError as e: | ||||||
|  |                 if "Connection refused" in e.stderr or "Connection reset" in e.stderr: | ||||||
|  |                     logger.warning( | ||||||
|  |                         f"{repo_name} connection refused/reset in clone. " | ||||||
|  |                         "Probably by JI firewall." | ||||||
|  |                     ) | ||||||
|  |                     logger.info(f"wait for {retry_interval} seconds to retry...") | ||||||
|  |                     sleep(retry_interval) | ||||||
|  |                     if retry_interval < 64: | ||||||
|  |                         retry_interval *= 2 | ||||||
|  |                 elif f"Remote branch {branch} not found in upstream origin" in e.stderr: | ||||||
|  |                     retry_interval = 0 | ||||||
|  |                     logger.error(f"{repo_name} origin/{branch} not found") | ||||||
|  |                 else: | ||||||
|  |                     raise | ||||||
|  |         return repo | ||||||
| 
 | 
 | ||||||
|     def get_repo(self, repo_name: str) -> Repo: |     def get_repo(self, repo_name: str) -> Optional[Repo]: | ||||||
|         repo_dir = os.path.join(self.repos_dir, repo_name) |         repo_dir = os.path.join(self.repos_dir, repo_name) | ||||||
|         if os.path.exists(repo_dir): |         if os.path.exists(repo_dir): | ||||||
|             return Repo(repo_dir) |             return Repo(repo_dir) | ||||||
|         return self.clone_repo(repo_name) |         return self.clone_repo(repo_name) | ||||||
| 
 | 
 | ||||||
|     def repo_clean_and_checkout(self, repo_name: str, checkout_dest: str) -> str: |     def repo_clean_and_checkout( | ||||||
|  |         self, repo_name: str, checkout_dest: str, auto_retry: bool = True | ||||||
|  |     ) -> str: | ||||||
|         repo_dir = os.path.join(self.repos_dir, repo_name) |         repo_dir = os.path.join(self.repos_dir, repo_name) | ||||||
|         repo = self.get_repo(repo_name) |         repo = self.get_repo(repo_name) | ||||||
|         repo.git.fetch("--tags", "--all", "-f") |         if not repo: | ||||||
|         repo.git.reset("--hard", f"origin/master") |             return repo_dir | ||||||
|         repo.git.clean("-d", "-f", "-x") |         retry_interval = 2 | ||||||
|         repo.git.checkout(checkout_dest) |         while retry_interval and auto_retry: | ||||||
|  |             try: | ||||||
|  |                 repo.git.fetch("--tags", "--all", "-f") | ||||||
|  |                 repo.git.reset("--hard", f"origin/master") | ||||||
|  |                 repo.git.clean("-d", "-f", "-x") | ||||||
|  |                 repo.git.checkout(checkout_dest) | ||||||
|  |                 retry_interval = 0 | ||||||
|  |             except GitCommandError as e: | ||||||
|  |                 if "Connection refused" in e.stderr or "Connection reset" in e.stderr: | ||||||
|  |                     logger.warning( | ||||||
|  |                         f"{repo_name} connection refused/reset in clone. " | ||||||
|  |                         "Probably by JI firewall." | ||||||
|  |                     ) | ||||||
|  |                     logger.info(f"wait for {retry_interval} seconds to retry...") | ||||||
|  |                     sleep(retry_interval) | ||||||
|  |                     if retry_interval < 64: | ||||||
|  |                         retry_interval *= 2 | ||||||
|  |                 elif "Remote branch master not found in upstream origin" in e.stderr: | ||||||
|  |                     retry_interval = 0 | ||||||
|  |                     logger.error(f"{repo_name} origin/master not found") | ||||||
|  |                 else: | ||||||
|  |                     raise | ||||||
|         return repo_dir |         return repo_dir | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import re | import re | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from functools import lru_cache | from functools import lru_cache | ||||||
| from typing import Any, Callable, Dict, List, Optional | from typing import Any, Callable, List, Optional, Tuple | ||||||
| 
 | 
 | ||||||
| import focs_gitea | import focs_gitea | ||||||
| from canvasapi.group import Group, GroupMembership | from canvasapi.group import Group, GroupMembership | ||||||
|  | @ -267,11 +267,15 @@ class Gitea: | ||||||
|                 logger.error(e) |                 logger.error(e) | ||||||
|         return res |         return res | ||||||
| 
 | 
 | ||||||
|     def get_repos_releases(self, repo_names: List[str]) -> List[List[Dict[str, Any]]]: |     def get_repo_releases(self, repo_name: str) -> List[Any]: | ||||||
|         return [ |         res = [] | ||||||
|             list_all(self.repository_api.repo_list_releases, self.org_name, repo_name) |         try: | ||||||
|             for repo_name in repo_names |             args = self.repository_api.repo_list_releases, self.org_name, repo_name | ||||||
|         ] |             res = list_all(*args) | ||||||
|  |         except ApiException as e: | ||||||
|  |             if e.status != 404: | ||||||
|  |                 raise | ||||||
|  |         return res | ||||||
| 
 | 
 | ||||||
|     def get_all_repo_names(self) -> List[str]: |     def get_all_repo_names(self) -> List[str]: | ||||||
|         return [ |         return [ | ||||||
|  | @ -291,19 +295,24 @@ class Gitea: | ||||||
|             res.append(data.name) |             res.append(data.name) | ||||||
|         return res |         return res | ||||||
| 
 | 
 | ||||||
|     def get_no_commit_repos(self) -> List[str]: |     def get_repos_status(self) -> List[Tuple[str, int, int]]: | ||||||
|         res = [] |         res = [] | ||||||
|         for data in list_all(self.organization_api.org_list_repos, self.org_name): |         for repo in list_all(self.organization_api.org_list_repos, self.org_name): | ||||||
|             try: |             try: | ||||||
|                 commits = self.repository_api.repo_get_all_commits( |                 commits = self.repository_api.repo_get_all_commits( | ||||||
|                     self.org_name, data.name |                     self.org_name, repo.name | ||||||
|                 ) |                 ) | ||||||
|             except ApiException as e: |             except ApiException as e: | ||||||
|                 if e.status == 409: |                 if e.status != 409: | ||||||
|                     logger.info(f"{self.org_name}/{data.name} has no commits") |                     raise | ||||||
|                     res.append(data.name) |                 commits = [] | ||||||
|                 else: |             issues = self.issue_api.issue_list_issues( | ||||||
|                     raise (e) |                 self.org_name, repo.name, state="all" | ||||||
|  |             ) | ||||||
|  |             # if not commits: | ||||||
|  |             #     logger.info(f"{self.org_name}/{repo.name} has no commits") | ||||||
|  |             #     res.append(repo.name) | ||||||
|  |             res.append((repo.name, len(commits), len(issues))) | ||||||
|         return res |         return res | ||||||
| 
 | 
 | ||||||
|     def create_issue( |     def create_issue( | ||||||
|  | @ -356,4 +365,3 @@ class Gitea: | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     gitea = Gitea() |     gitea = Gitea() | ||||||
|     res = gitea.get_no_commit_repos() |  | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user