Add Gitea support to git monitor (#9)
Adds polling and URL parsing support for gitea repositories. Reviewed-on: #9 Co-authored-by: Slipstream <me@slipstreamm.dev> Co-committed-by: Slipstream <me@slipstreamm.dev>
This commit is contained in:
parent
f142e0b8cf
commit
5cbece7420
@ -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
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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)),
|
||||
],
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user