AI Used Notice
Gemini AI was used to rewrite the article more engagingly and generate the image.
The Challenge
Picture this: Your devops team just completed a repository migration from Azure DevOps (ADO) to GitHub Enterprise. Everything seems fine until you realize that some team members kept working on the old ADO repository. Now you have 62 branches that need to be manually migrated to the new GitHub repo.
Doing this manually would mean:
- Checking out each branch locally
- Pushing it to the new remote
- Repeating 62 times
- Dealing with merge conflicts and authentication issues
- Probably making mistakes and losing track
There has to be a better way! 🤔
The Solution: Automation
Let's build a bash script that automates this entire process. I'll walk you through the solution, the challenges we faced, and how to handle them.
Understanding the Git Setup
When migrating repositories, you typically have two remotes:
git remote -v
# ado https://dev.azure.com/org/project/_git/repo (fetch)
# ado https://dev.azure.com/org/project/_git/repo (push)
# github https://github.company.com/org/repo.git (fetch)
# github https://github.company.com/org/repo.git (push)
Our goal is to:
- Fetch all branches from ADO
- Check if they exist on GitHub
- Push them to GitHub (updating if necessary)
- Skip branches that are already up-to-date
- Report the results
The Basic Migration Script
Here's the core concept:
#!/bin/bash
ADO_REMOTE="ado"
GITHUB_REMOTE="github"
# Fetch from both remotes
git fetch "$ADO_REMOTE" --prune
git fetch "$GITHUB_REMOTE" --prune
# Get list of branches from ADO
mapfile -t BRANCHES < <(git branch -r | grep "^ ${ADO_REMOTE}/" | \
grep -v "HEAD" | sed "s|^ ${ADO_REMOTE}/||" | sort)
# Process each branch
for BRANCH in "${BRANCHES[@]}"; do
echo "Processing: $BRANCH"
# Checkout the branch from ADO
git checkout -b "$BRANCH" "${ADO_REMOTE}/${BRANCH}" 2>/dev/null || \
git checkout "$BRANCH"
# Push to GitHub
git push "$GITHUB_REMOTE" "$BRANCH" --force-with-lease
done
Simple, right? Well, not quite. We encountered several real-world issues.
Challenge #1: The "Stale Info" Error
When using --force-with-lease (a safer alternative to --force), Git checks if the remote branch has changed since you last fetched. If there's a mismatch, you get:
! [rejected] branch-name -> branch-name (stale info)
error: failed to push some refs
Solution: Fetch from both remotes before starting the migration:
# Fetch to update remote tracking information
git fetch "$ADO_REMOTE" --prune
git fetch "$GITHUB_REMOTE" --prune
For a one-time migration where ADO is the source of truth, you can also use --force instead of --force-with-lease:
PUSH_STRATEGY="force" # or "safe" for --force-with-lease
if [ "$PUSH_STRATEGY" = "force" ]; then
git push "$GITHUB_REMOTE" "$BRANCH" --force
else
git push "$GITHUB_REMOTE" "$BRANCH" --force-with-lease
fi
Challenge #2: Windows Git Bash Compatibility
When running on Windows with Git Bash, the script would stop after processing just one branch. This was due to:
- Line ending issues (CRLF vs LF)
- Array handling differences in bash on Windows
-
The
set -eflag causing early exit on any error
Solution: Use mapfile to properly handle arrays and avoid set -e:
# DON'T use: set -e # This causes the script to exit on first error
# DO use: mapfile for array handling
mapfile -t BRANCHES < <(git branch -r | grep "^ ${ADO_REMOTE}/" | \
grep -v "HEAD" | sed "s|^ ${ADO_REMOTE}/||")
# Process with explicit error checking
for BRANCH in "${BRANCHES[@]}"; do
git checkout "$BRANCH" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Failed to checkout $BRANCH"
continue # Keep going!
fi
# ... rest of the logic
done
Challenge #3: Tracking Migration Results
You need to know:
- Which branches were successfully migrated
- Which were skipped (already up-to-date)
- Which failed
Solution: Use arrays to track results:
declare -a MIGRATED_BRANCHES
declare -a SKIPPED_BRANCHES
declare -a FAILED_BRANCHES
# During migration
if push_successful; then
MIGRATED_BRANCHES+=("$BRANCH")
elif already_up_to_date; then
SKIPPED_BRANCHES+=("$BRANCH")
else
FAILED_BRANCHES+=("$BRANCH")
fi
# Show results at the end
echo "Successfully migrated branches:"
for branch in "${MIGRATED_BRANCHES[@]}"; do
echo " ✓ $branch"
done
The Complete Solution
Here's the full production-ready script:
#!/bin/bash
# Configuration
ADO_REMOTE="ado"
GITHUB_REMOTE="github"
PUSH_STRATEGY="force" # or "safe"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Tracking arrays
declare -a MIGRATED_BRANCHES
declare -a SKIPPED_BRANCHES
declare -a FAILED_BRANCHES
SUCCESS_COUNT=0
FAILED_COUNT=0
SKIPPED_COUNT=0
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Git Branch Migration Script${NC}"
echo -e "${BLUE}From: $ADO_REMOTE -> To: $GITHUB_REMOTE${NC}"
echo -e "${BLUE}========================================${NC}\n"
# Verify remotes
for remote in "$ADO_REMOTE" "$GITHUB_REMOTE"; do
if ! git remote | grep -q "^${remote}$"; then
echo -e "${RED}Error: Remote '$remote' not found!${NC}"
exit 1
fi
done
# Fetch from both remotes
echo -e "${YELLOW}Fetching from $ADO_REMOTE...${NC}"
git fetch "$ADO_REMOTE" --prune
echo -e "${YELLOW}Fetching from $GITHUB_REMOTE...${NC}"
git fetch "$GITHUB_REMOTE" --prune
# Get branches
mapfile -t BRANCHES < <(git branch -r | grep "^ ${ADO_REMOTE}/" | \
grep -v "HEAD" | sed "s|^ ${ADO_REMOTE}/||" | sort)
TOTAL=${#BRANCHES[@]}
echo -e "${GREEN}Found $TOTAL branches to migrate${NC}\n"
# Save original branch
ORIGINAL_BRANCH=$(git branch --show-current)
# Process each branch
COUNTER=1
for BRANCH in "${BRANCHES[@]}"; do
[ -z "$BRANCH" ] && continue
echo -e "${BLUE}[$COUNTER/$TOTAL]${NC} Processing: ${YELLOW}$BRANCH${NC}"
# Check if already up-to-date
GITHUB_CHECK=$(git ls-remote --heads "$GITHUB_REMOTE" "$BRANCH" 2>&1)
if [ $? -eq 0 ] && echo "$GITHUB_CHECK" | grep -q "$BRANCH"; then
ADO_COMMIT=$(git rev-parse "${ADO_REMOTE}/${BRANCH}" 2>&1)
GITHUB_COMMIT=$(git rev-parse "${GITHUB_REMOTE}/${BRANCH}" 2>&1)
if [ "$ADO_COMMIT" = "$GITHUB_COMMIT" ]; then
echo -e " ${GREEN}✓ Already up-to-date, skipping${NC}"
((SKIPPED_COUNT++))
SKIPPED_BRANCHES+=("$BRANCH")
((COUNTER++))
continue
fi
fi
# Checkout branch
if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
git checkout "$BRANCH" > /dev/null 2>&1
git reset --hard "${ADO_REMOTE}/${BRANCH}" > /dev/null 2>&1
else
git checkout -b "$BRANCH" "${ADO_REMOTE}/${BRANCH}" > /dev/null 2>&1
fi
if [ $? -ne 0 ]; then
echo -e " ${RED}✗ Failed to checkout${NC}"
((FAILED_COUNT++))
FAILED_BRANCHES+=("$BRANCH")
((COUNTER++))
continue
fi
# Push to GitHub
if [ "$PUSH_STRATEGY" = "force" ]; then
PUSH_FLAG="--force"
else
PUSH_FLAG="--force-with-lease"
fi
git push "$GITHUB_REMOTE" "$BRANCH" $PUSH_FLAG > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e " ${GREEN}✓ Successfully pushed${NC}"
((SUCCESS_COUNT++))
MIGRATED_BRANCHES+=("$BRANCH")
else
echo -e " ${RED}✗ Failed to push${NC}"
((FAILED_COUNT++))
FAILED_BRANCHES+=("$BRANCH")
fi
((COUNTER++))
echo ""
done
# Return to original branch
git checkout "$ORIGINAL_BRANCH" > /dev/null 2>&1
# Summary
echo -e "\n${BLUE}========================================${NC}"
echo -e "${BLUE}Migration Summary${NC}"
echo -e "${BLUE}========================================${NC}"
echo -e "${GREEN}✓ Successfully migrated: ${SUCCESS_COUNT}${NC}"
echo -e "${YELLOW}⚠ Skipped: ${SKIPPED_COUNT}${NC}"
echo -e "${RED}✗ Failed: ${FAILED_COUNT}${NC}"
if [ ${#MIGRATED_BRANCHES[@]} -gt 0 ]; then
echo -e "\n${GREEN}Successfully migrated:${NC}"
for branch in "${MIGRATED_BRANCHES[@]}"; do
echo -e " ✓ $branch"
done
fi
if [ ${#SKIPPED_BRANCHES[@]} -gt 0 ]; then
echo -e "\n${YELLOW}Skipped (already up-to-date):${NC}"
for branch in "${SKIPPED_BRANCHES[@]}"; do
echo -e " ⚠ $branch"
done
fi
if [ ${#FAILED_BRANCHES[@]} -gt 0 ]; then
echo -e "\n${RED}Failed:${NC}"
for branch in "${FAILED_BRANCHES[@]}"; do
echo -e " ✗ $branch"
done
fi
echo -e "\n${GREEN}Migration complete!${NC}"
Usage
- Setup your remotes:
git remote add ado
git remote add github
- Configure the script: Edit the variables at the top:
ADO_REMOTE="ado" # Your ADO remote name
GITHUB_REMOTE="github" # Your GitHub remote name
PUSH_STRATEGY="force" # Use "force" for one-time migration
- Run it:
chmod +x migrate_branches.sh
./migrate_branches.sh
- Save the report (optional):
./migrate_branches.sh 2>&1 | tee migration_report.txt
Best Practices
1. Test First
Run the script on a test repository before using it on production code.
2. Communication
Inform your team before starting the migration to avoid conflicts.
3. Choose the Right Strategy
- Use
PUSH_STRATEGY="safe"if multiple people might be pushing to GitHub during migration - Use
PUSH_STRATEGY="force"for one-time migrations where ADO is the source of truth
4. Authentication
For GitHub Enterprise, you may need to configure authentication:
# For HTTPS
git config --global credential.helper cache
# Or use a personal access token in your URL
https://username:token@github.company.com/org/repo.git
5. Handle Failed Branches
If any branches fail, you can migrate them manually:
git checkout -b failed-branch ado/failed-branch
git push github failed-branch --force
Troubleshooting
Issue: Script stops after one branch
Cause: Line ending issues or set -e flag
Fix: Use mapfile and avoid set -e
Issue: "stale info" errors
Cause: Remote tracking information is outdated
Fix: Fetch from both remotes before migration, or use --force
Issue: Authentication failures
Cause: Missing or expired credentials
Fix:
# Clear and reconfigure credentials
git config --global --unset credential.helper
git config --global credential.helper cache
# Test authentication
git ls-remote github
Issue: TLS/SSL certificate warnings
Cause: Self-signed certificates in enterprise environments
Fix:
# Temporary (not recommended for production)
git config --global http.sslVerify false
# Better: Add the certificate to your trust store
Simplified Alternative
If you encounter issues with the full script, here's a simplified version that uses a temporary file:
#!/bin/bash
ADO_REMOTE="ado"
GITHUB_REMOTE="github"
# Get branches to a file
git branch -r | grep "^ ${ADO_REMOTE}/" | grep -v "HEAD" | \
sed "s|^ ${ADO_REMOTE}/||" > /tmp/branches.txt
# Process each line
while IFS= read -r BRANCH; do
[ -z "$BRANCH" ] && continue
echo "Processing: $BRANCH"
git checkout -b "$BRANCH" "${ADO_REMOTE}/${BRANCH}" 2>/dev/null || \
git checkout "$BRANCH"
git push "$GITHUB_REMOTE" "$BRANCH" --force
done < /tmp/branches.txt
rm /tmp/branches.txt
Results
After running this script on our 62-branch repository, we got:
- ✅ 58 branches successfully migrated
- ⚠️ 3 branches skipped (already up-to-date)
- ❌ 1 branch failed (required manual intervention)
Total time: ~5 minutes vs. several hours of manual work!
Conclusion
Automating Git branch migrations saves time and reduces errors. The key lessons:
- Fetch first to avoid stale info errors
- Handle Windows compatibility with proper array handling
- Track results for audit trails and documentation
- Choose appropriate force flags based on your scenario
- Test in a safe environment before production
The complete script is available as a GitHub Gist (link in comments). Feel free to adapt it for your needs!
Have you faced similar migration challenges? Share your experiences in the comments! 👇

Top comments (0)