Skip to content

Instantly share code, notes, and snippets.

@sandeepkv93
Created July 14, 2025 14:06
Show Gist options
  • Select an option

  • Save sandeepkv93/e3af454009d158abcc3f3d0703d70545 to your computer and use it in GitHub Desktop.

Select an option

Save sandeepkv93/e3af454009d158abcc3f3d0703d70545 to your computer and use it in GitHub Desktop.
Graphite CLI for Stacked Pull Requests - Complete Guide

Graphite CLI for Stacked Pull Requests - Complete Guide

Table of Contents

Introduction

This advanced guide covers complex scenarios, best practices, and deep dives into Graphite's stacked PR workflow. It assumes familiarity with basic Graphite concepts and commands.

Git Configuration for Graphite

Essential Git Settings

# Enable rerere (Reuse Recorded Resolution)
git config rerere.enabled true
git config rerere.autoupdate true

# Set up helpful aliases
git config alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

# Configure merge strategy
git config pull.rebase true
git config rebase.autoStash true

# Graphite-specific settings
gt repo init --trunk main

Why Git Rerere Matters

Git rerere (reuse recorded resolution) is crucial for Graphite workflows:

  • Automatically remembers how you resolved conflicts
  • Applies the same resolution when encountering identical conflicts
  • Essential when restacking multiple branches with similar conflicts

Complex Stack Examples

Example 1: Building a Feature with Multiple Layers

# Step 1: Database schema changes
gt branch create "feat/user-system-db-schema"
echo "CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR(255));" > migrations/001_users.sql
git add migrations/001_users.sql
git commit -m "Add users table schema"
gt pr create --title "Add users database schema" --body "Foundation for user system"

# Step 2: Model layer
gt branch create "feat/user-system-models" --parent "feat/user-system-db-schema"
cat > models/user.py << EOF
from dataclasses import dataclass
from typing import Optional

@dataclass
class User:
    id: Optional[int] = None
    email: str
EOF
git add models/user.py
git commit -m "Add User model"
gt pr create --title "Add User model" --body "Basic user model implementation"

# Step 3: Repository layer
gt branch create "feat/user-system-repository" --parent "feat/user-system-models"
cat > repositories/user_repository.py << EOF
from models.user import User
from typing import Optional, List

class UserRepository:
    async def create(self, user: User) -> User:
        # Implementation here
        pass
    
    async def find_by_email(self, email: str) -> Optional[User]:
        # Implementation here
        pass
EOF
git add repositories/user_repository.py
git commit -m "Add UserRepository"
gt pr create --title "Add UserRepository" --body "Repository pattern for user data access"

# Step 4: Service layer
gt branch create "feat/user-system-service" --parent "feat/user-system-repository"
cat > services/user_service.py << EOF
from repositories.user_repository import UserRepository
from models.user import User

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo
    
    async def register_user(self, email: str) -> User:
        # Business logic here
        user = User(email=email)
        return await self.repo.create(user)
EOF
git add services/user_service.py
git commit -m "Add UserService with registration logic"
gt pr create --title "Add UserService" --body "Business logic for user operations"

# Step 5: API endpoints
gt branch create "feat/user-system-api" --parent "feat/user-system-service"
cat > api/users.py << EOF
from fastapi import APIRouter, Depends
from services.user_service import UserService

router = APIRouter()

@router.post("/users")
async def create_user(email: str, service: UserService = Depends()):
    return await service.register_user(email)
EOF
git add api/users.py
git commit -m "Add user registration API endpoint"
gt pr create --title "Add user API endpoints" --body "REST API for user operations"

# View the complete stack
gt stack

Example 2: Refactoring with Incremental Changes

# Starting with a monolithic function that needs refactoring
gt branch create "refactor/split-payment-processing-step1"

# Step 1: Extract validation logic
cat > validators/payment_validator.py << EOF
def validate_credit_card(card_number: str) -> bool:
    # Luhn algorithm implementation
    return len(card_number) == 16 and card_number.isdigit()

def validate_amount(amount: float) -> bool:
    return amount > 0 and amount <= 10000
EOF
git add validators/payment_validator.py
git commit -m "Extract payment validation logic"
gt pr create --title "Extract payment validation" --body "Step 1: Separate validation concerns"

