wrolpi/modules/repos/conftest.py
Roland b3255e715c
Add a Repos module that mirrors git repositories (backend)
A Repo is a Collection (kind='repo') plus a `repository` row, kept up to
date by a recurring Download handled by the new `git` downloader.

* Full history by default (all branches and tags, no --prune; a force
  push saves the old HEAD under refs/wrolpi/backup/); snapshot mode
  (--depth 1) and a pinned branch are optional.  `/tree/<branch>` URLs
  select a branch.
* git runs HTTPS-only, never prompts, never runs hooks, and checks out
  symlinks as plain files.  Upstream disappearing defers the download
  with git's error; the local copy is kept.
* Repos are cloned to `repos_destination` (repos/%(repo_tag)s/%(repo_name)s)
  and the `repos` directory is always ignored, so repo files are never
  indexed.
* repos.yaml is the source of truth (import, dump, backup restore).
* /api/repos: list, create, get, edit, delete (files kept unless
  delete_files), update now, and a per-directory file tree.  Tagging uses
  the collections tag endpoint.

Also:
* run_command/process_runner can merge stderr into stdout (git writes
  progress to stderr).
* move_collection deletes an old directory that only holds empty
  directories (the move left an emptied .git behind).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:56:09 -06:00

87 lines
3.2 KiB
Python

import os
import pathlib
import subprocess
from typing import Callable
import pytest
from mock import mock
from modules.repos import lib
from modules.repos.downloader import GitDownloader
from wrolpi.downloader import DownloadManager
# Commits made by the tests have a fixed identity and do not read the developer's git config.
TEST_GIT_ENV = dict(
GIT_AUTHOR_NAME='WROLPi Test', GIT_AUTHOR_EMAIL='test@example.com',
GIT_COMMITTER_NAME='WROLPi Test', GIT_COMMITTER_EMAIL='test@example.com',
GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,
)
class GitRemote:
"""A local repo which the Repos module sees as `https://git.example.com/<owner>/<name>`."""
def __init__(self, path: pathlib.Path, url: str):
self.path = path
self.url = url
def git(self, *args: str) -> str:
env = dict(os.environ, **TEST_GIT_ENV)
result = subprocess.run(['git', *args], cwd=self.path, env=env, check=True, capture_output=True)
return result.stdout.decode().strip()
def commit(self, files: dict, message: str = 'Commit') -> str:
"""Write `files` ({relative path: contents}), commit them, return the commit's sha."""
for name, contents in files.items():
file = self.path / name
file.parent.mkdir(parents=True, exist_ok=True)
file.write_text(contents)
self.git('add', '--all')
self.git('commit', '--quiet', '-m', message)
return self.git('rev-parse', 'HEAD')
@property
def head(self) -> str:
return self.git('rev-parse', 'HEAD')
@pytest.fixture
def git_remote_factory(tmp_path_factory) -> Callable[..., GitRemote]:
"""Create local git repos which are cloned over `https://git.example.com/...` URLs.
git's `url.<base>.insteadOf` rewrites the https URL to the local path, and `file://` is allowed for the test,
so the Repos module runs the same commands it runs in production."""
remotes = dict()
# Outside the media directory (which is `tmp_path`).
remotes_directory = tmp_path_factory.mktemp('remotes')
def git_env():
env = original_git_env()
env['GIT_CONFIG_COUNT'] = str(len(remotes))
for idx, (url, path) in enumerate(remotes.items()):
env[f'GIT_CONFIG_KEY_{idx}'] = f'url.file://{path}.insteadOf'
env[f'GIT_CONFIG_VALUE_{idx}'] = url
return env
original_git_env = lib.git_env
def factory(name: str = 'example', owner: str = 'owner', branch: str = 'main',
files: dict = None) -> GitRemote:
path = remotes_directory / owner / name
path.mkdir(parents=True)
url = f'https://git.example.com/{owner}/{name}'
remote = GitRemote(path, url)
remote.git('init', '--quiet', f'--initial-branch={branch}')
remote.commit(files or {'README.md': f'# {name}\n\nThe {name} repo.\n'}, 'Initial commit')
remotes[url] = path
return remote
with mock.patch('modules.repos.lib.ALLOWED_PROTOCOLS', ('https', 'file')), \
mock.patch('modules.repos.downloader.git_env', git_env):
yield factory
@pytest.fixture
def repos_download_manager(test_download_manager) -> DownloadManager:
test_download_manager.register_downloader(GitDownloader())
yield test_download_manager