How to Use git subtree split to Extract a Subdirectory into an Independent Repo with Full Commit History

Git tutorial - IT technology blog
Git tutorial - IT technology blog

At 2:00 AM, the team’s CI/CD pipeline is flashing bright red. A legacy payment module buried deep inside a 15GB monolith monorepo has dragged in massive redundant dependencies, causing build times to spike from 4 minutes to over 35 minutes. The fastest way to rescue production: immediately extract packages/payment-core into an independent repository for separate hotfixing and releases. The biggest challenge? Not losing a single line of git blame or the past 3 years of commit history.

3 Ways to Split a Subdirectory in Git and Their Trade-offs

Faced with this challenge, developers usually consider three common approaches:

  • Approach 1: Manual copy-paste to a new repo
    Create a blank repo, drag-and-drop the packages/payment-core directory over, and run git init && git commit -m "Initial commit". Fast, but completely erases all history.
  • Approach 2: Using git filter-branch or git-filter-repo
    Rewrites the entire commit tree. These tools scan every past commit to strip out all files outside the target directory.
  • Approach 3: Using git subtree split
    A built-in Git command available since version 1.7.0. This mechanism extracts only commits related to the specified directory into an entirely isolated synthetic branch.

Real-World Comparison: Which Solution to Pick During a Production Incident?

Each option has clear pros and cons:

1. Manual Copy-Paste

  • Pros: Done in 30 seconds. Zero risk of running the wrong Git commands.
  • Cons: Loses all commit logs, authors, and timestamps. When an incorrect billing bug occurs at midnight, you have no way to run git blame to identify who modified the business logic 6 months ago.

2. Using git filter-branch or git-filter-repo

  • Pros: Thoroughly reduces repository size. Generates clean new commit hashes.
  • Cons: Extremely risky. git filter-branch alters commit hashes directly. An accidental push --force to the wrong remote branch could wreck the team’s entire main branch. Furthermore, git-filter-repo is an external tool requiring a Python package installation, which might not be readily available on every server.

3. Using git subtree split

  • Pros: Completely safe. The command only reads history to generate a secondary branch without overwriting or mutating your current working branch. Built natively into Git core with no additional dependencies.
  • Cons: Execution speed depends on the commit volume of the original repo. For repos with over 80,000 commits, scanning can take 2 to 5 minutes.

Why is git subtree split the Safest Choice?

During an urgent incident, the two most critical factors are data safety and traceability. git subtree split delivers on both: creating a new repository with complete commit history while leaving the source repo 100% intact.

In practice, when applying this workflow across a 10-person team, we successfully extracted 4 shared modules into independent micro-repos without running into any history conflicts.

Step-by-Step Implementation Guide

Assume we are in the root directory of the monorepo-core repository and need to extract packages/payment-core into a new repository called payment-service-repo.

Step 1: Prepare a Clean Working Tree

Make sure you are on the latest branch without any uncommitted changes:

# Check status
git status

# Pull latest code from main
git checkout main
git pull origin main

Step 2: Extract History into a Dedicated Branch

Run the git subtree split command with the -P flag (prefix: directory path) and -b flag (new branch name):

git subtree split -P packages/payment-core -b extract-payment-branch

Git will scan the entire commit tree. Any commits affecting files within packages/payment-core will be extracted into the extract-payment-branch branch. Additionally, all files inside this directory will be promoted to the root of the new branch.

Step 3: Push Code to the New Repository

Create an empty repository on GitHub/GitLab (for example: [email protected]:your-org/payment-service-repo.git). You have two ways to push:

Method 1: Push directly from the current repo (Fastest)

git push [email protected]:your-org/payment-service-repo.git extract-payment-branch:main

This command pushes the temporary branch directly as the main branch of the new repo.

Method 2: Clone to a separate directory for local verification first

# Create a new directory outside the monorepo
cd ..
mkdir payment-service-repo
cd payment-service-repo

# Initialize repo and pull the split branch
git init
git pull /path/to/monorepo-core extract-payment-branch

# Add new remote and push
git remote add origin [email protected]:your-org/payment-service-repo.git
git branch -M main
git push -u origin main

Step 4: Verify History and Git Blame

Open the new repo and check the log to verify that past commits and authors are preserved intact:

# View the 10 most recent commits
git log --oneline -n 10

# Inspect blame on an important file
git blame src/gateway.js

Result: the entire commit log, timestamps, and author emails for the payment module are fully preserved. All extraneous monolith files have been completely stripped out.

Step 5: Clean Up the Temporary Branch in the Source Repo

Once the new repository is running smoothly, return to the source repo and delete the temporary branch to keep the workspace clean:

cd /path/to/monorepo-core
git branch -D extract-payment-branch

Now, you can delete the packages/payment-core directory from the old repo or transition to managing it as a standalone package via npm/composer without worrying about losing development history.

Share: