feat(bot): Коммит — сообщение, Push, конфликты, публикация в репозиторий загрузок

- Publisher: пуши по одному (lock), fetch → проверка конфликта с base_sha → чистое дерево
  origin/main → черновик → коммит от tgit-bot с трейлером Telegram-User → push;
  отказ push → rebase и повтор (до 3 раз); защита от путей вне клона; лимит 20 пушей/час
- «💬 Сообщение коммита» (1–200 символов, первая строка ≤ 72) и «🚀 Push» с подтверждением;
  исходы: запушено (sha, ссылка, ⏳ CI) · конфликт → «Перезаписать» · нечего коммитить · лимит · сбой
- запись в pushes (chat_id, message_id — для обновления статуса CI/CD в #11), черновик очищается
- ruff: RUF001–003 выключены — кириллица в текстах это норма

Refs #9

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 19:23:09 +03:00
co-authored by Claude Opus 5.5
parent 614f8eafd5
commit ae0cd5d42d
11 changed files with 526 additions and 4 deletions
+8 -3
View File
@@ -19,19 +19,24 @@ from app.health import heartbeat
from app.logs_setup import setup_logging
from app.repo.drafts import DraftService
from app.repo.git import UploadsRepo
from app.repo.publisher import Publisher
from app.texts import Texts, load_texts
log = logging.getLogger("tgit")
def build_dispatcher(texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo) -> Dispatcher:
def build_dispatcher(
texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo, commit_url_template: str = ""
) -> Dispatcher:
dp = Dispatcher(storage=make_storage(sessions))
gate = AccessMiddleware(access, texts)
# Outer middleware: runs before filters, so nothing — not even a stale button — reaches a handler unchecked.
dp.message.outer_middleware(gate)
dp.callback_query.outer_middleware(gate)
dp.include_router(members.build_router(texts, access))
dp.include_router(commit.build_router(texts, repo, DraftService(sessions)))
dp.include_router(
commit.build_router(texts, repo, DraftService(sessions), Publisher(repo, sessions), commit_url_template)
)
dp.include_router(common.build_router(texts, access))
return dp
@@ -43,7 +48,7 @@ async def run(settings: Settings) -> None:
sessions = make_sessionmaker(engine)
access = AccessService(sessions, settings.access)
await access.bootstrap_admins()
dp = build_dispatcher(texts, sessions, access, UploadsRepo(settings.git))
dp = build_dispatcher(texts, sessions, access, UploadsRepo(settings.git), settings.git.commit_url_template)
rt = settings.runtime
background = [
asyncio.create_task(heartbeat(rt.heartbeat_file, rt.heartbeat_interval)),