# Step 2: Extract payment gateway interface
gt branch create "refactor/split-payment-processing-step2" --parent "refactor/split-payment-processing-step1"
cat > gateways/payment_gateway.py << EOF
from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    async def charge(self, amount: float, token: str) -> dict:
        pass

class StripeGateway(PaymentGateway):
    async def charge(self, amount: float, token: str) -> dict:
        # Stripe-specific implementation
        pass
EOF
git add gateways/payment_gateway.py
git commit -m "Create payment gateway abstraction"
gt pr create --title "Add payment gateway interface" --body "Step 2: Abstract payment provider logic"

# Step 3: Refactor main payment processor
gt branch create "refactor/split-payment-processing-step3" --parent "refactor/split-payment-processing-step2"
cat > processors/payment_processor.py << EOF
from validators.payment_validator import validate_credit_card, validate_amount
from gateways.payment_gateway import PaymentGateway

class PaymentProcessor:
    def __init__(self, gateway: PaymentGateway):
        self.gateway = gateway
    
    async def process_payment(self, card_number: str, amount: float) -> dict:
        if not validate_credit_card(card_number):
            raise ValueError("Invalid card number")
        
        if not validate_amount(amount):
            raise ValueError("Invalid amount")
        
        token = self._tokenize_card(card_number)
        return await self.gateway.charge(amount, token)
    
    def _tokenize_card(self, card_number: str) -> str:
        # Tokenization logic
        return f"tok_{card_number[-4:]}"
EOF
git add processors/payment_processor.py
git commit -m "Refactor payment processor to use new components"
gt pr create --title "Complete payment processing refactor" --body "Step 3: Wire everything together"

# Step 4: Add tests for the refactored code
gt branch create "refactor/split-payment-processing-tests" --parent "refactor/split-payment-processing-step3"
cat > tests/test_payment_processor.py << EOF
import pytest
from processors.payment_processor import PaymentProcessor
from gateways.payment_gateway import PaymentGateway

class MockGateway(PaymentGateway):
    async def charge(self, amount: float, token: str) -> dict:
        return {"status": "success", "transaction_id": "123"}

@pytest.mark.asyncio
async def test_valid_payment():
    processor = PaymentProcessor(MockGateway())
    result = await processor.process_payment("4111111111111111", 100.0)
    assert result["status"] == "success"

@pytest.mark.asyncio
async def test_invalid_card():
    processor = PaymentProcessor(MockGateway())
    with pytest.raises(ValueError, match="Invalid card number"):
        await processor.process_payment("invalid", 100.0)
EOF
git add tests/test_payment_processor.py
git commit -m "Add comprehensive tests for payment processing"
gt pr create --title "Add payment processing tests" --body "Step 4: Ensure refactoring maintains functionality"

Managing Partially Merged Stacks

Scenario: Middle PR Gets Merged First

When a PR in the middle of your stack gets merged before the ones below it:

# Initial stack state
# main
#  └── feat/api-base (#101) [OPEN]
#       └── feat/api-auth (#102) [MERGED] ← This got merged first
#            └── feat/api-endpoints (#103) [OPEN]

# Step 1: Sync with remote to get the latest main
gt repo sync

# Step 2: Graphite detects the merged PR and offers to restack
gt stack restack

# Graphite will:
# 1. Update main with the merged changes
# 2. Rebase feat/api-base onto the new main
# 3. Rebase feat/api-endpoints onto feat/api-base

# If there are conflicts during restacking:
gt stack fix

# Example conflict resolution flow:
# Graphite: Conflict detected in feat/api-base
# 1. Resolve conflicts in your editor
# 2. Stage resolved files: git add <files>
# 3. Continue: gt stack fix --continue

Scenario: Multiple PRs Merged with Feedback

# After reviewers approve and merge PRs #101 and #102 with squash commits
# Original stack:
# main
#  └── feat/logging-base (#101) [MERGED with changes]
#       └── feat/logging-impl (#102) [MERGED with changes]
#            └── feat/logging-tests (#103) [OPEN]
#                 └── feat/logging-docs (#104) [OPEN]

# Step 1: Sync and let Graphite detect merged PRs
gt repo sync

# Graphite output:
# Detected merged PRs: #101, #102
# Would you like to restack the remaining branches? [Y/n]

# Step 2: Restack remaining branches
gt stack restack

# If the squashed commits differ significantly from your original commits:
# You might encounter conflicts. Use rerere to help:
git rerere diff  # Shows recorded resolutions

