titan-cli 0.1.5__py3-none-any.whl → 0.1.7__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/workflows/project_step_source.py +52 -7
- titan_cli/core/workflows/workflow_filter_service.py +6 -4
- titan_cli/engine/__init__.py +5 -1
- titan_cli/engine/results.py +31 -1
- titan_cli/engine/steps/ai_assistant_step.py +18 -18
- titan_cli/engine/workflow_executor.py +7 -2
- titan_cli/ui/tui/screens/plugin_config_wizard.py +16 -0
- titan_cli/ui/tui/screens/workflow_execution.py +22 -24
- titan_cli/ui/tui/screens/workflows.py +8 -4
- titan_cli/ui/tui/textual_components.py +293 -189
- titan_cli/ui/tui/textual_workflow_executor.py +30 -2
- titan_cli/ui/tui/theme.py +34 -5
- titan_cli/ui/tui/widgets/__init__.py +15 -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/styled_option_list.py +107 -0
- titan_cli/ui/tui/widgets/text.py +51 -130
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/METADATA +5 -10
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/RECORD +54 -41
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/WHEEL +1 -1
- titan_plugin_git/clients/git_client.py +59 -2
- titan_plugin_git/plugin.py +10 -0
- titan_plugin_git/steps/ai_commit_message_step.py +8 -8
- titan_plugin_git/steps/branch_steps.py +6 -6
- titan_plugin_git/steps/checkout_step.py +66 -0
- titan_plugin_git/steps/commit_step.py +3 -3
- titan_plugin_git/steps/create_branch_step.py +131 -0
- titan_plugin_git/steps/diff_summary_step.py +11 -13
- titan_plugin_git/steps/pull_step.py +70 -0
- titan_plugin_git/steps/push_step.py +3 -3
- 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 +23 -13
- titan_plugin_git/workflows/commit-ai.yaml +4 -3
- titan_plugin_github/steps/ai_pr_step.py +90 -22
- titan_plugin_github/steps/create_pr_step.py +8 -8
- titan_plugin_github/steps/github_prompt_steps.py +13 -13
- titan_plugin_github/steps/issue_steps.py +14 -15
- titan_plugin_github/steps/preview_step.py +8 -8
- titan_plugin_github/workflows/create-pr-ai.yaml +1 -11
- titan_plugin_jira/messages.py +12 -0
- titan_plugin_jira/plugin.py +4 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +6 -6
- titan_plugin_jira/steps/get_issue_step.py +7 -7
- titan_plugin_jira/steps/list_versions_step.py +133 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +8 -9
- titan_plugin_jira/steps/search_jql_step.py +191 -0
- titan_plugin_jira/steps/search_saved_query_step.py +13 -13
- titan_plugin_jira/utils/__init__.py +1 -1
- {titan_cli-0.1.5.dist-info/licenses → titan_cli-0.1.7.dist-info}/LICENSE +0 -0
- {titan_cli-0.1.5.dist-info → titan_cli-0.1.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Checkout a 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 checkout_branch_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
10
|
+
"""
|
|
11
|
+
Checkout a Git branch.
|
|
12
|
+
|
|
13
|
+
Inputs (from ctx.data):
|
|
14
|
+
branch (str): Branch name to checkout
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Success: Branch checked out successfully
|
|
18
|
+
Error: Git operation failed
|
|
19
|
+
"""
|
|
20
|
+
if ctx.textual:
|
|
21
|
+
ctx.textual.begin_step("Checkout Branch")
|
|
22
|
+
|
|
23
|
+
if not ctx.textual:
|
|
24
|
+
return Error("Textual UI context is not available for this step.")
|
|
25
|
+
|
|
26
|
+
if not ctx.git:
|
|
27
|
+
ctx.textual.error_text("Git client not available in context")
|
|
28
|
+
ctx.textual.end_step("error")
|
|
29
|
+
return Error("Git client not available in context")
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Get branch from params, or use main_branch from git config
|
|
33
|
+
branch = ctx.get("branch")
|
|
34
|
+
if not branch:
|
|
35
|
+
branch = ctx.git.main_branch
|
|
36
|
+
ctx.textual.dim_text(f"Using main branch from config: {branch}")
|
|
37
|
+
|
|
38
|
+
ctx.textual.text("")
|
|
39
|
+
ctx.textual.dim_text(f"Checking out: {branch}")
|
|
40
|
+
|
|
41
|
+
# Checkout branch
|
|
42
|
+
try:
|
|
43
|
+
ctx.git.checkout(branch)
|
|
44
|
+
ctx.textual.success_text(f"✓ Checked out {branch}")
|
|
45
|
+
except GitError as e:
|
|
46
|
+
ctx.textual.text("")
|
|
47
|
+
ctx.textual.error_text(f"Failed to checkout {branch}: {str(e)}")
|
|
48
|
+
ctx.textual.end_step("error")
|
|
49
|
+
return Error(f"Failed to checkout: {str(e)}")
|
|
50
|
+
|
|
51
|
+
ctx.textual.text("")
|
|
52
|
+
ctx.textual.end_step("success")
|
|
53
|
+
return Success(
|
|
54
|
+
f"Checked out {branch}",
|
|
55
|
+
metadata={"branch": branch}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
ctx.textual.text("")
|
|
60
|
+
ctx.textual.error_text(f"Failed to checkout branch: {str(e)}")
|
|
61
|
+
ctx.textual.text("")
|
|
62
|
+
ctx.textual.end_step("error")
|
|
63
|
+
return Error(f"Failed to checkout: {str(e)}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["checkout_branch_step"]
|
|
@@ -37,7 +37,7 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
37
37
|
# Skip if there's nothing to commit
|
|
38
38
|
git_status = ctx.data.get("git_status")
|
|
39
39
|
if git_status and git_status.is_clean:
|
|
40
|
-
ctx.textual.
|
|
40
|
+
ctx.textual.dim_text(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN)
|
|
41
41
|
ctx.textual.end_step("skip")
|
|
42
42
|
return Skip(msg.Steps.Commit.WORKING_DIRECTORY_CLEAN)
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
47
47
|
|
|
48
48
|
commit_message = ctx.get('commit_message')
|
|
49
49
|
if not commit_message:
|
|
50
|
-
ctx.textual.
|
|
50
|
+
ctx.textual.dim_text(msg.Steps.Commit.NO_COMMIT_MESSAGE)
|
|
51
51
|
ctx.textual.end_step("skip")
|
|
52
52
|
return Skip(msg.Steps.Commit.NO_COMMIT_MESSAGE)
|
|
53
53
|
|
|
@@ -58,7 +58,7 @@ def create_git_commit_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
58
58
|
commit_hash = ctx.git.commit(message=commit_message, all=all_files, no_verify=no_verify)
|
|
59
59
|
|
|
60
60
|
# Show success message
|
|
61
|
-
ctx.textual.
|
|
61
|
+
ctx.textual.success_text(f"Commit created: {commit_hash[:7]}")
|
|
62
62
|
|
|
63
63
|
ctx.textual.end_step("success")
|
|
64
64
|
return Success(
|
|
@@ -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"]
|
|
@@ -24,16 +24,16 @@ def show_uncommitted_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
24
24
|
|
|
25
25
|
try:
|
|
26
26
|
# Get diff stat for uncommitted changes
|
|
27
|
-
stat_output = ctx.git.
|
|
27
|
+
stat_output = ctx.git.get_uncommitted_diff_stat()
|
|
28
28
|
|
|
29
29
|
if not stat_output or not stat_output.strip():
|
|
30
|
-
ctx.textual.
|
|
30
|
+
ctx.textual.dim_text("No uncommitted changes to show")
|
|
31
31
|
ctx.textual.end_step("success")
|
|
32
32
|
return Success("No changes")
|
|
33
33
|
|
|
34
34
|
# Show the stat summary with colors
|
|
35
35
|
ctx.textual.text("") # spacing
|
|
36
|
-
ctx.textual.
|
|
36
|
+
ctx.textual.bold_text("Changes summary:")
|
|
37
37
|
ctx.textual.text("") # spacing
|
|
38
38
|
|
|
39
39
|
# Parse lines to find max filename length for alignment
|
|
@@ -69,7 +69,7 @@ def show_uncommitted_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
69
69
|
for line in summary_lines:
|
|
70
70
|
colored_line = line.replace('(+)', '[green](+)[/green]')
|
|
71
71
|
colored_line = colored_line.replace('(-)', '[red](-)[/red]')
|
|
72
|
-
ctx.textual.
|
|
72
|
+
ctx.textual.dim_text(f" {colored_line}")
|
|
73
73
|
|
|
74
74
|
ctx.textual.text("") # spacing
|
|
75
75
|
|
|
@@ -104,13 +104,13 @@ def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
104
104
|
ctx.textual.begin_step("Show Branch Changes Summary")
|
|
105
105
|
|
|
106
106
|
if not ctx.git:
|
|
107
|
-
ctx.textual.
|
|
107
|
+
ctx.textual.error_text(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
|
|
108
108
|
ctx.textual.end_step("error")
|
|
109
109
|
return Error(msg.Steps.Push.GIT_CLIENT_NOT_AVAILABLE)
|
|
110
110
|
|
|
111
111
|
head_branch = ctx.get("pr_head_branch")
|
|
112
112
|
if not head_branch:
|
|
113
|
-
ctx.textual.
|
|
113
|
+
ctx.textual.dim_text("No head branch specified")
|
|
114
114
|
ctx.textual.end_step("skip")
|
|
115
115
|
return Skip("No head branch specified")
|
|
116
116
|
|
|
@@ -118,18 +118,16 @@ def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
118
118
|
|
|
119
119
|
try:
|
|
120
120
|
# Get diff stat between branches
|
|
121
|
-
stat_output = ctx.git.
|
|
122
|
-
"git", "diff", "--stat", f"{base_branch}...{head_branch}"
|
|
123
|
-
])
|
|
121
|
+
stat_output = ctx.git.get_branch_diff_stat(base_branch, head_branch)
|
|
124
122
|
|
|
125
123
|
if not stat_output or not stat_output.strip():
|
|
126
|
-
ctx.textual.
|
|
124
|
+
ctx.textual.dim_text(f"No changes between {base_branch} and {head_branch}")
|
|
127
125
|
ctx.textual.end_step("success")
|
|
128
126
|
return Success("No changes")
|
|
129
127
|
|
|
130
128
|
# Show the stat summary with colors
|
|
131
129
|
ctx.textual.text("") # spacing
|
|
132
|
-
ctx.textual.
|
|
130
|
+
ctx.textual.bold_text(f"Changes in {head_branch} vs {base_branch}:")
|
|
133
131
|
ctx.textual.text("") # spacing
|
|
134
132
|
|
|
135
133
|
# Parse lines to find max filename length for alignment
|
|
@@ -165,7 +163,7 @@ def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
165
163
|
for line in summary_lines:
|
|
166
164
|
colored_line = line.replace('(+)', '[green](+)[/green]')
|
|
167
165
|
colored_line = colored_line.replace('(-)', '[red](-)[/red]')
|
|
168
|
-
ctx.textual.
|
|
166
|
+
ctx.textual.dim_text(f" {colored_line}")
|
|
169
167
|
|
|
170
168
|
ctx.textual.text("") # spacing
|
|
171
169
|
|
|
@@ -174,7 +172,7 @@ def show_branch_diff_summary(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
174
172
|
|
|
175
173
|
except Exception as e:
|
|
176
174
|
# Don't fail the workflow, just skip
|
|
177
|
-
ctx.textual.
|
|
175
|
+
ctx.textual.warning_text(f"Could not show branch diff summary: {e}")
|
|
178
176
|
ctx.textual.end_step("skip")
|
|
179
177
|
return Skip(f"Could not show branch diff summary: {e}")
|
|
180
178
|
|
|
@@ -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"]
|
|
@@ -60,7 +60,7 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
60
60
|
if push_tags:
|
|
61
61
|
success_msg += " (with tags)"
|
|
62
62
|
|
|
63
|
-
ctx.textual.
|
|
63
|
+
ctx.textual.success_text(success_msg)
|
|
64
64
|
|
|
65
65
|
ctx.textual.end_step("success")
|
|
66
66
|
return Success(
|
|
@@ -69,11 +69,11 @@ def create_git_push_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
69
69
|
)
|
|
70
70
|
except GitCommandError as e:
|
|
71
71
|
error_msg = msg.Steps.Push.PUSH_FAILED.format(e=e)
|
|
72
|
-
ctx.textual.
|
|
72
|
+
ctx.textual.error_text(error_msg)
|
|
73
73
|
ctx.textual.end_step("error")
|
|
74
74
|
return Error(error_msg)
|
|
75
75
|
except Exception as e:
|
|
76
76
|
error_msg = msg.Git.UNEXPECTED_ERROR.format(e=e)
|
|
77
|
-
ctx.textual.
|
|
77
|
+
ctx.textual.error_text(error_msg)
|
|
78
78
|
ctx.textual.end_step("error")
|
|
79
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"]
|
|
@@ -3,7 +3,8 @@ from titan_cli.engine import (
|
|
|
3
3
|
WorkflowContext,
|
|
4
4
|
WorkflowResult,
|
|
5
5
|
Success,
|
|
6
|
-
Error
|
|
6
|
+
Error,
|
|
7
|
+
Exit
|
|
7
8
|
)
|
|
8
9
|
from titan_cli.messages import msg as global_msg
|
|
9
10
|
from ..messages import msg
|
|
@@ -12,6 +13,10 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
12
13
|
"""
|
|
13
14
|
Retrieves the current git status and saves it to the context.
|
|
14
15
|
|
|
16
|
+
Behavior:
|
|
17
|
+
- If there are uncommitted changes: Returns Success and continues workflow
|
|
18
|
+
- If working directory is clean: Returns Exit (stops workflow - nothing to commit)
|
|
19
|
+
|
|
15
20
|
Requires:
|
|
16
21
|
ctx.git: An initialized GitClient.
|
|
17
22
|
|
|
@@ -19,7 +24,8 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
19
24
|
git_status (GitStatus): The full git status object, which includes the `is_clean` flag.
|
|
20
25
|
|
|
21
26
|
Returns:
|
|
22
|
-
Success: If
|
|
27
|
+
Success: If there are changes to commit (workflow continues)
|
|
28
|
+
Exit: If working directory is clean (workflow stops - nothing to commit)
|
|
23
29
|
Error: If the GitClient is not available or the git command fails.
|
|
24
30
|
"""
|
|
25
31
|
if not ctx.textual:
|
|
@@ -34,22 +40,26 @@ def get_git_status_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
|
34
40
|
try:
|
|
35
41
|
status = ctx.git.get_status()
|
|
36
42
|
|
|
37
|
-
# If there are uncommitted changes,
|
|
43
|
+
# If there are uncommitted changes, continue with workflow
|
|
38
44
|
if not status.is_clean:
|
|
39
|
-
ctx.textual.
|
|
45
|
+
ctx.textual.warning_text(global_msg.Workflow.UNCOMMITTED_CHANGES_WARNING)
|
|
40
46
|
message = msg.Steps.Status.STATUS_RETRIEVED_WITH_UNCOMMITTED
|
|
47
|
+
ctx.textual.end_step("success")
|
|
48
|
+
|
|
49
|
+
return Success(
|
|
50
|
+
message=message,
|
|
51
|
+
metadata={"git_status": status}
|
|
52
|
+
)
|
|
41
53
|
else:
|
|
42
|
-
#
|
|
43
|
-
ctx.textual.
|
|
44
|
-
|
|
54
|
+
# Working directory is clean - exit workflow (nothing to commit)
|
|
55
|
+
ctx.textual.success_text(msg.Steps.Status.WORKING_DIRECTORY_IS_CLEAN)
|
|
56
|
+
ctx.textual.text("")
|
|
57
|
+
ctx.textual.dim_text("Nothing to commit. Skipping workflow.")
|
|
58
|
+
ctx.textual.end_step("success")
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
# Exit workflow early (not an error)
|
|
61
|
+
return Exit("No changes to commit", metadata={"git_status": status})
|
|
48
62
|
|
|
49
|
-
return Success(
|
|
50
|
-
message=message,
|
|
51
|
-
metadata={"git_status": status}
|
|
52
|
-
)
|
|
53
63
|
except Exception as e:
|
|
54
64
|
# End step container with error
|
|
55
65
|
ctx.textual.end_step("error")
|
|
@@ -5,13 +5,14 @@ hooks:
|
|
|
5
5
|
- before_commit # Hook for linting/testing
|
|
6
6
|
|
|
7
7
|
steps:
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Check for changes FIRST - exits workflow if nothing to commit
|
|
10
9
|
- id: git_status
|
|
11
10
|
name: "Check Git Status"
|
|
12
11
|
plugin: git
|
|
13
12
|
step: get_status
|
|
14
13
|
|
|
14
|
+
- hook: before_commit # This is where injected steps will run
|
|
15
|
+
|
|
15
16
|
- id: diff_summary
|
|
16
17
|
name: "Show Changes Summary"
|
|
17
18
|
plugin: git
|
|
@@ -26,7 +27,7 @@ steps:
|
|
|
26
27
|
name: "Create Commit"
|
|
27
28
|
plugin: git
|
|
28
29
|
step: create_commit
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
- id: push
|
|
31
32
|
name: "Push changes to remote"
|
|
32
33
|
plugin: git
|