From bab5a428dd179fda8767772f0b1e678c4af1dfe6 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Wed, 11 Jun 2025 19:03:36 +0000 Subject: [PATCH] Add Gitea support to git monitor --- cogs/git_monitor_cog.py | 135 +++++++++++++++++++++++++++----------- tests/test_git_monitor.py | 3 +- tests/test_url_parser.py | 37 +++++++---- 3 files changed, 125 insertions(+), 50 deletions(-) diff --git a/cogs/git_monitor_cog.py b/cogs/git_monitor_cog.py index 9023766..7fa1114 100644 --- a/cogs/git_monitor_cog.py +++ b/cogs/git_monitor_cog.py @@ -9,6 +9,7 @@ from typing import Literal, Optional, List, Dict, Any import asyncio # For sleep import aiohttp # For API calls import requests.utils # For url encoding gitlab project path +from urllib.parse import urlparse # Assuming settings_manager is in the parent directory # Adjust the import path if your project structure is different @@ -23,18 +24,29 @@ log = logging.getLogger(__name__) # Helper to parse repo URL and determine platform def parse_repo_url(url: str) -> tuple[Optional[str], Optional[str]]: """Parses a Git repository URL to extract platform and a simplified repo identifier.""" - # Fixed regex pattern for GitHub URLs - github_match = re.match( - r"^(?:https?://)?(?:www\.)?github\.com/([\w.-]+/[\w.-]+)(?:\.git)?/?$", url - ) - if github_match: - return "github", github_match.group(1) + if not re.match(r"^https?://", url): + url = "https://" + url - gitlab_match = re.match( - r"^(?:https?://)?(?:www\.)?gitlab\.com/([\w.-]+(?:/[\w.-]+)+)(?:\.git)?/?$", url - ) - if gitlab_match: - return "gitlab", gitlab_match.group(1) + try: + parsed = urlparse(url) + except Exception: + return None, None + + host = parsed.netloc.lower() + path_parts = [p for p in parsed.path.strip("/").split("/") if p] + + if host.endswith("github.com"): + if len(path_parts) >= 2: + return "github", "/".join(path_parts[:2]) + return None, None + + if host.endswith("gitlab.com"): + if len(path_parts) >= 2: + return "gitlab", "/".join(path_parts) + return None, None + + if host and len(path_parts) >= 2: + return "gitea", "/".join(path_parts[:2]) return None, None @@ -179,6 +191,45 @@ class GitMonitorCog(commands.Cog): log.error( f"Error fetching GitLab commits for {repo_url}: {response.status} - {await response.text()}" ) + elif platform == "gitea": + parsed = urlparse( + repo_url + if repo_url.startswith("http") + else "https://" + repo_url + ) + owner_repo = "/".join( + [p for p in parsed.path.strip("/").split("/") if p][:2] + ) + if owner_repo: + api_url = f"https://{parsed.netloc}/api/v1/repos/{owner_repo}/commits" + params = {"limit": 10} + if target_branch: + params["sha"] = target_branch + async with session.get( + api_url, params=params + ) as response: + if response.status == 200: + commits_payload = await response.json() + temp_new_commits = [] + for commit_item in reversed(commits_payload): + if commit_item["sha"] == last_sha: + temp_new_commits = [] + continue + temp_new_commits.append(commit_item) + + if temp_new_commits: + new_commits_data = temp_new_commits + latest_fetched_sha = new_commits_data[-1][ + "sha" + ] + elif response.status == 404: + log.error( + f"Repository {repo_url} not found on Gitea (404)." + ) + else: + log.error( + f"Error fetching Gitea commits for {repo_url}: {response.status} - {await response.text()}" + ) except aiohttp.ClientError as ce: log.error(f"AIOHTTP client error polling {repo_url}: {ce}") except Exception as ex: @@ -195,14 +246,8 @@ class GitMonitorCog(commands.Cog): commit_data = commit_item_data.get("commit", {}) commit_msg = commit_data.get("message", "No message.") commit_url = commit_item_data.get("html_url", "#") - author_info = commit_data.get( - "author", {} - ) # Committer info is also available + author_info = commit_data.get("author", {}) author_name = author_info.get("name", "Unknown Author") - # Branch information is not directly available in this specific commit object from /commits endpoint. - # It's part of the push event or needs to be inferred/fetched differently for polling. - # For polling, we typically monitor a specific branch, or assume default. - # Verification status verification = commit_data.get("verification", {}) verified_status = ( "Verified" @@ -216,19 +261,12 @@ class GitMonitorCog(commands.Cog): verified_status += ( f" ({verification.get('reason')})" ) - - # Files changed and stats require another API call per commit: GET /repos/{owner}/{repo}/commits/{sha} - # This is too API intensive for a simple polling loop. - # We will omit detailed file stats for polled GitHub commits for now. files_changed_str = ( "File stats not fetched for polled commits." ) - embed = discord.Embed( title=f"New Commit in {repo_url}", - description=commit_msg.splitlines()[ - 0 - ], # First line + description=commit_msg.splitlines()[0], color=discord.Color.blue(), url=commit_url, ) @@ -243,13 +281,11 @@ class GitMonitorCog(commands.Cog): value=verified_status, inline=True, ) - # embed.add_field(name="Branch", value="default (polling)", inline=True) # Placeholder embed.add_field( name="Changes", value=files_changed_str, inline=False, ) - elif platform == "gitlab": commit_id = commit_item_data.get("id", "N/A") commit_id_short = commit_item_data.get( @@ -257,18 +293,14 @@ class GitMonitorCog(commands.Cog): ) commit_msg = commit_item_data.get( "title", "No message." - ) # GitLab uses 'title' for first line + ) commit_url = commit_item_data.get("web_url", "#") author_name = commit_item_data.get( "author_name", "Unknown Author" ) - # Branch information is not directly in this commit object from /commits. - # It's part of the push event or needs to be inferred. - # GitLab commit stats (added/deleted lines) are in the commit details, not list. files_changed_str = ( "File stats not fetched for polled commits." ) - embed = discord.Embed( title=f"New Commit in {repo_url}", description=commit_msg.splitlines()[0], @@ -281,13 +313,42 @@ class GitMonitorCog(commands.Cog): value=f"[`{commit_id_short}`]({commit_url})", inline=True, ) - # embed.add_field(name="Branch", value="default (polling)", inline=True) # Placeholder embed.add_field( name="Changes", value=files_changed_str, inline=False, ) - + elif platform == "gitea": + commit_sha = commit_item_data.get("sha", "N/A") + commit_id_short = commit_sha[:7] + commit_msg = commit_item_data.get("commit", {}).get( + "message", "No message." + ) + commit_url = commit_item_data.get("html_url", "#") + author_info = commit_item_data.get("commit", {}).get( + "author", {} + ) + author_name = author_info.get("name", "Unknown Author") + files_changed_str = ( + "File stats not fetched for polled commits." + ) + embed = discord.Embed( + title=f"New Commit in {repo_url}", + description=commit_msg.splitlines()[0], + color=discord.Color.green(), + url=commit_url, + ) + embed.set_author(name=author_name) + embed.add_field( + name="Commit", + value=f"[`{commit_id_short}`]({commit_url})", + inline=True, + ) + embed.add_field( + name="Changes", + value=files_changed_str, + inline=False, + ) if embed: try: await channel.send(embed=embed) @@ -336,7 +397,7 @@ class GitMonitorCog(commands.Cog): name="add", description="Add a repository to monitor for commits." ) @app_commands.describe( - repository_url="The full URL of the GitHub or GitLab repository (e.g., https://github.com/user/repo).", + repository_url="The full URL of the GitHub, GitLab, or Gitea repository (e.g., https://github.com/user/repo).", channel="The channel where commit notifications should be sent.", monitoring_method="Choose 'webhook' for real-time (requires repo admin rights) or 'poll' for periodic checks.", branch="The specific branch to monitor (for 'poll' method, defaults to main/master if not specified).", @@ -366,7 +427,7 @@ class GitMonitorCog(commands.Cog): ) # Use cleaned URL if not platform or not repo_identifier: await interaction.followup.send( - f"Invalid repository URL: `{repository_url}`. Please provide a valid GitHub or GitLab URL (e.g., https://github.com/user/repo).", + f"Invalid repository URL: `{repository_url}`. Please provide a valid GitHub, GitLab, or Gitea URL (e.g., https://github.com/user/repo).", ephemeral=True, ) return diff --git a/tests/test_git_monitor.py b/tests/test_git_monitor.py index 8a286c2..25c5fc6 100644 --- a/tests/test_git_monitor.py +++ b/tests/test_git_monitor.py @@ -27,6 +27,8 @@ from cogs.git_monitor_cog import parse_repo_url "http://www.gitlab.com/group/subgroup/project/", ("gitlab", "group/subgroup/project"), ), + ("https://gitea.com/user/repo", ("gitea", "user/repo")), + ("https://git.example.com/org/repo", ("gitea", "org/repo")), ], ) def test_parse_repo_url_valid(url, expected): @@ -43,7 +45,6 @@ def test_parse_repo_url_valid(url, expected): "ftp://github.com/user/repo", "http:/github.com/user/repo", "not a url", - "https://gitlabx.com/group/project", "gitlab.com/group//project", "github.com/user/repo/extra", ], diff --git a/tests/test_url_parser.py b/tests/test_url_parser.py index 3e66adf..9946c6c 100644 --- a/tests/test_url_parser.py +++ b/tests/test_url_parser.py @@ -1,23 +1,34 @@ import re from typing import Optional, Tuple +from urllib.parse import urlparse import pytest def parse_repo_url(url: str) -> Tuple[Optional[str], Optional[str]]: """Parses a Git repository URL and returns platform and repo id.""" - github_match = re.match( - r"^(?:https?://)?(?:www\.)?github\.com/([\w.-]+/[\w.-]+)(?:\.git)?/?$", - url, - ) - if github_match: - return "github", github_match.group(1) + if not re.match(r"^https?://", url): + url = "https://" + url - gitlab_match = re.match( - r"^(?:https?://)?(?:www\.)?gitlab\.com/([\w.-]+(?:/[\w.-]+)+)(?:\.git)?/?$", - url, - ) - if gitlab_match: - return "gitlab", gitlab_match.group(1) + try: + parsed = urlparse(url) + except Exception: + return None, None + + host = parsed.netloc.lower() + parts = [p for p in parsed.path.strip("/").split("/") if p] + + if host.endswith("github.com"): + if len(parts) >= 2: + return "github", "/".join(parts[:2]) + return None, None + + if host.endswith("gitlab.com"): + if len(parts) >= 2: + return "gitlab", "/".join(parts) + return None, None + + if host and len(parts) >= 2: + return "gitea", "/".join(parts[:2]) return None, None @@ -40,6 +51,8 @@ def parse_repo_url(url: str) -> Tuple[Optional[str], Optional[str]]: "https://gitlab.com/group/subgroup/project", ("gitlab", "group/subgroup/project"), ), + ("https://gitea.com/org/repo", ("gitea", "org/repo")), + ("https://mygit.example/repo1/project", ("gitea", "repo1/project")), ("invalid-url", (None, None)), ], ) -- 2.49.0