Add Gitea support to git monitor
This commit is contained in:
parent
f142e0b8cf
commit
bab5a428dd
@ -9,6 +9,7 @@ from typing import Literal, Optional, List, Dict, Any
|
|||||||
import asyncio # For sleep
|
import asyncio # For sleep
|
||||||
import aiohttp # For API calls
|
import aiohttp # For API calls
|
||||||
import requests.utils # For url encoding gitlab project path
|
import requests.utils # For url encoding gitlab project path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
# Assuming settings_manager is in the parent directory
|
# Assuming settings_manager is in the parent directory
|
||||||
# Adjust the import path if your project structure is different
|
# 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
|
# Helper to parse repo URL and determine platform
|
||||||
def parse_repo_url(url: str) -> tuple[Optional[str], Optional[str]]:
|
def parse_repo_url(url: str) -> tuple[Optional[str], Optional[str]]:
|
||||||
"""Parses a Git repository URL to extract platform and a simplified repo identifier."""
|
"""Parses a Git repository URL to extract platform and a simplified repo identifier."""
|
||||||
# Fixed regex pattern for GitHub URLs
|
if not re.match(r"^https?://", url):
|
||||||
github_match = re.match(
|
url = "https://" + url
|
||||||
r"^(?:https?://)?(?:www\.)?github\.com/([\w.-]+/[\w.-]+)(?:\.git)?/?$", url
|
|
||||||
)
|
|
||||||
if github_match:
|
|
||||||
return "github", github_match.group(1)
|
|
||||||
|
|
||||||
gitlab_match = re.match(
|
try:
|
||||||
r"^(?:https?://)?(?:www\.)?gitlab\.com/([\w.-]+(?:/[\w.-]+)+)(?:\.git)?/?$", url
|
parsed = urlparse(url)
|
||||||
)
|
except Exception:
|
||||||
if gitlab_match:
|
return None, None
|
||||||
return "gitlab", gitlab_match.group(1)
|
|
||||||
|
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
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@ -179,6 +191,45 @@ class GitMonitorCog(commands.Cog):
|
|||||||
log.error(
|
log.error(
|
||||||
f"Error fetching GitLab commits for {repo_url}: {response.status} - {await response.text()}"
|
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:
|
except aiohttp.ClientError as ce:
|
||||||
log.error(f"AIOHTTP client error polling {repo_url}: {ce}")
|
log.error(f"AIOHTTP client error polling {repo_url}: {ce}")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -195,14 +246,8 @@ class GitMonitorCog(commands.Cog):
|
|||||||
commit_data = commit_item_data.get("commit", {})
|
commit_data = commit_item_data.get("commit", {})
|
||||||
commit_msg = commit_data.get("message", "No message.")
|
commit_msg = commit_data.get("message", "No message.")
|
||||||
commit_url = commit_item_data.get("html_url", "#")
|
commit_url = commit_item_data.get("html_url", "#")
|
||||||
author_info = commit_data.get(
|
author_info = commit_data.get("author", {})
|
||||||
"author", {}
|
|
||||||
) # Committer info is also available
|
|
||||||
author_name = author_info.get("name", "Unknown 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", {})
|
verification = commit_data.get("verification", {})
|
||||||
verified_status = (
|
verified_status = (
|
||||||
"Verified"
|
"Verified"
|
||||||
@ -216,19 +261,12 @@ class GitMonitorCog(commands.Cog):
|
|||||||
verified_status += (
|
verified_status += (
|
||||||
f" ({verification.get('reason')})"
|
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 = (
|
files_changed_str = (
|
||||||
"File stats not fetched for polled commits."
|
"File stats not fetched for polled commits."
|
||||||
)
|
)
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"New Commit in {repo_url}",
|
title=f"New Commit in {repo_url}",
|
||||||
description=commit_msg.splitlines()[
|
description=commit_msg.splitlines()[0],
|
||||||
0
|
|
||||||
], # First line
|
|
||||||
color=discord.Color.blue(),
|
color=discord.Color.blue(),
|
||||||
url=commit_url,
|
url=commit_url,
|
||||||
)
|
)
|
||||||
@ -243,13 +281,11 @@ class GitMonitorCog(commands.Cog):
|
|||||||
value=verified_status,
|
value=verified_status,
|
||||||
inline=True,
|
inline=True,
|
||||||
)
|
)
|
||||||
# embed.add_field(name="Branch", value="default (polling)", inline=True) # Placeholder
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Changes",
|
name="Changes",
|
||||||
value=files_changed_str,
|
value=files_changed_str,
|
||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif platform == "gitlab":
|
elif platform == "gitlab":
|
||||||
commit_id = commit_item_data.get("id", "N/A")
|
commit_id = commit_item_data.get("id", "N/A")
|
||||||
commit_id_short = commit_item_data.get(
|
commit_id_short = commit_item_data.get(
|
||||||
@ -257,18 +293,14 @@ class GitMonitorCog(commands.Cog):
|
|||||||
)
|
)
|
||||||
commit_msg = commit_item_data.get(
|
commit_msg = commit_item_data.get(
|
||||||
"title", "No message."
|
"title", "No message."
|
||||||
) # GitLab uses 'title' for first line
|
)
|
||||||
commit_url = commit_item_data.get("web_url", "#")
|
commit_url = commit_item_data.get("web_url", "#")
|
||||||
author_name = commit_item_data.get(
|
author_name = commit_item_data.get(
|
||||||
"author_name", "Unknown Author"
|
"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 = (
|
files_changed_str = (
|
||||||
"File stats not fetched for polled commits."
|
"File stats not fetched for polled commits."
|
||||||
)
|
)
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"New Commit in {repo_url}",
|
title=f"New Commit in {repo_url}",
|
||||||
description=commit_msg.splitlines()[0],
|
description=commit_msg.splitlines()[0],
|
||||||
@ -281,13 +313,42 @@ class GitMonitorCog(commands.Cog):
|
|||||||
value=f"[`{commit_id_short}`]({commit_url})",
|
value=f"[`{commit_id_short}`]({commit_url})",
|
||||||
inline=True,
|
inline=True,
|
||||||
)
|
)
|
||||||
# embed.add_field(name="Branch", value="default (polling)", inline=True) # Placeholder
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Changes",
|
name="Changes",
|
||||||
value=files_changed_str,
|
value=files_changed_str,
|
||||||
inline=False,
|
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:
|
if embed:
|
||||||
try:
|
try:
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
@ -336,7 +397,7 @@ class GitMonitorCog(commands.Cog):
|
|||||||
name="add", description="Add a repository to monitor for commits."
|
name="add", description="Add a repository to monitor for commits."
|
||||||
)
|
)
|
||||||
@app_commands.describe(
|
@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.",
|
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.",
|
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).",
|
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
|
) # Use cleaned URL
|
||||||
if not platform or not repo_identifier:
|
if not platform or not repo_identifier:
|
||||||
await interaction.followup.send(
|
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,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
@ -27,6 +27,8 @@ from cogs.git_monitor_cog import parse_repo_url
|
|||||||
"http://www.gitlab.com/group/subgroup/project/",
|
"http://www.gitlab.com/group/subgroup/project/",
|
||||||
("gitlab", "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):
|
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",
|
"ftp://github.com/user/repo",
|
||||||
"http:/github.com/user/repo",
|
"http:/github.com/user/repo",
|
||||||
"not a url",
|
"not a url",
|
||||||
"https://gitlabx.com/group/project",
|
|
||||||
"gitlab.com/group//project",
|
"gitlab.com/group//project",
|
||||||
"github.com/user/repo/extra",
|
"github.com/user/repo/extra",
|
||||||
],
|
],
|
||||||
|
@ -1,23 +1,34 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
from urllib.parse import urlparse
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def parse_repo_url(url: str) -> Tuple[Optional[str], Optional[str]]:
|
def parse_repo_url(url: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""Parses a Git repository URL and returns platform and repo id."""
|
"""Parses a Git repository URL and returns platform and repo id."""
|
||||||
github_match = re.match(
|
if not re.match(r"^https?://", url):
|
||||||
r"^(?:https?://)?(?:www\.)?github\.com/([\w.-]+/[\w.-]+)(?:\.git)?/?$",
|
url = "https://" + url
|
||||||
url,
|
|
||||||
)
|
|
||||||
if github_match:
|
|
||||||
return "github", github_match.group(1)
|
|
||||||
|
|
||||||
gitlab_match = re.match(
|
try:
|
||||||
r"^(?:https?://)?(?:www\.)?gitlab\.com/([\w.-]+(?:/[\w.-]+)+)(?:\.git)?/?$",
|
parsed = urlparse(url)
|
||||||
url,
|
except Exception:
|
||||||
)
|
return None, None
|
||||||
if gitlab_match:
|
|
||||||
return "gitlab", gitlab_match.group(1)
|
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
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +51,8 @@ def parse_repo_url(url: str) -> Tuple[Optional[str], Optional[str]]:
|
|||||||
"https://gitlab.com/group/subgroup/project",
|
"https://gitlab.com/group/subgroup/project",
|
||||||
("gitlab", "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)),
|
("invalid-url", (None, None)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user