Monorepo Migration Case Study: Moving 8 Repos Into One Without Losing History

Why They Migrated

The team had 8 microservices in separate repositories: auth-service, user-service, payment-service, notification-service, api-gateway, shared-lib, admin-portal, worker-service.

Problems:

The decision: migrate to a monorepo where all services live under /services/<name> and shared code lives under /packages/.

---

The Challenge: Preserving History

Simply copying files into a new repo loses all history. Blame, log, and bisect stop working. The engineering team insisted on preserving history — git log services/auth-service/src/ should show commits going back 3 years.

The tool for this: git filter-repo (preferred) or git subtree.

---

Step 1: Prepare Each Service Repo

For each service, rewrite its history so all files appear under services/<name>/ instead of the repo root:

# Clone auth-service fresh (filter-repo modifies in place)
git clone https://github.com/company/auth-service.git /tmp/auth-service-prep
cd /tmp/auth-service-prep

# Rewrite history to move all files into services/auth-service/
git filter-repo --to-subdirectory-filter services/auth-service

After this, the repo's history looks as if all commits always had files under services/auth-service/.

---

Step 2: Create the Monorepo

mkdir /tmp/monorepo && cd /tmp/monorepo
git init
git commit --allow-empty -m "init: create monorepo"

---

Step 3: Merge Each Service Repo

for service in auth-service user-service payment-service notification-service                api-gateway shared-lib admin-portal worker-service; do
  echo "==> Merging $service..."

  # Add the prepared repo as a remote
  git remote add "$service" "/tmp/${service}-prep"
  git fetch "$service" --tags

  # Merge its history into the monorepo (allow unrelated histories)
  git merge --allow-unrelated-histories -m "merge: import $service history"     "$service/main"

  # Remove the temporary remote
  git remote remove "$service"
done

After this loop, the monorepo has the complete history of all 8 services with each file correctly attributed.

---

Step 4: Add Shared Configuration

mkdir -p packages packages/shared-types packages/config-common

# Root-level CI, linting, etc.
cat > .github/workflows/ci.yml << 'EOF'
on: [push, pull_request]
jobs:
  changed-services:
    # detect which services changed, only run their tests
EOF

cat > package.json << 'EOF'
{
  "name": "company-monorepo",
  "workspaces": ["services/*", "packages/*"]
}
EOF

git add .
git commit -m "chore: add monorepo root configuration"

---

Step 5: Validate History

# Check that a file's history is intact
git log --oneline services/auth-service/src/handlers/login.js

# Check that blame works
git blame services/payment-service/src/processor.js | head -20

# Check tags from original repos were preserved
git tag | grep auth-service

---

Step 6: CI/CD Changes

The CI pipeline needed to detect which services changed and only run tests for those:

# Get changed services from the diff
CHANGED=$(git diff --name-only origin/main...HEAD |   grep "^services/" | cut -d/ -f2 | sort -u)

for service in $CHANGED; do
  echo "Running tests for $service"
  cd "services/$service" && npm test && cd ../..
done

---

The Cutover

The team used a 2-week transition period:

---

What Improved

After migration:

What got harder: CI runs are larger (though affected-service detection mitigates this), the repo is larger to clone (shallow clone + sparse checkout help).

Practice Git collaboration in the ShellGenius Git Labs — the git-branch-merge and git-staging challenges use real multi-branch scenarios.