diffsage 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. diffsage/__init__.py +0 -0
  2. diffsage/__main__.py +4 -0
  3. diffsage/cli.py +38 -0
  4. diffsage/commands/__init__.py +0 -0
  5. diffsage/commands/ask.py +62 -0
  6. diffsage/commands/auth.py +153 -0
  7. diffsage/commands/commit.py +125 -0
  8. diffsage/commands/config.py +216 -0
  9. diffsage/commands/doctor.py +18 -0
  10. diffsage/config/__init__.py +0 -0
  11. diffsage/config/defaults.py +20 -0
  12. diffsage/config/file_loader.py +21 -0
  13. diffsage/config/loader.py +8 -0
  14. diffsage/config/paths.py +61 -0
  15. diffsage/config/resolver.py +118 -0
  16. diffsage/config/schema.py +53 -0
  17. diffsage/config/scope.py +8 -0
  18. diffsage/config/settings.py +9 -0
  19. diffsage/exceptions/__init__.py +34 -0
  20. diffsage/exceptions/base.py +32 -0
  21. diffsage/exceptions/credentials.py +8 -0
  22. diffsage/exceptions/git.py +9 -0
  23. diffsage/exceptions/provider.py +17 -0
  24. diffsage/git/__init__.py +0 -0
  25. diffsage/git/client.py +155 -0
  26. diffsage/logging/__init__.py +0 -0
  27. diffsage/logging/formatter.py +0 -0
  28. diffsage/logging/logger.py +41 -0
  29. diffsage/models/__init__.py +0 -0
  30. diffsage/models/commit_message.py +9 -0
  31. diffsage/models/config.py +16 -0
  32. diffsage/models/credentials.py +8 -0
  33. diffsage/models/doctor.py +13 -0
  34. diffsage/models/git.py +32 -0
  35. diffsage/models/provider.py +20 -0
  36. diffsage/parsers/commit_message_parser.py +43 -0
  37. diffsage/prompts/__init__.py +0 -0
  38. diffsage/providers/__init__.py +0 -0
  39. diffsage/providers/base.py +8 -0
  40. diffsage/providers/factory.py +14 -0
  41. diffsage/providers/gemini_provider.py +77 -0
  42. diffsage/services/__init__.py +0 -0
  43. diffsage/services/ai_service.py +63 -0
  44. diffsage/services/commit_service.py +38 -0
  45. diffsage/services/config_service.py +109 -0
  46. diffsage/services/credentials_service.py +61 -0
  47. diffsage/services/doctor_service.py +47 -0
  48. diffsage/services/editor_service.py +75 -0
  49. diffsage/services/git_service.py +20 -0
  50. diffsage/services/prompt_service.py +60 -0
  51. diffsage/storage/__init__.py +0 -0
  52. diffsage/storage/base.py +0 -0
  53. diffsage/storage/config_repository.py +85 -0
  54. diffsage/storage/credentials_repository.py +121 -0
  55. diffsage/ui/__init__.py +0 -0
  56. diffsage/ui/ask_view.py +10 -0
  57. diffsage/ui/auth_view.py +51 -0
  58. diffsage/ui/base.py +15 -0
  59. diffsage/ui/commit_view.py +100 -0
  60. diffsage/ui/config_view.py +58 -0
  61. diffsage/ui/doctor_view.py +39 -0
  62. diffsage-1.2.0.dist-info/METADATA +471 -0
  63. diffsage-1.2.0.dist-info/RECORD +67 -0
  64. diffsage-1.2.0.dist-info/WHEEL +5 -0
  65. diffsage-1.2.0.dist-info/entry_points.txt +2 -0
  66. diffsage-1.2.0.dist-info/licenses/LICENSE +21 -0
  67. diffsage-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,43 @@
