2023-07-13 17:24:56 +03:00
import copy
2023-09-04 12:11:39 -04:00
import re
2023-12-17 16:52:03 +02:00
from functools import partial
2023-08-01 14:43:26 +03:00
from typing import List , Tuple
2023-07-13 17:24:56 +03:00
from jinja2 import Environment , StrictUndefined
2023-12-12 23:03:38 +08:00
from pr_agent . algo . ai_handlers . base_ai_handler import BaseAiHandler
2023-12-14 09:00:14 +02:00
from pr_agent . algo . ai_handlers . litellm_ai_handler import LiteLLMAIHandler
2023-07-23 16:16:36 +03:00
from pr_agent . algo . pr_processing import get_pr_diff , retry_with_fallback_models
2023-07-13 17:24:56 +03:00
from pr_agent . algo . token_handler import TokenHandler
2024-02-05 12:03:30 +02:00
from pr_agent . algo . utils import load_yaml , set_custom_labels , get_user_labels , ModelType
2023-08-01 14:43:26 +03:00
from pr_agent . config_loader import get_settings
2024-02-19 20:40:24 +02:00
from pr_agent . git_providers import get_git_provider , GithubProvider
2023-07-13 17:24:56 +03:00
from pr_agent . git_providers . git_provider import get_main_pr_language
2023-10-16 14:56:00 +03:00
from pr_agent . log import get_logger
2024-01-07 09:56:09 +02:00
from pr_agent . servers . help import HelpMessage
2023-07-13 17:24:56 +03:00
class PRDescription :
2023-12-14 09:00:14 +02:00
def __init__ ( self , pr_url : str , args : list = None ,
2023-12-17 16:52:03 +02:00
ai_handler : partial [ BaseAiHandler , ] = LiteLLMAIHandler ) :
2023-07-24 12:14:53 +03:00
"""
2023-08-01 14:43:26 +03:00
Initialize the PRDescription object with the necessary attributes and objects for generating a PR description
using an AI model .
2023-07-24 12:14:53 +03:00
Args :
pr_url ( str ) : The URL of the pull request .
2023-07-27 17:42:50 +03:00
args ( list , optional ) : List of arguments passed to the PRDescription class . Defaults to None .
2023-07-24 12:14:53 +03:00
"""
2023-07-24 12:41:00 +03:00
# Initialize the git provider and main PR language
2023-07-13 17:24:56 +03:00
self . git_provider = get_git_provider ( ) ( pr_url )
self . main_pr_language = get_main_pr_language (
self . git_provider . get_languages ( ) , self . git_provider . get_files ( )
)
2023-09-21 21:29:41 +03:00
self . pr_id = self . git_provider . get_pr_id ( )
2023-08-01 15:15:59 +03:00
2023-12-06 16:32:53 +02:00
if get_settings ( ) . pr_description . enable_semantic_files_types and not self . git_provider . is_supported (
" gfm_markdown " ) :
get_logger ( ) . debug ( f " Disabling semantic files types for { self . pr_id } " )
get_settings ( ) . pr_description . enable_semantic_files_types = False
2023-07-24 12:41:00 +03:00
# Initialize the AI handler
2023-12-17 16:52:03 +02:00
self . ai_handler = ai_handler ( )
2023-07-24 12:41:00 +03:00
# Initialize the variables dictionary
2023-07-13 17:24:56 +03:00
self . vars = {
" title " : self . git_provider . pr . title ,
" branch " : self . git_provider . get_pr_branch ( ) ,
2023-08-30 23:05:41 +03:00
" description " : self . git_provider . get_pr_description ( full = False ) ,
2023-07-13 17:24:56 +03:00
" language " : self . main_pr_language ,
" diff " : " " , # empty diff for initial calculation
2023-08-01 14:43:26 +03:00
" extra_instructions " : get_settings ( ) . pr_description . extra_instructions ,
2023-10-23 16:29:33 +03:00
" commit_messages_str " : self . git_provider . get_commit_messages ( ) ,
2023-10-29 11:40:36 +02:00
" enable_custom_labels " : get_settings ( ) . config . enable_custom_labels ,
2023-11-12 16:37:53 +02:00
" custom_labels_class " : " " , # will be filled if necessary in 'set_custom_labels' function
2023-12-04 18:22:35 +02:00
" enable_semantic_files_types " : get_settings ( ) . pr_description . enable_semantic_files_types ,
2023-07-13 17:24:56 +03:00
}
2023-08-17 15:40:24 +03:00
self . user_description = self . git_provider . get_user_description ( )
2023-07-24 12:41:00 +03:00
# Initialize the token handler
2023-07-24 12:14:53 +03:00
self . token_handler = TokenHandler (
self . git_provider . pr ,
self . vars ,
2023-08-01 14:43:26 +03:00
get_settings ( ) . pr_description_prompt . system ,
get_settings ( ) . pr_description_prompt . user ,
2023-07-24 12:14:53 +03:00
)
2023-07-24 12:41:00 +03:00
# Initialize patches_diff and prediction attributes
2023-07-13 17:24:56 +03:00
self . patches_diff = None
self . prediction = None
2023-08-01 14:43:26 +03:00
async def run ( self ) :
2023-07-24 12:14:53 +03:00
"""
Generates a PR description using an AI model and publishes it to the PR .
"""
2023-09-04 12:11:39 -04:00
2023-09-20 07:39:56 +03:00
try :
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " Generating a PR description { self . pr_id } " )
2023-09-20 07:39:56 +03:00
if get_settings ( ) . config . publish_output :
2023-09-23 08:08:46 -04:00
self . git_provider . publish_comment ( " Preparing PR description... " , is_temporary = True )
2023-09-04 12:11:39 -04:00
2024-02-05 12:03:30 +02:00
await retry_with_fallback_models ( self . _prepare_prediction , ModelType . TURBO ) # turbo model because larger context
2023-09-04 12:11:39 -04:00
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " Preparing answer { self . pr_id } " )
2023-09-20 07:39:56 +03:00
if self . prediction :
self . _prepare_data ( )
else :
2024-01-18 17:01:25 +02:00
self . git_provider . remove_initial_comment ( )
2023-09-20 07:39:56 +03:00
return None
2023-09-04 12:11:39 -04:00
2023-12-06 15:29:45 +02:00
if get_settings ( ) . pr_description . enable_semantic_files_types :
self . _prepare_file_labels ( )
2023-12-06 12:30:51 +02:00
2023-09-20 07:39:56 +03:00
pr_labels = [ ]
if get_settings ( ) . pr_description . publish_labels :
pr_labels = self . _prepare_labels ( )
2023-09-07 12:10:33 +03:00
2023-09-20 07:39:56 +03:00
if get_settings ( ) . pr_description . use_description_markers :
pr_title , pr_body = self . _prepare_pr_answer_with_markers ( )
2023-07-17 08:18:42 +03:00
else :
2023-09-20 07:39:56 +03:00
pr_title , pr_body , = self . _prepare_pr_answer ( )
2024-01-07 09:56:09 +02:00
# Add help text if gfm_markdown is supported
if self . git_provider . is_supported ( " gfm_markdown " ) and get_settings ( ) . pr_description . enable_help_text :
pr_body + = " <hr> \n \n <details> <summary><strong>✨ Usage guide:</strong></summary><hr> \n \n "
pr_body + = HelpMessage . get_describe_usage_guide ( )
2024-01-08 09:18:46 +02:00
pr_body + = " \n </details> \n "
2024-02-19 20:40:24 +02:00
elif get_settings ( ) . pr_description . enable_help_comment :
if isinstance ( self . git_provider , GithubProvider ) :
2024-02-19 21:10:20 +02:00
pr_body + = " \n \n ___ \n \n ✨ **PR-Agent usage guide**: \n \n - [ ] Mark this checkbox :gem:, or comment `/help`, to get a list of all PR-Agent tools and their descriptions. <!-- /help --> "
2024-02-19 20:40:24 +02:00
else :
2024-02-19 21:10:20 +02:00
pr_body + = " \n \n ___ \n \n >Comment `/help` on the PR to to get a list of all PR-Agent tools and their descriptions \n \n ___ \n \n "
2024-02-19 20:40:24 +02:00
2024-01-07 09:56:09 +02:00
# final markdown description
2023-09-20 07:39:56 +03:00
full_markdown_description = f " ## Title \n \n { pr_title } \n \n ___ \n { pr_body } "
2024-02-17 19:15:13 +02:00
# get_logger().debug(f"full_markdown_description:\n{full_markdown_description}")
2023-09-20 07:39:56 +03:00
if get_settings ( ) . config . publish_output :
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " Pushing answer { self . pr_id } " )
2024-01-25 11:07:43 +02:00
# publish labels
if get_settings ( ) . pr_description . publish_labels and self . git_provider . is_supported ( " get_labels " ) :
current_labels = self . git_provider . get_pr_labels ( )
user_labels = get_user_labels ( current_labels )
self . git_provider . publish_labels ( pr_labels + user_labels )
# publish description
2023-09-20 07:39:56 +03:00
if get_settings ( ) . pr_description . publish_description_as_comment :
2023-12-12 09:59:26 +02:00
get_logger ( ) . info ( f " Publishing answer as comment " )
2023-09-20 07:39:56 +03:00
self . git_provider . publish_comment ( full_markdown_description )
else :
self . git_provider . publish_description ( pr_title , pr_body )
2023-12-03 10:46:02 +02:00
2024-01-25 11:07:43 +02:00
# publish final update message
2023-12-03 10:46:02 +02:00
if ( get_settings ( ) . pr_description . final_update_message and
hasattr ( self . git_provider , ' pr_url ' ) and self . git_provider . pr_url ) :
latest_commit_url = self . git_provider . get_latest_commit_url ( )
if latest_commit_url :
self . git_provider . publish_comment (
2024-02-06 09:26:00 +02:00
f " **[PR Description]( { self . git_provider . get_pr_url ( ) } )** updated to latest commit ( { latest_commit_url } ) " )
2023-09-20 07:39:56 +03:00
self . git_provider . remove_initial_comment ( )
except Exception as e :
2023-10-16 14:56:00 +03:00
get_logger ( ) . error ( f " Error generating PR description { self . pr_id } : { e } " )
2023-07-24 12:14:53 +03:00
2023-07-13 17:24:56 +03:00
return " "
2023-07-24 12:14:53 +03:00
async def _prepare_prediction ( self , model : str ) - > None :
2023-09-04 12:11:39 -04:00
if get_settings ( ) . pr_description . use_description_markers and ' pr_agent: ' not in self . user_description :
return None
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " Getting PR diff { self . pr_id } " )
2023-07-23 16:16:36 +03:00
self . patches_diff = get_pr_diff ( self . git_provider , self . token_handler , model )
2024-01-18 17:01:25 +02:00
if self . patches_diff :
get_logger ( ) . info ( f " Getting AI prediction { self . pr_id } " )
self . prediction = await self . _get_prediction ( model )
else :
get_logger ( ) . error ( f " Error getting PR diff { self . pr_id } " )
self . prediction = None
2023-07-23 16:16:36 +03:00
2023-07-24 11:31:35 +03:00
async def _get_prediction ( self , model : str ) - > str :
"""
Generate an AI prediction for the PR description based on the provided model .
Args :
model ( str ) : The name of the model to be used for generating the prediction .
Returns :
str : The generated AI prediction .
"""
2023-07-13 17:24:56 +03:00
variables = copy . deepcopy ( self . vars )
variables [ " diff " ] = self . patches_diff # update diff
2023-07-24 11:31:35 +03:00
2023-07-13 17:24:56 +03:00
environment = Environment ( undefined = StrictUndefined )
2023-12-11 16:47:38 +02:00
set_custom_labels ( variables , self . git_provider )
2023-12-18 12:29:06 +02:00
self . variables = variables
2023-08-01 14:43:26 +03:00
system_prompt = environment . from_string ( get_settings ( ) . pr_description_prompt . system ) . render ( variables )
user_prompt = environment . from_string ( get_settings ( ) . pr_description_prompt . user ) . render ( variables )
2023-07-24 11:31:35 +03:00
2023-08-01 14:43:26 +03:00
if get_settings ( ) . config . verbosity_level > = 2 :
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " \n System prompt: \n { system_prompt } " )
get_logger ( ) . info ( f " \n User prompt: \n { user_prompt } " )
2023-07-24 11:31:35 +03:00
response , finish_reason = await self . ai_handler . chat_completion (
model = model ,
temperature = 0.2 ,
system = system_prompt ,
user = user_prompt
)
2023-11-15 09:06:26 +02:00
if get_settings ( ) . config . verbosity_level > = 2 :
get_logger ( ) . info ( f " \n AI response: \n { response } " )
2023-07-13 17:24:56 +03:00
return response
2023-07-24 09:15:45 +03:00
2023-09-04 12:11:39 -04:00
def _prepare_data ( self ) :
2023-07-24 09:15:45 +03:00
# Load the AI prediction data into a dictionary
2023-09-04 12:11:39 -04:00
self . data = load_yaml ( self . prediction . strip ( ) )
2023-07-24 09:15:45 +03:00
2024-01-04 17:46:24 +02:00
if get_settings ( ) . pr_description . add_original_user_description and self . user_description :
2024-01-04 18:01:55 +02:00
self . data [ " User Description " ] = self . user_description
2024-01-04 17:46:24 +02:00
2023-12-21 08:51:57 +02:00
# re-order keys
2024-01-04 17:46:24 +02:00
if ' User Description ' in self . data :
self . data [ ' User Description ' ] = self . data . pop ( ' User Description ' )
2023-12-21 08:51:57 +02:00
if ' title ' in self . data :
self . data [ ' title ' ] = self . data . pop ( ' title ' )
if ' type ' in self . data :
self . data [ ' type ' ] = self . data . pop ( ' type ' )
if ' labels ' in self . data :
self . data [ ' labels ' ] = self . data . pop ( ' labels ' )
if ' description ' in self . data :
self . data [ ' description ' ] = self . data . pop ( ' description ' )
if ' pr_files ' in self . data :
self . data [ ' pr_files ' ] = self . data . pop ( ' pr_files ' )
2024-01-04 17:46:24 +02:00
2023-09-04 12:11:39 -04:00
2023-08-17 15:40:24 +03:00
2023-09-14 08:13:00 +03:00
def _prepare_labels ( self ) - > List [ str ] :
2023-07-24 09:15:45 +03:00
pr_types = [ ]
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
2023-11-12 15:00:06 +02:00
if ' labels ' in self . data :
if type ( self . data [ ' labels ' ] ) == list :
pr_types = self . data [ ' labels ' ]
elif type ( self . data [ ' labels ' ] ) == str :
pr_types = self . data [ ' labels ' ] . split ( ' , ' )
elif ' type ' in self . data :
if type ( self . data [ ' type ' ] ) == list :
pr_types = self . data [ ' type ' ]
elif type ( self . data [ ' type ' ] ) == str :
pr_types = self . data [ ' type ' ] . split ( ' , ' )
2023-12-18 12:29:06 +02:00
# convert lowercase labels to original case
try :
if " labels_minimal_to_labels_dict " in self . variables :
d : dict = self . variables [ " labels_minimal_to_labels_dict " ]
for i , label_i in enumerate ( pr_types ) :
if label_i in d :
pr_types [ i ] = d [ label_i ]
except Exception as e :
get_logger ( ) . error ( f " Error converting labels to original case { self . pr_id } : { e } " )
2023-09-04 12:11:39 -04:00
return pr_types
2023-09-14 08:13:00 +03:00
def _prepare_pr_answer_with_markers ( self ) - > Tuple [ str , str ] :
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " Using description marker replacements { self . pr_id } " )
2023-09-04 12:11:39 -04:00
title = self . vars [ " title " ]
body = self . user_description
if get_settings ( ) . pr_description . include_generated_by_header :
ai_header = f " ### 🤖 Generated by PR Agent at { self . git_provider . last_commit_id . sha } \n \n "
else :
ai_header = " "
2023-11-12 15:00:06 +02:00
ai_type = self . data . get ( ' type ' )
2023-10-19 12:02:12 +03:00
if ai_type and not re . search ( r ' <!-- \ s*pr_agent:type \ s*--> ' , body ) :
pr_type = f " { ai_header } { ai_type } "
body = body . replace ( ' pr_agent:type ' , pr_type )
2023-11-12 15:00:06 +02:00
ai_summary = self . data . get ( ' description ' )
2023-09-04 12:11:39 -04:00
if ai_summary and not re . search ( r ' <!-- \ s*pr_agent:summary \ s*--> ' , body ) :
summary = f " { ai_header } { ai_summary } "
body = body . replace ( ' pr_agent:summary ' , summary )
2024-01-04 09:59:44 +02:00
ai_walkthrough = self . data . get ( ' pr_files ' )
if ai_walkthrough and not re . search ( r ' <!-- \ s*pr_agent:walkthrough \ s*--> ' , body ) :
try :
walkthrough_gfm = " "
walkthrough_gfm = self . process_pr_files_prediction ( walkthrough_gfm , self . file_label_dict )
body = body . replace ( ' pr_agent:walkthrough ' , walkthrough_gfm )
except Exception as e :
get_logger ( ) . error ( f " Failing to process walkthrough { self . pr_id } : { e } " )
body = body . replace ( ' pr_agent:walkthrough ' , " " )
2023-09-04 12:11:39 -04:00
return title , body
2023-09-07 12:10:33 +03:00
def _prepare_pr_answer ( self ) - > Tuple [ str , str ] :
2023-09-04 12:11:39 -04:00
"""
Prepare the PR description based on the AI prediction data .
Returns :
- title : a string containing the PR title .
- pr_body : a string containing the PR description body in a markdown format .
"""
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = " "
2023-11-06 11:35:22 +02:00
# Don't display 'PR Labels'
2023-11-12 15:00:06 +02:00
if ' labels ' in self . data and self . git_provider . is_supported ( " get_labels " ) :
self . data . pop ( ' labels ' )
2023-11-06 11:58:26 +02:00
if not get_settings ( ) . pr_description . enable_pr_type :
2023-11-12 15:00:06 +02:00
self . data . pop ( ' type ' )
2023-09-04 12:11:39 -04:00
for key , value in self . data . items ( ) :
2024-01-08 10:30:47 +02:00
markdown_text + = f " ## ** { key } ** \n \n "
2023-09-04 12:11:39 -04:00
markdown_text + = f " { value } \n \n "
2023-07-24 09:15:45 +03:00
2023-08-22 10:32:58 +03:00
# Remove the 'PR Title' key from the dictionary
2023-11-12 15:00:06 +02:00
ai_title = self . data . pop ( ' title ' , self . vars [ " title " ] )
2023-08-22 10:32:58 +03:00
if get_settings ( ) . pr_description . keep_original_user_title :
# Assign the original PR title to the 'title' variable
title = self . vars [ " title " ]
else :
# Assign the value of the 'PR Title' key to 'title' variable
title = ai_title
2023-07-24 09:15:45 +03:00
# Iterate over the remaining dictionary items and append the key and value to 'pr_body' in a markdown format,
# except for the items containing the word 'walkthrough'
2023-08-09 08:50:15 +03:00
pr_body = " "
2023-09-04 12:11:39 -04:00
for idx , ( key , value ) in enumerate ( self . data . items ( ) ) :
2023-12-06 12:30:51 +02:00
if key == ' pr_files ' :
value = self . file_label_dict
2024-01-04 10:27:07 +02:00
key_publish = " Changes walkthrough "
2023-12-06 15:29:45 +02:00
else :
key_publish = key . rstrip ( ' : ' ) . replace ( " _ " , " " ) . capitalize ( )
2024-01-08 10:30:47 +02:00
pr_body + = f " ## ** { key_publish } ** \n "
2023-07-13 17:31:28 +03:00
if ' walkthrough ' in key . lower ( ) :
2023-09-17 16:51:16 +03:00
if self . git_provider . is_supported ( " gfm_markdown " ) :
2023-09-17 16:56:23 +03:00
pr_body + = " <details> <summary>files:</summary> \n \n "
2023-08-09 08:50:15 +03:00
for file in value :
filename = file [ ' filename ' ] . replace ( " ' " , " ` " )
2023-11-12 15:00:06 +02:00
description = file [ ' changes_in_file ' ]
2023-11-06 08:43:15 +02:00
pr_body + = f ' - ` { filename } `: { description } \n '
2023-09-17 16:51:16 +03:00
if self . git_provider . is_supported ( " gfm_markdown " ) :
2023-12-06 17:01:21 +02:00
pr_body + = " </details> \n "
2023-12-06 12:30:51 +02:00
elif ' pr_files ' in key . lower ( ) :
2023-12-06 17:01:21 +02:00
pr_body = self . process_pr_files_prediction ( pr_body , value )
2023-07-13 17:24:56 +03:00
else :
2023-08-09 08:50:15 +03:00
# if the value is a list, join its items by comma
2023-12-04 21:06:56 +02:00
if isinstance ( value , list ) :
2023-08-09 08:50:15 +03:00
value = ' , ' . join ( v for v in value )
2023-08-17 15:40:24 +03:00
pr_body + = f " { value } \n "
2023-09-04 12:11:39 -04:00
if idx < len ( self . data ) - 1 :
2023-12-11 15:55:04 +02:00
pr_body + = " \n \n ___ \n \n "
2023-07-24 09:15:45 +03:00
2023-08-01 14:43:26 +03:00
if get_settings ( ) . config . verbosity_level > = 2 :
2023-10-16 14:56:00 +03:00
get_logger ( ) . info ( f " title: \n { title } \n { pr_body } " )
2023-07-24 09:15:45 +03:00
2023-12-06 15:29:45 +02:00
return title , pr_body
def _prepare_file_labels ( self ) :
self . file_label_dict = { }
for file in self . data [ ' pr_files ' ] :
try :
filename = file [ ' filename ' ] . replace ( " ' " , " ` " ) . replace ( ' " ' , ' ` ' )
changes_summary = file [ ' changes_summary ' ]
2024-01-21 13:43:37 +02:00
changes_title = file [ ' changes_title ' ] . strip ( )
2024-01-04 09:42:15 +02:00
label = file . get ( ' label ' )
2023-12-06 15:29:45 +02:00
if label not in self . file_label_dict :
self . file_label_dict [ label ] = [ ]
2024-01-21 13:43:37 +02:00
self . file_label_dict [ label ] . append ( ( filename , changes_title , changes_summary ) )
2023-12-06 15:29:45 +02:00
except Exception as e :
get_logger ( ) . error ( f " Error preparing file label dict { self . pr_id } : { e } " )
2023-12-06 16:32:53 +02:00
pass
2023-12-06 17:01:21 +02:00
def process_pr_files_prediction ( self , pr_body , value ) :
2024-01-06 10:36:36 +02:00
# logic for using collapsible file list
2024-01-04 10:27:07 +02:00
use_collapsible_file_list = get_settings ( ) . pr_description . collapsible_file_list
2024-01-06 10:36:36 +02:00
num_files = 0
if value :
for semantic_label in value . keys ( ) :
num_files + = len ( value [ semantic_label ] )
2024-01-04 10:27:07 +02:00
if use_collapsible_file_list == " adaptive " :
2024-01-06 10:36:36 +02:00
use_collapsible_file_list = num_files > 8
2023-12-06 17:01:21 +02:00
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 " )
return pr_body
try :
2023-12-07 09:50:36 +02:00
pr_body + = " <table> "
2023-12-07 10:24:36 +02:00
header = f " Relevant files "
2024-02-05 10:12:47 +02:00
delta = 75
2024-01-21 13:43:37 +02:00
# header += " " * delta
pr_body + = f """ <thead><tr><th></th><th align= " left " > { header } </th></tr></thead> """
2023-12-07 09:50:36 +02:00
pr_body + = """ <tbody> """
2023-12-06 17:01:21 +02:00
for semantic_label in value . keys ( ) :
s_label = semantic_label . strip ( " ' " ) . strip ( ' " ' )
2023-12-07 10:27:19 +02:00
pr_body + = f """ <tr><td><strong> { s_label . capitalize ( ) } </strong></td> """
2023-12-06 17:01:21 +02:00
list_tuples = value [ semantic_label ]
2024-01-04 10:27:07 +02:00
if use_collapsible_file_list :
pr_body + = f """ <td><details><summary> { len ( list_tuples ) } files</summary><table> """
else :
pr_body + = f """ <td><table> """
2024-01-21 13:43:37 +02:00
for filename , file_changes_title , file_change_description in list_tuples :
2024-02-09 11:45:12 +02:00
filename = filename . replace ( " ' " , " ` " ) . rstrip ( )
2023-12-06 17:01:21 +02:00
filename_publish = filename . split ( " / " ) [ - 1 ]
2024-02-11 11:32:16 +02:00
file_changes_title_code = f " <code> { file_changes_title } </code> "
file_changes_title_code_br = insert_br_after_x_chars ( file_changes_title_code , x = ( delta - 5 ) ) . strip ( )
if len ( file_changes_title_code_br ) < ( delta - 5 ) :
file_changes_title_code_br + = " " * ( ( delta - 5 ) - len ( file_changes_title_code_br ) )
filename_publish = f " <strong> { filename_publish } </strong><dd> { file_changes_title_code_br } </dd> "
2023-12-06 17:01:21 +02:00
diff_plus_minus = " "
2024-01-21 13:43:37 +02:00
delta_nbsp = " "
2023-12-06 17:01:21 +02:00
diff_files = self . git_provider . diff_files
for f in diff_files :
if f . filename . lower ( ) == filename . lower ( ) :
num_plus_lines = f . num_plus_lines
num_minus_lines = f . num_minus_lines
2023-12-07 10:24:36 +02:00
diff_plus_minus + = f " + { num_plus_lines } /- { num_minus_lines } "
2024-01-21 13:43:37 +02:00
delta_nbsp = " " * max ( 0 , ( 8 - len ( diff_plus_minus ) ) )
2023-12-06 17:01:21 +02:00
break
# try to add line numbers link to code suggestions
2023-12-07 09:50:36 +02:00
link = " "
2023-12-06 17:01:21 +02:00
if hasattr ( self . git_provider , ' get_line_link ' ) :
filename = filename . strip ( )
link = self . git_provider . get_line_link ( filename , relevant_line_start = - 1 )
2023-12-07 09:50:36 +02:00
2024-01-21 13:43:37 +02:00
file_change_description_br = insert_br_after_x_chars ( file_change_description , x = ( delta - 5 ) )
2023-12-07 09:50:36 +02:00
pr_body + = f """
< tr >
< td >
< details >
2024-01-21 13:43:37 +02:00
< summary > { filename_publish } < / summary >
< hr >
2023-12-20 16:45:21 +02:00
2024-01-21 13:43:37 +02:00
{ filename }
{ file_change_description_br }
2024-02-05 12:39:03 +02:00
< / details >
2024-02-05 13:00:57 +02:00
2023-12-07 09:50:36 +02:00
< / td >
2024-01-21 13:43:37 +02:00
< td > < a href = " {link} " > { diff_plus_minus } < / a > { delta_nbsp } < / td >
2023-12-07 09:50:36 +02:00
< / tr >
"""
2024-01-04 10:27:07 +02:00
if use_collapsible_file_list :
pr_body + = """ </table></details></td></tr> """
else :
pr_body + = """ </table></td></tr> """
2023-12-07 09:50:36 +02:00
pr_body + = """ </tr></tbody></table> """
2023-12-06 17:01:21 +02:00
except Exception as e :
get_logger ( ) . error ( f " Error processing pr files to markdown { self . pr_id } : { e } " )
pass
return pr_body
2024-02-11 11:37:11 +02:00
def count_chars_without_html ( string ) :
if ' < ' not in string :
return len ( string )
no_html_string = re . sub ( ' <[^>]+> ' , ' ' , string )
return len ( no_html_string )
2024-02-05 09:20:36 +02:00
def insert_br_after_x_chars ( text , x = 70 ) :
2024-01-15 15:10:54 +02:00
"""
Insert < br > into a string after a word that increases its length above x characters .
2024-02-05 09:20:36 +02:00
Use proper HTML tags for code and new lines .
2024-01-15 15:10:54 +02:00
"""
2024-02-11 11:37:11 +02:00
if count_chars_without_html ( text ) < x :
2024-01-15 15:10:54 +02:00
return text
2023-12-06 16:32:53 +02:00
2024-02-05 09:20:36 +02:00
# replace odd instances of ` with <code> and even instances of ` with </code>
text = replace_code_tags ( text )
2024-01-21 13:43:37 +02:00
2024-02-05 09:20:36 +02:00
# convert list items to <li>
if text . startswith ( " - " ) :
text = " <li> " + text [ 2 : ]
text = text . replace ( " \n - " , ' <br><li> ' ) . replace ( " \n - " , ' <br><li> ' )
2024-01-21 13:43:37 +02:00
2024-02-05 09:20:36 +02:00
# convert new lines to <br>
text = text . replace ( " \n " , ' <br> ' )
2024-01-21 13:43:37 +02:00
2024-02-05 09:20:36 +02:00
# split text into lines
lines = text . split ( ' <br> ' )
words = [ ]
for i , line in enumerate ( lines ) :
words + = line . split ( ' ' )
if i < len ( lines ) - 1 :
words [ - 1 ] + = " <br> "
new_text = [ ]
2024-01-21 13:43:37 +02:00
is_inside_code = False
2024-02-05 09:20:36 +02:00
current_length = 0
2024-01-15 15:10:54 +02:00
for word in words :
2024-02-05 09:20:36 +02:00
is_saved_word = False
if word == " <code> " or word == " </code> " or word == " <li> " or word == " <br> " :
is_saved_word = True
2023-12-06 16:32:53 +02:00
2024-02-05 10:12:47 +02:00
len_word = count_chars_without_html ( word )
if not is_saved_word and ( current_length + len_word > x ) :
2024-02-05 09:20:36 +02:00
if is_inside_code :
new_text . append ( " </code><br><code> " )
else :
new_text . append ( " <br> " )
current_length = 0 # Reset counter
new_text . append ( word + " " )
2023-07-24 09:15:45 +03:00
2024-02-05 09:20:36 +02:00
if not is_saved_word :
2024-02-05 10:12:47 +02:00
current_length + = len_word + 1 # Add 1 for the space
2024-01-21 13:43:37 +02:00
2024-02-05 09:20:36 +02:00
if word == " <li> " or word == " <br> " :
2024-01-21 13:43:37 +02:00
current_length = 0
2024-02-05 09:20:36 +02:00
2024-02-05 13:00:57 +02:00
if " <code> " in word :
is_inside_code = True
if " </code> " in word :
is_inside_code = False
2024-02-05 09:20:36 +02:00
return ' ' . join ( new_text ) . strip ( )
def replace_code_tags ( text ) :
"""
Replace odd instances of ` with < code > and even instances of ` with < / code >
"""
parts = text . split ( ' ` ' )
for i in range ( 1 , len ( parts ) , 2 ) :
parts [ i ] = ' <code> ' + parts [ i ] + ' </code> '
return ' ' . join ( parts )