mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-08 06:40:39 +08:00
Merge remote-tracking branch 'origin/main' into tr/user_description
# Conflicts: # pr_agent/git_providers/git_provider.py
This commit is contained in:
@ -48,7 +48,8 @@ Under the section 'pr_description', the [configuration file](./../pr_agent/setti
|
|||||||
|
|
||||||
- `final_update_message`: if set to true, it will add a comment message [`PR Description updated to latest commit...`](https://github.com/Codium-ai/pr-agent/pull/499#issuecomment-1837412176) after finishing calling `/describe`. Default is true.
|
- `final_update_message`: if set to true, it will add a comment message [`PR Description updated to latest commit...`](https://github.com/Codium-ai/pr-agent/pull/499#issuecomment-1837412176) after finishing calling `/describe`. Default is true.
|
||||||
|
|
||||||
- `enable_semantic_files_types`: if set to true, "PR changes walkthrough" section will be generated. Default is true.
|
- `enable_semantic_files_types`: if set to true, "Changes walkthrough" section will be generated. Default is true.
|
||||||
|
- `collapsible_file_list`: if set to true, the file list in the "Changes walkthrough" section will be collapsible. If set to "adaptive", the file list will be collapsible only if there are more than 8 files. Default is "adaptive".
|
||||||
|
|
||||||
### Markers template
|
### Markers template
|
||||||
|
|
||||||
|
@ -74,20 +74,22 @@ class GitProvider(ABC):
|
|||||||
|
|
||||||
def get_user_description(self) -> str:
|
def get_user_description(self) -> str:
|
||||||
description = (self.get_pr_description_full() or "").strip()
|
description = (self.get_pr_description_full() or "").strip()
|
||||||
|
description_lowercase = description.lower()
|
||||||
# if the existing description wasn't generated by the pr-agent, just return it as-is
|
# if the existing description wasn't generated by the pr-agent, just return it as-is
|
||||||
if not any(description.startswith(header) for header in ("## PR Type", "## PR Description")):
|
if not self._is_generated_by_pr_agent(description_lowercase):
|
||||||
return description
|
return description
|
||||||
# if the existing description was generated by the pr-agent, but it doesn't contain the user description,
|
# if the existing description was generated by the pr-agent, but it doesn't contain the user description,
|
||||||
# return nothing (empty string) because it means there is no user description
|
# return nothing (empty string) because it means there is no user description
|
||||||
if "## User Description:" not in description:
|
user_description_header = "## user description"
|
||||||
|
if user_description_header not in description_lowercase:
|
||||||
return ""
|
return ""
|
||||||
|
# otherwise, extract the original user description from the existing pr-agent description and return it
|
||||||
|
user_description_start_position = description_lowercase.find(user_description_header) + len(user_description_header)
|
||||||
|
return description[user_description_start_position:].split("\n", 1)[-1].strip()
|
||||||
|
|
||||||
# # otherwise, extract the original user description from the existing pr-agent description and return it
|
def _is_generated_by_pr_agent(self, description_lowercase: str) -> bool:
|
||||||
# return description.split("## User Description:", 1)[1].strip()
|
possible_headers = ("## pr type", "## pr description", "## pr labels", "## type", "## description", "## labels", "### 🤖 generated by pr agent")
|
||||||
|
return any(description_lowercase.startswith(header) for header in possible_headers)
|
||||||
# the 'user description' is in the beginning. extract it
|
|
||||||
return description.split("\n\n___", 1)[0].strip()
|
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_repo_settings(self):
|
def get_repo_settings(self):
|
||||||
|
@ -51,11 +51,10 @@ keep_original_user_title=false
|
|||||||
use_bullet_points=true
|
use_bullet_points=true
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
enable_pr_type=true
|
enable_pr_type=true
|
||||||
enable_file_walkthrough=false
|
|
||||||
enable_semantic_files_types=true
|
|
||||||
final_update_message = true
|
final_update_message = true
|
||||||
|
## changes walkthrough section
|
||||||
|
enable_semantic_files_types=true
|
||||||
|
collapsible_file_list='adaptive' # true, false, 'adaptive'
|
||||||
# markers
|
# markers
|
||||||
use_description_markers=false
|
use_description_markers=false
|
||||||
include_generated_by_header=true
|
include_generated_by_header=true
|
||||||
|
@ -35,12 +35,6 @@ class PRType(str, Enum):
|
|||||||
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if enable_file_walkthrough %}
|
|
||||||
class FileWalkthrough(BaseModel):
|
|
||||||
filename: str = Field(description="the relevant file full path")
|
|
||||||
changes_in_file: str = Field(description="minimal and concise summary of the changes in the relevant file")
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{%- if enable_semantic_files_types %}
|
{%- if enable_semantic_files_types %}
|
||||||
|
|
||||||
Class FileDescription(BaseModel):
|
Class FileDescription(BaseModel):
|
||||||
|
@ -52,7 +52,6 @@ class PRDescription:
|
|||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
||||||
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
|
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
|
||||||
"enable_file_walkthrough": get_settings().pr_description.enable_file_walkthrough,
|
|
||||||
"enable_semantic_files_types": get_settings().pr_description.enable_semantic_files_types,
|
"enable_semantic_files_types": get_settings().pr_description.enable_semantic_files_types,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,16 +250,15 @@ class PRDescription:
|
|||||||
summary = f"{ai_header}{ai_summary}"
|
summary = f"{ai_header}{ai_summary}"
|
||||||
body = body.replace('pr_agent:summary', summary)
|
body = body.replace('pr_agent:summary', summary)
|
||||||
|
|
||||||
if not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body):
|
ai_walkthrough = self.data.get('pr_files')
|
||||||
ai_walkthrough = self.data.get('PR Main Files Walkthrough')
|
if ai_walkthrough and not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body):
|
||||||
if ai_walkthrough:
|
try:
|
||||||
walkthrough = str(ai_header)
|
walkthrough_gfm = ""
|
||||||
for file in ai_walkthrough:
|
walkthrough_gfm = self.process_pr_files_prediction(walkthrough_gfm, self.file_label_dict)
|
||||||
filename = file['filename'].replace("'", "`")
|
body = body.replace('pr_agent:walkthrough', walkthrough_gfm)
|
||||||
description = file['changes in file'].replace("'", "`")
|
except Exception as e:
|
||||||
walkthrough += f'- `{filename}`: {description}\n'
|
get_logger().error(f"Failing to process walkthrough {self.pr_id}: {e}")
|
||||||
|
body = body.replace('pr_agent:walkthrough', "")
|
||||||
body = body.replace('pr_agent:walkthrough', walkthrough)
|
|
||||||
|
|
||||||
return title, body
|
return title, body
|
||||||
|
|
||||||
@ -299,7 +297,7 @@ class PRDescription:
|
|||||||
for idx, (key, value) in enumerate(self.data.items()):
|
for idx, (key, value) in enumerate(self.data.items()):
|
||||||
if key == 'pr_files':
|
if key == 'pr_files':
|
||||||
value = self.file_label_dict
|
value = self.file_label_dict
|
||||||
key_publish = "PR changes walkthrough"
|
key_publish = "Changes walkthrough"
|
||||||
else:
|
else:
|
||||||
key_publish = key.rstrip(':').replace("_", " ").capitalize()
|
key_publish = key.rstrip(':').replace("_", " ").capitalize()
|
||||||
pr_body += f"## {key_publish}\n"
|
pr_body += f"## {key_publish}\n"
|
||||||
@ -333,7 +331,7 @@ class PRDescription:
|
|||||||
try:
|
try:
|
||||||
filename = file['filename'].replace("'", "`").replace('"', '`')
|
filename = file['filename'].replace("'", "`").replace('"', '`')
|
||||||
changes_summary = file['changes_summary']
|
changes_summary = file['changes_summary']
|
||||||
label = file['label']
|
label = file.get('label')
|
||||||
if label not in self.file_label_dict:
|
if label not in self.file_label_dict:
|
||||||
self.file_label_dict[label] = []
|
self.file_label_dict[label] = []
|
||||||
self.file_label_dict[label].append((filename, changes_summary))
|
self.file_label_dict[label].append((filename, changes_summary))
|
||||||
@ -342,6 +340,9 @@ class PRDescription:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def process_pr_files_prediction(self, pr_body, value):
|
def process_pr_files_prediction(self, pr_body, value):
|
||||||
|
use_collapsible_file_list = get_settings().pr_description.collapsible_file_list
|
||||||
|
if use_collapsible_file_list == "adaptive":
|
||||||
|
use_collapsible_file_list = len(value) > 8
|
||||||
if not self.git_provider.is_supported("gfm_markdown"):
|
if not self.git_provider.is_supported("gfm_markdown"):
|
||||||
get_logger().info(f"Disabling semantic files types for {self.pr_id} since gfm_markdown is not supported")
|
get_logger().info(f"Disabling semantic files types for {self.pr_id} since gfm_markdown is not supported")
|
||||||
return pr_body
|
return pr_body
|
||||||
@ -356,7 +357,11 @@ class PRDescription:
|
|||||||
s_label = semantic_label.strip("'").strip('"')
|
s_label = semantic_label.strip("'").strip('"')
|
||||||
pr_body += f"""<tr><td><strong>{s_label.capitalize()}</strong></td>"""
|
pr_body += f"""<tr><td><strong>{s_label.capitalize()}</strong></td>"""
|
||||||
list_tuples = value[semantic_label]
|
list_tuples = value[semantic_label]
|
||||||
pr_body += f"""<td><details><summary>{len(list_tuples)} files</summary><table>"""
|
|
||||||
|
if use_collapsible_file_list:
|
||||||
|
pr_body += f"""<td><details><summary>{len(list_tuples)} files</summary><table>"""
|
||||||
|
else:
|
||||||
|
pr_body += f"""<td><table>"""
|
||||||
for filename, file_change_description in list_tuples:
|
for filename, file_change_description in list_tuples:
|
||||||
filename = filename.replace("'", "`")
|
filename = filename.replace("'", "`")
|
||||||
filename_publish = filename.split("/")[-1]
|
filename_publish = filename.split("/")[-1]
|
||||||
@ -395,7 +400,10 @@ class PRDescription:
|
|||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
"""
|
"""
|
||||||
pr_body += """</table></details></td></tr>"""
|
if use_collapsible_file_list:
|
||||||
|
pr_body += """</table></details></td></tr>"""
|
||||||
|
else:
|
||||||
|
pr_body += """</table></td></tr>"""
|
||||||
pr_body += """</tr></tbody></table>"""
|
pr_body += """</tr></tbody></table>"""
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
Reference in New Issue
Block a user