Last active
April 16, 2025 03:01
-
-
Save lynzrand/8a34ddbe0db4ea1409de7b6423c5df40 to your computer and use it in GitHub Desktop.
Pushes the whole stack of branches to remote. Vibe coded.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/env python | |
# This script pushes all stacked PRs until the trunk branch is reached | |
import subprocess | |
import sys | |
import argparse | |
def git(command, capture_output=True): | |
""" | |
Execute a git command using subprocess. | |
Args: | |
command: List of command arguments or string command | |
capture_output: Whether to capture and return command output | |
Returns: | |
Command output as string if capture_output is True | |
""" | |
cmd = ["git"] + (command if isinstance(command, list) else command.split()) | |
result = subprocess.run(cmd, capture_output=capture_output, text=True, check=False) | |
if capture_output: | |
return result.stdout.strip() | |
return "" | |
def get_trunk_branch(): | |
""" | |
Determine the trunk branch by checking for main/master/trunk locally. | |
If none are found, resort to the HEAD of the first remote. | |
Returns: | |
str: The name of the trunk branch | |
""" | |
# Check for common trunk branch names locally | |
for branch in ["main", "master", "trunk"]: | |
if git(f"branch --list {branch}"): | |
return branch | |
# If no common trunk branch found locally, check remote | |
remotes = git("remote").splitlines() | |
if not remotes: | |
return "main" # Default if no remotes | |
# Get the HEAD reference of the first remote | |
remote_head = git(f"symbolic-ref refs/remotes/{remotes[0]}/HEAD") | |
if remote_head: | |
# Extract branch name from the reference (e.g., refs/remotes/origin/main -> main) | |
return remote_head.split("/")[-1] | |
return "main" # Fallback to main if all else fails | |
def get_commits_between(base_branch, head_branch): | |
""" | |
Get a list of commit hashes that exist in head_branch but not in base_branch. | |
Args: | |
base_branch: The base branch name | |
head_branch: The head branch name | |
Returns: | |
list: A list of commit hashes | |
""" | |
commit_output = git(f"log {base_branch}..{head_branch} --pretty=format:%H") | |
return commit_output.splitlines() if commit_output else [] | |
def build_branch_commit_map(): | |
""" | |
Build mappings between branches and commits. | |
Returns: | |
tuple: (branch_to_commit, commit_to_branches) mappings | |
""" | |
# Get all local branches with their commits | |
refs_output = git("show-ref --heads") | |
if not refs_output: | |
return {}, {} | |
# Create the mappings | |
branch_to_commit = {} | |
commit_to_branches = {} | |
for line in refs_output.splitlines(): | |
if not line: | |
continue | |
commit_hash, ref_name = line.split(" ", 1) | |
branch_name = ref_name.replace("refs/heads/", "", 1) | |
branch_to_commit[branch_name] = commit_hash | |
if commit_hash not in commit_to_branches: | |
commit_to_branches[commit_hash] = [] | |
commit_to_branches[commit_hash].append(branch_name) | |
return branch_to_commit, commit_to_branches | |
def main(): | |
# Parse command line arguments | |
parser = argparse.ArgumentParser( | |
description="Push stacked PRs until the trunk branch is reached" | |
) | |
parser.add_argument( | |
"head_branch", nargs="?", help="Head branch to start pushing from" | |
) | |
parser.add_argument( | |
"--base-branch", help="Base branch to stop at (defaults to trunk branch)" | |
) | |
parser.add_argument( | |
"--remote", default="origin", help="Remote to push to (defaults to origin)" | |
) | |
# Any remaining arguments will be passed to git push | |
args, push_args = parser.parse_known_args() | |
# Determine head branch (current branch if not specified) | |
head_branch = args.head_branch or git("symbolic-ref --short HEAD") | |
if not head_branch: | |
print("Error: Not on a branch") | |
sys.exit(1) | |
# Determine base branch (trunk branch if not specified) | |
base_branch = args.base_branch or get_trunk_branch() | |
print(f"Using {base_branch} as base branch") | |
# Get all commits between base and head | |
commits = get_commits_between(base_branch, head_branch) | |
if not commits: | |
print(f"No commits between {base_branch} and {head_branch}") | |
sys.exit(0) | |
# Build branch mappings | |
_, commit_to_branches = build_branch_commit_map() | |
# Find all branches that are part of the stack | |
branches_to_push = set() | |
for commit in commits: | |
if commit in commit_to_branches: | |
branches_to_push.update(commit_to_branches[commit]) | |
# Always include the head branch | |
branches_to_push.add(head_branch) | |
# Sort branches based on their position in the commit history (oldest first) | |
branch_order = [] | |
for commit in reversed(commits): | |
for branch in commit_to_branches.get(commit, []): | |
if branch in branches_to_push and branch not in branch_order: | |
branch_order.append(branch) | |
# Make sure head branch is included in the sorted order if it's not already | |
if head_branch not in branch_order: | |
branch_order.append(head_branch) | |
# Push each branch | |
remote = args.remote | |
for branch in branch_order: | |
print(f"Pushing {branch} to {remote}...") | |
push_cmd = ["push"] + push_args + [remote, branch] | |
try: | |
git(push_cmd, capture_output=False) | |
print(f"Successfully pushed {branch}") | |
except subprocess.CalledProcessError as e: | |
print(f"Error pushing {branch}: {e}") | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment