Add Gitea commit lookup tools
This commit is contained in:
@@ -112,7 +112,7 @@ chat id.
|
||||
## Gitea integration
|
||||
|
||||
Set these optional values in `.env` to let Codex manage Gitea repositories,
|
||||
issues, pull requests, and HTTPS git authentication from Telegram:
|
||||
commits, issues, pull requests, and HTTPS git authentication from Telegram:
|
||||
|
||||
```env
|
||||
GITEA_BASE_URL=https://gitea.example.com
|
||||
@@ -124,8 +124,8 @@ GITEA_DEFAULT_REPO=your-default-repo
|
||||
GITEA_GIT_AUTH_ENABLED=true
|
||||
```
|
||||
|
||||
Codex receives Gitea MCP tools for repository, issue, branch, and pull-request
|
||||
metadata. The container also configures `GIT_ASKPASS` so HTTPS `git clone`,
|
||||
Codex receives Gitea MCP tools for repository, commit, issue, branch, and
|
||||
pull-request metadata. The container also configures `GIT_ASKPASS` so HTTPS `git clone`,
|
||||
`fetch`, and `push` can use the token without embedding it in command lines.
|
||||
|
||||
Example Telegram workflow:
|
||||
|
||||
@@ -1331,7 +1331,7 @@ class TelegramCodexBot:
|
||||
f"{runtime_settings}\n"
|
||||
"Configured Gitea integration:\n"
|
||||
f"{gitea_context}\n"
|
||||
"If the user asks for repository, issue, branch, or pull-request metadata in Gitea, "
|
||||
"If the user asks for repository, commit, issue, branch, or pull-request metadata in Gitea, "
|
||||
"use the gitea_* MCP tools. Never reveal GITEA_TOKEN or print commands that embed it. "
|
||||
"For HTTPS git clone/fetch/push, the container may provide GIT_ASKPASS from the "
|
||||
"configured Gitea environment.\n"
|
||||
@@ -1788,7 +1788,11 @@ def _scheduled_prompt(schedule: dict) -> str:
|
||||
return (
|
||||
"This is a scheduled Telegram bridge task. Run the task now and produce a concise "
|
||||
"Telegram-ready response. Do not create another schedule unless the task explicitly "
|
||||
"asks you to change scheduling.\n\n"
|
||||
"asks you to change scheduling. If repository history is relevant, prefer the Gitea "
|
||||
"commit tools. Do not describe a missing tool or unavailable detail as a permission "
|
||||
"problem unless an API error explicitly says permission was denied. Only include a "
|
||||
"commit summary when the task asks for repository history or a repository can be "
|
||||
"identified from the task/context.\n\n"
|
||||
f"Schedule: {format_schedule(schedule)}\n\n"
|
||||
f"Task:\n{schedule.get('prompt') or ''}"
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ class BridgeMcpServer:
|
||||
"notifications, manage bridge schedules, or persist personal assistant "
|
||||
"records such as work history, todos, purchase candidates, birthdays, "
|
||||
"contacts, and notes. Use the Gitea tools when the user asks to inspect "
|
||||
"or manage repositories, issues, pull requests, and code-review workflow "
|
||||
"or manage repositories, commits, issues, pull requests, and code-review workflow "
|
||||
"metadata in the user's configured Gitea instance. The Telegram chat_id and "
|
||||
"current thread_id are provided in the bridge context inside each user turn. "
|
||||
"Use that chat_id as the default notification target and do not ask for a "
|
||||
@@ -125,6 +125,10 @@ class BridgeMcpServer:
|
||||
return self.gitea_create_pull_request(arguments)
|
||||
if name == "gitea_list_branches":
|
||||
return self.gitea_list_branches(arguments)
|
||||
if name == "gitea_list_commits":
|
||||
return self.gitea_list_commits(arguments)
|
||||
if name == "gitea_get_commit":
|
||||
return self.gitea_get_commit(arguments)
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
def create_schedule(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -396,6 +400,35 @@ class BridgeMcpServer:
|
||||
"structuredContent": {"branches": branches},
|
||||
}
|
||||
|
||||
def gitea_list_commits(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
commits = client.list_commits(
|
||||
owner,
|
||||
repo,
|
||||
sha=_optional_string(arguments.get("sha") or arguments.get("branch")),
|
||||
path=_optional_string(arguments.get("path")),
|
||||
limit=int(arguments.get("limit") or 20),
|
||||
page=int(arguments.get("page") or 1),
|
||||
)
|
||||
if not commits:
|
||||
text = "No Gitea commits found."
|
||||
else:
|
||||
text = "\n".join(_format_gitea_commit(commit) for commit in commits)
|
||||
return {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
"structuredContent": {"commits": commits},
|
||||
}
|
||||
|
||||
def gitea_get_commit(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
client = GiteaClient.from_env()
|
||||
owner, repo = _repo_arguments(arguments, client)
|
||||
commit = client.get_commit(owner, repo, sha=str(arguments["sha"]))
|
||||
return {
|
||||
"content": [{"type": "text", "text": _format_gitea_commit(commit, include_files=True)}],
|
||||
"structuredContent": {"commit": commit},
|
||||
}
|
||||
|
||||
|
||||
def _tools() -> list[dict[str, Any]]:
|
||||
return [
|
||||
@@ -737,6 +770,44 @@ def _gitea_tools() -> list[dict[str, Any]]:
|
||||
},
|
||||
"annotations": {"title": "List Gitea branches", **readonly_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_list_commits",
|
||||
"description": "List recent commits for a Gitea repository, optionally scoped to a branch, SHA, or path.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"sha": {
|
||||
"type": "string",
|
||||
"description": "Branch name, tag, or commit SHA to list from. Defaults to the repository default branch.",
|
||||
},
|
||||
"branch": {
|
||||
"type": "string",
|
||||
"description": "Alias for sha when the user names a branch.",
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Optional repository path to filter commits by.",
|
||||
},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
"page": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
},
|
||||
"annotations": {"title": "List Gitea commits", **readonly_annotations},
|
||||
},
|
||||
{
|
||||
"name": "gitea_get_commit",
|
||||
"description": "Get one Gitea commit by SHA, including message, author, stats, and changed files when available.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
**repo_properties,
|
||||
"sha": {"type": "string", "description": "Commit SHA to inspect."},
|
||||
},
|
||||
"required": ["sha"],
|
||||
},
|
||||
"annotations": {"title": "Get Gitea commit", **readonly_annotations},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -754,6 +825,13 @@ def _repo_arguments(arguments: dict[str, Any], client: GiteaClient) -> tuple[str
|
||||
return owner, repo
|
||||
|
||||
|
||||
def _optional_string(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _format_gitea_repo(repo: dict[str, Any]) -> str:
|
||||
full_name = repo.get("full_name") or repo.get("name") or "(unknown)"
|
||||
default_branch = repo.get("default_branch") or repo.get("default_branch_name") or ""
|
||||
@@ -787,6 +865,62 @@ def _format_gitea_pull_request(pull: dict[str, Any]) -> str:
|
||||
return f"#{index} [{state}] {title}{suffix}"
|
||||
|
||||
|
||||
def _format_gitea_commit(commit: dict[str, Any], include_files: bool = False) -> str:
|
||||
sha = str(commit.get("sha") or "")[:10] or "(unknown)"
|
||||
payload = commit.get("commit") if isinstance(commit.get("commit"), dict) else {}
|
||||
message = str(payload.get("message") or commit.get("message") or "(no message)").strip()
|
||||
subject = message.splitlines()[0] if message else "(no message)"
|
||||
author = payload.get("author") if isinstance(payload.get("author"), dict) else {}
|
||||
author_name = author.get("name") or commit.get("author_name") or ""
|
||||
authored_at = commit.get("created") or author.get("date") or ""
|
||||
html_url = commit.get("html_url") or ""
|
||||
stats = commit.get("stats") if isinstance(commit.get("stats"), dict) else {}
|
||||
|
||||
bits = [sha]
|
||||
if authored_at:
|
||||
bits.append(str(authored_at))
|
||||
if author_name:
|
||||
bits.append(f"by {author_name}")
|
||||
bits.append(subject)
|
||||
if stats:
|
||||
additions = stats.get("additions")
|
||||
deletions = stats.get("deletions")
|
||||
total = stats.get("total")
|
||||
stat_bits = []
|
||||
if total is not None:
|
||||
stat_bits.append(f"{total} changed")
|
||||
if additions is not None:
|
||||
stat_bits.append(f"+{additions}")
|
||||
if deletions is not None:
|
||||
stat_bits.append(f"-{deletions}")
|
||||
if stat_bits:
|
||||
bits.append(", ".join(stat_bits))
|
||||
if html_url:
|
||||
bits.append(str(html_url))
|
||||
|
||||
text = " - ".join(bits)
|
||||
if not include_files:
|
||||
return text
|
||||
|
||||
files = commit.get("files")
|
||||
if not isinstance(files, list) or not files:
|
||||
return text
|
||||
|
||||
file_lines = []
|
||||
for changed_file in files[:20]:
|
||||
if not isinstance(changed_file, dict):
|
||||
continue
|
||||
filename = changed_file.get("filename") or changed_file.get("name")
|
||||
status = changed_file.get("status")
|
||||
if filename:
|
||||
file_lines.append(f"- {filename}" + (f" ({status})" if status else ""))
|
||||
if not file_lines:
|
||||
return text
|
||||
if len(files) > len(file_lines):
|
||||
file_lines.append(f"- ... {len(files) - len(file_lines)} more files")
|
||||
return text + "\nChanged files:\n" + "\n".join(file_lines)
|
||||
|
||||
|
||||
def _result(request_id: Any, result: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
|
||||
@@ -142,6 +142,34 @@ class GiteaClient:
|
||||
)
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
def list_commits(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
sha: str | None = None,
|
||||
path: str | None = None,
|
||||
limit: int = 20,
|
||||
page: int = 1,
|
||||
) -> list[dict[str, Any]]:
|
||||
query = {"limit": str(limit), "page": str(page)}
|
||||
if sha:
|
||||
query["sha"] = sha
|
||||
if path:
|
||||
query["path"] = path
|
||||
result = self._request(
|
||||
"GET",
|
||||
f"/repos/{_quote(owner)}/{_quote(repo)}/commits",
|
||||
query=query,
|
||||
)
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
def get_commit(self, owner: str, repo: str, sha: str) -> dict[str, Any]:
|
||||
result = self._request(
|
||||
"GET",
|
||||
f"/repos/{_quote(owner)}/{_quote(repo)}/git/commits/{_quote(sha)}",
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
|
||||
Reference in New Issue
Block a user