From e1dd9e558863dd3ef7cb0721193407c0bdfd77e7 Mon Sep 17 00:00:00 2001 From: Bastien Gourdon Date: Tue, 21 Jul 2026 18:54:45 +0200 Subject: [PATCH] feat: tools list_commits et get_commit (historique git) --- mcp_forgejo_server.py | 111 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/mcp_forgejo_server.py b/mcp_forgejo_server.py index 4a3ae78..4f11391 100644 --- a/mcp_forgejo_server.py +++ b/mcp_forgejo_server.py @@ -139,6 +139,34 @@ TOOLS = [ "required": ["repo", "path"], }, }, + + { + "name": "list_commits", + "description": "Historique des commits d un depot (sha, auteur, date, message). path optionnel : limite l historique a un fichier ou dossier precis. Ne retourne pas les diffs.", + "inputSchema": { + "type": "object", + "properties": { + "repo": {"type": "string", "description": "proprietaire/depot"}, + "path": {"type": "string", "description": "Optionnel - limite l historique a ce fichier ou dossier"}, + "ref": {"type": "string", "description": "Branche, tag ou commit (defaut: branche par defaut)"}, + "limit": {"type": "integer", "description": "Nombre de commits (defaut 20, max 50)"}, + }, + "required": ["repo"], + }, + }, + { + "name": "get_commit", + "description": "Detail d un commit : metadonnees, fichiers touches et statistiques. include_diff=true ajoute le diff complet (refuse au-dela de la limite de taille).", + "inputSchema": { + "type": "object", + "properties": { + "repo": {"type": "string", "description": "proprietaire/depot"}, + "sha": {"type": "string", "description": "SHA du commit (complet ou abrege)"}, + "include_diff": {"type": "boolean", "description": "Inclure le diff complet (defaut false)"}, + }, + "required": ["repo", "sha"], + }, + }, ] # --------------------------------------------------------------------------- @@ -241,12 +269,95 @@ def tool_get_metadata(repo, path, ref=None): "download_url": data.get("download_url"), } +MAX_DIFF_BYTES = 100000 # garde-fou diff : au-dela, on renvoie seulement les stats + + +def tool_list_commits(repo, path=None, ref=None, limit=20): + err = _valider_repo(repo) or _valider_path(path) + if err: + return err + try: + limit = int(limit) + except (TypeError, ValueError): + return {"error": "limit doit etre un entier"} + limit = max(1, min(limit, 50)) + params = {"limit": limit, "stat": "false", "verification": "false", "files": "false"} + if path: + params["path"] = path.lstrip("/") + if ref: + params["sha"] = ref + data = forgejo_api("GET", "/api/v1/repos/%s/commits" % repo, params=params) or [] + out = [] + for c in data: + commit = c.get("commit") or {} + author = commit.get("author") or {} + message = (commit.get("message") or "").strip() + parts = message.split("\n", 1) + out.append({ + "sha": (c.get("sha") or "")[:10], + "sha_full": c.get("sha"), + "date": author.get("date"), + "author": author.get("name"), + "subject": parts[0], + "body": parts[1].strip() if len(parts) > 1 else "", + }) + return {"repo": repo, "path": path or "", "count": len(out), "commits": out} + + +def tool_get_commit(repo, sha, include_diff=False): + err = _valider_repo(repo) + if err: + return err + if not sha or "/" in sha or ".." in sha: + return {"error": "sha invalide"} + data = forgejo_api("GET", "/api/v1/repos/%s/git/commits/%s" % (repo, sha)) + if not data: + return {"error": "Commit introuvable : %s" % sha} + commit = data.get("commit") or {} + author = commit.get("author") or {} + stats = data.get("stats") or {} + message = (commit.get("message") or "").strip() + parts = message.split("\n", 1) + result = { + "repo": repo, + "sha": data.get("sha"), + "date": author.get("date"), + "author": author.get("name"), + "subject": parts[0], + "body": parts[1].strip() if len(parts) > 1 else "", + "stats": {"additions": stats.get("additions"), + "deletions": stats.get("deletions"), + "total": stats.get("total")}, + "files": [{"filename": f.get("filename"), + "status": f.get("status"), + "additions": f.get("additions"), + "deletions": f.get("deletions")} + for f in (data.get("files") or [])], + } + if include_diff: + url = "%s/api/v1/repos/%s/git/commits/%s.diff" % (FORGEJO_URL.rstrip("/"), repo, sha) + resp = requests.get(url, headers={"Authorization": "token %s" % FORGEJO_TOKEN}, timeout=30) + resp.raise_for_status() + raw = resp.content + if len(raw) > MAX_DIFF_BYTES: + result["diff_error"] = ("Diff trop volumineux (%d octets, limite %d). " + "Utiliser read_file sur les fichiers concernes." + % (len(raw), MAX_DIFF_BYTES)) + else: + try: + result["diff"] = raw.decode("utf-8") + except UnicodeDecodeError: + result["diff_error"] = "Diff non decodable en UTF-8 (fichiers binaires)" + return result + DISPATCH = { "list_repos": tool_list_repos, "list_files": tool_list_files, "read_file": tool_read_file, "get_metadata": tool_get_metadata, + "list_commits": tool_list_commits, + "get_commit": tool_get_commit, } # ---------------------------------------------------------------------------