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.
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 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.
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
-
When conflicts happen, monopoly auto-aborts and asks
you to pick a different
--as. -
Resetting the target repo is one command:
git merge --abort. - Monopoly always uses
--no-commit. You run the final commit step.
What you still have to mind
- Type the right
<source>and--to. - Target must be on the branch you want the files to land on. Whatever's checked out in the target is what gets the merge.
- Read the diff before
git commit. - Try
--dry-runfirst to validate paths.
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:
-
git log --followon the moved file shows one commit, by whoever ran the squash, dated "now". -
git blameattributes every line to that same squash commit. Original authorship is gone from the target repo. -
The filter-repo'd commits still exist as unreachable objects
until
git gcdrops them, but nothing in the visible history points at them anymore.
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.