import os from pathlib import Path from typing import Any from gitea.client import GiteaClient class FileTools: """Tools for Gitea file/content operations.""" def __init__(self, client: GiteaClient, repo_path: str | None = None) -> None: self._client = client self._repo_path: str | None = repo_path def _paginate_lines( self, content: str, offset: int, limit: int, ) -> str: """Return lines[offset-1 : offset-1+limit] with a paging footer if truncated. Uses the same 1-indexed convention as CodingTools.read_file. """ lines: list[str] = content.splitlines() total: int = len(lines) start: int = offset - 1 # convert to 0-indexed page: list[str] = lines[start : start + limit] formatted: list[str] = [ f"{start + i + 1}: {line}" for i, line in enumerate(page) ] result: str = "\n".join(formatted) end_line: int = start + len(page) if end_line < total: next_offset: int = end_line + 1 result += ( f"\n\n[{total} lines total — showing lines {offset}–{end_line}. " f"Re-call with offset={next_offset} to read more.]" ) return result def _resolve_local_path(self, owner: str, repo: str, path: str) -> str | None: """Resolve owner/repo/path to a local filesystem path if the repo is cloned.""" if not self._repo_path: return None local_repo: str = os.path.join(self._repo_path, owner, repo) if os.path.isdir(local_repo): return os.path.join(local_repo, path) return None def get_file_content( self, owner: str, repo: str, path: str, offset: int = 1, limit: int = 250, ) -> str: """Get the content of a file from a Gitea repository with line paging. Checks the local workspace first if repo_path is configured, falling back to the remote API when the file is not available locally. Args: offset: 1-indexed line to start from (default 1). limit: Maximum number of lines to return (default 250). """ local_path: str | None = self._resolve_local_path(owner, repo, path) if local_path and os.path.isfile(local_path): try: with open(local_path, 'r', encoding='utf-8', errors='replace') as f: raw: str = f.read() return self._paginate_lines(raw, offset, limit) except Exception: pass try: content = self._client.files.get_file_content(owner, repo, path) raw: str = "\n".join(content) if isinstance(content, list) else content return self._paginate_lines(raw, offset, limit) except Exception as e: return f"Error getting file content: {str(e)}" def get_file_content_with_ref( self, owner: str, repo: str, path: str, ref: str = "master", offset: int = 1, limit: int = 250, ) -> str: """Get file content at a specific git ref with line paging. Checks the local workspace first using ``git show`` if the repo is cloned locally, falling back to the remote API. Args: ref: Branch, tag, or commit SHA (default 'master'). offset: 1-indexed line to start from (default 1). limit: Maximum number of lines to return (default 250). """ if self._repo_path: local_repo: str = os.path.join(self._repo_path, owner, repo) if os.path.isdir(local_repo): try: import subprocess result = subprocess.run( ["git", "-C", local_repo, "show", f"{ref}:{path}"], capture_output=True, text=True, timeout=15, ) if result.returncode == 0: return self._paginate_lines(result.stdout, offset, limit) except Exception: pass try: content = self._client.files.get_file_content(owner, repo, path, ref) raw: str = "\n".join(content) if isinstance(content, list) else content return self._paginate_lines(raw, offset, limit) except Exception as e: return f"Error getting file content: {str(e)}" def commit_file( self, owner: str, repo: str, path: str, message: str, content: str, branch: str ) -> str: try: self._client.files.update_file(owner, repo, path, message, content, branch) return f"File '{path}' committed successfully to {owner}/{repo}." except Exception as e: return f"Error committing file: {str(e)}" def update_file( self, owner: str, repo: str, path: str, message: str, content: str, branch: str ) -> str: try: self._client.files.update_file(owner, repo, path, message, content, branch) return f"File '{path}' updated in {owner}/{repo}." except Exception as e: return f"Error updating file: {str(e)}"