Skip to content

📝 Add docstrings to Feature/config_check #33

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
151 changes: 117 additions & 34 deletions gitlab_integration/gitlab_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@

class GitlabMergeRequestFetcher:
def __init__(self, project_id, merge_request_iid):
"""
Initialize a GitLab merge request fetcher.

Assigns the project identifier and merge request IID, and sets up caches for
tracking changes, file content, and merge request information.

Parameters:
project_id: The unique identifier for the GitLab project.
merge_request_iid: The internal identifier for the merge request.
"""
self.project_id = project_id
self.iid = merge_request_iid
self._changes_cache = None
Expand All @@ -22,17 +32,23 @@ def __init__(self, project_id, merge_request_iid):
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def get_changes(self, force=False):
"""
Get the changes of the merge request
:return: changes
Retrieve merge request changes via GitLab API.

If cached changes are available and force is False, returns the cached data.
Otherwise, performs a GET request to fetch the latest changes, caches them on success,
and returns the list of changes. Returns None if the API request fails.

Args:
force (bool): If True, bypasses the cache to fetch fresh changes.
"""
if self._changes_cache and not force:
return self._changes_cache
# URL for the GitLab API endpoint
url = f"{GITLAB_SERVER_URL}/api/v4/projects/{self.project_id}/merge_requests/{self.iid}/changes"
url = f"{gitlab_server_url}/api/v4/projects/{self.project_id}/merge_requests/{self.iid}/changes"

# Headers for the request
headers = {
"PRIVATE-TOKEN": GITLAB_PRIVATE_TOKEN
"PRIVATE-TOKEN": gitlab_private_token
}

# Make the GET request
Expand All @@ -49,20 +65,31 @@ def get_changes(self, force=False):
# 获取文件内容
def get_file_content(self, file_path, branch_name='main', force=False):
"""
Get the content of the file
:param file_path: The path of the file
:return: The content of the file
Fetch the raw content of a repository file via the GitLab API.

This method retrieves the file content from the specified branch by making a GET
request to the GitLab API. The provided file path is URL-encoded for proper API
endpoint formatting. Cached content is returned if available, unless the force
flag is set to True.

Args:
file_path: The repository path of the file; forward slashes are URL-encoded.
branch_name: The branch to fetch the file from (default is 'main').
force: If True, bypasses the cache to retrieve fresh content.

Returns:
The raw file content as a string if the request is successful; otherwise, None.
"""
# 对file_path中的'/'转换为'%2F'
file_path = file_path.replace('/', '%2F')
if file_path in self._file_content_cache and not force:
return self._file_content_cache[file_path]
# URL for the GitLab API endpoint
url = f"{GITLAB_SERVER_URL}/api/v4/projects/{self.project_id}/repository/files/{file_path}/raw?ref={branch_name}"
url = f"{gitlab_server_url}/api/v4/projects/{self.project_id}/repository/files/{file_path}/raw?ref={branch_name}"

# Headers for the request
headers = {
"PRIVATE-TOKEN": GITLAB_PRIVATE_TOKEN
"PRIVATE-TOKEN": gitlab_private_token
}

# Make the GET request
Expand All @@ -78,17 +105,26 @@ def get_file_content(self, file_path, branch_name='main', force=False):
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def get_info(self, force=False):
"""
Get the merge request information
:return: Merge request information
Retrieve merge request information.

If cached data is available and force is False, the cached merge request details
are returned. Otherwise, the method calls the GitLab API to fetch fresh information,
caches the result, and returns it. If the API request fails, None is returned.

Args:
force (bool): If True, bypass the cache and retrieve fresh data.

Returns:
dict or None: The merge request information if successful; otherwise, None.
"""
if self._info_cache and not force:
return self._info_cache
# URL for the GitLab API endpoint
url = f"{GITLAB_SERVER_URL}/api/v4/projects/{self.project_id}/merge_requests/{self.iid}"
url = f"{gitlab_server_url}/api/v4/projects/{self.project_id}/merge_requests/{self.iid}"

# Headers for the request
headers = {
"PRIVATE-TOKEN": GITLAB_PRIVATE_TOKEN
"PRIVATE-TOKEN": gitlab_private_token
}

