первый коммит
This commit is contained in:
217
app/modules/rag_repo/webhook_service.py
Normal file
217
app/modules/rag_repo/webhook_service.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
_STORY_ID_RE = re.compile(r"\b[A-Z][A-Z0-9_]*-\d+\b")
|
||||
|
||||
|
||||
class StoryCommitWriter(Protocol):
|
||||
def record_story_commit(
|
||||
self,
|
||||
*,
|
||||
story_id: str,
|
||||
project_id: str,
|
||||
title: str,
|
||||
commit_sha: str | None,
|
||||
branch: str | None,
|
||||
changed_files: list[str],
|
||||
summary: str,
|
||||
actor: str | None,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class RepoCacheWriter(Protocol):
|
||||
def record_repo_cache(
|
||||
self,
|
||||
*,
|
||||
project_id: str,
|
||||
commit_sha: str | None,
|
||||
changed_files: list[str],
|
||||
summary: str,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class RepoWebhookService:
|
||||
def __init__(self, story_writer: StoryCommitWriter, cache_writer: RepoCacheWriter | None = None) -> None:
|
||||
self._story_writer = story_writer
|
||||
self._cache_writer = cache_writer
|
||||
|
||||
def process(self, *, payload: dict, provider: str | None = None, headers: dict | None = None) -> dict:
|
||||
resolved_provider = self._resolve_provider(provider=provider, payload=payload, headers=headers or {})
|
||||
normalized = self._normalize(provider=resolved_provider, payload=payload)
|
||||
if not normalized:
|
||||
return {"accepted": False, "reason": "unsupported_or_invalid_payload"}
|
||||
|
||||
cache_recorded = False
|
||||
if self._cache_writer is not None:
|
||||
self._cache_writer.record_repo_cache(
|
||||
project_id=normalized["project_id"],
|
||||
commit_sha=normalized["commit_sha"],
|
||||
changed_files=normalized["changed_files"],
|
||||
summary=normalized["summary"],
|
||||
)
|
||||
cache_recorded = True
|
||||
|
||||
story_id = self._extract_story_id(normalized["messages"])
|
||||
if not story_id:
|
||||
return {
|
||||
"accepted": True,
|
||||
"indexed": False,
|
||||
"story_bound": False,
|
||||
"cache_recorded": cache_recorded,
|
||||
"reason": "story_id_not_found",
|
||||
}
|
||||
|
||||
self._story_writer.record_story_commit(
|
||||
story_id=story_id,
|
||||
project_id=normalized["project_id"],
|
||||
title=f"Story {story_id}",
|
||||
commit_sha=normalized["commit_sha"],
|
||||
branch=normalized["branch"],
|
||||
changed_files=normalized["changed_files"],
|
||||
summary=normalized["summary"],
|
||||
actor=normalized["actor"],
|
||||
)
|
||||
return {
|
||||
"accepted": True,
|
||||
"indexed": False,
|
||||
"story_bound": True,
|
||||
"cache_recorded": cache_recorded,
|
||||
"story_id": story_id,
|
||||
"project_id": normalized["project_id"],
|
||||
"commit_sha": normalized["commit_sha"],
|
||||
"changed_files": normalized["changed_files"],
|
||||
}
|
||||
|
||||
def _resolve_provider(self, *, provider: str | None, payload: dict, headers: dict[str, str]) -> str:
|
||||
value = (provider or "").strip().lower()
|
||||
if value in {"gitea", "bitbucket"}:
|
||||
return value
|
||||
|
||||
lowered = {str(k).lower(): str(v) for k, v in headers.items()}
|
||||
if "x-gitea-event" in lowered:
|
||||
return "gitea"
|
||||
if "x-event-key" in lowered:
|
||||
return "bitbucket"
|
||||
|
||||
if isinstance(payload.get("commits"), list) and ("ref" in payload or "pusher" in payload):
|
||||
return "gitea"
|
||||
push = payload.get("push")
|
||||
if isinstance(push, dict) and isinstance(push.get("changes"), list):
|
||||
return "bitbucket"
|
||||
return ""
|
||||
|
||||
def _normalize(self, *, provider: str, payload: dict) -> dict | None:
|
||||
key = provider.lower().strip()
|
||||
if key == "gitea":
|
||||
return self._normalize_gitea(payload)
|
||||
if key == "bitbucket":
|
||||
return self._normalize_bitbucket(payload)
|
||||
return None
|
||||
|
||||
def _normalize_gitea(self, payload: dict) -> dict:
|
||||
repo = payload.get("repository") or {}
|
||||
commits = payload.get("commits") or []
|
||||
project_id = str(repo.get("full_name") or repo.get("name") or "unknown_repo")
|
||||
ref = str(payload.get("ref") or "")
|
||||
branch = ref.replace("refs/heads/", "") if ref.startswith("refs/heads/") else ref or None
|
||||
actor = str((payload.get("pusher") or {}).get("username") or "") or None
|
||||
|
||||
messages: list[str] = []
|
||||
changed_files: set[str] = set()
|
||||
commit_sha: str | None = None
|
||||
for commit in commits:
|
||||
if not isinstance(commit, dict):
|
||||
continue
|
||||
cid = str(commit.get("id") or "").strip()
|
||||
if cid:
|
||||
commit_sha = cid
|
||||
msg = str(commit.get("message") or "").strip()
|
||||
if msg:
|
||||
messages.append(msg)
|
||||
for key in ("added", "modified", "removed"):
|
||||
for path in commit.get(key) or []:
|
||||
path_value = str(path).strip()
|
||||
if path_value:
|
||||
changed_files.add(path_value)
|
||||
|
||||
summary = messages[-1] if messages else "Webhook commit without message"
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"branch": branch,
|
||||
"commit_sha": commit_sha,
|
||||
"changed_files": sorted(changed_files),
|
||||
"messages": messages,
|
||||
"summary": summary,
|
||||
"actor": actor,
|
||||
}
|
||||
|
||||
def _normalize_bitbucket(self, payload: dict) -> dict:
|
||||
repo = payload.get("repository") or {}
|
||||
project_id = str(repo.get("full_name") or repo.get("name") or "unknown_repo")
|
||||
|
||||
changes = (((payload.get("push") or {}).get("changes")) or [])
|
||||
messages: list[str] = []
|
||||
changed_files: set[str] = set()
|
||||
commit_sha: str | None = None
|
||||
branch: str | None = None
|
||||
actor = None
|
||||
|
||||
actor_raw = payload.get("actor") or {}
|
||||
if isinstance(actor_raw, dict):
|
||||
actor = str(actor_raw.get("display_name") or actor_raw.get("username") or "") or None
|
||||
|
||||
for change in changes:
|
||||
if not isinstance(change, dict):
|
||||
continue
|
||||
new_ref = change.get("new") or {}
|
||||
if isinstance(new_ref, dict):
|
||||
branch_name = str(new_ref.get("name") or "").strip()
|
||||
if branch_name:
|
||||
branch = branch_name
|
||||
target = new_ref.get("target") or {}
|
||||
if isinstance(target, dict):
|
||||
h = str(target.get("hash") or "").strip()
|
||||
if h:
|
||||
commit_sha = h
|
||||
msg = str(target.get("message") or "").strip()
|
||||
if msg:
|
||||
messages.append(msg)
|
||||
|
||||
for commit in change.get("commits") or []:
|
||||
if not isinstance(commit, dict):
|
||||
continue
|
||||
h = str(commit.get("hash") or "").strip()
|
||||
if h:
|
||||
commit_sha = h
|
||||
msg = str(commit.get("message") or "").strip()
|
||||
if msg:
|
||||
messages.append(msg)
|
||||
for key in ("added", "modified", "removed"):
|
||||
for item in commit.get(key) or []:
|
||||
if isinstance(item, dict):
|
||||
path_value = str(item.get("path") or "").strip()
|
||||
else:
|
||||
path_value = str(item).strip()
|
||||
if path_value:
|
||||
changed_files.add(path_value)
|
||||
|
||||
summary = messages[-1] if messages else "Webhook commit without message"
|
||||
return {
|
||||
"project_id": project_id,
|
||||
"branch": branch,
|
||||
"commit_sha": commit_sha,
|
||||
"changed_files": sorted(changed_files),
|
||||
"messages": messages,
|
||||
"summary": summary,
|
||||
"actor": actor,
|
||||
}
|
||||
|
||||
def _extract_story_id(self, messages: list[str]) -> str | None:
|
||||
for msg in messages:
|
||||
match = _STORY_ID_RE.search(msg)
|
||||
if match:
|
||||
return match.group(0)
|
||||
return None
|
||||
Reference in New Issue
Block a user