panqake 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panqake/__init__.py +3 -0
- panqake/__main__.py +6 -0
- panqake/cli.py +112 -0
- panqake/commands/__init__.py +1 -0
- panqake/commands/delete.py +153 -0
- panqake/commands/list.py +74 -0
- panqake/commands/new.py +56 -0
- panqake/commands/pr.py +173 -0
- panqake/commands/switch.py +83 -0
- panqake/commands/update.py +122 -0
- panqake/utils/__init__.py +1 -0
- panqake/utils/config.py +102 -0
- panqake/utils/git.py +71 -0
- panqake/utils/prompt.py +86 -0
- panqake/utils/questionary_prompt.py +173 -0
- panqake-0.1.0.dist-info/METADATA +125 -0
- panqake-0.1.0.dist-info/RECORD +19 -0
- panqake-0.1.0.dist-info/WHEEL +4 -0
- panqake-0.1.0.dist-info/entry_points.txt +3 -0
panqake/__init__.py
ADDED
panqake/__main__.py
ADDED
panqake/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Panqake - Git Branch Stacking Utility
|
|
4
|
+
A Python implementation of git-stacking workflow management
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from panqake.commands.delete import delete_branch
|
|
11
|
+
from panqake.commands.list import list_branches
|
|
12
|
+
from panqake.commands.new import create_new_branch
|
|
13
|
+
from panqake.commands.pr import create_pull_requests
|
|
14
|
+
from panqake.commands.switch import switch_branch
|
|
15
|
+
from panqake.commands.update import update_branches
|
|
16
|
+
from panqake.utils.config import init_panqake
|
|
17
|
+
from panqake.utils.git import is_git_repo
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
"""Main entry point for the panqake CLI."""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="Panqake - Git Branch Stacking Utility"
|
|
24
|
+
)
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
26
|
+
|
|
27
|
+
# new command
|
|
28
|
+
new_parser = subparsers.add_parser("new", help="Create a new branch in the stack")
|
|
29
|
+
new_parser.add_argument(
|
|
30
|
+
"branch_name",
|
|
31
|
+
nargs="?",
|
|
32
|
+
help="Name of the new branch to create",
|
|
33
|
+
)
|
|
34
|
+
new_parser.add_argument(
|
|
35
|
+
"base_branch",
|
|
36
|
+
nargs="?",
|
|
37
|
+
help="Optional base branch (defaults to current branch)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# list command
|
|
41
|
+
list_parser = subparsers.add_parser("list", help="List the branch stack")
|
|
42
|
+
list_parser.add_argument(
|
|
43
|
+
"branch_name",
|
|
44
|
+
nargs="?",
|
|
45
|
+
help="Optional branch to start from (defaults to current branch)",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# update command
|
|
49
|
+
update_parser = subparsers.add_parser(
|
|
50
|
+
"update", help="Update branches after changes"
|
|
51
|
+
)
|
|
52
|
+
update_parser.add_argument(
|
|
53
|
+
"branch_name",
|
|
54
|
+
nargs="?",
|
|
55
|
+
help="Optional branch to start from (defaults to current branch)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# delete command
|
|
59
|
+
delete_parser = subparsers.add_parser(
|
|
60
|
+
"delete", help="Delete a branch and relink the stack"
|
|
61
|
+
)
|
|
62
|
+
delete_parser.add_argument("branch_name", help="Name of the branch to delete")
|
|
63
|
+
|
|
64
|
+
# pr command
|
|
65
|
+
pr_parser = subparsers.add_parser("pr", help="Create PRs for the branch stack")
|
|
66
|
+
pr_parser.add_argument(
|
|
67
|
+
"branch_name",
|
|
68
|
+
nargs="?",
|
|
69
|
+
help="Optional branch to start from (defaults to current branch)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# switch command
|
|
73
|
+
switch_parser = subparsers.add_parser(
|
|
74
|
+
"switch", help="Interactively switch between branches"
|
|
75
|
+
)
|
|
76
|
+
switch_parser.add_argument(
|
|
77
|
+
"branch_name",
|
|
78
|
+
nargs="?",
|
|
79
|
+
help="Optional branch to switch to (defaults to interactive selection)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
args = parser.parse_args()
|
|
83
|
+
|
|
84
|
+
if not args.command:
|
|
85
|
+
parser.print_help()
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Initialize panqake directory and files
|
|
89
|
+
init_panqake()
|
|
90
|
+
|
|
91
|
+
# Check if we're in a git repository
|
|
92
|
+
if not is_git_repo():
|
|
93
|
+
print("Error: Not in a git repository")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
# Execute the appropriate command
|
|
97
|
+
if args.command == "new":
|
|
98
|
+
create_new_branch(args.branch_name, args.base_branch)
|
|
99
|
+
elif args.command == "list":
|
|
100
|
+
list_branches(args.branch_name)
|
|
101
|
+
elif args.command == "update":
|
|
102
|
+
update_branches(args.branch_name)
|
|
103
|
+
elif args.command == "delete":
|
|
104
|
+
delete_branch(args.branch_name)
|
|
105
|
+
elif args.command == "pr":
|
|
106
|
+
create_pull_requests(args.branch_name)
|
|
107
|
+
elif args.command == "switch":
|
|
108
|
+
switch_branch(args.branch_name)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command modules for panqake git-stacking utility."""
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Command for deleting a branch and relinking the stack."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from panqake.utils.config import (
|
|
6
|
+
add_to_stack,
|
|
7
|
+
get_child_branches,
|
|
8
|
+
get_parent_branch,
|
|
9
|
+
remove_from_stack,
|
|
10
|
+
)
|
|
11
|
+
from panqake.utils.git import branch_exists, get_current_branch, run_git_command
|
|
12
|
+
from panqake.utils.questionary_prompt import (
|
|
13
|
+
format_branch,
|
|
14
|
+
print_formatted_text,
|
|
15
|
+
prompt_confirm,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_branch_for_deletion(branch_name):
|
|
20
|
+
"""Validate that a branch can be deleted."""
|
|
21
|
+
current_branch = get_current_branch()
|
|
22
|
+
|
|
23
|
+
# Check if target branch exists
|
|
24
|
+
if not branch_exists(branch_name):
|
|
25
|
+
print_formatted_text(
|
|
26
|
+
f"<warning>Error: Branch '{branch_name}' does not exist</warning>"
|
|
27
|
+
)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
# Check if target branch is the current branch
|
|
31
|
+
if branch_name == current_branch:
|
|
32
|
+
print_formatted_text(
|
|
33
|
+
"<warning>Error: Cannot delete the current branch. Please checkout another branch first.</warning>"
|
|
34
|
+
)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
return current_branch
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_branch_relationships(branch_name):
|
|
41
|
+
"""Get parent and child branches and validate parent exists."""
|
|
42
|
+
parent_branch = get_parent_branch(branch_name)
|
|
43
|
+
child_branches = get_child_branches(branch_name)
|
|
44
|
+
|
|
45
|
+
# Ensure parent branch exists
|
|
46
|
+
if parent_branch and not branch_exists(parent_branch):
|
|
47
|
+
print_formatted_text(
|
|
48
|
+
f"<warning>Error: Parent branch '{parent_branch}' does not exist</warning>"
|
|
49
|
+
)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
return parent_branch, child_branches
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def display_deletion_info(branch_name, parent_branch, child_branches):
|
|
56
|
+
"""Display deletion information and ask for confirmation."""
|
|
57
|
+
print_formatted_text(
|
|
58
|
+
f"<info>Branch to delete:</info> {format_branch(branch_name, danger=True)}"
|
|
59
|
+
)
|
|
60
|
+
if parent_branch:
|
|
61
|
+
print_formatted_text(
|
|
62
|
+
f"<info>Parent branch:</info> {format_branch(parent_branch)}"
|
|
63
|
+
)
|
|
64
|
+
if child_branches:
|
|
65
|
+
print_formatted_text("<info>Child branches that will be relinked:</info>")
|
|
66
|
+
for child in child_branches:
|
|
67
|
+
print_formatted_text(f" {format_branch(child)}")
|
|
68
|
+
|
|
69
|
+
# Confirm deletion
|
|
70
|
+
if not prompt_confirm("Are you sure you want to delete this branch?"):
|
|
71
|
+
print_formatted_text("<info>Branch deletion cancelled.</info>")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def relink_child_branches(child_branches, parent_branch, current_branch, branch_name):
|
|
78
|
+
"""Relink child branches to the parent branch."""
|
|
79
|
+
if not child_branches:
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
print_formatted_text(
|
|
83
|
+
f"<info>Relinking child branches to parent '{parent_branch}'...</info>"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
for child in child_branches:
|
|
87
|
+
print_formatted_text(
|
|
88
|
+
f"<info>Processing child branch:</info> {format_branch(child)}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Checkout the child branch
|
|
92
|
+
checkout_result = run_git_command(["checkout", child])
|
|
93
|
+
if checkout_result is None:
|
|
94
|
+
print_formatted_text(
|
|
95
|
+
f"<warning>Error: Failed to checkout branch '{child}'</warning>"
|
|
96
|
+
)
|
|
97
|
+
run_git_command(["checkout", current_branch])
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
# Rebase onto the grandparent branch
|
|
101
|
+
if parent_branch:
|
|
102
|
+
rebase_result = run_git_command(["rebase", parent_branch])
|
|
103
|
+
if rebase_result is None:
|
|
104
|
+
print_formatted_text(
|
|
105
|
+
f"<warning>Error: Rebase conflict detected in branch '{child}'</warning>"
|
|
106
|
+
)
|
|
107
|
+
print_formatted_text(
|
|
108
|
+
"<warning>Please resolve conflicts and run 'git rebase --continue'</warning>"
|
|
109
|
+
)
|
|
110
|
+
print_formatted_text(
|
|
111
|
+
f"<warning>Then run 'panqake delete {branch_name}' again to retry</warning>"
|
|
112
|
+
)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
# Update stack metadata
|
|
116
|
+
add_to_stack(child, parent_branch)
|
|
117
|
+
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def delete_branch(branch_name):
|
|
122
|
+
"""Delete a branch and relink the stack."""
|
|
123
|
+
current_branch = validate_branch_for_deletion(branch_name)
|
|
124
|
+
parent_branch, child_branches = get_branch_relationships(branch_name)
|
|
125
|
+
|
|
126
|
+
if not display_deletion_info(branch_name, parent_branch, child_branches):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
print_formatted_text(
|
|
130
|
+
f"<info>Deleting branch '{branch_name}' from the stack...</info>"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Process child branches
|
|
134
|
+
relink_child_branches(child_branches, parent_branch, current_branch, branch_name)
|
|
135
|
+
|
|
136
|
+
# Return to original branch if it's not the one being deleted
|
|
137
|
+
if branch_name != current_branch:
|
|
138
|
+
run_git_command(["checkout", current_branch])
|
|
139
|
+
|
|
140
|
+
# Delete the branch
|
|
141
|
+
delete_result = run_git_command(["branch", "-D", branch_name])
|
|
142
|
+
if delete_result is None:
|
|
143
|
+
print_formatted_text(
|
|
144
|
+
f"<warning>Error: Failed to delete branch '{branch_name}'</warning>"
|
|
145
|
+
)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
# Remove from stack metadata
|
|
149
|
+
remove_from_stack(branch_name)
|
|
150
|
+
|
|
151
|
+
print_formatted_text(
|
|
152
|
+
f"<success>Success! Deleted branch '{branch_name}' and relinked the stack</success>"
|
|
153
|
+
)
|
panqake/commands/list.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Command for listing branches in the stack."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from panqake.utils.config import get_child_branches, get_parent_branch
|
|
6
|
+
from panqake.utils.git import branch_exists, get_current_branch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_stack_root(branch):
|
|
10
|
+
"""Find the root of the stack for a given branch."""
|
|
11
|
+
parent = get_parent_branch(branch)
|
|
12
|
+
|
|
13
|
+
if not parent:
|
|
14
|
+
return branch
|
|
15
|
+
else:
|
|
16
|
+
return find_stack_root(parent)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def print_branch_tree(branch, indent="", is_last_sibling=True):
|
|
20
|
+
"""Recursively print the branch tree."""
|
|
21
|
+
current_branch = get_current_branch()
|
|
22
|
+
is_current = branch == current_branch
|
|
23
|
+
|
|
24
|
+
# Determine the connector for the current branch
|
|
25
|
+
if indent: # Not the root
|
|
26
|
+
connector = "└── " if is_last_sibling else "├── "
|
|
27
|
+
else: # Root branch
|
|
28
|
+
connector = ""
|
|
29
|
+
|
|
30
|
+
# Format and print the current branch line
|
|
31
|
+
prefix = f"{indent}{connector}"
|
|
32
|
+
if is_current:
|
|
33
|
+
# Apply branch styling using ANSI codes
|
|
34
|
+
branch_display = f"* {branch}"
|
|
35
|
+
# Print prefix first, then styled branch with ANSI codes
|
|
36
|
+
print(f"{prefix}\033[92m{branch_display}\033[0m")
|
|
37
|
+
else:
|
|
38
|
+
# Non-current branches use default terminal text color
|
|
39
|
+
print(f"{prefix}{branch}")
|
|
40
|
+
|
|
41
|
+
# Prepare the indentation for children
|
|
42
|
+
# Add a vertical bar if this branch is not the last sibling, otherwise add spaces
|
|
43
|
+
child_indent = indent + (" " if is_last_sibling else "│ ")
|
|
44
|
+
|
|
45
|
+
# Get children of this branch
|
|
46
|
+
children = get_child_branches(branch)
|
|
47
|
+
num_children = len(children)
|
|
48
|
+
|
|
49
|
+
if children:
|
|
50
|
+
for i, child in enumerate(children):
|
|
51
|
+
is_last_child = i == num_children - 1
|
|
52
|
+
print_branch_tree(child, child_indent, is_last_child)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_branches(branch_name=None):
|
|
56
|
+
"""List the branch stack."""
|
|
57
|
+
# If no branch specified, use current branch
|
|
58
|
+
if not branch_name:
|
|
59
|
+
branch_name = get_current_branch()
|
|
60
|
+
|
|
61
|
+
# Check if target branch exists
|
|
62
|
+
if not branch_exists(branch_name):
|
|
63
|
+
# Use standard print with ANSI codes for warning style (yellow)
|
|
64
|
+
print(f"\033[93mError: Branch '{branch_name}' does not exist\033[0m")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
# Find the root of the stack for the target branch
|
|
68
|
+
root_branch = find_stack_root(branch_name)
|
|
69
|
+
|
|
70
|
+
# Use standard print with ANSI codes for info style (cyan)
|
|
71
|
+
print(f"\033[96mBranch stack (current: {get_current_branch()})\033[0m")
|
|
72
|
+
|
|
73
|
+
# Initial call starts with no indent and assumes the root is the 'last sibling' conceptually
|
|
74
|
+
print_branch_tree(root_branch, indent="", is_last_sibling=True)
|
panqake/commands/new.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Command for creating a new branch in the stack."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from panqake.utils.config import add_to_stack
|
|
6
|
+
from panqake.utils.git import (
|
|
7
|
+
branch_exists,
|
|
8
|
+
get_current_branch,
|
|
9
|
+
list_all_branches,
|
|
10
|
+
run_git_command,
|
|
11
|
+
)
|
|
12
|
+
from panqake.utils.questionary_prompt import BranchNameValidator, prompt_input
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_new_branch(branch_name=None, base_branch=None):
|
|
16
|
+
"""Create a new branch in the stack."""
|
|
17
|
+
# If no branch name specified, prompt for it
|
|
18
|
+
if not branch_name:
|
|
19
|
+
validator = BranchNameValidator()
|
|
20
|
+
branch_name = prompt_input("Enter new branch name: ", validator=validator)
|
|
21
|
+
|
|
22
|
+
# If no base branch specified, use current branch but offer selection
|
|
23
|
+
current = get_current_branch()
|
|
24
|
+
if not base_branch:
|
|
25
|
+
base_branch = current
|
|
26
|
+
branches = list_all_branches()
|
|
27
|
+
if branches:
|
|
28
|
+
base_branch = prompt_input(
|
|
29
|
+
f"Enter base branch [default: {current}]: ",
|
|
30
|
+
completer=branches,
|
|
31
|
+
default=current,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Check if the new branch already exists
|
|
35
|
+
if branch_exists(branch_name):
|
|
36
|
+
print(f"Error: Branch '{branch_name}' already exists")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
# Check if the base branch exists
|
|
40
|
+
if base_branch and not branch_exists(base_branch):
|
|
41
|
+
print(f"Error: Base branch '{base_branch}' does not exist")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
print(f"Creating new branch '{branch_name}' based on '{base_branch}'...")
|
|
45
|
+
|
|
46
|
+
# Create the new branch
|
|
47
|
+
result = run_git_command(["checkout", "-b", branch_name, base_branch])
|
|
48
|
+
if result is None:
|
|
49
|
+
print("Error: Failed to create new branch")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
# Record the dependency information
|
|
53
|
+
add_to_stack(branch_name, base_branch)
|
|
54
|
+
|
|
55
|
+
print(f"Success! Created new branch '{branch_name}' in the stack")
|
|
56
|
+
print(f"Parent branch: {base_branch}")
|
panqake/commands/pr.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Command for creating pull requests for branches in the stack."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from panqake.utils.config import get_child_branches, get_parent_branch
|
|
8
|
+
from panqake.utils.git import branch_exists, get_current_branch, run_git_command
|
|
9
|
+
from panqake.utils.questionary_prompt import (
|
|
10
|
+
PRTitleValidator,
|
|
11
|
+
format_branch,
|
|
12
|
+
print_formatted_text,
|
|
13
|
+
prompt_confirm,
|
|
14
|
+
prompt_input,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_oldest_branch_without_pr(branch):
|
|
19
|
+
"""Find the bottom-most branch without a PR."""
|
|
20
|
+
parent = get_parent_branch(branch)
|
|
21
|
+
|
|
22
|
+
# If no parent or parent is main/master, we've reached the bottom
|
|
23
|
+
if not parent or parent in ["main", "master"]:
|
|
24
|
+
return branch
|
|
25
|
+
|
|
26
|
+
# Check if parent branch already has a PR
|
|
27
|
+
if branch_has_pr(parent):
|
|
28
|
+
# Parent already has a PR, so this is the bottom-most branch without one
|
|
29
|
+
return branch
|
|
30
|
+
else:
|
|
31
|
+
# Parent doesn't have a PR, check further down the stack
|
|
32
|
+
return find_oldest_branch_without_pr(parent)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def branch_has_pr(branch):
|
|
36
|
+
"""Check if a branch already has a PR."""
|
|
37
|
+
try:
|
|
38
|
+
subprocess.run(
|
|
39
|
+
["gh", "pr", "view", branch],
|
|
40
|
+
check=True,
|
|
41
|
+
stdout=subprocess.PIPE,
|
|
42
|
+
stderr=subprocess.PIPE,
|
|
43
|
+
)
|
|
44
|
+
return True
|
|
45
|
+
except subprocess.CalledProcessError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_pr_for_branch(branch, parent):
|
|
50
|
+
"""Create a PR for a specific branch."""
|
|
51
|
+
# Get commit message for default PR title
|
|
52
|
+
commit_message = run_git_command(["log", "-1", "--pretty=%s", branch])
|
|
53
|
+
default_title = (
|
|
54
|
+
f"[{branch}] {commit_message}" if commit_message else f"[{branch}] Stacked PR"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Prompt for PR details
|
|
58
|
+
title = prompt_input(
|
|
59
|
+
"Enter PR title: ", validator=PRTitleValidator(), default=default_title
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
description = prompt_input(
|
|
63
|
+
"Enter PR description (optional): ",
|
|
64
|
+
default="This is part of a stacked PR series.",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Show summary and confirm
|
|
68
|
+
print_formatted_text(f"<info>PR for branch:</info> {format_branch(branch)}")
|
|
69
|
+
print_formatted_text(f"<info>Target branch:</info> {format_branch(parent)}")
|
|
70
|
+
print_formatted_text(f"<info>Title:</info> {title}")
|
|
71
|
+
|
|
72
|
+
if not prompt_confirm("Create this pull request?"):
|
|
73
|
+
print_formatted_text("<info>PR creation skipped.</info>")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# Create the PR
|
|
77
|
+
try:
|
|
78
|
+
subprocess.run(
|
|
79
|
+
[
|
|
80
|
+
"gh",
|
|
81
|
+
"pr",
|
|
82
|
+
"create",
|
|
83
|
+
"--base",
|
|
84
|
+
parent,
|
|
85
|
+
"--head",
|
|
86
|
+
branch,
|
|
87
|
+
"--title",
|
|
88
|
+
title,
|
|
89
|
+
"--body",
|
|
90
|
+
description,
|
|
91
|
+
],
|
|
92
|
+
check=True,
|
|
93
|
+
)
|
|
94
|
+
print_formatted_text(
|
|
95
|
+
f"<success>PR created successfully for {format_branch(branch)}</success>"
|
|
96
|
+
)
|
|
97
|
+
return True
|
|
98
|
+
except subprocess.CalledProcessError:
|
|
99
|
+
print_formatted_text(
|
|
100
|
+
f"<warning>Error: Failed to create PR for branch '{branch}'</warning>"
|
|
101
|
+
)
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_branch_in_path_to_target(child, branch_name, parent_branch):
|
|
106
|
+
"""Check if a child branch is in the path to the target branch."""
|
|
107
|
+
current = branch_name
|
|
108
|
+
while current and current != parent_branch:
|
|
109
|
+
if current == child:
|
|
110
|
+
return True
|
|
111
|
+
current = get_parent_branch(current)
|
|
112
|
+
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def process_branch_for_pr(branch, target_branch):
|
|
117
|
+
"""Process a branch to create PR and handle its children."""
|
|
118
|
+
if branch_has_pr(branch):
|
|
119
|
+
print_formatted_text(f"Branch {format_branch(branch)} already has an open PR")
|
|
120
|
+
else:
|
|
121
|
+
print_formatted_text(
|
|
122
|
+
f"<info>Creating PR for branch:</info> {format_branch(branch)}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Get parent branch for PR target
|
|
126
|
+
parent = get_parent_branch(branch)
|
|
127
|
+
if not parent:
|
|
128
|
+
parent = "main" # Default to main if no parent
|
|
129
|
+
|
|
130
|
+
create_pr_for_branch(branch, parent)
|
|
131
|
+
|
|
132
|
+
# Process any children of this branch that lead to the target
|
|
133
|
+
for child in get_child_branches(branch):
|
|
134
|
+
if (
|
|
135
|
+
is_branch_in_path_to_target(child, target_branch, branch)
|
|
136
|
+
or child == target_branch
|
|
137
|
+
):
|
|
138
|
+
process_branch_for_pr(child, target_branch)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_pull_requests(branch_name=None):
|
|
142
|
+
"""Create pull requests for branches in the stack."""
|
|
143
|
+
# Check for GitHub CLI
|
|
144
|
+
if not shutil.which("gh"):
|
|
145
|
+
print_formatted_text(
|
|
146
|
+
"<warning>Error: GitHub CLI (gh) is required but not installed.</warning>"
|
|
147
|
+
)
|
|
148
|
+
print_formatted_text(
|
|
149
|
+
"<info>Please install GitHub CLI: https://cli.github.com/manual/installation</info>"
|
|
150
|
+
)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
# If no branch specified, use current branch
|
|
154
|
+
if not branch_name:
|
|
155
|
+
branch_name = get_current_branch()
|
|
156
|
+
|
|
157
|
+
# Check if target branch exists
|
|
158
|
+
if not branch_exists(branch_name):
|
|
159
|
+
print_formatted_text(
|
|
160
|
+
f"<warning>Error: Branch '{branch_name}' does not exist</warning>"
|
|
161
|
+
)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
# Find the oldest branch in the stack that needs a PR
|
|
165
|
+
oldest_branch = find_oldest_branch_without_pr(branch_name)
|
|
166
|
+
|
|
167
|
+
print_formatted_text(
|
|
168
|
+
f"<info>Creating PRs from the bottom of the stack up to:</info> {format_branch(branch_name)}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
process_branch_for_pr(oldest_branch, branch_name)
|
|
172
|
+
|
|
173
|
+
print_formatted_text("<success>Pull request creation complete</success>")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Command for switching between Git branches."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
|
|
7
|
+
from panqake.utils.git import get_current_branch, list_all_branches, run_git_command
|
|
8
|
+
from panqake.utils.questionary_prompt import print_formatted_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def switch_branch(branch_name=None):
|
|
12
|
+
"""Switch to another git branch using interactive selection.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
branch_name: Optional branch name to switch to directly.
|
|
16
|
+
If not provided, shows an interactive selection.
|
|
17
|
+
"""
|
|
18
|
+
# Get all available branches
|
|
19
|
+
branches = list_all_branches()
|
|
20
|
+
|
|
21
|
+
if not branches:
|
|
22
|
+
print_formatted_text("<warning>No branches found in repository</warning>")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
current = get_current_branch()
|
|
26
|
+
|
|
27
|
+
# If branch name is provided, switch directly
|
|
28
|
+
if branch_name:
|
|
29
|
+
if branch_name not in branches:
|
|
30
|
+
print_formatted_text(
|
|
31
|
+
f"<warning>Error: Branch '{branch_name}' does not exist</warning>"
|
|
32
|
+
)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
if branch_name == current:
|
|
36
|
+
print_formatted_text(f"<info>Already on branch '{branch_name}'</info>")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
_checkout_branch(branch_name)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Format branches for display, marking the current branch
|
|
43
|
+
choices = []
|
|
44
|
+
for branch in branches:
|
|
45
|
+
is_current = branch == current
|
|
46
|
+
if is_current:
|
|
47
|
+
# Add a special marker for the current branch
|
|
48
|
+
choices.append(
|
|
49
|
+
questionary.Choice(
|
|
50
|
+
title=f"* {branch} (current)",
|
|
51
|
+
value=branch,
|
|
52
|
+
disabled="current branch",
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
choices.append(branch)
|
|
57
|
+
|
|
58
|
+
# Show interactive branch selection
|
|
59
|
+
selected = questionary.select(
|
|
60
|
+
"Select a branch to switch to:",
|
|
61
|
+
choices=choices,
|
|
62
|
+
).ask()
|
|
63
|
+
|
|
64
|
+
if selected:
|
|
65
|
+
if selected == current:
|
|
66
|
+
print_formatted_text(f"<info>Already on branch '{selected}'</info>")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
_checkout_branch(selected)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _checkout_branch(branch_name):
|
|
73
|
+
"""Checkout to the specified branch."""
|
|
74
|
+
print_formatted_text(f"<info>Switching to branch '{branch_name}'...</info>")
|
|
75
|
+
result = run_git_command(["checkout", branch_name])
|
|
76
|
+
|
|
77
|
+
if result is not None:
|
|
78
|
+
print_formatted_text(
|
|
79
|
+
f"<success>Successfully switched to branch '{branch_name}'</success>"
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
print_formatted_text("<danger>Failed to switch branches</danger>")
|
|
83
|
+
sys.exit(1)
|