Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 =====
Expand Down
Empty file added src/app/files/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions src/app/files/files.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions src/app/files/group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import click

from app.files.files import history


@click.group()
def files():
"""
File Management Commands
"""
pass


files.add_command(history)
23 changes: 20 additions & 3 deletions src/core/container.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
)
Empty file added src/core/files/__init__.py
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions src/core/files/queries/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from dataclasses import dataclass


@dataclass
class FileQueryModel:
commit_hash: str
message: str
author: str
date: str
50 changes: 50 additions & 0 deletions src/core/files/queries/queries.py
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
Banyango marked this conversation as resolved.
"""
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 list(results)
Comment thread
Banyango marked this conversation as resolved.
Outdated
17 changes: 17 additions & 0 deletions src/core/interfaces/git_client.py
Original file line number Diff line number Diff line change
@@ -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 name of the file within the repository.
repo_path (str): The path to the file within the repository.
Comment thread
Banyango marked this conversation as resolved.
Outdated
Comment thread
Banyango marked this conversation as resolved.
Outdated
"""
pass
7 changes: 7 additions & 0 deletions src/libs/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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,
Expand All @@ -30,3 +32,8 @@ class Container(containers.DeclarativeContainer):
DuckDbClient,
app_config=app_config,
)

# Git Client
git_client = providers.Singleton(
PythonGitClient,
)
22 changes: 22 additions & 0 deletions src/libs/git/providers.py
Original file line number Diff line number Diff line change
@@ -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 name of the repository.
repo_path (str): The path to the file within the repository.
Comment thread
Banyango marked this conversation as resolved.
Outdated

Returns:
Dict: A dictionary containing the file history.
Comment thread
Banyango marked this conversation as resolved.
Outdated
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type description is incorrect. The function returns Iterator[Commit], not a dictionary.

Suggested change
Dict: A dictionary containing the file history.
Iterator[Commit]: An iterator over `Commit` objects representing the file's history.

Copilot uses AI. Check for mistakes.
"""
git_repo = Repo(repo_path)

return git_repo.iter_commits(paths=file_name)
2 changes: 0 additions & 2 deletions src/libs/git/service.py

This file was deleted.

2 changes: 2 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from app.repos.group import repos
from app.commits.group import commits
from app.files.group import files


@click.group()
Expand All @@ -20,6 +21,7 @@ def cli():

cli.add_command(repos)
cli.add_command(commits)
cli.add_command(files)

if __name__ == "__main__":
cli()
14 changes: 5 additions & 9 deletions tests/core/repos/operations/test_update_repo_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand All @@ -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"},
)


Expand Down