titan-cli 0.1.3__py3-none-any.whl → 0.1.5__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.
- titan_cli/core/config.py +3 -1
- titan_cli/core/plugins/models.py +35 -7
- titan_cli/core/plugins/plugin_registry.py +11 -2
- titan_cli/core/workflows/__init__.py +2 -1
- titan_cli/core/workflows/project_step_source.py +48 -30
- titan_cli/core/workflows/workflow_filter_service.py +14 -8
- titan_cli/core/workflows/workflow_registry.py +12 -1
- titan_cli/core/workflows/workflow_sources.py +1 -1
- titan_cli/engine/steps/ai_assistant_step.py +42 -7
- titan_cli/engine/workflow_executor.py +6 -1
- titan_cli/ui/tui/screens/plugin_config_wizard.py +40 -9
- titan_cli/ui/tui/screens/workflow_execution.py +8 -28
- titan_cli/ui/tui/textual_components.py +59 -6
- titan_cli/ui/tui/textual_workflow_executor.py +9 -1
- titan_cli/ui/tui/widgets/__init__.py +2 -0
- titan_cli/ui/tui/widgets/step_container.py +70 -0
- {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/METADATA +6 -3
- {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/RECORD +42 -40
- {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/WHEEL +1 -1
- titan_plugin_git/clients/git_client.py +82 -4
- titan_plugin_git/plugin.py +3 -0
- titan_plugin_git/steps/ai_commit_message_step.py +33 -28
- titan_plugin_git/steps/branch_steps.py +18 -37
- titan_plugin_git/steps/commit_step.py +18 -22
- titan_plugin_git/steps/diff_summary_step.py +182 -0
- titan_plugin_git/steps/push_step.py +27 -11
- titan_plugin_git/steps/status_step.py +15 -18
- titan_plugin_git/workflows/commit-ai.yaml +5 -0
- titan_plugin_github/agents/pr_agent.py +15 -2
- titan_plugin_github/steps/ai_pr_step.py +12 -21
- titan_plugin_github/steps/create_pr_step.py +17 -7
- titan_plugin_github/steps/github_prompt_steps.py +52 -0
- titan_plugin_github/steps/issue_steps.py +28 -14
- titan_plugin_github/steps/preview_step.py +11 -0
- titan_plugin_github/utils.py +5 -4
- titan_plugin_github/workflows/create-pr-ai.yaml +5 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +8 -3
- titan_plugin_jira/steps/get_issue_step.py +16 -12
- titan_plugin_jira/steps/prompt_select_issue_step.py +22 -9
- titan_plugin_jira/steps/search_saved_query_step.py +21 -19
- {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info}/entry_points.txt +0 -0
- {titan_cli-0.1.3.dist-info → titan_cli-0.1.5.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# plugins/titan-plugin-git/titan_plugin_git/steps/ai_commit_message_step.py
|
|
2
2
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
|
|
3
3
|
from titan_plugin_git.messages import msg
|
|
4
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
|
|
@@ -29,29 +28,25 @@ def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
29
28
|
if not ctx.textual:
|
|
30
29
|
return Error("Textual UI context is not available for this step.")
|
|
31
30
|
|
|
31
|
+
# Begin step container
|
|
32
|
+
ctx.textual.begin_step("AI Commit Message")
|
|
33
|
+
|
|
32
34
|
# Check if AI is configured
|
|
33
35
|
if not ctx.ai or not ctx.ai.is_available():
|
|
34
|
-
ctx.textual.
|
|
35
|
-
|
|
36
|
-
text=msg.Steps.AICommitMessage.AI_NOT_CONFIGURED,
|
|
37
|
-
panel_type="info"
|
|
38
|
-
)
|
|
39
|
-
)
|
|
36
|
+
ctx.textual.text(msg.Steps.AICommitMessage.AI_NOT_CONFIGURED, markup="dim")
|
|
37
|
+
ctx.textual.end_step("skip")
|
|
40
38
|
return Skip(msg.Steps.AICommitMessage.AI_NOT_CONFIGURED)
|
|
41
39
|
|
|
42
40
|
# Get git client
|
|
43
41
|
if not ctx.git:
|
|
42
|
+
ctx.textual.end_step("error")
|
|
44
43
|
return Error(msg.Steps.AICommitMessage.GIT_CLIENT_NOT_AVAILABLE)
|
|
45
44
|
|
|
46
45
|
# Get git status
|
|
47
46
|
git_status = ctx.get('git_status')
|
|
48
47
|
if not git_status or git_status.is_clean:
|
|
49
|
-
ctx.textual.
|
|
50
|
-
|
|
51
|
-
text=msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT,
|
|
52
|
-
panel_type="info"
|
|
53
|
-
)
|
|
54
|
-
)
|
|
48
|
+
ctx.textual.text(msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT, markup="dim")
|
|
49
|
+
ctx.textual.end_step("skip")
|
|
55
50
|
return Skip(msg.Steps.AICommitMessage.NO_CHANGES_TO_COMMIT)
|
|
56
51
|
|
|
57
52
|
try:
|
|
@@ -62,6 +57,7 @@ def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
62
57
|
diff_text = ctx.git.get_uncommitted_diff()
|
|
63
58
|
|
|
64
59
|
if not diff_text or diff_text.strip() == "":
|
|
60
|
+
ctx.textual.end_step("skip")
|
|
65
61
|
return Skip(msg.Steps.AICommitMessage.NO_UNCOMMITTED_CHANGES)
|
|
66
62
|
|
|
67
63
|
# Build AI prompt
|
|
@@ -86,17 +82,17 @@ def ai_generate_commit_message(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
86
82
|
|
|
87
83
|
## CRITICAL Instructions
|
|
88
84
|
Generate ONE single-line conventional commit message following this EXACT format:
|
|
89
|
-
- type(scope):
|
|
85
|
+
- type(scope): Description
|
|
90
86
|
- Types: feat, fix, refactor, docs, test, chore, style, perf
|
|
91
87
|
- Scope: area affected (e.g., auth, api, ui)
|
|
92
|
-
- Description: clear summary in imperative mood (be descriptive, concise, and at least 5 words long)
|
|
88
|
+
- Description: clear summary in imperative mood, starting with CAPITAL letter (be descriptive, concise, and at least 5 words long)
|
|
93
89
|
- NO line breaks, NO body, NO additional explanation
|
|
94
90
|
|
|
95
|
-
Examples (notice they are all one line):
|
|
96
|
-
- feat(auth):
|
|
97
|
-
- fix(api):
|
|
98
|
-
- refactor(ui):
|
|
99
|
-
- refactor(workflows):
|
|
91
|
+
Examples (notice they start with capital letter and are all one line):
|
|
92
|
+
- feat(auth): Add OAuth2 integration with Google provider
|
|
93
|
+
- fix(api): Resolve race condition in cache invalidation
|
|
94
|
+
- refactor(ui): Simplify menu component and remove unused props
|
|
95
|
+
- refactor(workflows): Add support for nested workflow execution
|
|
100
96
|
|
|
101
97
|
Return ONLY the single-line commit message, absolutely nothing else."""
|
|
102
98
|
|
|
@@ -115,6 +111,18 @@ Return ONLY the single-line commit message, absolutely nothing else."""
|
|
|
115
111
|
# Take only the first line if AI returned multiple lines
|
|
116
112
|
commit_message = commit_message.split('\n')[0].strip()
|
|
117
113
|
|
|
114
|
+
# Ensure subject starts with capital letter (conventional commits requirement)
|
|
115
|
+
# Format: type(scope): Description
|
|
116
|
+
if ':' in commit_message:
|
|
117
|
+
parts = commit_message.split(':', 1)
|
|
118
|
+
if len(parts) == 2:
|
|
119
|
+
prefix = parts[0] # type(scope)
|
|
120
|
+
subject = parts[1].strip() # description
|
|
121
|
+
# Capitalize first letter of subject
|
|
122
|
+
if subject and subject[0].islower():
|
|
123
|
+
subject = subject[0].upper() + subject[1:]
|
|
124
|
+
commit_message = f"{prefix}: {subject}"
|
|
125
|
+
|
|
118
126
|
# Show preview to user
|
|
119
127
|
ctx.textual.text("") # spacing
|
|
120
128
|
ctx.textual.text(msg.Steps.AICommitMessage.GENERATED_MESSAGE_TITLE, markup="bold")
|
|
@@ -136,25 +144,21 @@ Return ONLY the single-line commit message, absolutely nothing else."""
|
|
|
136
144
|
try:
|
|
137
145
|
manual_message = ctx.textual.ask_text(msg.Prompts.ENTER_COMMIT_MESSAGE)
|
|
138
146
|
if not manual_message:
|
|
147
|
+
ctx.textual.end_step("error")
|
|
139
148
|
return Error(msg.Steps.Commit.COMMIT_MESSAGE_REQUIRED)
|
|
140
149
|
|
|
141
150
|
# Overwrite the metadata to ensure the manual message is used
|
|
151
|
+
ctx.textual.end_step("success")
|
|
142
152
|
return Success(
|
|
143
153
|
message=msg.Steps.Prompt.COMMIT_MESSAGE_CAPTURED,
|
|
144
154
|
metadata={"commit_message": manual_message}
|
|
145
155
|
)
|
|
146
156
|
except (KeyboardInterrupt, EOFError):
|
|
157
|
+
ctx.textual.end_step("error")
|
|
147
158
|
return Error(msg.Steps.Prompt.USER_CANCELLED)
|
|
148
159
|
|
|
149
|
-
# Show success panel
|
|
150
|
-
ctx.textual.mount(
|
|
151
|
-
Panel(
|
|
152
|
-
text="AI commit message generated successfully",
|
|
153
|
-
panel_type="success"
|
|
154
|
-
)
|
|
155
|
-
)
|
|
156
|
-
|
|
157
160
|
# Success - save to context
|
|
161
|
+
ctx.textual.end_step("success")
|
|
158
162
|
return Success(
|
|
159
163
|
msg.Steps.AICommitMessage.SUCCESS_MESSAGE,
|
|
160
164
|
metadata={"commit_message": commit_message}
|
|
@@ -164,6 +168,7 @@ Return ONLY the single-line commit message, absolutely nothing else."""
|
|
|
164
168
|
ctx.textual.text(msg.Steps.AICommitMessage.GENERATION_FAILED.format(e=e), markup="yellow")
|
|
165
169
|
ctx.textual.text(msg.Steps.AICommitMessage.FALLBACK_TO_MANUAL, markup="dim")
|
|
166
170
|
|
|
171
|
+
ctx.textual.end_step("skip")
|
|
167
172
|
return Skip(msg.Steps.AICommitMessage.GENERATION_FAILED.format(e=e))
|
|
168
173
|
|
|
169
174
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# plugins/titan-plugin-git/titan_plugin_git/steps/branch_steps.py
|
|
2
2
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
3
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
4
3
|
from titan_plugin_git.messages import msg
|
|
5
4
|
|
|
6
5
|
def get_current_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
@@ -20,37 +19,28 @@ def get_current_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
20
19
|
if not ctx.textual:
|
|
21
20
|
return Error("Textual UI context is not available for this step.")
|
|
22
21
|
|
|
22
|
+
# Begin step container
|
|
23
|
+
ctx.textual.begin_step("Get Head Branch")
|
|
24
|
+
|
|
23
25
|
if not ctx.git:
|
|
24
26
|
error_msg = msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE
|
|
25
|
-
ctx.textual.
|
|
26
|
-
|
|
27
|
-
text=error_msg,
|
|
28
|
-
panel_type="error"
|
|
29
|
-
)
|
|
30
|
-
)
|
|
27
|
+
ctx.textual.text(error_msg, markup="red")
|
|
28
|
+
ctx.textual.end_step("error")
|
|
31
29
|
return Error(error_msg)
|
|
32
30
|
|
|
33
31
|
try:
|
|
34
32
|
current_branch = ctx.git.get_current_branch()
|
|
35
33
|
success_msg = msg.Steps.Branch.GET_CURRENT_BRANCH_SUCCESS.format(branch=current_branch)
|
|
36
|
-
ctx.textual.
|
|
37
|
-
|
|
38
|
-
text=success_msg,
|
|
39
|
-
panel_type="success"
|
|
40
|
-
)
|
|
41
|
-
)
|
|
34
|
+
ctx.textual.text(success_msg, markup="green")
|
|
35
|
+
ctx.textual.end_step("success")
|
|
42
36
|
return Success(
|
|
43
37
|
success_msg,
|
|
44
38
|
metadata={"pr_head_branch": current_branch}
|
|
45
39
|
)
|
|
46
40
|
except Exception as e:
|
|
47
41
|
error_msg = msg.Steps.Branch.GET_CURRENT_BRANCH_FAILED.format(e=e)
|
|
48
|
-
ctx.textual.
|
|
49
|
-
|
|
50
|
-
text=error_msg,
|
|
51
|
-
panel_type="error"
|
|
52
|
-
)
|
|
53
|
-
)
|
|
42
|
+
ctx.textual.text(error_msg, markup="red")
|
|
43
|
+
ctx.textual.end_step("error")
|
|
54
44
|
return Error(error_msg, exception=e)
|
|
55
45
|
|
|
56
46
|
def get_base_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
@@ -70,35 +60,26 @@ def get_base_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
70
60
|
if not ctx.textual:
|
|
71
61
|
return Error("Textual UI context is not available for this step.")
|
|
72
62
|
|
|
63
|
+
# Begin step container
|
|
64
|
+
ctx.textual.begin_step("Get Base Branch")
|
|
65
|
+
|
|
73
66
|
if not ctx.git:
|
|
74
67
|
error_msg = msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE
|
|
75
|
-
ctx.textual.
|
|
76
|
-
|
|
77
|
-
text=error_msg,
|
|
78
|
-
panel_type="error"
|
|
79
|
-
)
|
|
80
|
-
)
|
|
68
|
+
ctx.textual.text(error_msg, markup="red")
|
|
69
|
+
ctx.textual.end_step("error")
|
|
81
70
|
return Error(error_msg)
|
|
82
71
|
|
|
83
72
|
try:
|
|
84
73
|
base_branch = ctx.git.main_branch
|
|
85
74
|
success_msg = msg.Steps.Branch.GET_BASE_BRANCH_SUCCESS.format(branch=base_branch)
|
|
86
|
-
ctx.textual.
|
|
87
|
-
|
|
88
|
-
text=success_msg,
|
|
89
|
-
panel_type="success"
|
|
90
|
-
)
|
|
91
|
-
)
|
|
75
|
+
ctx.textual.text(success_msg, markup="green")
|
|
76
|
+
ctx.textual.end_step("success")
|
|
92
77
|
return Success(
|
|
93
78
|
success_msg,
|
|
94
79
|
metadata={"pr_base_branch": base_branch}
|
|
95
80
|
)
|
|
96
81
|
except Exception as e:
|
|
97
82
|
error_msg = msg.Steps.Branch.GET_BASE_BRANCH_FAILED.format(e=e)
|
|
98
|
-
ctx.textual.
|
|
99
|
-
|
|
100
|
-
text=error_msg,
|
|
101
|
-
panel_type="error"
|
|
102
|
-
)
|
|
103
|
-
)
|
|
83
|
+
ctx.textual.text(error_msg, markup="red")
|
|
84
|
+
ctx.textual.end_step("error")
|
|
104
85
|
return Error(error_msg, exception=e)
|
|
@@ -3,7 +3,6 @@ from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
|
3
3
|
from titan_cli.engine.results import Skip
|
|
4
4
|
from titan_plugin_git.exceptions import GitClientError, GitCommandError
|
|
5
5
|
from titan_plugin_git.messages import msg
|
|
6
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
@@ -18,6 +17,7 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
18
17
|
git_status (GitStatus): The git status object, used to check if the working directory is clean.
|
|
19
18
|
commit_message (str): The message for the commit.
|
|
20
19
|
all_files (bool, optional): Whether to commit all modified and new files. Defaults to True.
|
|
20
|
+
no_verify (bool, optional): Skip pre-commit and commit-msg hooks. Defaults to False.
|
|
21
21
|
commit_hash (str, optional): If present, indicates a commit was already created.
|
|
22
22
|
|
|
23
23
|
Outputs (saved to ctx.data):
|
|
@@ -31,50 +31,46 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
31
31
|
if not ctx.textual:
|
|
32
32
|
return Error("Textual UI context is not available for this step.")
|
|
33
33
|
|
|
34
|
+
# Begin step container
|
|
35
|
+
ctx.textual.begin_step("Create Commit")
|
|
36
|
+
|
|
34
37
|
# Skip if there's nothing to commit
|
|
35
38
|
git_status = ctx.data.get("git_status")
|
|
36
39
|
if git_status and git_status.is_clean:
|
|
37
|
-
ctx.textual.
|
|
38
|
-
|
|
39
|
-
text=msg.Steps.Commit.WORKING_DIRECTORY_CLEAN,
|
|
40
|
-
panel_type="info"
|
|
41
|
-
)
|
|
42
|
-
)
|
|
40
|
+
ctx.textual.text(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN, markup="dim")
|
|
41
|
+
ctx.textual.end_step("skip")
|
|
43
42
|
return Skip(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN)
|
|
44
43
|
|
|
45
44
|
if not ctx.git:
|
|
45
|
+
ctx.textual.end_step("error")
|
|
46
46
|
return Error(msg.Steps.Commit.GIT_CLIENT_NOT_AVAILABLE)
|
|
47
47
|
|
|
48
48
|
commit_message = ctx.get('commit_message')
|
|
49
49
|
if not commit_message:
|
|
50
|
-
ctx.textual.
|
|
51
|
-
|
|
52
|
-
text=msg.Steps.Commit.NO_COMMIT_MESSAGE,
|
|
53
|
-
panel_type="info"
|
|
54
|
-
)
|
|
55
|
-
)
|
|
50
|
+
ctx.textual.text(msg.Steps.Commit.NO_COMMIT_MESSAGE, markup="dim")
|
|
51
|
+
ctx.textual.end_step("skip")
|
|
56
52
|
return Skip(msg.Steps.Commit.NO_COMMIT_MESSAGE)
|
|
57
|
-
|
|
53
|
+
|
|
58
54
|
all_files = ctx.get('all_files', True)
|
|
55
|
+
no_verify = ctx.get('no_verify', False)
|
|
59
56
|
|
|
60
57
|
try:
|
|
61
|
-
commit_hash = ctx.git.commit(message=commit_message, all=all_files)
|
|
58
|
+
commit_hash = ctx.git.commit(message=commit_message, all=all_files, no_verify=no_verify)
|
|
62
59
|
|
|
63
|
-
# Show success
|
|
64
|
-
ctx.textual.
|
|
65
|
-
Panel(
|
|
66
|
-
text=f"Commit created: {commit_hash[:7]}",
|
|
67
|
-
panel_type="success"
|
|
68
|
-
)
|
|
69
|
-
)
|
|
60
|
+
# Show success message
|
|
61
|
+
ctx.textual.text(f"Commit created: {commit_hash[:7]}", markup="green")
|
|
70
62
|
|
|
63
|
+
ctx.textual.end_step("success")
|
|
71
64
|
return Success(
|
|
72
65
|
message=msg.Steps.Commit.COMMIT_SUCCESS.format(commit_hash=commit_hash),
|
|
73
66
|
metadata={"commit_hash": commit_hash}
|
|
74
67
|
)
|
|
75
68
|
except GitClientError as e:
|
|
69
|
+
ctx.textual.end_step("error")
|
|
76
70
|
return Error(msg.Steps.Commit.CLIENT_ERROR_DURING_COMMIT.format(e=e))
|
|
77
71
|
except GitCommandError as e:
|
|
72
|
+
ctx.textual.end_step("error")
|
|
78
73
|
return Error(msg.Steps.Commit.COMMAND_FAILED_DURING_COMMIT.format(e=e))
|
|
79
74
|
except Exception as e:
|
|
75
|
+
ctx.textual.end_step("error")
|
|
80
76
|
return Error(msg.Steps.Commit.UNEXPECTED_ERROR_DURING_COMMIT.format(e=e))
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# plugins/titan-plugin-git/titan_plugin_git/steps/diff_summary_step.py
|
|
2
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
|
|
3
|
+
from titan_plugin_git.messages import msg
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def show_uncommitted_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
7
|
+
"""
|
|
8
|
+
Show summary of uncommitted changes (git diff --stat).
|
|
9
|
+
|
|
10
|
+
Provides a visual overview of files changed and lines modified
|
|
11
|
+
before generating commit messages.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Success: Always (even if no changes, for workflow continuity)
|
|
15
|
+
"""
|
|
16
|
+
if not ctx.textual:
|
|
17
|
+
return Error("Textual UI context is not available for this step.")
|
|
18
|
+
|
|
19
|
+
if not ctx.git:
|
|
20
|
+
return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
|
|
21
|
+
|
|
22
|
+
# Begin step container
|
|
23
|
+
ctx.textual.begin_step("Show Changes Summary")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Get diff stat for uncommitted changes
|
|
27
|
+
stat_output = ctx.git._run_command(["git", "diff", "--stat", "HEAD"])
|
|
28
|
+
|
|
29
|
+
if not stat_output or not stat_output.strip():
|
|
30
|
+
ctx.textual.text("No uncommitted changes to show", markup="dim")
|
|
31
|
+
ctx.textual.end_step("success")
|
|
32
|
+
return Success("No changes")
|
|
33
|
+
|
|
34
|
+
# Show the stat summary with colors
|
|
35
|
+
ctx.textual.text("") # spacing
|
|
36
|
+
ctx.textual.text("Changes summary:", markup="bold")
|
|
37
|
+
ctx.textual.text("") # spacing
|
|
38
|
+
|
|
39
|
+
# Parse lines to find max filename length for alignment
|
|
40
|
+
file_lines = []
|
|
41
|
+
summary_lines = []
|
|
42
|
+
max_filename_len = 0
|
|
43
|
+
|
|
44
|
+
for line in stat_output.split('\n'):
|
|
45
|
+
if not line.strip():
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if '|' in line:
|
|
49
|
+
parts = line.split('|')
|
|
50
|
+
filename = parts[0].strip()
|
|
51
|
+
stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
|
|
52
|
+
file_lines.append((filename, stats))
|
|
53
|
+
max_filename_len = max(max_filename_len, len(filename))
|
|
54
|
+
else:
|
|
55
|
+
summary_lines.append(line)
|
|
56
|
+
|
|
57
|
+
# Display aligned file changes
|
|
58
|
+
for filename, stats in file_lines:
|
|
59
|
+
# Pad filename to align pipes
|
|
60
|
+
padded_filename = filename.ljust(max_filename_len)
|
|
61
|
+
|
|
62
|
+
# Replace + with green and - with red
|
|
63
|
+
stats = stats.replace('+', '[green]+[/green]')
|
|
64
|
+
stats = stats.replace('-', '[red]-[/red]')
|
|
65
|
+
|
|
66
|
+
ctx.textual.text(f" {padded_filename} | {stats}")
|
|
67
|
+
|
|
68
|
+
# Display summary lines
|
|
69
|
+
for line in summary_lines:
|
|
70
|
+
colored_line = line.replace('(+)', '[green](+)[/green]')
|
|
71
|
+
colored_line = colored_line.replace('(-)', '[red](-)[/red]')
|
|
72
|
+
ctx.textual.text(f" {colored_line}", markup="dim")
|
|
73
|
+
|
|
74
|
+
ctx.textual.text("") # spacing
|
|
75
|
+
|
|
76
|
+
# End step container with success
|
|
77
|
+
ctx.textual.end_step("success")
|
|
78
|
+
|
|
79
|
+
return Success("Diff summary displayed")
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
# Don't fail the workflow, just skip
|
|
83
|
+
ctx.textual.end_step("skip")
|
|
84
|
+
return Skip(f"Could not show diff summary: {e}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
88
|
+
"""
|
|
89
|
+
Show summary of branch changes (git diff base...head --stat).
|
|
90
|
+
|
|
91
|
+
Provides a visual overview of files changed between branches
|
|
92
|
+
before generating PR descriptions.
|
|
93
|
+
|
|
94
|
+
Inputs (from ctx.data):
|
|
95
|
+
pr_head_branch (str): Head branch name
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Success: Always (even if no changes, for workflow continuity)
|
|
99
|
+
"""
|
|
100
|
+
if not ctx.textual:
|
|
101
|
+
return Error("Textual UI context is not available for this step.")
|
|
102
|
+
|
|
103
|
+
# Begin step container
|
|
104
|
+
ctx.textual.begin_step("Show Branch Changes Summary")
|
|
105
|
+
|
|
106
|
+
if not ctx.git:
|
|
107
|
+
ctx.textual.text(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE, markup="red")
|
|
108
|
+
ctx.textual.end_step("error")
|
|
109
|
+
return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
|
|
110
|
+
|
|
111
|
+
head_branch = ctx.get("pr_head_branch")
|
|
112
|
+
if not head_branch:
|
|
113
|
+
ctx.textual.text("No head branch specified", markup="dim")
|
|
114
|
+
ctx.textual.end_step("skip")
|
|
115
|
+
return Skip("No head branch specified")
|
|
116
|
+
|
|
117
|
+
base_branch = ctx.git.main_branch
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Get diff stat between branches
|
|
121
|
+
stat_output = ctx.git._run_command([
|
|
122
|
+
"git", "diff", "--stat", f"{base_branch}...{head_branch}"
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
if not stat_output or not stat_output.strip():
|
|
126
|
+
ctx.textual.text(f"No changes between {base_branch} and {head_branch}", markup="dim")
|
|
127
|
+
ctx.textual.end_step("success")
|
|
128
|
+
return Success("No changes")
|
|
129
|
+
|
|
130
|
+
# Show the stat summary with colors
|
|
131
|
+
ctx.textual.text("") # spacing
|
|
132
|
+
ctx.textual.text(f"Changes in {head_branch} vs {base_branch}:", markup="bold")
|
|
133
|
+
ctx.textual.text("") # spacing
|
|
134
|
+
|
|
135
|
+
# Parse lines to find max filename length for alignment
|
|
136
|
+
file_lines = []
|
|
137
|
+
summary_lines = []
|
|
138
|
+
max_filename_len = 0
|
|
139
|
+
|
|
140
|
+
for line in stat_output.split('\n'):
|
|
141
|
+
if not line.strip():
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if '|' in line:
|
|
145
|
+
parts = line.split('|')
|
|
146
|
+
filename = parts[0].strip()
|
|
147
|
+
stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
|
|
148
|
+
file_lines.append((filename, stats))
|
|
149
|
+
max_filename_len = max(max_filename_len, len(filename))
|
|
150
|
+
else:
|
|
151
|
+
summary_lines.append(line)
|
|
152
|
+
|
|
153
|
+
# Display aligned file changes
|
|
154
|
+
for filename, stats in file_lines:
|
|
155
|
+
# Pad filename to align pipes
|
|
156
|
+
padded_filename = filename.ljust(max_filename_len)
|
|
157
|
+
|
|
158
|
+
# Replace + with green and - with red
|
|
159
|
+
stats = stats.replace('+', '[green]+[/green]')
|
|
160
|
+
stats = stats.replace('-', '[red]-[/red]')
|
|
161
|
+
|
|
162
|
+
ctx.textual.text(f" {padded_filename} | {stats}")
|
|
163
|
+
|
|
164
|
+
# Display summary lines
|
|
165
|
+
for line in summary_lines:
|
|
166
|
+
colored_line = line.replace('(+)', '[green](+)[/green]')
|
|
167
|
+
colored_line = colored_line.replace('(-)', '[red](-)[/red]')
|
|
168
|
+
ctx.textual.text(f" {colored_line}", markup="dim")
|
|
169
|
+
|
|
170
|
+
ctx.textual.text("") # spacing
|
|
171
|
+
|
|
172
|
+
ctx.textual.end_step("success")
|
|
173
|
+
return Success("Branch diff summary displayed")
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
# Don't fail the workflow, just skip
|
|
177
|
+
ctx.textual.text(f"Could not show branch diff summary: {e}", markup="yellow")
|
|
178
|
+
ctx.textual.end_step("skip")
|
|
179
|
+
return Skip(f"Could not show branch diff summary: {e}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
__all__ = ["show_uncommitted_diff_summary", "show_branch_diff_summary"]
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
3
3
|
from titan_plugin_git.exceptions import GitCommandError
|
|
4
4
|
from titan_plugin_git.messages import msg
|
|
5
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
6
5
|
|
|
7
6
|
def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
8
7
|
"""
|
|
@@ -12,6 +11,7 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
12
11
|
remote (str, optional): The name of the remote to push to. Defaults to the client's default remote.
|
|
13
12
|
branch (str, optional): The name of the branch to push. Defaults to the current branch.
|
|
14
13
|
set_upstream (bool, optional): Whether to set the upstream tracking branch. Defaults to False.
|
|
14
|
+
push_tags (bool, optional): Whether to push tags along with the branch. Defaults to False.
|
|
15
15
|
|
|
16
16
|
Requires:
|
|
17
17
|
ctx.git: An initialized GitClient.
|
|
@@ -29,10 +29,14 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
29
29
|
if not ctx.git:
|
|
30
30
|
return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
|
|
31
31
|
|
|
32
|
+
# Begin step container
|
|
33
|
+
ctx.textual.begin_step("Push changes to remote")
|
|
34
|
+
|
|
32
35
|
# Get params from context
|
|
33
36
|
remote = ctx.get('remote')
|
|
34
37
|
branch = ctx.get('branch')
|
|
35
38
|
set_upstream = ctx.get('set_upstream', False)
|
|
39
|
+
push_tags = ctx.get('push_tags', False)
|
|
36
40
|
|
|
37
41
|
# Use defaults from the GitClient if not provided in the context
|
|
38
42
|
remote_to_use = remote or ctx.git.default_remote
|
|
@@ -43,21 +47,33 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
43
47
|
if not ctx.git.branch_exists_on_remote(branch=branch_to_use, remote=remote_to_use):
|
|
44
48
|
set_upstream = True
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
panel_type="success"
|
|
53
|
-
)
|
|
50
|
+
# Push branch (and tags if requested)
|
|
51
|
+
ctx.git.push(
|
|
52
|
+
remote=remote_to_use,
|
|
53
|
+
branch=branch_to_use,
|
|
54
|
+
set_upstream=set_upstream,
|
|
55
|
+
tags=push_tags
|
|
54
56
|
)
|
|
55
57
|
|
|
58
|
+
# Show success message
|
|
59
|
+
success_msg = f"Pushed to {remote_to_use}/{branch_to_use}"
|
|
60
|
+
if push_tags:
|
|
61
|
+
success_msg += " (with tags)"
|
|
62
|
+
|
|
63
|
+
ctx.textual.text(success_msg, markup="green")
|
|
64
|
+
|
|
65
|
+
ctx.textual.end_step("success")
|
|
56
66
|
return Success(
|
|
57
67
|
message=msg.Git.PUSH_SUCCESS.format(remote=remote_to_use, branch=branch_to_use),
|
|
58
68
|
metadata={"pr_head_branch": branch_to_use}
|
|
59
69
|
)
|
|
60
70
|
except GitCommandError as e:
|
|
61
|
-
|
|
71
|
+
error_msg = msg.Steps.Push.PUSH_FAILED.format(e=e)
|
|
72
|
+
ctx.textual.text(error_msg, markup="red")
|
|
73
|
+
ctx.textual.end_step("error")
|
|
74
|
+
return Error(error_msg)
|
|
62
75
|
except Exception as e:
|
|
63
|
-
|
|
76
|
+
error_msg = msg.Git.UNEXPECTED_ERROR.format(e=e)
|
|
77
|
+
ctx.textual.text(error_msg, markup="red")
|
|
78
|
+
ctx.textual.end_step("error")
|
|
79
|
+
return Error(error_msg)
|
|
@@ -7,7 +7,6 @@ from titan_cli.engine import (
|
|
|
7
7
|
)
|
|
8
8
|
from titan_cli.messages import msg as global_msg
|
|
9
9
|
from ..messages import msg
|
|
10
|
-
from titan_cli.ui.tui.widgets import Panel
|
|
11
10
|
|
|
12
11
|
def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
13
12
|
"""
|
|
@@ -23,37 +22,35 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
23
22
|
Success: If the status was retrieved successfully.
|
|
24
23
|
Error: If the GitClient is not available or the git command fails.
|
|
25
24
|
"""
|
|
25
|
+
if not ctx.textual:
|
|
26
|
+
return Error("Textual UI context is not available for this step.")
|
|
27
|
+
|
|
26
28
|
if not ctx.git:
|
|
27
29
|
return Error(msg.Steps.Status.GIT_CLIENT_NOT_AVAILABLE)
|
|
28
30
|
|
|
31
|
+
# Begin step container
|
|
32
|
+
ctx.textual.begin_step("Check Git Status")
|
|
33
|
+
|
|
29
34
|
try:
|
|
30
35
|
status = ctx.git.get_status()
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
return Error("Textual UI context is not available for this step.")
|
|
34
|
-
|
|
35
|
-
# If there are uncommitted changes, show warning panel
|
|
37
|
+
# If there are uncommitted changes, show text (border will be green on success)
|
|
36
38
|
if not status.is_clean:
|
|
37
|
-
ctx.textual.
|
|
38
|
-
Panel(
|
|
39
|
-
text=global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING,
|
|
40
|
-
panel_type="warning"
|
|
41
|
-
)
|
|
42
|
-
)
|
|
39
|
+
ctx.textual.text(global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING, markup="yellow")
|
|
43
40
|
message = msg.Steps.Status.STATUS_RETRIEVED_WITH_UNCOMMITTED
|
|
44
41
|
else:
|
|
45
|
-
# Show
|
|
46
|
-
ctx.textual.
|
|
47
|
-
Panel(
|
|
48
|
-
text=msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN,
|
|
49
|
-
panel_type="success"
|
|
50
|
-
)
|
|
51
|
-
)
|
|
42
|
+
# Show text for clean working directory (border will be green)
|
|
43
|
+
ctx.textual.text(msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN, markup="green")
|
|
52
44
|
message = msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN
|
|
53
45
|
|
|
46
|
+
# End step container with success
|
|
47
|
+
ctx.textual.end_step("success")
|
|
48
|
+
|
|
54
49
|
return Success(
|
|
55
50
|
message=message,
|
|
56
51
|
metadata={"git_status": status}
|
|
57
52
|
)
|
|
58
53
|
except Exception as e:
|
|
54
|
+
# End step container with error
|
|
55
|
+
ctx.textual.end_step("error")
|
|
59
56
|
return Error(msg.Steps.Status.FAILED_TO_GET_STATUS.format(e=e))
|
|
@@ -420,8 +420,9 @@ COMMIT_MESSAGE: <conventional commit message>"""
|
|
|
420
420
|
```
|
|
421
421
|
|
|
422
422
|
## CRITICAL Instructions
|
|
423
|
-
1. **Title**: Follow conventional commits (type(scope):
|
|
424
|
-
-
|
|
423
|
+
1. **Title**: Follow conventional commits (type(scope): Description), be clear and descriptive
|
|
424
|
+
- Start description with CAPITAL letter (imperative mood)
|
|
425
|
+
- Examples: "feat(auth): Add OAuth2 integration with Google provider", "fix(api): Resolve race condition in cache invalidation"
|
|
425
426
|
|
|
426
427
|
2. **Description**: MUST follow the template structure above but keep it under {max_chars} characters total
|
|
427
428
|
- Fill in the template sections (Summary, Type of Change, Changes Made, etc.)
|
|
@@ -460,6 +461,18 @@ DESCRIPTION:
|
|
|
460
461
|
# Clean up title
|
|
461
462
|
title = title.strip('"').strip("'")
|
|
462
463
|
|
|
464
|
+
# Ensure title subject starts with capital letter (conventional commits requirement)
|
|
465
|
+
# Format: type(scope): Description
|
|
466
|
+
if ':' in title:
|
|
467
|
+
parts = title.split(':', 1)
|
|
468
|
+
if len(parts) == 2:
|
|
469
|
+
prefix = parts[0] # type(scope)
|
|
470
|
+
subject = parts[1].strip() # description
|
|
471
|
+
# Capitalize first letter of subject
|
|
472
|
+
if subject and subject[0].islower():
|
|
473
|
+
subject = subject[0].upper() + subject[1:]
|
|
474
|
+
title = f"{prefix}: {subject}"
|
|
475
|
+
|
|
463
476
|
# Truncate description if needed (but not title)
|
|
464
477
|
if len(description) > max_chars:
|
|
465
478
|
description = description[:max_chars - 3] + "..."
|