diff --git a/INSTALL.md b/INSTALL.md index 31042b60..4593dccf 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -123,7 +123,7 @@ jobs: OPENAI_KEY: ${{ secrets.OPENAI_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -** if you want to pin your action to a specific commit for stability reasons +** if you want to pin your action to a specific release (v0.7 for example) for stability reasons, use: ```yaml on: pull_request: @@ -140,7 +140,7 @@ jobs: steps: - name: PR Agent action step id: pragent - uses: Codium-ai/pr-agent@ + uses: Codium-ai/pr-agent@v0.7 env: OPENAI_KEY: ${{ secrets.OPENAI_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 44ab208e..af0b5af2 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull See the [usage guide](./Usage.md) for instructions how to run the different tools from [CLI](./Usage.md#working-from-a-local-repo-cli), or by [online usage](./Usage.md#online-usage), as well as additional details on optional commands and configurations. +[Release notes](./RELEASE_NOTES.md) +

Example results:

/describe:

@@ -89,9 +91,8 @@ See the [usage guide](./Usage.md) for instructions how to run the different tool - [Overview](#overview) - [Try it now](#try-it-now) - [Installation](#installation) -- [Usage guide](./Usage.md) - [How it works](#how-it-works) -- [Why use PR-Agent](#why-use-pr-agent) +- [Why use PR-Agent?](#why-use-pr-agent) - [Roadmap](#roadmap) @@ -105,8 +106,8 @@ See the [usage guide](./Usage.md) for instructions how to run the different tool | | Auto-Description | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | -| | Reflect and Review | :white_check_mark: | | :white_check_mark: | | :white_check_mark: | :white_check_mark: | -| | Update CHANGELOG.md | :white_check_mark: | | :white_check_mark: | | | | +| | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: | +| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | | | Find similar issue | :white_check_mark: | | | | | | | | | | | | | | | USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 00000000..bdb0165b --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,25 @@ +## [Version 0.7] - 2023-09-20 + +### Docker Tags +- codiumai/pr-agent:0.7 +- codiumai/pr-agent:0.7-github_app +- codiumai/pr-agent:0.7-bitbucket-app +- codiumai/pr-agent:0.7-gitlab_webhook +- codiumai/pr-agent:0.7-github_polling +- codiumai/pr-agent:0.7-github_action + +### Added::Algo +- New tool /similar_issue - Currently on GitHub app and CLI: indexes the issues in the repo, find the most similar issues to the target issue. +- Describe markers: Empower the /describe tool with a templating capability (see more details in https://github.com/Codium-ai/pr-agent/pull/273). +- New feature in the /review tool - added an estimated effort estimation to the review (https://github.com/Codium-ai/pr-agent/pull/306). + +### Added::Infrastructure +- Implementation of a GitLab webhook. +- Implementation of a BitBucket app. + +### Fixed +- Protection against no code suggestions generated. +- Resilience to repositories where the languages cannot be automatically detected. + + + diff --git a/pr_agent/algo/git_patch_processing.py b/pr_agent/algo/git_patch_processing.py index 1a2bd22b..58d05235 100644 --- a/pr_agent/algo/git_patch_processing.py +++ b/pr_agent/algo/git_patch_processing.py @@ -40,12 +40,16 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str: extended_patch_lines.extend( original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines]) + res = list(match.groups()) + for i in range(len(res)): + if res[i] is None: + res[i] = 0 try: - start1, size1, start2, size2 = map(int, match.groups()[:4]) + start1, size1, start2, size2 = map(int, res[:4]) except: # '@@ -0,0 +1 @@' case - start1, size1, size2 = map(int, match.groups()[:3]) + start1, size1, size2 = map(int, res[:3]) start2 = 0 - section_header = match.groups()[4] + section_header = res[4] extended_start1 = max(1, start1 - num_lines) extended_size1 = size1 + (start1 - extended_start1) + num_lines extended_start2 = max(1, start2 - num_lines) @@ -207,10 +211,15 @@ __old hunk__ old_content_lines = [] if match: prev_header_line = header_line + + res = list(match.groups()) + for i in range(len(res)): + if res[i] is None: + res[i] = 0 try: - start1, size1, start2, size2 = map(int, match.groups()[:4]) + start1, size1, start2, size2 = map(int, res[:4]) except: # '@@ -0,0 +1 @@' case - start1, size1, size2 = map(int, match.groups()[:3]) + start1, size1, size2 = map(int, res[:3]) start2 = 0 elif line.startswith('+'): diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 0911d2d2..a4a242f3 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -127,6 +127,9 @@ class GitProvider(ABC): def get_commit_messages(self): pass + def get_pr_id(self): + return "" + def get_main_pr_language(languages, files) -> str: """ Get the main language of the commit. Return an empty string if cannot determine. diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 0521716b..39a58144 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -446,3 +446,10 @@ class GithubProvider(GitProvider): logging.info(f"Failed adding line link, error: {e}") return "" + + def get_pr_id(self): + try: + pr_id = f"{self.repo}/{self.pr_num}" + return pr_id + except: + return "" diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index a1d0b334..33cbee2b 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -378,4 +378,11 @@ class GitLabProvider(GitProvider): commit_messages_str = "" if max_tokens: commit_messages_str = clip_tokens(commit_messages_str, max_tokens) - return commit_messages_str \ No newline at end of file + return commit_messages_str + + def get_pr_id(self): + try: + pr_id = self.mr.web_url + return pr_id + except: + return "" diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index ba45598e..4ba27dd8 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -48,27 +48,33 @@ class PRCodeSuggestions: get_settings().pr_code_suggestions_prompt.user) async def run(self): - logging.info('Generating code suggestions for PR...') - if get_settings().config.publish_output: - self.git_provider.publish_comment("Preparing review...", is_temporary=True) + try: + logging.info('Generating code suggestions for PR...') + if get_settings().config.publish_output: + self.git_provider.publish_comment("Preparing review...", is_temporary=True) - logging.info('Preparing PR review...') - if not self.is_extended: - await retry_with_fallback_models(self._prepare_prediction) - data = self._prepare_pr_code_suggestions() - else: - data = await retry_with_fallback_models(self._prepare_prediction_extended) + logging.info('Preparing PR review...') + if not self.is_extended: + await retry_with_fallback_models(self._prepare_prediction) + data = self._prepare_pr_code_suggestions() + else: + data = await retry_with_fallback_models(self._prepare_prediction_extended) + if (not data) or (not 'Code suggestions' in data): + logging.info('No code suggestions found for PR.') + return - if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \ - (self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions): - logging.info('Ranking Suggestions...') - data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions']) + if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \ + (self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions): + logging.info('Ranking Suggestions...') + data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions']) - if get_settings().config.publish_output: - logging.info('Pushing PR review...') - self.git_provider.remove_initial_comment() - logging.info('Pushing inline code suggestions...') - self.push_inline_code_suggestions(data) + if get_settings().config.publish_output: + logging.info('Pushing PR review...') + self.git_provider.remove_initial_comment() + logging.info('Pushing inline code suggestions...') + self.push_inline_code_suggestions(data) + except Exception as e: + logging.error(f"Failed to generate code suggestions for PR, error: {e}") async def _prepare_prediction(self, model: str): logging.info('Getting PR diff...') diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index 3c388eb4..2214f3b6 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -29,7 +29,7 @@ class PRDescription: self.main_pr_language = get_main_pr_language( self.git_provider.get_languages(), self.git_provider.get_files() ) - self.pr_id = f"{self.git_provider.repo}/{self.git_provider.pr_num}" + self.pr_id = self.git_provider.get_pr_id() # Initialize the AI handler self.ai_handler = AiHandler() @@ -63,40 +63,44 @@ class PRDescription: """ Generates a PR description using an AI model and publishes it to the PR. """ - logging.info(f"Generating a PR description {self.pr_id}") - if get_settings().config.publish_output: - self.git_provider.publish_comment("Preparing pr description...", is_temporary=True) - await retry_with_fallback_models(self._prepare_prediction) + try: + logging.info(f"Generating a PR description {self.pr_id}") + if get_settings().config.publish_output: + self.git_provider.publish_comment("Preparing pr description...", is_temporary=True) - logging.info(f"Preparing answer {self.pr_id}") - if self.prediction: - self._prepare_data() - else: - return None + await retry_with_fallback_models(self._prepare_prediction) - pr_labels = [] - if get_settings().pr_description.publish_labels: - pr_labels = self._prepare_labels() - - if get_settings().pr_description.use_description_markers: - pr_title, pr_body = self._prepare_pr_answer_with_markers() - else: - pr_title, pr_body, = self._prepare_pr_answer() - full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}" - - if get_settings().config.publish_output: - logging.info(f"Pushing answer {self.pr_id}") - if get_settings().pr_description.publish_description_as_comment: - self.git_provider.publish_comment(full_markdown_description) + logging.info(f"Preparing answer {self.pr_id}") + if self.prediction: + self._prepare_data() else: - self.git_provider.publish_description(pr_title, pr_body) - if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"): - current_labels = self.git_provider.get_labels() - if current_labels is None: - current_labels = [] - self.git_provider.publish_labels(pr_labels + current_labels) - self.git_provider.remove_initial_comment() + return None + + pr_labels = [] + if get_settings().pr_description.publish_labels: + pr_labels = self._prepare_labels() + + if get_settings().pr_description.use_description_markers: + pr_title, pr_body = self._prepare_pr_answer_with_markers() + else: + pr_title, pr_body, = self._prepare_pr_answer() + full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}" + + if get_settings().config.publish_output: + logging.info(f"Pushing answer {self.pr_id}") + if get_settings().pr_description.publish_description_as_comment: + self.git_provider.publish_comment(full_markdown_description) + else: + self.git_provider.publish_description(pr_title, pr_body) + if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"): + current_labels = self.git_provider.get_labels() + if current_labels is None: + current_labels = [] + self.git_provider.publish_labels(pr_labels + current_labels) + self.git_provider.remove_initial_comment() + except Exception as e: + logging.error(f"Error generating PR description {self.pr_id}: {e}") return "" diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index b6bca536..01e3f276 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -95,28 +95,32 @@ class PRReviewer: """ Review the pull request and generate feedback. """ - if self.is_auto and not get_settings().pr_reviewer.automatic_review: - logging.info(f'Automatic review is disabled {self.pr_url}') - return None - logging.info(f'Reviewing PR: {self.pr_url} ...') + try: + if self.is_auto and not get_settings().pr_reviewer.automatic_review: + logging.info(f'Automatic review is disabled {self.pr_url}') + return None - if get_settings().config.publish_output: - self.git_provider.publish_comment("Preparing review...", is_temporary=True) - - await retry_with_fallback_models(self._prepare_prediction) - - logging.info('Preparing PR review...') - pr_comment = self._prepare_pr_review() - - if get_settings().config.publish_output: - logging.info('Pushing PR review...') - self.git_provider.publish_comment(pr_comment) - self.git_provider.remove_initial_comment() - - if get_settings().pr_reviewer.inline_code_comments: - logging.info('Pushing inline code comments...') - self._publish_inline_code_comments() + logging.info(f'Reviewing PR: {self.pr_url} ...') + + if get_settings().config.publish_output: + self.git_provider.publish_comment("Preparing review...", is_temporary=True) + + await retry_with_fallback_models(self._prepare_prediction) + + logging.info('Preparing PR review...') + pr_comment = self._prepare_pr_review() + + if get_settings().config.publish_output: + logging.info('Pushing PR review...') + self.git_provider.publish_comment(pr_comment) + self.git_provider.remove_initial_comment() + + if get_settings().pr_reviewer.inline_code_comments: + logging.info('Pushing inline code comments...') + self._publish_inline_code_comments() + except Exception as e: + logging.error(f"Failed to review PR: {e}") async def _prepare_prediction(self, model: str) -> None: """