mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-06-11 15:15:18 +02:00
feat: +git tools
This commit is contained in:
parent
5ee95b1461
commit
85adc47366
5 changed files with 133 additions and 1 deletions
|
|
@ -47,7 +47,7 @@ class Config(CLIParams, YamlModel):
|
|||
# Key Parameters
|
||||
llm: LLMConfig
|
||||
|
||||
# Global Proxy. Will be used if llm.proxy is not set
|
||||
# Global Proxy. Not used by LLM, but by other tools such as browsers.
|
||||
proxy: str = ""
|
||||
|
||||
# Tool Parameters
|
||||
|
|
|
|||
65
metagpt/tools/libs/git.py
Normal file
65
metagpt/tools/libs/git.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from metagpt.tools.tool_registry import register_tool
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
@register_tool(tags=["git"])
|
||||
async def git_clone(url: str, output_dir: str | Path = None) -> Path:
|
||||
"""
|
||||
Clones a Git repository from the given URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL of the Git repository to clone.
|
||||
output_dir (str or Path, optional): The directory where the repository will be cloned.
|
||||
If not provided, the repository will be cloned into the current working directory.
|
||||
|
||||
Returns:
|
||||
Path: The path to the cloned repository.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified Git root is invalid.
|
||||
|
||||
Example:
|
||||
>>> # git clone to /TO/PATH
|
||||
>>> url = 'https://github.com/geekan/MetaGPT.git'
|
||||
>>> output_dir = "/TO/PATH"
|
||||
>>> repo_dir = await git_clone(url=url, output_dir=output_dir)
|
||||
>>> print(repo_dir)
|
||||
/TO/PATH/MetaGPT
|
||||
|
||||
>>> # git clone to default directory.
|
||||
>>> url = 'https://github.com/geekan/MetaGPT.git'
|
||||
>>> repo_dir = await git_clone(url)
|
||||
>>> print(repo_dir)
|
||||
/WORK_SPACE/downloads/MetaGPT
|
||||
"""
|
||||
repo = await GitRepository.clone_from(url, output_dir)
|
||||
return repo.workdir
|
||||
|
||||
|
||||
async def git_checkout(repo_dir: str | Path, commit_id: str):
|
||||
"""
|
||||
Checks out a specific commit in a Git repository.
|
||||
|
||||
Args:
|
||||
repo_dir (str or Path): The directory containing the Git repository.
|
||||
commit_id (str): The ID of the commit to check out.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified Git root is invalid.
|
||||
|
||||
Example:
|
||||
>>> repo_dir = '/TO/GIT/REPO'
|
||||
>>> commit_id = 'main'
|
||||
>>> await git_checkout(repo_dir=repo_dir, commit_id=commit_id)
|
||||
git checkout main
|
||||
"""
|
||||
repo = GitRepository(local_path=repo_dir, auto_init=False)
|
||||
if not repo.is_valid:
|
||||
ValueError(f"Invalid git root: {repo_dir}")
|
||||
await repo.checkout(commit_id)
|
||||
|
|
@ -9,6 +9,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
|
@ -283,3 +285,36 @@ class GitRepository:
|
|||
continue
|
||||
files.append(filename)
|
||||
return files
|
||||
|
||||
@classmethod
|
||||
async def clone_from(cls, url: str | Path, output_dir: str | Path = None) -> "GitRepository":
|
||||
from metagpt.context import Context
|
||||
|
||||
to_path = Path(output_dir or Path(__file__).parent / f"../../workspace/downloads/{uuid.uuid4().hex}").resolve()
|
||||
to_path.mkdir(parents=True, exist_ok=True)
|
||||
ctx = Context()
|
||||
env = ctx.new_environ()
|
||||
proxy = ["-c", f"http.proxy={ctx.config.proxy}"] if ctx.config.proxy else []
|
||||
command = ["git", "clone"] + proxy + [str(url)]
|
||||
logger.info(" ".join(command))
|
||||
process = subprocess.Popen(command, cwd=str(to_path), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||
try:
|
||||
# Wait for the process to complete, with a timeout
|
||||
stdout, stderr = process.communicate(timeout=600)
|
||||
info = f"{stdout.decode('utf-8')}\n{stderr.decode('utf-8')}"
|
||||
logger.info(info)
|
||||
dir_name = Path(url).with_suffix("").name
|
||||
to_path = to_path / dir_name
|
||||
if not cls.is_git_dir(to_path):
|
||||
raise ValueError(info)
|
||||
logger.info(f"git clone to {to_path}")
|
||||
return GitRepository(local_path=to_path, auto_init=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.info("The command did not complete within the given timeout.")
|
||||
process.kill() # Kill the process if it times out
|
||||
stdout, stderr = process.communicate()
|
||||
raise ValueError(f"{stdout.decode('utf-8')}\n{stderr.decode('utf-8')}")
|
||||
|
||||
async def checkout(self, commit_id: str):
|
||||
self._repository.git.checkout(commit_id)
|
||||
logger.info(f"git checkout {commit_id}")
|
||||
|
|
|
|||
1
tests/data/swe-bench/swe-bench-dev.json
Normal file
1
tests/data/swe-bench/swe-bench-dev.json
Normal file
File diff suppressed because one or more lines are too long
31
tests/metagpt/tools/libs/test_git.py
Normal file
31
tests/metagpt/tools/libs/test_git.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from metagpt.tools.libs.git import git_checkout, git_clone
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
class SWEBenchItem(BaseModel):
|
||||
base_commit: str
|
||||
repo: str
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
["url", "commit_id"], [("https://github.com/sqlfluff/sqlfluff.git", "d19de0ecd16d298f9e3bfb91da122734c40c01e5")]
|
||||
)
|
||||
async def test_git(url: str, commit_id: str):
|
||||
repo_dir = await git_clone(url)
|
||||
assert repo_dir
|
||||
|
||||
await git_checkout(repo_dir, commit_id)
|
||||
|
||||
repo = GitRepository(repo_dir, auto_init=False)
|
||||
repo.delete_repository()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-s"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue