From 8eb291f918afffef2c579aa8ff4b0fa313d1099d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 11 May 2024 10:44:52 +0800 Subject: [PATCH 1/4] feat: create_pull_url --- metagpt/tools/libs/git.py | 7 ++-- metagpt/utils/git_repository.py | 52 +++++++++++++++++++++------- tests/metagpt/tools/libs/test_git.py | 34 ++++++++++++++++++ 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index aa701e49b..74d427e24 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -175,9 +175,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") - + >>> elif 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:dev Returns: PullRequest: The created pull request. diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index 3c45c4085..808ad3737 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -14,7 +14,7 @@ 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 git.repo import Repo from git.repo.fun import is_git_dir @@ -456,7 +456,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. @@ -505,17 +505,26 @@ class GitRepository: 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, - ) + base_branch = base_repo.get_branch(base) + head_branch = head_repo.get_branch(head) + try: + pr = base_repo.create_pull( + base=base_branch.name, + head=f"{head_repo.full_name}:{head_branch.name}", + 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_pull_url( + clone_url=base_repo.clone_url, + base=base_branch.name, + head=head_branch.name, + head_repo_name=head_repo.full_name, + ) return pr @staticmethod @@ -594,3 +603,20 @@ class GitRepository: user = git.get_user() v = user.get_repos(visibility="public") return [i.full_name for i in v] + + @staticmethod + def create_pull_url(clone_url: str, base: str, head: str, head_repo_name: 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): 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}..." + head_repo_name.replace("/", ":") + ":" + head + return url diff --git a/tests/metagpt/tools/libs/test_git.py b/tests/metagpt/tools/libs/test_git.py index c96a3eb46..27ac3c234 100644 --- a/tests/metagpt/tools/libs/test_git.py +++ b/tests/metagpt/tools/libs/test_git.py @@ -91,6 +91,39 @@ 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( + base_repo_name="iorisa/MetaGPT", + base="fixbug/vscode", + head_repo_name="send18/MetaGPT", + head="dev", + title="Test pr", + body=body, + access_token=await get_env(key="access_token", app_name="github"), + ) + # 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 +157,7 @@ async def test_github(context): assert pr +@pytest.mark.skip @pytest.mark.asyncio @pytest.mark.parametrize( "content", From 97677f2ccaf3a74dd8274b65f912099f4810c9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 11 May 2024 11:50:08 +0800 Subject: [PATCH 2/4] feat: create_github_pull_url --- metagpt/tools/libs/git.py | 12 ++-- metagpt/utils/git_repository.py | 89 +++++++++++++++++----------- tests/metagpt/tools/libs/test_git.py | 19 ++---- 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index 74d427e24..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" @@ -178,9 +182,9 @@ async def git_create_pull( >>> if isinstance(pr, PullRequest): >>> print(pr) PullRequest("feat: modify http lib") - >>> elif 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:dev + >>> 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 808ad3737..a078efe59 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -15,6 +15,7 @@ from enum import Enum from pathlib import Path from subprocess import TimeoutExpired 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 @@ -483,31 +484,26 @@ 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: - pr = base_repo.create_pull( - base=base, - head=head, - title=title, - body=body, - maintainer_can_modify=maintainer_can_modify, - draft=draft, - issue=issue, - ) - else: - base_branch = base_repo.get_branch(base) - head_branch = head_repo.get_branch(head) - try: + 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 not head_repo: + pr = base_repo.create_pull( + base=base, + head=head, + title=title, + body=body, + maintainer_can_modify=maintainer_can_modify, + draft=draft, + issue=issue, + ) + else: + base_branch = base_repo.get_branch(base) + head_branch = head_repo.get_branch(head) pr = base_repo.create_pull( base=base_branch.name, head=f"{head_repo.full_name}:{head_branch.name}", @@ -517,14 +513,14 @@ class GitRepository: draft=draft, issue=issue, ) - except Exception as e: - logger.warning(f"Pull Request Error: {e}") - return GitRepository.create_pull_url( - clone_url=base_repo.clone_url, - base=base_branch.name, - head=head_branch.name, - head_repo_name=head_repo.full_name, - ) + 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 @staticmethod @@ -605,7 +601,7 @@ class GitRepository: return [i.full_name for i in v] @staticmethod - def create_pull_url(clone_url: str, base: str, head: str, head_repo_name: str = None) -> str: + 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. @@ -613,10 +609,31 @@ class GitRepository: 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): The name of the repository for the head branch. If not provided, assumes the same repository. + 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}..." + head_repo_name.replace("/", ":") + ":" + head + 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 27ac3c234..ad843a8a3 100644 --- a/tests/metagpt/tools/libs/test_git.py +++ b/tests/metagpt/tools/libs/test_git.py @@ -68,7 +68,6 @@ async def test_new_issue(): pass -@pytest.mark.skip @pytest.mark.asyncio async def test_new_pr(): body = """ @@ -91,7 +90,6 @@ async def test_new_pr(): assert pr -# @pytest.mark.skip @pytest.mark.asyncio async def test_new_pr1(): body = """ @@ -103,23 +101,14 @@ async def test_new_pr1(): >>> - [x] Send 'POST' request with/without body """ pr = await GitRepository.create_pull( - base_repo_name="iorisa/MetaGPT", - base="fixbug/vscode", - head_repo_name="send18/MetaGPT", - head="dev", + 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"), ) - # 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 From eff9f7e456ad044728e654d03314bdb14c5a582a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 11 May 2024 14:38:58 +0800 Subject: [PATCH 3/4] fixbug: pull request between fork --- metagpt/utils/git_repository.py | 76 +++++++++++++++++++++++++++- tests/metagpt/tools/libs/test_git.py | 2 + 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index a078efe59..ff4710e3d 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -17,6 +17,7 @@ from subprocess import TimeoutExpired from typing import Dict, List, Optional, Union from urllib.parse import quote +import aiohttp from git.repo import Repo from git.repo.fun import is_git_dir from github import Auth, BadCredentialsException, Github @@ -504,14 +505,18 @@ class GitRepository: else: base_branch = base_repo.get_branch(base) head_branch = head_repo.get_branch(head) - pr = base_repo.create_pull( + pr = await GitRepository.post_github_pull_request( base=base_branch.name, - head=f"{head_repo.full_name}:{head_branch.name}", + head=head_branch.name, + base_repo_name=base_repo.full_name, + head_repo_name=head_repo.full_name, title=title, body=body, maintainer_can_modify=maintainer_can_modify, draft=draft, issue=issue, + access_token=access_token, + auth=auth, ) except Exception as e: logger.warning(f"Pull Request Error: {e}") @@ -523,6 +528,73 @@ class GitRepository: ) return pr + @staticmethod + async def post_github_pull_request( + base: str, + head: str, + base_repo_name: str, + head_repo_name: Optional[str] = None, + *, + title: Optional[str] = None, + body: Optional[str] = None, + maintainer_can_modify: Optional[bool] = None, + draft: Optional[bool] = None, + issue: Optional[Issue] = None, + access_token: Optional[str] = None, + auth: Optional[Auth] = None, + ): + """ + Posts a pull request to GitHub. + + Args: + base (str): The name of the base branch (e.g., 'main'). + head (str): The name of the head branch (e.g., 'feature-branch'). + base_repo_name (str): The name of the base repository (e.g., 'username/repository'). + head_repo_name (Optional[str]): The name of the head repository. Defaults to None. + title (Optional[str]): The title of the pull request. Defaults to None. + body (Optional[str]): The body of the pull request. Defaults to None. + maintainer_can_modify (Optional[bool]): Whether maintainers can modify the pull request. Defaults to None. + draft (Optional[bool]): Whether the pull request is a draft. Defaults to None. + issue (Optional[Issue]): The issue associated with the pull request. Defaults to None. + access_token (Optional[str]): The access token for authenticating with GitHub. Defaults to None. + auth (Optional[Auth]): The authentication method. Defaults to None. + + Returns: + PullRequest: The created pull request object. + """ + url = f"https://api.github.com/repos/{base_repo_name}/pulls" + auth = auth or Auth.Token(access_token) + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {auth.token}", + "Content-Type": "application/json", + } + head_repo_name = head_repo_name.split("/")[0] if head_repo_name else "" + data = { + "title": title or "", + "body": body or "", + "head": f"{head_repo_name}:{head}" if head_repo_name else head, + "base": base, + } + if maintainer_can_modify is not None and maintainer_can_modify != NotSet: + data["maintainer_can_modify"] = maintainer_can_modify + if draft is not None and draft != NotSet: + data["draft"] = draft + if issue is not None and issue != NotSet: + data["issue"] = issue + + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, json=data) as response: + if response.status == 201: + response_json = await response.json() + else: + raise ValueError(f"{response.status}:{response.content}") + g = Github(auth=auth) + repo = g.get_repo(base_repo_name) + pull_request_number = response_json["number"] + pull_request = repo.get_pull(pull_request_number) + return pull_request + @staticmethod async def create_issue( repo_name: str, diff --git a/tests/metagpt/tools/libs/test_git.py b/tests/metagpt/tools/libs/test_git.py index ad843a8a3..f200b900e 100644 --- a/tests/metagpt/tools/libs/test_git.py +++ b/tests/metagpt/tools/libs/test_git.py @@ -68,6 +68,7 @@ async def test_new_issue(): pass +@pytest.mark.skip @pytest.mark.asyncio async def test_new_pr(): body = """ @@ -90,6 +91,7 @@ async def test_new_pr(): assert pr +@pytest.mark.skip @pytest.mark.asyncio async def test_new_pr1(): body = """ From 1dbcf4e1a24d26f573e61dc389ce0870a10fd0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 11 May 2024 15:01:48 +0800 Subject: [PATCH 4/4] fixbug: pull request between fork --- metagpt/utils/git_repository.py | 106 ++++---------------------------- 1 file changed, 12 insertions(+), 94 deletions(-) diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index ff4710e3d..8fc52579a 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -17,7 +17,6 @@ from subprocess import TimeoutExpired from typing import Dict, List, Optional, Union from urllib.parse import quote -import aiohttp from git.repo import Repo from git.repo.fun import is_git_dir from github import Auth, BadCredentialsException, Github @@ -492,32 +491,18 @@ class GitRepository: 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 not head_repo: - pr = base_repo.create_pull( - base=base, - head=head, - title=title, - body=body, - maintainer_can_modify=maintainer_can_modify, - draft=draft, - issue=issue, - ) - else: - base_branch = base_repo.get_branch(base) - head_branch = head_repo.get_branch(head) - pr = await GitRepository.post_github_pull_request( - base=base_branch.name, - head=head_branch.name, - base_repo_name=base_repo.full_name, - head_repo_name=head_repo.full_name, - title=title, - body=body, - maintainer_can_modify=maintainer_can_modify, - draft=draft, - issue=issue, - access_token=access_token, - auth=auth, - ) + if head_repo: + user = head_repo.full_name.split("/")[0] + head = f"{user}:{head}" + pr = base_repo.create_pull( + base=base, + head=head, + 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( @@ -528,73 +513,6 @@ class GitRepository: ) return pr - @staticmethod - async def post_github_pull_request( - base: str, - head: str, - base_repo_name: str, - head_repo_name: Optional[str] = None, - *, - title: Optional[str] = None, - body: Optional[str] = None, - maintainer_can_modify: Optional[bool] = None, - draft: Optional[bool] = None, - issue: Optional[Issue] = None, - access_token: Optional[str] = None, - auth: Optional[Auth] = None, - ): - """ - Posts a pull request to GitHub. - - Args: - base (str): The name of the base branch (e.g., 'main'). - head (str): The name of the head branch (e.g., 'feature-branch'). - base_repo_name (str): The name of the base repository (e.g., 'username/repository'). - head_repo_name (Optional[str]): The name of the head repository. Defaults to None. - title (Optional[str]): The title of the pull request. Defaults to None. - body (Optional[str]): The body of the pull request. Defaults to None. - maintainer_can_modify (Optional[bool]): Whether maintainers can modify the pull request. Defaults to None. - draft (Optional[bool]): Whether the pull request is a draft. Defaults to None. - issue (Optional[Issue]): The issue associated with the pull request. Defaults to None. - access_token (Optional[str]): The access token for authenticating with GitHub. Defaults to None. - auth (Optional[Auth]): The authentication method. Defaults to None. - - Returns: - PullRequest: The created pull request object. - """ - url = f"https://api.github.com/repos/{base_repo_name}/pulls" - auth = auth or Auth.Token(access_token) - headers = { - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {auth.token}", - "Content-Type": "application/json", - } - head_repo_name = head_repo_name.split("/")[0] if head_repo_name else "" - data = { - "title": title or "", - "body": body or "", - "head": f"{head_repo_name}:{head}" if head_repo_name else head, - "base": base, - } - if maintainer_can_modify is not None and maintainer_can_modify != NotSet: - data["maintainer_can_modify"] = maintainer_can_modify - if draft is not None and draft != NotSet: - data["draft"] = draft - if issue is not None and issue != NotSet: - data["issue"] = issue - - async with aiohttp.ClientSession() as session: - async with session.post(url, headers=headers, json=data) as response: - if response.status == 201: - response_json = await response.json() - else: - raise ValueError(f"{response.status}:{response.content}") - g = Github(auth=auth) - repo = g.get_repo(base_repo_name) - pull_request_number = response_json["number"] - pull_request = repo.get_pull(pull_request_number) - return pull_request - @staticmethod async def create_issue( repo_name: str,