# Step 3: Update PR descriptions for remaining PRs
gt pr update --body "Updated after base PRs were merged"

# Step 4: Check the new stack state
gt stack
# main
#  └── feat/logging-tests (#103) [OPEN] ← Now based on main
#       └── feat/logging-docs (#104) [OPEN]

Handling Review Comments Before Merge

# PR #102 has review comments that need addressing
gt checkout feat/api-auth

# Make the requested changes
vim src/auth.py  # Address review comments
git add src/auth.py
git commit -m "Address review: Add input validation"

# Update the PR
gt pr update

# Push changes and update upstack branches
gt repo sync

# The changes automatically propagate to dependent branches
# Graphite handles rebasing feat/api-endpoints onto the updated feat/api-auth

Conflict Resolution Strategies

Strategy 1: Incremental Conflict Resolution

# When dealing with complex conflicts across multiple branches
gt stack fix --one-at-a-time

# This allows you to:
# 1. Fix conflicts in one branch
# 2. Test that branch
# 3. Move to the next branch
# 4. Repeat until all conflicts are resolved

Strategy 2: Bulk Conflict Resolution

# For similar conflicts across multiple branches
gt stack fix

# Use git rerere to record resolutions
git add .
git rerere  # Records the resolution

# Continue with the fix
gt stack fix --continue

# Future similar conflicts will be auto-resolved

Strategy 3: Conflict Avoidance

# Before creating a new branch in the stack
# Check for potential conflicts
git diff main..HEAD --name-only | grep -E "(config|settings)"

# If files that commonly cause conflicts are modified,
# consider creating a separate base branch for those changes
gt branch create "config/update-settings"
# Make config changes
git add config/
git commit -m "Update configuration files"
gt pr create

# Then base your feature stack on this branch
gt branch create "feat/new-feature" --parent "config/update-settings"

Advanced Workflows

Workflow 1: Parallel Development Tracks

# Create two parallel tracks that merge at the end
# Track 1: Backend changes
gt branch create "feat/backend-models"
# ... make changes ...
gt pr create

gt branch create "feat/backend-api" --parent "feat/backend-models"
# ... make changes ...
gt pr create

# Track 2: Frontend changes (parallel to backend)
gt checkout main
gt branch create "feat/frontend-components"
# ... make changes ...
gt pr create

gt branch create "feat/frontend-views" --parent "feat/frontend-components"
# ... make changes ...
gt pr create

# Convergence point: Integration
gt branch create "feat/integration" --parent "feat/backend-api"
# Manually merge frontend changes
git merge feat/frontend-views --no-ff
git commit -m "Integrate frontend and backend changes"
gt pr create

# View the complex stack
gt stack --all

Workflow 2: Feature Flags with Stacked PRs

# Base: Add feature flag infrastructure
gt branch create "feat/add-feature-flags"
cat > feature_flags.py << EOF
FLAGS = {
    "new_payment_flow": False,
    "enhanced_analytics": False,
}
EOF
git add feature_flags.py
git commit -m "Add feature flag system"
gt pr create

# Implementation behind flag
gt branch create "feat/new-payment-impl" --parent "feat/add-feature-flags"
cat > payment_v2.py << EOF
from feature_flags import FLAGS

def process_payment(amount):
    if FLAGS["new_payment_flow"]:
        return _new_payment_flow(amount)
    return _legacy_payment_flow(amount)
EOF
git add payment_v2.py
git commit -m "Implement new payment flow behind flag"
gt pr create

# Enable flag
gt branch create "feat/enable-payment-flag" --parent "feat/new-payment-impl"
sed -i '' 's/"new_payment_flow": False/"new_payment_flow": True/' feature_flags.py
git add feature_flags.py
git commit -m "Enable new payment flow"
gt pr create --draft  # Keep as draft until ready to enable

Workflow 3: Hotfix in the Middle of a Stack

# Current stack has 5 PRs, but you need to hotfix PR #2

# Save current stack state
gt stack --format json > stack-backup.json

# Create hotfix branch from the problematic PR
gt checkout feat/problematic-branch
gt branch create "hotfix/urgent-fix"

# Make the fix
vim src/critical_file.py
git add src/critical_file.py
git commit -m "Fix critical bug in data processing"

# Create PR directly against main (bypass stack)
gt pr create --base main --no-stack