1
+ from diffsage.exceptions import InvalidCommitMessageError
2
+ from diffsage.models.commit_message import CommitMessage
3
+
4
+
5
+ class CommitMessageParser:
6
+ @staticmethod
7
+ def parse(message: str) -> CommitMessage:
8
+ lines = message.splitlines()
9
+
10
+ if not lines:
11
+ raise InvalidCommitMessageError("Commit message is empty.")
12
+
13
+ header = lines[0]
14
+ body = lines[1:]
15
+
16
+ if ":" not in header:
17
+ raise InvalidCommitMessageError(
18
+ "Commit message header must use '<type>: <subject>' format."
19
+ )
20
+
21
+ header_parts = header.split(":", maxsplit=1)
22
+ prefix = header_parts[0].strip()
23
+ subject = header_parts[1].strip()
24
+
25
+ if "(" in prefix:
26
+ type_part = prefix.split("(", maxsplit=1)[0]
27
+ scope_part = prefix.split("(", maxsplit=1)[1].rstrip(")")
28
+ else:
29
+ type_part = prefix
30
+ scope_part = None
31
+
32
+ if not type_part:
33
+ raise InvalidCommitMessageError("Commit message type is missing.")
34
+
35
+ if not subject:
36
+ raise InvalidCommitMessageError("Commit message subject is missing.")
37
+
38
+ return CommitMessage(
39
+ type=type_part,
40
+ scope=scope_part,
41
+ subject=subject,
42
+ body=body,
43
+ )
File without changes
File without changes
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from diffsage.models.provider import ProviderRequest, ProviderResponse
4
+
5
+
6
+ class BaseProvider(ABC):
7
+ @abstractmethod
8
+ def generate(self, request: ProviderRequest) -> ProviderResponse: ...
@@ -0,0 +1,14 @@
1
+ from diffsage.config.settings import Settings
2
+ from diffsage.exceptions.base import ConfigError
3
+ from diffsage.models.credentials import Credential
4
+ from diffsage.providers.base import BaseProvider
5
+ from diffsage.providers.gemini_provider import GeminiProvider
6
+
7
+
8
+ def create_provider(settings: Settings, credential: Credential) -> BaseProvider:
9
+ """Create the configured AI provider."""
10
+
11
+ if settings.provider == "gemini":
12
+ return GeminiProvider(settings, credential)
13
+
14
+ raise ConfigError(f"Unsupported provider: {settings.provider}")
@@ -0,0 +1,77 @@
1
+ import time
2
+
3
+ from google import genai
4
+ from google.genai import errors as genai_errors
5
+ from google.genai import types
6
+
7
+ from diffsage.config.settings import Settings
8
+ from diffsage.exceptions import (
9
+ AuthenticationError,
10
+ ModelNotFoundError,
11
+ ProviderError,
12
+ ProviderUnavailableError,
13
+ RateLimitError,
14
+ )
15
+ from diffsage.models.credentials import Credential
16
+ from diffsage.models.provider import ProviderRequest, ProviderResponse
17
+ from diffsage.providers.base import BaseProvider
18
+
19
+
20
+ class GeminiProvider(BaseProvider):
21
+ """Google Gemini provider implementation."""
22
+
23
+ def __init__(self, settings: Settings, credential: Credential) -> None:
24
+ self._settings = settings
25
+ self._credential = credential
26
+
27
+ self._client = genai.Client(api_key=self._credential.api_key)
28
+
29
+ def generate(self, request: ProviderRequest) -> ProviderResponse:
30
+ start_time = time.perf_counter()
31
+
32
+ timeout_ms = self._settings.timeout * 1000
33
+
34
+ config = types.GenerateContentConfig(
35
+ temperature=request.temperature,
36
+ max_output_tokens=request.max_tokens,
37
+ http_options=types.HttpOptions(timeout=timeout_ms),
38
+ )
39
+
40
+ try:
41
+ response = self._client.models.generate_content(
42
+ model=request.model, contents=request.prompt, config=config
43
+ )
44
+ except genai_errors.APIError as e:
45
+ status = e.status
46
+
47
+ if status == "NOT_FOUND":
48
+ raise ModelNotFoundError(f"Model '{request.model}' was not found.") from e
49
+
50
+ elif status == "UNAUTHENTICATED":
51
+ raise AuthenticationError("Authentication with Gemini failed.") from e
52
+
53
+ elif status == "INVALID_ARGUMENT":
54
+ if e.code == 400:
55
+ raise AuthenticationError(
56
+ "Authentication with Gemini failed. Please check your API Key."
57
+ ) from e
58
+
59
+ elif status == "RESOURCE_EXHAUSTED":
60
+ raise RateLimitError("Gemini API rate limit exceeded.") from e
61
+
62
+ elif status in ("UNAVAILABLE", "DEADLINE_EXCEEDED"):
63
+ raise ProviderUnavailableError("Gemini service is currently unavailable.") from e
64
+
65
+ raise ProviderError(f"Gemini API request failed: {e}") from e
66
+
67
+ latency_ms = round((time.perf_counter() - start_time) * 1000)
68
+
69
+ return ProviderResponse(
70
+ content=response.candidates[0].content.parts[0].text,
71
+ provider="gemini",
72
+ model=response.model_version,
73
+ input_tokens=response.usage_metadata.prompt_token_count,
74
+ output_tokens=response.usage_metadata.candidates_token_count,
75
+ finish_reason=response.candidates[0].finish_reason.value,
76
+ latency_ms=latency_ms,
77
+ )
File without changes
@@ -0,0 +1,63 @@
1
+ import time
2
+
3
+ from diffsage.config.settings import Settings
4
+ from diffsage.exceptions import CredentialNotFoundError, ProviderUnavailableError
5
+ from diffsage.models.provider import ProviderRequest, ProviderResponse
6
+ from diffsage.providers.base import BaseProvider
7
+ from diffsage.providers.factory import create_provider
8
+ from diffsage.services.credentials_service import CredentialService
9
+
10
+
11
+ class AIService:
12
+ """Coordinates AI provider interactions."""
13
+
14
+ def __init__(
15
+ self,
16
+ settings: Settings,
17
+ credential_service: CredentialService,
18
+ provider: BaseProvider | None = None,
19
+ ) -> None:
20
+ self._settings = settings
21
+
22
+ if provider is not None:
23
+ self._provider = provider
24
+ else:
25
+ credential = credential_service.get_credential(
26
+ settings.provider,
27
+ "default",
28
+ )
29
+
30
+ if credential is None:
31
+ raise CredentialNotFoundError(
32
+ settings.provider,
33
+ "default",
34
+ )
35
+
36
+ self._provider = create_provider(settings, credential)
37
+
38
+ def _ask_once(self, prompt: str) -> ProviderResponse:
39
+ request = ProviderRequest(
40
+ prompt=prompt,
41
+ model=self._settings.ai_model,
42
+ temperature=0.2,
43
+ max_tokens=1000,
44
+ )
45
+
46
+ return self._provider.generate(request)
47
+
48
+ def _backoff_delay(self, attempt: int) -> int:
49
+ return 2**attempt
50
+
51
+ def ask(self, prompt: str) -> ProviderResponse:
52
+ total_attempts = self._settings.max_retries + 1
53
+
54
+ for attempt in range(total_attempts):
55
+ try:
56
+ return self._ask_once(prompt)
57
+
58
+ except ProviderUnavailableError:
59
+ if attempt == total_attempts - 1:
60
+ raise
61
+
62
+ delay = self._backoff_delay(attempt)
63
+ time.sleep(delay)
@@ -0,0 +1,38 @@
1
+ from diffsage.exceptions import NoStagedChangesError, NotGitRepositoryError
2
+ from diffsage.git.client import GitClient
3
+ from diffsage.logging.logger import get_logger
4
+ from diffsage.services.ai_service import AIService
5
+ from diffsage.services.git_service import GitService
6
+ from diffsage.services.prompt_service import PromptService
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class CommitService:
12
+ def __init__(
13
+ self,
14
+ git_client: GitClient,
15
+ git_service: GitService,
16
+ prompt_service: PromptService,
17
+ ai_service: AIService,
18
+ ) -> None:
19
+ self._git_client = git_client
20
+ self._git_service = git_service
21
+ self._prompt_service = prompt_service
22
+ self._ai_service = ai_service
23
+
24
+ def generate_commit_message(self) -> str:
25
+ if not self._git_client.is_git_repository():
26
+ raise NotGitRepositoryError
27
+
28
+ if not self._git_client.staged_diff():
29
+ raise NoStagedChangesError
30
+
31
+ context = self._git_service.build_commit_context()
32
+ prompt = self._prompt_service.build_commit_prompt(context)
33
+
34
+ logger.info("Generating commit message.")
35
+ response = self._ai_service.ask(prompt)
36
+ logger.info("Commit message generated successfully.")
37
+
38
+ return response.content
@@ -0,0 +1,109 @@
1
+ from diffsage.config.loader import load_settings
2
+ from diffsage.config.settings import Settings
3
+ from diffsage.exceptions import InvalidConfigurationValueError, UnknownConfigurationKeyError
4
+ from diffsage.models.config import ConfigReport, ConfigValueReport
5
+ from diffsage.storage.config_repository import ConfigRepository
6
+
7
+
8
+ class ConfigService:
9
+ _ATTRIBUTE_MAP = {
10
+ "provider": "provider",
11
+ "model": "ai_model",
12
+ "timeout": "timeout",
13
+ "max_retries": "max_retries",
14
+ "log_level": "log_level",
15
+ }
16
+
17
+ _NORMALIZERS = {
18
+ "provider": str.lower,
19
+ "model": str.lower,
20
+ "log_level": str.upper,
21
+ }
22
+
23
+ def __init__(self, settings: Settings, repository: ConfigRepository) -> None:
24
+ self._settings = settings
25
+ self._repository = repository
26
+
27
+ def _create_report(self, settings: Settings) -> ConfigReport:
28
+ return ConfigReport(
29
+ provider=settings.provider,
30
+ model=settings.ai_model,
31
+ timeout=settings.timeout,
32
+ max_retries=settings.max_retries,
33
+ log_level=settings.log_level,
34
+ )
35
+
36
+ def get_configuration_raw(self) -> ConfigReport:
37
+ config = self._repository.list()
38
+
39
+ return ConfigReport(
40
+ provider=config.get("provider"),
41
+ model=config.get("model"),
42
+ timeout=config.get("timeout"),
43
+ max_retries=config.get("max_retries"),
44
+ log_level=config.get("log_level"),
45
+ )
46
+
47
+ def get_configuration_value_raw(self, key: str) -> ConfigValueReport:
48
+ if key not in self._ATTRIBUTE_MAP:
49
+ raise UnknownConfigurationKeyError(key)
50
+
51
+ value = self._repository.get(key)
52
+
53
+ return ConfigValueReport(
54
+ key=key,
55
+ value=value,
56
+ )
57
+
58
+ def get_configuration(self) -> ConfigReport:
59
+ return self._create_report(self._settings)
60
+
61
+ def get_value(self, key: str) -> ConfigValueReport:
62
+ current_value = self._ATTRIBUTE_MAP.get(key)
63
+
64
+ if current_value is None:
65
+ raise UnknownConfigurationKeyError(key)
66
+
67
+ value = getattr(self._settings, current_value)
68
+
69
+ return ConfigValueReport(key=key, value=str(value))
70
+
71
+ def set_value(self, key: str, value: str) -> ConfigReport:
72
+ attribute_name = self._ATTRIBUTE_MAP.get(key)
73
+
74
+ if attribute_name is None:
75
+ raise UnknownConfigurationKeyError(key)
76
+
77
+ value = value.strip()
78
+ normalizer = self._NORMALIZERS.get(key)
79
+
80
+ if normalizer is not None:
81
+ value = normalizer(value)
82
+
83
+ expected_value = getattr(self._settings, attribute_name)
84
+
85
+ try:
86
+ if isinstance(expected_value, int):
87
+ converted_value = int(value)
88
+ else:
89
+ converted_value = value
90
+ except ValueError:
91
+ raise InvalidConfigurationValueError(value) from None
92
+
93
+ self._repository.set(key, converted_value)
94
+
95
+ updated_settings = load_settings()
96
+
97
+ return self._create_report(updated_settings)
98
+
99
+ def unset_value(self, key: str) -> ConfigReport:
100
+ attribute_name = self._ATTRIBUTE_MAP.get(key)
101
+
102
+ if attribute_name is None:
103
+ raise UnknownConfigurationKeyError(key)
104
+
105
+ self._repository.unset(key)
106
+
107
+ updated_settings = load_settings()
108
+
109
+ return self._create_report(updated_settings)
@@ -0,0 +1,61 @@
1
+ from diffsage.models.credentials import Credential
2
+ from diffsage.storage.credentials_repository import CredentialsRepository
3
+
4
+
5
+ class CredentialService:
6
+ _NORMALIZERS = {
7
+ "provider": str.lower,
8
+ "name": str.lower,
9
+ }
10
+
11
+ def __init__(self, repository: CredentialsRepository) -> None:
12
+ self._repository = repository
13
+
14
+ def set_credential(self, provider: str, api_key: str, name: str = "default") -> None:
15
+ provider = self._NORMALIZERS["provider"](provider.strip())
16
+ name = self._NORMALIZERS["name"](name.strip())
17
+ api_key = api_key.strip()
18
+
19
+ if not provider:
20
+ raise ValueError("Provider cannot be empty.")
21
+
22
+ if not name:
23
+ raise ValueError("Credential profile name cannot be empty.")
24
+
25
+ if not api_key:
26
+ raise ValueError("API key cannot be empty.")
27
+
28
+ credential = Credential(
29
+ provider=provider,
30
+ name=name,
31
+ api_key=api_key,
32
+ )
33
+
34
+ self._repository.save(credential)
35
+
36
+ def get_credential(self, provider: str, name: str = "default") -> Credential | None:
37
+ provider = self._NORMALIZERS["provider"](provider.strip())
38
+ name = self._NORMALIZERS["name"](name.strip())
39
+
40
+ if not provider:
41
+ raise ValueError("Provider cannot be empty.")
42
+
43
+ if not name:
44
+ raise ValueError("Credential profile name cannot be empty.")
45
+
46
+ return self._repository.get(provider, name)
47
+
48
+ def list_credentials(self) -> list[Credential]:
49
+ return self._repository.list()
50
+
51
+ def delete_credential(self, provider: str, name: str = "default") -> bool:
52
+ provider = self._NORMALIZERS["provider"](provider.strip())
53
+ name = self._NORMALIZERS["name"](name.strip())
54
+
55
+ if not provider:
56
+ raise ValueError("Provider cannot be empty.")
57
+
58
+ if not name:
59
+ raise ValueError("Credential profile cannot be empty.")
60
+
61
+ return self._repository.delete(provider, name)
@@ -0,0 +1,47 @@
1
+ import os
2
+ import platform
3
+ import shutil
4
+ import subprocess
5
+
6
+ from diffsage.config.loader import load_settings
7
+ from diffsage.logging.logger import get_logger
8
+ from diffsage.models.doctor import DoctorReport
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class DoctorService:
14
+ """Runs environment diagnostics."""
15
+
16
+ def run(self) -> DoctorReport:
17
+ logger.info("Loading application settings.")
18
+
19
+ settings = load_settings()
20
+
21
+ git_path = shutil.which("git")
22
+ git_installed = git_path is not None
23
+ git_version = None
24
+
25
+ if git_installed:
26
+ try:
27
+ result = subprocess.run(
28
+ ["git", "--version"],
29
+ capture_output=True,
30
+ text=True,
31
+ check=True,
32
+ )
33
+ git_version = result.stdout.strip()
34
+ except subprocess.SubprocessError:
35
+ git_version = None
36
+
37
+ return DoctorReport(
38
+ python_version=platform.python_version(),
39
+ git_installed=git_installed,
40
+ git_version=git_version,
41
+ provider=settings.provider,
42
+ timeout=settings.timeout,
43
+ max_retries=settings.max_retries,
44
+ log_level=settings.log_level,
45
+ virtual_environment=os.getenv("VIRTUAL_ENV") is not None,
46
+ configuration_loaded=True,
47
+ )
@@ -0,0 +1,75 @@
1
+ import os
2
+ import platform
3
+ import shlex
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+
9
+ class EditorService:
10
+ def edit(self, text: str) -> str:
11
+ editor = self._resolve_editor()
12
+
13
+ temp_file = self._create_temp_file(text)
14
+
15
+ command = shlex.split(editor)
16
+ command.append(str(temp_file))
17
+
18
+ try:
19
+ subprocess.run(command, check=False)
20
+ text = temp_file.read_text(encoding="utf-8")
21
+ return text.rstrip()
22
+
23
+ except FileNotFoundError as e:
24
+ raise RuntimeError(
25
+ f'Unable to launch "{editor}". Check your EDITOR or VISUAL configuration.'
26
+ ) from e
27
+
28
+ finally:
29
+ temp_file.unlink(missing_ok=True)
30
+
31
+ def _resolve_editor(self) -> str:
32
+ editor = os.environ.get("VISUAL")
33
+ if editor:
34
+ return editor
35
+
36
+ editor = os.environ.get("EDITOR")
37
+ if editor:
38
+ return editor
39
+
40
+ try:
41
+ result = subprocess.run(
42
+ [
43
+ "git",
44
+ "config",
45
+ "--global",
46
+ "core.editor",
47
+ ],
48
+ capture_output=True,
49
+ text=True,
50
+ check=True,
51
+ )
52
+ editor = result.stdout.strip()
53
+ if editor:
54
+ return editor
55
+
56
+ except (subprocess.CalledProcessError, FileNotFoundError):
57
+ pass
58
+
59
+ if platform.system() == "Windows":
60
+ return "notepad"
61
+
62
+ return "nano"
63
+
64
+ def _create_temp_file(self, text: str) -> Path:
65
+ with tempfile.NamedTemporaryFile(
66
+ delete=False,
67
+ mode="w",
68
+ encoding="utf-8",
69
+ suffix=".txt",
70
+ ) as temp:
71
+ temp.write(text)
72
+ temp.flush()
73
+ path = Path(temp.name)
74
+
75
+ return path
@@ -0,0 +1,20 @@
1
+ from diffsage.git.client import GitClient
2
+ from diffsage.models.git import CommitContext
3
+
4
+
5
+ class GitService:
6
+ def __init__(self, git_client: GitClient):
7
+ self._git_client = git_client
8
+
9
+ def build_commit_context(
10
+ self,
11
+ recent_commit_limit: int = 10,
12
+ ) -> CommitContext:
13
+ return CommitContext(
14
+ branch=self._git_client.current_branch(),
15
+ staged_diff=self._git_client.staged_diff(),
16
+ unstaged_diff=self._git_client.unstaged_diff(),
17
+ recent_commits=self._git_client.recent_commits(
18
+ limit=recent_commit_limit,
19
+ ),
20
+ )
@@ -0,0 +1,60 @@
1
+ from diffsage.models.git import CommitContext
2
+
3
+
4
+ class PromptService:
5
+ def _build_commit_context(
6
+ self,
7
+ context: CommitContext,
8
+ ) -> str:
9
+ lines: list[str] = []
10
+
11
+ lines.extend(
12
+ [
13
+ "Branch:",
14
+ context.branch,
15
+ "",
16
+ "Staged Diff:",
17
+ context.staged_diff,
18
+ "",
19
+ "Unstaged Diff:",
20
+ context.unstaged_diff,
21
+ "",
22
+ "Recent Commits:",
23
+ ]
24
+ )
25
+
26
+ for commit in context.recent_commits:
27
+ lines.append(f"{commit.hash} | {commit.author} | {commit.message} | {commit.date}")
28
+
29
+ return "\n".join(lines)
30
+
31
+ def _build_commit_instructions(self) -> str:
32
+ """Build a prompt for generating a Git commit message."""
33
+
34
+ return """
35
+ Generate a concise Conventional Commit message for the following Git diff.
36
+
37
+ Requirements:
38
+ - Return only the commit message.
39
+ - Follow the Conventional Commits specification.
40
+ - Keep the subject under 72 characters.
41
+ - Write the subject in the imperative mood.
42
+ - Leave exactly one blank line after the subject.
43
+ - For simple changes, return only the subject line.
44
+ - For changes affecting multiple files, features,
45
+ refactoring, architecture, or significant fixes, always include a body.
46
+ - If a body is included, it MUST contain 3–6 bullet points.
47
+ - Use dashes (-) for bullets.
48
+ - Do not write body paragraphs.
49
+ - Start each bullet with an imperative verb (e.g., Add, Update, Remove, Refactor, Fix).
50
+ - End each bullet with a period.
51
+ - Each bullet should describe one significant change.
52
+ - Keep each bullet concise.
53
+ - Do not include Markdown headings, explanations, or code fences.
54
+
55
+ Repository Context:
56
+
57
+ """.strip()
58
+
59
+ def build_commit_prompt(self, context: CommitContext) -> str:
60
+ return f"{self._build_commit_instructions()}\n\n{self._build_commit_context(context)}"
File without changes
File without changes