feat(bot): Коммит — навигация по дереву репозитория загрузок

- UploadsRepo: локальный клон, fetch не чаще fetch_interval, ls-tree -z (пробелы/кириллица),
  cat-file, последний коммит по пути; git без shell, ssh с -F /dev/null (без конфига хоста)
- «📝 Коммит»: папки/файлы с размерами, ⬆️ Вверх, страницы по 20, карточка файла
  с последним коммитом, «👁 Показать» (<pre> или документом); в кнопках только индексы
- ключ для контейнера: GIT_SSH_KEY_B64 → временный файл 0600 (смонтированный файл хоста
  контейнерному uid не прочитать); volume /data для клона
- тесты на настоящем bare-репозитории во временной папке

Refs #8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 19:10:30 +03:00
co-authored by Claude Opus 5.5
parent 219bebd5a2
commit dbb17bd774
17 changed files with 692 additions and 12 deletions
+5 -3
View File
@@ -10,25 +10,27 @@ from app import __version__
from app.access.service import AccessService
from app.bot.fsm import make_storage
from app.bot.middlewares.access import AccessMiddleware
from app.bot.routers import common, members
from app.bot.routers import commit, common, members
from app.config import Settings
from app.db.maintenance import cleanup_loop
from app.db.migrate import upgrade
from app.db.session import SessionMaker, make_engine, make_sessionmaker
from app.health import heartbeat
from app.logs_setup import setup_logging
from app.repo.git import UploadsRepo
from app.texts import Texts, load_texts
log = logging.getLogger("tgit")
def build_dispatcher(texts: Texts, sessions: SessionMaker, access: AccessService) -> Dispatcher:
def build_dispatcher(texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo) -> 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))
dp.include_router(common.build_router(texts, access))
return dp
@@ -40,7 +42,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)
dp = build_dispatcher(texts, sessions, access, UploadsRepo(settings.git))
rt = settings.runtime
background = [
asyncio.create_task(heartbeat(rt.heartbeat_file, rt.heartbeat_interval)),