diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index aa701e49b..44acb1bb0 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -154,8 +154,12 @@ async def git_create_pull( >>> body=body, >>> access_token=access_token, >>> ) - >>> print(pr) + >>> if isinstance(pr, PullRequest): + >>> print(pr) PullRequest("feat: modify http lib") + >>> if isinstance(pr, str): + >>> print(f"Visit this url to create a new pull request: '{pr}'") + Visit this url to create a new pull request: 'https://github.com/iorisa/snake-game/compare/master...feature/new' >>> # create pull request >>> base_repo_name = "geekan/MetaGPT" @@ -175,9 +179,12 @@ async def git_create_pull( >>> body=body, >>> access_token=access_token, >>> ) - >>> print(pr) + >>> if isinstance(pr, PullRequest): + >>> print(pr) PullRequest("feat: modify http lib") - + >>> if isinstance(pr, str): + >>> print(f"Visit this url to create a new pull request: '{pr}'") + Visit this url to create a new pull request: 'https://github.com/geekan/MetaGPT/compare/master...iorisa:MetaGPT:feature/http' Returns: PullRequest: The created pull request. diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index 3c45c4085..8fc52579a 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -14,7 +14,8 @@ import uuid from enum import Enum from pathlib import Path from subprocess import TimeoutExpired -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union +from urllib.parse import quote from git.repo import Repo from git.repo.fun import is_git_dir @@ -456,7 +457,7 @@ class GitRepository: issue: Optional[Issue] = None, access_token: Optional[str] = None, auth: Optional[Auth] = None, - ) -> PullRequest: + ) -> Union[PullRequest, str]: """ Creates a pull request in the specified repository. @@ -483,18 +484,16 @@ class GitRepository: issue = issue or NotSet if not auth and not access_token: raise ValueError('`access_token` is invalid. Visit: "https://github.com/settings/tokens"') - auth = auth or Auth.Token(access_token) - g = Github(auth=auth) - base_repo = g.get_repo(base_repo_name) - head_repo = g.get_repo(head_repo_name) if head_repo_name and head_repo_name != base_repo_name else None - x_ratelimit_remaining = base_repo.raw_headers.get("x-ratelimit-remaining") - if ( - x_ratelimit_remaining - and bool(re.match(r"^-?\d+$", x_ratelimit_remaining)) - and int(x_ratelimit_remaining) <= 0 - ): - raise RateLimitError() - if not head_repo: + clone_url = f"https://github.com/{base_repo_name}.git" + try: + auth = auth or Auth.Token(access_token) + g = Github(auth=auth) + base_repo = g.get_repo(base_repo_name) + clone_url = base_repo.clone_url + head_repo = g.get_repo(head_repo_name) if head_repo_name and head_repo_name != base_repo_name else None + if head_repo: + user = head_repo.full_name.split("/")[0] + head = f"{user}:{head}" pr = base_repo.create_pull( base=base, head=head, @@ -504,17 +503,13 @@ class GitRepository: draft=draft, issue=issue, ) - else: - head_branch = base_repo.get_branch(base) - base_branch = head_repo.get_branch(head) - pr = base_repo.create_pull( - base=base_branch.name, - head=head_branch.commit.sha, - title=title, - body=body, - maintainer_can_modify=maintainer_can_modify, - draft=draft, - issue=issue, + except Exception as e: + logger.warning(f"Pull Request Error: {e}") + return GitRepository.create_github_pull_url( + clone_url=clone_url, + base=base, + head=head, + head_repo_name=head_repo_name, ) return pr @@ -594,3 +589,41 @@ class GitRepository: user = git.get_user() v = user.get_repos(visibility="public") return [i.full_name for i in v] + + @staticmethod + def create_github_pull_url(clone_url: str, base: str, head: str, head_repo_name: Optional[str] = None) -> str: + """ + Create a URL for comparing changes between branches or repositories on GitHub. + + Args: + clone_url (str): The URL used for cloning the repository, ending with '.git'. + base (str): The base branch or commit. + head (str): The head branch or commit. + head_repo_name (str, optional): The name of the repository for the head branch. If not provided, assumes the same repository. + + Returns: + str: The URL for comparing changes between the specified branches or commits. + """ + url = clone_url.removesuffix(".git") + f"/compare/{base}..." + if head_repo_name: + url += head_repo_name.replace("/", ":") + url += ":" + head + return url + + @staticmethod + def create_gitlab_merge_request_url(clone_url: str, head: str) -> str: + """ + Create a URL for creating a new merge request on GitLab. + + Args: + clone_url (str): The URL used for cloning the repository, ending with '.git'. + head (str): The name of the branch to be merged. + + Returns: + str: The URL for creating a new merge request for the specified branch. + """ + return ( + clone_url.removesuffix(".git") + + "/-/merge_requests/new?merge_request%5Bsource_branch%5D=" + + quote(head, safe="") + ) diff --git a/tests/metagpt/tools/libs/test_git.py b/tests/metagpt/tools/libs/test_git.py index c96a3eb46..f200b900e 100644 --- a/tests/metagpt/tools/libs/test_git.py +++ b/tests/metagpt/tools/libs/test_git.py @@ -91,6 +91,30 @@ async def test_new_pr(): assert pr +@pytest.mark.skip +@pytest.mark.asyncio +async def test_new_pr1(): + body = """ + >>> SUMMARY + >>> Change HTTP library used to send requests + >>> + >>> TESTS + >>> - [x] Send 'GET' request + >>> - [x] Send 'POST' request with/without body + """ + pr = await GitRepository.create_pull( + head_repo_name="iorisa/MetaGPT", + head="fixbug/vscode", + base_repo_name="send18/MetaGPT", + base="dev", + title="Test pr", + body=body, + access_token=await get_env(key="access_token", app_name="github"), + ) + print(pr) + assert pr + + @pytest.mark.skip @pytest.mark.asyncio async def test_auth(): @@ -124,6 +148,7 @@ async def test_github(context): assert pr +@pytest.mark.skip @pytest.mark.asyncio @pytest.mark.parametrize( "content",