Skip to content

chore: [CG-10339] support codebase create_pr #420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions src/codegen/git/repo_operator/local_repo_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
from typing import Self, override

from codeowners import CodeOwners as CodeOwnersParser
from git import Remote
from git import Repo as GitCLI
from git.remote import PushInfoList
from github import Github
from github.PullRequest import PullRequest
from github.Repository import Repository

from codegen.git.clients.git_repo_client import GitRepoClient
from codegen.git.repo_operator.repo_operator import RepoOperator
from codegen.git.schemas.enums import FetchResult
from codegen.git.schemas.repo_config import RepoConfig
from codegen.git.utils.clone_url import url_to_github
from codegen.git.utils.file_utils import create_files
from codegen.shared.configs.config import config

logger = logging.getLogger(__name__)

Expand All @@ -41,7 +41,7 @@ def __init__(
github_api_key: str | None = None,
bot_commit: bool = False,
) -> None:
self._github_api_key = github_api_key
self._github_api_key = github_api_key or config.secrets.github_token
self._remote_git_repo = None
super().__init__(repo_config, bot_commit)
os.makedirs(self.repo_path, exist_ok=True)
Expand All @@ -52,7 +52,7 @@ def __init__(
####################################################################################################################

@property
def remote_git_repo(self) -> GitRepoClient:
def remote_git_repo(self) -> Repository:
if self._remote_git_repo is None:
if not self._github_api_key:
return None
Expand Down Expand Up @@ -173,10 +173,6 @@ def base_url(self) -> str | None:
if remote := next(iter(self.git_cli.remotes), None):
return url_to_github(remote.url, self.get_active_branch_or_commit())

@override
def push_changes(self, remote: Remote | None = None, refspec: str | None = None, force: bool = False) -> PushInfoList:
raise OperatorIsLocal()

@override
def pull_repo(self) -> None:
"""Pull the latest commit down to an existing local repo"""
Expand Down
40 changes: 1 addition & 39 deletions src/codegen/git/repo_operator/remote_repo_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from typing import override

from codeowners import CodeOwners as CodeOwnersParser
from git import GitCommandError, Remote
from git.remote import PushInfoList
from git import GitCommandError

from codegen.git.clients.git_repo_client import GitRepoClient
from codegen.git.repo_operator.repo_operator import RepoOperator
Expand Down Expand Up @@ -59,7 +58,7 @@
@property
def remote_git_repo(self) -> GitRepoClient:
if not self._remote_git_repo:
self._remote_git_repo = GitRepoClient(self.repo_config, access_token=self.access_token)

Check failure on line 61 in src/codegen/git/repo_operator/remote_repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument "access_token" to "GitRepoClient" has incompatible type "str | None"; expected "str" [arg-type]
return self._remote_git_repo

@property
Expand All @@ -69,7 +68,7 @@
return self._default_branch

@property
def codeowners_parser(self) -> CodeOwnersParser | None:

Check failure on line 71 in src/codegen/git/repo_operator/remote_repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Signature of "codeowners_parser" incompatible with supertype "RepoOperator" [override]
if not self._codeowners_parser:
self._codeowners_parser = create_codeowners_parser_for_repo(self.remote_git_repo)
return self._codeowners_parser
Expand Down Expand Up @@ -165,45 +164,8 @@
"""
return self.checkout_branch(branch_name, remote_name=remote_name, remote=True, create_if_missing=False)

@stopwatch
def push_changes(self, remote: Remote | None = None, refspec: str | None = None, force: bool = False) -> PushInfoList:
"""Push the changes to the given refspec of the remote.

Args:
refspec (str | None): refspec to push. If None, the current active branch is used.
remote (Remote | None): Remote to push too. Defaults to 'origin'.
force (bool): If True, force push the changes. Defaults to False.
"""
# Use default remote if not provided
if not remote:
remote = self.git_cli.remote(name="origin")

# Use the current active branch if no branch is specified
if not refspec:
# TODO: doesn't work with detached HEAD state
refspec = self.git_cli.active_branch.name

res = remote.push(refspec=refspec, force=force, progress=CustomRemoteProgress())
for push_info in res:
if push_info.flags & push_info.ERROR:
# Handle the error case
logger.warning(f"Error pushing {refspec}: {push_info.summary}")
elif push_info.flags & push_info.FAST_FORWARD:
# Successful fast-forward push
logger.info(f"{refspec} pushed successfully (fast-forward).")
elif push_info.flags & push_info.NEW_HEAD:
# Successful push of a new branch
logger.info(f"{refspec} pushed successfully as a new branch.")
elif push_info.flags & push_info.NEW_TAG:
# Successful push of a new tag (if relevant)
logger.info("New tag pushed successfully.")
else:
# Successful push, general case
logger.info(f"{refspec} pushed successfully.")
return res

@cached_property
def base_url(self) -> str | None:

Check failure on line 168 in src/codegen/git/repo_operator/remote_repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Signature of "base_url" incompatible with supertype "RepoOperator" [override]
repo_config = self.repo_config
clone_url = get_clone_url_for_repo_config(repo_config)
branch = self.get_active_branch_or_commit()
Expand Down
45 changes: 42 additions & 3 deletions src/codegen/git/repo_operator/repo_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from codegen.git.configs.constants import CODEGEN_BOT_EMAIL, CODEGEN_BOT_NAME
from codegen.git.schemas.enums import CheckoutResult, FetchResult
from codegen.git.schemas.repo_config import RepoConfig
from codegen.git.utils.remote_progress import CustomRemoteProgress
from codegen.shared.performance.stopwatch_utils import stopwatch
from codegen.shared.performance.time_utils import humanize_duration

Expand Down Expand Up @@ -97,7 +98,7 @@
email_level = None
levels = ["system", "global", "user", "repository"]
for level in levels:
with git_cli.config_reader(level) as reader:

Check failure on line 101 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 to "config_reader" of "Repo" has incompatible type "str"; expected "Literal['system', 'global', 'user', 'repository'] | None" [arg-type]
if reader.has_option("user", "name") and not username:
username = reader.get("user", "name")
user_level = level
Expand Down Expand Up @@ -137,7 +138,17 @@

@property
def default_branch(self) -> str:
return self._default_branch or self.git_cli.active_branch.name
# Priority 1: If default branch has been set
if self._default_branch:
return self._default_branch

# Priority 2: If origin/HEAD ref exists
origin_prefix = "origin"
if f"{origin_prefix}/HEAD" in self.git_cli.refs:
return self.git_cli.refs[f"{origin_prefix}/HEAD"].reference.name.removeprefix(f"{origin_prefix}/")

# Priority 3: Fallback to the active branch
return self.git_cli.active_branch.name

@abstractmethod
def codeowners_parser(self) -> CodeOwnersParser | None: ...
Expand Down Expand Up @@ -372,14 +383,42 @@
logger.info("No changes to commit. Do nothing.")
return False

@abstractmethod
@stopwatch
def push_changes(self, remote: Remote | None = None, refspec: str | None = None, force: bool = False) -> PushInfoList:
"""Push the changes to the given refspec of the remote repository.
"""Push the changes to the given refspec of the remote.

Args:
refspec (str | None): refspec to push. If None, the current active branch is used.
remote (Remote | None): Remote to push too. Defaults to 'origin'.
force (bool): If True, force push the changes. Defaults to False.
"""
# Use default remote if not provided
if not remote:
remote = self.git_cli.remote(name="origin")

# Use the current active branch if no branch is specified
if not refspec:
# TODO: doesn't work with detached HEAD state
refspec = self.git_cli.active_branch.name

res = remote.push(refspec=refspec, force=force, progress=CustomRemoteProgress())
for push_info in res:
if push_info.flags & push_info.ERROR:
# Handle the error case
logger.warning(f"Error pushing {refspec}: {push_info.summary}")
elif push_info.flags & push_info.FAST_FORWARD:
# Successful fast-forward push
logger.info(f"{refspec} pushed successfully (fast-forward).")
elif push_info.flags & push_info.NEW_HEAD:
# Successful push of a new branch
logger.info(f"{refspec} pushed successfully as a new branch.")
elif push_info.flags & push_info.NEW_TAG:
# Successful push of a new tag (if relevant)
logger.info("New tag pushed successfully.")
else:
# Successful push, general case
logger.info(f"{refspec} pushed successfully.")
return res

def relpath(self, abspath) -> str:
# TODO: check if the path is an abspath (i.e. contains self.repo_path)
Expand Down Expand Up @@ -420,7 +459,7 @@
return content
except UnicodeDecodeError:
print(f"Warning: Unable to decode file {file_path}. Skipping.")
return None

Check failure on line 462 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible return value type (got "None", expected "str") [return-value]

def write_file(self, relpath: str, content: str) -> None:
"""Writes file content to disk"""
Expand Down Expand Up @@ -465,7 +504,7 @@

# Iterate through files and yield contents
for rel_filepath in filepaths:
rel_filepath: str

Check failure on line 507 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Name "rel_filepath" already defined on line 506 [no-redef]
filepath = os.path.join(self.repo_path, rel_filepath)

# Filter by subdirectory (includes full filenames)
Expand Down Expand Up @@ -496,7 +535,7 @@
list_files = []

for rel_filepath in self.git_cli.git.ls_files().split("\n"):
rel_filepath: str

Check failure on line 538 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Name "rel_filepath" already defined on line 537 [no-redef]
if subdirs and not any(d in rel_filepath for d in subdirs):
continue
if extensions is None or any(rel_filepath.endswith(e) for e in extensions):
Expand All @@ -520,7 +559,7 @@

def get_modified_files_in_last_n_days(self, days: int = 1) -> tuple[list[str], list[str]]:
"""Returns a list of files modified and deleted in the last n days"""
modified_files = []

Check failure on line 562 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Need type annotation for "modified_files" (hint: "modified_files: list[<type>] = ...") [var-annotated]
deleted_files = []
allowed_extensions = [".py"]

Expand All @@ -536,9 +575,9 @@
if file in modified_files:
modified_files.remove(file)
else:
if file not in modified_files and file[-3:] in allowed_extensions:

Check failure on line 578 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Value of type "str | PathLike[str]" is not indexable [index]
modified_files.append(file)
return modified_files, deleted_files

Check failure on line 580 in src/codegen/git/repo_operator/repo_operator.py

View workflow job for this annotation

GitHub Actions / mypy

error: Incompatible return value type (got "tuple[list[str | PathLike[str]], list[str | PathLike[str]]]", expected "tuple[list[str], list[str]]") [return-value]

@abstractmethod
def base_url(self) -> str | None: ...
Expand Down
14 changes: 14 additions & 0 deletions src/codegen/sdk/core/codebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from git import Commit as GitCommit
from git import Diff
from git.remote import PushInfoList
from github.PullRequest import PullRequest
from networkx import Graph
from rich.console import Console
from typing_extensions import deprecated
Expand Down Expand Up @@ -872,6 +873,19 @@ def restore_stashed_changes(self):
"""Restore the most recent stash in the codebase."""
self._op.stash_pop()

####################################################################################################################
# GITHUB
####################################################################################################################

def create_pr(self, title: str, body: str) -> PullRequest:
"""Creates a PR from the current branch."""
if self._op.git_cli.head.is_detached:
msg = "Cannot make a PR from a detached HEAD"
raise ValueError(msg)
self._op.stage_and_commit_all_changes(message=title)
self._op.push_changes()
return self._op.remote_git_repo.create_pull(head=self._op.git_cli.active_branch.name, base=self._op.default_branch, title=title, body=body)

####################################################################################################################
# GRAPH VISUALIZATION
####################################################################################################################
Expand Down
Loading