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
+4 -3
View File
@@ -6,14 +6,15 @@ from app.access.service import AccessService
from app.bot.menu import Nav, NavAction
from app.db.session import SessionMaker
from app.main import build_dispatcher
from app.repo.git import UploadsRepo
from app.texts import Texts
from tests.conftest import ADMIN, OUTSIDER
from tests.fakes import TgClient
@pytest.fixture
def tg(texts: Texts, sessions: SessionMaker, access: AccessService) -> TgClient:
return TgClient(build_dispatcher(texts, sessions, access))
def tg(texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo) -> TgClient:
return TgClient(build_dispatcher(texts, sessions, access, repo))
def buttons(markup: ReplyKeyboardMarkup) -> list[str]:
@@ -45,7 +46,7 @@ async def test_outsider_stale_button_is_swallowed(tg: TgClient) -> None:
assert len(tg.session.sent(AnswerCallbackQuery)) == 1
@pytest.mark.parametrize("section", ["commit", "status", "logs", "alerts"])
@pytest.mark.parametrize("section", ["status", "logs", "alerts"])
async def test_menu_button_opens_section_with_nav(tg: TgClient, texts: Texts, section: str) -> None:
await tg.send(ADMIN, getattr(texts.menu, section))