""" Data models for Git Operations MCP Server. Defines data structures for git operations, workspace management, and provider interactions. """ from dataclasses import dataclass, field from datetime import UTC, datetime from enum import Enum from typing import Any from pydantic import BaseModel, Field class FileChangeType(str, Enum): """Types of file changes in git.""" ADDED = "added" MODIFIED = "modified" DELETED = "deleted" RENAMED = "renamed" COPIED = "copied" UNTRACKED = "untracked" IGNORED = "ignored" class MergeStrategy(str, Enum): """Merge strategies for pull requests.""" MERGE = "merge" # Create a merge commit SQUASH = "squash" # Squash and merge REBASE = "rebase" # Rebase and merge class PRState(str, Enum): """Pull request states.""" OPEN = "open" CLOSED = "closed" MERGED = "merged" class ProviderType(str, Enum): """Supported git providers.""" GITEA = "gitea" GITHUB = "github" GITLAB = "gitlab" class WorkspaceState(str, Enum): """Workspace lifecycle states.""" INITIALIZING = "initializing" READY = "ready" LOCKED = "locked" STALE = "stale" DELETED = "deleted" # Dataclasses for internal data structures @dataclass class FileChange: """A file change in git status.""" path: str change_type: FileChangeType old_path: str | None = None # For renames additions: int = 0 deletions: int = 0 def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "path": self.path, "change_type": self.change_type.value, "old_path": self.old_path, "additions": self.additions, "deletions": self.deletions, } @dataclass class BranchInfo: """Information about a git branch.""" name: str is_current: bool = False is_remote: bool = False tracking_branch: str | None = None commit_sha: str | None = None commit_message: str | None = None ahead: int = 0 behind: int = 0 def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "name": self.name, "is_current": self.is_current, "is_remote": self.is_remote, "tracking_branch": self.tracking_branch, "commit_sha": self.commit_sha, "commit_message": self.commit_message, "ahead": self.ahead, "behind": self.behind, } @dataclass class CommitInfo: """Information about a git commit.""" sha: str short_sha: str message: str author_name: str author_email: str authored_date: datetime committer_name: str committer_email: str committed_date: datetime parents: list[str] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "sha": self.sha, "short_sha": self.short_sha, "message": self.message, "author_name": self.author_name, "author_email": self.author_email, "authored_date": self.authored_date.isoformat(), "committer_name": self.committer_name, "committer_email": self.committer_email, "committed_date": self.committed_date.isoformat(), "parents": self.parents, } @dataclass class DiffHunk: """A hunk of diff content.""" old_start: int old_lines: int new_start: int new_lines: int content: str def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "old_start": self.old_start, "old_lines": self.old_lines, "new_start": self.new_start, "new_lines": self.new_lines, "content": self.content, } @dataclass class FileDiff: """Diff for a single file.""" path: str change_type: FileChangeType old_path: str | None = None hunks: list[DiffHunk] = field(default_factory=list) additions: int = 0 deletions: int = 0 is_binary: bool = False def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "path": self.path, "change_type": self.change_type.value, "old_path": self.old_path, "hunks": [h.to_dict() for h in self.hunks], "additions": self.additions, "deletions": self.deletions, "is_binary": self.is_binary, } @dataclass class PRInfo: """Information about a pull request.""" number: int title: str body: str state: PRState source_branch: str target_branch: str author: str created_at: datetime updated_at: datetime merged_at: datetime | None = None closed_at: datetime | None = None url: str | None = None labels: list[str] = field(default_factory=list) assignees: list[str] = field(default_factory=list) reviewers: list[str] = field(default_factory=list) mergeable: bool | None = None draft: bool = False def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "number": self.number, "title": self.title, "body": self.body, "state": self.state.value, "source_branch": self.source_branch, "target_branch": self.target_branch, "author": self.author, "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), "merged_at": self.merged_at.isoformat() if self.merged_at else None, "closed_at": self.closed_at.isoformat() if self.closed_at else None, "url": self.url, "labels": self.labels, "assignees": self.assignees, "reviewers": self.reviewers, "mergeable": self.mergeable, "draft": self.draft, } @dataclass class WorkspaceInfo: """Information about a project workspace.""" project_id: str path: str state: WorkspaceState repo_url: str | None = None current_branch: str | None = None last_accessed: datetime = field(default_factory=lambda: datetime.now(UTC)) size_bytes: int = 0 lock_holder: str | None = None lock_expires: datetime | None = None def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "project_id": self.project_id, "path": self.path, "state": self.state.value, "repo_url": self.repo_url, "current_branch": self.current_branch, "last_accessed": self.last_accessed.isoformat(), "size_bytes": self.size_bytes, "lock_holder": self.lock_holder, "lock_expires": self.lock_expires.isoformat() if self.lock_expires else None, } # Pydantic Request/Response Models class CloneRequest(BaseModel): """Request to clone a repository.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") repo_url: str = Field(..., description="Repository URL to clone") branch: str | None = Field(default=None, description="Branch to checkout after clone") depth: int | None = Field( default=None, ge=1, description="Shallow clone depth (None = full clone)" ) class CloneResult(BaseModel): """Result of a clone operation.""" success: bool = Field(..., description="Whether clone succeeded") project_id: str = Field(..., description="Project ID") workspace_path: str = Field(..., description="Path to cloned workspace") branch: str = Field(..., description="Current branch after clone") commit_sha: str = Field(..., description="HEAD commit SHA") error: str | None = Field(default=None, description="Error message if failed") class StatusRequest(BaseModel): """Request for git status.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") include_untracked: bool = Field(default=True, description="Include untracked files") class StatusResult(BaseModel): """Result of a status operation.""" project_id: str = Field(..., description="Project ID") branch: str = Field(..., description="Current branch") commit_sha: str = Field(..., description="HEAD commit SHA") is_clean: bool = Field(..., description="Whether working tree is clean") staged: list[dict[str, Any]] = Field( default_factory=list, description="Staged changes" ) unstaged: list[dict[str, Any]] = Field( default_factory=list, description="Unstaged changes" ) untracked: list[str] = Field(default_factory=list, description="Untracked files") ahead: int = Field(default=0, description="Commits ahead of upstream") behind: int = Field(default=0, description="Commits behind upstream") class BranchRequest(BaseModel): """Request for branch operations.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") branch_name: str = Field(..., description="Branch name") from_ref: str | None = Field( default=None, description="Reference to create branch from" ) checkout: bool = Field(default=True, description="Checkout after creation") class BranchResult(BaseModel): """Result of a branch operation.""" success: bool = Field(..., description="Whether operation succeeded") branch: str = Field(..., description="Branch name") commit_sha: str | None = Field(default=None, description="HEAD commit SHA") is_current: bool = Field(default=False, description="Whether branch is checked out") error: str | None = Field(default=None, description="Error message if failed") class ListBranchesRequest(BaseModel): """Request to list branches.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") include_remote: bool = Field(default=False, description="Include remote branches") class ListBranchesResult(BaseModel): """Result of listing branches.""" project_id: str = Field(..., description="Project ID") current_branch: str = Field(..., description="Currently checked out branch") local_branches: list[dict[str, Any]] = Field( default_factory=list, description="Local branches" ) remote_branches: list[dict[str, Any]] = Field( default_factory=list, description="Remote branches" ) class CheckoutRequest(BaseModel): """Request to checkout a branch or ref.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") ref: str = Field(..., description="Branch, tag, or commit to checkout") create_branch: bool = Field(default=False, description="Create new branch") force: bool = Field(default=False, description="Force checkout (discard changes)") class CheckoutResult(BaseModel): """Result of a checkout operation.""" success: bool = Field(..., description="Whether checkout succeeded") ref: str = Field(..., description="Checked out reference") commit_sha: str | None = Field(default=None, description="HEAD commit SHA") error: str | None = Field(default=None, description="Error message if failed") class CommitRequest(BaseModel): """Request to create a commit.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") message: str = Field(..., description="Commit message") files: list[str] | None = Field( default=None, description="Files to commit (None = all staged)" ) author_name: str | None = Field(default=None, description="Author name override") author_email: str | None = Field(default=None, description="Author email override") allow_empty: bool = Field(default=False, description="Allow empty commit") class CommitResult(BaseModel): """Result of a commit operation.""" success: bool = Field(..., description="Whether commit succeeded") commit_sha: str | None = Field(default=None, description="New commit SHA") short_sha: str | None = Field(default=None, description="Short commit SHA") message: str | None = Field(default=None, description="Commit message") files_changed: int = Field(default=0, description="Number of files changed") insertions: int = Field(default=0, description="Lines added") deletions: int = Field(default=0, description="Lines removed") error: str | None = Field(default=None, description="Error message if failed") class PushRequest(BaseModel): """Request to push to remote.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") branch: str | None = Field(default=None, description="Branch to push (None = current)") remote: str = Field(default="origin", description="Remote name") force: bool = Field(default=False, description="Force push") set_upstream: bool = Field(default=True, description="Set upstream tracking") class PushResult(BaseModel): """Result of a push operation.""" success: bool = Field(..., description="Whether push succeeded") branch: str = Field(..., description="Pushed branch") remote: str = Field(..., description="Remote name") commits_pushed: int = Field(default=0, description="Number of commits pushed") error: str | None = Field(default=None, description="Error message if failed") class PullRequest(BaseModel): """Request to pull from remote.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") branch: str | None = Field(default=None, description="Branch to pull (None = current)") remote: str = Field(default="origin", description="Remote name") rebase: bool = Field(default=False, description="Rebase instead of merge") class PullResult(BaseModel): """Result of a pull operation.""" success: bool = Field(..., description="Whether pull succeeded") branch: str = Field(..., description="Pulled branch") commits_received: int = Field(default=0, description="New commits received") fast_forward: bool = Field(default=False, description="Was fast-forward") conflicts: list[str] = Field( default_factory=list, description="Conflicting files if any" ) error: str | None = Field(default=None, description="Error message if failed") class DiffRequest(BaseModel): """Request for diff.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") base: str | None = Field(default=None, description="Base reference (None = working tree)") head: str | None = Field(default=None, description="Head reference (None = HEAD)") files: list[str] | None = Field(default=None, description="Specific files to diff") context_lines: int = Field(default=3, ge=0, description="Context lines") class DiffResult(BaseModel): """Result of a diff operation.""" project_id: str = Field(..., description="Project ID") base: str | None = Field(default=None, description="Base reference") head: str | None = Field(default=None, description="Head reference") files: list[dict[str, Any]] = Field( default_factory=list, description="File diffs" ) total_additions: int = Field(default=0, description="Total lines added") total_deletions: int = Field(default=0, description="Total lines removed") files_changed: int = Field(default=0, description="Number of files changed") class LogRequest(BaseModel): """Request for commit log.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") ref: str | None = Field(default=None, description="Reference to start from") limit: int = Field(default=20, ge=1, le=100, description="Max commits to return") skip: int = Field(default=0, ge=0, description="Commits to skip") path: str | None = Field(default=None, description="Filter by path") class LogResult(BaseModel): """Result of a log operation.""" project_id: str = Field(..., description="Project ID") commits: list[dict[str, Any]] = Field( default_factory=list, description="Commit history" ) total_commits: int = Field(default=0, description="Total commits in range") # PR Operations class CreatePRRequest(BaseModel): """Request to create a pull request.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") title: str = Field(..., description="PR title") body: str = Field(default="", description="PR description") source_branch: str = Field(..., description="Source branch") target_branch: str = Field(default="main", description="Target branch") draft: bool = Field(default=False, description="Create as draft") labels: list[str] = Field(default_factory=list, description="Labels to add") assignees: list[str] = Field(default_factory=list, description="Assignees") reviewers: list[str] = Field(default_factory=list, description="Reviewers") class CreatePRResult(BaseModel): """Result of creating a pull request.""" success: bool = Field(..., description="Whether creation succeeded") pr_number: int | None = Field(default=None, description="PR number") pr_url: str | None = Field(default=None, description="PR URL") error: str | None = Field(default=None, description="Error message if failed") class GetPRRequest(BaseModel): """Request to get a pull request.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") pr_number: int = Field(..., description="PR number") class GetPRResult(BaseModel): """Result of getting a pull request.""" success: bool = Field(..., description="Whether fetch succeeded") pr: dict[str, Any] | None = Field(default=None, description="PR info") error: str | None = Field(default=None, description="Error message if failed") class ListPRsRequest(BaseModel): """Request to list pull requests.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") state: PRState | None = Field(default=None, description="Filter by state") author: str | None = Field(default=None, description="Filter by author") limit: int = Field(default=20, ge=1, le=100, description="Max PRs to return") class ListPRsResult(BaseModel): """Result of listing pull requests.""" success: bool = Field(..., description="Whether list succeeded") pull_requests: list[dict[str, Any]] = Field( default_factory=list, description="PRs" ) total_count: int = Field(default=0, description="Total matching PRs") error: str | None = Field(default=None, description="Error message if failed") class MergePRRequest(BaseModel): """Request to merge a pull request.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") pr_number: int = Field(..., description="PR number") merge_strategy: MergeStrategy = Field( default=MergeStrategy.MERGE, description="Merge strategy" ) commit_message: str | None = Field( default=None, description="Custom merge commit message" ) delete_branch: bool = Field( default=True, description="Delete source branch after merge" ) class MergePRResult(BaseModel): """Result of merging a pull request.""" success: bool = Field(..., description="Whether merge succeeded") merge_commit_sha: str | None = Field(default=None, description="Merge commit SHA") branch_deleted: bool = Field(default=False, description="Whether branch was deleted") error: str | None = Field(default=None, description="Error message if failed") class UpdatePRRequest(BaseModel): """Request to update a pull request.""" project_id: str = Field(..., description="Project ID for scoping") agent_id: str = Field(..., description="Agent ID making the request") pr_number: int = Field(..., description="PR number") title: str | None = Field(default=None, description="New title") body: str | None = Field(default=None, description="New description") state: PRState | None = Field(default=None, description="New state") labels: list[str] | None = Field(default=None, description="Replace labels") assignees: list[str] | None = Field(default=None, description="Replace assignees") class UpdatePRResult(BaseModel): """Result of updating a pull request.""" success: bool = Field(..., description="Whether update succeeded") pr: dict[str, Any] | None = Field(default=None, description="Updated PR info") error: str | None = Field(default=None, description="Error message if failed") # Workspace Operations class GetWorkspaceRequest(BaseModel): """Request to get or create workspace.""" project_id: str = Field(..., description="Project ID") agent_id: str = Field(..., description="Agent ID making the request") class GetWorkspaceResult(BaseModel): """Result of getting workspace.""" success: bool = Field(..., description="Whether operation succeeded") workspace: dict[str, Any] | None = Field(default=None, description="Workspace info") error: str | None = Field(default=None, description="Error message if failed") class LockWorkspaceRequest(BaseModel): """Request to lock a workspace.""" project_id: str = Field(..., description="Project ID") agent_id: str = Field(..., description="Agent ID requesting lock") timeout: int = Field(default=300, ge=10, le=3600, description="Lock timeout seconds") class LockWorkspaceResult(BaseModel): """Result of locking workspace.""" success: bool = Field(..., description="Whether lock acquired") lock_holder: str | None = Field(default=None, description="Current lock holder") lock_expires: str | None = Field(default=None, description="Lock expiry ISO timestamp") error: str | None = Field(default=None, description="Error message if failed") class UnlockWorkspaceRequest(BaseModel): """Request to unlock a workspace.""" project_id: str = Field(..., description="Project ID") agent_id: str = Field(..., description="Agent ID releasing lock") force: bool = Field(default=False, description="Force unlock (admin only)") class UnlockWorkspaceResult(BaseModel): """Result of unlocking workspace.""" success: bool = Field(..., description="Whether unlock succeeded") error: str | None = Field(default=None, description="Error message if failed") # Health and Status class HealthStatus(BaseModel): """Health status response.""" status: str = Field(..., description="Health status") version: str = Field(..., description="Server version") workspace_count: int = Field(default=0, description="Active workspaces") gitea_connected: bool = Field(default=False, description="Gitea connectivity") github_connected: bool = Field(default=False, description="GitHub connectivity") gitlab_connected: bool = Field(default=False, description="GitLab connectivity") redis_connected: bool = Field(default=False, description="Redis connectivity") class ProviderStatus(BaseModel): """Provider connection status.""" provider: str = Field(..., description="Provider name") connected: bool = Field(..., description="Connection status") url: str | None = Field(default=None, description="Provider URL") user: str | None = Field(default=None, description="Authenticated user") error: str | None = Field(default=None, description="Error if not connected")