titan-cli 0.1.4__py3-none-any.whl → 0.1.6__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/workflows/__init__.py +2 -1
- titan_cli/core/workflows/project_step_source.py +95 -32
- titan_cli/core/workflows/workflow_filter_service.py +16 -8
- titan_cli/core/workflows/workflow_registry.py +12 -1
- titan_cli/core/workflows/workflow_sources.py +1 -1
- titan_cli/engine/__init__.py +5 -1
- titan_cli/engine/results.py +31 -1
- titan_cli/engine/steps/ai_assistant_step.py +47 -12
- titan_cli/engine/workflow_executor.py +13 -3
- titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
- titan_cli/ui/tui/screens/workflow_execution.py +28 -50
- titan_cli/ui/tui/screens/workflows.py +8 -4
- titan_cli/ui/tui/textual_components.py +342 -185
- titan_cli/ui/tui/textual_workflow_executor.py +39 -3
- titan_cli/ui/tui/theme.py +34 -5
- titan_cli/ui/tui/widgets/__init__.py +17 -0
- titan_cli/ui/tui/widgets/multiline_input.py +32 -0
- titan_cli/ui/tui/widgets/prompt_choice.py +138 -0
- titan_cli/ui/tui/widgets/prompt_input.py +74 -0
- titan_cli/ui/tui/widgets/prompt_selection_list.py +150 -0
- titan_cli/ui/tui/widgets/prompt_textarea.py +87 -0
- titan_cli/ui/tui/widgets/step_container.py +70 -0
- titan_cli/ui/tui/widgets/styled_option_list.py +107 -0
- titan_cli/ui/tui/widgets/text.py +51 -130
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/METADATA +3 -5
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/RECORD +61 -46
- titan_plugin_git/clients/git_client.py +140 -5
- titan_plugin_git/plugin.py +13 -0
- titan_plugin_git/steps/ai_commit_message_step.py +39 -34
- titan_plugin_git/steps/branch_steps.py +18 -37
- titan_plugin_git/steps/checkout_step.py +66 -0
- titan_plugin_git/steps/commit_step.py +18 -22
- titan_plugin_git/steps/create_branch_step.py +131 -0
- titan_plugin_git/steps/diff_summary_step.py +180 -0
- titan_plugin_git/steps/pull_step.py +70 -0
- titan_plugin_git/steps/push_step.py +27 -11
- titan_plugin_git/steps/restore_original_branch_step.py +97 -0
- titan_plugin_git/steps/save_current_branch_step.py +82 -0
- titan_plugin_git/steps/status_step.py +32 -25
- titan_plugin_git/workflows/commit-ai.yaml +9 -3
- titan_plugin_github/agents/pr_agent.py +15 -2
- titan_plugin_github/steps/ai_pr_step.py +99 -40
- titan_plugin_github/steps/create_pr_step.py +18 -8
- titan_plugin_github/steps/github_prompt_steps.py +53 -1
- titan_plugin_github/steps/issue_steps.py +31 -18
- titan_plugin_github/steps/preview_step.py +15 -4
- titan_plugin_github/utils.py +5 -4
- titan_plugin_github/workflows/create-pr-ai.yaml +6 -11
- titan_plugin_jira/messages.py +12 -0
- titan_plugin_jira/plugin.py +4 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +12 -7
- titan_plugin_jira/steps/get_issue_step.py +17 -13
- titan_plugin_jira/steps/list_versions_step.py +133 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +20 -8
- titan_plugin_jira/steps/search_jql_step.py +191 -0
- titan_plugin_jira/steps/search_saved_query_step.py +26 -24
- titan_plugin_jira/utils/__init__.py +1 -1
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/LICENSE +0 -0
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/WHEEL +0 -0
- {titan_cli-0.1.4.dist-info → titan_cli-0.1.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Create a new Git branch.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from titan_plugin_git.exceptions import GitError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
10
|
+
"""
|
|
11
|
+
Create a new Git branch.
|
|
12
|
+
|
|
13
|
+
Inputs (from ctx.data):
|
|
14
|
+
new_branch (str): Name of the branch to create
|
|
15
|
+
start_point (str, optional): Starting point for the branch (defaults to HEAD)
|
|
16
|
+
delete_if_exists (bool, optional): Delete the branch if it already exists (default: False)
|
|
17
|
+
checkout (bool, optional): Checkout the new branch after creation (default: True)
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Success: Branch created successfully
|
|
21
|
+
Error: Git operation failed
|
|
22
|
+
"""
|
|
23
|
+
if ctx.textual:
|
|
24
|
+
ctx.textual.begin_step("Create Branch")
|
|
25
|
+
|
|
26
|
+
if not ctx.textual:
|
|
27
|
+
return Error("Textual UI context is not available for this step.")
|
|
28
|
+
|
|
29
|
+
if not ctx.git:
|
|
30
|
+
ctx.textual.error_text("Git client not available in context")
|
|
31
|
+
ctx.textual.end_step("error")
|
|
32
|
+
return Error("Git client not available in context")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Get params from context
|
|
36
|
+
new_branch = ctx.get("new_branch")
|
|
37
|
+
if not new_branch:
|
|
38
|
+
ctx.textual.error_text("No branch name specified")
|
|
39
|
+
ctx.textual.dim_text("Set 'new_branch' in workflow params or previous step")
|
|
40
|
+
ctx.textual.end_step("error")
|
|
41
|
+
return Error("No branch name specified")
|
|
42
|
+
|
|
43
|
+
start_point = ctx.get("start_point", "HEAD")
|
|
44
|
+
delete_if_exists = ctx.get("delete_if_exists", False)
|
|
45
|
+
checkout = ctx.get("checkout", True)
|
|
46
|
+
|
|
47
|
+
ctx.textual.text("")
|
|
48
|
+
ctx.textual.dim_text(f"Creating branch: {new_branch}")
|
|
49
|
+
ctx.textual.dim_text(f"From: {start_point}")
|
|
50
|
+
|
|
51
|
+
# Check if branch exists
|
|
52
|
+
all_branches = ctx.git.get_branches()
|
|
53
|
+
branch_names = [b.name for b in all_branches]
|
|
54
|
+
branch_exists = new_branch in branch_names
|
|
55
|
+
|
|
56
|
+
# Delete if exists and requested
|
|
57
|
+
if branch_exists and delete_if_exists:
|
|
58
|
+
ctx.textual.text("")
|
|
59
|
+
ctx.textual.warning_text(f"Branch {new_branch} exists, deleting...")
|
|
60
|
+
|
|
61
|
+
# If we're on the branch, checkout another one first
|
|
62
|
+
current_branch = ctx.git.get_current_branch()
|
|
63
|
+
if current_branch == new_branch:
|
|
64
|
+
# Checkout main branch before deleting
|
|
65
|
+
main_branch = ctx.git.main_branch
|
|
66
|
+
if main_branch in branch_names and main_branch != new_branch:
|
|
67
|
+
try:
|
|
68
|
+
ctx.git.checkout(main_branch)
|
|
69
|
+
ctx.textual.dim_text(f"Switched to {main_branch}")
|
|
70
|
+
except GitError as e:
|
|
71
|
+
ctx.textual.error_text(f"Failed to checkout {main_branch}: {str(e)}")
|
|
72
|
+
ctx.textual.end_step("error")
|
|
73
|
+
return Error(f"Cannot checkout {main_branch}: {str(e)}")
|
|
74
|
+
else:
|
|
75
|
+
ctx.textual.error_text(f"Cannot delete current branch {new_branch}")
|
|
76
|
+
ctx.textual.end_step("error")
|
|
77
|
+
return Error("Cannot delete current branch")
|
|
78
|
+
|
|
79
|
+
# Delete the branch
|
|
80
|
+
try:
|
|
81
|
+
ctx.git.safe_delete_branch(new_branch, force=True)
|
|
82
|
+
ctx.textual.success_text(f"✓ Deleted existing branch {new_branch}")
|
|
83
|
+
except GitError as e:
|
|
84
|
+
ctx.textual.error_text(f"Failed to delete {new_branch}: {str(e)}")
|
|
85
|
+
ctx.textual.end_step("error")
|
|
86
|
+
return Error(f"Failed to delete branch: {str(e)}")
|
|
87
|
+
|
|
88
|
+
elif branch_exists:
|
|
89
|
+
ctx.textual.error_text(f"Branch {new_branch} already exists")
|
|
90
|
+
ctx.textual.dim_text("Set 'delete_if_exists: true' to recreate it")
|
|
91
|
+
ctx.textual.end_step("error")
|
|
92
|
+
return Error(f"Branch {new_branch} already exists")
|
|
93
|
+
|
|
94
|
+
# Create the branch
|
|
95
|
+
ctx.textual.text("")
|
|
96
|
+
try:
|
|
97
|
+
ctx.git.create_branch(new_branch, start_point=start_point)
|
|
98
|
+
ctx.textual.success_text(f"✓ Created branch {new_branch}")
|
|
99
|
+
except GitError as e:
|
|
100
|
+
ctx.textual.error_text(f"Failed to create {new_branch}: {str(e)}")
|
|
101
|
+
ctx.textual.end_step("error")
|
|
102
|
+
return Error(f"Failed to create branch: {str(e)}")
|
|
103
|
+
|
|
104
|
+
# Checkout if requested
|
|
105
|
+
if checkout:
|
|
106
|
+
try:
|
|
107
|
+
ctx.git.checkout(new_branch)
|
|
108
|
+
ctx.textual.success_text(f"✓ Checked out {new_branch}")
|
|
109
|
+
except GitError as e:
|
|
110
|
+
ctx.textual.warning_text(f"Branch created but failed to checkout: {str(e)}")
|
|
111
|
+
|
|
112
|
+
ctx.textual.text("")
|
|
113
|
+
ctx.textual.end_step("success")
|
|
114
|
+
return Success(
|
|
115
|
+
f"Created branch {new_branch}",
|
|
116
|
+
metadata={
|
|
117
|
+
"new_branch": new_branch,
|
|
118
|
+
"start_point": start_point,
|
|
119
|
+
"checked_out": checkout
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
ctx.textual.text("")
|
|
125
|
+
ctx.textual.error_text(f"Failed to create branch: {str(e)}")
|
|
126
|
+
ctx.textual.text("")
|
|
127
|
+
ctx.textual.end_step("error")
|
|
128
|
+
return Error(f"Failed to create branch: {str(e)}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = ["create_branch_step"]
|
|
@@ -0,0 +1,180 @@
|
|
|
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.get_uncommitted_diff_stat()
|
|
28
|
+
|
|
29
|
+
if not stat_output or not stat_output.strip():
|
|
30
|
+
ctx.textual.dim_text("No uncommitted changes to show")
|
|
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.bold_text("Changes summary:")
|
|
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.dim_text(f" {colored_line}")
|
|
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.error_text(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
|
|
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.dim_text("No head branch specified")
|
|
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.get_branch_diff_stat(base_branch, head_branch)
|
|
122
|
+
|
|
123
|
+
if not stat_output or not stat_output.strip():
|
|
124
|
+
ctx.textual.dim_text(f"No changes between {base_branch} and {head_branch}")
|
|
125
|
+
ctx.textual.end_step("success")
|
|
126
|
+
return Success("No changes")
|
|
127
|
+
|
|
128
|
+
# Show the stat summary with colors
|
|
129
|
+
ctx.textual.text("") # spacing
|
|
130
|
+
ctx.textual.bold_text(f"Changes in {head_branch} vs {base_branch}:")
|
|
131
|
+
ctx.textual.text("") # spacing
|
|
132
|
+
|
|
133
|
+
# Parse lines to find max filename length for alignment
|
|
134
|
+
file_lines = []
|
|
135
|
+
summary_lines = []
|
|
136
|
+
max_filename_len = 0
|
|
137
|
+
|
|
138
|
+
for line in stat_output.split('\n'):
|
|
139
|
+
if not line.strip():
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if '|' in line:
|
|
143
|
+
parts = line.split('|')
|
|
144
|
+
filename = parts[0].strip()
|
|
145
|
+
stats = '|'.join(parts[1:]) if len(parts) > 1 else ''
|
|
146
|
+
file_lines.append((filename, stats))
|
|
147
|
+
max_filename_len = max(max_filename_len, len(filename))
|
|
148
|
+
else:
|
|
149
|
+
summary_lines.append(line)
|
|
150
|
+
|
|
151
|
+
# Display aligned file changes
|
|
152
|
+
for filename, stats in file_lines:
|
|
153
|
+
# Pad filename to align pipes
|
|
154
|
+
padded_filename = filename.ljust(max_filename_len)
|
|
155
|
+
|
|
156
|
+
# Replace + with green and - with red
|
|
157
|
+
stats = stats.replace('+', '[green]+[/green]')
|
|
158
|
+
stats = stats.replace('-', '[red]-[/red]')
|
|
159
|
+
|
|
160
|
+
ctx.textual.text(f" {padded_filename} | {stats}")
|
|
161
|
+
|
|
162
|
+
# Display summary lines
|
|
163
|
+
for line in summary_lines:
|
|
164
|
+
colored_line = line.replace('(+)', '[green](+)[/green]')
|
|
165
|
+
colored_line = colored_line.replace('(-)', '[red](-)[/red]')
|
|
166
|
+
ctx.textual.dim_text(f" {colored_line}")
|
|
167
|
+
|
|
168
|
+
ctx.textual.text("") # spacing
|
|
169
|
+
|
|
170
|
+
ctx.textual.end_step("success")
|
|
171
|
+
return Success("Branch diff summary displayed")
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
# Don't fail the workflow, just skip
|
|
175
|
+
ctx.textual.warning_text(f"Could not show branch diff summary: {e}")
|
|
176
|
+
ctx.textual.end_step("skip")
|
|
177
|
+
return Skip(f"Could not show branch diff summary: {e}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["show_uncommitted_diff_summary", "show_branch_diff_summary"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pull from Git remote.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
7
|
+
from titan_plugin_git.exceptions import GitError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pull_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
11
|
+
"""
|
|
12
|
+
Pull from Git remote.
|
|
13
|
+
|
|
14
|
+
Inputs (from ctx.data):
|
|
15
|
+
remote (str, optional): Remote name (defaults to "origin")
|
|
16
|
+
branch (str, optional): Branch name (defaults to current branch)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Success: Pull completed successfully
|
|
20
|
+
Error: Git operation failed
|
|
21
|
+
"""
|
|
22
|
+
if ctx.textual:
|
|
23
|
+
ctx.textual.begin_step("Pull from Remote")
|
|
24
|
+
|
|
25
|
+
if not ctx.textual:
|
|
26
|
+
return Error("Textual UI context is not available for this step.")
|
|
27
|
+
|
|
28
|
+
if not ctx.git:
|
|
29
|
+
ctx.textual.error_text("Git client not available in context")
|
|
30
|
+
ctx.textual.end_step("error")
|
|
31
|
+
return Error("Git client not available in context")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# Get params from context (optional)
|
|
35
|
+
remote = ctx.get("remote", "origin")
|
|
36
|
+
branch: Optional[str] = ctx.get("pull_branch") # Optional, defaults to current
|
|
37
|
+
|
|
38
|
+
ctx.textual.text("")
|
|
39
|
+
if branch:
|
|
40
|
+
ctx.textual.dim_text(f"Pulling {remote}/{branch}...")
|
|
41
|
+
else:
|
|
42
|
+
ctx.textual.dim_text(f"Pulling from {remote}...")
|
|
43
|
+
|
|
44
|
+
# Pull
|
|
45
|
+
try:
|
|
46
|
+
with ctx.textual.loading("Pulling from remote..."):
|
|
47
|
+
ctx.git.pull(remote=remote, branch=branch)
|
|
48
|
+
ctx.textual.success_text(f"✓ Pulled from {remote}")
|
|
49
|
+
except GitError as e:
|
|
50
|
+
ctx.textual.text("")
|
|
51
|
+
ctx.textual.error_text(f"Failed to pull: {str(e)}")
|
|
52
|
+
ctx.textual.end_step("error")
|
|
53
|
+
return Error(f"Failed to pull: {str(e)}")
|
|
54
|
+
|
|
55
|
+
ctx.textual.text("")
|
|
56
|
+
ctx.textual.end_step("success")
|
|
57
|
+
return Success(
|
|
58
|
+
f"Pulled from {remote}",
|
|
59
|
+
metadata={"remote": remote, "branch": branch}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
ctx.textual.text("")
|
|
64
|
+
ctx.textual.error_text(f"Failed to pull: {str(e)}")
|
|
65
|
+
ctx.textual.text("")
|
|
66
|
+
ctx.textual.end_step("error")
|
|
67
|
+
return Error(f"Failed to pull: {str(e)}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["pull_step"]
|
|
@@ -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.success_text(success_msg)
|
|
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.error_text(error_msg)
|
|
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.error_text(error_msg)
|
|
78
|
+
ctx.textual.end_step("error")
|
|
79
|
+
return Error(error_msg)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Restore original branch and pop stashed changes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from titan_plugin_git.exceptions import GitError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def restore_original_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
10
|
+
"""
|
|
11
|
+
Restore original branch and pop stashed changes.
|
|
12
|
+
|
|
13
|
+
Returns the user to their original branch and restores any changes
|
|
14
|
+
that were stashed at the beginning of the workflow.
|
|
15
|
+
|
|
16
|
+
This step ALWAYS executes, even if the workflow failed.
|
|
17
|
+
|
|
18
|
+
Inputs (from ctx.data):
|
|
19
|
+
original_branch (str): Name of the branch to restore
|
|
20
|
+
has_stashed_changes (bool): Whether to pop stashed changes
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Success: Branch restored and changes popped if needed
|
|
24
|
+
Error: Git operation failed
|
|
25
|
+
"""
|
|
26
|
+
if ctx.textual:
|
|
27
|
+
ctx.textual.begin_step("Restore Original Branch")
|
|
28
|
+
|
|
29
|
+
if not ctx.textual:
|
|
30
|
+
return Error("Textual UI context is not available for this step.")
|
|
31
|
+
|
|
32
|
+
if not ctx.git:
|
|
33
|
+
ctx.textual.error_text("Git client not available in context")
|
|
34
|
+
ctx.textual.end_step("error")
|
|
35
|
+
return Error("Git client not available in context")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# Get original branch from context
|
|
39
|
+
original_branch = ctx.get("original_branch")
|
|
40
|
+
has_stashed_changes = ctx.get("has_stashed_changes", False)
|
|
41
|
+
|
|
42
|
+
if not original_branch:
|
|
43
|
+
ctx.textual.warning_text("No original branch to restore")
|
|
44
|
+
ctx.textual.end_step("success")
|
|
45
|
+
return Success("No branch to restore")
|
|
46
|
+
|
|
47
|
+
ctx.textual.text("")
|
|
48
|
+
ctx.textual.dim_text(f"Returning to: {original_branch}")
|
|
49
|
+
|
|
50
|
+
# Checkout original branch
|
|
51
|
+
try:
|
|
52
|
+
ctx.git.checkout(original_branch)
|
|
53
|
+
ctx.textual.success_text(f"✓ Checked out {original_branch}")
|
|
54
|
+
except GitError as e:
|
|
55
|
+
ctx.textual.error_text(f"Failed to checkout {original_branch}: {str(e)}")
|
|
56
|
+
ctx.textual.end_step("error")
|
|
57
|
+
return Error(f"Failed to checkout: {str(e)}")
|
|
58
|
+
|
|
59
|
+
# Pop stashed changes if any
|
|
60
|
+
if has_stashed_changes:
|
|
61
|
+
ctx.textual.text("")
|
|
62
|
+
ctx.textual.dim_text("Restoring stashed changes...")
|
|
63
|
+
|
|
64
|
+
with ctx.textual.loading("Popping stashed changes..."):
|
|
65
|
+
success = ctx.git.stash_pop()
|
|
66
|
+
|
|
67
|
+
if not success:
|
|
68
|
+
ctx.textual.warning_text("Failed to pop stash automatically")
|
|
69
|
+
ctx.textual.dim_text("Run: git stash pop")
|
|
70
|
+
# Don't fail the step, just warn
|
|
71
|
+
else:
|
|
72
|
+
ctx.textual.success_text("✓ Changes restored")
|
|
73
|
+
|
|
74
|
+
ctx.textual.text("")
|
|
75
|
+
ctx.textual.end_step("success")
|
|
76
|
+
return Success(
|
|
77
|
+
f"Restored to {original_branch}",
|
|
78
|
+
metadata={
|
|
79
|
+
"original_branch": original_branch,
|
|
80
|
+
"stash_popped": has_stashed_changes
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
ctx.textual.text("")
|
|
86
|
+
ctx.textual.error_text(f"Failed to restore original branch: {str(e)}")
|
|
87
|
+
ctx.textual.dim_text("You may need to manually restore your branch:")
|
|
88
|
+
if ctx.get("original_branch"):
|
|
89
|
+
ctx.textual.dim_text(f" git checkout {ctx.get('original_branch')}")
|
|
90
|
+
if ctx.get("has_stashed_changes"):
|
|
91
|
+
ctx.textual.dim_text(" git stash pop")
|
|
92
|
+
ctx.textual.text("")
|
|
93
|
+
ctx.textual.end_step("error")
|
|
94
|
+
return Error(f"Failed to restore: {str(e)}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["restore_original_branch_step"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Save current branch and stash uncommitted changes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def save_current_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
9
|
+
"""
|
|
10
|
+
Save current branch and stash uncommitted changes.
|
|
11
|
+
|
|
12
|
+
This allows the workflow to create release notes in a separate branch
|
|
13
|
+
without affecting the user's current work.
|
|
14
|
+
|
|
15
|
+
Outputs (saved to ctx.data):
|
|
16
|
+
original_branch (str): Name of the branch the user was on
|
|
17
|
+
has_stashed_changes (bool): Whether changes were stashed
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Success: Branch saved and changes stashed if needed
|
|
21
|
+
Error: Git operation failed
|
|
22
|
+
"""
|
|
23
|
+
if ctx.textual:
|
|
24
|
+
ctx.textual.begin_step("Save Current Branch")
|
|
25
|
+
|
|
26
|
+
if not ctx.textual:
|
|
27
|
+
return Error("Textual UI context is not available for this step.")
|
|
28
|
+
|
|
29
|
+
if not ctx.git:
|
|
30
|
+
ctx.textual.error_text("Git client not available in context")
|
|
31
|
+
ctx.textual.end_step("error")
|
|
32
|
+
return Error("Git client not available in context")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Get current branch
|
|
36
|
+
current_branch = ctx.git.get_current_branch()
|
|
37
|
+
ctx.set("original_branch", current_branch)
|
|
38
|
+
|
|
39
|
+
ctx.textual.text("")
|
|
40
|
+
ctx.textual.dim_text(f"Current branch: {current_branch}")
|
|
41
|
+
|
|
42
|
+
# Check for uncommitted changes
|
|
43
|
+
has_changes = ctx.git.has_uncommitted_changes()
|
|
44
|
+
|
|
45
|
+
if has_changes:
|
|
46
|
+
ctx.textual.dim_text("Uncommitted changes detected")
|
|
47
|
+
ctx.textual.text("")
|
|
48
|
+
|
|
49
|
+
# Stash changes
|
|
50
|
+
with ctx.textual.loading("Stashing uncommitted changes..."):
|
|
51
|
+
success = ctx.git.stash_push(message="titan-release-notes-workflow")
|
|
52
|
+
|
|
53
|
+
if not success:
|
|
54
|
+
ctx.textual.error_text("Failed to stash changes")
|
|
55
|
+
ctx.textual.end_step("error")
|
|
56
|
+
return Error("Failed to stash changes")
|
|
57
|
+
|
|
58
|
+
ctx.set("has_stashed_changes", True)
|
|
59
|
+
ctx.textual.success_text("✓ Changes stashed successfully")
|
|
60
|
+
else:
|
|
61
|
+
ctx.textual.dim_text("No uncommitted changes")
|
|
62
|
+
ctx.set("has_stashed_changes", False)
|
|
63
|
+
|
|
64
|
+
ctx.textual.text("")
|
|
65
|
+
ctx.textual.end_step("success")
|
|
66
|
+
return Success(
|
|
67
|
+
f"Saved branch: {current_branch}",
|
|
68
|
+
metadata={
|
|
69
|
+
"original_branch": current_branch,
|
|
70
|
+
"has_stashed_changes": has_changes
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
ctx.textual.text("")
|
|
76
|
+
ctx.textual.error_text(f"Failed to save current branch: {str(e)}")
|
|
77
|
+
ctx.textual.text("")
|
|
78
|
+
ctx.textual.end_step("error")
|
|
79
|
+
return Error(f"Failed to save current branch: {str(e)}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["save_current_branch_step"]
|