From 050b018f92579489d29d678b43f3b111b755ab1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 25 Apr 2024 20:57:55 +0800 Subject: [PATCH] feat: +git create pull, create issue --- metagpt/tools/libs/git.py | 148 +++++++++++++++++++++++++++ metagpt/utils/git_repository.py | 62 ++++++++--- tests/metagpt/tools/libs/test_git.py | 24 ++++- 3 files changed, 218 insertions(+), 16 deletions(-) diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index afbcb8b0b..11b6c52b7 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -3,6 +3,10 @@ from __future__ import annotations from pathlib import Path +from typing import Optional + +from github.Issue import Issue +from github.PullRequest import PullRequest from metagpt.tools.tool_registry import register_tool from metagpt.utils.git_repository import GitRepository @@ -63,3 +67,147 @@ async def git_checkout(repo_dir: str | Path, commit_id: str): if not repo.is_valid: ValueError(f"Invalid git root: {repo_dir}") await repo.checkout(commit_id) + + +@register_tool(tags=["git"]) +async def git_create_pull_request( + access_token: str, + base: str, + head: str, + base_repo_name: str, + head_repo_name: Optional[str] = None, + title: Optional[str] = None, + body: Optional[str] = None, +) -> PullRequest: + """ + Creates a pull request in a Git repository. + + Args: + access_token (str): The access token for authentication. + base (str): The name of the base branch of the pull request (e.g., 'main', 'master'). + head (str): The name of the head branch of the pull request (e.g., 'feature-branch'). + base_repo_name (str): The full repository name (user/repo) where the pull request will be created. + head_repo_name (Optional[str], optional): The full repository name (user/repo) where the pull request will merge from. Defaults to None. + title (Optional[str]): The title of the pull request. + body (Optional[str]): The body of the pull request. + + + Returns: + PullRequest: The created pull request object. + + Raises: + ValueError: If `access_token` is invalid. Visit: "https://github.com/settings/tokens" + Any exceptions that might occur during the pull request creation process. + + Note: + This function is intended to be used in an asynchronous context (with `await`). + + Example: + >>> # Merge Request + >>> repo_name = "user/repo" # "user/repo" for example: "https://github.com/user/repo.git" + >>> base = "master" # branch that merge to + >>> head = "feature/new_feature" # branch that merge from + >>> title = "Implement new feature" + >>> body = "This pull request adds functionality X, Y, and Z." + >>> pull_request = await git_create_pull_request( + repo_name=repo_name, + base=base, + head=head, + title=title, + body=body, + access_token=get_env("git_access_token") + ) + >>> print(pull_request) + PullRequest(title="Implement new feature", number=26) + + >>> # Pull Request + >>> base_repo_name = "user1/repo1" # for example: "user1/repo1" from "https://github.com/user1/repo1.git" + >>> head_repo_name = "user2/repo2" # for example: "user2/repo2" from "https://github.com/user2/repo2.git" + >>> base = "master" # branch that merge to + >>> head = "feature/new_feature" # branch that merge from + >>> title = "Implement new feature" + >>> body = "This pull request adds functionality X, Y, and Z." + >>> pull_request = await git_create_pull_request( + base_repo_name=base_repo_name, + head_repo_name=head_repo_name, + base=base, + head=head, + title=title, + body=body, + access_token=get_env("git_access_token") + ) + >>> print(pull_request) + PullRequest(title="Implement new feature", number=26) + + """ + return await GitRepository.create_pull( + base_repo_name=base_repo_name, + head_repo_name=head_repo_name, + base=base, + head=head, + title=title, + body=body, + access_token=access_token, + ) + + +@register_tool(tags=["git"]) +async def create_issue( + access_token: str, + repo_name: str, + title: str, + body: Optional[str] = None, + assignee: Optional[str] = None, + labels: Optional[list[str]] = None, +) -> Issue: + """ + Creates an issue in the specified repository. + + Args: + access_token (str): The access token for authentication. + Visit `https://github.com/settings/tokens` to obtain a personal access token. + For more authentication options, visit: `https://pygithub.readthedocs.io/en/latest/examples/Authentication.html` + repo_name (str): The full repository name (user/repo) where the issue will be created. + title (str): The title of the issue. + body (Optional[str], optional): The body of the issue. Defaults to None. + assignee (Optional[str], optional): The username of the assignee for the issue. Defaults to None. + labels (Optional[list[str]], optional): A list of label names to associate with the issue. Defaults to None. + + + Returns: + Issue: The created issue object. + + Example: + >>> # Create an issue with title and body + >>> repo_name = "username/repository" + >>> title = "Bug Report" + >>> body = "I found a bug in the application." + >>> issue = await create_issue( + repo_name=repo_name, + title=title, + body=body, + access_token=get_env("git_access_token") + ) + >>> print(issue) + Issue(title="Bug Report", number=26) + + >>> # Create an issue with title, body, assignee, and labels + >>> repo_name = "username/repository" + >>> title = "Bug Report" + >>> body = "I found a bug in the application." + >>> assignee = "john_doe" + >>> labels = ["enhancement", "help wanted"] + >>> issue = await create_issue( + repo_name=repo_name, + title=title, + body=body, + assignee=assigee, + labels=labels, + access_token=get_env("git_access_token") + ) + >>> print(issue) + Issue(title="Bug Report", number=26) + """ + return await GitRepository.create_issue( + repo_name=repo_name, title=title, body=body, assignee=assignee, labels=labels, access_token=access_token + ) diff --git a/metagpt/utils/git_repository.py b/metagpt/utils/git_repository.py index 569bb3413..4ca166b73 100644 --- a/metagpt/utils/git_repository.py +++ b/metagpt/utils/git_repository.py @@ -339,9 +339,10 @@ class GitRepository: @staticmethod async def create_pull( - repo_name: str, base: str, head: str, + base_repo_name: str, + head_repo_name: Optional[str] = None, *, title: Optional[str] = None, body: Optional[str] = None, @@ -355,9 +356,10 @@ class GitRepository: Creates a pull request in the specified repository. Args: - repo_name (str): The full repository name (user/repo) where the pull request will be created. base (str): The name of the base branch. head (str): The name of the head branch. + base_repo_name (str): The full repository name (user/repo) where the pull request will be created. + head_repo_name (Optional[str], optional): The full repository name (user/repo) where the pull request will merge from. Defaults to None. title (Optional[str], optional): The title of the pull request. Defaults to None. body (Optional[str], optional): The body of the pull request. Defaults to None. maintainer_can_modify (Optional[bool], optional): Whether maintainers can modify the pull request. Defaults to None. @@ -378,23 +380,37 @@ class GitRepository: raise ValueError('`access_token` is invalid. Visit: "https://github.com/settings/tokens"') auth = auth or Auth.Token(access_token) g = Github(auth=auth) - repo = g.get_repo(repo_name) - x_ratelimit_remaining = repo.raw_headers.get("x-ratelimit-remaining") + 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() - pr = repo.create_pull( - base=base, - head=head, - title=title, - body=body, - maintainer_can_modify=maintainer_can_modify, - draft=draft, - issue=issue, - ) + 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: + 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, + ) return pr @staticmethod @@ -453,3 +469,23 @@ class GitRepository: assignees=assignees, ) return issue + + @staticmethod + async def get_repos(access_token: Optional[str] = None, auth: Optional[Auth] = None) -> List[str]: + """ + Fetches a list of public repositories belonging to the authenticated user. + + Args: + access_token (Optional[str], optional): The access token for authentication. Defaults to None. + Visit `https://github.com/settings/tokens` for obtaining a personal access token. + auth (Optional[Auth], optional): The authentication method. Defaults to None. + Visit `https://pygithub.readthedocs.io/en/latest/examples/Authentication.html` for more information. + + Returns: + List[str]: A list of full names of the public repositories belonging to the user. + """ + auth = auth or Auth.Token(access_token) + git = Github(auth=auth) + user = git.get_user() + v = user.get_repos(visibility="public") + return [i.full_name for i in v] diff --git a/tests/metagpt/tools/libs/test_git.py b/tests/metagpt/tools/libs/test_git.py index 232a96bac..a20a0c545 100644 --- a/tests/metagpt/tools/libs/test_git.py +++ b/tests/metagpt/tools/libs/test_git.py @@ -49,18 +49,22 @@ def test_login(): @pytest.mark.skip -def test_new_issue(): +@pytest.mark.asyncio +async def test_new_issue(): issue = await GitRepository.create_issue( repo_name="iorisa/MetaGPT", title="This is a new issue", body="This is the issue body", access_token=get_env("GITHUB_PERSONAL_ACCESS_TOKEN"), ) + print(issue) assert issue.number + pass @pytest.mark.skip -def test_new_pr(): +@pytest.mark.asyncio +async def test_new_pr(): body = """ >>> SUMMARY >>> Change HTTP library used to send requests @@ -72,13 +76,27 @@ def test_new_pr(): pr = await GitRepository.create_pull( repo_name="iorisa/MetaGPT", base="send18", - head="featur/intent_detect", + head="fixbug/gbk", title="Test pr", body=body, access_token=get_env("GITHUB_PERSONAL_ACCESS_TOKEN"), ) + print(pr) assert pr +@pytest.mark.skip +def test_auth(): + access_token = get_env("GITHUB_PERSONAL_ACCESS_TOKEN") + auth = Auth.Token(access_token) + g = Github(auth=auth) + u = g.get_user() + v = u.get_repos(visibility="public") + a = [i.full_name for i in v] + assert a + print(a) + pass + + if __name__ == "__main__": pytest.main([__file__, "-s"])