# After hotfix is merged, restore stack
gt repo sync
gt stack restack

Automation and Scripts

Auto-restack Script

#!/bin/bash
# save as ~/.graphite/scripts/auto-restack.sh

set -e

echo "πŸ”„ Starting auto-restack process..."

# Fetch latest changes
gt repo sync

# Check for merged PRs
MERGED_COUNT=$(gt stack --format json | jq '[.[] | select(.pr_state == "MERGED")] | length')

if [ "$MERGED_COUNT" -gt 0 ]; then
    echo "πŸ“Š Found $MERGED_COUNT merged PRs"
    
    # Backup current state
    gt stack --format json > ~/.graphite/stack-backup-$(date +%Y%m%d-%H%M%S).json
    
    # Attempt restack
    if gt stack restack --no-interactive; then
        echo "βœ… Restack successful!"
        
        # Run tests on each affected branch
        for branch in $(gt stack --format json | jq -r '.[].name'); do
            echo "πŸ§ͺ Testing $branch..."
            gt checkout "$branch"
            if npm test; then
                echo "βœ… Tests passed for $branch"
            else
                echo "❌ Tests failed for $branch"
                exit 1
            fi
        done
    else
        echo "❌ Restack failed, manual intervention required"
        exit 1
    fi
else
    echo "ℹ️ No merged PRs found, stack is up to date"
fi

PR Description Generator

#!/bin/bash
# save as ~/.graphite/scripts/generate-pr-description.sh

BRANCH=$(git rev-parse --abbrev-ref HEAD)
PARENT=$(gt branch info --json | jq -r '.parent // "main"')

echo "## Summary"
echo ""
echo "### Changes"
git log --oneline "$PARENT".."$BRANCH" | sed 's/^/- /'
echo ""
echo "### Files Modified"
git diff --stat "$PARENT".."$BRANCH" | head -n -1
echo ""
echo "### Stack Position"
gt stack | grep -A2 -B2 "$BRANCH" || echo "Branch: $BRANCH"
echo ""
echo "### Testing"
echo "- [ ] Unit tests pass"
echo "- [ ] Integration tests pass"
echo "- [ ] Manual testing completed"

Team Collaboration

Setting Up Team Conventions

# .graphite/team-config.yml
stack_conventions:
  branch_naming:
    pattern: "{type}/{ticket}-{description}"
    types: ["feat", "fix", "refactor", "docs", "test"]
  
  pr_size_limits:
    max_lines_changed: 400
    max_files_changed: 20
  
  required_stack_depth:
    min: 2
    max: 7
  
  auto_restack:
    enabled: true
    on_pr_merge: true
    run_tests: true

Collaborative Stack Development

# Developer A starts the foundation
gt branch create "feat/JIRA-123-api-foundation"
# ... implements base API structure ...
gt pr create --reviewers "developerB,developerC"

# Developer B can start working on top while A's PR is in review
gt checkout feat/JIRA-123-api-foundation
gt branch create "feat/JIRA-124-api-auth"
# ... implements authentication ...
gt pr create --reviewers "developerA,developerC"

# Developer C can work on a parallel feature
gt checkout feat/JIRA-123-api-foundation
gt branch create "feat/JIRA-125-api-validation"
# ... implements validation ...
gt pr create --reviewers "developerA,developerB"

# Handling updates from Developer A
# When A pushes updates to the foundation:
gt repo sync
gt stack restack  # Automatically rebases B and C's work

Performance Optimization

Large Stack Management

# For stacks with 10+ PRs, use targeted operations

# Update only a specific part of the stack
gt stack restack --only "feat/specific-branch"

# Skip CI for intermediate branches
gt pr create --no-ci --title "[SKIP CI] Intermediate change"

# Batch PR creation
for i in {1..5}; do
    gt branch create "feat/part-$i" --parent "feat/part-$((i-1))"
    # make changes
    git commit -m "Part $i implementation"
done
gt pr create --all  # Creates all PRs at once

Efficient Conflict Resolution

# Pre-record common conflict resolutions
# Create a rerere training script
cat > train-rerere.sh << 'EOF'
#!/bin/bash
# Common conflict patterns in your codebase

# Pattern 1: Import conflicts
echo "Training rerere for import conflicts..."
git checkout -b rerere-train-1
echo "import { A } from './a';" > test.js
git add test.js && git commit -m "Add import A"

