diff --git a/src/app/container.py b/src/app/container.py index d00b2ac..a51c151 100644 --- a/src/app/container.py +++ b/src/app/container.py @@ -17,7 +17,7 @@ class Container(containers.DeclarativeContainer): config = providers.Configuration() wiring_config = containers.WiringConfiguration( - packages=["app.repos", "app.commits", "core.repos", "core.commits"], + packages=["app.repos", "app.commits", "app.files"], ) # ==== AppConfig ===== diff --git a/src/app/files/__init__.py b/src/app/files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/files/files.py b/src/app/files/files.py new file mode 100644 index 0000000..8e88b4d --- /dev/null +++ b/src/app/files/files.py @@ -0,0 +1,36 @@ +import click +from loguru import logger + +from app.container import Container + +from core.files.queries.queries import FilesQueries + +from dependency_injector.wiring import Provide, inject + + +@click.option("--name", type=str, required=True) +@click.option("--file", type=str, required=True) +@click.command() +@inject +def history( + name: str, + file: str, + file_queries: FilesQueries = Provide[Container.core.file_queries], +) -> None: + """ + Lists the history of a file in a repository. + + Args: + name (str): The name of the repository. + file (str): The path to the file within the repository. + file_queries (FilesQueries): The queries to fetch repositories. + """ + files = file_queries.get_file_history(file, name) + + if not files: + logger.info("No file history found.") + return + + logger.info(f"File history for '{file}' in repository '{name}':") + for file in files: + logger.info(file) diff --git a/src/app/files/group.py b/src/app/files/group.py new file mode 100644 index 0000000..1eb7cab --- /dev/null +++ b/src/app/files/group.py @@ -0,0 +1,14 @@ +import click + +from app.files.files import history + + +@click.group() +def files(): + """ + File Management Commands + """ + pass + + +files.add_command(history) diff --git a/src/core/container.py b/src/core/container.py index 616f863..5f8f02c 100644 --- a/src/core/container.py +++ b/src/core/container.py @@ -1,12 +1,17 @@ from dependency_injector import containers, providers from app.config import AppConfig -from core.commits.queries.queries import CommitQueries + +from core.interfaces.git_client import GitClient from core.interfaces.query_client import QueryClient from core.interfaces.search_client import SearchClient + from core.repos.operations.add_repo_operation import AddRepoOperation from core.repos.operations.delete_repo_operation import DeleteRepoOperation from core.repos.operations.update_repo_operation import UpdateRepoOperation + +from core.commits.queries.queries import CommitQueries +from core.files.queries.queries import FilesQueries from core.repos.queries.queries import RepoQueries @@ -19,6 +24,10 @@ class Container(containers.DeclarativeContainer): app_config = providers.Singleton(AppConfig) + wiring_config = containers.WiringConfiguration( + packages=["core.repos", "core.commits", "core.files"], + ) + query_client: providers.Provider[QueryClient] = providers.AbstractSingleton( instance_of=QueryClient, app_config=app_config, @@ -29,6 +38,10 @@ class Container(containers.DeclarativeContainer): app_config=app_config, ) + git_client: providers.Provider[GitClient] = providers.AbstractSingleton( + instance_of=GitClient, + ) + # Repo add_repo_operation = providers.Factory( AddRepoOperation, @@ -52,11 +65,15 @@ class Container(containers.DeclarativeContainer): repo_queries = providers.Factory( RepoQueries, query_client=query_client, - search_client=search_client, ) commit_queries = providers.Factory( CommitQueries, query_client=query_client, - search_client=search_client, + ) + + file_queries = providers.Factory( + FilesQueries, + git_client=git_client, + query_client=query_client, ) diff --git a/src/core/files/__init__.py b/src/core/files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/files/queries/__init__.py b/src/core/files/queries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/files/queries/models.py b/src/core/files/queries/models.py new file mode 100644 index 0000000..9a6eb5c --- /dev/null +++ b/src/core/files/queries/models.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class FileQueryModel: + commit_hash: str + message: str + author: str + date: str diff --git a/src/core/files/queries/queries.py b/src/core/files/queries/queries.py new file mode 100644 index 0000000..86d9852 --- /dev/null +++ b/src/core/files/queries/queries.py @@ -0,0 +1,50 @@ +from typing import List + +from core.interfaces.git_client import GitClient +from core.interfaces.query_client import QueryClient +from entities.repos import Repo + +from core.files.queries.models import FileQueryModel + + +class FilesQueries: + def __init__(self, git_client: GitClient, query_client: QueryClient) -> None: + """ + Initialize the FilesQueries class. + This class is responsible for executing queries related to repositories. + + Args: + git_client (GitClient): An instance of GitClient for Git operations. + query_client (QueryClient): An instance of QueryClient for database operations. + """ + self.git_client = git_client + self.query_client = query_client + + def get_file_history(self, file_path: str, repo_name: str) -> List[FileQueryModel]: + """ + Returns the history of a file in the repository. + + Args: + file_path (str): The path to the file within the repository. + repo_name (str): The path to the repository. + """ + with self.query_client.session() as session: + repo: Repo | None = ( + session.query(Repo).filter(Repo.name == repo_name).one_or_none() + ) + + if not repo: + return [] + + results = [] + for commit in self.git_client.iter_file_history(file_path, repo.path): + results.append( + FileQueryModel( + commit_hash=commit.hexsha, + message=commit.message.strip(), + author=commit.author.name, + date=commit.committed_datetime.isoformat(), + ) + ) + + return results diff --git a/src/core/interfaces/git_client.py b/src/core/interfaces/git_client.py new file mode 100644 index 0000000..03200a2 --- /dev/null +++ b/src/core/interfaces/git_client.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod +from typing import Iterator + +from git import Commit + + +class GitClient(ABC): + @abstractmethod + def iter_file_history(self, file_name: str, repo_path: str) -> Iterator[Commit]: + """ + Retrieve the history of a file in the repository. + + Args: + file_name (str): The path to the file relative to the repository root. + repo_path (str): The path to the repository. + """ + pass diff --git a/src/libs/container.py b/src/libs/container.py index f16e7f4..c06826d 100644 --- a/src/libs/container.py +++ b/src/libs/container.py @@ -3,6 +3,7 @@ from app.config import AppConfig from libs.chromadb.providers import ChromaClient from libs.duckdb.provider import DuckDbClient +from libs.git.providers import PythonGitClient class Container(containers.DeclarativeContainer): @@ -19,6 +20,7 @@ class Container(containers.DeclarativeContainer): AppConfig, ) + # note these override the dependencies in core.container.Container # Chroma Client search_client = providers.Singleton( ChromaClient, @@ -30,3 +32,8 @@ class Container(containers.DeclarativeContainer): DuckDbClient, app_config=app_config, ) + + # Git Client + git_client = providers.Singleton( + PythonGitClient, + ) diff --git a/src/libs/git/providers.py b/src/libs/git/providers.py new file mode 100644 index 0000000..baff4a0 --- /dev/null +++ b/src/libs/git/providers.py @@ -0,0 +1,22 @@ +from typing import Iterator + +from git import Repo, Commit + +from core.interfaces.git_client import GitClient + + +class PythonGitClient(GitClient): + def iter_file_history(self, file_name: str, repo_path: str) -> Iterator[Commit]: + """ + Retrieve the history of a file in the repository. + + Args: + file_name (str): The path to the file within the repository. + repo_path (str): The path to the repository. + + Returns: + Dict: A dictionary containing the file history. + """ + git_repo = Repo(repo_path) + + return git_repo.iter_commits(paths=file_name) diff --git a/src/libs/git/service.py b/src/libs/git/service.py deleted file mode 100644 index 733bd34..0000000 --- a/src/libs/git/service.py +++ /dev/null @@ -1,2 +0,0 @@ -class GitService: - pass diff --git a/src/main.py b/src/main.py index a7d374e..f0608a3 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,7 @@ from app.repos.group import repos from app.commits.group import commits +from app.files.group import files @click.group() @@ -20,6 +21,7 @@ def cli(): cli.add_command(repos) cli.add_command(commits) +cli.add_command(files) if __name__ == "__main__": cli() diff --git a/tests/core/repos/operations/test_update_repo_operation.py b/tests/core/repos/operations/test_update_repo_operation.py index c9a270d..deb2b47 100644 --- a/tests/core/repos/operations/test_update_repo_operation.py +++ b/tests/core/repos/operations/test_update_repo_operation.py @@ -62,7 +62,7 @@ def test_execute_should_add_commits_when_commits_do_not_exist_in_db(): # Arrange client = MagicMock() session = MagicMock() - repo = Repo(id=1,name="test-repo", path="/old/path/to/repo") + repo = Repo(id=1, name="test-repo", path="/old/path/to/repo") commit1 = Commit(id=1, commit_hash="abc123", repo_id=repo.id) session.query.return_value.filter.return_value.one_or_none.side_effect = [ repo, @@ -83,7 +83,7 @@ def mock_assign_id_that_flush_would_assign(commit): mock_git_repo = MagicMock() mock_git_repo.iter_commits.return_value = [ MagicMock(hexsha="abc123"), - MagicMock(hexsha="def456", message="New commit message") + MagicMock(hexsha="def456", message="New commit message"), ] with patch( "core.repos.operations.update_repo_operation.GitRepo", @@ -92,19 +92,15 @@ def mock_assign_id_that_flush_would_assign(commit): op = UpdateRepoOperation(query_client=client, search_client=search_client) op.execute("test-repo") - # Assert + # Assert session.commit.assert_called() session.add.assert_called() mock_git_repo.iter_commits.assert_called_once() search_client.add_to_collection.assert_called_with( collection_name="commits", data="New commit message", - id=f"rep1_com1", - metadata={ - "repo_id": repo.id, - "commit_id": "1", - "commit_hash": "def456" - }, + id="rep1_com1", + metadata={"repo_id": repo.id, "commit_id": "1", "commit_hash": "def456"}, )