Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 results
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 path to the file relative to the repository root.
repo_path (str): The path to the repository.
"""
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 path to the file within the repository.
repo_path (str): The path to the repository.

Returns:
Dict: A dictionary containing the file history.
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