# Make the GET request
Expand All @@ -104,22 +140,37 @@ def get_info(self, force=False):
# gitlab仓库clone和管理
class GitlabRepoManager:
def __init__(self, project_id, branch_name = ""):
"""
Initialize a GitlabRepoManager instance.

Creates a unique repository path by combining the provided project ID with the current
timestamp. The repository is initially marked as not cloned. The optional branch name
parameter is accepted for potential branch-related operations, although it is not used
during initialization.

Args:
project_id: Identifier for the GitLab project.
branch_name: Optional branch name for repository operations.
"""
self.project_id = project_id
self.timestamp = int(time.time() * 1000)
self.repo_path = f"./repo/{self.project_id}_{self.timestamp}"
self.has_cloned = False

def get_info(self):
"""
Get the project information
:return: Project information
Retrieve project information from GitLab.

Makes a GET request to the GitLab API to fetch details for the project identified by the
instance's project_id. Returns the JSON-decoded response if the request is successful (HTTP 200);
otherwise, returns None.
"""
# URL for the GitLab API endpoint
url = f"{GITLAB_SERVER_URL}/api/v4/projects/{self.project_id}"
url = f"{gitlab_server_url}/api/v4/projects/{self.project_id}"

# Headers for the request
headers = {
"PRIVATE-TOKEN": GITLAB_PRIVATE_TOKEN
"PRIVATE-TOKEN": gitlab_private_token
}

# Make the GET request
Expand All @@ -129,14 +180,20 @@ def get_info(self):
if response.status_code == 200:
return response.json()
else:
log.error(f"获取项目信息失败: {response.status_code} {response.text}")
return None

@retry(stop_max_attempt_number=3, wait_fixed=2000)
def shallow_clone(self, branch_name = "main"):
"""
Perform a shallow clone of the repository
param branch_name: The name of the branch to clone
Shallow clones the repository to a local directory.

Deletes any existing local clone, constructs an authenticated Git URL using
repository information, and executes a shallow clone (depth of 1) for the specified
branch. If cloning fails, an error is logged; otherwise, the repository is marked
as cloned.

Args:
branch_name (str): The branch to clone (default "main").
"""
# If the target directory exists, remove it
self.delete_repo()
Expand All @@ -159,6 +216,17 @@ def shallow_clone(self, branch_name = "main"):
# 切换分支
def checkout_branch(self, branch_name, force=False):
# Build the Git command
"""
Checks out the specified branch by performing a shallow clone if necessary.

If the repository has not been cloned already, the method executes a shallow clone for the target branch.
If the repository is already cloned, it verifies whether the branch is already checked out (unless forced)
and performs a shallow clone if the branch differs or if force is True.

Args:
branch_name: The name of the branch to check out.
force: If True, forces re-cloning of the branch even if it appears to be already checked out.
"""
if not self.has_cloned:
self.shallow_clone(branch_name)
else:
Expand All @@ -170,11 +238,30 @@ def checkout_branch(self, branch_name, force=False):

# 删除库
def delete_repo(self):
"""
Deletes the cloned repository directory if it exists.

This method checks whether the repository path exists on the filesystem and removes it along with its contents. If the directory is not present, no action is taken.
"""
if os.path.exists(self.repo_path):
shutil.rmtree(self.repo_path)

# 查找相关文件列表
def find_files_by_keyword(self, keyword, branch_name="main"):
"""
Search for files whose content matches a regex pattern.

Checks out the specified branch and recursively searches for files whose content
matches the provided regular expression. Files that cannot be read due to encoding,
permission, or existence issues are skipped.

Args:
keyword: Regular expression pattern to search for in file contents.
branch_name: Branch to search in; defaults to "main".

Returns:
A list of file paths for files containing a match to the keyword.
"""
matching_files = []
regex = re.compile(keyword)
self.checkout_branch(branch_name)
Expand All @@ -196,23 +283,19 @@ def find_files_by_keyword(self, keyword, branch_name="main"):
# 构建带有身份验证信息的 URL
def _build_authenticated_url(self, repo_url):
# 如果 URL 使用 https
token = GITLAB_PRIVATE_TOKEN
"""
Builds an authenticated URL for repository access.

This method embeds an OAuth2 token into the provided repository URL, supporting only
URLs beginning with "http://" or "https://". For HTTPS URLs, it returns a URL in the
format "https://oauth2:{token}@<rest_of_url>" and similarly for HTTP URLs. If the URL
scheme is unsupported, a ValueError is raised.
"""
token = gitlab_private_token
if repo_url.startswith("https://"):
return f"https://oauth2:{token}@{repo_url[8:]}"
# 如果 URL 使用 http
elif repo_url.startswith("http://"):
return f"http://oauth2:{token}@{repo_url[7:]}"
else:
raise ValueError("Unsupported URL scheme")

