ticket2pr 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {ticket2pr-0.3.0/ticket2pr.egg-info → ticket2pr-0.3.2}/PKG-INFO +1 -1
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/pyproject.toml +1 -1
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/setup.py +1 -1
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/agents/base.py +26 -16
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/agents/pre_commit_fixer.py +11 -4
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/agents/ticket_solver.py +5 -6
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/branch_creator.py +19 -1
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/cli.py +75 -24
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/clients/github_client.py +4 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/enhanced_git.py +32 -2
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/exceptions.py +5 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/settings.py +1 -1
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/settings_init.py +3 -16
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/workflow.py +13 -3
- {ticket2pr-0.3.0 → ticket2pr-0.3.2/ticket2pr.egg-info}/PKG-INFO +1 -1
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/LICENSE +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/README.md +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/setup.cfg +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/agents/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/agents/commit_message.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/agents/pr_generator.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/clients/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/clients/jira_client.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/console_utils.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/logging_setup.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/main.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/pr_content.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/shell/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/shell/base.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/shell/pre_commit_runner.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/src/validators.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/tests/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/tests/integration/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/tests/integration/test_find_first_toml.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/tests/unit/__init__.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/tests/unit/test_nothing.py +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/ticket2pr.egg-info/SOURCES.txt +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/ticket2pr.egg-info/dependency_links.txt +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/ticket2pr.egg-info/entry_points.txt +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/ticket2pr.egg-info/not-zip-safe +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/ticket2pr.egg-info/requires.txt +0 -0
- {ticket2pr-0.3.0 → ticket2pr-0.3.2}/ticket2pr.egg-info/top_level.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from claude_agent_sdk import (
|
|
@@ -5,6 +6,7 @@ from claude_agent_sdk import (
|
|
|
5
6
|
ClaudeAgentOptions,
|
|
6
7
|
ContentBlock,
|
|
7
8
|
Message,
|
|
9
|
+
PermissionMode,
|
|
8
10
|
ResultMessage,
|
|
9
11
|
SystemMessage,
|
|
10
12
|
TextBlock,
|
|
@@ -18,6 +20,18 @@ from claude_agent_sdk import (
|
|
|
18
20
|
from src.exceptions import AgentQueryUnknownError
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
def extract_session_id(message: Message) -> str | None:
|
|
24
|
+
"""
|
|
25
|
+
Extract session_id from an init message.
|
|
26
|
+
|
|
27
|
+
Returns the session_id if the message is an init SystemMessage, None otherwise.
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(message, SystemMessage) and message.subtype == "init":
|
|
30
|
+
session_id: str | None = message.data.get("session_id")
|
|
31
|
+
return session_id
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
def _format_content_blocks(content: list[ContentBlock]) -> list[str]:
|
|
22
36
|
output_parts = []
|
|
23
37
|
|
|
@@ -59,10 +73,12 @@ def _format_content_blocks(content: list[ContentBlock]) -> list[str]:
|
|
|
59
73
|
|
|
60
74
|
elif tool_name == "Bash":
|
|
61
75
|
command = tool_input.get("command", "unknown")
|
|
62
|
-
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
description = tool_input.get("description")
|
|
77
|
+
if description:
|
|
78
|
+
output_parts.append(f"💻 Bash: {description}")
|
|
79
|
+
output_parts.append(f" ↳ {command}")
|
|
80
|
+
else:
|
|
81
|
+
output_parts.append(f"💻 Bash: {command}")
|
|
66
82
|
|
|
67
83
|
else:
|
|
68
84
|
output_parts.append(f"🔧 Using tool: {tool_name}")
|
|
@@ -126,11 +142,11 @@ async def run_agent_query(
|
|
|
126
142
|
prompt: str,
|
|
127
143
|
system_prompt: str,
|
|
128
144
|
allowed_tools: list[str],
|
|
129
|
-
permission_mode:
|
|
145
|
+
permission_mode: PermissionMode = "bypassPermissions",
|
|
130
146
|
cwd: Path | None = None,
|
|
131
147
|
mcp_config_path: Path | None = None,
|
|
132
148
|
session_id: str | None = None,
|
|
133
|
-
) -> Message:
|
|
149
|
+
) -> AsyncGenerator[Message]:
|
|
134
150
|
"""
|
|
135
151
|
Execute a Claude Agent SDK query with standardized message handling.
|
|
136
152
|
|
|
@@ -141,34 +157,28 @@ async def run_agent_query(
|
|
|
141
157
|
prompt: The user prompt to send to the agent
|
|
142
158
|
system_prompt: The system prompt defining the agent's role and behavior
|
|
143
159
|
allowed_tools: List of tool names the agent is allowed to use
|
|
144
|
-
permission_mode:
|
|
145
|
-
|
|
160
|
+
permission_mode: Permission mode for the agent. Defaults to bypassPermissions
|
|
161
|
+
to allow full access without prompts.
|
|
146
162
|
cwd: Optional current working directory for the agent to run from.
|
|
147
163
|
mcp_config_path: Optional path to mcp.json configuration file for MCP servers.
|
|
148
164
|
"""
|
|
149
165
|
options_kwargs = {
|
|
150
166
|
"allowed_tools": allowed_tools,
|
|
151
167
|
"system_prompt": system_prompt,
|
|
168
|
+
"permission_mode": permission_mode,
|
|
152
169
|
}
|
|
153
|
-
if permission_mode is not None:
|
|
154
|
-
options_kwargs["permission_mode"] = permission_mode
|
|
155
170
|
|
|
156
171
|
if cwd is not None:
|
|
157
172
|
options_kwargs["cwd"] = str(cwd)
|
|
158
173
|
|
|
159
174
|
if mcp_config_path is not None:
|
|
160
|
-
options_kwargs["
|
|
175
|
+
options_kwargs["mcp_servers"] = mcp_config_path
|
|
161
176
|
|
|
162
177
|
if session_id is not None:
|
|
163
178
|
options_kwargs["resume"] = session_id
|
|
164
|
-
|
|
165
179
|
options = ClaudeAgentOptions(**options_kwargs)
|
|
166
180
|
try:
|
|
167
181
|
async for message in query(prompt=prompt, options=options):
|
|
168
|
-
# The first message is a system init message with the session ID
|
|
169
|
-
if hasattr(message, "subtype") and message.subtype == "init":
|
|
170
|
-
session_id = message.data.get("session_id")
|
|
171
|
-
yield session_id
|
|
172
182
|
yield message
|
|
173
183
|
except Exception as e:
|
|
174
184
|
raise AgentQueryUnknownError(str(e)) from e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from src.agents.base import print_agent_message, run_agent_query
|
|
3
|
+
from src.agents.base import extract_session_id, print_agent_message, run_agent_query
|
|
4
4
|
from src.shell.pre_commit_runner import run_pre_commit
|
|
5
5
|
|
|
6
6
|
SYSTEM_PROMPT = """
|
|
@@ -21,6 +21,7 @@ CRITICAL RULES:
|
|
|
21
21
|
type checking)
|
|
22
22
|
- Apply fixes to the files that failed the pre-commit checks
|
|
23
23
|
- Be precise and minimal - only change what's necessary to pass the hooks
|
|
24
|
+
- NEVER run pre-commit with --all-files flag. ONLY run on staged files.
|
|
24
25
|
|
|
25
26
|
WORKFLOW:
|
|
26
27
|
1. Read the error messages carefully to understand what needs to be fixed
|
|
@@ -31,6 +32,7 @@ black expects)
|
|
|
31
32
|
you modified
|
|
32
33
|
5. Only stage the files you actually fixed - do not stage unrelated changes
|
|
33
34
|
6. After staging fixes, run `pre-commit run` again to verify your fixes worked
|
|
35
|
+
(NEVER use --all-files)
|
|
34
36
|
7. If pre-commit still fails, analyze the new errors and fix them
|
|
35
37
|
8. Retry this fix-and-verify cycle up to {max_retries} times total
|
|
36
38
|
9. If pre-commit passes, you're done - stop retrying
|
|
@@ -47,17 +49,17 @@ async def verify_pre_commit_and_fix(
|
|
|
47
49
|
workspace_path: Path, max_retries: int = 5, mcp_config_path: Path | None = None
|
|
48
50
|
) -> bool:
|
|
49
51
|
"""
|
|
50
|
-
Verify pre-commit hooks pass, fixing issues if needed.
|
|
52
|
+
Verify pre-commit hooks pass on STAGED FILES ONLY, fixing issues if needed.
|
|
51
53
|
|
|
52
54
|
This function:
|
|
53
|
-
1. Runs pre-commit hooks on staged files
|
|
55
|
+
1. Runs pre-commit hooks on staged files ONLY (never --all-files)
|
|
54
56
|
2. If successful, returns True
|
|
55
57
|
3. If failed, uses AI to fix the issues (AI will stage its own fixes and retry)
|
|
56
58
|
|
|
57
59
|
Args:
|
|
58
|
-
git: EnhancedGit instance for staging changes
|
|
59
60
|
workspace_path: Path to the workspace root
|
|
60
61
|
max_retries: Maximum number of retry attempts to tell the AI (default: 5)
|
|
62
|
+
mcp_config_path: Optional path to MCP config file
|
|
61
63
|
|
|
62
64
|
Returns:
|
|
63
65
|
True if pre-commit passes, False otherwise
|
|
@@ -73,6 +75,7 @@ async def verify_pre_commit_and_fix(
|
|
|
73
75
|
pre_commit_output=result.output,
|
|
74
76
|
)
|
|
75
77
|
|
|
78
|
+
session_id = None
|
|
76
79
|
async for message in run_agent_query(
|
|
77
80
|
prompt=prompt,
|
|
78
81
|
system_prompt=system_prompt,
|
|
@@ -81,6 +84,10 @@ async def verify_pre_commit_and_fix(
|
|
|
81
84
|
cwd=workspace_path,
|
|
82
85
|
mcp_config_path=mcp_config_path,
|
|
83
86
|
):
|
|
87
|
+
if session_id is None:
|
|
88
|
+
session_id = extract_session_id(message)
|
|
89
|
+
if session_id:
|
|
90
|
+
continue
|
|
84
91
|
print_agent_message(message)
|
|
85
92
|
|
|
86
93
|
final_result = run_pre_commit(workspace_path)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from src.agents.base import print_agent_message, run_agent_query
|
|
4
|
+
from src.agents.base import extract_session_id, print_agent_message, run_agent_query
|
|
5
5
|
from src.clients.jira_client import JiraIssue
|
|
6
6
|
from src.exceptions import PlanNotFoundError
|
|
7
7
|
|
|
@@ -112,11 +112,11 @@ async def plan_ticket(
|
|
|
112
112
|
cwd=workspace_path,
|
|
113
113
|
mcp_config_path=mcp_config_path,
|
|
114
114
|
):
|
|
115
|
-
# First message is the session ID
|
|
116
115
|
if session_id is None:
|
|
117
|
-
session_id = message
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
session_id = extract_session_id(message)
|
|
117
|
+
if session_id:
|
|
118
|
+
continue
|
|
119
|
+
print_agent_message(message)
|
|
120
120
|
|
|
121
121
|
if not plan_path.exists():
|
|
122
122
|
raise PlanNotFoundError(plan_path)
|
|
@@ -153,7 +153,6 @@ async def execute_plan(
|
|
|
153
153
|
prompt=execution_prompt,
|
|
154
154
|
system_prompt=EXECUTION_PHASE_SYSTEM_PROMPT,
|
|
155
155
|
allowed_tools=["Glob", "Bash", "Read", "Grep", "Write"], # Full access
|
|
156
|
-
permission_mode="acceptEdits", # Auto-approve edits without asking
|
|
157
156
|
cwd=workspace_path,
|
|
158
157
|
mcp_config_path=mcp_config_path,
|
|
159
158
|
session_id=session_id,
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import re
|
|
3
|
+
from datetime import datetime
|
|
2
4
|
|
|
3
5
|
from src.clients.github_client import GitHubClient
|
|
4
6
|
from src.clients.jira_client import JiraClient, JiraIssue
|
|
7
|
+
from src.exceptions import GithubBranchAlreadyExistsError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
def sanitize_branch_name(name: str, max_length: int = 100) -> str:
|
|
@@ -42,7 +47,20 @@ def create_branch_from_jira_issue(
|
|
|
42
47
|
branch_name = generate_branch_name(jira_issue.key, jira_issue.summary, jira_issue.type)
|
|
43
48
|
# TODO: Consider replacing the 2 next lines with local git client
|
|
44
49
|
base_ref = github_client.get_base_branch_ref(base_branch)
|
|
45
|
-
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
branch_url = github_client.create_branch(branch_name, base_ref)
|
|
53
|
+
except GithubBranchAlreadyExistsError:
|
|
54
|
+
# Branch already exists, add timestamp suffix and retry
|
|
55
|
+
original_branch_name = branch_name
|
|
56
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
57
|
+
branch_name = f"{branch_name}-{timestamp}"
|
|
58
|
+
logger.info(
|
|
59
|
+
"Branch '%s' already exists. Retrying with new name: '%s'",
|
|
60
|
+
original_branch_name,
|
|
61
|
+
branch_name,
|
|
62
|
+
)
|
|
63
|
+
branch_url = github_client.create_branch(branch_name, base_ref)
|
|
46
64
|
|
|
47
65
|
jira_client.link_branch(jira_issue.key, branch_url, branch_name)
|
|
48
66
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
4
6
|
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Generator
|
|
9
|
+
from contextlib import contextmanager
|
|
5
10
|
from pathlib import Path
|
|
6
11
|
from typing import TYPE_CHECKING
|
|
7
12
|
|
|
@@ -24,6 +29,8 @@ from src.enhanced_git import EnhancedGit
|
|
|
24
29
|
from src.logging_setup import LoggerHandlerType, SetupLoggerParams, setup_logger
|
|
25
30
|
from src.settings import AppSettings
|
|
26
31
|
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
27
34
|
if TYPE_CHECKING:
|
|
28
35
|
from src.clients.github_client import GitHubClient
|
|
29
36
|
from src.clients.jira_client import JiraClient
|
|
@@ -48,10 +55,9 @@ def _load_settings() -> AppSettings:
|
|
|
48
55
|
sys.exit(1)
|
|
49
56
|
|
|
50
57
|
|
|
51
|
-
def _initialize_clients(settings: AppSettings) -> tuple[GitHubClient, JiraClient
|
|
58
|
+
def _initialize_clients(settings: AppSettings) -> tuple[GitHubClient, JiraClient]:
|
|
52
59
|
from src.clients.github_client import GitHubClient
|
|
53
60
|
from src.clients.jira_client import JiraClient
|
|
54
|
-
from src.enhanced_git import EnhancedGit
|
|
55
61
|
|
|
56
62
|
github_client = GitHubClient(
|
|
57
63
|
github_token=settings.github.api_token,
|
|
@@ -62,8 +68,7 @@ def _initialize_clients(settings: AppSettings) -> tuple[GitHubClient, JiraClient
|
|
|
62
68
|
username=settings.jira.username,
|
|
63
69
|
password=settings.jira.api_token,
|
|
64
70
|
)
|
|
65
|
-
|
|
66
|
-
return github_client, jira_client, git
|
|
71
|
+
return github_client, jira_client
|
|
67
72
|
|
|
68
73
|
|
|
69
74
|
def _init() -> None:
|
|
@@ -74,6 +79,40 @@ def _init() -> None:
|
|
|
74
79
|
initialize_settings(config_path)
|
|
75
80
|
|
|
76
81
|
|
|
82
|
+
@contextmanager
|
|
83
|
+
def _setup_workspace(
|
|
84
|
+
workspace_path_arg: Path | None,
|
|
85
|
+
workspace_path_settings: Path | None,
|
|
86
|
+
github_client: GitHubClient,
|
|
87
|
+
) -> Generator[tuple[EnhancedGit, Path]]:
|
|
88
|
+
"""
|
|
89
|
+
Set up the workspace for the workflow.
|
|
90
|
+
|
|
91
|
+
If no workspace_path is provided (neither arg nor settings), clones the repository
|
|
92
|
+
to a temp directory and cleans it up when done.
|
|
93
|
+
|
|
94
|
+
Yields:
|
|
95
|
+
A tuple of (EnhancedGit instance, workspace_path)
|
|
96
|
+
"""
|
|
97
|
+
workspace_path = workspace_path_arg or workspace_path_settings
|
|
98
|
+
temp_dir: Path | None = None
|
|
99
|
+
try:
|
|
100
|
+
if workspace_path is None:
|
|
101
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="ticket2pr_"))
|
|
102
|
+
logger.info(
|
|
103
|
+
"No workspace path provided, cloning repository to temp directory: %s", temp_dir
|
|
104
|
+
)
|
|
105
|
+
local_git = EnhancedGit.clone_repo(github_client.clone_url, temp_dir)
|
|
106
|
+
logger.info("Repository cloned successfully")
|
|
107
|
+
yield local_git, temp_dir
|
|
108
|
+
else:
|
|
109
|
+
yield EnhancedGit(workspace_path), workspace_path
|
|
110
|
+
finally:
|
|
111
|
+
if temp_dir and temp_dir.exists():
|
|
112
|
+
logger.info("Cleaning up temp directory: %s", temp_dir)
|
|
113
|
+
shutil.rmtree(temp_dir)
|
|
114
|
+
|
|
115
|
+
|
|
77
116
|
async def workflow_with_prints(
|
|
78
117
|
jira_issue_key: str,
|
|
79
118
|
workspace_path: Path,
|
|
@@ -82,6 +121,7 @@ async def workflow_with_prints(
|
|
|
82
121
|
jira_client: JiraClient,
|
|
83
122
|
local_git: EnhancedGit,
|
|
84
123
|
mcp_config_path: Path | None = None,
|
|
124
|
+
commit_no_verify: bool = False,
|
|
85
125
|
) -> None:
|
|
86
126
|
header_msg = f"Running workflow for {format_yellow(jira_issue_key)}"
|
|
87
127
|
print_info(header_msg)
|
|
@@ -99,6 +139,7 @@ async def workflow_with_prints(
|
|
|
99
139
|
git=local_git,
|
|
100
140
|
base_branch=base_branch,
|
|
101
141
|
mcp_config_path=mcp_config_path,
|
|
142
|
+
commit_no_verify=commit_no_verify,
|
|
102
143
|
)
|
|
103
144
|
|
|
104
145
|
success_msg = format_success_with_checkmark("Workflow completed successfully!")
|
|
@@ -123,12 +164,17 @@ def run(
|
|
|
123
164
|
mcp_config_path: Path | None = typer.Option( # noqa: B008
|
|
124
165
|
None, "--mcp-config-path", "-m", help="Path to mcp.json config file for Claude agents"
|
|
125
166
|
),
|
|
167
|
+
commit_no_verify: bool = typer.Option(
|
|
168
|
+
False,
|
|
169
|
+
"--commit-no-verify",
|
|
170
|
+
"-c",
|
|
171
|
+
help="bypass pre-commit and commit-msg hooks when committing (git commit --no-verify)",
|
|
172
|
+
),
|
|
126
173
|
) -> None:
|
|
127
174
|
"""Execute the workflow for a specific Jira ticket."""
|
|
128
175
|
|
|
129
176
|
settings = _load_settings()
|
|
130
177
|
|
|
131
|
-
final_workspace_path = workspace_path or settings.core.workspace_path
|
|
132
178
|
final_base_branch = base_branch or settings.core.base_branch
|
|
133
179
|
|
|
134
180
|
setup_logger(
|
|
@@ -141,30 +187,35 @@ def run(
|
|
|
141
187
|
|
|
142
188
|
with get_status("Initializing clients...", spinner="dots"):
|
|
143
189
|
try:
|
|
144
|
-
github_client, jira_client
|
|
190
|
+
github_client, jira_client = _initialize_clients(settings)
|
|
145
191
|
except Exception as e:
|
|
146
192
|
print_error(str(e))
|
|
147
193
|
sys.exit(1)
|
|
148
194
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
with _setup_workspace(workspace_path, settings.core.workspace_path, github_client) as (
|
|
196
|
+
local_git,
|
|
197
|
+
final_workspace_path,
|
|
198
|
+
):
|
|
199
|
+
try:
|
|
200
|
+
asyncio.run(
|
|
201
|
+
workflow_with_prints(
|
|
202
|
+
jira_issue_key,
|
|
203
|
+
final_workspace_path,
|
|
204
|
+
final_base_branch,
|
|
205
|
+
github_client,
|
|
206
|
+
jira_client,
|
|
207
|
+
local_git,
|
|
208
|
+
mcp_config_path,
|
|
209
|
+
commit_no_verify,
|
|
210
|
+
)
|
|
159
211
|
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
sys.exit(1)
|
|
212
|
+
except KeyboardInterrupt:
|
|
213
|
+
print_empty_line()
|
|
214
|
+
print_warning("Workflow interrupted by user")
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
print_error(str(e), title="Error")
|
|
218
|
+
sys.exit(1)
|
|
168
219
|
|
|
169
220
|
|
|
170
221
|
@app.command()
|
|
@@ -43,6 +43,10 @@ class GitHubClient:
|
|
|
43
43
|
self.client = Github(github_token)
|
|
44
44
|
self.repo = self.client.get_repo(repo_full_name)
|
|
45
45
|
|
|
46
|
+
@property
|
|
47
|
+
def clone_url(self) -> str:
|
|
48
|
+
return self.repo.clone_url # type: ignore[no-any-return]
|
|
49
|
+
|
|
46
50
|
def get_base_branch_ref(self, base_branch: str) -> GitRef:
|
|
47
51
|
try:
|
|
48
52
|
return self.repo.get_git_ref(f"heads/{base_branch}")
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from typing import Self
|
|
2
3
|
|
|
3
4
|
import git
|
|
4
5
|
|
|
5
6
|
from src.exceptions import (
|
|
7
|
+
GitCloneError,
|
|
6
8
|
GitFetchCheckoutError,
|
|
7
9
|
GitPushError,
|
|
8
10
|
GitWorkspacePathNotExistsError,
|
|
@@ -31,6 +33,27 @@ class EnhancedGit:
|
|
|
31
33
|
self.repo_path = repo_path.expanduser()
|
|
32
34
|
self._repo = None
|
|
33
35
|
|
|
36
|
+
@classmethod
|
|
37
|
+
def clone_repo(cls, clone_url: str, target_path: Path) -> Self:
|
|
38
|
+
"""
|
|
39
|
+
Clone a repository to the specified path and return an EnhancedGit instance.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
clone_url: The URL of the repository to clone
|
|
43
|
+
target_path: The path where the repository will be cloned
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
An EnhancedGit instance for the cloned repository
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
GitCloneError: If cloning fails
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
git.Repo.clone_from(clone_url, target_path)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise GitCloneError(clone_url, str(e)) from e
|
|
55
|
+
return cls(target_path)
|
|
56
|
+
|
|
34
57
|
@property
|
|
35
58
|
def repo(self) -> git.Repo:
|
|
36
59
|
"""Lazy-load the git repository."""
|
|
@@ -73,7 +96,9 @@ class EnhancedGit:
|
|
|
73
96
|
"""
|
|
74
97
|
self.repo.git.add(A=True)
|
|
75
98
|
|
|
76
|
-
def commit_and_push(
|
|
99
|
+
def commit_and_push(
|
|
100
|
+
self, message: str, remote: str = "origin", no_verify: bool = False
|
|
101
|
+
) -> git.Commit | None:
|
|
77
102
|
"""
|
|
78
103
|
Combined operation: Stage all changes, commit, and push to remote.
|
|
79
104
|
|
|
@@ -86,6 +111,7 @@ class EnhancedGit:
|
|
|
86
111
|
Args:
|
|
87
112
|
message: Commit message
|
|
88
113
|
remote: Remote name to push to (default: "origin")
|
|
114
|
+
no_verify: If True, bypasses pre-commit hooks with --no-verify (default: False)
|
|
89
115
|
|
|
90
116
|
Returns:
|
|
91
117
|
The created commit, or None if there were no changes
|
|
@@ -99,7 +125,11 @@ class EnhancedGit:
|
|
|
99
125
|
print("No changes detected. Nothing to commit.")
|
|
100
126
|
return None
|
|
101
127
|
|
|
102
|
-
|
|
128
|
+
if no_verify:
|
|
129
|
+
commit = self.repo.git.commit("-m", message, "--no-verify")
|
|
130
|
+
commit = self.repo.head.commit
|
|
131
|
+
else:
|
|
132
|
+
commit = self.repo.index.commit(message)
|
|
103
133
|
|
|
104
134
|
# Push to remote
|
|
105
135
|
remote_obj = self.repo.remote(name=remote)
|
|
@@ -31,6 +31,11 @@ class GitPushError(EnhancedGitError):
|
|
|
31
31
|
super().__init__("Failed to commit and push changes")
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class GitCloneError(EnhancedGitError):
|
|
35
|
+
def __init__(self, clone_url: str, error_msg: str) -> None:
|
|
36
|
+
super().__init__(f"Failed to clone repository from '{clone_url}': {error_msg}")
|
|
37
|
+
|
|
38
|
+
|
|
34
39
|
class InvalidGitRepositoryError(EnhancedGitError):
|
|
35
40
|
def __init__(self) -> None:
|
|
36
41
|
super().__init__("Error: The directory provided is not a valid Git repository.")
|
|
@@ -22,7 +22,6 @@ from src.validators import (
|
|
|
22
22
|
validate_non_empty,
|
|
23
23
|
validate_repo_format,
|
|
24
24
|
validate_url,
|
|
25
|
-
validate_workspace_path,
|
|
26
25
|
)
|
|
27
26
|
|
|
28
27
|
|
|
@@ -33,7 +32,6 @@ def _show_welcome() -> None:
|
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
def _show_summary(
|
|
36
|
-
workspace_path: Path,
|
|
37
35
|
base_branch: str,
|
|
38
36
|
jira_base_url: str,
|
|
39
37
|
jira_username: str,
|
|
@@ -43,7 +41,6 @@ def _show_summary(
|
|
|
43
41
|
[
|
|
44
42
|
format_bold("Configuration Summary"),
|
|
45
43
|
"",
|
|
46
|
-
f"{format_dim('Workspace:')} {workspace_path!s}",
|
|
47
44
|
f"{format_dim('Base branch:')} {base_branch!s}",
|
|
48
45
|
f"{format_dim('Jira URL:')} {jira_base_url!s}",
|
|
49
46
|
f"{format_dim('Jira username:')} {jira_username!s}",
|
|
@@ -108,22 +105,16 @@ def section_decorator(section_name: str) -> Callable[[Callable[..., Any]], Calla
|
|
|
108
105
|
|
|
109
106
|
|
|
110
107
|
@section_decorator("Core Settings")
|
|
111
|
-
def _collect_core_settings() ->
|
|
108
|
+
def _collect_core_settings() -> str:
|
|
112
109
|
from src.validators import validate_branch_name
|
|
113
110
|
|
|
114
|
-
workspace_path = _prompt_with_validation(
|
|
115
|
-
"Workspace path",
|
|
116
|
-
validate_workspace_path,
|
|
117
|
-
default=str(Path.cwd()),
|
|
118
|
-
)
|
|
119
|
-
|
|
120
111
|
base_branch = _prompt_with_validation(
|
|
121
112
|
"Base branch",
|
|
122
113
|
validate_branch_name,
|
|
123
114
|
default="main",
|
|
124
115
|
)
|
|
125
116
|
|
|
126
|
-
return
|
|
117
|
+
return base_branch
|
|
127
118
|
|
|
128
119
|
|
|
129
120
|
@section_decorator("Jira Settings")
|
|
@@ -167,7 +158,6 @@ def _collect_github_settings() -> tuple[str, str]:
|
|
|
167
158
|
|
|
168
159
|
def _write_toml_config(
|
|
169
160
|
config_path: Path,
|
|
170
|
-
workspace_path: Path,
|
|
171
161
|
base_branch: str,
|
|
172
162
|
jira_base_url: str,
|
|
173
163
|
jira_username: str,
|
|
@@ -180,7 +170,6 @@ def _write_toml_config(
|
|
|
180
170
|
|
|
181
171
|
config = {
|
|
182
172
|
"core": {
|
|
183
|
-
"workspace_path": str(workspace_path),
|
|
184
173
|
"base_branch": base_branch,
|
|
185
174
|
},
|
|
186
175
|
"jira": {
|
|
@@ -202,12 +191,11 @@ def initialize_settings(config_path: Path) -> None:
|
|
|
202
191
|
try:
|
|
203
192
|
_show_welcome()
|
|
204
193
|
|
|
205
|
-
|
|
194
|
+
base_branch = _collect_core_settings()
|
|
206
195
|
jira_base_url, jira_username, jira_api_token = _collect_jira_settings()
|
|
207
196
|
github_api_token, repo_full_name = _collect_github_settings()
|
|
208
197
|
|
|
209
198
|
_show_summary(
|
|
210
|
-
workspace_path,
|
|
211
199
|
base_branch,
|
|
212
200
|
jira_base_url,
|
|
213
201
|
jira_username,
|
|
@@ -216,7 +204,6 @@ def initialize_settings(config_path: Path) -> None:
|
|
|
216
204
|
if _confirm_save():
|
|
217
205
|
_write_toml_config(
|
|
218
206
|
config_path,
|
|
219
|
-
workspace_path,
|
|
220
207
|
base_branch,
|
|
221
208
|
jira_base_url,
|
|
222
209
|
jira_username,
|
|
@@ -30,6 +30,7 @@ async def workflow(
|
|
|
30
30
|
git: EnhancedGit,
|
|
31
31
|
base_branch: str,
|
|
32
32
|
mcp_config_path: Path | None = None,
|
|
33
|
+
commit_no_verify: bool = False,
|
|
33
34
|
) -> WorkflowResult:
|
|
34
35
|
logger.info("Fetching Jira issue: %s", jira_issue_key)
|
|
35
36
|
issue = jira_client.fetch_issue(jira_issue_key)
|
|
@@ -46,12 +47,21 @@ async def workflow(
|
|
|
46
47
|
session_id = await try_solve_ticket(
|
|
47
48
|
issue, workspace_path=git.repo_path, mcp_config_path=mcp_config_path
|
|
48
49
|
)
|
|
49
|
-
if
|
|
50
|
+
if commit_no_verify:
|
|
51
|
+
logger.info("Skipping pre-commit verification: --commit-no-verify flag is set")
|
|
52
|
+
elif is_pre_commit_installed():
|
|
50
53
|
logger.info("pre-commit is installed. Trying to run it")
|
|
51
54
|
result = run_pre_commit(git.repo_path)
|
|
52
55
|
if not result.success:
|
|
53
56
|
logger.info("pre-commit failed. trying to fix it (workspace: %s)", git.repo_path)
|
|
54
|
-
await verify_pre_commit_and_fix(
|
|
57
|
+
commit_no_verify = not await verify_pre_commit_and_fix(
|
|
58
|
+
git.repo_path, mcp_config_path=mcp_config_path
|
|
59
|
+
)
|
|
60
|
+
if commit_no_verify:
|
|
61
|
+
logger.warning(
|
|
62
|
+
"pre-commit verification failed after fix attempts. "
|
|
63
|
+
"Will commit with --no-verify"
|
|
64
|
+
)
|
|
55
65
|
else:
|
|
56
66
|
logger.info("Skipping pre-commit fix: pre-commit is is already passing")
|
|
57
67
|
else:
|
|
@@ -65,7 +75,7 @@ async def workflow(
|
|
|
65
75
|
branch_name,
|
|
66
76
|
commit_message.split("\n")[0] if commit_message else "N/A",
|
|
67
77
|
)
|
|
68
|
-
git.commit_and_push(commit_message)
|
|
78
|
+
git.commit_and_push(commit_message, no_verify=commit_no_verify)
|
|
69
79
|
logger.info("Generating PR title for issue: %s", issue.key)
|
|
70
80
|
pr_title = generate_pr_title_from_jira_issue(issue)
|
|
71
81
|
logger.info("Creating PR: title='%s', head=%s, base=%s", pr_title, branch_name, base_branch)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|