Skip to content

Instantly share code, notes, and snippets.

@lynzrand
Last active April 16, 2025 03:01
Show Gist options
  • Save lynzrand/8a34ddbe0db4ea1409de7b6423c5df40 to your computer and use it in GitHub Desktop.
Save lynzrand/8a34ddbe0db4ea1409de7b6423c5df40 to your computer and use it in GitHub Desktop.
Pushes the whole stack of branches to remote. Vibe coded.
#!/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