Support cloning repo

Support forcing accurate token calculation (claude)
Help docs: Add desired branch in case of user supplied git repo, with default set to "main"
Better documentation for getting canonical url parts
This commit is contained in:
Eyal Sharon
2025-03-23 09:55:58 +02:00
parent 5e7e353670
commit dd80276f3f
13 changed files with 263 additions and 47 deletions

View File

@ -1,5 +1,8 @@
from abc import ABC, abstractmethod
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
import os
import shutil
import subprocess
from typing import Optional, Tuple
from pr_agent.algo.types import FilePatchInfo
@ -20,13 +23,69 @@ class GitProvider(ABC):
return ""
# Given a git repo url, return prefix and suffix of the provider in order to view a given file belonging to that repo. Needs to be implemented by the provider.
# For example: For a git: https://git_provider.com/MY_PROJECT/MY_REPO.git then it should return ('https://git_provider.com/projects/MY_PROJECT/repos/MY_REPO', '?=<SOME HEADER>')
# so that to properly view the file: docs/readme.md -> <PREFIX>/docs/readme.md<SUFFIX> -> https://git_provider.com/projects/MY_PROJECT/repos/MY_REPO/docs/readme.md?=<SOME HEADER>)
# For example: For a git: https://git_provider.com/MY_PROJECT/MY_REPO.git and desired branch: <MY_BRANCH> then it should return ('https://git_provider.com/projects/MY_PROJECT/repos/MY_REPO/.../<MY_BRANCH>', '?=<SOME HEADER>')
# so that to properly view the file: docs/readme.md -> <PREFIX>/docs/readme.md<SUFFIX> -> https://git_provider.com/projects/MY_PROJECT/repos/MY_REPO/<MY_BRANCH>/docs/readme.md?=<SOME HEADER>)
def get_canonical_url_parts(self, repo_git_url:str, desired_branch:str) -> Tuple[str, str]:
get_logger().warning("Not implemented! Returning empty prefix and suffix")
return ("", "")
#Clone related API
#An object which ensures deletion of a cloned repo, once it becomes out of scope.
# Example usage:
# with TemporaryDirectory() as tmp_dir:
# returned_obj: GitProvider.ScopedClonedRepo = self.git_provider.clone(self.repo_url, tmp_dir, remove_dest_folder=False)
# print(returned_obj.path) #Use returned_obj.path.
# #From this point, returned_obj.path may be deleted at any point and therefore must not be used.
class ScopedClonedRepo(object):
def __init__(self, dest_folder):
self.path = dest_folder
def __del__(self):
if self.path and os.path.exists(self.path):
shutil.rmtree(self.path, ignore_errors=True)
@abstractmethod
#Method to allow implementors to manipulate the repo url to clone (such as embedding tokens in the url string).
def _prepare_clone_url_with_token(self, repo_url_to_clone: str) -> str | None:
pass
# Does a shallow clone, using a forked process to support a timeout guard.
# In case operation has failed, it is expected to throw an exception as this method does not return a value.
def _clone_inner(self, repo_url: str, dest_folder: str, operation_timeout_in_seconds: int=None) -> None:
#The following ought to be equivalent to:
# #Repo.clone_from(repo_url, dest_folder)
# , but with throwing an exception upon timeout.
# Note: This can only be used in context that supports using pipes.
subprocess.run([
"git", "clone",
"--filter=blob:none",
"--depth", "1",
repo_url, dest_folder
], check=True, # check=True will raise an exception if the command fails
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=operation_timeout_in_seconds)
CLONE_TIMEOUT_SEC = 20
# Clone a given url to a destination folder. If successful, returns an object that wraps the destination folder,
# deleting it once it is garbage collected. See: GitProvider.ScopedClonedRepo for more details.
def clone(self, repo_url_to_clone: str, dest_folder: str, remove_dest_folder: bool = True,
operation_timeout_in_seconds: int=CLONE_TIMEOUT_SEC) -> ScopedClonedRepo|None:
returned_obj = None
clone_url = self._prepare_clone_url_with_token(repo_url_to_clone)
if not clone_url:
get_logger().error("Clone failed: Unable to obtain url to clone.")
return returned_obj
try:
if remove_dest_folder and os.path.exists(dest_folder) and os.path.isdir(dest_folder):
shutil.rmtree(dest_folder)
self._clone_inner(clone_url, dest_folder, operation_timeout_in_seconds)
returned_obj = GitProvider.ScopedClonedRepo(dest_folder)
except Exception as e:
get_logger().exception(f"Clone failed: Could not clone url.",
artifact={"error": str(e), "url": clone_url, "dest_folder": dest_folder})
finally:
return returned_obj
@abstractmethod
def get_files(self) -> list:
pass