Move one folder or a set of folders from one Git repository to another with `git subtree` while preserving commit history, authorship, and timestamps. Use when asked to migrate directories across repositories, split monorepos, import a subproject, keep syncing with `git subtree split/add/merge/pull`, or retire the source path after a successful import.
Resources
1Install
npx skillscat add squirrel289/pax/migrating-git-subtree Install via the SkillsCat registry.
Git Subtree Migration
When to Use
Use this skill when you need history-preserving folder migration between repositories, including one-time imports and ongoing subtree sync.
Allowed Tools
terminalgit
Use no other tools unless the user explicitly asks.
Clarification Gate
Before running commands, collect:
- Source repository path/URL and source branch.
- Target repository path/URL and target branch.
- Mapping list: source prefix -> target prefix.
- Migration mode: one-time import or ongoing sync.
- Scope plan: one folder per commit and preferred PR grouping.
- Source-retirement policy: keep source prefix, remove immediately after verify, or remove in follow-up PR.
If any item is missing or ambiguous, stop and ask.
Preflight Checks
Run before migration:
git -C <source-repo> rev-parse --is-inside-work-tree
git -C <target-repo> rev-parse --is-inside-work-tree
git -C <source-repo> status --short
git -C <target-repo> status --shortRequire clean working trees unless the user explicitly approves working with local changes.
Rules:
- Preserve history: never use
--squash. - Create a target backup branch before import.
- Use a unique split branch name (
subtree/<id>-$(date +%Y%m%d%H%M%S)) to avoid collisions. git subtree mergetakes a single commit/ref argument. Do not pass<remote> <branch>tosubtree merge.
Decision Gate: Which Import Path?
Determine destination state before import:
DEST_EXISTS=no
[ -d "$TARGET_REPO/$DEST_PREFIX" ] && DEST_EXISTS=yes
HAS_SUBTREE_HISTORY=no
git -C "$TARGET_REPO" log --grep="git-subtree-dir: $DEST_PREFIX" --format=%H -n 1 >/dev/null && HAS_SUBTREE_HISTORY=yes
echo "DEST_EXISTS=$DEST_EXISTS HAS_SUBTREE_HISTORY=$HAS_SUBTREE_HISTORY"Pick workflow:
DEST_EXISTS=no: usesubtree add.DEST_EXISTS=yesandHAS_SUBTREE_HISTORY=yes: usesubtree mergeorsubtree pull.DEST_EXISTS=yesandHAS_SUBTREE_HISTORY=no: use subtree strategy merge (git merge -s subtree -Xsubtree=<prefix> --allow-unrelated-histories <split-ref>).
Plan Output (before acting)
Publish this plan before running migration commands:
Migration Goal: <what is moving>
Source: <repo + branch>
Target: <repo + branch>
Mappings: <src-prefix -> dst-prefix list>
Commit Scope: <one folder per commit>
PR Scope: <single mapping per PR or approved grouped mappings>
Verification: <exact git log/blame checks to run>
Rollback: <revert commit strategy>Use concrete paths and branch names; do not leave placeholders.
Workflow A: First Import to New Destination Prefix
Example variables:
SOURCE_REPO=/path/to/source
TARGET_REPO=/path/to/target
SOURCE_BRANCH=main
TARGET_BRANCH=main
SOURCE_PREFIX=packages/foo
DEST_PREFIX=libs/foo
MIGRATION_ID=foo-$(date +%Y%m%d%H%M%S)
SPLIT_BRANCH=subtree/${MIGRATION_ID}- Create split branch from source prefix:
git -C "$SOURCE_REPO" checkout "$SOURCE_BRANCH"
if git -C "$SOURCE_REPO" rev-parse --abbrev-ref --symbolic-full-name "${SOURCE_BRANCH}@{upstream}" >/dev/null 2>&1; then
git -C "$SOURCE_REPO" pull --ff-only
fi
git -C "$SOURCE_REPO" subtree split --prefix "$SOURCE_PREFIX" --branch "$SPLIT_BRANCH"- Prepare target and fetch split history:
git -C "$TARGET_REPO" checkout "$TARGET_BRANCH"
if git -C "$TARGET_REPO" rev-parse --abbrev-ref --symbolic-full-name "${TARGET_BRANCH}@{upstream}" >/dev/null 2>&1; then
git -C "$TARGET_REPO" pull --ff-only
fi
git -C "$TARGET_REPO" checkout -b "migrate/${MIGRATION_ID}"
git -C "$TARGET_REPO" branch "backup/pre-subtree-${MIGRATION_ID}"
git -C "$TARGET_REPO" remote add source-tmp "$SOURCE_REPO" 2>/dev/null || true
git -C "$TARGET_REPO" fetch source-tmp "$SPLIT_BRANCH"- Import into target with full history:
git -C "$TARGET_REPO" subtree add \
--prefix "$DEST_PREFIX" \
source-tmp "$SPLIT_BRANCH" \
-m "chore(subtree): import $SOURCE_PREFIX from source repo"Workflow B: Update Existing Subtree-Managed Prefix
SPLIT_REF="source-tmp/$SPLIT_BRANCH"
git -C "$TARGET_REPO" subtree merge \
--prefix "$DEST_PREFIX" \
"$SPLIT_REF" \
-m "chore(subtree): merge updates for $DEST_PREFIX"For direct split+sync in one command after initial add, subtree pull is also valid.
Workflow C: Adopt Existing Destination Prefix (Not Yet Subtree-Managed)
Use when destination path exists but has no subtree metadata in target history.
SPLIT_REF="source-tmp/$SPLIT_BRANCH"
git -C "$TARGET_REPO" merge \
--allow-unrelated-histories \
-s subtree \
-Xsubtree="$DEST_PREFIX" \
"$SPLIT_REF" \
-m "chore(subtree): merge $SOURCE_PREFIX into existing $DEST_PREFIX"Workflow D: Multiple Folder Imports
Preferred approach: process each mapping independently with one commit per mapping.
SOURCE_REPO=/path/to/source
TARGET_REPO=/path/to/target
MIGRATION_ID=batch-$(date +%Y%m%d%H%M%S)
git -C "$TARGET_REPO" remote add source-tmp "$SOURCE_REPO" 2>/dev/null || true
import_mapping() {
mapping="$1"
src="${mapping%%:*}"
dst="${mapping##*:}"
key="$(echo "$src" | tr '/ ' '--')"
split_branch="subtree/${MIGRATION_ID}/${key}"
git -C "$SOURCE_REPO" subtree split --prefix "$src" --branch "$split_branch"
git -C "$TARGET_REPO" fetch source-tmp "$split_branch"
# Select add/merge/strategy-merge based on destination state for this mapping.
}
for mapping in "packages/a:libs/a" "packages/b:libs/b"; do
import_mapping "$mapping"
doneVerification (Required)
Run after each mapping:
git -C "$TARGET_REPO" log --oneline -- "$DEST_PREFIX" | head -n 20
git -C "$TARGET_REPO" log --format='%h %an %ad %s' -- "$DEST_PREFIX" | tail -n 10
git -C "$TARGET_REPO" blame "$DEST_PREFIX/<known-long-lived-file>" | head -n 10Success signals:
- Imported path exists under target prefix.
- Log for the imported path shows historical commits (not just one new commit).
- Blame on a known long-lived file shows historical authors from source history.
Note: If blame is run on a brand-new file, all lines may point to the merge commit; that is not enough to prove history preservation.
Optional: Retire Source Prefix After Verified Import
Run only if the user requested source removal.
git -C "$SOURCE_REPO" checkout "$SOURCE_BRANCH"
git -C "$SOURCE_REPO" rm -r "$SOURCE_PREFIX"
git -C "$SOURCE_REPO" commit -m "chore(subtree): remove migrated $SOURCE_PREFIX after verified import"Post-removal verification:
if [ -d "$SOURCE_REPO/$SOURCE_PREFIX" ]; then
echo "Residual untracked/ignored files remain under source prefix"
find "$SOURCE_REPO/$SOURCE_PREFIX" -type f | head -n 20
fiIf residual files exist (for example ignored artifacts), remove them only with explicit user approval.
Ongoing Sync (Optional)
To sync later changes from source to target:
git -C "$SOURCE_REPO" subtree split --prefix "$SOURCE_PREFIX" --branch "$SPLIT_BRANCH"
git -C "$TARGET_REPO" fetch source-tmp "$SPLIT_BRANCH"
git -C "$TARGET_REPO" subtree pull \
--prefix "$DEST_PREFIX" \
source-tmp "$SPLIT_BRANCH" \
-m "chore(subtree): sync $DEST_PREFIX from source"Rollback
If migration commit is incorrect, prefer safe revert:
git -C "$TARGET_REPO" log --oneline -n 10
git -C "$TARGET_REPO" revert <subtree-commit-sha>Avoid destructive history rewrites on shared branches unless the user explicitly requests it.
Cleanup
After merge or confirmed success:
git -C "$TARGET_REPO" remote remove source-tmp || true
git -C "$SOURCE_REPO" branch -D "$SPLIT_BRANCH" || trueTroubleshooting
fatal: prefix '<path>' does not exist: source prefix is wrong or not present on selected source branch.fatal: prefix '<path>' already exists: destination exists; use decision gate to select merge strategy.fatal: 'source-tmp' does not refer to a commit:subtree mergewas called with remote name instead of split ref/commit.fatal: refusing to merge unrelated histories: use Workflow C when adopting a pre-existing destination path.- Missing history after import: check for accidental
--squashusage and rerun from a clean migration branch.