Skip to content

Support dynamic git provider #2662

Description

@misery

Feature request

I would like to make the Git provider system in PR-Agent fully extensible.

Currently, adding support for another Git hosting platform requires modifying PR-Agent itself and adding the provider to a central list of supported providers.

Instead, I would like to treat the Git provider system as a dynamic registry that can discover providers at runtime.

This would allow third-party projects or hosting platforms to provide their own Git provider as a separate Python package without requiring any changes to PR-Agent.

Maybe use Python package entry points to register Git providers dynamically.

[project.entry-points."pr_agent.git_providers"]
github = "pr_agent.git_providers.github_provider:GithubProvider"
gitlab = "pr_agent.git_providers.gitlab_provider:GitLabProvider"
bitbucket = "pr_agent.git_providers.bitbucket_provider:BitbucketProvider"
bitbucket_server = "pr_agent.git_providers.bitbucket_server_provider:BitbucketServerProvider"
azure = "pr_agent.git_providers.azuredevops_provider:AzureDevopsProvider"
codecommit = "pr_agent.git_providers.codecommit_provider:CodeCommitProvider"
local = "pr_agent.git_providers.local_git_provider:LocalGitProvider"
gerrit = "pr_agent.git_providers.gerrit_provider:GerritProvider"
gitea = "pr_agent.git_providers.gitea_provider:GiteaProvider"
plain-diff = "pr_agent.git_providers.plain_diff_provider:PlainDiffGitProvider"
from __future__ import annotations

from importlib.metadata import entry_points
from typing import Type

from pr_agent.git_providers.git_provider import GitProvider


ENTRY_POINT_GROUP = "pr_agent.git_providers"


def get_git_provider_classes() -> dict[str, Type[GitProvider]]:
    providers: dict[str, Type[GitProvider]] = {}

    for entry_point in entry_points(group=ENTRY_POINT_GROUP):
        provider = entry_point.load()

        if not isinstance(provider, type):
            raise TypeError(
                f"Git provider entry point {entry_point.name!r} "
                f"must point to a class"
            )

        if not issubclass(provider, GitProvider):
            raise TypeError(
                f"Git provider {entry_point.name!r} "
                f"must inherit from GitProvider"
            )

        provider_id = getattr(provider, "provider_id", None)

        if not provider_id:
            raise ValueError(
                f"Git provider {provider.__name__!r} "
                f"does not define provider_id"
            )

        if provider_id in providers:
            raise ValueError(
                f"Duplicate git provider id: {provider_id!r}"
            )

        providers[provider_id] = provider

    return providers


def get_git_provider_class(provider_id: str) -> Type[GitProvider]:
    providers = get_git_provider_classes()

    try:
        return providers[provider_id]
    except KeyError as e:
        available = ", ".join(sorted(providers)) or "<none>"

        raise ValueError(
            f"Unknown git provider: {provider_id!r}. "
            f"Available providers: {available}"
        ) from e


def _get_provider_id() -> str:
    try:
        provider_id = get_settings().config.git_provider
    except AttributeError as e:
        raise ValueError(
            "git_provider is a required attribute in the configuration file"
        ) from e

    if get_settings().get("plain_diff.content", None):
        return "plain-diff"

    return provider_id


def get_git_provider() -> type[GitProvider]:
    return get_git_provider_class(_get_provider_id())


def get_git_provider_with_context(pr_url) -> GitProvider:
    """
    Get a GitProvider instance for the given PR URL.
    If the GitProvider instance is already in the context, return it.
    """

    is_context_env = None

    try:
        is_context_env = context.get("settings", None)
    except Exception:
        pass

    if is_context_env and context.get("git_provider", {}).get(pr_url):
        return context["git_provider"][pr_url]

    try:
        provider_id = _get_provider_id()
        provider_class = get_git_provider_class(provider_id)

        git_provider = provider_class(pr_url)

        if is_context_env:
            context["git_provider"] = {
                pr_url: git_provider,
            }

        return git_provider

    except Exception as e:
        raise ValueError(
            f"Failed to get git provider for {pr_url}"
        ) from e

Motivation

This allows:

  • Custom git provider with special settings
  • Easier development of new git provider until it can be merged

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions