| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | git 2.52.0git-filter-repo 2.47.0 |
| Applies to | Linux, macOS, and Windows with Git, Python 3, and git-filter-repo; GitHub, GitLab, Bitbucket, or self-hosted remotes |
| Privilege | Read or clone access to the source repository; push access to the target repository; push access to the source only if you remove the moved paths afterward (optional Step 9) |
| Scope | Move selected files or directories from a source repo into an existing target repo while keeping relevant commit history; does not cover whole-repository mirror migration or copying files without history |
| Related guides | Git tutorial git delete file or directory |
If you need to move one or more files or directories from an existing Git repository into another repository without losing their relevant commit history, use git filter-repo to extract that history from a disposable source clone, then merge the filtered repository into the target.
This tutorial covers that case: selected paths into an existing target repository. You need read or clone access to the source and push access to the target. The filter step rewrites only your local migration clone; it does not modify the source remote. Replace SOURCE_REPO, TARGET_REPO, and path/to/directory/ with your own remote URLs and paths. The examples use main; replace it with the branch you want to migrate when your repository uses a different branch.
Quick answer: choose the correct Git migration method
| Goal | Approach |
|---|---|
| Selected files or folder into an existing repository | git filter-repo + merge |
| Directory into a new repository | git filter-repo + push |
| Entire repository to a new remote | bare clone + mirror push |
| Files only, history not required | copy + commit |
The rest of this article walks through the main workflow. If you do not need the original commit history, simply copy the files into the target repository and create a normal commit.
Move files to another Git repository and preserve history
git filter-repo rewrites the repository so the selected paths and the history relevant to them remain. In commits that originally changed both selected and unrelated files, only the selected paths remain in the rewritten commit. Commits that become empty may be removed. Retained commits receive new commit IDs because the history was rewritten. git-filter-repo removes commit and tag signatures during the rewrite because changing a commit or tag changes its object ID, so the original signature would no longer validate.
This workflow requires git-filter-repo. Check first:
git filter-repo --versionIf the command is missing, install it. On RHEL and Fedora, package installs use dnf command with sudo:
sudo dnf install git-filter-repoOn Debian and Ubuntu, update indexes and install with apt command:
sudo apt update && sudo apt install git-filter-repoOn macOS with Homebrew:
brew install git-filter-repoIf the packaged version is too old, use pipx install git-filter-repo instead.
git filter-branch for this workflow. Git recommends git-filter-repo for new history-rewriting work.
1. Clone the source repository
Clone SOURCE_REPO into a disposable working directory:
git clone SOURCE_REPO source-repo-migrationChange into the clone:
cd source-repo-migrationgit filter-repo has a safety check that normally expects you to run destructive filtering on a fresh clone. Use a disposable clone instead of your everyday working copy, and do not pass --force in the normal workflow. For a local filesystem source, clone with --no-local. Do not use a shallow clone (--depth) when you need complete history.
2. Check the history you want to preserve
List the commits that touched the path you plan to move:
git log --oneline -- path/to/directory/Note one or two commit subjects so you can confirm they remain after filtering.
git-filter-repo does not automatically follow renames during path filtering — the same limitation applies when you rename a file or directory inside one repo and later need every old path in the filter. Run git filter-repo --analyze and review .git/filter-repo/analysis/. The reports can help identify paths and renames that existed earlier in the repository's history. Include any relevant historical path names in the filter. See Path renamed in old commits if that applies.
3. Filter the files or directory
Keep a directory and its history at the same path:
git filter-repo --path path/to/directory/git filter-repo removes the origin remote as a safety measure and rewrites the clone so only the selected paths remain. This is expected. Removing origin helps prevent you from accidentally pushing the rewritten history back to the original repository. The source remote on your hosting site is not changed.
Use the command that matches what you want to move:
| Goal | Command |
|---|---|
| Single file | git filter-repo --path path/to/file |
| Multiple paths | git filter-repo --path dir1/ --path shared/config.yaml |
| Directory contents at repository root | git filter-repo --subdirectory-filter path/to/directory |
| Different destination path in target | git filter-repo --path dir1/ --path-rename dir1/:components/dir1/ |
Each --path adds another path to the retained history. Changes to paths you did not select are removed. Commits that become empty as a result may also be removed. --subdirectory-filter lifts the directory contents to the repository root instead of keeping the directory prefix.
Path renamed in old commits
If legacy-app/ became path/to/directory/ in an older commit, include every historical name:
git filter-repo --path legacy-app/ --path path/to/directory/Adding both paths keeps history from both names. Older commits still refer to the historical path unless you rewrite that path too. Verify with:
git log --oneline --all -- legacy-app/ path/to/directory/For one file, git log --follow can help follow a detected rename inside the filtered tree.
4. Verify the filtered history
Confirm the filtered clone before you touch the target:
git ls-filesReview the rewritten history:
git log --oneline --allFor a path that was not renamed:
git log --oneline -- path/to/directory/For a path with historical names:
git log --oneline --all -- legacy-app/ path/to/directory/You should see only the selected files in git ls-files and new commit IDs because the history was rewritten. If the path was not renamed, the commit subjects you noted in Step 2 should still appear in git log -- path/to/directory/. If the path was renamed in the past, verify all historical path names together. git filter-repo retains the selected old and new paths but does not automatically rename the old path inside historical commits.
5. Import the filtered history into the target
Clone the existing target repository next to the filtered source clone, then create a branch for the migration with git switch:
cd ..
git clone TARGET_REPO target-repo
cd target-repo
git switch -c import-historyUsing a separate branch lets you review the migration before it reaches your target branch.
Register the filtered clone, fetch it, and list remote branches. See git remote add, git fetch, and git list branches for command options:
git remote add filtered-source ../source-repo-migration
git fetch filtered-source
git branch -rFind the filtered source branch you want to import. The examples below use filtered-source/main; replace main with the actual branch name when necessary.
Make sure the target working tree is clean before starting the merge:
git statusThese repositories were created independently, so Git sees them as two unrelated histories. A normal merge is rejected because they have no shared ancestor. --allow-unrelated-histories tells Git that joining them is intentional:
git merge --allow-unrelated-histories filtered-source/main -m "Import filtered history from source repository"6. Resolve conflicts if necessary
If both repositories contain different versions of the same path, Git may stop with a merge conflict. Run:
git statusEdit the conflicted files, choose the content you want to keep, stage each resolved file, and complete the merge:
git add path/to/resolved-fileRepeat git add for each resolved path, then finish the merge:
git commitFor a broader merge conflict walkthrough, see git merge examples.
7. Verify the imported history
Check that the moved paths are tracked:
git ls-files path/to/directory/Review the combined graph and path history:
git log --oneline --graph --allFor a path that was not renamed:
git log --oneline -- path/to/directory/For a path with historical names:
git log --oneline --all -- legacy-app/ path/to/directory/For a single file, follow renames inside the filtered tree:
git log --follow --oneline -- path/to/directory/fileThe graph should show that the target history and imported source history both lead into the migration merge commit. If the path was renamed in the past, verify all historical path names together rather than relying on git log -- path/to/directory/ alone.
8. Push the migration
Push the import branch to the remote:
git push -u origin import-historyOpen a pull or merge request and use a normal merge. Do not squash the migration branch if you need the imported commits to stay reachable as individual revisions — see git squash commits for when squash is appropriate on ordinary feature work, not history imports.
Remove the temporary remote when you are done; see git remove remote if you need to drop other remotes later:
git remote remove filtered-sourceDelete the source-repo-migration directory when you no longer need it.
9. Remove the content from the source repository (optional)
After verifying the target, remove the original paths from a normal checkout of the source repository with git rm. This step needs push access to the source remote:
git rm -r path/to/directory/Commit the removal:
git commit -m "Move directory to target repository"Push the removal. Replace main with the source branch you are cleaning up:
git push origin mainDo this only after verifying the migration. Skip it when you intend to copy or split the content instead.
Common migration cases
Move to a new empty repository
When the destination is empty rather than an existing repository, filter the source clone, add a new remote, and push:
git remote add new-origin NEW_REPO_URLPush the filtered main branch to the empty repository:
git push -u new-origin mainThat creates a standalone repository with the filtered history. It does not merge into existing target commits the way Step 5 does.
Move the entire repository instead
When you want to move the entire repository, including its branches and tags, path filtering is unnecessary:
git clone --bare OLD_REPO_URLEnter the bare repository directory:
cd old-repo.gitMirror every ref to the new remote:
git push --mirror NEW_REPO_URLgit push --mirror force-updates matching refs on the destination and can delete destination refs that do not exist in the source. Use it when the new remote should replace the old one, especially when the destination starts empty.
What happens to branches and tags?
git filter-repo normally rewrites the branches and tags available in the source clone. This tutorial imports only filtered-source/main into the target. Other filtered branches and tags remain separate refs and are not automatically published on the target by the merge shown here. If you need selected-path history for multiple branches or tags, plan those refs explicitly and check for naming conflicts.
Git LFS files
Git history stores Git LFS pointer files, not the large objects themselves. When moved paths use LFS, install Git LFS on the push host, confirm the source still has the objects you need, make sure the required LFS objects are transferred to the target remote as part of the migration, and verify the result with a fresh clone. See the Git LFS documentation and GitHub: duplicating a repository.
Common problems
| Symptom | Likely cause | Fix |
|---|---|---|
git filter-repo says the repository is not a fresh clone |
Safety check failed on a reused working copy | Make a new clone; avoid --force unless you understand the risk |
| Older commits disappeared after filtering | Historical path rename not included | Add older path names with extra --path values; run --analyze first |
fatal: refusing to merge unrelated histories |
Missing flag on unrelated roots | Run git merge --allow-unrelated-histories filtered-source/main |
| Target already has the same path | Content overlap | Resolve conflicts with git status, then git add and git commit |
| Commit IDs changed | Expected after git filter-repo |
Compare subjects and authors, not old object IDs |
git log path/ misses old commits |
Path rename or wrong path filter | Use git log --all -- legacy-app/ path/to/directory/ and include legacy paths when filtering |
| Push rejected by branch protection | Direct push to protected branch | Push an import branch and merge through the normal review flow without squash |
git-filter-repo vs subtree vs format-patch
| Method | Best fit |
|---|---|
git filter-repo |
Extract selected history before migration |
git subtree split |
Subtree-oriented workflows and splitting |
git format-patch + git am |
Replay selected patches or commits |
git clone --bare + push --mirror |
Entire repository migration |
This article uses git filter-repo for selected-path migration. To drop large blobs from one repository instead of moving paths elsewhere, see reduce Git repository size with git-filter-repo.
Final verification checklist
Before you delete data from the source repository, confirm:
- selected files exist in the target tree
- unrelated source files were not imported
- expected commit authors, messages, and dates are visible on the moved paths
git log -- path/shows the history you expect for an unchanged path, orgit log --all -- old-path/ new-path/when the path was renamedgit log --follow -- path/fileshows the file history you expect- the target project still builds or tests cleanly
- Git LFS objects are available on the target when applicable
- the source cleanup happens only after target verification
- the migration pull request was not squash-merged when you need per-commit ancestry
Summary
Moving selected paths between repositories starts with a fresh source clone and git filter-repo --path to keep only the history you need. That rewrite creates new commit IDs, so verify the filtered history before importing it into the target.
The target side fetches the filtered clone as a temporary remote, merges with --allow-unrelated-histories, and pushes a review branch such as import-history. Merge without squash when you need the imported commits to remain individually reachable. Remove the paths from the source only after the target is verified.
References
- git-filter-repo documentation
- git-filter-repo installation notes
- git-filter-repo FAQ
- Git merge documentation
- git push documentation
- GitHub: splitting a subfolder into a new repository
- GitHub: duplicating a repository
- Git LFS
Frequently Asked Questions
How do I move files from one Git repository to another and keep history?
Clone the source repository into a disposable directory, run git filter-repo with --path for the files or directories you need, clone the target repository, add the filtered clone as a remote, and merge with --allow-unrelated-histories. Push the result on a review branch and merge without squash if you need the imported commits to stay separate.
How do I move one directory to another Git repository?
Use git filter-repo --path path/to/directory/ on a fresh source clone, then merge filtered-source/main into the target branch. Use --subdirectory-filter instead when the directory contents should become the repository root in the filtered clone.
Does git-filter-repo preserve commit history?
It rewrites the repository so the selected paths and their relevant history remain, including authors, dates, and commit messages. Commit IDs change, and commits that contain no retained changes may disappear.
Why did older history disappear after git filter-repo?
The path probably changed in an earlier commit. Include every historical directory or file name with additional --path arguments, then filter again from a fresh clone. Run git filter-repo --analyze first on large repositories.
Should I use git filter-branch or git-filter-repo?
Use git-filter-repo for new work. Git's own documentation warns that filter-branch has safety and performance problems.
What is the difference between --path and --subdirectory-filter?
--path dir1/ keeps the dir1/ prefix in the filtered repository. --subdirectory-filter dir1 moves the contents of dir1/ to the repository root.
Can I move only one file and preserve its history?
Yes. Pass the file path to --path, for example git filter-repo --path src/config.yaml.
How do I move the entire Git repository with all branches and tags?
Use a bare clone and git push --mirror to the new remote when the destination should mirror every ref. That is a whole-repository migration, not the selected-path workflow in this article.
Can I move content into a repository that already has commits?
Yes. That is the primary scenario here. The target keeps its existing history, and the merge adds the filtered source history as a second root line.
Should I squash the migration pull request?
No, not if you need the imported commits to remain individually reachable. Squash merging collapses them into one commit on the target branch.