def is_merge_request_opened(gitlab_payload) -> bool:
"""
判断是否是merge request打开事件
"""
try:
gitlab_merge_request_old = gitlab_payload.get("object_attributes").get("state") == "opened" and gitlab_payload.get("object_attributes").get("merge_status") == "preparing"
gitlab_merge_request_new = gitlab_payload.get("object_attributes").get("state") == "merged" and gitlab_payload.get("object_attributes").get("merge_status") == "can_be_merged"
return gitlab_merge_request_old or gitlab_merge_request_new
except Exception as e:
log.error(f"判断是否是merge request打开事件失败: {e}")
return False
raise ValueError("Unsupported URL scheme")
32 changes: 29 additions & 3 deletions gitlab_integration/webhook_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from response_module.response_controller import ReviewResponse
from review_engine.review_engine import ReviewEngine
from utils.logger import log
from gitlab_integration.gitlab_fetcher import is_merge_request_opened


class WebhookListener:
def __init__(self):
Expand All @@ -25,6 +25,19 @@ def handle_webhook(self):
return self.call_handle(gitlab_payload, event_type)

def call_handle(self, gitlab_payload, event_type):
"""
Dispatches a GitLab webhook payload to the corresponding event handler.

Determines the event type and builds a configuration dictionary for a ReviewResponse,
which is then passed to the appropriate handler—merge request, push, or other events.

Args:
gitlab_payload: A dictionary containing data from a GitLab webhook.
event_type: A string specifying the event type (e.g., 'merge_request', 'push').

Returns:
The response object returned by the invoked event-specific handler.
"""
if event_type == 'merge_request':
config = {
'type': 'merge_request',
Expand All @@ -51,9 +64,22 @@ def call_handle(self, gitlab_payload, event_type):

def handle_merge_request(self, gitlab_payload, reply):
"""
处理合并请求事件
Process a GitLab merge request event.

When the merge request is in the "opened" state and its merge status is "preparing", this
function logs the event, extracts the project and merge request identifiers, and initializes
a ReviewEngine to process the merge asynchronously in a new thread using a GitlabMergeRequestFetcher
and a GitlabRepoManager. It returns a JSON response with a status of "success". If the event does
not meet these criteria, a JSON response indicating that no further check is needed is returned.

Args:
gitlab_payload: A dictionary containing GitLab merge request event details.
reply: A ReviewResponse configuration object used for initializing the ReviewEngine.

Returns:
A tuple with a JSON response and an HTTP status code (200).
"""
if is_merge_request_opened(gitlab_payload):
if gitlab_payload.get("object_attributes").get("state") == "opened" and gitlab_payload.get("object_attributes").get("merge_status") == "preparing":
log.info("首次merge_request ", gitlab_payload)
project_id = gitlab_payload.get('project')['id']
merge_request_iid = gitlab_payload.get("object_attributes")["iid"]
Expand Down
38 changes: 38 additions & 0 deletions response_module/abstract_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,66 @@
class AbstractResponse(ABC):
@abstractmethod
def __init__(self, config):
"""
Initialize the response instance with a configuration.

The provided configuration is stored for use by subclasses.
"""
self.config = config


class AbstractResponseMessage(AbstractResponse):
@abstractmethod
def __init__(self, config):
"""
Initialize the instance with the provided configuration.

Delegates to the superclass constructor to set up the instance.
"""
super().__init__(config)

@abstractmethod
def send(self, message):
"""
Sends a message.

Subclasses must override this method to deliver the provided message using the
appropriate communication mechanism.

Args:
message: The content or payload of the message to be sent.
"""
pass


class AbstractResponseOther(AbstractResponse):
@abstractmethod
def __init__(self, config):
"""
Initialize the response instance with the specified configuration.

This constructor passes the configuration to the parent initializer to establish
the necessary settings for the response object.
"""
super().__init__(config)

@abstractmethod
def set_state(self, *args, **kwargs):
"""
Set the state of the response.

Subclasses must override this method to update the internal state based on the
provided positional and keyword arguments.
"""
pass

@abstractmethod
def send(self, *args, **kwargs):
"""
Sends a response using provided arguments.

Subclasses must override this method to handle sending a response
or triggering appropriate actions based on supplied positional and keyword
arguments.
"""
pass
Loading