How it works

How to move files with their commit history between git repos.

Monopoly takes the commit history of the files you're moving and asks git to graft it into the target repo's history, alongside the files themselves.

SOURCE REPO . ├── apps/ ├── packages/ │ └── auth/ ← ├── tools/ └── README.md 1 clone + filter-repo TEMP CLONE (filtered) . ├── login.ts ├── session.ts └── README.md history: only commits touching packages/auth 2 merge (unrelated) TARGET REPO . ├── existing/ ├── README.md └── auth/ + NEW ├── login.ts ├── session.ts └── README.md
Bird's eye view of the process; all "dirty" work is done in a temporary repo.

1. Clone the source into a temp dir

Create a temporary clone of the source repo so the rewriting that follows can't affect your real repo or its history.

git clone --single-branch <source> <tmp>/extract

2. Filter history to just the path you care about

git filter-repo rewrites the commit graph of the temp clone, keeping only commits that touched the path you're moving.

# for a directory:
git filter-repo --subdirectory-filter packages/auth --force

# for a single file:
git filter-repo --path src/logger.ts --force
Source history — every commit on the branch touched packages/auth git filter-repo --subdirectory-filter packages/auth Filtered history — path-touching commits, in order
The four commits that touched packages/auth survive, with author, date, and message preserved.

3. Restructure paths to match --as

If you passed --as, monopoly uses git mv to move the files to that path and records a chore commit.

AFTER FILTER . ├── login.ts ├── session.ts └── README.md files at repo root (prefix stripped) git mv + chore commit AFTER RESTRUCTURE (--as auth) . └── auth/ ├── login.ts ├── session.ts └── README.md ready for merge
Captured as a chore(monopoly): restructure for target path commit.

4. Merge into the target repo

Source and target repos share no commits, so the merge needs --allow-unrelated-histories.

cd <target>
git remote add monopoly-temp <tmp>/extract    # local path, no network
git fetch monopoly-temp                       # pull the filtered history in
git merge monopoly-temp/<branch> \
  --allow-unrelated-histories \               # override: no common ancestor
  --no-commit                                 # stage the merge, don't commit
git remote remove monopoly-temp               # cleanup

The filtered commits attach as a parallel ancestry meeting at the merge commit, so git log --follow and git blame walk straight back to the originals.

5. Hand the merge off to you

The merge stops at --no-commit. monopoly writes a descriptive message into .git/MERGE_MSG (source repo, source path, source HEAD, commit count) and steps out. You run git commit when you're happy.

This gives you time to evaluate changes and do follow-up work in target (fixing build, adding docs, etc.)

About --allow-unrelated-histories

What it means

A merge needs a single commit that the two branches last shared. In monopoly's case we have two repos that were never connected, so there isn't one. The flag tells git "merge them anyway." The result has two starting commits in its history.

The risk

If you move a file to a path that already exists in the target repo, the two versions share no past. Git can't pick a winner, so the merge produces a conflict you need to resolve manually.

Safety nets

What you still have to mind

About squash-merges

What squash does

A squash-merge takes the diff a merge would introduce and writes it as one new commit on top of the target branch. The merge commit and its imported parents are not part of the new commit's ancestry.

What this does to monopoly's work

Monopoly's whole value sits in the parallel ancestry the merge commit creates — that's the chain git log --follow and git blame walk back through to find the originals. Squash flattens that ancestry. After the squash:

The recommendation

Don't squash a monopoly PR. Use a regular merge commit. If your repo defaults to squash-merge, switch to "Create a merge commit" on that one PR (GitHub's merge button has a dropdown).

If a squash already happened

The original commits still live in the source repo — you can git log there for archaeology. Blame in the target repo will lie, but the past isn't actually destroyed, just hidden.