Architecture ============ dist-git-manager uses a plugin-based architecture that separates concerns and allows easy extension. Overview -------- :: ┌─────────────────────────────────────────────────────────┐ │ CLI Interface │ │ (config-driven OR CLI-only, single repo mode) │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ DistGitManager │ │ (orchestrates entire workflow) │ └─────────────────────────────────────────────────────────┘ │ ┌──────────────┼──────────────┐ │ │ │ ┌────────┴────────┐ ┌┴──────┐ ┌──┴───────┐ │ VersionSource │ │ Spec │ │ Git │ │ (plugin) │ │Manager│ │ Manager │ └─────────────────┘ └───────┘ └──────────┘ Core Components --------------- DistGitManager ~~~~~~~~~~~~~~ **Location:** ``src/dist_git_manager/core/manager.py`` The main orchestrator that: 1. Loads configuration 2. Instantiates plugins (version sources and spec managers) 3. For each project: - Fetches latest version from version source - Clones/updates git repository - Determines current version from spec file - If new version available: - Determines target branch (via BranchManager) - Updates spec file (via SpecManager) - Creates/updates release branches hierarchically - Commits and optionally pushes changes 4. Implements file locking to prevent concurrent runs 5. Continues processing on individual project failures GitOperations ~~~~~~~~~~~~~ **Location:** ``src/dist_git_manager/core/git_ops.py`` Wrapper around git commands with: - Dry-run support (logs commands without executing) - Clone, fetch, branch, checkout operations - Commit with custom author/email - Push with auto-push option - Fast-forward merging BranchManager ~~~~~~~~~~~~~ **Location:** ``src/dist_git_manager/core/branch_mgr.py`` Handles hierarchical branch management: **For SemVer releases:** - Detects release series (X.Y) from version - Creates ``release-X.Y`` branches - Determines superior branches that should fast-forward merge - Performs hierarchical merges (release-1.20 → release-1 → main) **For rolling releases:** - Simply checks out default branch **Key methods:** - ``handle_semver_update()``: Determine and checkout target branch - ``finalize_semver_update()``: Create branches and perform merges - ``handle_rolling_update()``: Checkout default branch VersionParser ~~~~~~~~~~~~~ **Location:** ``src/dist_git_manager/core/version_parser.py`` Parses and compares versions: - Supports SemVer (X.Y.Z) - Supports PEP 440 (Python versions) - Supports Cargo versions (Rust) - Extracts release tracks (major.minor) - Compares versions to determine if newer Plugin Architecture ------------------- Version Sources ~~~~~~~~~~~~~~~ **Base class:** ``src/dist_git_manager/version_sources/base.py`` Abstract interface: .. code-block:: python class VersionSource(ABC): @abstractmethod def get_latest_version( self, identifier: str, release_type: str = "semver" ) -> Optional[str]: """Get latest version for a package.""" pass **Implementations:** - ``AnityaVersionSource``: Queries Anitya Release Monitoring API - ``CratesIOVersionSource``: Parses crates.io sparse index (NDJSON) - ``PyPIVersionSource``: Queries PyPI JSON API **Adding a new version source:** 1. Create ``src/dist_git_manager/version_sources/new_source.py`` 2. Inherit from ``VersionSource`` 3. Implement ``get_latest_version()`` 4. Register in ``DistGitManager._get_version_source()`` 5. Add to CLI choices and validation Spec Managers ~~~~~~~~~~~~~ **Base class:** ``src/dist_git_manager/spec_managers/base.py`` Abstract interface: .. code-block:: python class SpecManager(ABC): @abstractmethod def update_spec( self, repo_path: Path, branch: str, package_name: str, version: str, dry_run: bool ) -> None: """Update spec file to new version.""" pass @abstractmethod def parse_version(self, spec_path: Path) -> Optional[str]: """Extract current version from spec file.""" pass @abstractmethod def find_spec_file( self, repo_path: Path, package_name: str ) -> Optional[Path]: """Find spec file in repository.""" pass **Implementations:** - ``PackitSpecManager``: Uses Packit to update existing specs - ``Rust2RpmSpecManager``: Generates specs from crates.io metadata - ``PyP2SpecSpecManager``: Generates specs from PyPI metadata **Adding a new spec manager:** 1. Create ``src/dist_git_manager/spec_managers/new_manager.py`` 2. Inherit from ``SpecManager`` 3. Implement required methods 4. Register in ``DistGitManager._get_spec_manager()`` 5. Add to CLI choices and validation Configuration System -------------------- **Location:** ``src/dist_git_manager/config.py`` The ``Config`` class: - Loads YAML configuration files - Builds configuration from CLI arguments - Validates configuration (required fields, valid values) - Supports global plugin defaults with per-project overrides - Provides accessor methods (``get_project_version_source()``, etc.) **Two modes:** 1. **Config-driven:** Load from YAML file with ``Config.from_file()`` 2. **CLI-only:** Build from args with ``Config.from_cli_args()`` CLI can override YAML values when both provided. Optional Components ------------------- DNFPackageChecker ~~~~~~~~~~~~~~~~~ **Location:** ``src/dist_git_manager/core/dnf_checker.py`` Checks if packages already exist in DNF repositories: - Supports different provider templates: - ``crate({name})`` for Rust - ``python3dist({name})`` for Python - ``{name}`` for general packages - Implements caching for performance - Gracefully degrades if DNF unavailable DependencyResolver ~~~~~~~~~~~~~~~~~~ **Location:** ``src/dist_git_manager/dependency/resolver.py`` Framework for recursive dependency resolution: - Abstract base class for language-specific resolvers - Cycle detection - Integration with DNFPackageChecker to skip existing packages - CratesDependencyResolver implementation for Rust Testing Architecture -------------------- **Unit Tests** Located in ``tests/unit/``: - Mock external dependencies (HTTP, subprocess, filesystem) - Test components in isolation - Fast, offline, high coverage **Fixtures:** - ``responses`` library for HTTP mocking - ``unittest.mock`` for subprocess mocking - ``tempfile`` for filesystem isolation **Integration Tests** Located in ``tests/integration/``: - Use real git repos in temp directories - Mock HTTP APIs and tool subprocess calls - Test full workflows end-to-end - Verify branch states and file changes Error Handling -------------- Custom exception hierarchy: - ``DistGitManagerError``: Base exception - ``ConfigError``: Configuration validation failures - ``GitError``: Git operation failures - ``VersionSourceError``: Version fetching failures - ``SpecManagerError``: Spec update failures - ``BranchManagementError``: Branch operation failures - ``DependencyResolutionError``: Dependency resolution failures - ``LockError``: File locking failures Logging ------- **Location:** ``src/dist_git_manager/logger.py`` - Centralized logger setup - Supports verbose mode - Structured logging for different components - Debug level includes subprocess commands Data Flow --------- 1. **CLI** parses arguments or loads YAML config 2. **Config** validates and builds configuration object 3. **DistGitManager** acquires lock and processes each project: a. Instantiates **VersionSource** plugin b. Fetches latest version c. **GitOperations** clones/updates repository d. Instantiates **SpecManager** plugin e. Parses current version from spec f. If update needed: - **BranchManager** determines target branch - **SpecManager** updates spec file - **GitOperations** commits changes - **BranchManager** creates/merges branches - **GitOperations** pushes if enabled 4. **DistGitManager** releases lock Design Principles ----------------- **Separation of Concerns** Each component has a single, well-defined responsibility. **Plugin Architecture** Easy to add new version sources and spec managers without modifying core code. **Dry-Run Support** All components support dry-run mode for safe testing. **Offline Testing** External dependencies (HTTP, subprocess) are mocked for fast, reliable tests. **Graceful Degradation** Optional features (DNF checking) degrade gracefully when dependencies unavailable. **Configuration Flexibility** Supports both YAML config-driven and CLI-only modes.