Merging Two Git Repositories into a Monorepo: A Complete Guide
The Challenge
I recently faced a common yet complex challenge: merging two separate Git repositories — a Rails backend and a React frontend — into a unified monorepo. What made this particularly challenging was the scale: each repository had approximately 100 branches, representing years of parallel development work.
The goal was simple in concept but complex in execution: create a single repository where both projects could coexist while preserving their complete histories, all branches, and ensuring future developers could trace every commit back to its origin.
Requirements and Constraints
Before diving into the solution, let me outline the specific requirements:
1. Directory Structure
monorepo/
├── backend/ # All Rails application files
└── frontend/ # All React application files
2. Commit History Preservation
- Preserve complete commit history from both repositories
- Interleave commits chronologically by date (not by repository)
- Add prefixes to commit messages: [backend] and [frontend] for easy identification
3. Branch Management
- Migrate only common branches (branches with identical names in both repos)
- For branches like develop, main, or feature/user-auth that exist in both repos:
- Merge them into a single branch containing both backend/ and frontend/ directories
- The merged branch represents the complete state of that feature across both applications
4. Independence
- The backend and frontend are completely independent (no shared dependencies)
- No need for monorepo tooling like Turborepo or Nx
- CI/CD pipelines would be handled separately post-migration
The Strategy
After researching various approaches, I settled on using git-filter-repo (the modern replacement for git filter-branch) combined with strategic Git operations. Here’s the high-level strategy:
Phase 1: History Rewriting
- Create working copies of both repositories
- Use git-filter-repo to rewrite history:
- Move all files into subdirectories (backend/ or frontend/)
- Add prefixes to all commit messages - This operation rewrites the entire Git history, touching every commit
Phase 2: Branch Analysis
- Identify which branches exist in both repositories
- Create lists of common branches vs. unique branches
- Focus only on common branches for the monorepo
Phase 3: Monorepo Assembly
1. Initialize a fresh Git repository
2. Add both rewritten repos as remotes
3. Fetch all branches from both remotes
4. Merge corresponding branches using — allow-unrelated-histories
Phase 4: Deployment
- Push all merged branches to the new remote repository
- Verify integrity and completeness
Technical Implementation
Let me walk you through the actual implementation, step by step.
Prerequisites
First, ensure you have git-filter-repo installed:
# macOS
brew install git-filter-repo
# Ubuntu/Debian
apt-get install git-filter-repo
# Or via pip
pip3 install git-filter-repoStep 1: Create Working Copies
# Create a workspace for the migration
mkdir -p /tmp/monorepo-migration
cd /tmp/monorepo-migration
# Clone both repositories (or copy existing local clones)
cp -R /path/to/backend backend-temp
cp -R /path/to/frontend frontend-tempStep 2: Create Local Branches
Before rewriting history, ensure all remote branches are available locally:
#!/bin/bash
# create-local-branches.sh
REPO_DIR=$1
cd "$REPO_DIR" || exit 1
echo "Creating local branches in $REPO_DIR"
git branch -r | grep -v '\->' | while read -r remote; do
branch_name=$(echo "$remote" | sed 's/origin\///')
if git show-ref - verify - quiet "refs/heads/$branch_name"; then
echo " Branch $branch_name already exists locally, skipping"
else
echo " Creating local branch: $branch_name"
git branch "$branch_name" "$remote" 2>/dev/null
fi
done
echo "Done creating local branches"
Run this for both repositories:
chmod +x create-local-branches.sh
./create-local-branches.sh backend-temp
./create-local-branches.sh frontend-tempStep 3: Rewrite History with git-filter-repo
This is where the magic happens. We’ll rewrite the entire history of both repositories:
# Rewrite backend history
cd backend-temp
git-filter-repo \
- to-subdirectory-filter backend \
- message-callback 'return b"[backend] " + message' \
- force
# Rewrite frontend history
cd ../frontend-temp
git-filter-repo \
- to-subdirectory-filter frontend \
- message-callback 'return b"[frontend] " + message' \
- forceWhat this does:
- to-subdirectory-filter: Moves all files into the specified subdirectory across all commits
- message-callback: Adds a prefix to every commit message
- force: Required since we’re rewriting history
⚠️ Warning: This operation is destructive and rewrites Git history. Always work on copies!
Step 4: Initialize the Monorepo
# Create the monorepo directory
mkdir monorepo
cd monorepo
# Initialize as a Git repository
git init
# Add both rewritten repos as remotes
git remote add backend /tmp/monorepo-migration/backend-temp
git remote add frontend /tmp/monorepo-migration/frontend-temp
# Fetch all branches from both remotes
git fetch backend
git fetch frontendStep 5: Analyze Common Branches
Create a script to identify which branches exist in both repositories:
#!/bin/bash
# analyze-branches.sh
cd monorepo
# Extract branch names (without remote prefixes)
git branch -r | grep 'backend/' | sed 's|.*backend/||' | sort > /tmp/backend-branches.txt
git branch -r | grep 'frontend/' | sed 's|.*frontend/||' | sort > /tmp/frontend-branches.txt
# Find common branches
comm -12 /tmp/backend-branches.txt /tmp/frontend-branches.txt > /tmp/common-branches.txt
echo "Common branches: $(wc -l < /tmp/common-branches.txt)"
echo "Backend-only: $(wc -l < /tmp/backend-only-branches.txt)"
echo "Frontend-only: $(wc -l < /tmp/frontend-only-branches.txt)"In my case, this identified 213 common branches out of ~500 total branches across both repositories.
Step 6: Merge Common Branches
Now we merge each common branch:
#!/bin/bash
# merge-branches.sh
set -e # Exit on error
cd monorepo
TOTAL=$(wc -l < /tmp/common-branches.txt)
PROCESSED=0
echo "Merging $TOTAL common branches…"
while IFS= read -r branch; do
PROCESSED=$((PROCESSED + 1))
echo "[$PROCESSED/$TOTAL] Merging branch: $branch"
# Create branch from backend
git checkout -b "$branch" "backend/$branch" 2>/dev/null || git checkout "$branch"
# Merge frontend branch
git merge "frontend/$branch" \
-m "Merge frontend and backend for branch: $branch" \
- allow-unrelated-histories \
- no-edit
done < /tmp/common-branches.txt
echo "Branch migration complete!"Key points:
- allow-unrelated-histories: Essential because backend and frontend have completely separate Git histories
-m: Provides a clear merge commit message
- no-edit: Prevents interactive merge message editing (useful for automation)
Step 7: Push to Remote
cd monorepo
# Add the new monorepo remote
git remote add origin git@github.com:yourorg/monorepo.git
# Push all branches
git push origin - all - forceNote: Use — force carefully. This is safe for a fresh repository but dangerous for existing repos!
Verification
Get Braga J’s stories in your inbox
Join Medium for free to get updates from this writer.
After the migration, verify everything worked correctly:
# Count local branches
git branch | wc -l
# Verify on remote
git ls-remote - heads origin | wc -l
# Check a sample branch structure
git checkout develop
ls -la
# Should show: backend/ and frontend/ directories
# Verify commit history has prefixes
git log - oneline | head -20
# Should show [backend] and [frontend] prefixes
# Check chronological ordering
git log - all - pretty=format:"%h %ai %s" | head -20Results
The migration was successful! Here’s what we achieved:
Statistics
- 213 common branches successfully merged
- ~6,700 backend commits + ~4,600 frontend commits = ~11,300 total commits
- Complete history preserved dating back to the initial commits
- Chronological interleaving working perfectly
Sample Commit History
4be30009d Merge frontend and backend for branch: develop
027ae1623 [backend] Fix authentication token expiration
994c5154e [frontend] Update dashboard component styling
11d685978 [frontend] Set version to 1.2.1
1c6db03b4 [backend] Add rate limiting to API endpoints
98f04ef44 [frontend] Implement user profile page
Branch Structure
Every branch now contains both backend/ and frontend/ directories:
develop branch
├── backend/
│ ├── app/
│ ├── config/
│ ├── db/
│ └── …
└── frontend/
├── src/
├── public/
├── package.json
└── …
Lessons Learned
1. Always Fetch Latest Before Starting
I initially ran the migration but discovered the frontend had new commits. Had to redo the entire process with fresh data. Lesson: Always
run git fetch — all on both repos immediately before starting.
2. git-filter-repo is Fast
Rewriting ~11,000 commits across hundreds of branches took only a few minutes. Much faster than the deprecated git filter-branch.
3. Work on Copies
This cannot be stressed enough. Git history rewriting is destructive. I always worked on copies in /tmp and never touched the original
repositories.
4. Plan for Large Files
GitHub warned about some large files (>50MB) in our backend. Consider using Git LFS for large binary files before migration.
5. Test the Merge Strategy First
Before processing all 213 branches, I tested the merge strategy on a few branches to ensure the approach worked correctly.
6. Branch Naming Matters
We only migrated common branches (same name in both repos). If your repos use different naming conventions, you’ll need to create a mapping
strategy.
Alternative Approaches Considered
1. Git Subtree
git subtree add — prefix=backend git@github.com:org/backend.git main
Pros: Simpler, keeps history
Cons: Doesn’t handle multiple branches well, history is not as clean
2. Manual Copy-Paste
Simply copying files into subdirectories.
Pros: Simple
Cons: Loses all Git history (unacceptable for our requirements)
3. Monorepo Tools (Lerna, Nx)
Tools designed for monorepos from the start.
Pros: Great for new projects
Cons: Doesn’t help with migrating existing repositories
Common Pitfalls and Solutions
Pitfall 1: Remote Branches Not Available Locally
Problem: git-filter-repo only processes local branches.
Solution: Create local tracking branches for all remotes (see Step 2).
Pitfall 2: Merge Conflicts
Problem: Some branches had conflicting files in root directory.
Solution: Because we moved everything to subdirectories first, conflicts were impossible!
Pitfall 3: Lost Remote After git-filter-repo
Problem: git-filter-repo removes the origin remote by design.
Solution: This is intentional (to prevent accidental pushes). Re-add remotes as needed.
Pitfall 4: Incorrect Commit Ordering
Problem: Commits not in chronological order.
Solution: Git naturally orders by commit date. The merge strategy preserves this.
Performance Considerations
The migration script processed:
- 213 branches
- ~11,300 commits
- ~2.5 GB of repository data
Total time: Approximately 20 minutes
Breakdown:
— History rewriting: ~5 minutes
— Branch analysis: ~30 seconds
— Branch merging: ~12 minutes
— Pushing to GitHub: ~3 minutes
Post-Migration Checklist
After completing the migration:
- Verify branch count matches expectations
- Check several branches for correct directory structure
- Confirm commit history shows prefixes
- Test that chronological ordering is preserved
- Update CI/CD pipelines for new structure
- Update documentation and README files
- Notify team members of the new repository structure
- Archive old repositories (don’t delete immediately)
- Update local clones for all developers
- Set up branch protection rules on new repository
Conclusion
Migrating two mature repositories with hundreds of branches into a monorepo is a complex task, but with the right tools and strategy, it’s
entirely achievable. The key is careful planning, working on copies, and using modern Git tools like git-filter-repo.
The resulting monorepo provides several benefits:
— Unified development workflow across frontend and backend
— Atomic commits spanning both applications
— Simplified dependency management (when needed)
— Easier code review for full-stack features
— Complete historical context preserved
If you’re considering a similar migration, I hope this guide helps you avoid the pitfalls I encountered and successfully merge your
repositories!
Resources
Francium Tech is a technology company laser focused on delivering top quality software of scale at extreme speeds. Numbers and Size of the data don’t scare us. If you have any requirements or want a free health check of your systems or architecture, feel free to shoot an email to contact@francium.tech, we will get in touch with you!