git checkout -b rerere-train-2
echo "import { B } from './b';" > test.js
git add test.js && git commit -m "Add import B"

git checkout rerere-train-1
git merge rerere-train-2  # Creates conflict

# Resolve the conflict
echo -e "import { A } from './a';\nimport { B } from './b';" > test.js
git add test.js
git commit -m "Merge imports"

# Clean up
git checkout main
git branch -D rerere-train-1 rerere-train-2
EOF

chmod +x train-rerere.sh
./train-rerere.sh

Real-World Scenarios

Scenario 1: Emergency Rollback Mid-Stack

# Stack: A -> B -> C -> D -> E
# Problem: PR B introduced a critical bug after merge

# Step 1: Create revert PR for B
gt pr create --revert PR_NUMBER_OF_B

# Step 2: Rebase remaining stack without B's changes
gt checkout C
git rebase --onto A B C
gt checkout D
git rebase --onto C B@{1} D
gt checkout E  
git rebase --onto D C@{1} E

# Step 3: Force update the PRs
gt pr update --force

Scenario 2: Cross-Team Feature Integration

# Team A's stack: auth-base -> auth-impl -> auth-tests
# Team B's stack: ui-base -> ui-components -> ui-integration
# Need to create integration PR

# Step 1: Create integration branch
gt checkout auth-tests
gt branch create "feat/auth-ui-integration"

# Step 2: Merge UI changes
git merge ui-integration --no-ff -m "Merge UI components for auth"

# Step 3: Integration code
cat > integration/auth_ui_connector.py << EOF
from auth.service import AuthService
from ui.components import LoginForm

class AuthUIConnector:
    def __init__(self):
        self.auth_service = AuthService()
        self.login_form = LoginForm()
    
    def wire_authentication(self):
        self.login_form.on_submit = self.auth_service.authenticate
EOF

git add integration/
git commit -m "Wire auth backend to UI components"
gt pr create --reviewers "teamA-lead,teamB-lead"

Scenario 3: Gradual Migration Stack

# Migrating from OldAPI to NewAPI across the codebase

# Step 1: Add new API alongside old
gt branch create "migrate/add-new-api"
cp -r src/old_api src/new_api
# ... modify new_api implementation ...
git add src/new_api
git commit -m "Add NewAPI implementation"
gt pr create

# Step 2: Add compatibility layer
gt branch create "migrate/compatibility-layer" --parent "migrate/add-new-api"
cat > src/api_compat.py << EOF
from src.old_api import OldAPI
from src.new_api import NewAPI

class APICompat:
    def __init__(self, use_new=False):
        self.impl = NewAPI() if use_new else OldAPI()
    
    def __getattr__(self, name):
        return getattr(self.impl, name)
EOF
git add src/api_compat.py
git commit -m "Add API compatibility layer"
gt pr create

# Step 3-N: Migrate each component
components=("auth" "payment" "user" "admin")
for comp in "${components[@]}"; do
    parent=$(gt branch current)
    gt branch create "migrate/${comp}-to-new-api" --parent "$parent"
    
    # Update imports
    find "src/${comp}" -name "*.py" -exec sed -i '' 's/from src.old_api/from src.api_compat/g' {} +
    
    git add "src/${comp}"
    git commit -m "Migrate ${comp} to use compatibility layer"
    gt pr create
done

# Final step: Remove old API
gt branch create "migrate/remove-old-api" --parent $(gt branch current)
rm -rf src/old_api
sed -i '' 's/use_new=False/use_new=True/g' src/api_compat.py
git add -A
git commit -m "Remove OldAPI and default to NewAPI"
gt pr create

Summary

This advanced guide demonstrates:

  1. Complex Stack Patterns: Building multi-layered features with clear dependencies
  2. Merge Management: Handling partial merges and automatic restacking
  3. Conflict Resolution: Using git rerere and strategic approaches
  4. Team Workflows: Coordinating multiple developers on interconnected features
  5. Automation: Scripts and tools to streamline stack management
  6. Real-World Solutions: Practical approaches to common challenges

Remember:

  • Always run gt repo sync before starting work
  • Use gt stack fix for complex conflict scenarios
  • Enable git rerere for repeated conflict patterns
  • Keep individual PRs focused and reviewable
  • Automate repetitive tasks with scripts
  • Communicate stack dependencies clearly in PR descriptions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment